Smart Contract

ZeedzMarketplace

A.62b3063fbe672fc8.ZeedzMarketplace

Deployed

16h ago
Feb 28, 2026, 02:29:24 AM UTC

Dependents

0 imports
1import FungibleToken from 0xf233dcee88fe0abe
2import NFTStorefront from 0x4eb8a10cb9f87357
3
4pub contract ZeedzMarketplace {
5
6    pub event AddedListing(
7        storefrontAddress: Address,
8        listingResourceID: UInt64,
9        nftType: Type,
10        nftID: UInt64,
11        ftVaultType: Type,
12        price: UFix64
13    )
14
15    pub event RemovedListing(
16        listingResourceID: UInt64,
17        nftType: Type,
18        nftID: UInt64,
19        ftVaultType: Type,
20        price: UFix64
21    )
22
23    //
24    // A NFT listed on the Zeedz Marketplace, contains the NFTStorefront listingID, capability, listingDetails and timestamp.
25    //
26    pub struct Item {
27        pub let storefrontPublicCapability: Capability<&{NFTStorefront.StorefrontPublic}>
28
29        // NFTStorefront.Listing resource uuid
30        pub let listingID: UInt64
31
32        // Store listingDetails to prevent vanishing from storefrontPublicCapability
33        pub let listingDetails: NFTStorefront.ListingDetails
34
35        // Time when the listing was added to the Zeedz Marketplace
36        pub let timestamp: UFix64
37
38        init(storefrontPublicCapability: Capability<&{NFTStorefront.StorefrontPublic}>, listingID: UInt64) {
39            self.storefrontPublicCapability = storefrontPublicCapability
40            self.listingID = listingID
41            if storefrontPublicCapability.check() {
42                let storefrontPublic = storefrontPublicCapability.borrow()
43                let listingPublic = storefrontPublic!.borrowListing(listingResourceID: listingID) ?? panic("no listing id")
44                assert(listingPublic.borrowNFT() != nil, message: "could not borrow NFT")
45                self.listingDetails = listingPublic.getDetails()
46                self.timestamp = getCurrentBlock().timestamp
47            } else {
48                panic("Could not borrow public storefront from capability")
49            }
50
51        }
52    }
53
54    //
55    // A Sale cut requirement for each listing to be listed on the Zeedz Marketplace, updatable by the administrator.
56    // Contains a FungibleToken reciever capability for the sale cut recieving address and a ratio which defines the percentage of the sale cut.
57    //
58    pub struct SaleCutRequirement {
59        pub let receiver: Capability<&{FungibleToken.Receiver}>
60
61        pub let ratio: UFix64
62
63        init(receiver: Capability<&{FungibleToken.Receiver}>, ratio: UFix64) {
64            pre {
65                ratio <= 1.0: "ratio must be less than or equal to 1.0"
66            }
67            self.receiver = receiver
68            self.ratio = ratio
69        }
70    }
71
72    pub let ZeedzMarketplaceAdminStoragePath: StoragePath
73
74    // listingID order by time, listingID asc
75    access(contract) let listingIDs: [UInt64]
76
77    // listingID => item
78    access(contract) let listingIDItems: {UInt64: Item}
79
80    // collection identifier => (NFT id => listingID)
81    access(contract) let collectionNFTListingIDs: {String: {UInt64: UInt64}}
82
83    // {Type of the FungibleToken => array of SaleCutRequirements}
84    access(contract) var saleCutRequirements: {String : [SaleCutRequirement]}
85
86    //
87    // Administrator resource, owner account can update the Zeedz Marketplace sale cut requirements and remove listings.
88    //
89    pub resource Administrator {
90
91        pub fun updateSaleCutRequirement(requirements: [SaleCutRequirement], vaultType: Type) {
92            var totalRatio: UFix64 = 0.0
93            for requirement in requirements {
94                totalRatio = totalRatio + requirement.ratio
95            }
96            assert(totalRatio <= 1.0, message: "total ratio must be less than or equal to 1.0")
97            ZeedzMarketplace.saleCutRequirements[vaultType.identifier] = requirements
98        }
99
100        pub fun forceRemoveListing(id: UInt64) {
101            if let item = ZeedzMarketplace.listingIDItems[id] {
102                ZeedzMarketplace.removeItem(item)
103            }
104        }
105    }
106
107    //
108    // Adds a listing with the specified id and storefrontPublicCapability to the marketplace.
109    //
110    pub fun addListing(id: UInt64, storefrontPublicCapability: Capability<&{NFTStorefront.StorefrontPublic}>) {
111        let item = Item(storefrontPublicCapability: storefrontPublicCapability, listingID: id)
112
113        let indexToInsertListingID = self.getIndexToAddListingID(item: item, items: self.listingIDs)
114
115        self.addItem(
116            item,
117            storefrontPublicCapability: storefrontPublicCapability,
118            indexToInsertListingID: indexToInsertListingID)
119    }
120
121    //
122    // Can be used by anyone to remove a listing if the listed item has been removed or purchased.
123    //
124    pub fun removeListing(id: UInt64) {
125        if let item = self.listingIDItems[id] {
126            // Skip if the listing item hasn't been purchased
127            if item.storefrontPublicCapability.check() {
128                if let storefrontPublic = item.storefrontPublicCapability.borrow() {
129                    if let listingItem = storefrontPublic.borrowListing(listingResourceID: id) {
130                        let listingDetails = listingItem.getDetails()
131                        if listingDetails.purchased == false {
132                            return
133                        }
134                    }
135                }
136            }
137
138            self.removeItem(item)
139        }
140    }
141
142    //
143    // Returns an array of all listingsIDs currently listend on the marketplace.
144    //
145    pub fun getListingIDs(): [UInt64] {
146        return self.listingIDs
147    }
148
149    //
150    // Returns the item listed with the specified listingID.
151    //
152    pub fun getListingIDItem(listingID: UInt64): Item? {
153        return self.listingIDItems[listingID]
154    }
155
156    //
157    // Returns the listingID of the item from the specified nftType and nftID.
158    //
159    pub fun getListingID(nftType: Type, nftID: UInt64): UInt64? {
160        let nftListingIDs = self.collectionNFTListingIDs[nftType.identifier] ?? {}
161        return nftListingIDs[nftID]
162    }
163
164    //
165    // Returns an array of the current marketplace SaleCutRequirements
166    //
167    pub fun getAllSaleCutRequirements(): {String: [SaleCutRequirement]} {
168        return self.saleCutRequirements
169    }
170
171    //
172    // Returns an array of the current marketplace SaleCutRequirements for the specified VaultType
173    //
174    pub fun getVaultTypeSaleCutRequirements(vaultType: Type): [SaleCutRequirement]? {
175        return self.saleCutRequirements[vaultType.identifier]
176    }
177
178    //
179    // Helper function to add an item to the marketplace
180    //
181    access(contract) fun addItem(
182        _ item: Item,
183        storefrontPublicCapability: Capability<&{NFTStorefront.StorefrontPublic}>,
184        indexToInsertListingID: Int
185    ) {
186        pre {
187            self.listingIDItems[item.listingID] == nil: "could not add duplicate listing"
188        }
189
190        assert(item.listingDetails.purchased == false, message: "the item has been purchased")
191
192        // find previous duplicate NFT
193        let nftListingIDs = self.collectionNFTListingIDs[item.listingDetails.nftType.identifier]
194        var previousItem: Item? = nil
195        if let nftListingIDs = nftListingIDs {
196            if let listingID = nftListingIDs[item.listingDetails.nftID] {
197                previousItem = self.listingIDItems[listingID]!
198
199                // panic only if they're same address
200                if previousItem!.storefrontPublicCapability.address == item.storefrontPublicCapability.address {
201                    panic("could not add duplicate NFT")
202                }
203            }
204        }
205
206        // check sale cut
207        if let requirements = self.saleCutRequirements[item.listingDetails.salePaymentVaultType.identifier] {
208            for requirement in requirements {
209                let saleCutAmount = item.listingDetails.salePrice * requirement.ratio
210
211                var match = false
212                for saleCut in item.listingDetails.saleCuts {
213                    if saleCut.receiver.check() && requirement.receiver.check() && saleCut.receiver.address == requirement.receiver.address &&
214                    saleCut.receiver.borrow() == requirement.receiver.borrow() {
215                        if saleCut.amount >= saleCutAmount {
216                            match = true
217                        }
218                        break
219                    }
220                }
221
222                assert(match == true, message: "saleCut must follow SaleCutRequirements")
223            }
224        }
225
226        // all by time
227        self.listingIDs.insert(at: indexToInsertListingID, item.listingID)
228
229        // update index data
230        self.listingIDItems[item.listingID] = item
231        if let nftListingIDs = nftListingIDs {
232            nftListingIDs[item.listingDetails.nftID] = item.listingID
233            self.collectionNFTListingIDs[item.listingDetails.nftType.identifier] = nftListingIDs
234        } else {
235            self.collectionNFTListingIDs[item.listingDetails.nftType.identifier] = {item.listingDetails.nftID: item.listingID}
236        }
237
238        // remove previous item
239        if let previousItem = previousItem {
240            self.removeItem(previousItem)
241        }
242
243        emit AddedListing(
244            storefrontAddress: storefrontPublicCapability.address,
245            listingResourceID: item.listingID,
246            nftType: item.listingDetails.nftType,
247            nftID: item.listingDetails.nftID,
248            ftVaultType: item.listingDetails.salePaymentVaultType,
249            price: item.listingDetails.salePrice
250        )
251    }
252
253    //
254    // Helper function to remove item. The indexes will be found automatically.
255    //
256    access(contract) fun removeItem(_ item: Item) {
257        let indexToRemoveListingID = self.getIndexToRemoveListingID(
258            item: item,
259            items: self.listingIDs)
260
261        self.removeItemWithIndexes(
262            item,
263            indexToRemoveListingID: indexToRemoveListingID)
264    }
265
266    //
267    // Helper function to remove item with index. The index should be checked before calling this function.
268    //
269    access(contract) fun removeItemWithIndexes(_ item: Item, indexToRemoveListingID: Int?) {
270        // remove from listingIDs
271        if let indexToRemoveListingID = indexToRemoveListingID {
272            self.listingIDs.remove(at: indexToRemoveListingID)
273        }
274
275        // update index data
276        self.listingIDItems.remove(key: item.listingID)
277        let nftListingIDs = self.collectionNFTListingIDs[item.listingDetails.nftType.identifier] ?? {}
278        nftListingIDs.remove(key: item.listingDetails.nftID)
279        self.collectionNFTListingIDs[item.listingDetails.nftType.identifier] = nftListingIDs
280
281        emit RemovedListing(
282            listingResourceID: item.listingID,
283            nftType: item.listingDetails.nftType,
284            nftID: item.listingDetails.nftID,
285            ftVaultType: item.listingDetails.salePaymentVaultType,
286            price: item.listingDetails.salePrice,
287        )
288    }
289
290    //
291    // Run reverse for loop to find out the index to insert.
292    //
293    access(contract) fun getIndexToAddListingID(item: Item, items: [UInt64]): Int {
294        var index = items.length - 1
295        while index >= 0 {
296            let currentListingID = items[index]
297            let currentItem = self.listingIDItems[currentListingID]!
298
299            if item.timestamp == currentItem.timestamp {
300                if item.listingID > currentListingID {
301                    break
302                }
303                index = index - 1
304            } else {
305                break
306            }
307        }
308        return index + 1
309    }
310
311    //
312    // Run binary search to find the listing ID.
313    //
314    access(contract) fun getIndexToRemoveListingID(item: Item, items: [UInt64]): Int? {
315        var startIndex = 0
316        var endIndex = items.length
317
318        while startIndex < endIndex {
319            var midIndex = startIndex + (endIndex - startIndex) / 2
320            var midListingID = items[midIndex]!
321            var midItem = self.listingIDItems[midListingID]!
322
323            if item.timestamp > midItem.timestamp {
324                startIndex = midIndex + 1
325            } else if item.timestamp < midItem.timestamp {
326                endIndex = midIndex
327            } else {
328                if item.listingID > midListingID {
329                    startIndex = midIndex + 1
330                }  else if item.listingID < midListingID {
331                    endIndex = midIndex
332                } else {
333                    return midIndex
334                }
335            }
336        }
337        return nil
338    }
339
340    init () {
341        self.ZeedzMarketplaceAdminStoragePath = /storage/ZeedzMarketplaceAdmin
342
343        self.listingIDs = []
344        self.listingIDItems = {}
345        self.collectionNFTListingIDs = {}
346        self.saleCutRequirements = {}
347
348        let admin <- create Administrator()
349        self.account.save(<-admin, to: self.ZeedzMarketplaceAdminStoragePath)
350    }
351}
352