Create an NFT Listing
Create an NFT Listing
14 Oct 2022
Contributed by Flow Blockchain
List an NFT to be sold using the NFTStorefront contract.
Smart Contract Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
pub contract NFTStorefront {
//More NFTStorefront contract code above
....
pub struct SaleCut {
// The receiver for the payment.
// Note that we do not store an address to find the Vault that this represents,
// as the link or resource that we fetch in this way may be manipulated,
// so to find the address that a cut goes to you must get this struct and then
// call receiver.borrow()!.owner.address on it.
// This can be done efficiently in a script.
pub let receiver: Capability<&{FungibleToken.Receiver}>
// The amount of the payment FungibleToken that will be paid to the receiver.
pub let amount: UFix64
// initializer
//
init(receiver: Capability<&{FungibleToken.Receiver}>, amount: UFix64) {
self.receiver = receiver
self.amount = amount
}
}
// ListingDetails
// A struct containing a Listing's data.
//
pub struct ListingDetails {
// The Storefront that the Listing is stored in.
// Note that this resource cannot be moved to a different Storefront,
// so this is OK. If we ever make it so that it *can* be moved,
// this should be revisited.
pub var storefrontID: UInt64
// Whether this listing has been purchased or not.
pub var purchased: Bool
// The Type of the NonFungibleToken.NFT that is being listed.
pub let nftType: Type
// The ID of the NFT within that type.
pub let nftID: UInt64
// The Type of the FungibleToken that payments must be made in.
pub let salePaymentVaultType: Type
// The amount that must be paid in the specified FungibleToken.
pub let salePrice: UFix64
// This specifies the division of payment between recipients.
pub let saleCuts: [SaleCut]
// setToPurchased
// Irreversibly set this listing as purchased.
//
access(contract) fun setToPurchased() {
self.purchased = true
}
// initializer
//
init (
nftType: Type,
nftID: UInt64,
salePaymentVaultType: Type,
saleCuts: [SaleCut],
storefrontID: UInt64
) {
self.storefrontID = storefrontID
self.purchased = false
self.nftType = nftType
self.nftID = nftID
self.salePaymentVaultType = salePaymentVaultType
// Store the cuts
assert(saleCuts.length > 0, message: "Listing must have at least one payment cut recipient")
self.saleCuts = saleCuts
// Calculate the total price from the cuts
var salePrice = 0.0
// Perform initial check on capabilities, and calculate sale price from cut amounts.
for cut in self.saleCuts {
// Make sure we can borrow the receiver.
// We will check this again when the token is sold.
cut.receiver.borrow()
?? panic("Cannot borrow receiver")
// Add the cut amount to the total price
salePrice = salePrice + cut.amount
}
assert(salePrice > 0.0, message: "Listing must have non-zero price")
// Store the calculated sale price
self.salePrice = salePrice
}
}
.....
pub resource Listing: ListingPublic {
// The simple (non-Capability, non-complex) details of the sale
access(self) let details: ListingDetails
// A capability allowing this resource to withdraw the NFT with the given ID from its collection.
// This capability allows the resource to withdraw *any* NFT, so you should be careful when giving
// such a capability to a resource and always check its code to make sure it will use it in the
// way that it claims.
access(contract) let nftProviderCapability: Capability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>
// borrowNFT
// This will assert in the same way as the NFT standard borrowNFT()
// if the NFT is absent, for example if it has been sold via another listing.
//
pub fun borrowNFT(): &NonFungibleToken.NFT {
let ref = self.nftProviderCapability.borrow()!.borrowNFT(id: self.getDetails().nftID)
//- CANNOT DO THIS IN PRECONDITION: "member of restricted type is not accessible: isInstance"
// result.isInstance(self.getDetails().nftType): "token has wrong type"
assert(ref.isInstance(self.getDetails().nftType), message: "token has wrong type")
assert(ref.id == self.getDetails().nftID, message: "token has wrong ID")
return ref as &NonFungibleToken.NFT
}
// getDetails
// Get the details of the current state of the Listing as a struct.
// This avoids having more public variables and getter methods for them, and plays
// nicely with scripts (which cannot return resources).
//
pub fun getDetails(): ListingDetails {
return self.details
}
// purchase
// Purchase the listing, buying the token.
// This pays the beneficiaries and returns the token to the buyer.
//
pub fun purchase(payment: @FungibleToken.Vault): @NonFungibleToken.NFT {
pre {
self.details.purchased == false: "listing has already been purchased"
payment.isInstance(self.details.salePaymentVaultType): "payment vault is not requested fungible token"
payment.balance == self.details.salePrice: "payment vault does not contain requested price"
}
// Make sure the listing cannot be purchased again.
self.details.setToPurchased()
// Fetch the token to return to the purchaser.
let nft <-self.nftProviderCapability.borrow()!.withdraw(withdrawID: self.details.nftID)
// Neither receivers nor providers are trustworthy, they must implement the correct
// interface but beyond complying with its pre/post conditions they are not gauranteed
// to implement the functionality behind the interface in any given way.
// Therefore we cannot trust the Collection resource behind the interface,
// and we must check the NFT resource it gives us to make sure that it is the correct one.
assert(nft.isInstance(self.details.nftType), message: "withdrawn NFT is not of specified type")
assert(nft.id == self.details.nftID, message: "withdrawn NFT does not have specified ID")
// Rather than aborting the transaction if any receiver is absent when we try to pay it,
// we send the cut to the first valid receiver.
// The first receiver should therefore either be the seller, or an agreed recipient for
// any unpaid cuts.
var residualReceiver: &{FungibleToken.Receiver}? = nil
// Pay each beneficiary their amount of the payment.
for cut in self.details.saleCuts {
if let receiver = cut.receiver.borrow() {
let paymentCut <- payment.withdraw(amount: cut.amount)
receiver.deposit(from: <-paymentCut)
if (residualReceiver == nil) {
residualReceiver = receiver
}
}
}
assert(residualReceiver != nil, message: "No valid payment receivers")
// At this point, if all recievers were active and availabile, then the payment Vault will have
// zero tokens left, and this will functionally be a no-op that consumes the empty vault
residualReceiver!.deposit(from: <-payment)
// If the listing is purchased, we regard it as completed here.
// Otherwise we regard it as completed in the destructor.
emit ListingCompleted(
listingResourceID: self.uuid,
storefrontResourceID: self.details.storefrontID,
purchased: self.details.purchased
)
return <-nft
}
// destructor
//
destroy () {
// If the listing has not been purchased, we regard it as completed here.
// Otherwise we regard it as completed in purchase().
// This is because we destroy the listing in Storefront.removeListing()
// or Storefront.cleanup() .
// If we change this destructor, revisit those functions.
if !self.details.purchased {
emit ListingCompleted(
listingResourceID: self.uuid,
storefrontResourceID: self.details.storefrontID,
purchased: self.details.purchased
)
}
}
// initializer
//
init (
nftProviderCapability: Capability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>,
nftType: Type,
nftID: UInt64,
salePaymentVaultType: Type,
saleCuts: [SaleCut],
storefrontID: UInt64
) {
// Store the sale information
self.details = ListingDetails(
nftType: nftType,
nftID: nftID,
salePaymentVaultType: salePaymentVaultType,
saleCuts: saleCuts,
storefrontID: storefrontID
)
// Store the NFT provider
self.nftProviderCapability = nftProviderCapability
// Check that the provider contains the NFT.
// We will check it again when the token is sold.
// We cannot move this into a function because initializers cannot call member functions.
let provider = self.nftProviderCapability.borrow()
assert(provider != nil, message: "cannot borrow nftProviderCapability")
// This will precondition assert if the token is not available.
let nft = provider!.borrowNFT(id: self.details.nftID)
assert(nft.isInstance(self.details.nftType), message: "token is not of specified type")
assert(nft.id == self.details.nftID, message: "token does not have specified ID")
}
}
....
pub resource Storefront : StorefrontManager, StorefrontPublic {
// The dictionary of Listing uuids to Listing resources.
access(self) var listings: @{UInt64: Listing}
// insert
// Create and publish a Listing for an NFT.
//
pub fun createListing(
nftProviderCapability: Capability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>,
nftType: Type,
nftID: UInt64,
salePaymentVaultType: Type,
saleCuts: [SaleCut]
): UInt64 {
let listing <- create Listing(
nftProviderCapability: nftProviderCapability,
nftType: nftType,
nftID: nftID,
salePaymentVaultType: salePaymentVaultType,
saleCuts: saleCuts,
storefrontID: self.uuid
)
let listingResourceID = listing.uuid
let listingPrice = listing.getDetails().salePrice
// Add the new listing to the dictionary.
let oldListing <- self.listings[listingResourceID] <- listing
// Note that oldListing will always be nil, but we have to handle it.
destroy oldListing
emit ListingAvailable(
storefrontAddress: self.owner?.address!,
listingResourceID: listingResourceID,
nftType: nftType,
nftID: nftID,
ftVaultType: salePaymentVaultType,
price: listingPrice
)
return listingResourceID
}
.....
}
//More NFTStorefront contract code below
}
When creating an NFT listing it will need a few parameters passed in, in order to make the listing go through.
First you will need to pass in a capability to the NFT that allows us to withdraw the NFT when it is being sold to the purchaser. Remember to link the capability in your private storage path as you do not want anyone to be able to withdraw any NFT from your account!
You'll also need to pass in the type of NFT that is being sold, as well as the NFT ID that is being sold. Afterwards you pass in the payment vault type for whichever crypto currency users will be purchasing the listing in.
You'll also include a struct called SaleCut. These Sales Cuts will determine the total price of your item. Let's say you want to have a marketplace fee implemented in your transaction. You would include a sales cut struct that would be passed in, as well as a sales cut that will go to the seller of the item. You can check out the example for multiple sales cuts to see how this would be implemented.
Lastly you would pass in the storefrontID which is the ID of your storefront containing the listing. Once all of this information is passed in, you have created a listing in your Storefront.
Afterwards you would link the public capability to the ListingPublic interface so that anyone can call the purchase function and buy your NFT.
Transaction Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import NFTStorefront from 0x03
import NonFungibleToken from 0x02
import ExampleNFT from 0x04
import FungibleToken from 0x01
// This transaction sets up account 0x01 for the marketplace tutorial
// by publishing a Vault reference and creating an empty NFT Collection.
transaction {
let storefront: &NFTStorefront.Storefront
let exampleNFTProvider: Capability<&ExampleNFT.Collection{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>
let tokenReceiver: Capability<&FungibleToken.Vault{FungibleToken.Receiver}>
prepare(acct: AuthAccount) {
self.storefront = acct.borrow<&NFTStorefront.Storefront>(from: NFTStorefront.StorefrontStoragePath) ?? panic("can't borrow storefront")
if acct.getCapability<&ExampleNFT.Collection{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>(ExampleNFT.CollectionPrivatePath).check() == false {
acct.link<&ExampleNFT.Collection{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>(ExampleNFT.CollectionPrivatePath, target: ExampleNFT.CollectionStoragePath)
}
self.exampleNFTProvider = acct.getCapability<&ExampleNFT.Collection{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>(ExampleNFT.CollectionPrivatePath)!
assert(self.exampleNFTProvider.borrow() != nil, message: "Missing or mis-typed ExampleNFT.Collection provider")
self.tokenReceiver = acct.getCapability<&FungibleToken.Vault{FungibleToken.Receiver}>(/public/MainReceiver)!
assert(self.tokenReceiver.borrow() != nil, message: "Missing or mis-typed FlowToken receiver")
let saleCut = NFTStorefront.SaleCut(
receiver: self.tokenReceiver,
amount: 10.0
)
self.storefront.createListing(
nftProviderCapability: self.exampleNFTProvider,
nftType: Type<@NonFungibleToken.NFT>(),
nftID: 0,
salePaymentVaultType: Type<@FungibleToken.Vault>(),
saleCuts: [saleCut]
)
log("storefront listing created")
}
}
Here we are defining initially the types that we are expected to return for each of the listed variables.
Firstly we borrow the storefront from the account that contains a storefront in it. Afterwards we need to get the capability to withdraw NFTS from the account so that the storefront can withdraw the correct NFT of the correct Type.
After that, we'll also get the Vault receiver capability so that it can be included in the sales cut struct. That way when it's time to pay out the earnings from the NFT, this vault will receive its cut.
Once that's all done, we take self.storefront and call the createListing function and pass in all the parameters need. Then we execute the transaction.
Up Next: Purchase NFT on Marketplace
67%