Smart Contract

MotoGPNFTStorefront

A.a49cc0ee46c54bfb.MotoGPNFTStorefront

Deployed

1d ago
Feb 26, 2026, 09:43:52 PM UTC

Dependents

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