Smart Contract
SolarpupsMarket
A.a8d493db1bb4df56.SolarpupsMarket
1import NonFungibleToken from 0x1d7e57aa55817448
2import SolarpupsNFT from 0xa8d493db1bb4df56
3import FungibleToken from 0xf233dcee88fe0abe
4import FlowToken from 0x1654653399040a61
5
6/*
7 * This contract is used to realize all kind of market sell activities within Solarpups.
8 * The market supports direct payments for custom assets. A SolarpupsCredit is used for
9 * all market activities so a buyer have to exchange his source currency in order to buy something.
10 *
11 * A market item is a custom asset which is offered by the token holder for sale. These items can either be
12 * already minted (list offering) or can be minted on the fly during the payment process handling (lazy offering).
13 * Lazy offerings are especially useful to rule a time based drop or an edition based drop with a hard supply cut after the drop.
14 *
15 * Each payment is divided into different shares for the platform, creator (royalty) and the owner of the asset.
16 */
17pub contract SolarpupsMarket {
18 pub event MarketItemLocked(assetId: String)
19 pub event MarketItemUnlocked(assetId: String)
20 pub event MarketItemInserted(assetId: String, owner: Address, price: UFix64)
21 pub event MarketItemRemoved (assetId: String, owner: Address)
22 pub event MarketItemSold(assetId: String, owner: Address, tokenIds: [UInt64])
23 pub event MarketItemSoldOut(assetId: String, owner: Address)
24 pub event MarketItemPayout(assetId: String, amount: UFix64)
25
26 pub let SolarpupsMarketStorePublicPath: PublicPath
27 pub let SolarpupsMarketAdminStoragePath: StoragePath
28 pub let SolarpupsMarketStoreStoragePath: StoragePath
29 pub let SolarpupsMarketTokenStoragePath: StoragePath
30
31 access(self) var totalPayments: UInt64
32
33 /**
34 * The resource interface definition for all payment implementations.
35 * A payment resource is used to buy a Solarpups asset, and it is created
36 * by an PaymentExchange resource.
37 */
38 pub resource Payment {
39 pub var amount: UFix64
40 pub let paymentVault: @FungibleToken.Vault
41
42 init(vault: @FungibleToken.Vault) {
43 SolarpupsMarket.totalPayments = SolarpupsMarket.totalPayments + (1 as UInt64)
44 self.paymentVault <- vault as! @FlowToken.Vault
45 self.amount = self.paymentVault.balance
46 }
47
48 pub fun split(_ amount: UFix64): @Payment {
49 pre { amount <= self.amount: "amount must be lower than or equal to payment amount" }
50 self.amount = self.amount - amount
51
52 return <- create Payment(vault: <- self.paymentVault.withdraw(amount: amount))
53 }
54
55 destroy() {
56 destroy self.paymentVault
57 }
58 }
59
60 /**
61 * Resource interface which can be used to read public information about a market item.
62 */
63 pub resource interface PublicMarketItem {
64 pub let assetId: String
65 pub var price: UFix64
66 pub fun getSupply(): Int
67 pub fun getLocked(): UInt64
68 pub fun getShares(): {Address:UFix64}
69 }
70
71 /**
72 * Resource interface for all nft offerings on the Solarpups market.
73 */
74 pub resource interface NFTOffering {
75 pub fun provide(): @NonFungibleToken.Collection
76 pub fun getSupply(): Int
77 pub fun lock()
78 pub fun unlock()
79 pub fun getReceiver(): Capability<&{FungibleToken.Receiver}>
80 pub fun getRoyaltyReceiver(): Capability<&{FungibleToken.Receiver}>
81 }
82
83 /**
84 * A ListOffering is a nft offering based on a list of already minted NFTs.
85 * These NFTs were directly handled out of the owners NFT collection.
86 */
87 pub resource ListOffering: NFTOffering {
88 pub let tokenIds: [UInt64]
89 pub let assetId: String
90 pub var locked: UInt64
91 access(self) let provider: Capability<&{NonFungibleToken.Provider}>
92 access(self) let receiver: Capability<&{FungibleToken.Receiver}>
93 access(self) let royaltyReceiver: Capability<&{FungibleToken.Receiver}>
94
95 pub fun provide(): @NonFungibleToken.Collection {
96 let sourceCollection = self.provider.borrow()!
97 let targetCollection <- SolarpupsNFT.createEmptyCollection()
98 let tokenId = self.tokenIds.removeFirst()
99 let token <- sourceCollection.withdraw(withdrawID: tokenId) as! @SolarpupsNFT.NFT
100
101 assert(token.data.assetId == self.assetId, message: "asset id mismatch")
102 targetCollection.deposit(token: <- token)
103 return <- targetCollection
104 }
105
106 pub fun getReceiver(): Capability<&{FungibleToken.Receiver}> {
107 return self.receiver
108 }
109
110 pub fun getRoyaltyReceiver(): Capability<&{FungibleToken.Receiver}> {
111 return self.royaltyReceiver
112 }
113
114 pub fun getSupply(): Int {
115 return self.tokenIds.length
116 }
117
118 pub fun lock() {
119 pre { self.tokenIds.length >= 1: "not enough elements to lock" }
120 self.locked = self.locked + (1 as UInt64)
121 }
122
123 pub fun unlock() {
124 pre { self.locked >= (1 as UInt64): "not enough elements to unlock" }
125 self.locked = self.locked - (1 as UInt64)
126 }
127
128 init(tokenIds: [UInt64], assetId: String, provider: Capability<&{NonFungibleToken.Provider}>, receiver: Capability<&{FungibleToken.Receiver}>, royaltyReceiver: Capability<&{FungibleToken.Receiver}>) {
129 pre {
130 provider.borrow() != nil: "Cannot borrow seller"
131 tokenIds.length > 0: "token ids must not be empty"
132 }
133 self.tokenIds = tokenIds
134 self.assetId = assetId
135 self.provider = provider
136 self.receiver = receiver
137 self.royaltyReceiver = royaltyReceiver
138 self.locked = 0
139 }
140 }
141
142 /**
143 * A LazyOffering is a nft offering based on a NFT minter resource which means that these NFTs
144 * are going to be minted only after a successful sale.
145 */
146 pub resource LazyOffering: NFTOffering {
147 pub let assetId: String
148 pub var locked: UInt64
149 pub let minter: @SolarpupsNFT.Minter
150 access(self) let receiver: Capability<&{FungibleToken.Receiver}>
151 access(self) let royaltyReceiver: Capability<&{FungibleToken.Receiver}>
152
153 pub fun provide(): @NonFungibleToken.Collection {
154 return <- self.minter.mint(assetId: self.assetId)
155 }
156
157 pub fun getReceiver(): Capability<&{FungibleToken.Receiver}> {
158 return self.receiver
159 }
160
161 pub fun getRoyaltyReceiver(): Capability<&{FungibleToken.Receiver}> {
162 return self.royaltyReceiver
163 }
164
165 pub fun getSupply(): Int {
166 let supply = SolarpupsNFT.getAsset(assetId: self.assetId)?.supply
167 let maxSupply = Int(supply!.max)
168 let curSupply = Int(supply!.cur)
169 return maxSupply - curSupply
170 }
171
172 pub fun lock() {
173 pre { self.getSupply() >= 1: "not enough elements to lock" }
174 self.locked = self.locked + (1 as UInt64)
175 }
176
177 pub fun unlock() {
178 pre { self.locked >= (1 as UInt64): "not enough elements to unlock" }
179 self.locked = self.locked - (1 as UInt64)
180 }
181
182 init(assetId: String, minter: @SolarpupsNFT.Minter, receiver: Capability<&{FungibleToken.Receiver}>, royaltyReceiver: Capability<&{FungibleToken.Receiver}>) {
183 self.assetId = assetId
184 self.minter <- minter
185 self.locked = 0
186 self.receiver = receiver
187 self.royaltyReceiver = royaltyReceiver
188 }
189
190 destroy() {
191 destroy self.minter
192 }
193 }
194
195 /**
196 * This resource represents a Solarpups asset for sale and can be offered based on a list of already minted NFT tokens
197 * or in a lazy manner where NFTs were only minted after a successful sale. The price of a market item can be changed.
198 */
199 pub resource MarketItem: PublicMarketItem {
200 pub let assetId: String
201 pub var price: UFix64
202 pub var locked: UInt64
203 access(self) let shares: {Address:UFix64}
204 access(self) let nftOffering: @{NFTOffering}
205
206 // Returns a boolean value which indicates if the market item is sold out.
207 access(contract) fun sell(nftReceiver: &{NonFungibleToken.Receiver}, payment: @Payment): Bool {
208
209 let receiver = self.nftOffering.getReceiver()
210 let balance = payment.amount
211 let royalty = SolarpupsNFT.getAsset(assetId: self.assetId)?.royalty;
212 let royaltyReceiver = self.nftOffering.getRoyaltyReceiver()
213
214 self.emitRoyaltyShare(payment: <- payment.split(balance * UFix64(royalty!)), receiver: royaltyReceiver)
215 self.emitDefaultShare(payment: <- payment, receiver: receiver)
216
217 let tokens <- self.nftOffering.provide()
218 let ids = tokens.getIDs()
219
220 for key in ids {
221 nftReceiver.deposit(token: <-tokens.withdraw(withdrawID: key))
222 }
223 if (self.owner?.address != nil) {
224 let owner = self.owner?.address!
225 emit MarketItemSold(assetId: self.assetId, owner: owner, tokenIds: ids)
226 }
227 destroy tokens
228
229 return self.nftOffering.getSupply() == 0
230 }
231
232 access(self) fun emitDefaultShare(payment: @Payment, receiver: Capability<&{FungibleToken.Receiver}>) {
233 let balance = payment.amount
234 for recipient in self.shares.keys {
235 let share <- payment.split(balance * self.shares[recipient]!)
236 self.payout(payment: <- share, receiver: receiver)
237 }
238 assert(payment.amount == 0.0, message: "invalid recipient payments")
239 destroy payment
240 }
241
242 access(self) fun emitRoyaltyShare(payment: @Payment, receiver: Capability<&{FungibleToken.Receiver}>) {
243 let balance = payment.amount
244 let creators = SolarpupsNFT.getAsset(assetId: self.assetId)!.creators
245 let creatorMap = creators as {Address: UFix64}
246 for creatorId in creatorMap.keys {
247 let share <- payment.split(balance * creatorMap[creatorId]!)
248 self.payout(payment: <- share, receiver: receiver)
249 }
250 assert(payment.amount == 0.0, message: "invalid royalty payments")
251 destroy payment
252 }
253
254 access(self) fun payout(payment: @Payment, receiver: Capability<&{FungibleToken.Receiver}>) {
255 emit MarketItemPayout(assetId: self.assetId, amount: payment.amount)
256
257 let receiverCapability = receiver.borrow()
258 let vault <- payment.paymentVault.withdraw(amount: payment.amount)
259 let vaultCopy <- vault
260 receiverCapability!.deposit(from: <- vaultCopy)
261
262 destroy payment
263 }
264
265 pub fun getSupply(): Int {
266 return self.nftOffering.getSupply()
267 }
268
269 pub fun lock() {
270 self.nftOffering.lock()
271 self.locked = self.locked + (1 as UInt64)
272 emit MarketItemLocked(assetId: self.assetId)
273 }
274
275 pub fun unlock() {
276 self.nftOffering.unlock()
277 self.locked = self.locked - (1 as UInt64)
278 emit MarketItemUnlocked(assetId: self.assetId)
279 }
280
281 pub fun getLocked(): UInt64 {
282 return self.locked
283 }
284
285 pub fun getShares(): {Address:UFix64} {
286 return self.shares
287 }
288
289 pub fun setPrice(price: UFix64) {
290 pre { self.locked == (0 as UInt64): "cannot change price due to locked items" }
291 self.price = price
292 }
293
294 destroy() {
295 assert(self.locked == (0 as UInt64), message: "cannot destroy market item due to locked items")
296 destroy self.nftOffering
297 }
298
299 init(assetId: String, price: UFix64, nftOffering: @{NFTOffering}, shares: {Address:UFix64}) {
300 self.assetId = assetId
301 self.price = price
302 self.nftOffering <- nftOffering
303 self.shares = shares
304 self.locked = 0
305
306 // check if asset is available
307 SolarpupsNFT.getAsset(assetId: assetId)
308
309 assert(shares.length > 0, message: "no recipient(s) found")
310 var sum:UFix64 = 0.0
311 for share in shares.values {
312 sum = sum + share
313 }
314 assert(sum == 1.0, message: "invalid recipient shares")
315 }
316 }
317
318 pub fun createMarketItem(assetId: String, price: UFix64, nftOffering: @{NFTOffering}, shares: {Address:UFix64}): @MarketItem {
319 return <-create MarketItem(assetId: assetId, price: price, nftOffering: <- nftOffering, shares: shares)
320 }
321
322 /**
323 * This resource interface defines all admin functions of a market store
324 */
325 pub resource interface MarketStoreAdmin {
326 pub fun lock(token: &MarketToken, assetId: String)
327 pub fun unlock(token: &MarketToken, assetId: String)
328 pub fun lockOffering(token: &MarketToken, assetId: String)
329 pub fun unlockOffering(token: &MarketToken, assetId: String)
330 }
331
332 /**
333 * This resource interface defines all functions of a market store resource used by the market store owner.
334 */
335 pub resource interface MarketStoreManager {
336 pub fun insert(item: @MarketItem)
337 pub fun remove(assetId: String): @MarketItem
338 }
339
340 /**
341 * This resource interface defines all public functions of a market store resource.
342 */
343 pub resource interface PublicMarketStore {
344 pub fun getAssetIds(): [String]
345 pub fun borrowMarketItem(assetId: String): &MarketItem{PublicMarketItem}?
346 pub fun buy(assetId: String, payment: @Payment, receiver: &{NonFungibleToken.Receiver})
347 }
348
349 /**
350 * The MarketStore resource is used to collect all market items for sale.
351 * Market items can either be directly bought.
352 */
353 pub resource MarketStore : MarketStoreManager, PublicMarketStore, MarketStoreAdmin {
354 pub let items: @{String: MarketItem}
355 pub let lockedItems: {String:String}
356
357 pub fun insert(item: @MarketItem) {
358 let assetId = item.assetId
359 let price = item.price
360 let ex = "listing exists for assetId: ".concat(assetId)
361 assert(self.items[item.assetId] == nil, message: ex)
362 let oldOffer <- self.items[item.assetId] <- item
363 destroy oldOffer
364
365 if (self.owner?.address != nil) {
366 emit MarketItemInserted(assetId: assetId, owner: self.owner?.address!, price: price)
367 }
368 }
369
370 pub fun remove(assetId: String): @MarketItem {
371 if (self.owner?.address != nil) {
372 emit MarketItemRemoved(assetId: assetId, owner: self.owner?.address!)
373 }
374 return <-(self.items.remove(key: assetId) ?? panic("missing market item"))
375 }
376
377 pub fun buy(assetId: String, payment: @Payment, receiver: &{NonFungibleToken.Receiver}) {
378 pre {
379 self.items[assetId] != nil: "market item not found"
380 self.lockedItems[assetId] == nil: "market item is locked"
381 }
382
383 let offer = &self.items[assetId] as &MarketItem?
384 let offerPrice = offer?.price
385 let price = UFix64(offerPrice!) * 1.0
386
387 if (offer == nil) {
388 destroy payment
389 } else {
390 let ex = "payment mismatch: ".concat(payment.amount.toString()).concat(" != ").concat(price.toString())
391 assert(payment.amount == price, message: ex)
392 let soldOut = offer!.sell(nftReceiver: receiver, payment: <- payment)
393 let itemSoldOut = soldOut as! Bool
394 if (itemSoldOut) {
395 destroy self.remove(assetId: assetId)
396 if (self.owner?.address != nil) {
397 emit MarketItemSoldOut(assetId: assetId, owner: self.owner?.address!)
398 }
399 }
400 }
401 }
402
403 pub fun lock(token: &MarketToken, assetId: String) {
404 self.lockedItems[assetId] = assetId
405 }
406
407 pub fun unlock(token: &MarketToken, assetId: String) {
408 self.lockedItems.remove(key: assetId)
409 }
410
411 pub fun lockOffering(token: &MarketToken, assetId: String) {
412 pre { self.items[assetId] != nil: "asset not found" }
413 let item = &self.items[assetId] as! &MarketItem?
414 item?.lock()
415 }
416
417 pub fun unlockOffering(token: &MarketToken, assetId: String) {
418 pre { self.items[assetId] != nil: "asset not found" }
419 let item = &self.items[assetId] as! &MarketItem?
420 item?.unlock()
421 }
422
423 pub fun getAssetIds(): [String] {
424 return self.items.keys
425 }
426
427 pub fun borrowMarketItem(assetId: String): &MarketItem{PublicMarketItem}? {
428 if self.items[assetId] == nil { return nil }
429 else { return &self.items[assetId] as &MarketItem{PublicMarketItem}? }
430 }
431
432 destroy() {
433 destroy self.items
434 }
435
436 init() {
437 self.items <- {}
438 self.lockedItems = {}
439 }
440 }
441
442 pub fun createMarketStore(): @MarketStore {
443 return <-create MarketStore()
444 }
445
446 pub fun createListOffer(tokenIds: [UInt64], assetId: String, provider: Capability<&{NonFungibleToken.Provider}>, receiver: Capability<&{FungibleToken.Receiver}>, royaltyReceiver: Capability<&{FungibleToken.Receiver}>): @ListOffering {
447 return <- create ListOffering(tokenIds: tokenIds, assetId: assetId, provider: provider, receiver: receiver, royaltyReceiver: royaltyReceiver)
448 }
449
450 pub fun createLazyOffer(assetId: String, minter: @SolarpupsNFT.Minter, receiver: Capability<&{FungibleToken.Receiver}>, royaltyReceiver: Capability<&{FungibleToken.Receiver}>): @LazyOffering {
451 return <- create LazyOffering(assetId: assetId, minter: <- minter, receiver: receiver, royaltyReceiver: royaltyReceiver)
452 }
453
454 pub fun createMarketAdmin(): @MarketAdmin {
455 return <-create MarketAdmin()
456 }
457
458 /**
459 * This resource is used by the administrator as an argument of a public function
460 * in order to restrict access to that function.
461 */
462 pub resource MarketToken {}
463
464 /*
465 * This resource is the administrator object of the Solarpups market.
466 * It can be used to alter the payment mechanisms without redeploying the contract.
467 */
468 pub resource MarketAdmin {
469 pub fun createPayment(vault: @FungibleToken.Vault): @Payment {
470 return <- create Payment(vault: <- vault)
471 }
472 }
473
474 init() {
475 self.SolarpupsMarketStorePublicPath = /public/SolarpupsMarketStoreProd01
476 self.SolarpupsMarketAdminStoragePath = /storage/SolarpupsMarketAdminProd01
477 self.SolarpupsMarketStoreStoragePath = /storage/SolarpupsMarketStoreProd01
478 self.SolarpupsMarketTokenStoragePath = /storage/SolarpupsMarketTokenProd01
479
480 self.totalPayments = 0
481
482 self.account.save(<- create MarketAdmin(), to: self.SolarpupsMarketAdminStoragePath)
483 self.account.save(<- create MarketStore(), to: self.SolarpupsMarketStoreStoragePath)
484 self.account.save(<- create MarketToken(), to: self.SolarpupsMarketTokenStoragePath)
485 self.account.link<&{SolarpupsMarket.PublicMarketStore, SolarpupsMarket.MarketStoreAdmin}>(self.SolarpupsMarketStorePublicPath, target: self.SolarpupsMarketStoreStoragePath)
486 }
487}
488