Smart Contract
BulkPurchase
A.9212a87501a8a6a2.BulkPurchase
1/*
2 BulkPurchase.cdc
3
4 The contract enables the bulk purchasing of NFTs from multiple storefronts in a single transaction
5
6 Author: Brian Min brian@flowverse.co
7*/
8
9// import "DapperUtilityCoin"
10import FungibleToken from 0xf233dcee88fe0abe
11import MetadataViews from 0x1d7e57aa55817448
12import NonFungibleToken from 0x1d7e57aa55817448
13import NFTCatalog from 0x49a7cda3a1eecc29
14import NFTStorefront from 0x4eb8a10cb9f87357
15import NFTStorefrontV2 from 0x4eb8a10cb9f87357
16// import "TopShot"
17// import "Market"
18// import "TopShotMarketV3"
19
20access(all) contract BulkPurchase {
21 access(all) event BulkPurchaseCompleted(completedOrders: [CompletedPurchaseOrder])
22
23 access(all) enum Storefront: UInt8 {
24 access(all) case StorefrontV1
25 access(all) case StorefrontV2
26 access(all) case Flowty
27 access(all) case TopShotV1
28 access(all) case TopShotV3
29 access(all) case Flovatar
30 }
31
32 access(all) fun getStorefrontFromIdentifier(_ identifier: String): Storefront {
33 switch identifier {
34 case "A.4eb8a10cb9f87357.NFTStorefront":
35 return Storefront.StorefrontV1
36 case "A.4eb8a10cb9f87357.NFTStorefrontV2":
37 return Storefront.StorefrontV2
38 case "A.c1e4f4f4c4257510.Market":
39 return Storefront.TopShotV1
40 case "A.c1e4f4f4c4257510.TopShotMarketV3":
41 return Storefront.TopShotV3
42 }
43 panic("Invalid storefront identifier")
44 }
45
46 access(all) fun getStorefrontIdentifier(_ storefront: Storefront): String {
47 switch storefront {
48 case Storefront.StorefrontV1:
49 return "A.4eb8a10cb9f87357.NFTStorefront"
50 case Storefront.StorefrontV2:
51 return "A.4eb8a10cb9f87357.NFTStorefrontV2"
52 case Storefront.TopShotV1:
53 return "A.c1e4f4f4c4257510.Market"
54 case Storefront.TopShotV3:
55 return "A.c1e4f4f4c4257510.TopShotMarketV3"
56 }
57 return ""
58 }
59
60 // Deprecated
61 // Use 'PurchaseOrder' struct instead
62 access(all) struct Order {
63 access(all) let listingID: UInt64
64 access(all) let ownerAddress: Address
65 access(all) var storefront: Storefront
66 access(all) var salePrice: UFix64
67 access(all) var salePaymentVaultType: Type
68 access(all) var nftID: UInt64
69 access(all) var nftType: Type
70 access(all) var purchaserAddress: Address
71 access(all) var completed: Bool
72 access(all) var failureReason: String?
73
74 init(
75 listingID: UInt64,
76 ownerAddress: Address,
77 storefront: Storefront,
78 salePrice: UFix64,
79 salePaymentVaultType: Type,
80 nftID: UInt64,
81 nftType: Type,
82 purchaserAddress: Address
83 ) {
84 self.listingID = listingID
85 self.ownerAddress = ownerAddress
86 self.storefront = storefront
87 self.salePrice = salePrice
88 self.salePaymentVaultType = salePaymentVaultType
89 self.nftID = nftID
90 self.nftType = nftType
91 self.purchaserAddress = purchaserAddress
92 self.completed = false
93 self.failureReason = nil
94 }
95 }
96
97 access(all) struct PurchaseOrder {
98 access(all) let listingID: UInt64
99 access(all) let ownerAddress: Address
100 access(all) let storefront: Storefront
101
102 access(all) var salePrice: UFix64?
103 access(all) var salePaymentVaultType: Type?
104 access(all) var nftID: UInt64?
105 access(all) var nftType: Type?
106 access(all) var purchaserAddress: Address?
107 access(all) var completed: Bool
108 access(all) var failureReason: String?
109
110 init(
111 listingID: UInt64,
112 ownerAddress: Address,
113 storefront: Storefront
114 ) {
115 self.listingID = listingID
116 self.ownerAddress = ownerAddress
117 self.storefront = storefront
118 self.salePrice = nil
119 self.salePaymentVaultType = nil
120 self.nftID = nil
121 self.nftType = nil
122 self.purchaserAddress = nil
123 self.completed = false
124 self.failureReason = nil
125 }
126
127 access(all) fun complete() {
128 self.completed = true
129 }
130
131 access(all) fun fail(_ reason: String) {
132 self.completed = false
133 self.failureReason = reason
134 }
135
136 access(all) fun setSalePrice(_ salePrice: UFix64) {
137 self.salePrice = salePrice
138 }
139
140 access(all) fun setSalePaymentVaultType(_ salePaymentVaultType: Type) {
141 self.salePaymentVaultType = salePaymentVaultType
142 }
143
144 access(all) fun setNFTID(_ nftID: UInt64) {
145 self.nftID = nftID
146 }
147
148 access(all) fun setNFTType(_ nftType: Type) {
149 self.nftType = nftType
150 }
151
152 access(all) fun setPurchaserAddress(_ purchaserAddress: Address) {
153 self.purchaserAddress = purchaserAddress
154 }
155 }
156
157 access(all) struct CompletedPurchaseOrder {
158 access(all) let listingID: UInt64
159 access(all) let ownerAddress: String
160 access(all) var storefront: String
161 access(all) var salePrice: UFix64
162 access(all) var salePaymentVaultType: String
163 access(all) var nftID: UInt64
164 access(all) var nftType: String
165 access(all) var purchaserAddress: String
166 access(all) var completed: Bool
167 access(all) var failureReason: String?
168
169 init (
170 _ order: PurchaseOrder
171 ) {
172 self.listingID = order.listingID
173 self.ownerAddress = order.ownerAddress.toString()
174 self.storefront = BulkPurchase.getStorefrontIdentifier(order.storefront)
175 self.salePrice = order.salePrice ?? 0.0
176 self.salePaymentVaultType = order.salePaymentVaultType?.identifier ?? ""
177 self.nftID = order.nftID ?? 0
178 self.nftType = order.nftType?.identifier ?? ""
179 self.purchaserAddress = order.purchaserAddress?.toString() ?? ""
180 self.completed = order.completed
181 self.failureReason = order.failureReason
182 }
183 }
184
185 // Helper functions
186 access(contract) view fun getStorefrontV1Ref(address: Address): &{NFTStorefront.StorefrontPublic}? {
187 return getAccount(address)
188 .capabilities.borrow<&{NFTStorefront.StorefrontPublic}>(NFTStorefront.StorefrontPublicPath)
189 }
190
191 access(contract) view fun getStorefrontV2Ref(address: Address): &{NFTStorefrontV2.StorefrontPublic}? {
192 return getAccount(address)
193 .capabilities.borrow<&{NFTStorefrontV2.StorefrontPublic}>(NFTStorefrontV2.StorefrontPublicPath)
194 }
195
196 // access(contract) view fun getTopshotV1MarketRef(address: Address): &{Market.SalePublic}? {
197 // return getAccount(address)
198 // .capabilities.borrow<&{Market.SalePublic}>(/public/topshotSaleCollection)
199 // }
200
201 // access(contract) view fun getTopshotV3MarketRef(address: Address): &{Market.SalePublic}? {
202 // return getAccount(address)
203 // .capabilities.borrow<&{Market.SalePublic}>(TopShotMarketV3.marketPublicPath)
204 // }
205
206 access(contract) fun processStorefrontV1Order (
207 order: PurchaseOrder,
208 paymentVaultRefs: {String: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}},
209 nftReceiverCapabilities: {String: Capability<&{NonFungibleToken.Receiver}>},
210 storefront: &{NFTStorefront.StorefrontPublic},
211 unsafeMode: Bool
212 ): PurchaseOrder {
213 let listing = storefront.borrowListing(listingResourceID: order.listingID)
214 if listing == nil {
215 if unsafeMode {
216 order.fail("listing was not found")
217 return order
218 }
219 panic("Storefront V1 listing was not found, listing id: ".concat(order.listingID.toString()))
220 }
221 let listingDetails = listing!.getDetails()
222 if listingDetails.purchased {
223 if unsafeMode {
224 order.fail("listing was already purchased")
225 return order
226 }
227 panic("Storefront V1 listing was already purchased, listing id: ".concat(order.listingID.toString()))
228 }
229 let paymentVaultRef = paymentVaultRefs[listingDetails.salePaymentVaultType.identifier]!
230 assert(listingDetails.salePaymentVaultType == paymentVaultRef.getType(),
231 message: "payment vault type does not match, listing id: ".concat(order.listingID.toString()))
232 if listingDetails.salePrice > paymentVaultRef.balance {
233 if unsafeMode {
234 order.fail("insufficient balance")
235 return order
236 }
237 panic("Insufficient balance to purchase Storefront V2 listing, listing id: ".concat(order.listingID.toString()))
238 }
239
240 let receiverCapability = nftReceiverCapabilities[listingDetails.nftType.identifier]
241 ?? panic("failed to get nft receiver capability for Storefront V1, NFT type: ".concat(listingDetails.nftType.identifier))
242 let receiver = receiverCapability.borrow()
243 ?? panic("failed to borrow receiver for Storefront V1, NFT type: ".concat(listingDetails.nftType.identifier))
244
245 let payment <- paymentVaultRef.withdraw(amount: listingDetails.salePrice)
246 let nft <- listing!.purchase(payment: <-payment)
247 receiver.deposit(token: <-nft)
248
249 order.setSalePrice(listingDetails.salePrice)
250 order.setSalePaymentVaultType(listingDetails.salePaymentVaultType)
251 order.setNFTType(listingDetails.nftType)
252 order.setNFTID(listingDetails.nftID)
253 order.setPurchaserAddress(receiverCapability.address)
254 order.complete()
255 return order
256 }
257
258 access(contract) fun processStorefrontV2Order (
259 order: PurchaseOrder,
260 paymentVaultRefs: {String: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}},
261 nftReceiverCapabilities: {String: Capability<&{NonFungibleToken.Receiver}>},
262 storefront: &{NFTStorefrontV2.StorefrontPublic},
263 commissionReceiverCapabilities: {String: Capability<&{FungibleToken.Receiver}>},
264 unsafeMode: Bool
265 ): PurchaseOrder {
266 let listing = storefront.borrowListing(listingResourceID: order.listingID)
267 if listing == nil {
268 if unsafeMode {
269 order.fail("listing was not found")
270 return order
271 }
272 panic("Storefront V2 listing was not found, listing id: ".concat(order.listingID.toString()))
273 }
274 let listingDetails = listing!.getDetails()
275 if listingDetails.purchased {
276 if unsafeMode {
277 order.fail("listing was already purchased")
278 return order
279 }
280 panic("Storefront V2 listing was already purchased, listing id: ".concat(order.listingID.toString()))
281 }
282 let paymentVaultRef = paymentVaultRefs[listingDetails.salePaymentVaultType.identifier]!
283 assert(listingDetails.salePaymentVaultType == paymentVaultRef.getType(),
284 message: "payment vault type does not match, listing id: ".concat(order.listingID.toString()))
285 if listingDetails.salePrice > paymentVaultRef.balance {
286 if unsafeMode {
287 order.fail("insufficient balance")
288 return order
289 }
290 panic("Insufficient balance to purchase Storefront V2 listing, listing id: ".concat(order.listingID.toString()))
291 }
292
293 let receiverCapability = nftReceiverCapabilities[listingDetails.nftType.identifier]
294 ?? panic("failed to get nft receiver capability for Storefront V2, NFT type: ".concat(listingDetails.nftType.identifier))
295 let receiver = receiverCapability.borrow()
296 ?? panic("failed to borrow receiver for Storefront V2, NFT type: ".concat(listingDetails.nftType.identifier))
297
298 var commissionRecipient: Capability<&{FungibleToken.Receiver}>? = nil
299 if (listingDetails.commissionAmount > 0.0) {
300 commissionRecipient = commissionReceiverCapabilities[listingDetails.salePaymentVaultType.identifier]
301 if let allowedCommissionReceivers = listing!.getAllowedCommissionReceivers() {
302 var found = false
303 for allowedReceiver in allowedCommissionReceivers {
304 // set commission receiver if specified and allowed
305 if commissionRecipient != nil &&
306 commissionRecipient!.address == allowedReceiver.address &&
307 commissionRecipient!.getType() == allowedReceiver.getType() {
308 found = true
309 break
310 }
311 }
312 if !found {
313 commissionRecipient = nil
314 }
315 }
316 }
317
318 let payment <- paymentVaultRef.withdraw(amount: listingDetails.salePrice)
319 let nft <- listing!.purchase(
320 payment: <-payment,
321 commissionRecipient: commissionRecipient
322 )
323 receiver.deposit(token: <-nft)
324
325 order.setSalePrice(listingDetails.salePrice)
326 order.setSalePaymentVaultType(listingDetails.salePaymentVaultType)
327 order.setNFTType(listingDetails.nftType)
328 order.setNFTID(listingDetails.nftID)
329 order.setPurchaserAddress(receiverCapability.address)
330 order.complete()
331 return order
332 }
333
334 // access(contract) fun processTopshotOrder(
335 // order: PurchaseOrder,
336 // paymentVaultRefs: {String: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}},
337 // nftReceiverCapabilities: {String: Capability<&{NonFungibleToken.Receiver}>},
338 // topShotMarket: &{Market.SalePublic},
339 // unsafeMode: Bool
340 // ): PurchaseOrder {
341 // if !topShotMarket.getIDs().contains(order.listingID) {
342 // if unsafeMode {
343 // order.fail("listing was not found")
344 // return order
345 // }
346 // panic("TopShot listing was not found, listing id: ".concat(order.listingID.toString()))
347 // }
348 // let salePrice = topShotMarket.getPrice(tokenID: order.listingID)!
349 // let paymentVaultRef = paymentVaultRefs[Type<@DapperUtilityCoin.Vault>().identifier]!
350 // assert(paymentVaultRef.getType() == Type<@DapperUtilityCoin.Vault>(),
351 // message: "payment vault type does not match, listing id: ".concat(order.listingID.toString()))
352 // if (salePrice > paymentVaultRef.balance) {
353 // if unsafeMode {
354 // order.fail("insufficient balance")
355 // return order
356 // }
357 // panic("Insufficient balance to purchase TopShot listing, listing id: ".concat(order.listingID.toString()))
358 // }
359 // let receiverCapability = nftReceiverCapabilities[Type<@TopShot.NFT>().identifier]
360 // ?? panic("failed to get nft receiver capability for topshot")
361 // let receiver = receiverCapability.borrow()
362 // ?? panic("failed to borrow receiver for TopShot")
363
364 // let buyTokens <- paymentVaultRef.withdraw(amount: salePrice) as! @DapperUtilityCoin.Vault
365 // let purchasedItem <- topShotMarket!.purchase(tokenID: order.listingID, buyTokens: <-buyTokens)
366 // receiver.deposit(token: <-purchasedItem)
367
368 // order.setSalePrice(salePrice)
369 // order.setSalePaymentVaultType(Type<@DapperUtilityCoin.Vault>())
370 // order.setNFTType(Type<@TopShot.NFT>())
371 // order.setNFTID(order.listingID)
372 // order.setPurchaserAddress(receiverCapability.address)
373 // order.complete()
374 // return order
375 // }
376
377 access(all) fun purchase(
378 orders: [PurchaseOrder],
379 paymentVaultRefs: {String: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}},
380 nftReceiverCapabilities: {String: Capability<&{NonFungibleToken.Receiver}>},
381 commissionReceiverCapabilities: {String: Capability<&{FungibleToken.Receiver}>},
382 unsafeMode: Bool
383 ) {
384 pre {
385 orders.length <= 30: "maximum 30 orders per transaction"
386 }
387
388 // storefront references
389 let storefrontV1Refs: {Address: &{NFTStorefront.StorefrontPublic}} = {}
390 let storefrontV2Refs: {Address: &{NFTStorefrontV2.StorefrontPublic}} = {}
391 // let topShotV1MarketRefs: {Address: &{Market.SalePublic}} = {}
392 // let topShotV3MarketRefs: {Address: &{Market.SalePublic}} = {}
393
394 let completedOrders: [CompletedPurchaseOrder] = []
395
396 for order in orders {
397 switch order.storefront {
398 case Storefront.StorefrontV1:
399 if !storefrontV1Refs.containsKey(order.ownerAddress) {
400 let storefrontV1Ref = self.getStorefrontV1Ref(address: order.ownerAddress)
401 storefrontV1Refs[order.ownerAddress] = storefrontV1Ref ?? panic("missing storefront v1 reference in owner account")
402 }
403 let storefront = storefrontV1Refs[order.ownerAddress]!
404 let processedOrder = self.processStorefrontV1Order(
405 order: order,
406 paymentVaultRefs: paymentVaultRefs,
407 nftReceiverCapabilities: nftReceiverCapabilities,
408 storefront: storefront,
409 unsafeMode: unsafeMode
410 )
411 storefront.cleanup(listingResourceID: order.listingID)
412 completedOrders.append(CompletedPurchaseOrder(processedOrder))
413 case Storefront.StorefrontV2:
414 if !storefrontV2Refs.containsKey(order.ownerAddress) {
415 let storefrontV2Ref = self.getStorefrontV2Ref(address: order.ownerAddress)
416 storefrontV2Refs[order.ownerAddress] = storefrontV2Ref ?? panic("missing storefront v2 reference in owner account")
417 }
418 let processedOrder = self.processStorefrontV2Order(
419 order: order,
420 paymentVaultRefs: paymentVaultRefs,
421 nftReceiverCapabilities: nftReceiverCapabilities,
422 storefront: storefrontV2Refs[order.ownerAddress]!,
423 commissionReceiverCapabilities: commissionReceiverCapabilities,
424 unsafeMode: unsafeMode
425 )
426 completedOrders.append(CompletedPurchaseOrder(processedOrder))
427 // case Storefront.TopShotV1:
428 // if !topShotV1MarketRefs.containsKey(order.ownerAddress) {
429 // let topShotV1MarketRef = self.getTopshotV1MarketRef(address: order.ownerAddress)
430 // topShotV1MarketRefs[order.ownerAddress] = topShotV1MarketRef ?? panic("missing topshot v1 market reference in owner account")
431 // }
432 // let processedOrder = self.processTopshotOrder(
433 // order: order,
434 // paymentVaultRefs: paymentVaultRefs,
435 // nftReceiverCapabilities: nftReceiverCapabilities,
436 // topShotMarket: topShotV1MarketRefs[order.ownerAddress]!,
437 // unsafeMode: unsafeMode
438 // )
439 // completedOrders.append(CompletedPurchaseOrder(processedOrder))
440 // case Storefront.TopShotV3:
441 // if !topShotV3MarketRefs.containsKey(order.ownerAddress) {
442 // let topShotV3MarketRef = self.getTopshotV3MarketRef(address: order.ownerAddress)
443 // topShotV3MarketRefs[order.ownerAddress] = topShotV3MarketRef ?? panic("missing topshot v3 market reference in owner account")
444 // }
445 // let processedOrder = self.processTopshotOrder(
446 // order: order,
447 // paymentVaultRefs: paymentVaultRefs,
448 // nftReceiverCapabilities: nftReceiverCapabilities,
449 // topShotMarket: topShotV3MarketRefs[order.ownerAddress]!,
450 // unsafeMode: unsafeMode
451 // )
452 // completedOrders.append(CompletedPurchaseOrder(processedOrder))
453 }
454 }
455 emit BulkPurchaseCompleted(completedOrders: completedOrders)
456 }
457}
458