Smart Contract

Market

A.c38aea683c0c4d38.Market

Deployed

2h ago
Mar 01, 2026, 05:18:34 PM UTC

Dependents

0 imports
1/*
2    This contract is mostly copied from the MarketTopShot contract but with
3    modifications to integrate with Eternal's influencer system, such that
4    influencers receive cuts from transactions that take place on the marketplace.
5*/
6
7import FungibleToken from 0xf233dcee88fe0abe
8import NonFungibleToken from 0x1d7e57aa55817448
9import Eternal from 0xc38aea683c0c4d38
10import InfluencerRegistry from 0xc38aea683c0c4d38
11
12pub contract Market {
13
14    // -----------------------------------------------------------------------
15    // Eternal Market contract Event definitions
16    // -----------------------------------------------------------------------
17
18    // emitted when a Eternal moment is listed for sale
19    pub event MomentListed(id: UInt64, price: UFix64, seller: Address?)
20    // emitted when the price of a listed moment has changed
21    pub event MomentPriceChanged(id: UInt64, newPrice: UFix64, seller: Address?)
22    // emitted when a token is purchased from the market
23    pub event MomentPurchased(id: UInt64, price: UFix64, seller: Address?)
24    // emitted when a moment has been withdrawn from the sale
25    pub event MomentWithdrawn(id: UInt64, owner: Address?)
26    // emitted when the cut percentage of the sale has been changed by the owner
27    pub event CutPercentageChanged(newPercent: UFix64, seller: Address?)
28    // emitted when an influencer has received a cut
29    pub event InfluencerCutReceived(name: String, ftType: Type, cut: UFix64)
30
31    // SalePublic 
32    //
33    // The interface that a user can publish a capability to their sale
34    // to allow others to access their sale
35    pub resource interface SalePublic {
36        pub var cutPercentage: UFix64
37        pub fun purchase(tokenID: UInt64, buyTokens: @FungibleToken.Vault): @Eternal.NFT {
38            post {
39                result.id == tokenID: "The ID of the withdrawn token must be the same as the requested ID"
40            }
41        }
42        pub fun getPrice(tokenID: UInt64): UFix64?
43        pub fun getIDs(): [UInt64]
44        pub fun borrowMoment(id: UInt64): &Eternal.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    // This is the main resource that token sellers will store in their account
57    // to manage the NFTs that they are selling. The SaleCollection
58    // holds a Eternal Collection resource to store the moments that are for sale.
59    // The SaleCollection also keeps track of the price of each token.
60    // 
61    // When a token is purchased, a cut is taken from the tokens
62    // and sent to the beneficiary, then the rest are sent to the seller.
63    //
64    // The seller chooses who the beneficiary is and what percentage
65    // of the tokens gets taken from the purchase
66    pub resource SaleCollection: SalePublic {
67
68        // A collection of the moments that the user has for sale
69        access(self) var forSale: @Eternal.Collection
70
71        // Dictionary of the low low prices for each NFT by ID
72        access(self) var prices: {UInt64: UFix64}
73
74        // The fungible token vault of the seller
75        // so that when someone buys a token, the tokens are deposited
76        // to this Vault
77        access(self) var ownerCapability: Capability
78
79        // The capability that is used for depositing 
80        // the beneficiary's cut of every sale
81        access(self) var beneficiaryCapability: Capability
82
83        // The percentage that is taken from every purchase for the beneficiary
84        // For example, if the percentage is 15%, cutPercentage = 0.15
85        pub var cutPercentage: UFix64
86
87        // The fungible token that should be used to transact with this
88        // sale collection.
89        pub var ftType: Type
90
91        init (ftType: Type, ownerCapability: Capability, beneficiaryCapability: Capability, cutPercentage: UFix64) {
92            pre {
93                // Check that both capabilities are for fungible token Vault receivers
94                ownerCapability.borrow<&{FungibleToken.Receiver}>()!.isInstance(ftType): 
95                    "Owner's Receiver Capability is invalid!"
96                beneficiaryCapability.borrow<&{FungibleToken.Receiver}>()!.isInstance(ftType): 
97                    "Beneficiary's Receiver Capability is invalid!" 
98            }
99
100            // create an empty collection to store the moments that are for sale
101            self.forSale <- Eternal.createEmptyCollection() as! @Eternal.Collection
102            self.ownerCapability = ownerCapability
103            self.beneficiaryCapability = beneficiaryCapability
104            // prices are initially empty because there are no moments for sale
105            self.prices = {}
106            self.cutPercentage = cutPercentage
107            self.ftType = ftType
108        }
109
110        // listForSale lists an NFT for sale in this sale collection
111        // at the specified price
112        //
113        // Parameters: token: The NFT to be put up for sale
114        //             price: The price of the NFT
115        pub fun listForSale(token: @Eternal.NFT, price: UFix64) {
116
117            // get the ID of the token
118            let id = token.id
119
120            // Set the token's price
121            self.prices[token.id] = price
122
123            // Deposit the token into the sale collection
124            self.forSale.deposit(token: <-token)
125
126            emit MomentListed(id: id, price: price, seller: self.owner?.address)
127        }
128
129        // Withdraw removes a moment that was listed for sale
130        // and clears its price
131        //
132        // Parameters: tokenID: the ID of the token to withdraw from the sale
133        //
134        // Returns: @Eternal.NFT: The nft that was withdrawn from the sale
135        pub fun withdraw(tokenID: UInt64): @Eternal.NFT {
136
137            // Remove and return the token.
138            // Will revert if the token doesn't exist
139            let token <- self.forSale.withdraw(withdrawID: tokenID) as! @Eternal.NFT
140
141            // Remove the price from the prices dictionary
142            self.prices.remove(key: tokenID)
143
144            // Set prices to nil for the withdrawn ID
145            self.prices[tokenID] = nil
146            
147            // Emit the event for withdrawing a moment from the Sale
148            emit MomentWithdrawn(id: token.id, owner: self.owner?.address)
149
150            // Return the withdrawn token
151            return <-token
152        }
153
154        // purchase lets a user send tokens to purchase an NFT that is for sale
155        // the purchased NFT is returned to the transaction context that called it
156        //
157        // Parameters: tokenID: the ID of the NFT to purchase
158        //             butTokens: the fungible tokens that are used to buy the NFT
159        //
160        // Returns: @Eternal.NFT: the purchased NFT
161        pub fun purchase(tokenID: UInt64, buyTokens: @FungibleToken.Vault): @Eternal.NFT {
162            pre {
163                self.forSale.ownedNFTs[tokenID] != nil && self.prices[tokenID] != nil:
164                    "No token matching this ID for sale!"           
165                buyTokens.isInstance(self.ftType): "payment vault is not requested fungible token"
166                buyTokens.balance == (self.prices[tokenID] ?? UFix64(0)):
167                    "Not enough tokens to buy the NFT!"
168            }
169
170            // Read the price for the token
171            let price = self.prices[tokenID]!
172
173            // Set the price for the token to nil
174            self.prices[tokenID] = nil
175
176            // Return the purchased token
177            let nft <- self.withdraw(tokenID: tokenID)
178
179            // Find the influencer name
180            let influencerName = self.getInfluencerNameFromMoment(moment: &nft as &Eternal.NFT)
181
182            // Withdraw the influener cut
183            let influencerCutPercentage = InfluencerRegistry.getCutPercentage(name: influencerName)
184            let influencerCutAmount = price*influencerCutPercentage
185            let influencerCut <- buyTokens.withdraw(amount: influencerCutAmount)
186
187            // Deposit the influencer cut
188            let influencerCap = InfluencerRegistry.getCapability(name: influencerName, ftType: self.ftType)
189                ?? panic("Cannot find the influencer in the registry")
190            let influencerReceiverRef = influencerCap.borrow<&{FungibleToken.Receiver}>()
191                ?? panic("Cannot find a token receiver for the influencer")
192            influencerReceiverRef.deposit(from: <-influencerCut)
193            emit InfluencerCutReceived(name: influencerName, ftType: self.ftType, cut: influencerCutAmount)
194
195            // Withdraw the beneficiary cut
196            let beneficiaryCut <- buyTokens.withdraw(amount: price*self.cutPercentage)
197
198            // Deposit the beneficiary Vault
199            self.beneficiaryCapability.borrow<&{FungibleToken.Receiver}>()!
200                .deposit(from: <-beneficiaryCut)
201
202            // Deposit the remaining tokens into the owners vault
203            self.ownerCapability.borrow<&{FungibleToken.Receiver}>()!
204                .deposit(from: <-buyTokens)
205
206            emit MomentPurchased(id: tokenID, price: price, seller: self.owner?.address)
207            return <-nft
208        }
209
210        access(self) fun getInfluencerNameFromMoment(moment: &Eternal.NFT): String {
211            let playID = moment.data.playID
212            let metadata = Eternal.getPlayMetaData(playID: playID)
213            return metadata!["Influencer"]!
214        }
215
216        // changePrice changes the price of a token that is currently for sale
217        //
218        // Parameters: tokenID: The ID of the NFT's price that is changing
219        //             newPrice: The new price for the NFT
220        pub fun changePrice(tokenID: UInt64, newPrice: UFix64) {
221            pre {
222                self.prices[tokenID] != nil: "Cannot change the price for a token that is not for sale"
223            }
224            // Set the new price
225            self.prices[tokenID] = newPrice
226
227            emit MomentPriceChanged(id: tokenID, newPrice: newPrice, seller: self.owner?.address)
228        }
229
230        // changePercentage changes the cut percentage of the tokens that are for sale
231        //
232        // Parameters: newPercent: The new cut percentage for the sale
233        pub fun changePercentage(_ newPercent: UFix64) {
234            self.cutPercentage = newPercent
235
236            emit CutPercentageChanged(newPercent: newPercent, seller: self.owner?.address)
237        }
238
239        // changeOwnerReceiver updates the capability for the sellers fungible token Vault
240        //
241        // Parameters: newOwnerCapability: The new fungible token capability for the account 
242        //                                 who received tokens for purchases
243        pub fun changeOwnerReceiver(_ newOwnerCapability: Capability) {
244            pre {
245                newOwnerCapability.borrow<&{FungibleToken.Receiver}>() != nil: 
246                    "Owner's Receiver Capability is invalid!"
247            }
248            self.ownerCapability = newOwnerCapability
249        }
250
251        // changeBeneficiaryReceiver updates the capability for the beneficiary of the cut of the sale
252        //
253        // Parameters: newBeneficiaryCapability the new capability for the beneficiary of the cut of the sale
254        //
255        pub fun changeBeneficiaryReceiver(_ newBeneficiaryCapability: Capability) {
256            pre {
257                newBeneficiaryCapability.borrow<&{FungibleToken.Receiver}>() != nil: 
258                    "Beneficiary's Receiver Capability is invalid!" 
259            }
260            self.beneficiaryCapability = newBeneficiaryCapability
261        }
262
263        // getPrice returns the price of a specific token in the sale
264        // 
265        // Parameters: tokenID: The ID of the NFT whose price to get
266        //
267        // Returns: UFix64: The price of the token
268        pub fun getPrice(tokenID: UInt64): UFix64? {
269            return self.prices[tokenID]
270        }
271
272        // getIDs returns an array of token IDs that are for sale
273        pub fun getIDs(): [UInt64] {
274            return self.forSale.getIDs()
275        }
276
277        // borrowMoment Returns a borrowed reference to a Moment in the collection
278        // so that the caller can read data from it
279        //
280        // Parameters: id: The ID of the moment to borrow a reference to
281        //
282        // Returns: &Eternal.NFT? Optional reference to a moment for sale 
283        //                        so that the caller can read its data
284        //
285        pub fun borrowMoment(id: UInt64): &Eternal.NFT? {
286            let ref = self.forSale.borrowMoment(id: id)
287            return ref
288        }
289
290        // If the sale collection is destroyed, 
291        // destroy the tokens that are for sale inside of it
292        destroy() {
293            destroy self.forSale
294        }
295    }
296
297    // createCollection returns a new collection resource to the caller
298    pub fun createSaleCollection(ftType: Type, ownerCapability: Capability, beneficiaryCapability: Capability, cutPercentage: UFix64): @SaleCollection {
299        return <- create SaleCollection(ftType: ftType, ownerCapability: ownerCapability, beneficiaryCapability: beneficiaryCapability, cutPercentage: cutPercentage)
300    }
301}
302