Smart Contract

AFLMarketplaceV2

A.8f9231920da9af6d.AFLMarketplaceV2

Valid From

84,649,905

Deployed

1d ago
Feb 26, 2026, 09:00:49 PM UTC

Dependents

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