Smart Contract
FlowversePrimarySaleV2
A.9212a87501a8a6a2.FlowversePrimarySaleV2
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