Smart Contract
ZeedzMarketplace
A.62b3063fbe672fc8.ZeedzMarketplace
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