Smart Contract

BulkPurchase

A.9212a87501a8a6a2.BulkPurchase

Deployed

1d ago
Feb 27, 2026, 08:40:02 AM UTC

Dependents

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