Smart Contract

DarkCountryMarket

A.c8c340cebd11f690.DarkCountryMarket

Deployed

14h ago
Feb 28, 2026, 02:29:49 AM UTC

Dependents

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