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