Smart Contract
Market
A.c38aea683c0c4d38.Market
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