Smart Contract

OpenEditionV2

A.f5b0eb433389ac3f.OpenEditionV2

Deployed

12h ago
Feb 28, 2026, 04:34:50 AM UTC

Dependents

0 imports
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