Smart Contract
OpenEdition
A.f5b0eb433389ac3f.OpenEdition
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