Smart Contract

FlowversePrimarySale

A.9212a87501a8a6a2.FlowversePrimarySale

Deployed

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

Dependents

0 imports
1/*
2    FlowversePrimarySale.cdc
3
4    Author: Brian Min brian@flowverse.co
5*/
6
7import NonFungibleToken from 0x1d7e57aa55817448
8import FungibleToken from 0xf233dcee88fe0abe
9// TODO: uncomment after FUT is migrated to C1.0
10// import "FlowUtilityToken"
11import FlowversePass from 0x9212a87501a8a6a2
12// TODO: uncomment after Socks is staged for C1.0 migration in mainnet
13// import "FlowverseSocks"
14import Crypto
15
16access(all) contract FlowversePrimarySale {
17    // Entitlements
18    access(all) entitlement SaleAdmin
19
20    access(all) let AdminStoragePath: StoragePath
21
22    // Incremented ID used to create entities
23    access(all) var nextPrimarySaleID: UInt64
24
25    access(contract) var primarySales: @{UInt64: PrimarySale}
26    access(contract) var primarySaleIDs: {String: UInt64}
27
28    access(all) event PrimarySaleCreated(
29        primarySaleID: UInt64,
30        contractName: String,
31        contractAddress: Address,
32        setID: UInt64,
33        prices: {String: PriceData},
34        launchDate: String,
35        endDate: String,
36        pooled: Bool,
37    )
38    access(all) event PrimarySaleStatusChanged(primarySaleID: UInt64, status: String)
39    access(all) event PrimarySaleDateUpdated(primarySaleID: UInt64, date: String, isLaunch: Bool)
40    access(all) event PriceSet(primarySaleID: UInt64, type: String, price: UFix64, eligibleAddresses: [Address]?, maxMintsPerUser: UInt64)
41    access(all) event EntityAdded(primarySaleID: UInt64, entityID: UInt64, pool: String?, quantity: UInt64)
42    access(all) event EntityRemoved(primarySaleID: UInt64, entityID: UInt64, pool: String?)
43    // NFTPurchased is deprecated - please refer to PurchasedOrder event instead
44    // access(all) event NFTPurchased(primarySaleID: UInt64, entityID: UInt64, nftID: UInt64, purchaserAddress: Address, priceType: String, price: UFix64)
45    access(all) event PurchasedOrder(primarySaleID: UInt64, entityID: UInt64, quantity: UInt64, nftIDs: [UInt64], purchaserAddress: Address, priceType: String, price: UFix64)
46    access(all) event ClaimedTreasures(primarySaleID: UInt64, nftIDs: [UInt64], passIDs: [UInt64], sockIDs: [UInt64], claimerAddress: Address, pool: String)
47
48    access(all) resource interface IMinter {
49        access(all) fun mint(entityID: UInt64, minterAddress: Address): @{NonFungibleToken.NFT}
50    }
51    
52    // Data struct signed by admin account - allows accounts to purchase from a primary sale for a period of time.
53    access(all) struct AdminSignedPayload {
54        access(all) let primarySaleID: UInt64
55        access(all) let purchaserAddress: Address
56        access(all) let expiration: UInt64 // unix timestamp
57
58        init(primarySaleID: UInt64, purchaserAddress: Address, expiration: UInt64){
59            self.primarySaleID = primarySaleID
60            self.purchaserAddress = purchaserAddress
61            self.expiration = expiration
62        }
63
64        access(all) view fun toString(): String {
65            return self.primarySaleID.toString().concat("-")
66                .concat(self.purchaserAddress.toString()).concat("-")
67                .concat(self.expiration.toString())
68        }
69    }
70
71    access(all) struct PriceData {
72        access(all) let priceType: String
73        access(all) let eligibleAddresses: [Address]?
74        access(all) let price: UFix64
75        access(all) var maxMintsPerUser: UInt64
76
77        init(priceType: String, eligibleAddresses: [Address]?, price: UFix64, maxMintsPerUser: UInt64?){
78            self.priceType = priceType
79            self.eligibleAddresses = eligibleAddresses
80            self.price = price
81            self.maxMintsPerUser = maxMintsPerUser ?? 10
82        }
83    }
84
85    access(all) struct Order {
86        access(all) let entityID: UInt64
87        access(all) let quantity: UInt64
88
89        init(entityID: UInt64, quantity: UInt64){
90            self.entityID = entityID
91            self.quantity = quantity
92        }
93    }
94
95    access(all) struct PurchaseData {
96        access(all) let primarySaleID: UInt64
97        access(all) let purchaserAddress: Address
98        access(all) let purchaserCollectionRef: &{NonFungibleToken.Receiver}
99        access(all) let orders: [Order]
100        access(all) let priceType: String
101
102        init(primarySaleID: UInt64, purchaserAddress: Address, purchaserCollectionRef: &{NonFungibleToken.Receiver}, orders: [Order], priceType: String){
103            self.primarySaleID = primarySaleID
104            self.purchaserAddress = purchaserAddress
105            self.purchaserCollectionRef = purchaserCollectionRef
106            self.orders = orders
107            self.priceType = priceType
108        }
109    }
110    
111    access(all) struct PurchaseDataRandom {
112        access(all) let primarySaleID: UInt64
113        access(all) let purchaserAddress: Address
114        access(all) let purchaserCollectionRef: &{NonFungibleToken.Receiver}
115        access(all) let quantity: UInt64
116        access(all) let priceType: String
117
118        init(primarySaleID: UInt64, purchaserAddress: Address, purchaserCollectionRef: &{NonFungibleToken.Receiver}, quantity: UInt64, priceType: String){
119            self.primarySaleID = primarySaleID
120            self.purchaserAddress = purchaserAddress
121            self.purchaserCollectionRef = purchaserCollectionRef
122            self.quantity = quantity
123            self.priceType = priceType
124        }
125    }
126
127    access(all) enum PrimarySaleStatus: UInt8 {
128        access(all) case PAUSED
129        access(all) case OPEN
130        access(all) case CLOSED
131    }
132
133    
134    access(all) resource interface PrimarySalePublic {
135        access(all) fun getSupply(pool: String?): UInt64
136        access(all) view fun getPrices(): {String: PriceData}
137        access(all) view fun getStatus(): String
138        access(all) fun purchaseRandomNFTs(
139            payment: @{FungibleToken.Vault},
140            data: PurchaseDataRandom,
141            adminSignedPayload: AdminSignedPayload, 
142            signature: String
143        )
144        access(all) fun purchaseNFTs(
145            payment: @{FungibleToken.Vault},
146            data: PurchaseData,
147            adminSignedPayload: AdminSignedPayload, 
148            signature: String
149        )
150        access(all) fun claimTreasures(
151            primarySaleID: UInt64,
152            pool: String?,
153            claimerAddress: Address,
154            claimerCollectionRef: &{NonFungibleToken.Receiver},
155            adminSignedPayload: AdminSignedPayload,
156            signature: String
157        )
158        access(all) view fun getNumMintedByUser(userAddress: Address, priceType: String): UInt64
159        access(all) view fun getNumMintedPerUser(): {String: {Address: UInt64}}
160        access(all) fun getAllAvailableEntities(pool: String?): {UInt64: UInt64}
161        access(all) view fun getAvailableEntities(): {UInt64: UInt64}
162        access(all) view fun getPooledEntities(): {String: {UInt64: UInt64}}
163        access(all) view fun getLaunchDate(): String
164        access(all) view fun getEndDate(): String
165        access(all) view fun getPooled(): Bool
166        access(all) view fun getContractName(): String
167        access(all) view fun getContractAddress(): Address
168        access(all) view fun getID(): UInt64
169        access(all) view fun getSetID(): UInt64
170    }
171
172    access(all) resource PrimarySale: PrimarySalePublic {
173        access(self) var primarySaleID: UInt64
174        access(self) var contractName: String
175        access(self) var contractAddress: Address
176        access(self) var setID: UInt64
177        access(self) var status: PrimarySaleStatus
178        access(self) var prices: {String: PriceData}
179        access(self) var availableEntities: {UInt64: UInt64}
180        access(self) var pooledEntities: {String: {UInt64: UInt64}}
181        access(self) var numMintedPerUser: {String: {Address: UInt64}}
182        access(self) var launchDate: String
183        access(self) var endDate: String
184        access(self) var pooled: Bool
185
186        access(self) let minterCap: Capability<&{IMinter}>
187        access(self) var paymentReceiverCap: Capability<&{FungibleToken.Receiver}>
188
189        init(
190            contractName: String,
191            contractAddress: Address,
192            setID: UInt64,
193            prices: {String: PriceData},
194            minterCap: Capability<&{IMinter}>,
195            paymentReceiverCap: Capability<&{FungibleToken.Receiver}>,
196            launchDate: String, 
197            endDate: String,
198            pooled: Bool
199        ) {
200            self.contractName = contractName
201            self.contractAddress = contractAddress
202            self.setID = setID
203            self.status = PrimarySaleStatus.PAUSED // primary sale is paused initially
204            self.availableEntities = {}
205            self.pooledEntities = {}
206            self.prices = prices
207
208            self.minterCap = minterCap
209            self.paymentReceiverCap = paymentReceiverCap
210            
211            self.launchDate = launchDate
212            self.endDate = endDate
213
214            self.pooled = pooled
215
216            self.primarySaleID = FlowversePrimarySale.nextPrimarySaleID
217            let key = contractName.concat(contractAddress.toString().concat(setID.toString()))
218            FlowversePrimarySale.primarySaleIDs[key] = self.primarySaleID
219            
220            self.numMintedPerUser = {}
221            for priceType in prices.keys {
222                self.numMintedPerUser.insert(key: priceType, {})
223            }
224
225            emit PrimarySaleCreated(
226                primarySaleID: self.primarySaleID,
227                contractName: contractName,
228                contractAddress: contractAddress,
229                setID: setID,
230                prices: prices,
231                launchDate: launchDate,
232                endDate: endDate,
233                pooled: pooled
234            )
235        }
236
237        access(all) view fun getStatus(): String {
238            if (self.status == PrimarySaleStatus.PAUSED) {
239                return "PAUSED"
240            } else if (self.status == PrimarySaleStatus.OPEN) {
241                return "OPEN"
242            } else if (self.status == PrimarySaleStatus.CLOSED) {
243                return "CLOSED"
244            } else {
245                return ""
246            }
247        }
248
249        access(SaleAdmin) fun setPrice(priceData: PriceData) {
250            self.prices[priceData.priceType] = priceData
251            if !self.numMintedPerUser.containsKey(priceData.priceType) {
252                self.numMintedPerUser.insert(key: priceData.priceType, {})
253            }
254            emit PriceSet(primarySaleID: self.primarySaleID, type: priceData.priceType, price: priceData.price, eligibleAddresses: priceData.eligibleAddresses, maxMintsPerUser: priceData.maxMintsPerUser)
255        }
256
257        access(all) view fun getPrices(): {String: PriceData} {
258            return self.prices
259        }
260
261        access(all) fun getSupply(pool: String?): UInt64 {
262            var supply = UInt64(0)
263            if self.pooled {
264                for poolKey in self.pooledEntities.keys {
265                    if pool == nil || pool == poolKey {
266                        let pooledDict = self.pooledEntities[poolKey]!
267                        for entityID in pooledDict.keys {
268                            supply = supply + pooledDict[entityID]!
269                        }
270                    }
271                }
272            } else {
273                for entityID in self.availableEntities.keys {
274                    supply = supply + self.availableEntities[entityID]!
275                }
276            }
277            return supply
278        }
279
280        access(all) view fun getLaunchDate(): String {
281            return self.launchDate
282        }
283
284        access(all) view fun getEndDate(): String {
285            return self.endDate
286        }
287
288        access(all) fun getPaymentReceiverAddress(): Address? {
289            let receiver = self.paymentReceiverCap.borrow()!
290            if receiver.owner != nil {
291                return receiver.owner!.address
292            }
293            return nil
294        }
295
296        access(all) view fun getPooled(): Bool {
297            return self.pooled
298        }
299
300        access(all) view fun getNumMintedPerUser(): {String: {Address: UInt64}} {
301            return self.numMintedPerUser
302        }
303
304        access(all) view fun getNumMintedByUser(userAddress: Address, priceType: String): UInt64 {
305            assert(self.numMintedPerUser.containsKey(priceType), message: "invalid priceType")
306            let numMintedDict = self.numMintedPerUser[priceType]!
307            return numMintedDict[userAddress] ?? 0
308        }
309        
310        access(all) view fun getContractName(): String {
311            return self.contractName
312        }
313        
314        access(all) view fun getContractAddress(): Address {
315            return self.contractAddress
316        }
317
318        access(all) view fun getID(): UInt64 {
319            return self.primarySaleID
320        }
321
322        access(all) view fun getSetID(): UInt64 {
323            return self.setID
324        }
325
326        access(SaleAdmin) fun addEntity(entityID: UInt64, pool: String?, quantity: UInt64) {
327            if self.pooled {
328                assert(pool != nil, message: "must specify pool")
329                let poolStr = pool!
330                if !self.pooledEntities.containsKey(poolStr) {
331                    self.pooledEntities.insert(key: poolStr, {})
332                }
333                self.pooledEntities[poolStr]!.insert(key: entityID, quantity)
334                emit EntityAdded(primarySaleID: self.primarySaleID, entityID: entityID, pool: poolStr, quantity: quantity)
335            } else {
336                self.availableEntities[entityID] = quantity
337                emit EntityAdded(primarySaleID: self.primarySaleID, entityID: entityID, pool: nil, quantity: quantity)
338            }
339        }
340
341        access(SaleAdmin) fun removeEntity(entityID: UInt64, pool: String?) {
342            if self.pooled {
343                assert(pool != nil, message: "must specify pool")
344                let poolStr = pool!
345                if self.pooledEntities.containsKey(poolStr) {
346                    let entity = self.pooledEntities[poolStr]!.remove(key: entityID)
347                    if entity != nil {
348                        emit EntityRemoved(primarySaleID: self.primarySaleID, entityID: entityID, pool: poolStr)
349                    }
350                }
351            } else {
352                let entity = self.availableEntities.remove(key: entityID)
353                if entity != nil {
354                    emit EntityRemoved(primarySaleID: self.primarySaleID, entityID: entityID, pool: nil)
355                }
356            }
357        }
358
359        access(SaleAdmin) fun addEntities(entityIDs: [UInt64], pool: String?) {
360            for id in entityIDs {
361                self.addEntity(entityID: id, pool: pool, quantity: 1)
362            }
363        }
364
365        access(SaleAdmin) fun pause() {
366            self.status = PrimarySaleStatus.PAUSED
367            emit PrimarySaleStatusChanged(primarySaleID: self.primarySaleID, status: self.getStatus())
368        }
369
370        access(SaleAdmin) fun open() {
371            pre {
372                self.status != PrimarySaleStatus.OPEN : "Primary sale is already open"
373                self.status != PrimarySaleStatus.CLOSED : "Cannot re-open primary sale that is closed"
374            }
375
376            self.status = PrimarySaleStatus.OPEN
377            emit PrimarySaleStatusChanged(primarySaleID: self.primarySaleID, status: self.getStatus())
378        }
379
380        access(SaleAdmin) fun close() {
381            self.status = PrimarySaleStatus.CLOSED
382            emit PrimarySaleStatusChanged(primarySaleID: self.primarySaleID, status: self.getStatus())
383        }
384
385        access(self) fun verifyAdminSignedPayload(signedPayloadData: AdminSignedPayload, signature: String): Bool {
386            // Gets the Crypto.KeyList and the public key of the collection's owner
387            let keyList = Crypto.KeyList()
388            let accountKey = self.owner!.keys.get(keyIndex: 0)!.publicKey
389            
390            let publicKey = PublicKey(
391                publicKey: accountKey.publicKey,
392                signatureAlgorithm: accountKey.signatureAlgorithm
393            )
394
395            return publicKey.verify(
396                signature: signature.decodeHex(),
397                signedData: signedPayloadData.toString().utf8,
398                domainSeparationTag: "FLOW-V0.0-user",
399                hashAlgorithm: HashAlgorithm.SHA3_256
400            )
401        }
402
403        access(all) fun purchaseRandomNFTs(
404            payment: @{FungibleToken.Vault},
405            data: PurchaseDataRandom,
406            adminSignedPayload: AdminSignedPayload,
407            signature: String
408        ) {
409            pre {
410                self.primarySaleID == data.primarySaleID: "primarySaleID mismatch"
411                self.status == PrimarySaleStatus.OPEN: "primary sale is not open"
412                data.quantity > 0: "must purchase at least one NFT"
413                self.minterCap.borrow() != nil: "cannot borrow minter"
414                adminSignedPayload.expiration >= UInt64(getCurrentBlock().timestamp): "expired signature"
415                self.contractName != "FlowverseTreasures": "cannot purchase a Flowverse Treasures NFT"
416            }
417
418            assert(
419                self.verifyAdminSignedPayload(signedPayloadData: adminSignedPayload, signature: signature) == true,
420                message: "failed to validate signature for the primary sale purchase"
421            )
422
423            var pool: String? = nil
424            if self.pooled {
425                pool = data.priceType
426            }
427            
428            let supply = self.getSupply(pool: pool)
429            assert(data.quantity <= supply, message: "insufficient supply")
430
431            let orders: [Order] = []
432            var quantityNeeded = data.quantity
433            var availableEntities = self.getAllAvailableEntities(pool: pool)!
434            for entityID in availableEntities.keys {
435                if quantityNeeded > 0 {
436                    var quantityToAdd: UInt64 = availableEntities[entityID]!
437                    if quantityToAdd > quantityNeeded {
438                        quantityToAdd = quantityNeeded
439                    }
440                    orders.append(Order(entityID: entityID, quantity: quantityToAdd))
441                    quantityNeeded = quantityNeeded - quantityToAdd
442                } else {
443                    break
444                }
445            }
446
447            let purchaseData = PurchaseData(
448                primarySaleID: data.primarySaleID,
449                purchaserAddress: data.purchaserAddress,
450                purchaserCollectionRef: data.purchaserCollectionRef,
451                orders: orders,
452                priceType: data.priceType
453            )
454            
455            self.purchase(payment: <-payment, data: purchaseData)
456        }
457
458        access(all) fun purchaseNFTs(
459            payment: @{FungibleToken.Vault},
460            data: PurchaseData,
461            adminSignedPayload: AdminSignedPayload,
462            signature: String
463        ) {
464            pre {
465                self.primarySaleID == data.primarySaleID: "primarySaleID mismatch"
466                self.status == PrimarySaleStatus.OPEN: "primary sale is not open"
467                data.orders.length > 0: "must purchase at least one NFT"
468                self.minterCap.borrow() != nil: "cannot borrow minter"
469                adminSignedPayload.expiration >= UInt64(getCurrentBlock().timestamp): "expired signature"
470                self.contractName != "FlowverseTreasures": "cannot purchase a Flowverse Treasures NFT"
471            }
472
473            assert(
474                self.verifyAdminSignedPayload(signedPayloadData: adminSignedPayload, signature: signature) == true,
475                message: "failed to validate signature for the primary sale purchase"
476            )
477
478            self.purchase(payment: <-payment, data: data)
479        }
480
481        access(all) fun claimTreasures(
482            primarySaleID: UInt64,
483            pool: String?,
484            claimerAddress: Address,
485            claimerCollectionRef: &{NonFungibleToken.Receiver},
486            adminSignedPayload: AdminSignedPayload,
487            signature: String
488        ) {
489            pre {
490                self.primarySaleID == primarySaleID: "primarySaleID mismatch"
491                self.status == PrimarySaleStatus.OPEN: "primary sale is not open"
492                self.minterCap.borrow() != nil: "cannot borrow minter"
493                adminSignedPayload.expiration >= UInt64(getCurrentBlock().timestamp): "expired signature"
494                self.contractName == "FlowverseTreasures": "primary sale must be for a Flowverse Treasure"
495            }
496            
497            assert(
498                self.verifyAdminSignedPayload(signedPayloadData: adminSignedPayload, signature: signature) == true,
499                message: "failed to validate signature for the primary sale purchase"
500            )
501
502            // Get available entity IDs
503            let availableEntityIDs = self.getAllAvailableEntities(pool: pool)!.keys
504            assert(availableEntityIDs.length > 0, message: "No available entities")
505
506            // Check if claimer is eligible for treasure based on whether they own a Flowverse Pass
507            let mysteryPassCollectionRef = getAccount(claimerAddress).capabilities.borrow<&{NonFungibleToken.Collection}>(
508                /public/FlowversePassCollection
509            ) ?? panic("FlowversePass Collection reference not found")
510            var passIDs = mysteryPassCollectionRef.getIDs()
511            let numPassesOwned = passIDs.length
512            assert(numPassesOwned > 0, message: "ineligible for treasure claim as user does not own a Flowverse Pass")
513
514            // If pool is sockholder, check if claimer owns a Flowverse Sock
515            var sockIDs: [UInt64] = []
516            var numSocksOwned = 0
517
518            // TODO: uncomment after Socks is staged for C1.0 migration in mainnet
519            // if pool == "sockholders" {
520            //     let socksCollectionRef = getAccount(claimerAddress).capabilities.borrow<&{NonFungibleToken.Collection}>(
521            //         /public/MatrixMarketFlowverseSocksCollection
522            //     ) ?? panic("FlowverseSocks Collection reference not found")
523            //     sockIDs = socksCollectionRef.getIDs()
524            //     numSocksOwned = sockIDs.length
525            //     passIDs = []
526            //     assert(numSocksOwned > 0, message: "ineligible for treasure claim in sockholder pool as user does not own a Flowverse Sock")
527            // }
528
529            let priceType = pool ?? "public"
530
531            // Gets the number of treasure NFTs minted by this user in the current pool
532            let numMintedByUser = self.getNumMintedByUser(userAddress: claimerAddress, priceType: priceType)
533
534            // Checks if the user has already claimed all their treasure NFTs
535            var quantity = UInt64(numPassesOwned) - numMintedByUser
536            if pool == "sockholders" {
537                quantity = UInt64(numSocksOwned) - numMintedByUser
538            }
539            assert(quantity > 0, message: "User has already claimed all their treasure NFTs")
540
541            let minter = self.minterCap.borrow()!
542            var n: UInt64 = 0
543            let claimedNFTIDs: [UInt64] = []
544            while n < quantity {
545                let randomIndex = revertibleRandom<UInt64>(modulo: UInt64(availableEntityIDs.length))
546                let entityID = availableEntityIDs[randomIndex]
547                let nft <- minter.mint(entityID: entityID, minterAddress: claimerAddress)
548                claimedNFTIDs.append(nft.id)
549                claimerCollectionRef.deposit(token: <-nft)
550                n = n + 1
551            }
552            emit ClaimedTreasures(primarySaleID: primarySaleID, nftIDs: claimedNFTIDs, passIDs: passIDs, sockIDs: sockIDs, claimerAddress: claimerAddress, pool: priceType)
553            // Increments the number of NFTs minted by the user
554            self.numMintedPerUser[priceType]!.insert(key: claimerAddress, numMintedByUser + quantity)
555        }
556
557        access(self) fun purchase(
558            payment: @{FungibleToken.Vault},
559            data: PurchaseData,
560        ) {
561            pre {
562                self.primarySaleID == data.primarySaleID: "primarySaleID mismatch"
563                self.status == PrimarySaleStatus.OPEN: "primary sale is not open"
564                data.orders.length > 0: "must purchase at least one NFT"
565                self.minterCap.borrow() != nil: "cannot borrow minter"
566            }
567            
568            let priceData = self.prices[data.priceType] ?? panic("Invalid price type")
569
570            if priceData.eligibleAddresses != nil {
571                // check if purchaser is in eligible address list
572                if !priceData.eligibleAddresses!.contains(data.purchaserAddress) {
573                     panic("Address is ineligible for purchase")
574                }
575            }
576
577            if self.pooled {
578                assert(self.pooledEntities.containsKey(data.priceType), message: "Pool does not exist for price type")
579            }
580
581            // Gets the number of NFTs minted by this user
582            let numMintedByUser = self.getNumMintedByUser(userAddress: data.purchaserAddress, priceType: data.priceType)
583
584            // check if purchaser does not exceed maxMintsPerUser limit
585            var totalQuantity: UInt64 = 0
586            for order in data.orders {
587                totalQuantity = totalQuantity + order.quantity
588            }
589            assert(totalQuantity + UInt64(numMintedByUser) <= priceData.maxMintsPerUser, message: "maximum number of mints exceeded")
590
591            assert(payment.balance == priceData.price * UFix64(totalQuantity), message: "payment vault does not contain requested price")
592
593            var receiver = self.paymentReceiverCap.borrow()!
594
595            // TODO: uncomment after FUT is migrated to C1.0
596            // // check if payment is in FUT (FlowUtilityCoin used by Dapper)
597            // if payment.isInstance(Type<@FlowUtilityToken.Vault>()) {
598            //     let dapperFUTCapability = getAccount(0x36b7cdd242ce29c0).capabilities.get<&{FungibleToken.Receiver}>(/public/flowUtilityTokenReceiver)!
599            //     receiver = dapperFUTCapability.borrow()!
600            // }
601
602            receiver.deposit(from: <- payment)
603
604            let minter = self.minterCap.borrow()!
605            var i: Int = 0
606            while i < data.orders.length {
607                let entityID = data.orders[i].entityID
608                let quantity = data.orders[i].quantity
609                if self.pooled {
610                    let pooledDict = self.pooledEntities[data.priceType]!
611                    assert(pooledDict.containsKey(entityID) && pooledDict[entityID]! >= quantity, message: "NFT is not available for purchase: ".concat(entityID.toString()))
612                    if pooledDict[entityID]! > quantity {
613                        self.pooledEntities[data.priceType]!.insert(key: entityID, pooledDict[entityID]! - quantity)
614                    } else {
615                        self.pooledEntities[data.priceType]!.remove(key: entityID)
616                    }
617                } else {
618                    assert(self.availableEntities.containsKey(entityID) && self.availableEntities[entityID]! >= quantity, message: "NFT is not available for purchase: ".concat(entityID.toString()))
619                    if self.availableEntities[entityID]! > quantity {
620                        self.availableEntities[entityID] = self.availableEntities[entityID]! - quantity
621                    } else {
622                        self.availableEntities.remove(key: entityID)
623                    }
624                }
625                var n: UInt64 = 0
626                let purchasedNFTIds: [UInt64] = []
627                while n < quantity {
628                    let nft <- minter.mint(entityID: entityID, minterAddress: data.purchaserAddress)
629                    purchasedNFTIds.append(nft.id)
630                    data.purchaserCollectionRef.deposit(token: <-nft)
631                    n = n + 1
632                }
633                i = i + 1
634                emit PurchasedOrder(primarySaleID: self.primarySaleID, entityID: entityID, quantity: quantity, nftIDs: purchasedNFTIds, purchaserAddress: data.purchaserAddress, priceType: data.priceType, price: priceData.price)
635            }
636            // Increments the number of NFTs minted by the user
637            self.numMintedPerUser[data.priceType]!.insert(key: data.purchaserAddress, numMintedByUser + totalQuantity)
638        }
639        
640        access(all) fun getAllAvailableEntities(pool: String?): {UInt64: UInt64} {
641            var availableEntities: {UInt64: UInt64} = {}
642            if pool != nil {
643                assert(self.pooledEntities.containsKey(pool!), message: "Pool does not exist")
644                let pooledDict = self.pooledEntities[pool!]!
645                for entityID in pooledDict.keys {
646                    availableEntities[entityID] = pooledDict[entityID]!
647                }
648            } else {
649                for entityID in self.availableEntities.keys {
650                    availableEntities[entityID] = self.availableEntities[entityID]!
651                }
652            }
653            return availableEntities
654        }
655
656        access(all) view fun getAvailableEntities(): {UInt64: UInt64} {
657            return self.availableEntities
658        }
659
660        access(all) view fun getPooledEntities(): {String: {UInt64: UInt64}} {
661            return self.pooledEntities
662        }
663
664        access(SaleAdmin) fun updateLaunchDate(date: String) {
665            self.launchDate = date
666            emit PrimarySaleDateUpdated(primarySaleID: self.primarySaleID, date: date, isLaunch: true)
667        }
668
669        access(SaleAdmin) fun updateEndDate(date: String) {
670            self.endDate = date
671            emit PrimarySaleDateUpdated(primarySaleID: self.primarySaleID, date: date, isLaunch: false)
672        }
673
674        access(SaleAdmin) fun updatePaymentReceiver(paymentReceiverCap: Capability<&{FungibleToken.Receiver}>) {
675            pre {
676                paymentReceiverCap.borrow() != nil: "Could not borrow payment receiver capability"
677            }
678            self.paymentReceiverCap = paymentReceiverCap
679        }
680    }
681
682    // Admin is a special authorization resource that 
683    // allows the owner to create primary sales
684    //
685    access(all) resource Admin {
686        access(all) fun createPrimarySale(
687            contractName: String,
688            contractAddress: Address,
689            setID: UInt64,
690            prices: {String: PriceData},
691            minterCap: Capability<&{IMinter}>,
692            paymentReceiverCap: Capability<&{FungibleToken.Receiver}>,
693            launchDate: String,
694            endDate: String,
695            pooled: Bool
696        ) {
697            pre {
698                minterCap.borrow() != nil: "Could not borrow minter capability"
699                paymentReceiverCap.borrow() != nil: "Could not borrow payment receiver capability"
700            }
701
702            let key = contractName.concat(contractAddress.toString().concat(setID.toString()))
703            assert(!FlowversePrimarySale.primarySaleIDs.containsKey(key), message: "Primary sale with contractName, contractAddress, setID already exists")
704
705            var primarySale <- create PrimarySale(
706                contractName: contractName,
707                contractAddress: contractAddress,
708                setID: setID,
709                prices: prices,
710                minterCap: minterCap,
711                paymentReceiverCap: paymentReceiverCap,
712                launchDate: launchDate,
713                endDate: endDate,
714                pooled: pooled
715            )
716
717            let primarySaleID = FlowversePrimarySale.nextPrimarySaleID
718
719            FlowversePrimarySale.nextPrimarySaleID = FlowversePrimarySale.nextPrimarySaleID + UInt64(1)
720
721            FlowversePrimarySale.primarySales[primarySaleID] <-! primarySale
722        }
723
724        access(all) fun getPrimarySale(primarySaleID: UInt64): auth(SaleAdmin) &PrimarySale? {
725            if FlowversePrimarySale.primarySales.containsKey(primarySaleID) {
726                return (&FlowversePrimarySale.primarySales[primarySaleID] as auth(SaleAdmin) &PrimarySale?)! 
727            }
728            return nil
729        }
730
731        access(all) fun createNewAdmin(): @Admin {
732            return <-create Admin()
733        }
734    }
735
736    access(all) struct PrimarySaleData {
737        access(all) let primarySaleID: UInt64
738        access(all) let contractName: String
739        access(all) let contractAddress: Address
740        access(all) let setID: UInt64
741        access(all) let supply: UInt64
742        access(all) let prices: {String: FlowversePrimarySale.PriceData}
743        access(all) let status: String
744        access(all) let availableEntities: {UInt64: UInt64}
745        access(all) let pooledEntities: {String: {UInt64: UInt64}}
746        access(all) let launchDate: String
747        access(all) let endDate: String
748        access(all) let pooled: Bool
749        access(all) let numMintedPerUser: {String: {Address: UInt64}}
750
751        init(
752            primarySaleID: UInt64,
753            contractName: String,
754            contractAddress: Address,
755            setID: UInt64,
756            supply: UInt64,
757            prices: {String: FlowversePrimarySale.PriceData},
758            status: String,
759            availableEntities: {UInt64: UInt64},
760            pooledEntities: {String: {UInt64: UInt64}},
761            launchDate: String,
762            endDate: String,
763            pooled: Bool,
764            numMintedPerUser: {String: {Address: UInt64}}
765        ) {
766            self.primarySaleID = primarySaleID
767            self.contractName = contractName
768            self.contractAddress = contractAddress
769            self.setID = setID
770            self.supply = supply
771            self.prices = prices
772            self.status = status
773            self.availableEntities = availableEntities
774            self.pooledEntities = pooledEntities
775            self.launchDate = launchDate
776            self.endDate = endDate
777            self.pooled = pooled
778            self.numMintedPerUser = numMintedPerUser
779        }
780    }
781
782    access(all) fun getPrimarySaleData(primarySaleID: UInt64): PrimarySaleData {
783        pre {
784            FlowversePrimarySale.primarySales.containsKey(primarySaleID): "Primary sale does not exist"
785        }
786        let primarySale = (&FlowversePrimarySale.primarySales[primarySaleID] as &PrimarySale?)!
787        return PrimarySaleData(
788            primarySaleID: primarySale.getID(),
789            contractName: primarySale.getContractName(),
790            contractAddress: primarySale.getContractAddress(),
791            setID: primarySale.getSetID(),
792            supply: primarySale.getSupply(pool: nil),
793            prices: primarySale.getPrices(),
794            status: primarySale.getStatus(),
795            availableEntities: primarySale.getAvailableEntities(),
796            pooledEntities: primarySale.getPooledEntities(),
797            launchDate: primarySale.getLaunchDate(),
798            endDate: primarySale.getEndDate(),
799            pooled: primarySale.getPooled(),
800            numMintedPerUser: primarySale.getNumMintedPerUser()
801        )
802    }
803
804    access(all) fun getPaymentReceiverAddress(primarySaleID: UInt64): Address? {
805        pre {
806            FlowversePrimarySale.primarySales.containsKey(primarySaleID): "Primary sale does not exist"
807        }
808        let primarySale = (&FlowversePrimarySale.primarySales[primarySaleID] as &PrimarySale?)!
809        return primarySale.getPaymentReceiverAddress()
810    }
811    
812    access(all) view fun getPrice(primarySaleID: UInt64, type: String): UFix64 {
813        pre {
814            FlowversePrimarySale.primarySales.containsKey(primarySaleID): "Primary sale does not exist"
815        }
816        let primarySale = (&FlowversePrimarySale.primarySales[primarySaleID] as &PrimarySale?)!
817        let prices = primarySale.getPrices()
818        assert(prices.containsKey(type), message: "price type does not exist")
819        return prices[type]!.price
820    }
821
822    access(all) fun purchaseRandomNFTs(
823        payment: @{FungibleToken.Vault},
824        data: PurchaseDataRandom,
825        adminSignedPayload: AdminSignedPayload,
826        signature: String
827    ) {
828        pre {
829            FlowversePrimarySale.primarySales.containsKey(data.primarySaleID): "Primary sale does not exist"
830        }
831        let primarySale = (&FlowversePrimarySale.primarySales[data.primarySaleID] as &PrimarySale?)!
832        primarySale.purchaseRandomNFTs(payment: <-payment, data: data, adminSignedPayload: adminSignedPayload, signature: signature)
833    }
834
835    access(all) fun purchaseNFTs(
836        payment: @{FungibleToken.Vault},
837        data: PurchaseData,
838        adminSignedPayload: AdminSignedPayload,
839        signature: String
840    ) {
841        pre {
842            FlowversePrimarySale.primarySales.containsKey(data.primarySaleID): "Primary sale does not exist"
843        }
844        let primarySale = (&FlowversePrimarySale.primarySales[data.primarySaleID] as &PrimarySale?)!
845        primarySale.purchaseNFTs(payment: <-payment, data: data, adminSignedPayload: adminSignedPayload, signature: signature)
846    }
847
848    access(all) fun claimTreasures(
849            primarySaleID: UInt64,
850            pool: String?,
851            claimerAddress: Address,
852            claimerCollectionRef: &{NonFungibleToken.Receiver},
853            adminSignedPayload: AdminSignedPayload,
854            signature: String
855        ){
856        pre {
857            FlowversePrimarySale.primarySales.containsKey(primarySaleID): "Primary sale does not exist"
858        }
859        let primarySale = (&FlowversePrimarySale.primarySales[primarySaleID] as &PrimarySale?)!
860        primarySale.claimTreasures(
861            primarySaleID: primarySaleID,
862            pool: pool,
863            claimerAddress: claimerAddress,
864            claimerCollectionRef: claimerCollectionRef,
865            adminSignedPayload: adminSignedPayload,
866            signature: signature
867        )
868    }
869    
870    access(all) view fun getID(contractName: String, contractAddress: Address, setID: UInt64): UInt64 {
871        let key = contractName.concat(contractAddress.toString().concat(setID.toString()))
872        assert(FlowversePrimarySale.primarySaleIDs.containsKey(key), message: "primary sale does not exist")
873        return FlowversePrimarySale.primarySaleIDs[key]!
874    }
875
876    init() {
877        self.AdminStoragePath = /storage/FlowversePrimarySaleAdminStoragePath
878
879        self.primarySales <- {}
880        self.primarySaleIDs = {}
881
882        self.nextPrimarySaleID = 1
883        
884        self.account.storage.save<@Admin>(<- create Admin(), to: self.AdminStoragePath)
885    }
886}
887