Smart Contract

AFLMarketplace

A.8f9231920da9af6d.AFLMarketplace

Valid From

84,588,062

Deployed

1d ago
Feb 26, 2026, 10:18:21 PM UTC

Dependents

0 imports
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