Smart Contract

DriverzNFTStorefront

A.f887ece39166906e.DriverzNFTStorefront

Deployed

1d ago
Feb 26, 2026, 09:44:38 PM UTC

Dependents

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