Smart Contract

FlowversePrimarySaleV2

A.9212a87501a8a6a2.FlowversePrimarySaleV2

Deployed

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

Dependents

0 imports
1/*
2    FlowversePrimarySaleV2.cdc
3
4    The contract handles the primary sale of NFTs, enabling purchasing and minting NFTs on-the-fly.
5
6    Author: Brian Min brian@flowverse.co
7*/
8
9import NonFungibleToken from 0x1d7e57aa55817448
10import FungibleToken from 0xf233dcee88fe0abe
11import Crypto
12
13access(all) contract FlowversePrimarySaleV2 {
14    // Entitlements
15    access(all) entitlement SaleAdmin
16
17    access(all) let AdminStoragePath: StoragePath
18
19    // Incremented ID used to create entities
20    access(all) var nextPrimarySaleID: UInt64
21
22    access(contract) var primarySales: @{UInt64: PrimarySale}
23    access(contract) var primarySaleIDs: {String: UInt64}
24    
25    access(all) event PurchaseComplete(primarySaleID: UInt64, orders: [Order], nftIDs: [UInt64], purchaserAddress: Address, pool: String, price: UFix64, salePaymentVaultType: String)
26
27    access(all) resource interface IMinter {
28        access(all) fun mint(entityID: UInt64, minterAddress: Address): @{NonFungibleToken.NFT}
29    }
30
31    // Data struct signed by admin account - allows accounts to purchase from a primary sale for a period of time.
32    access(all) struct AdminSignedPayload {
33        access(all) let primarySaleID: UInt64
34        access(all) let purchaserAddress: Address
35        access(all) let expiration: UInt64 // unix timestamp
36
37        init(primarySaleID: UInt64, purchaserAddress: Address, expiration: UInt64){
38            self.primarySaleID = primarySaleID
39            self.purchaserAddress = purchaserAddress
40            self.expiration = expiration
41        }
42
43        access(all) fun toString(): String {
44            return self.primarySaleID.toString().concat("-")
45                .concat(self.purchaserAddress.toString()).concat("-")
46                .concat(self.expiration.toString())
47        }
48    }
49
50    access(all) struct PriceData {
51        access(all) let price: {String: UFix64}
52        access(all) let pool: String
53        access(all) var eligibleAddresses: [Address]?
54        access(all) var maxMintsPerUser: UInt64?
55
56        init(price: {String: UFix64}, pool: String, eligibleAddresses: [Address]?, maxMintsPerUser: UInt64?){
57            self.price = price
58            self.pool = pool
59            self.eligibleAddresses = eligibleAddresses
60            self.maxMintsPerUser = maxMintsPerUser
61        }
62
63        access(all) fun setEligibleAddresses(_ eligibleAddresses: [Address]?) {
64            self.eligibleAddresses = eligibleAddresses
65        }
66    }
67
68    access(all) struct PurchaseData {
69        access(all) let primarySaleID: UInt64
70        access(all) let purchaserAddress: Address
71        access(all) let purchaserCollectionRef: &{NonFungibleToken.Receiver}
72        access(all) let orders: [Order]
73        access(all) let pool: String
74
75        init(primarySaleID: UInt64, purchaserAddress: Address, purchaserCollectionRef: &{NonFungibleToken.Receiver}, orders: [Order], pool: String){
76            self.primarySaleID = primarySaleID
77            self.purchaserAddress = purchaserAddress
78            self.purchaserCollectionRef = purchaserCollectionRef
79            self.orders = orders
80            self.pool = pool
81        }
82    }
83    
84    access(all) struct PurchaseDataSequential {
85        access(all) let primarySaleID: UInt64
86        access(all) let purchaserAddress: Address
87        access(all) let purchaserCollectionRef: &{NonFungibleToken.Receiver}
88        access(all) let quantity: UInt64
89        access(all) let pool: String
90
91        init(primarySaleID: UInt64, purchaserAddress: Address, purchaserCollectionRef: &{NonFungibleToken.Receiver}, quantity: UInt64, pool: String){
92            self.primarySaleID = primarySaleID
93            self.purchaserAddress = purchaserAddress
94            self.purchaserCollectionRef = purchaserCollectionRef
95            self.quantity = quantity
96            self.pool = pool
97        }
98    }
99
100    access(all) struct Order {
101        access(all) let entityID: UInt64
102        access(all) let quantity: UInt64
103
104        init(entityID: UInt64, quantity: UInt64){
105            self.entityID = entityID
106            self.quantity = quantity
107        }
108    }
109
110    access(all) enum PrimarySaleStatus: UInt8 {
111        access(all) case PAUSED
112        access(all) case OPEN
113        access(all) case CLOSED
114    }
115
116    access(all) resource interface PrimarySalePublic {
117        access(all) view fun getSupply(pool: String?): UInt64
118        access(all) view fun getPrices(): {String: PriceData}
119        access(all) view fun getStatus(): String
120        access(all) fun purchaseHeroesBox(
121            payment: @{FungibleToken.Vault},
122            data: PurchaseDataSequential,
123            adminSignedPayload: AdminSignedPayload, 
124            signature: String
125        )
126        access(all) fun purchaseSequentialNFTs(
127            payment: @{FungibleToken.Vault},
128            data: PurchaseDataSequential,
129            adminSignedPayload: AdminSignedPayload, 
130            signature: String
131        )
132        access(all) view fun getNumMintedByUser(userAddress: Address, pool: String): UInt64
133        access(all) view fun getNumMintedPerUser(): {String: {Address: UInt64}}
134        access(all) fun getAllAvailableEntities(pool: String): {UInt64: UInt64}
135        access(all) view fun getLaunchDate(): String
136        access(all) view fun getEndDate(): String
137        access(all) fun getPaymentReceivers(): {String: Address} 
138        access(all) view fun getContractName(): String
139        access(all) view fun getContractAddress(): Address
140        access(all) view fun getID(): UInt64
141    }
142
143    access(all) resource PrimarySale: PrimarySalePublic {
144        access(self) var primarySaleID: UInt64
145        access(self) var contractName: String
146        access(self) var contractAddress: Address
147        access(self) var status: PrimarySaleStatus
148        access(self) var prices: {String: PriceData}
149        access(self) var pooledEntities: {String: {UInt64: UInt64}}
150        access(self) var numMintedPerUser: {String: {Address: UInt64}}
151        access(self) var launchDate: String
152        access(self) var endDate: String
153
154        access(self) let minterCap: Capability<&{IMinter}>
155        access(self) var paymentReceiverCaps: {String: Capability<&{FungibleToken.Receiver}>}
156
157        init(
158            contractName: String,
159            contractAddress: Address,
160            prices: {String: PriceData},
161            minterCap: Capability<&{IMinter}>,
162            paymentReceiverCaps: {String: Capability<&{FungibleToken.Receiver}>},
163            launchDate: String, 
164            endDate: String
165        ) {
166            self.contractName = contractName
167            self.contractAddress = contractAddress
168            self.status = PrimarySaleStatus.PAUSED // primary sale is paused initially
169            self.pooledEntities = {}
170            self.prices = prices
171
172            self.minterCap = minterCap
173            self.paymentReceiverCaps = paymentReceiverCaps
174            
175            self.launchDate = launchDate
176            self.endDate = endDate
177
178            self.primarySaleID = FlowversePrimarySaleV2.nextPrimarySaleID
179            let key = contractName.concat(contractAddress.toString())
180            FlowversePrimarySaleV2.primarySaleIDs[key] = self.primarySaleID
181            
182            self.numMintedPerUser = {}
183            for pool in prices.keys {
184                self.numMintedPerUser.insert(key: pool, {})
185            }
186        }
187
188        access(all) view fun getStatus(): String {
189            if (self.status == PrimarySaleStatus.PAUSED) {
190                return "PAUSED"
191            } else if (self.status == PrimarySaleStatus.OPEN) {
192                return "OPEN"
193            } else if (self.status == PrimarySaleStatus.CLOSED) {
194                return "CLOSED"
195            } else {
196                return ""
197            }
198        }
199
200        access(SaleAdmin) fun setPrice(priceData: PriceData) {
201            self.prices[priceData.pool] = priceData
202            if !self.numMintedPerUser.containsKey(priceData.pool) {
203                self.numMintedPerUser.insert(key: priceData.pool, {})
204            }
205        }
206        
207        access(SaleAdmin) fun removePool(pool: String) {
208            self.prices.remove(key: pool)
209            self.numMintedPerUser.remove(key: pool)
210            self.pooledEntities.remove(key: pool)
211        }
212
213        access(all) view fun getPrices(): {String: PriceData} {
214            return self.prices
215        }
216
217        access(SaleAdmin) fun addEligibleAddressesToPool(pool: String, eligibleAddresses: [Address]) {
218            assert(self.prices.containsKey(pool), message: "Pool does not exist")
219            let price = self.prices[pool]!
220            if price.eligibleAddresses == nil {
221                price.setEligibleAddresses(eligibleAddresses)
222            } else {
223                let updatedEligibleAddresses = price.eligibleAddresses!
224                updatedEligibleAddresses.appendAll(eligibleAddresses)
225                price.setEligibleAddresses(updatedEligibleAddresses)
226            }
227            self.prices[pool] = price
228        }
229
230        access(SaleAdmin) fun clearEligibleAddressesForPool(pool: String) {
231            assert(self.prices.containsKey(pool), message: "Pool does not exist")
232            let price = self.prices[pool]!
233            price.setEligibleAddresses(nil)
234            self.prices[pool] = price
235        }
236
237        access(all) view fun getSupply(pool: String?): UInt64 {
238            var supply = UInt64(0)
239            for poolKey in self.pooledEntities.keys {
240                if pool == nil || pool == poolKey {
241                    let pooledDict = self.pooledEntities[poolKey]!
242                    for entityID in pooledDict.keys {
243                        supply = supply + pooledDict[entityID]!
244                    }
245                }
246            }
247            return supply
248        }
249
250        access(all) view fun getLaunchDate(): String {
251            return self.launchDate
252        }
253
254        access(all) view fun getEndDate(): String {
255            return self.endDate
256        }
257
258        access(all) fun getPaymentReceivers(): {String: Address}  {
259            let paymentReceivers: {String: Address} = {}
260            for salePaymentVaultType in self.paymentReceiverCaps.keys {
261                let receiver = self.paymentReceiverCaps[salePaymentVaultType]!.borrow()!
262                if receiver.owner != nil {
263                    paymentReceivers[salePaymentVaultType] = receiver.owner!.address
264                }
265            }
266            return paymentReceivers
267        }
268
269        access(all) fun getPaymentReceiverAddress(salePaymentVaultType: String): Address? {
270            assert(self.paymentReceiverCaps.containsKey(salePaymentVaultType), message: "payment receiver does not exist for vault type: ".concat(salePaymentVaultType))
271            let receiver = self.paymentReceiverCaps[salePaymentVaultType]!.borrow()!
272            if receiver.owner != nil {
273                return receiver.owner!.address
274            }
275            return nil
276        }
277        
278        access(all) view fun getNumMintedPerUser(): {String: {Address: UInt64}} {
279            return self.numMintedPerUser
280        }
281
282        access(all) view fun getNumMintedByUser(userAddress: Address, pool: String): UInt64 {
283            assert(self.numMintedPerUser.containsKey(pool), message: "invalid pool")
284            let numMintedDict = self.numMintedPerUser[pool]!
285            return numMintedDict[userAddress] ?? 0
286        }
287        
288        access(all) view fun getContractName(): String {
289            return self.contractName
290        }
291        
292        access(all) view fun getContractAddress(): Address {
293            return self.contractAddress
294        }
295
296        access(all) view fun getID(): UInt64 {
297            return self.primarySaleID
298        }
299
300        access(SaleAdmin) fun addEntity(entityID: UInt64, pool: String, quantity: UInt64) {
301            if !self.pooledEntities.containsKey(pool) {
302                self.pooledEntities.insert(key: pool, {})
303            }
304            self.pooledEntities[pool]!.insert(key: entityID, quantity)
305        }
306
307        access(SaleAdmin) fun removeEntity(entityID: UInt64, pool: String) {
308            if self.pooledEntities.containsKey(pool) {
309                self.pooledEntities[pool]!.remove(key: entityID)
310            }
311        }
312
313        access(SaleAdmin) fun addEntities(entityIDs: [UInt64], pool: String) {
314            for id in entityIDs {
315                self.addEntity(entityID: id, pool: pool, quantity: 1)
316            }
317        }
318
319        access(SaleAdmin) fun pause() {
320            self.status = PrimarySaleStatus.PAUSED
321        }
322
323        access(SaleAdmin) fun open() {
324            pre {
325                self.status != PrimarySaleStatus.OPEN : "Primary sale is already open"
326                self.status != PrimarySaleStatus.CLOSED : "Cannot re-open primary sale that is closed"
327            }
328
329            self.status = PrimarySaleStatus.OPEN
330        }
331
332        access(SaleAdmin) fun close() {
333            self.status = PrimarySaleStatus.CLOSED
334        }
335        
336        access(self) fun verifyAdminSignedPayload(signedPayloadData: AdminSignedPayload, signature: String): Bool {
337            // Gets the Crypto.KeyList and the public key of the collection's owner
338            let keyList = Crypto.KeyList()
339            let accountKey = self.owner!.keys.get(keyIndex: 0)!.publicKey
340            
341            let publicKey = PublicKey(
342                publicKey: accountKey.publicKey,
343                signatureAlgorithm: accountKey.signatureAlgorithm
344            )
345
346            return publicKey.verify(
347                signature: signature.decodeHex(),
348                signedData: signedPayloadData.toString().utf8,
349                domainSeparationTag: "FLOW-V0.0-user",
350                hashAlgorithm: HashAlgorithm.SHA3_256
351            )
352        }
353
354        access(all) fun purchaseHeroesBox(
355            payment: @{FungibleToken.Vault},
356            data: PurchaseDataSequential,
357            adminSignedPayload: AdminSignedPayload,
358            signature: String
359        ) {
360            pre {
361                self.primarySaleID == data.primarySaleID: "primarySaleID mismatch"
362                self.status == PrimarySaleStatus.OPEN: "primary sale is not open"
363                data.quantity > 0: "must purchase at least one NFT"
364                self.minterCap.borrow() != nil: "cannot borrow minter"
365                adminSignedPayload.expiration >= UInt64(getCurrentBlock().timestamp): "expired signature"
366                self.contractName == "HeroesOfTheFlow": "only supports heroes of the flow contract"
367            }
368
369            assert(
370                self.verifyAdminSignedPayload(signedPayloadData: adminSignedPayload, signature: signature) == true,
371                message: "failed to validate signature for the primary sale purchase"
372            )
373
374            let pool = data.pool
375            let supply = self.getSupply(pool: pool)
376            let totalQuantity = data.quantity * 3
377            assert(totalQuantity <= supply, message: "insufficient supply")
378
379            let orders: [Order] = []
380            var quantityNeeded = totalQuantity
381            var availableEntities = self.getAllAvailableEntities(pool: pool)
382            for entityID in availableEntities.keys {
383                if quantityNeeded > 0 {
384                    var quantityToAdd: UInt64 = availableEntities[entityID]!
385                    if quantityToAdd > quantityNeeded {
386                        quantityToAdd = quantityNeeded
387                    }
388                    orders.append(Order(entityID: entityID, quantity: quantityToAdd))
389                    quantityNeeded = quantityNeeded - quantityToAdd
390                } else {
391                    break
392                }
393            }
394
395            let purchaseData = PurchaseData(
396                primarySaleID: data.primarySaleID,
397                purchaserAddress: data.purchaserAddress,
398                purchaserCollectionRef: data.purchaserCollectionRef,
399                orders: orders,
400                pool: data.pool
401            )
402            
403            self.purchase(payment: <-payment, data: purchaseData)
404        }
405
406        access(all) fun purchaseSequentialNFTs(
407            payment: @{FungibleToken.Vault},
408            data: PurchaseDataSequential,
409            adminSignedPayload: AdminSignedPayload,
410            signature: String
411        ) {
412            pre {
413                self.primarySaleID == data.primarySaleID: "primarySaleID mismatch"
414                self.status == PrimarySaleStatus.OPEN: "primary sale is not open"
415                data.quantity > 0: "must purchase at least one NFT"
416                self.minterCap.borrow() != nil: "cannot borrow minter"
417                adminSignedPayload.expiration >= UInt64(getCurrentBlock().timestamp): "expired signature"
418                self.contractName != "HeroesOfTheFlow": "HeroesOfTheFlow does not support sequential purchase"
419            }
420
421            assert(
422                self.verifyAdminSignedPayload(signedPayloadData: adminSignedPayload, signature: signature) == true,
423                message: "failed to validate signature for the primary sale purchase"
424            )
425
426            let pool = data.pool
427            let supply = self.getSupply(pool: pool)
428            assert(data.quantity <= supply, message: "insufficient supply")
429
430            let orders: [Order] = []
431            var quantityNeeded = data.quantity
432            var availableEntities = self.getAllAvailableEntities(pool: pool)
433            for entityID in availableEntities.keys {
434                if quantityNeeded > 0 {
435                    var quantityToAdd: UInt64 = availableEntities[entityID]!
436                    if quantityToAdd > quantityNeeded {
437                        quantityToAdd = quantityNeeded
438                    }
439                    orders.append(Order(entityID: entityID, quantity: quantityToAdd))
440                    quantityNeeded = quantityNeeded - quantityToAdd
441                } else {
442                    break
443                }
444            }
445
446            let purchaseData = PurchaseData(
447                primarySaleID: data.primarySaleID,
448                purchaserAddress: data.purchaserAddress,
449                purchaserCollectionRef: data.purchaserCollectionRef,
450                orders: orders,
451                pool: data.pool
452            )
453            
454            self.purchase(payment: <-payment, data: purchaseData)
455        }
456
457        access(self) fun purchase(
458            payment: @{FungibleToken.Vault},
459            data: PurchaseData,
460        ) {
461            pre {
462                self.primarySaleID == data.primarySaleID: "primarySaleID mismatch"
463                self.status == PrimarySaleStatus.OPEN: "primary sale is not open"
464                data.orders.length > 0: "must purchase at least one NFT"
465                self.minterCap.borrow() != nil: "cannot borrow minter"
466                self.pooledEntities.containsKey(data.pool):"Pool does not exist"
467            }
468            
469            let priceData = self.prices[data.pool] ?? panic("Invalid pool")
470
471            if priceData.eligibleAddresses != nil {
472                // check if purchaser is in eligible address list
473                if !priceData.eligibleAddresses!.contains(data.purchaserAddress) {
474                     panic("Address is ineligible for purchase")
475                }
476            }
477
478            // Check if payment type is supported
479            let salePaymentVaultType: String = payment.getType().identifier
480            let price: UFix64 = priceData.price[salePaymentVaultType] ?? panic("payment type not supported")
481            
482
483            // Gets the number of NFTs minted by this user
484            let numMintedByUser = self.getNumMintedByUser(userAddress: data.purchaserAddress, pool: data.pool)
485
486            // Check payment amount is correct
487            var totalQuantity: UInt64 = 0
488            for order in data.orders {
489                totalQuantity = totalQuantity + order.quantity
490            }
491
492            // Heroes Of The Flow is sold in a box of 3 NFTs
493            if self.contractName == "HeroesOfTheFlow" {
494                totalQuantity = totalQuantity / 3
495            }
496            
497            assert(payment.balance == price * UFix64(totalQuantity), message: "payment vault does not contain requested price")
498
499            // Check maxMintsPerUser limit
500            if priceData.maxMintsPerUser != nil {
501                assert(totalQuantity + UInt64(numMintedByUser) <= priceData.maxMintsPerUser!, message: "maximum number of mints exceeded")
502            }
503
504            // Deposit payment to payment receiver based on vault type
505            assert(self.paymentReceiverCaps.containsKey(salePaymentVaultType), message: "payment receiver capability does not exist for vault type: ".concat(salePaymentVaultType))
506            let receiver = self.paymentReceiverCaps[salePaymentVaultType]!.borrow()!
507            receiver.deposit(from: <- payment)
508
509            let minter = self.minterCap.borrow()!
510            var i: Int = 0
511            let purchasedNFTIds: [UInt64] = []
512            while i < data.orders.length {
513                let entityID = data.orders[i].entityID
514                let quantity = data.orders[i].quantity
515                let pooledDict = self.pooledEntities[data.pool]!
516                assert(pooledDict.containsKey(entityID) && pooledDict[entityID]! >= quantity, message: "NFT is not available for purchase, entityID: ".concat(entityID.toString()))
517                if pooledDict[entityID]! > quantity {
518                    self.pooledEntities[data.pool]!.insert(key: entityID, pooledDict[entityID]! - quantity)
519                } else {
520                    self.pooledEntities[data.pool]!.remove(key: entityID)
521                }
522
523                var n: UInt64 = 0
524                while n < quantity {
525                    let nft <- minter.mint(entityID: entityID, minterAddress: data.purchaserAddress)
526                    purchasedNFTIds.append(nft.id)
527                    data.purchaserCollectionRef.deposit(token: <-nft)
528                    n = n + 1
529                }
530                i = i + 1
531            }
532            emit PurchaseComplete(primarySaleID: self.primarySaleID, orders: data.orders, nftIDs: purchasedNFTIds, purchaserAddress: data.purchaserAddress, pool: data.pool, price: price, salePaymentVaultType: salePaymentVaultType)
533            // Increments the number of NFTs minted by the user
534            self.numMintedPerUser[data.pool]!.insert(key: data.purchaserAddress, numMintedByUser + totalQuantity)
535        }
536        
537        access(all) fun getAllAvailableEntities(pool: String): {UInt64: UInt64} {
538            var availableEntities: {UInt64: UInt64} = {}
539            assert(self.pooledEntities.containsKey(pool), message: "Pool does not exist")
540            let pooledDict = self.pooledEntities[pool]!
541            for entityID in pooledDict.keys {
542                availableEntities[entityID] = pooledDict[entityID]!
543            }
544            return availableEntities
545        }
546
547        access(all) view fun getPooledEntities(): {String: {UInt64: UInt64}} {
548            return self.pooledEntities
549        }
550
551        access(SaleAdmin) fun updateLaunchDate(date: String) {
552            self.launchDate = date
553        }
554
555        access(SaleAdmin) fun updateEndDate(date: String) {
556            self.endDate = date
557        }
558
559        access(SaleAdmin) fun updatePaymentReceiver(salePaymentVaultType: String, paymentReceiverCap: Capability<&{FungibleToken.Receiver}>) {
560            pre {
561                paymentReceiverCap.borrow() != nil: "Could not borrow payment receiver capability"
562            }
563            self.paymentReceiverCaps[salePaymentVaultType] = paymentReceiverCap
564        }
565    }
566
567    // Admin is a special authorization resource that 
568    // allows the owner to create primary sales
569    //
570    access(all) resource Admin {
571        access(all) fun createPrimarySale(
572            contractName: String,
573            contractAddress: Address,
574            prices: {String: PriceData},
575            minterCap: Capability<&{IMinter}>,
576            paymentReceiverCaps: {String: Capability<&{FungibleToken.Receiver}>},
577            launchDate: String,
578            endDate: String
579        ) {
580            pre {
581                minterCap.borrow() != nil: "Could not borrow minter capability"
582            }
583
584            let key = contractName.concat(contractAddress.toString())
585            assert(!FlowversePrimarySaleV2.primarySaleIDs.containsKey(key), message: "Primary sale with contractName, contractAddress already exists")
586
587            var primarySale <- create PrimarySale(
588                contractName: contractName,
589                contractAddress: contractAddress,
590                prices: prices,
591                minterCap: minterCap,
592                paymentReceiverCaps: paymentReceiverCaps,
593                launchDate: launchDate,
594                endDate: endDate
595            )
596
597            let primarySaleID = FlowversePrimarySaleV2.nextPrimarySaleID
598
599            FlowversePrimarySaleV2.nextPrimarySaleID = FlowversePrimarySaleV2.nextPrimarySaleID + UInt64(1)
600
601            FlowversePrimarySaleV2.primarySales[primarySaleID] <-! primarySale
602        }
603
604        access(all) fun getPrimarySale(primarySaleID: UInt64): auth(SaleAdmin) &PrimarySale? {
605            if FlowversePrimarySaleV2.primarySales.containsKey(primarySaleID) {
606                return (&FlowversePrimarySaleV2.primarySales[primarySaleID] as auth(SaleAdmin) &PrimarySale?)! 
607            }
608            return nil
609        }
610
611        access(all) fun createNewAdmin(): @Admin {
612            return <-create Admin()
613        }
614    }
615
616    access(all) struct PrimarySaleData {
617        access(all) let primarySaleID: UInt64
618        access(all) let contractName: String
619        access(all) let contractAddress: Address
620        access(all) let supply: UInt64
621        access(all) let prices: {String: FlowversePrimarySaleV2.PriceData}
622        access(all) let status: String
623        access(all) let pooledEntities: {String: {UInt64: UInt64}}
624        access(all) let launchDate: String
625        access(all) let endDate: String
626        access(all) let numMintedPerUser: {String: {Address: UInt64}}
627        access(all) let paymentReceivers: {String: Address} 
628
629        init(
630            primarySaleID: UInt64,
631            contractName: String,
632            contractAddress: Address,
633            supply: UInt64,
634            prices: {String: FlowversePrimarySaleV2.PriceData},
635            status: String,
636            pooledEntities: {String: {UInt64: UInt64}},
637            launchDate: String,
638            endDate: String,
639            numMintedPerUser: {String: {Address: UInt64}},
640            paymentReceivers: {String: Address} ,
641        ) {
642            self.primarySaleID = primarySaleID
643            self.contractName = contractName
644            self.contractAddress = contractAddress
645            self.supply = supply
646            self.prices = prices
647            self.status = status
648            self.pooledEntities = pooledEntities
649            self.launchDate = launchDate
650            self.endDate = endDate
651            self.numMintedPerUser = numMintedPerUser
652            self.paymentReceivers = paymentReceivers
653        }
654    }
655
656    access(all) fun getPrimarySaleData(primarySaleID: UInt64): PrimarySaleData {
657        pre {
658            FlowversePrimarySaleV2.primarySales.containsKey(primarySaleID): "Primary sale does not exist"
659        }
660        let primarySale = (&FlowversePrimarySaleV2.primarySales[primarySaleID] as &PrimarySale?)!
661        return PrimarySaleData(
662            primarySaleID: primarySale.getID(),
663            contractName: primarySale.getContractName(),
664            contractAddress: primarySale.getContractAddress(),
665            supply: primarySale.getSupply(pool: nil),
666            prices: primarySale.getPrices(),
667            status: primarySale.getStatus(),
668            pooledEntities: primarySale.getPooledEntities(),
669            launchDate: primarySale.getLaunchDate(),
670            endDate: primarySale.getEndDate(),
671            numMintedPerUser: primarySale.getNumMintedPerUser(),
672            paymentReceivers: primarySale.getPaymentReceivers()
673        )
674    }
675    
676    access(all) view fun getPrice(primarySaleID: UInt64, pool: String, salePaymentVaultType: String): UFix64 {
677        pre {
678            FlowversePrimarySaleV2.primarySales.containsKey(primarySaleID): "Primary sale does not exist"
679        }
680        let primarySale = (&FlowversePrimarySaleV2.primarySales[primarySaleID] as &PrimarySale?)!
681        let prices = primarySale.getPrices()
682        assert(prices.containsKey(pool), message: "pool does not exist")
683        assert(prices[pool]!.price.containsKey(salePaymentVaultType), message: "salePaymentVaultType not supported")
684        return prices[pool]!.price[salePaymentVaultType]!
685    }
686
687    access(all) fun purchaseHeroesBox(
688        payment: @{FungibleToken.Vault},
689        data: PurchaseDataSequential,
690        adminSignedPayload: AdminSignedPayload,
691        signature: String
692    ) {
693        pre {
694            FlowversePrimarySaleV2.primarySales.containsKey(data.primarySaleID): "Primary sale does not exist"
695        }
696        let primarySale = (&FlowversePrimarySaleV2.primarySales[data.primarySaleID] as &PrimarySale?)!
697        primarySale.purchaseHeroesBox(payment: <-payment, data: data, adminSignedPayload: adminSignedPayload, signature: signature)
698    }
699
700    access(all) fun purchaseSequentialNFTs(
701        payment: @{FungibleToken.Vault},
702        data: PurchaseDataSequential,
703        adminSignedPayload: AdminSignedPayload,
704        signature: String
705    ) {
706        pre {
707            FlowversePrimarySaleV2.primarySales.containsKey(data.primarySaleID): "Primary sale does not exist"
708        }
709        let primarySale = (&FlowversePrimarySaleV2.primarySales[data.primarySaleID] as &PrimarySale?)!
710        primarySale.purchaseSequentialNFTs(payment: <-payment, data: data, adminSignedPayload: adminSignedPayload, signature: signature)
711    }
712
713    access(all) view fun getID(contractName: String, contractAddress: Address): UInt64 {
714        let key = contractName.concat(contractAddress.toString())
715        assert(FlowversePrimarySaleV2.primarySaleIDs.containsKey(key), message: "primary sale does not exist")
716        return FlowversePrimarySaleV2.primarySaleIDs[key]!
717    }
718
719    init() {
720        self.AdminStoragePath = /storage/FlowversePrimarySaleV2AdminStoragePath
721
722        self.primarySales <- {}
723        self.primarySaleIDs = {}
724
725        self.nextPrimarySaleID = 1
726        
727        self.account.storage.save<@Admin>(<- create Admin(), to: self.AdminStoragePath)
728    }
729}
730