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