Smart Contract
OpenEditionV2
A.f5b0eb433389ac3f.OpenEditionV2
1import FungibleToken from 0xf233dcee88fe0abe
2import FlowToken from 0x1654653399040a61
3import Collectible from 0xf5b0eb433389ac3f
4import NonFungibleToken from 0x1d7e57aa55817448
5import Edition from 0xf5b0eb433389ac3f
6import FUSD from 0x3c5959b568896393
7
8pub contract OpenEditionV2 {
9
10 pub let CollectionStoragePath: StoragePath
11 pub let CollectionPublicPath: PublicPath
12
13 pub struct OpenEditionStatus{
14 pub let id: UInt64
15 pub let price : UFix64
16 pub let active: Bool
17 pub let timeRemaining : Fix64
18 pub let endTime : Fix64
19 pub let startTime : Fix64
20 pub let metadata: Collectible.Metadata?
21 pub let completed: Bool
22 pub let expired: Bool
23 pub let cancelled: Bool
24
25 init(
26 id:UInt64,
27 price: UFix64,
28 active: Bool,
29 timeRemaining:Fix64,
30 metadata: Collectible.Metadata?,
31 startTime: Fix64,
32 endTime: Fix64,
33 completed: Bool,
34 expired:Bool,
35 cancelled: Bool
36 ) {
37 self.id = id
38 self.price = price
39 self.active = active
40 self.timeRemaining = timeRemaining
41 self.metadata = metadata
42 self.startTime = startTime
43 self.endTime = endTime
44 self.completed = completed
45 self.expired = expired
46 self.cancelled = cancelled
47 }
48 }
49
50 // The total amount of OpenEditions that have been created
51 pub var totalOpenEditions: UInt64
52
53 // Events
54 pub event OpenEditionCollectionCreated()
55 pub event Created(id: UInt64, price: UFix64, startTime: UFix64)
56 pub event Purchase(openEditionId: UInt64, buyer: Address, price: UFix64, NFTid: UInt64, edition: UInt64)
57 pub event Earned(nftID: UInt64, amount: UFix64, owner: Address, type: String)
58 pub event FailEarned(nftID: UInt64, amount: UFix64, owner: Address, type: String)
59 pub event Settled(id: UInt64, price: UFix64, amountMintedNFT: UInt64)
60 pub event Canceled(id: UInt64)
61
62 // OpenEditionItem contains the Resources and metadata for a single sale
63 pub resource OpenEditionItem {
64
65 // Number of purchased NFTs
66 priv var numberOfMintedNFT: UInt64
67
68 // The id of this individual open edition
69 pub let openEditionID: UInt64
70
71 // The current price
72 pub let price: UFix64
73
74 // The time the open edition should start at
75 priv var startTime: UFix64
76
77 // The length in seconds for this open edition
78 priv var saleLength: UFix64
79
80 // After settle open edition
81 priv var completed: Bool
82
83 // Set if an open edition will be cancelled
84 priv var cancelled: Bool
85
86 // Common number for all copies one item
87 priv let editionNumber: UInt64
88
89 // Metadata for minted NFT
90 priv let metadata: Collectible.Metadata
91
92 //The vault receive FUSD in case of the recipient of commissiona is unreachable
93 priv let platformVaultCap: Capability<&FUSD.Vault{FungibleToken.Receiver}>
94
95 init(
96 price: UFix64,
97 startTime: UFix64,
98 saleLength: UFix64,
99 editionNumber: UInt64,
100 metadata: Collectible.Metadata,
101 platformVaultCap: Capability<&FUSD.Vault{FungibleToken.Receiver}>
102 ) {
103 OpenEditionV2.totalOpenEditions = OpenEditionV2.totalOpenEditions + (1 as UInt64)
104 self.price = price
105 self.startTime = startTime
106 self.saleLength = saleLength
107 self.editionNumber = editionNumber
108 self.numberOfMintedNFT = 0
109 self.openEditionID = OpenEditionV2.totalOpenEditions
110 self.completed = false
111 self.cancelled = false
112 self.metadata = metadata
113 self.platformVaultCap = platformVaultCap
114 }
115
116 pub fun settleOpenEdition(clientEdition: &Edition.EditionCollection) {
117
118 pre {
119 !self.cancelled : "Open edition was cancelled"
120 !self.completed : "The open edition has already settled"
121 self.isExpired() : "Open edtion time has not expired yet"
122 }
123
124 self.completed = true
125
126 // Write final amount of copies for this NFT
127 clientEdition.changeMaxEdition(id: self.editionNumber, maxEdition: self.numberOfMintedNFT)
128
129 emit Settled(id: self.openEditionID, price: self.price, amountMintedNFT: self.numberOfMintedNFT)
130 }
131
132 //this can be negative if is expired
133 priv fun timeRemaining() : Fix64 {
134 let length = self.saleLength
135
136 let startTime = self.startTime
137
138 let currentTime = getCurrentBlock().timestamp
139
140 let remaining = Fix64(startTime + length) - Fix64(currentTime)
141
142 return remaining
143 }
144
145 pub fun getPrice(): UFix64 {
146 return self.price
147 }
148
149 priv fun isExpired(): Bool {
150 let timeRemaining = self.timeRemaining()
151 return timeRemaining < Fix64(0.0)
152 }
153
154 priv fun sendCommissionPayments(buyerTokens: @FUSD.Vault, tokenID: UInt64) {
155 // Capability to resource with commission information
156 let editionRef = OpenEditionV2.account.getCapability<&{Edition.EditionCollectionPublic}>(Edition.CollectionPublicPath).borrow()!
157
158 // Commission informaton for all copies of on item
159 let editionStatus = editionRef.getEdition(self.editionNumber)!
160
161 // Vault for platform account
162 let platformVault = self.platformVaultCap.borrow()!
163
164 for key in editionStatus.royalty.keys {
165 // Commission is paid all recepient except platform
166 if (editionStatus.royalty[key]!.firstSalePercent > 0.0 && key != platformVault.owner!.address) {
167 let commission = self.price * editionStatus.royalty[key]!.firstSalePercent * 0.01
168
169 let account = getAccount(key)
170
171 let vaultCap = account.getCapability<&FUSD.Vault{FungibleToken.Receiver}>(/public/fusdReceiver)
172
173 // vaultCap was checked during creation of commission info on Edition contract, therefore this is extra check
174 // if vault capability is not avaliable, the rest tokens will sent to platform vault
175 if (vaultCap.check()) {
176 let vault = vaultCap.borrow()!
177 vault.deposit(from: <- buyerTokens.withdraw(amount: commission))
178 emit Earned(nftID: tokenID, amount: commission, owner: key, type: editionStatus.royalty[key]!.description)
179 } else {
180 emit FailEarned(nftID: tokenID, amount: commission, owner: key, type: editionStatus.royalty[key]!.description)
181 }
182 }
183 }
184
185 // Platform get the rest of Fungible tokens and tokens from failed transactions
186 let amount = buyerTokens.balance
187
188 platformVault.deposit(from: <- (buyerTokens as! @FungibleToken.Vault))
189
190 emit Earned(nftID: tokenID, amount: amount, owner: platformVault.owner!.address, type: "PLATFORM")
191 }
192
193 pub fun purchase(
194 buyerTokens: @FUSD.Vault,
195 buyerCollectionCap: Capability<&{Collectible.CollectionPublic}>,
196 minterCap: Capability<&Collectible.NFTMinter>
197 ) {
198 pre {
199 self.startTime < getCurrentBlock().timestamp : "The open edition has not started yet"
200 !self.isExpired() : "The open edition time expired"
201 !self.cancelled : "Open edition was cancelled"
202 buyerTokens.balance == self.price: "Not exact amount tokens to buy the NFT"
203 }
204
205 // Get minter reference to create NFT
206 let minterRef = minterCap.borrow()!
207
208 // Change amount of copies in this edition
209 self.numberOfMintedNFT = self.numberOfMintedNFT + UInt64(1)
210
211 // Change copy number in NFT
212 let metadata = Collectible.Metadata(
213 link: self.metadata.link,
214 name: self.metadata.name,
215 author: self.metadata.author,
216 description: self.metadata.description,
217 // Copy number for this NFT in metadata
218 edition: self.numberOfMintedNFT,
219 properties: self.metadata.properties
220 )
221
222 // Mint NFT
223 let newNFT <- minterRef.mintNFT(metadata: metadata, editionNumber: self.editionNumber)
224
225 // NFT number
226 let NFTid = newNFT.id
227
228 // Get buyer's NFT Collection reference
229 let buyerNFTCollection = buyerCollectionCap.borrow()!
230
231 // Sent NFT to buyer
232 buyerNFTCollection.deposit(token: <- newNFT)
233
234 // Pay commission to recipients
235 self.sendCommissionPayments(
236 buyerTokens: <- buyerTokens,
237 tokenID: NFTid
238 )
239
240 // Purchase event
241 emit Purchase(openEditionId: self.openEditionID, buyer: buyerCollectionCap.borrow()!.owner!.address, price: self.price, NFTid: NFTid, edition: self.numberOfMintedNFT)
242 }
243
244 pub fun getOpenEditionStatus() : OpenEditionStatus {
245
246 return OpenEditionStatus(
247 id: self.openEditionID,
248 price: self.price,
249 active: !self.completed && !self.isExpired(),
250 timeRemaining: self.timeRemaining(),
251 metadata: self.metadata,
252 startTime: Fix64(self.startTime),
253 endTime: Fix64(self.startTime + self.saleLength),
254 completed: self.completed,
255 expired: self.isExpired(),
256 cancelled: self.cancelled
257 )
258 }
259
260 pub fun cancelOpenEdition(clientEdition: &Edition.EditionCollection) {
261 pre {
262 !self.completed : "The open edition has already settled"
263 !self.cancelled : "Open edition has been cancelled earlier"
264 }
265 // Write final amount of copies for this NFT
266 clientEdition.changeMaxEdition(id: self.editionNumber, maxEdition: self.numberOfMintedNFT)
267
268 self.cancelled = true
269 }
270
271 destroy() {
272 log("destroy open editions")
273 }
274 }
275
276 // OpenEditionPublic is a resource interface that restricts users to
277 // retreiving the auction price list and placing bids
278 pub resource interface OpenEditionCollectionPublic {
279
280 pub fun getOpenEditionStatuses(): {UInt64: OpenEditionStatus}?
281 pub fun getOpenEditionStatus(_ id : UInt64): OpenEditionStatus?
282 pub fun getPrice(_ id:UInt64): UFix64?
283
284 pub fun purchase(
285 id: UInt64,
286 buyerTokens: @FUSD.Vault,
287 collectionCap: Capability<&{Collectible.CollectionPublic}>
288 )
289 }
290
291 // OpenEditionCollection contains a dictionary of OpenEditionItems and provides
292 // methods for manipulating the OpenEditionItems
293 pub resource OpenEditionCollection: OpenEditionCollectionPublic {
294 // OpenEdition Items
295 access(account) var openEditionsItems: @{UInt64: OpenEditionItem}
296
297 access(contract) let minterCap: Capability<&Collectible.NFTMinter>
298
299 init(minterCap: Capability<&Collectible.NFTMinter>) {
300 self.openEditionsItems <- {}
301 self.minterCap = minterCap
302 }
303
304 pub fun keys() : [UInt64] {
305 return self.openEditionsItems.keys
306 }
307
308 // addTokenToauctionItems adds an NFT to the auction items and sets the meta data
309 // for the auction item
310 pub fun createOpenEdition(
311 price: UFix64,
312 startTime: UFix64,
313 saleLength: UFix64,
314 editionNumber: UInt64,
315 metadata: Collectible.Metadata,
316 platformVaultCap: Capability<&FUSD.Vault{FungibleToken.Receiver}>
317 ) {
318 pre {
319 saleLength > 0.00 : "Sale lenght should be more than 0.00"
320 startTime > getCurrentBlock().timestamp : "Start time can't be in the past"
321 price > 0.00 : "Price should be more than 0.00"
322 price <= 999999.99 : "Price should be less than 1 000 000.00"
323 platformVaultCap.check() : "Platform vault should be reachable"
324 }
325
326 let editionRef = OpenEditionV2.account.getCapability<&{Edition.EditionCollectionPublic}>(Edition.CollectionPublicPath).borrow()!
327
328 // Check edition info in contract Edition in order to manage commission and all amount of copies of the same item
329 // This error throws inside Edition contract. But I put this check for redundant
330 if editionRef.getEdition(editionNumber) == nil {
331 panic("Edition doesn't exist")
332 }
333
334 let item <- create OpenEditionItem(
335 price: price,
336 startTime: startTime,
337 saleLength: saleLength,
338 editionNumber: editionNumber,
339 metadata: metadata,
340 platformVaultCap: platformVaultCap
341 )
342
343 let id = item.openEditionID
344
345 // update the auction items dictionary with the new resources
346 let oldItem <- self.openEditionsItems[id] <- item
347
348 destroy oldItem
349
350 emit Created(id: id, price: price, startTime: startTime)
351 }
352
353 // getOpenEditionPrices returns a dictionary of available NFT IDs with their current price
354 pub fun getOpenEditionStatuses(): {UInt64: OpenEditionStatus}? {
355
356 if self.openEditionsItems.keys.length == 0 {
357 return nil
358 }
359
360 let priceList: {UInt64: OpenEditionStatus} = {}
361
362 for id in self.openEditionsItems.keys {
363 let itemRef = &self.openEditionsItems[id] as? &OpenEditionItem
364 priceList[id] = itemRef.getOpenEditionStatus()
365 }
366
367 return priceList
368 }
369
370 pub fun getOpenEditionStatus(_ id:UInt64): OpenEditionStatus? {
371 if self.openEditionsItems[id] == nil {
372 return nil
373 }
374
375 // Get the auction item resources
376 let itemRef = &self.openEditionsItems[id] as &OpenEditionItem
377 return itemRef.getOpenEditionStatus()
378 }
379
380 pub fun getPrice(_ id:UInt64): UFix64? {
381 if self.openEditionsItems[id] == nil {
382 return nil
383 }
384
385 // Get the open edition item resources
386 let itemRef = &self.openEditionsItems[id] as &OpenEditionItem
387 return itemRef.getPrice()
388 }
389
390 // settleOpenEdition sends the auction item to the highest bidder
391 // and deposits the FungibleTokens into the auction owner's account
392 pub fun settleOpenEdition(id: UInt64, clientEdition: &Edition.EditionCollection) {
393 pre {
394 self.openEditionsItems[id] != nil:
395 "Open Edition does not exist"
396 }
397
398 let itemRef = &self.openEditionsItems[id] as &OpenEditionItem
399 itemRef.settleOpenEdition(clientEdition: clientEdition)
400 }
401
402 pub fun cancelOpenEdition(id: UInt64, clientEdition: &Edition.EditionCollection) {
403 pre {
404 self.openEditionsItems[id] != nil:
405 "Open Edition does not exist"
406 }
407 let itemRef = &self.openEditionsItems[id] as &OpenEditionItem
408 itemRef.cancelOpenEdition(clientEdition: clientEdition)
409 emit Canceled(id: id)
410 }
411
412 // purchase sends the buyer's tokens to the buyer's tokens vault
413 pub fun purchase(
414 id: UInt64,
415 buyerTokens: @FUSD.Vault,
416 collectionCap: Capability<&{Collectible.CollectionPublic}>
417 ) {
418 pre {
419 self.openEditionsItems[id] != nil: "Open Edition does not exist"
420 collectionCap.check(): "NFT storage does not exist on the account"
421 }
422
423 // Get the auction item resources
424 let itemRef = &self.openEditionsItems[id] as &OpenEditionItem
425
426 itemRef.purchase(
427 buyerTokens: <- buyerTokens,
428 buyerCollectionCap: collectionCap,
429 minterCap: self.minterCap
430 )
431 }
432
433 destroy() {
434 log("destroy open edition collection")
435 // destroy the empty resources
436 destroy self.openEditionsItems
437 }
438 }
439
440 // createOpenEditionCollection returns a OpenEditionCollection resource
441 priv fun createOpenEditionCollection(minterCap: Capability<&Collectible.NFTMinter>): @OpenEditionCollection {
442 let openEditionCollection <- create OpenEditionCollection(minterCap: minterCap)
443
444 emit OpenEditionCollectionCreated()
445 return <- openEditionCollection
446 }
447
448 init() {
449 self.totalOpenEditions = (0 as UInt64)
450 self.CollectionPublicPath = /public/NFTbloctoXtinglesOpenEdition
451 self.CollectionStoragePath = /storage/NFTbloctoXtinglesOpenEdition
452
453 let minterCap = self.account.getCapability<&Collectible.NFTMinter>(Collectible.MinterPrivatePath)!
454 let openEdition <- OpenEditionV2.createOpenEditionCollection(minterCap: minterCap)
455 self.account.save(<-openEdition, to: OpenEditionV2.CollectionStoragePath)
456 self.account.link<&{OpenEditionV2.OpenEditionCollectionPublic}>(OpenEditionV2.CollectionPublicPath, target: OpenEditionV2.CollectionStoragePath)
457 }
458}
459