Smart Contract

FindMarketSale

A.097bafa4e0b48eef.FindMarketSale

Deployed

2d ago
Feb 26, 2026, 03:12:51 AM UTC

Dependents

4 imports
1import FungibleToken from 0xf233dcee88fe0abe
2import NonFungibleToken from 0x1d7e57aa55817448
3import MetadataViews from 0x1d7e57aa55817448
4import FindViews from 0x097bafa4e0b48eef
5import Clock from 0x097bafa4e0b48eef
6import FIND from 0x097bafa4e0b48eef
7import Profile from 0x097bafa4e0b48eef
8import FindMarket from 0x097bafa4e0b48eef
9
10/*
11
12A Find Market for direct sales
13*/
14access(all) contract FindMarketSale {
15
16    // A seller can list, delist and relist leases for sale
17    access(all) entitlement Seller
18
19    access(all) event Sale(tenant: String, id: UInt64, saleID: UInt64, seller: Address, sellerName: String?, amount: UFix64, status: String, vaultType:String, nft: FindMarket.NFTInfo?, buyer:Address?, buyerName:String?, buyerAvatar: String?, endsAt:UFix64?)
20
21    //A sale item for a direct sale
22    access(all) resource SaleItem : FindMarket.SaleItem{
23
24        //this is set when bought so that pay will work
25        access(self) var buyer: Address?
26
27        access(contract) let vaultType: Type //The type of vault to use for this sale Item
28        access(contract) var pointer: FindViews.AuthNFTPointer
29
30        //this field is set if this is a saleItem
31        access(contract) var salePrice: UFix64
32        access(contract) var validUntil: UFix64?
33        access(contract) let saleItemExtraField: {String : AnyStruct}
34
35        access(contract) let totalRoyalties: UFix64
36        init(pointer: FindViews.AuthNFTPointer, vaultType: Type, price:UFix64, validUntil: UFix64?, saleItemExtraField: {String : AnyStruct}) {
37            self.vaultType=vaultType
38            self.pointer=pointer
39            self.salePrice=price
40            self.buyer=nil
41            self.validUntil=validUntil
42            self.saleItemExtraField=saleItemExtraField
43            var royalties : UFix64 = 0.0
44            self.totalRoyalties=self.pointer.getTotalRoyaltiesCut()
45        }
46
47        access(all) fun getPointer() : FindViews.AuthNFTPointer {
48            return self.pointer
49        }
50
51        access(all) fun getSaleType() : String {
52            return "active_listed"
53        }
54
55        access(all) fun getListingType() : Type {
56            return Type<@SaleItem>()
57        }
58
59        access(all) fun getListingTypeIdentifier(): String {
60            return Type<@SaleItem>().identifier
61        }
62
63        access(account) fun setBuyer(_ address:Address) {
64            self.buyer=address
65        }
66
67        access(all) fun getBuyer(): Address? {
68            return self.buyer
69        }
70
71        access(all) fun getBuyerName() : String? {
72            if let address = self.buyer {
73                return FIND.reverseLookup(address)
74            }
75            return nil
76        }
77
78        access(all) fun getId() : UInt64{
79            return self.pointer.getUUID()
80        }
81
82        access(all) fun getItemID() : UInt64 {
83            return self.pointer.id
84        }
85
86        access(all) fun getItemType() : Type {
87            return self.pointer.getItemType()
88        }
89
90        access(all) fun getRoyalty() : MetadataViews.Royalties {
91            return self.pointer.getRoyalty()
92        }
93
94        access(all) fun getSeller() : Address {
95            return self.pointer.owner()
96        }
97
98        access(all) fun getSellerName() : String? {
99            return FIND.reverseLookup(self.pointer.owner())
100        }
101
102        access(all) fun getBalance() : UFix64 {
103            return self.salePrice
104        }
105
106        access(all) fun getAuction(): FindMarket.AuctionItem? {
107            return nil
108        }
109
110        access(all) fun getFtType() : Type  {
111            return self.vaultType
112        }
113
114        access(contract) fun setValidUntil(_ time: UFix64?) {
115            self.validUntil=time
116        }
117
118        access(all) fun getValidUntil() : UFix64? {
119            return self.validUntil
120        }
121
122        access(all) fun toNFTInfo(_ detail: Bool) : FindMarket.NFTInfo{
123            return FindMarket.NFTInfo(self.pointer.getViewResolver(), id: self.pointer.id, detail:detail)
124        }
125
126        access(all) fun checkPointer() : Bool {
127            return self.pointer.valid()
128        }
129
130        access(all) fun checkSoulBound() : Bool {
131            return self.pointer.checkSoulBound()
132        }
133
134        access(all) fun getSaleItemExtraField() : {String : AnyStruct} {
135            return self.saleItemExtraField
136        }
137
138        access(all) fun getTotalRoyalties() : UFix64 {
139            return self.totalRoyalties
140        }
141
142        access(all) fun validateRoyalties() : Bool {
143            return self.totalRoyalties == self.pointer.getTotalRoyaltiesCut()
144        }
145
146        access(all) fun getDisplay() : MetadataViews.Display {
147            return self.pointer.getDisplay()
148        }
149
150        access(all) fun getNFTCollectionData() : MetadataViews.NFTCollectionData {
151            return self.pointer.getNFTCollectionData()
152        }
153    }
154
155    access(all) resource interface SaleItemCollectionPublic {
156        //fetch all the tokens in the collection
157        access(all) fun getIds(): [UInt64]
158        access(all) fun borrowSaleItem(_ id: UInt64) : &{FindMarket.SaleItem}?
159        access(all) fun containsId(_ id: UInt64): Bool
160        access(all) fun buy(id: UInt64, vault: @{FungibleToken.Vault}, nftCap: Capability<&{NonFungibleToken.Receiver}>)
161    }
162
163    access(all) resource SaleItemCollection: SaleItemCollectionPublic, FindMarket.SaleItemCollectionPublic {
164        //is this the best approach now or just put the NFT inside the saleItem?
165        access(contract) var items: @{UInt64: SaleItem}
166
167        //Not sure if this should be the interface or the impl
168        access(contract) let tenantCapability: Capability<&FindMarket.Tenant>
169
170        init (_ tenantCapability: Capability<&FindMarket.Tenant>) {
171            self.items <- {}
172            self.tenantCapability=tenantCapability
173        }
174
175        access(self) fun getTenant() : &FindMarket.Tenant {
176            if !self.tenantCapability.check() {
177                panic("Tenant client is not linked anymore")
178            }
179            return self.tenantCapability.borrow()!
180        }
181
182        access(all) fun getListingType() : Type {
183            return Type<@SaleItem>()
184        }
185
186        access(all) fun buy(id: UInt64, vault: @{FungibleToken.Vault}, nftCap: Capability<&{NonFungibleToken.Receiver}>) {
187            if !self.items.containsKey(id) {
188                panic("Invalid id=".concat(id.toString()))
189            }
190
191            if self.owner!.address == nftCap.address {
192                panic("You cannot buy your own listing")
193            }
194
195            let saleItem=self.borrow(id)
196
197            if saleItem.salePrice != vault.balance {
198                panic("Incorrect balance sent in vault. Expected ".concat(saleItem.salePrice.toString()).concat(" got ").concat(vault.balance.toString()))
199            }
200
201            if saleItem.validUntil != nil && saleItem.validUntil! < Clock.time() {
202                panic("This sale item listing is already expired")
203            }
204
205            if saleItem.vaultType != vault.getType() {
206                panic("This item can be bought using ".concat(saleItem.vaultType.identifier).concat(" you have sent in ").concat(vault.getType().identifier))
207            }
208            let tenant=self.getTenant()
209            let ftType=saleItem.vaultType
210            let nftType=saleItem.getItemType()
211
212            //TOOD: method on saleItems that returns a cacheKey listingType-nftType-ftType
213            let actionResult=tenant.allowedAction(listingType: Type<@FindMarketSale.SaleItem>(), nftType: nftType, ftType: ftType, action: FindMarket.MarketAction(listing:false, name: "buy item for sale"), seller: self.owner!.address, buyer: nftCap.address)
214
215            if !actionResult.allowed {
216                panic(actionResult.message)
217            }
218
219            let cuts= tenant.getCuts(name: actionResult.name, listingType: Type<@FindMarketSale.SaleItem>(), nftType: nftType, ftType: ftType)
220
221            let nftInfo= saleItem.toNFTInfo(true)
222            saleItem.setBuyer(nftCap.address)
223            let buyer=nftCap.address
224            let buyerName=FIND.reverseLookup(buyer)
225            let sellerName=FIND.reverseLookup(self.owner!.address)
226
227            emit Sale(tenant:tenant.name, id: id, saleID: saleItem.uuid, seller:self.owner!.address, sellerName: FIND.reverseLookup(self.owner!.address), amount: saleItem.getBalance(), status:"sold", vaultType: ftType.identifier, nft:nftInfo, buyer: buyer, buyerName: buyerName, buyerAvatar: Profile.find(nftCap.address).getAvatar() ,endsAt:saleItem.validUntil)
228            let resolved : {Address : String} = {}
229
230            resolved[buyer] = buyerName ?? ""
231            resolved[self.owner!.address] = sellerName ?? ""
232            resolved[FindMarketSale.account.address] = "find"
233            // Have to make sure the tenant always have the valid find name
234            resolved[FindMarket.tenantNameAddress[tenant.name]!] = tenant.name
235
236            FindMarket.pay(tenant:tenant.name, id:id, saleItem: saleItem, vault: <- vault, royalty:saleItem.getRoyalty(), nftInfo:nftInfo, cuts:cuts, resolver: fun(address:Address): String? { return FIND.reverseLookup(address) }, resolvedAddress: resolved)
237
238
239            if !nftCap.check() {
240                let cp =getAccount(nftCap.address).capabilities.borrow<&{NonFungibleToken.Collection}>(saleItem.getNFTCollectionData().publicPath)
241                if cp == nil {
242                    panic("The nft receiver capability passed in is invalid.")
243                } else {
244                    cp!.deposit(token: <- saleItem.pointer.withdraw())
245                }
246            } else {
247                nftCap.borrow()!.deposit(token: <- saleItem.pointer.withdraw())
248            }
249
250            destroy <- self.items.remove(key: id)
251        }
252
253        access(Seller) fun listForSale(pointer: FindViews.AuthNFTPointer, vaultType: Type, directSellPrice:UFix64, validUntil: UFix64?, extraField: {String:AnyStruct}) {
254
255
256            // ensure it is not a 0 dollar listing
257            if directSellPrice <= 0.0 {
258                panic("Listing price should be greater than 0")
259            }
260
261            if validUntil != nil && validUntil! < Clock.time() {
262                panic("Valid until is before current time")
263            }
264
265            // check soul bound
266            if pointer.checkSoulBound() {
267                panic("This item is soul bounded and cannot be traded")
268            }
269
270            // What happends if we relist
271            let saleItem <- create SaleItem(pointer: pointer, vaultType:vaultType, price: directSellPrice, validUntil: validUntil, saleItemExtraField:extraField)
272
273            let tenant=self.getTenant()
274
275            // Check if it is onefootball. If so, listing has to be at least $0.65 (DUC)
276            if tenant.name == "onefootball" {
277                // ensure it is not a 0 dollar listing
278                if directSellPrice <= 0.65 {
279                    panic("Listing price should be greater than 0.65")
280                }
281            }
282
283            let nftType=saleItem.getItemType()
284            let ftType=saleItem.getFtType()
285
286            let actionResult=tenant.allowedAction(listingType: Type<@FindMarketSale.SaleItem>(), nftType: nftType, ftType: ftType, action: FindMarket.MarketAction(listing:true, name: "list item for sale"), seller: self.owner!.address, buyer: nil)
287
288            if !actionResult.allowed {
289                panic(actionResult.message)
290                // let message = "vault : ".concat(vaultType.identifier).concat(" . NFT Type : ".concat(saleItem.getItemType().identifier))
291                // panic(message)
292            }
293
294            let owner=self.owner!.address
295            emit Sale(tenant: tenant.name, id: pointer.getUUID(), saleID: saleItem.uuid, seller:owner, sellerName: FIND.reverseLookup(owner), amount: saleItem.salePrice, status: "active_listed", vaultType: vaultType.identifier, nft:saleItem.toNFTInfo(true), buyer: nil, buyerName:nil, buyerAvatar:nil, endsAt:saleItem.validUntil)
296            let old <- self.items[pointer.getUUID()] <- saleItem
297            destroy old
298
299        }
300
301        access(Seller) fun delist(_ id: UInt64) {
302            if !self.items.containsKey(id) {
303                panic("Unknown item with id=".concat(id.toString()))
304            }
305
306            let saleItem <- self.items.remove(key: id)!
307
308            let tenant=self.getTenant()
309
310            var status = "cancel"
311            var nftInfo:FindMarket.NFTInfo?=nil
312            if saleItem.checkPointer() {
313                nftInfo=saleItem.toNFTInfo(false)
314            }
315
316            let owner=self.owner!.address
317            emit Sale(tenant:tenant.name, id: id, saleID: saleItem.uuid, seller:owner, sellerName:FIND.reverseLookup(owner), amount: saleItem.salePrice, status: status, vaultType: saleItem.vaultType.identifier,nft: nftInfo, buyer:nil, buyerName:nil, buyerAvatar:nil, endsAt:saleItem.validUntil)
318            destroy saleItem
319        }
320
321        access(Seller) fun relist(_ id: UInt64) {
322            let saleItem = self.borrow(id)
323
324            let pointer = saleItem.getPointer()
325            let vaultType= saleItem.vaultType
326            let directSellPrice=saleItem.salePrice
327            var validUntil= saleItem.validUntil
328            if validUntil != nil && saleItem.validUntil! <= Clock.time() {
329                validUntil = nil
330            }
331            let extraField= saleItem.getSaleItemExtraField()
332
333            self.delist(id)
334            self.listForSale(pointer: pointer, vaultType: vaultType, directSellPrice:directSellPrice, validUntil: validUntil, extraField: extraField)
335        }
336
337        access(all) fun getIds(): [UInt64] {
338            return self.items.keys
339        }
340
341        access(all) fun getRoyaltyChangedIds(): [UInt64] {
342            let ids : [UInt64] = []
343            for id in self.getIds() {
344                let item = self.borrow(id)
345                if !item.validateRoyalties() {
346                    ids.append(id)
347                }
348            }
349            return ids
350        }
351
352        access(all) fun containsId(_ id: UInt64): Bool {
353            return self.items.containsKey(id)
354        }
355
356        access(all) fun borrow(_ id: UInt64): &SaleItem {
357            return (&self.items[id])!
358        }
359
360        access(all) fun borrowSaleItem(_ id: UInt64) : &{FindMarket.SaleItem}? {
361            if !self.items.containsKey(id) {
362                panic("This id does not exist : ".concat(id.toString()))
363            }
364            return &self.items[id]
365        }
366    }
367
368    //Create an empty lease collection that store your leases to a name
369    access(all) fun createEmptySaleItemCollection(_ tenantCapability: Capability<&FindMarket.Tenant>) : @SaleItemCollection {
370        return <- create SaleItemCollection(tenantCapability)
371    }
372
373    access(all) fun getSaleItemCapability(marketplace:Address, user:Address) : Capability<&{FindMarketSale.SaleItemCollectionPublic, FindMarket.SaleItemCollectionPublic}>? {
374        if let  tenantCap=FindMarket.getTenantCapability(marketplace) {
375            let tenant=tenantCap.borrow() ?? panic("Invalid tenant")
376            return getAccount(user).capabilities.get<&{FindMarketSale.SaleItemCollectionPublic, FindMarket.SaleItemCollectionPublic}>(tenant.getPublicPath(Type<@SaleItemCollection>()))
377        }
378        return nil
379    }
380
381
382    init() {
383        FindMarket.addSaleItemType(Type<@SaleItem>())
384        FindMarket.addSaleItemCollectionType(Type<@SaleItemCollection>())
385    }
386}
387