Smart Contract
FindMarketSale
A.097bafa4e0b48eef.FindMarketSale
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