Smart Contract
TiresStorefront
A.f887ece39166906e.TiresStorefront
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}