Smart Contract
MotoGPNFTStorefront
A.a49cc0ee46c54bfb.MotoGPNFTStorefront
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}