Smart Contract
DriverzNFTStorefront
A.f887ece39166906e.DriverzNFTStorefront
1import FungibleToken from 0xf233dcee88fe0abe
2import NonFungibleToken from 0x1d7e57aa55817448
3
4pub contract DriverzNFTStorefront {
5
6 pub event DriverzNFTStorefrontInitialized()
7 pub event StorefrontInitialized(storefrontResourceID: UInt64)
8 pub event StorefrontDestroyed(storefrontResourceID: UInt64)
9
10 pub event ListingAvailable(
11 storefrontAddress: Address,
12 listingResourceID: UInt64,
13 nftType: Type,
14 nftID: UInt64,
15 ftVaultType: Type,
16 price: UFix64
17 )
18
19 pub event ListingCompleted(
20 listingResourceID: UInt64,
21 storefrontResourceID: UInt64,
22 purchased: Bool,
23 nftType: Type,
24 nftID: UInt64
25 )
26
27
28 pub let StorefrontStoragePath: StoragePath
29 pub let StorefrontPublicPath: PublicPath
30
31 access(contract) let ListedDriverz: {Address: [UInt64]}
32
33 pub fun getAddresses(): [Address] {
34 return DriverzNFTStorefront.ListedDriverz.keys
35 }
36
37 pub fun getList(): {Address: [UInt64]} {
38 return DriverzNFTStorefront.ListedDriverz
39 }
40
41 //A function that it's called only inside contract that add a listing to the contract ListedDriverz dictionary
42 access(contract) fun addListing(address: Address, storefrontID: UInt64){
43 if(DriverzNFTStorefront.ListedDriverz[address] == nil){
44 DriverzNFTStorefront.ListedDriverz[address] = [storefrontID]
45 } else {
46 DriverzNFTStorefront.ListedDriverz[address]!.append(storefrontID)
47 }
48 }
49
50 //A function that it's called only inside contract that remove a listing from the contract ListedDriverz dictionary
51 access(contract) fun removeListing(address: Address, storefrontID: UInt64){
52 if(DriverzNFTStorefront.ListedDriverz[address] != nil){
53 if(DriverzNFTStorefront.ListedDriverz[address]!.length == 1){
54 DriverzNFTStorefront.ListedDriverz.remove(key: address)
55 } else {
56 let index = DriverzNFTStorefront.ListedDriverz[address]!.firstIndex(of: storefrontID)!
57 DriverzNFTStorefront.ListedDriverz[address]!.remove(at: index)
58 }
59 }
60 }
61
62 pub struct SaleCut {
63 pub let receiver: Capability<&{FungibleToken.Receiver}>
64
65 // The amount of the payment FungibleToken that will be paid to the receiver.
66 pub let amount: UFix64
67
68 // initializer
69 //
70 init(receiver: Capability<&{FungibleToken.Receiver}>, amount: UFix64) {
71 self.receiver = receiver
72 self.amount = amount
73 }
74 }
75
76 pub struct ListingDetails {
77
78 pub var storefrontID: UInt64
79 // Whether this listing has been purchased or not.
80 pub var purchased: Bool
81 // The Type of the NonFungibleToken.NFT that is being listed.
82 pub let nftType: Type
83 // The ID of the NFT within that type.
84 pub let nftID: UInt64
85 // The Type of the FungibleToken that payments must be made in.
86 pub let salePaymentVaultType: Type
87 // The amount that must be paid in the specified FungibleToken.
88 pub let salePrice: UFix64
89 // This specifies the division of payment between recipients.
90 pub let saleCuts: [SaleCut]
91
92 // setToPurchased
93 // Irreversibly set this listing as purchased.
94 //
95 access(contract) fun setToPurchased() {
96 self.purchased = true
97 }
98
99 // initializer
100 //
101 init (
102 nftType: Type,
103 nftID: UInt64,
104 salePaymentVaultType: Type,
105 saleCuts: [SaleCut],
106 storefrontID: UInt64
107 ) {
108 self.storefrontID = storefrontID
109 self.purchased = false
110 self.nftType = nftType
111 self.nftID = nftID
112 self.salePaymentVaultType = salePaymentVaultType
113 // Store the cuts
114 assert(saleCuts.length > 0, message: "Listing must have at least one payment cut recipient")
115 self.saleCuts = saleCuts
116
117 // Calculate the total price from the cuts
118 var salePrice = 0.0
119 // Perform initial check on capabilities, and calculate sale price from cut amounts.
120 for cut in self.saleCuts {
121 // Make sure we can borrow the receiver.
122 // We will check this again when the token is sold.
123 cut.receiver.borrow()
124 ?? panic("Cannot borrow receiver")
125 // Add the cut amount to the total price
126 salePrice = salePrice + cut.amount
127 }
128 assert(salePrice > 0.0, message: "Listing must have non-zero price")
129
130 // Store the calculated sale price
131 self.salePrice = salePrice
132 }
133 }
134
135
136 // ListingPublic
137 // An interface providing a useful public interface to a Listing.
138 //
139 pub resource interface ListingPublic {
140 // borrowNFT
141 // This will assert in the same way as the NFT standard borrowNFT()
142 // if the NFT is absent, for example if it has been sold via another listing.
143 //
144 pub fun borrowNFT(): &NonFungibleToken.NFT
145
146 // purchase
147 // Purchase the listing, buying the token.
148 // This pays the beneficiaries and returns the token to the buyer.
149 //
150 pub fun purchase(payment: @FungibleToken.Vault): @NonFungibleToken.NFT
151
152 // getDetails
153 //
154 pub fun getDetails(): ListingDetails
155
156 }
157
158
159 // Listing
160 // A resource that allows an NFT to be sold for an amount of a given FungibleToken,
161 // and for the proceeds of that sale to be split between several recipients.
162 //
163 pub resource Listing: ListingPublic {
164 // The simple (non-Capability, non-complex) details of the sale
165 access(self) let details: ListingDetails
166
167 // A capability allowing this resource to withdraw the NFT with the given ID from its collection.
168 // This capability allows the resource to withdraw *any* NFT, so you should be careful when giving
169 // such a capability to a resource and always check its code to make sure it will use it in the
170 // way that it claims.
171 access(contract) let nftProviderCapability: Capability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>
172
173 // borrowNFT
174 // This will assert in the same way as the NFT standard borrowNFT()
175 // if the NFT is absent, for example if it has been sold via another listing.
176 //
177 pub fun borrowNFT(): &NonFungibleToken.NFT {
178 let ref = self.nftProviderCapability.borrow()!.borrowNFT(id: self.getDetails().nftID)
179 //- CANNOT DO THIS IN PRECONDITION: "member of restricted type is not accessible: isInstance"
180 // result.isInstance(self.getDetails().nftType): "token has wrong type"
181 assert(ref.isInstance(self.getDetails().nftType), message: "token has wrong type")
182 assert(ref.id == self.getDetails().nftID, message: "token has wrong ID")
183 return (ref as &NonFungibleToken.NFT?)!
184 }
185
186 // getDetails
187 // Get the details of the current state of the Listing as a struct.
188 // This avoids having more public variables and getter methods for them, and plays
189 // nicely with scripts (which cannot return resources).
190 //
191 pub fun getDetails(): ListingDetails {
192 return self.details
193 }
194
195 // purchase
196 // Purchase the listing, buying the token.
197 // This pays the beneficiaries and returns the token to the buyer.
198 //
199 pub fun purchase(payment: @FungibleToken.Vault): @NonFungibleToken.NFT {
200 pre {
201 self.details.purchased == false: "listing has already been purchased"
202 payment.isInstance(self.details.salePaymentVaultType): "payment vault is not requested fungible token"
203 payment.balance == self.details.salePrice: "payment vault does not contain requested price"
204 }
205
206 // Make sure the listing cannot be purchased again.
207 self.details.setToPurchased()
208 let sellerAddress = self.nftProviderCapability.borrow()!.owner!.address
209 DriverzNFTStorefront.removeListing(address: sellerAddress, storefrontID: self.uuid)
210
211
212 // Fetch the token to return to the purchaser.
213 let nft <-self.nftProviderCapability.borrow()!.withdraw(withdrawID: self.details.nftID)
214 // Neither receivers nor providers are trustworthy, they must implement the correct
215 // interface but beyond complying with its pre/post conditions they are not gauranteed
216 // to implement the functionality behind the interface in any given way.
217 // Therefore we cannot trust the Collection resource behind the interface,
218 // and we must check the NFT resource it gives us to make sure that it is the correct one.
219 assert(nft.isInstance(self.details.nftType), message: "withdrawn NFT is not of specified type")
220 assert(nft.id == self.details.nftID, message: "withdrawn NFT does not have specified ID")
221
222 // Rather than aborting the transaction if any receiver is absent when we try to pay it,
223 // we send the cut to the first valid receiver.
224 // The first receiver should therefore either be the seller, or an agreed recipient for
225 // any unpaid cuts.
226 var residualReceiver: &{FungibleToken.Receiver}? = nil
227
228 // Pay each beneficiary their amount of the payment.
229 for cut in self.details.saleCuts {
230 if let receiver = cut.receiver.borrow() {
231 let paymentCut <- payment.withdraw(amount: cut.amount)
232 receiver.deposit(from: <-paymentCut)
233 if (residualReceiver == nil) {
234 residualReceiver = receiver
235 }
236 }
237 }
238
239 assert(residualReceiver != nil, message: "No valid payment receivers")
240
241 // At this point, if all recievers were active and availabile, then the payment Vault will have
242 // zero tokens left, and this will functionally be a no-op that consumes the empty vault
243 residualReceiver!.deposit(from: <-payment)
244
245 // If the listing is purchased, we regard it as completed here.
246 // Otherwise we regard it as completed in the destructor.
247
248 emit ListingCompleted(
249 listingResourceID: self.uuid,
250 storefrontResourceID: self.details.storefrontID,
251 purchased: self.details.purchased,
252 nftType: self.details.nftType,
253 nftID: self.details.nftID
254 )
255
256 return <-nft
257 }
258
259 // destructor
260 //
261 destroy () {
262 // If the listing has not been purchased, we regard it as completed here.
263 // Otherwise we regard it as completed in purchase().
264 // This is because we destroy the listing in Storefront.removeListing()
265 // or Storefront.cleanup() .
266 // If we change this destructor, revisit those functions.
267 if !self.details.purchased {
268
269 emit ListingCompleted(
270 listingResourceID: self.uuid,
271 storefrontResourceID: self.details.storefrontID,
272 purchased: self.details.purchased,
273 nftType: self.details.nftType,
274 nftID: self.details.nftID
275 )
276 }
277 }
278
279 // initializer
280 //
281 init (
282 nftProviderCapability: Capability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>,
283 nftType: Type,
284 nftID: UInt64,
285 salePaymentVaultType: Type,
286 saleCuts: [SaleCut],
287 storefrontID: UInt64
288 ) {
289 // Store the sale information
290 self.details = ListingDetails(
291 nftType: nftType,
292 nftID: nftID,
293 salePaymentVaultType: salePaymentVaultType,
294 saleCuts: saleCuts,
295 storefrontID: storefrontID
296 )
297
298 // Store the NFT provider
299 self.nftProviderCapability = nftProviderCapability
300
301 // Check that the provider contains the NFT.
302 // We will check it again when the token is sold.
303 // We cannot move this into a function because initializers cannot call member functions.
304 let provider = self.nftProviderCapability.borrow()
305 assert(provider != nil, message: "cannot borrow nftProviderCapability")
306
307 // This will precondition assert if the token is not available.
308 let nft = provider!.borrowNFT(id: self.details.nftID)
309 assert(nft.isInstance(self.details.nftType), message: "token is not of specified type")
310 assert(nft.id == self.details.nftID, message: "token does not have specified ID")
311 }
312 }
313
314 // StorefrontManager
315 // An interface for adding and removing Listings within a Storefront,
316 // intended for use by the Storefront's own
317 //
318 pub resource interface StorefrontManager {
319 // createListing
320 // Allows the Storefront owner to create and insert Listings.
321 //
322 pub fun createListing(
323 nftProviderCapability: Capability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>,
324 nftType: Type,
325 nftID: UInt64,
326 salePaymentVaultType: Type,
327 saleCuts: [SaleCut]
328 ): UInt64
329 // removeListing
330 // Allows the Storefront owner to remove any sale listing, acepted or not.
331 //
332 pub fun removeListing(listingResourceID: UInt64, address: Address)
333 }
334
335 // StorefrontPublic
336 // An interface to allow listing and borrowing Listings, and purchasing items via Listings
337 // in a Storefront.
338 //
339 pub resource interface StorefrontPublic {
340 pub fun getListingIDs(): [UInt64]
341 pub fun borrowListing(listingResourceID: UInt64): &Listing{ListingPublic}?
342 pub fun cleanup(listingResourceID: UInt64)
343 }
344
345 // Storefront
346 // A resource that allows its owner to manage a list of Listings, and anyone to interact with them
347 // in order to query their details and purchase the NFTs that they represent.
348 //
349 pub resource Storefront : StorefrontManager, StorefrontPublic {
350 // The dictionary of Listing uuids to Listing resources.
351 access(self) var listings: @{UInt64: Listing}
352
353 // insert
354 // Create and publish a Listing for an NFT.
355 //
356 pub fun createListing(
357 nftProviderCapability: Capability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>,
358 nftType: Type,
359 nftID: UInt64,
360 salePaymentVaultType: Type,
361 saleCuts: [SaleCut]
362 ): UInt64 {
363 let listing <- create Listing(
364 nftProviderCapability: nftProviderCapability,
365 nftType: nftType,
366 nftID: nftID,
367 salePaymentVaultType: salePaymentVaultType,
368 saleCuts: saleCuts,
369 storefrontID: self.uuid
370 )
371
372 let listingResourceID = listing.uuid
373 let listingPrice = listing.getDetails().salePrice
374
375 // Add the new listing to the dictionary.
376 let oldListing <- self.listings[listingResourceID] <- listing
377 // Note that oldListing will always be nil, but we have to handle it.
378
379 DriverzNFTStorefront.addListing(address: self.owner?.address!, storefrontID: listingResourceID)
380
381 destroy oldListing
382
383 emit ListingAvailable(
384 storefrontAddress: self.owner?.address!,
385 listingResourceID: listingResourceID,
386 nftType: nftType,
387 nftID: nftID,
388 ftVaultType: salePaymentVaultType,
389 price: listingPrice
390 )
391
392 return listingResourceID
393 }
394
395
396 // removeListing
397 // Remove a Listing that has not yet been purchased from the collection and destroy it.
398 //
399 pub fun removeListing(listingResourceID: UInt64, address: Address) {
400 let listing <- self.listings.remove(key: listingResourceID)
401 ?? panic("missing Listing")
402
403 if(DriverzNFTStorefront.ListedDriverz[address] != nil && DriverzNFTStorefront.ListedDriverz[address]!.contains(listingResourceID)) {
404 DriverzNFTStorefront.removeListing(address: address, storefrontID: listingResourceID)
405 }
406
407 // This will emit a ListingCompleted event.
408 destroy listing
409 }
410
411 // getListingIDs
412 // Returns an array of the Listing resource IDs that are in the collection
413 //
414 pub fun getListingIDs(): [UInt64] {
415 return self.listings.keys
416 }
417
418 // borrowSaleItem
419 // Returns a read-only view of the SaleItem for the given listingID if it is contained by this collection.
420 //
421 pub fun borrowListing(listingResourceID: UInt64): &Listing{ListingPublic}? {
422 if self.listings[listingResourceID] != nil {
423 return (&self.listings[listingResourceID] as &Listing{ListingPublic}?)
424 } else {
425 return nil
426 }
427 }
428
429 // cleanup
430 // Remove an listing *if* it has been purchased.
431 // Anyone can call, but at present it only benefits the account owner to do so.
432 // Kind purchasers can however call it if they like.
433 //
434 pub fun cleanup(listingResourceID: UInt64) {
435 pre {
436 self.listings[listingResourceID] != nil: "could not find listing with given id"
437 }
438
439 let listing <- self.listings.remove(key: listingResourceID)!
440 assert(listing.getDetails().purchased == true, message: "listing is not purchased, only admin can remove")
441 destroy listing
442 }
443
444 // destructor
445 //
446 destroy () {
447 destroy self.listings
448
449 // Let event consumers know that this storefront will no longer exist
450 emit StorefrontDestroyed(storefrontResourceID: self.uuid)
451 }
452
453 // constructor
454 //
455 init () {
456 self.listings <- {}
457
458 // Let event consumers know that this storefront exists
459 emit StorefrontInitialized(storefrontResourceID: self.uuid)
460 }
461 }
462
463 pub resource DriverzStorefrontAdmin {
464 pub fun removeListing(address: Address, listingID: UInt64){
465 DriverzNFTStorefront.removeListing(address: address, storefrontID: listingID)
466 }
467
468 pub fun removeAllListingsFromAddress(address: Address){
469 DriverzNFTStorefront.ListedDriverz.remove(key: address)
470 }
471 }
472
473 // createStorefront
474 // Make creating a Storefront publicly accessible.
475 //
476 pub fun createStorefront(): @Storefront {
477 return <-create Storefront()
478 }
479
480 init () {
481 self.ListedDriverz = {}
482 self.StorefrontStoragePath = /storage/DriverzNFTStorefront
483 self.StorefrontPublicPath = /public/DriverzNFTStorefront
484
485 emit DriverzNFTStorefrontInitialized()
486 }
487}
488