Smart Contract

TiresStorefront

A.f887ece39166906e.TiresStorefront

Deployed

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

Dependents

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