Smart Contract
AFLMarketplaceV2
A.8f9231920da9af6d.AFLMarketplaceV2
1// This is identical to AFLMarketplace.cdc but using the wrapped USDCFlow instead of FiatToken
2
3import FungibleToken from 0xf233dcee88fe0abe
4import NonFungibleToken from 0x1d7e57aa55817448
5import AFLNFT from 0x8f9231920da9af6d
6import USDCFlow from 0xf1ab99c82dee3526 // from 0x64adf39cbc354fcb // testnet
7import StorageHelper from 0x8f9231920da9af6d
8import Burner from 0xf233dcee88fe0abe
9
10access(all) contract AFLMarketplaceV2 {
11 // Owner entitlement required to access ListForSale, Withdraw or ChangePrice functions
12 access(all) entitlement Owner
13
14 // Capability to receive USDC marketplace fee from each sale
15 access(contract) var marketplaceWallet: Capability<&USDCFlow.Vault>
16 // Market fee percentage
17 access(contract) var cutPercentage : UFix64
18
19 // commented out on testnet .... there on mainnet!
20
21 // Storage Path for Admin resource
22 access(all) let AdminStoragePath: StoragePath
23 // Storage Path for SaleCollection resource
24 access(all) let SaleCollectionStoragePath: StoragePath
25 // Storage Path for SalePublic resource
26 access(all) let SaleCollectionPublicPath: PublicPath
27
28 ////////////
29 // EVENTS //
30 ////////////
31 // Emitted when a new AFLNFT is put up for sale
32 access(all) event ForSale(id: UInt64, price: UFix64, owner: Address?)
33 // Emitted when the price of an NFT is changed
34 access(all) event PriceChanged(id: UInt64, newPrice: UFix64, owner: Address?)
35 // Emitted when a token is purchased
36 access(all) event TokenPurchased(id: UInt64, price: UFix64, owner: Address?, to: Address?)
37 // Emitted when a seller withdraws their NFT from the sale
38 access(all) event SaleCanceled(id: UInt64, owner: Address?)
39 // Emitted when the cut percentage of the sale has been changed by the owner
40 access(all) event CutPercentageChanged(newPercent: UFix64, owner: Address?)
41 // Emitted when a new sale collection is created
42 access(all) event SaleCollectionCreated(owner: Address?)
43 // Emitted when marketplace wallet is changed
44 access(all) event MarketplaceWalletChanged(address: Address)
45
46
47 // SalePublic
48 //
49 // The interface that a user can publish a capability to their sale
50 // to allow others to access their sale
51 access(all)resource interface SalePublic {
52 access(all) view fun getPrice(tokenID: UInt64): UFix64?
53 access(all) view fun getIDs(): [UInt64]
54 access(all) view fun getDetails(): {UInt64: UFix64}
55 access(all) fun purchase(tokenID: UInt64, recipientCap: Capability<&{AFLNFT.AFLNFTCollectionPublic}>, buyTokens: @{FungibleToken.Vault})
56 access(all) fun borrowMoment(id: UInt64): &AFLNFT.NFT? {
57 // If the result isn't nil, the id of the returned reference
58 // should be the same as the argument to the function
59 post {
60 (result == nil) || (result?.id == id):
61 "Cannot borrow Moment reference: The ID of the returned reference is incorrect"
62 }
63 }
64 }
65
66 // SaleCollection
67 //
68 // NFT Collection object that allows a user to put their NFT up for sale
69 // where others can send fungible tokens to purchase it
70 //
71 access(all) resource SaleCollection: SalePublic {
72
73 // Dictionary of the NFTs that the user is putting up for sale
74 access(self) var forSale: @{UInt64: AFLNFT.NFT}
75
76 // Dictionary of the flow prices for each NFT by ID
77 access(self) var prices: {UInt64: UFix64}
78
79 // The fungible token vault of the owner of this sale.
80 // When someone buys a token, this resource can deposit
81 // tokens into their account.
82 access(self) let ownerVault: Capability<&USDCFlow.Vault>
83
84 init (vault: Capability<&USDCFlow.Vault>) {
85 pre {
86 // Check that both capabilities are for fungible token Vault receivers
87 vault.check():
88 "Owner's Receiver Capability is invalid!"
89 }
90
91 // create an empty collection to store the moments that are for sale
92 self.forSale <- {}
93 self.ownerVault = vault
94 // prices are initially empty because there are no moments for sale
95 self.prices = {}
96 }
97
98
99 // purchase lets a user send tokens to purchase an NFT that is for sale
100 // the purchased NFT is returned to the transaction context that called it
101 //
102 // Parameters: tokenID: the ID of the NFT to purchase
103 // butTokens: the fungible tokens that are used to buy the NFT
104
105 access(all) fun purchase(tokenID: UInt64, recipientCap: Capability<&{AFLNFT.AFLNFTCollectionPublic}>, buyTokens: @{FungibleToken.Vault}) {
106 pre {
107 buyTokens.getType() == self.ownerVault.borrow()!.getType():
108 "The tokens being sent to purchase the NFT must be the same type as the listing"
109 self.forSale[tokenID] != nil && self.prices[tokenID] != nil:
110 "No token matching this ID for sale!"
111 buyTokens.balance >= (self.prices[tokenID] ?? 0.0):
112 "Not enough tokens to buy the NFT!"
113 }
114
115 StorageHelper.topUpAccount(address: recipientCap.address)
116
117 let recipient: &{AFLNFT.AFLNFTCollectionPublic} = recipientCap.borrow()!
118 // Read the price for the token
119 let salePrice: UFix64 = self.prices[tokenID]!
120
121 // Set the price for the token to nil
122 self.prices[tokenID] = nil
123
124 let saleOwnerVaultRef: &USDCFlow.Vault = self.ownerVault.borrow() ?? panic("could not borrow reference to the owner vault")
125
126 // remove price
127 self.prices.remove(key: tokenID)
128 // remove and return the token
129 let token: @AFLNFT.NFT <- self.forSale.remove(key: tokenID) ?? panic("missing NFT")
130
131 let marketplaceWallet: &USDCFlow.Vault = AFLMarketplaceV2.marketplaceWallet.borrow() ?? panic("Couldn't borrow Vault reference")
132 let marketplaceAmount: UFix64 = salePrice * AFLMarketplaceV2.cutPercentage
133
134 // withdraw and deposit marketplace fee
135 let tempMarketplaceWallet: @{FungibleToken.Vault} <- buyTokens.withdraw(amount: marketplaceAmount)
136 marketplaceWallet.deposit(from: <- tempMarketplaceWallet)
137
138 // deposit remaining tokens to sale owner and transfer nft to recipient
139 saleOwnerVaultRef.deposit(from: <- buyTokens)
140 recipient.deposit(token: <- token)
141
142 emit TokenPurchased(id: tokenID, price: salePrice, owner: self.owner?.address, to: recipient.owner!.address)
143 }
144
145 // listForSale lists an NFT for sale in this sale collection
146 // at the specified price
147 //
148 // Parameters: token: The NFT to be put up for sale
149 // price: The price of the NFT
150 access(Owner) fun listForSale(token: @AFLNFT.NFT, price: UFix64) {
151
152 // get the ID of the token
153 let id: UInt64 = token.id
154
155 // get the templateID
156 let templateID: UInt64 = AFLNFT.getNFTData(nftId: id).templateId
157
158 let teamBadgeIds: [UInt64] = [22436, 22437, 22438, 22439, 22440, 22441, 22442, 22443, 22444, 22445, 22446, 22447, 22448, 22449, 22450, 22451, 22452, 22453] // mainnet templateIds for team badges
159 let wrongRookies: [UInt64] = [34609, 34610, 34611, 34612, 34613, 34614, 34615, 34616, 34617, 34618, 34619, 34620, 34621, 34622]
160 assert(!teamBadgeIds.contains(templateID), message: "Team Badges cannot be listed for sale.")
161 assert(!wrongRookies.contains(templateID), message: "Rookies cannot be listed for sale.")
162
163 // Set the token's price
164 self.prices[token.id] = price
165
166 let oldToken: @AFLNFT.NFT? <- self.forSale[id] <- token
167
168 Burner.burn(<-oldToken)
169
170 emit ForSale(id: id, price: price, owner: self.owner?.address)
171 }
172
173 // Withdraw removes a moment that was listed for sale
174 // and clears its price
175 //
176 // Parameters: tokenID: the ID of the token to withdraw from the sale
177 //
178 // Returns: @AFLNFT.NFT: The nft that was withdrawn from the sale
179 access(Owner) fun withdraw(tokenID: UInt64): @AFLNFT.NFT {
180 // remove the price
181 self.prices.remove(key: tokenID)
182 // remove and return the token
183 let token: @AFLNFT.NFT <- self.forSale.remove(key: tokenID) ?? panic("missing NFT")
184
185 emit SaleCanceled(id: tokenID, owner: self.owner!.address)
186 return <-token
187 }
188
189
190 // changePrice changes the price of a token that is currently for sale
191 //
192 // Parameters: tokenID: The ID of the NFT's price that is changing
193 // newPrice: The new price for the NFT
194 access(Owner) fun changePrice(tokenID: UInt64, newPrice: UFix64) {
195 pre {
196 self.prices[tokenID] != nil: "Cannot change the price for a token that is not for sale"
197 }
198 // Set the new price
199 self.prices[tokenID] = newPrice
200
201 emit PriceChanged(id: tokenID, newPrice: newPrice, owner: self.owner?.address)
202 }
203
204
205 // getPrice returns the price of a specific token in the sale
206 //
207 // Parameters: tokenID: The ID of the NFT whose price to get
208 //
209 // Returns: UFix64: The price of the token
210 access(all) view fun getPrice(tokenID: UInt64): UFix64? {
211 return self.prices[tokenID]
212 }
213
214 /// getDetails returns the prices of all tokens listed for sale
215 access(all) view fun getDetails(): {UInt64: UFix64} {
216 return self.prices
217 }
218
219 // getIDs returns an array of token IDs that are for sale
220 access(all) view fun getIDs(): [UInt64] {
221 return self.forSale.keys
222 }
223
224 // borrowMoment Returns a borrowed reference to a Moment in the collection
225 // so that the caller can read data from it
226 //
227 // Parameters: id: The ID of the moment to borrow a reference to
228 //
229 // Returns: &AFL.NFT? Optional reference to a moment for sale
230 // so that the caller can read its data
231 //
232 access(all) view fun borrowMoment(id: UInt64): &AFLNFT.NFT? {
233 if self.forSale[id] != nil{
234 return (&self.forSale[id] as &AFLNFT.NFT?)!
235 }
236 else {
237 return nil
238 }
239 }
240
241 // If the sale collection is destroyed,
242 // destroy the tokens that are for sale inside of it
243 // destroy() {
244 // destroy self.forSale
245 // }
246 }
247
248 // createCollection returns a new collection resource to the caller
249 access(all) fun createSaleCollection(ownerVault: Capability<&USDCFlow.Vault>): @SaleCollection {
250 emit SaleCollectionCreated(owner: ownerVault.address)
251 return <- create SaleCollection(vault: ownerVault)
252 }
253
254 access(all) resource AFLMarketAdmin {
255 // changePercentage changes the cut percentage of the tokens that are for sale
256 //
257 // Parameters: newPercent: The new cut percentage for the sale
258 access(all) fun changePercentage(_ newPercent: UFix64) {
259 pre {
260 newPercent <= 1.0: "Cannot set cut percentage to greater than 100%"
261 }
262 AFLMarketplaceV2.cutPercentage = newPercent
263 emit CutPercentageChanged(newPercent: newPercent, owner: self.owner!.address)
264 }
265
266 access(all) fun changeMarketplaceWallet(_ newCap: Capability<&USDCFlow.Vault>) {
267 AFLMarketplaceV2.marketplaceWallet = newCap
268 emit MarketplaceWalletChanged(address: newCap.address)
269 }
270 }
271
272 access(all) view fun getPercentage(): UFix64 {
273 return AFLMarketplaceV2.cutPercentage
274 }
275
276 init(){
277 self.cutPercentage = 0.10
278
279 self.AdminStoragePath = /storage/AFLMarketAdminV2
280 self.SaleCollectionStoragePath = /storage/AFLMarketplaceSaleCollectionV2
281 self.SaleCollectionPublicPath = /public/AFLMarketplaceSaleCollectionV2
282
283 self.marketplaceWallet = self.account.capabilities.get<&USDCFlow.Vault>(USDCFlow.ReceiverPublicPath)
284 assert(self.marketplaceWallet.check(), message: "Marketplace wallet is not a valid receiver")
285
286 self.account.storage.save(<- create AFLMarketAdmin(), to: AFLMarketplaceV2.AdminStoragePath) // /storage/AFLMarketAdmin
287 }
288}
289