Smart Contract
DarkCountryMarket
A.c8c340cebd11f690.DarkCountryMarket
1/*
2 DarkCountryMarket.cdc
3
4 Description: Contract definitions for users to sell and buy their DarkCountry NFTs
5
6 authors: Ivan Kravets evan@dapplica.io
7
8 Marketplace is where users can create a sale collection that they
9 store in their account storage. In the sale collection,
10 they can put their NFTs up for sale with a price and publish a
11 reference so that others can see the sale.
12
13 If another user sees an NFT that they want to buy,
14 they can send fungible tokens that equal or exceed the buy price
15 to buy the NFT. The NFT is transferred to them when
16 they make the purchase.
17
18 Each user who wants to sell NFTs will have a sale collection
19 instance in their account that holds the NFTs that they are putting up for sale
20
21 They can give a reference to this collection to a central contract
22 so that it can list the sales in a central place
23
24 When a user creates a sale, they will supply four arguments:
25 - A DarkCountry.Collection capability that allows their sale to withdraw
26 a NFT when it is purchased.
27 - A FungibleToken.Receiver capability as the place where the payment for the token goes.
28 - Item ID as the identifier of the item for sale
29 - Price of the item for sale
30
31 DarkCountry Market has smart contract level setting that are managed by an account with Admin resource.
32 Such setting are as follows:
33 - beneficiaryCapability: A FungibleToken.Receiver capability specifying a beneficiary,
34 where a cut of the purchase gets sent.
35 - cutPercentage: A cut percentage, specifying how much the beneficiary will recieve.
36 - preOrders: A dictionary of Adress to {ItemTemplate : number of preordered items} mapping that indicates
37 how many items of a specific Item Template are resevred for the Address
38
39
40 Only Admins can create sale offers wich can be used in pre-sales only. Such offers can not be accepted by users
41 that do not have records in the preOrders. Once such sale is accepted, the preOrders value is adjusted accordingly.
42
43*/
44
45import FlowToken from 0x1654653399040a61
46import DarkCountry from 0xc8c340cebd11f690
47import DarkCountryStaking from 0xc8c340cebd11f690
48import FungibleToken from 0xf233dcee88fe0abe
49import NonFungibleToken from 0x1d7e57aa55817448
50
51
52pub contract DarkCountryMarket {
53 // SaleOffer events.
54 //
55 // A sale offer has been created.
56 pub event SaleOfferCreated(itemID: UInt64, price: UFix64)
57 // Someone has purchased an item that was offered for sale.
58 pub event SaleOfferAccepted(itemID: UInt64, buyerAddress: Address)
59 // A sale offer has been destroyed, with or without being accepted.
60 pub event SaleOfferFinished(itemID: UInt64)
61
62 // A sale offer has been removed from the collection of Address.
63 pub event CollectionRemovedSaleOffer(itemID: UInt64, owner: Address)
64
65 // A sale offer has been inserted into the collection of Address.
66 pub event CollectionInsertedSaleOffer(
67 itemID: UInt64,
68 itemTemplateID: UInt64,
69 owner: Address,
70 price: UFix64
71 )
72
73 // emitted when the cut percentage has been changed by the DarkCountry Market admin
74 // the same cut percentage value is used for the all sales within the market
75 pub event CutPercentageChanged(newPercent: UFix64)
76
77 // emitted when a user's pre orders have been changed by the DarkCountry Market admin
78 pub event PreOrderChanged(userAddress: Address, newPreOrders: {UInt64: UInt64})
79
80
81 // Named paths
82 //
83 pub let CollectionStoragePath: StoragePath
84 pub let CollectionPublicPath: PublicPath
85 pub let AdminStoragePath: StoragePath
86
87 // The capability that is used for depositing
88 // the beneficiary's cut of every sale
89 // The beneficiary is set at the Dark Country Market level by the Market's admin and be the same
90 // for all the DarkCountry NFTs
91 access(account) var beneficiaryCapability: Capability
92
93 // The percentage that is taken from every purchase for the beneficiary
94 // For example, if the percentage is 15%, cutPercentage = 0.15
95 // The percentage cut is set at the Dark Country Market level by the Market's admin and be the same
96 // for all the DarkCountry NFTs
97 pub var cutPercentage: UFix64
98
99 // Pre Orders for a drop. Optional.
100 // Indicates how many NFTs of a certain Item Template booked for a user.
101 // The Admin resource manages the data.
102 // Note: We do not make it as a resource that can be stored in user's storage
103 // since the pre-order might be requested off chain
104 access(account) var preOrders: { Address: { UInt64 : UInt64 } }
105
106 // SaleOfferPublicView
107 // An interface providing a read-only view of a SaleOffer
108 //
109 pub resource interface SaleOfferPublicView {
110 pub let itemID: UInt64
111 pub let itemTemplateID: UInt64
112 pub let price: UFix64
113 }
114
115 // SaleOffer
116 // A DarkCountry NFT being offered to sale for a set fee paid in FlowToken.
117 //
118 pub resource SaleOffer: SaleOfferPublicView {
119 // Whether the sale has completed with someone purchasing the item.
120 pub var saleCompleted: Bool
121
122 // The DarkCountry NFT ID for sale.
123 pub let itemID: UInt64
124
125 // The Item Template of NFT
126 pub let itemTemplateID: UInt64
127
128 // The sale payment price.
129 pub let price: UFix64
130
131 // Indicates if the Sale for pre-ordered items only
132 // That means only buyers that pre-ordered corresponding item can accept the offer
133 // Only account with the Admin resource can create such sales
134 pub let isPreOrdersOnly: Bool
135
136 // The collection containing that ID.
137 access(self) let sellerItemProvider: Capability<&DarkCountry.Collection{NonFungibleToken.Provider}>
138
139 // The FlowToken vault that will receive that payment if the sale completes successfully.
140 access(self) let sellerPaymentReceiver: Capability<&FlowToken.Vault{FungibleToken.Receiver}>
141
142 // Called by a purchaser to accept the sale offer.
143 // If they send the correct payment in FlowToken, and if the item is still available,
144 // the DarkCountry NFT will be placed in their DarkCountry.Collection
145 // If the sale offer is for pre ordered items only,
146 // the preOrders dictionary is checked for a corresponding record
147 //
148 pub fun accept(
149 buyerCollection: &DarkCountry.Collection{NonFungibleToken.Receiver},
150 buyerPayment: @FungibleToken.Vault,
151 ) {
152 pre {
153 buyerPayment.balance == self.price: "payment does not equal offer price"
154 self.saleCompleted == false: "the sale offer has already been accepted"
155 }
156
157 let buyerAccount = buyerCollection.owner ?? panic("Could not get buyer address during accepting the pre sale")
158
159 // Check if the sale is for pre-ordered items only
160 if self.isPreOrdersOnly == true {
161
162 let buyerPreOrders = DarkCountryMarket.preOrders[buyerAccount.address] ?? {}
163
164 let preOrderedCount = buyerPreOrders[self.itemTemplateID] ?? (0 as UInt64)
165
166 if preOrderedCount < (1 as UInt64) {
167 panic("Could not find pre ordered items")
168 }
169
170 buyerPreOrders[self.itemTemplateID] = preOrderedCount - (1 as UInt64)
171
172 DarkCountryMarket.preOrders[buyerAccount.address] = buyerPreOrders
173 }
174
175 self.saleCompleted = true
176
177 // Take the cut of the tokens that the beneficiary gets from the sent tokens
178 let beneficiaryCut <- buyerPayment.withdraw(amount: self.price * DarkCountryMarket.cutPercentage)
179
180 // Deposit it into the beneficiary's Vault
181 DarkCountryMarket.beneficiaryCapability.borrow<&{FungibleToken.Receiver}>()!
182 .deposit(from: <- beneficiaryCut)
183
184 // Deposit the remaining tokens into the seller's vault
185 self.sellerPaymentReceiver.borrow()!.deposit(from: <- buyerPayment)
186
187 let nft <- self.sellerItemProvider.borrow()!.withdraw(withdrawID: self.itemID)
188 buyerCollection.deposit(token: <-nft)
189
190 emit SaleOfferAccepted(itemID: self.itemID, buyerAddress: buyerAccount.address)
191 }
192
193 // destructor
194 //
195 destroy() {
196 // Whether the sale completed or not, publicize that it is being withdrawn.
197 emit SaleOfferFinished(itemID: self.itemID)
198 }
199
200 // initializer
201 // Take the information required to create a sale offer, notably the capability
202 // to transfer the DarkCountry NFT and the capability to receive FlowToken in payment.
203 //
204 init(
205 sellerItemProvider: Capability<&DarkCountry.Collection{NonFungibleToken.Provider}>,
206 itemID: UInt64,
207 sellerPaymentReceiver: Capability<&FlowToken.Vault{FungibleToken.Receiver}>,
208 price: UFix64,
209 isPreOrdersOnly: Bool
210 ) {
211 pre {
212 sellerItemProvider.borrow() != nil: "Cannot borrow seller"
213 sellerPaymentReceiver.borrow() != nil: "Cannot borrow sellerPaymentReceiver"
214 }
215
216 let saleOwner = sellerItemProvider.borrow()!.owner!
217
218 let collectionBorrow = saleOwner.getCapability(DarkCountry.CollectionPublicPath)!
219 .borrow<&{DarkCountry.DarkCountryCollectionPublic}>()
220 ?? panic("Could not borrow DarkCountryCollectionPublic")
221
222 // borrow a reference to a specific NFT in the collection
223 let nft = collectionBorrow.borrowDarkCountryNFT(id: itemID)
224 ?? panic("No such itemID in that collection")
225
226 // make sure the NFT is not staked
227 if DarkCountryStaking.stakedItems.containsKey(nft.owner?.address!) &&
228 DarkCountryStaking.stakedItems[nft.owner?.address!]!.contains(itemID) {
229 panic("Cannot withdraw: the NFT is staked.")
230 }
231
232 self.itemTemplateID = nft.itemTemplateID
233
234 self.saleCompleted = false
235
236 self.sellerItemProvider = sellerItemProvider
237 self.itemID = itemID
238
239 self.sellerPaymentReceiver = sellerPaymentReceiver
240 self.price = price
241
242 self.isPreOrdersOnly = isPreOrdersOnly
243
244 emit SaleOfferCreated(itemID: self.itemID, price: self.price)
245 }
246 }
247
248 // createSaleOffer
249 // Make creating a SaleOffer publicly accessible.
250 //
251 // NOTE: the function will be private in the initial release of the market smart contract
252 //
253 pub fun createSaleOffer (
254 sellerItemProvider: Capability<&DarkCountry.Collection{NonFungibleToken.Provider}>,
255 itemID: UInt64,
256 sellerPaymentReceiver: Capability<&FlowToken.Vault{FungibleToken.Receiver}>,
257 price: UFix64
258 ): @SaleOffer {
259 return <-create SaleOffer(
260 sellerItemProvider: sellerItemProvider,
261 itemID: itemID,
262 sellerPaymentReceiver: sellerPaymentReceiver,
263 price: price,
264 isPreOrdersOnly: false
265 )
266 }
267
268 // CollectionManager
269 // An interface for adding and removing SaleOffers to a collection, intended for
270 // use by the collection's owner.
271 //
272 pub resource interface CollectionManager {
273 pub fun insert(offer: @DarkCountryMarket.SaleOffer)
274 pub fun remove(itemID: UInt64): @SaleOffer
275 }
276
277 // CollectionPurchaser
278 // An interface to allow purchasing items via SaleOffers in a collection.
279 // This function is also provided by CollectionPublic, it is here to support
280 // more fine-grained access to the collection for as yet unspecified future use cases.
281 //
282 pub resource interface CollectionPurchaser {
283 pub fun purchase(
284 itemID: UInt64,
285 buyerCollection: &DarkCountry.Collection{NonFungibleToken.Receiver},
286 buyerPayment: @FungibleToken.Vault
287 )
288 }
289
290 // CollectionPublic
291 // An interface to allow listing and borrowing SaleOffers, and purchasing items via SaleOffers in a collection.
292 //
293 pub resource interface CollectionPublic {
294 pub fun getSaleOfferIDs(): [UInt64]
295 pub fun borrowSaleItem(itemID: UInt64): &SaleOffer{SaleOfferPublicView}?
296 pub fun purchase(
297 itemID: UInt64,
298 buyerCollection: &DarkCountry.Collection{NonFungibleToken.Receiver},
299 buyerPayment: @FungibleToken.Vault
300 )
301 }
302
303 // Collection
304 // A resource that allows its owner to manage a list of SaleOffers, and purchasers to interact with them.
305 //
306 pub resource Collection : CollectionManager, CollectionPurchaser, CollectionPublic {
307 pub var saleOffers: @{UInt64: SaleOffer}
308
309 // insert
310 // Insert a SaleOffer into the collection, replacing one with the same itemID if present.
311 //
312 pub fun insert(offer: @DarkCountryMarket.SaleOffer) {
313 let itemID: UInt64 = offer.itemID
314 let itemTemplateID: UInt64 = offer.itemTemplateID
315 let price: UFix64 = offer.price
316
317 // add the new offer to the dictionary which removes the old one
318 let oldOffer <- self.saleOffers[itemID] <- offer
319 destroy oldOffer
320
321 emit CollectionInsertedSaleOffer(
322 itemID: itemID,
323 itemTemplateID: itemTemplateID,
324 owner: self.owner?.address!,
325 price: price
326 )
327 }
328
329 // remove
330 // Remove and return a SaleOffer from the collection.
331 pub fun remove(itemID: UInt64): @SaleOffer {
332 emit CollectionRemovedSaleOffer(itemID: itemID, owner: self.owner?.address!)
333 return <-(self.saleOffers.remove(key: itemID) ?? panic("missing SaleOffer"))
334 }
335
336 // purchase
337 // If the caller passes a valid itemID and the item is still for sale, and passes a FlowToken vault
338 // typed as a FungibleToken.Vault (FlowToken.deposit() handles the type safety of this)
339 // containing the correct payment amount, this will transfer the KittyItem to the caller's
340 // DarkCountry collection.
341 // It will then remove and destroy the offer.
342 // Note that is means that events will be emitted in this order:
343 // 1. Collection.CollectionRemovedSaleOffer
344 // 2. DarkCountry.Withdraw
345 // 3. DarkCountry.Deposit
346 // 4. SaleOffer.SaleOfferFinished
347 //
348 pub fun purchase(
349 itemID: UInt64,
350 buyerCollection: &DarkCountry.Collection{NonFungibleToken.Receiver},
351 buyerPayment: @FungibleToken.Vault
352 ) {
353 pre {
354 self.saleOffers[itemID] != nil: "SaleOffer does not exist in the collection!"
355 }
356 let offer <- self.remove(itemID: itemID)
357 offer.accept(buyerCollection: buyerCollection, buyerPayment: <-buyerPayment)
358 // We destroy the offer. The purchase history should be tracked off chain
359 destroy offer
360 }
361
362 // getSaleOfferIDs
363 // Returns an array of the IDs that are in the collection
364 //
365 pub fun getSaleOfferIDs(): [UInt64] {
366 return self.saleOffers.keys
367 }
368
369 // borrowSaleItem
370 // Returns an Optional read-only view of the SaleItem for the given itemID if it is contained by this collection.
371 // The optional will be nil if the provided itemID is not present in the collection.
372 //
373 pub fun borrowSaleItem(itemID: UInt64): &SaleOffer{SaleOfferPublicView}? {
374 if self.saleOffers[itemID] == nil {
375 return nil
376 } else {
377 return &self.saleOffers[itemID] as &SaleOffer{SaleOfferPublicView}?
378 }
379 }
380
381 // destructor
382 //
383 destroy () {
384 destroy self.saleOffers
385 }
386
387 // constructor
388 //
389 init () {
390 self.saleOffers <- {}
391 }
392 }
393
394 // createEmptyCollection
395 // Make creating a Collection publicly accessible.
396 //
397 pub fun createEmptyCollection(): @Collection {
398 return <-create Collection()
399 }
400
401 // Admin is a special authorization resource that
402 // allows the owner to perform functions to modify the following:
403 // 1. Beneficiary
404 // 2. Beneficiary cut percentage
405 // 3. Pre-orders
406 pub resource Admin {
407
408 // setPercentage changes the cut percentage of the tokens that are for sale
409 //
410 // Parameters: newPercent: The new cut percentage for the sale
411 pub fun setPercentage(_ newPercent: UFix64) {
412
413 DarkCountryMarket.cutPercentage = newPercent
414
415 emit CutPercentageChanged(newPercent: newPercent)
416 }
417
418 // setBeneficiaryReceiver updates the capability for the beneficiary of the cut of the sale
419 //
420 // Parameters: newBeneficiary the new capability for the beneficiary of the cut of the sale
421 //
422 pub fun setBeneficiaryReceiver(_ newBeneficiaryCapability: Capability) {
423 pre {
424 newBeneficiaryCapability.borrow<&{FungibleToken.Receiver}>() != nil:
425 "Beneficiary's Receiver Capability is invalid!"
426 }
427
428 DarkCountryMarket.beneficiaryCapability = newBeneficiaryCapability
429 }
430
431 // sets pre orders for a user by theirs addresss
432 //
433 // Parameters: userAddress: The address of the user's account
434 // newPreOrders: dictionaty of Item Template and corresponding amount of items that are booked
435 pub fun setPreOrdersForAddress(userAddress: Address, newPreOrders: {UInt64: UInt64}) {
436
437 DarkCountryMarket.preOrders[userAddress] = newPreOrders
438
439 emit PreOrderChanged(userAddress: userAddress, newPreOrders: newPreOrders)
440 }
441
442
443 // createSaleOffer
444 // Make creating a SaleOffer publicly accessible.
445 //
446 pub fun createPreOrderSaleOffer (
447 sellerItemProvider: Capability<&DarkCountry.Collection{NonFungibleToken.Provider}>,
448 itemID: UInt64,
449 sellerPaymentReceiver: Capability<&FlowToken.Vault{FungibleToken.Receiver}>,
450 price: UFix64
451 ): @SaleOffer {
452 return <-create SaleOffer(
453 sellerItemProvider: sellerItemProvider,
454 itemID: itemID,
455 sellerPaymentReceiver: sellerPaymentReceiver,
456 price: price,
457 isPreOrdersOnly: true
458 )
459 }
460
461 // createNewAdmin creates a new Admin resource
462 //
463 pub fun createNewAdmin(): @Admin {
464 return <-create Admin()
465 }
466 }
467
468 init () {
469 self.CollectionStoragePath = /storage/DarkCountryMarketCollection
470 self.CollectionPublicPath = /public/DarkCountryMarketCollection
471 self.AdminStoragePath = /storage/DarkCountryAdmin
472
473 let admin <- create Admin()
474 self.account.save(<-admin, to: self.AdminStoragePath)
475
476 // The default cut percentage value can be changed by Admin
477 self.cutPercentage = (0.15 as UFix64)
478
479 // The default beneficiary capability value can be changed by Admin
480 self.beneficiaryCapability = self.account.getCapability(/public/flowTokenReceiver)
481
482 self.preOrders = {}
483 }
484}