Smart Contract

SolarpupsMarket

A.a8d493db1bb4df56.SolarpupsMarket

Deployed

2h ago
Mar 01, 2026, 02:22:31 PM UTC

Dependents

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