Smart Contract

OpenEdition

A.f5b0eb433389ac3f.OpenEdition

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