Smart Contract
NFTSales
A.a49cc0ee46c54bfb.NFTSales
1import FungibleToken from 0xf233dcee88fe0abe
2import REVV from 0xd01e482eb680ec9f
3import SHRD from 0xd01e482eb680ec9f
4import MotoGPAdmin from 0xa49cc0ee46c54bfb
5import MotoGPCard from 0xa49cc0ee46c54bfb
6import CardMintAccess from 0xa49cc0ee46c54bfb
7import ContractVersion from 0xa49cc0ee46c54bfb
8import SHRDMintAccess from 0xd01e482eb680ec9f
9import MotoGPCardMetadata from 0xa49cc0ee46c54bfb
10import MotoGPCardSerialPoolV2 from 0xa49cc0ee46c54bfb
11
12pub contract NFTSales : ContractVersion {
13
14 pub fun getVersion(): String {
15 return "1.0.5"
16 }
17
18 pub event OrderIdRegistered(orderId: String)
19 pub event PackOpened(cardIDs: [UInt64], serials: [UInt64], shrdAmount: UFix64)
20
21 pub resource OpenedPack{
22 pub var collection: @MotoGPCard.Collection
23 pub var vault: @SHRD.Vault
24 init(collection_: @MotoGPCard.Collection, vault_: @SHRD.Vault) {
25 self.collection <- collection_
26 self.vault <- vault_
27 }
28 destroy() {
29 // Don't allow destruction if not empty
30 assert(self.collection.getIDs().length == 0, message: "OpenedPack's card collection is not empty")
31 assert(self.vault.balance == 0.0, message: "OpenedPack's shrd vault is not empty")
32 destroy self.collection
33 destroy self.vault
34 }
35 }
36
37 pub enum SalesOpenOverride: UInt8 {
38 pub case NoOverride
39 pub case ForceOpen
40 pub case ForceClose
41 }
42
43 pub resource Sales {
44 pub let id: UInt64
45 pub var salesOpenOverride: UInt8 // Used to override the blocktime-based open status, for emergency situations, e.g. block start time doesn't sync with IRL time
46 pub var maxSHRD: UFix64 //The maximum allowed amount of SHRD which can be minted by this Sales
47 pub var mintedSHRD: UFix64 //Keeps count of how much SHRD has been minted
48 access(self) var shrdPerGrade: {String:UFix64} // grade (rarity, e.g. Legendary) => amount of SHRD
49 pub var cardsPerPack: UInt64 //how many cards will be minted when a pack is opened
50 pub let nonces: {Address: UInt64} //how many packs have been opened for an account
51 pub var price: UFix64 // for use when paying with Vault
52 pub var publicKey: String // used to verify the signature during an open pack call
53 pub var signatureAlgorithm: UInt8 // SignatureAlorithm type is not storable
54 pub var startTime: UFix64 // start time in unix time stamp seconds (not milliseconds), to be compared to block time. Note: block timestamps are UFix64.
55 pub var endTime: UFix64 // end time in unix time stamp seconds (not milliseconds), to be compared to block time. Note: block timestamps are UFix64.
56 pub var maxPerWallet: UInt64 // max number of cards a buyer can mint
57 pub var cardCountByWallet: {Address: UInt64} // keeps track of how many cards a buyer has minted
58 pub var cardCount: UInt64 // the number of cards minted from this Sales resource
59 access(self) var cardTypeWeights: [[UInt32]] // A list of bucket card probability weights. Each inner array holds the weights for one bucket
60 access(self) var cardTypes: [[UInt64]] // A list of bucket card types. Each inner array holds the types for one bucket. Needs to be ordered to match cardTypeWeights's order
61
62 init(
63 maxSHRD: UFix64,
64 shrdPerGrade: {String: UFix64},
65 cardsPerPack: UInt64,
66 price: UFix64,
67 startTime: UFix64,
68 endTime: UFix64,
69 maxPerWallet: UInt64,
70 cardTypeWeights: [[UInt32]], // If empty, should be [], not [[],[],[]]
71 cardTypes: [[UInt64]] // If empty, should be [], not [[],[],[]]
72 ) {
73 pre {
74 NFTSales.isEqual2DArrayLengths(cardTypeWeights, cardTypes) : "inconsistent initial card type weight array lengths"
75 }
76 self.id = self.uuid
77 self.salesOpenOverride = SalesOpenOverride.NoOverride.rawValue
78 self.maxSHRD = maxSHRD
79 self.mintedSHRD = 0.0
80 self.shrdPerGrade = shrdPerGrade
81 self.cardsPerPack = cardsPerPack
82 self.nonces = {}
83 self.price = price
84 self.publicKey = ""
85 self.signatureAlgorithm = SignatureAlgorithm.ECDSA_P256.rawValue// To convert back to enum, use: SignatureAlgorithm(rawValue: self.signatureAlgorithm)
86 self.startTime = startTime
87 self.endTime = endTime
88 self.maxPerWallet = maxPerWallet
89 self.cardCountByWallet = {}
90 self.cardCount = 0
91 self.cardTypeWeights = cardTypeWeights
92 self.cardTypes = cardTypes
93 }
94
95 access(contract) fun addCardTypeWeights(cardTypeWeights: [UInt32], cardTypes: [UInt64]){
96 pre {
97 cardTypeWeights.length == cardTypes.length : "inconsistent card type weight array lengths"
98 }
99 self.cardTypeWeights.append(cardTypeWeights)
100 self.cardTypes.append(cardTypes)
101 }
102
103 access(contract) fun removeCardTypeWeights(at index: UInt64) {
104 self.cardTypeWeights.remove(at: index)
105 self.cardTypes.remove(at: index)
106 }
107
108 access(contract) fun clearCardTypeWeights() {
109 self.cardTypeWeights = []
110 self.cardTypes = []
111 }
112
113 access(contract) fun setSalesOpenOverride(state: SalesOpenOverride) {
114 self.salesOpenOverride = state.rawValue
115 }
116
117 access(contract) fun setMaxSHRD(maxSHRD: UFix64) {
118 self.maxSHRD = maxSHRD
119 }
120
121 access(contract) fun setStartTime(startTime: UFix64) {
122 self.startTime = startTime
123 }
124
125 access(contract) fun setEndTime(endTime: UFix64) {
126 self.endTime = endTime
127 }
128
129 access(contract) fun setPublicKey(publicKey: String, signatureAlgorithm: SignatureAlgorithm) {
130 self.publicKey = publicKey
131 self.signatureAlgorithm = signatureAlgorithm.rawValue
132 }
133
134 access(contract) fun setMaxPerWallet(maxPerWallet: UInt64) {
135 self.maxPerWallet = maxPerWallet
136 }
137
138 access(contract) fun setCardsPerPack(cardsPerPack: UInt64) {
139 self.cardsPerPack = cardsPerPack
140 }
141
142 access(contract) fun setSHRDPerGrade(shrdPerGrade: {String: UFix64}) {
143 self.shrdPerGrade = shrdPerGrade
144 }
145
146 access(contract) fun setPrice(price: UFix64) {
147 self.price = price
148 }
149
150 pub fun getSHRDPerGrade(): {String: UFix64} {
151 return self.shrdPerGrade
152 }
153
154 pub fun isOpen(): Bool {
155 if self.salesOpenOverride == SalesOpenOverride.ForceOpen.rawValue {
156 return true
157 }
158 if self.salesOpenOverride == SalesOpenOverride.ForceClose.rawValue {
159 return false
160 }
161
162 let ts = getCurrentBlock().timestamp
163 return self.startTime <= ts && self.endTime >= ts
164 }
165
166 pub fun openPacksWithCreditCardPayment(
167 signature: String,
168 orderId: String,
169 address: Address,
170 quantity: UInt64,
171 hashAlgorithm: HashAlgorithm
172 ): @OpenedPack {
173
174 pre {
175 NFTSales.orderIds[orderId] == nil : "orderId already used"
176 }
177
178 NFTSales.orderIds[orderId] = getCurrentBlock().height
179
180 let message = address.toString().concat(orderId).concat(self.uuid.toString()).concat(quantity.toString())
181
182 let isValid = NFTSales.isValidSignature(
183 publicKey: self.publicKey,
184 signatureAlgorithm: SignatureAlgorithm(rawValue: self.signatureAlgorithm)!,
185 hashAlgorithm: hashAlgorithm,
186 signature: signature,
187 message: message)
188 if isValid == false {
189 panic("Signature isn't valid")
190 }
191
192 emit OrderIdRegistered(orderId: orderId)
193
194 let res <- self.openPack(
195 signature: signature,
196 address: address,
197 quantity: quantity,
198 message: message
199 )
200
201 return <- res
202 }
203
204 pub fun openPacksWithVaultPayment(
205 signature: String,
206 revvVault: @REVV.Vault,
207 address: Address,
208 quantity: UInt64,
209 hashAlgorithm: HashAlgorithm
210 ): @OpenedPack {
211
212 pre {
213 self.isOpen() : "Sales is not open"
214 revvVault.balance == self.price * UFix64(quantity) : "revvVault balance doesn't match price * quantity"
215 }
216
217 NFTSales.paymentReceiverCap!.borrow()!.deposit(from: <- revvVault)
218
219 if self.nonces[address] == nil {
220 self.nonces[address] = UInt64(1)
221 } else {
222 self.nonces[address] = self.nonces[address]! + UInt64(1)
223 }
224
225 let message = self.nonces[address]!.toString().concat(address.toString()).concat(self.uuid.toString())
226
227 let isValid = NFTSales.isValidSignature(
228 publicKey: self.publicKey,
229 signatureAlgorithm: SignatureAlgorithm(rawValue: self.signatureAlgorithm)!,
230 hashAlgorithm: hashAlgorithm,
231 signature: signature,
232 message: message)
233 if isValid == false {
234 panic("Signature isn't valid")
235 }
236
237 let res <- self.openPack(
238 signature: signature,
239 address: address,
240 quantity: quantity,
241 message: message,
242 )
243
244 return <- res
245 }
246
247 access(contract) fun openPack( // TODO: change to access(self)
248 signature: String,
249 address: Address,
250 quantity: UInt64,
251 message: String
252 ): @OpenedPack {
253
254 // Mint cards
255 let cardIDsAndSerials = self.generateCardIDsAndSerials(signature: signature, quantity: quantity)
256
257 let cardIDs = cardIDsAndSerials[0]
258 let serials = cardIDsAndSerials[1]
259 let collection:@MotoGPCard.Collection <- NFTSales.mintCards(cardIDs: cardIDs, serials: serials)
260
261 // Mint shrd
262 var shrdAmount = self.calculateSHRDAmount(cardIDs: cardIDs)
263 let vault <- SHRD.createEmptyVault() as! @SHRD.Vault
264
265 if (shrdAmount > 0.0) {
266 vault.deposit(from: <- NFTSales.mintSHRD(amount: shrdAmount))
267 }
268
269 // Check and update minted card counts
270 self.cardCount = self.cardCount + UInt64(cardIDs.length)
271
272 if self.cardCountByWallet[address] == nil {
273 self.cardCountByWallet[address] = 0
274 }
275 let newCountForWallet = self.cardCountByWallet[address]! + UInt64(cardIDs.length)
276 if newCountForWallet > self.maxPerWallet {
277 panic("max cards exceeded for this wallet")
278 }
279 self.cardCountByWallet[address] = newCountForWallet
280
281 emit PackOpened(cardIDs: cardIDs, serials: serials, shrdAmount: shrdAmount)
282
283 return <- create OpenedPack(collection_: <- collection, vault_: <- vault)
284 }
285
286 pub fun getNonce(address: Address): UInt64 {
287 return self.nonces[address] ?? 0 as UInt64
288 }
289
290 access(contract) fun digestToUInt64(digest: [UInt8]): UInt64 {
291 return (UInt64(digest[0]) << UInt64(56))
292 + (UInt64(digest[1]) << UInt64(48))
293 + (UInt64(digest[2]) << UInt64(40))
294 + (UInt64(digest[3]) << UInt64(32))
295 + (UInt64(digest[4]) << UInt64(24))
296 + (UInt64(digest[5]) << UInt64(16))
297 + (UInt64(digest[6]) << UInt64(8))
298 + (UInt64(digest[7]))
299 }
300
301 access(contract) fun generateCardIDsAndSerials(signature: String, quantity: UInt64): [[UInt64]; 2] {
302 let res: [[UInt64]; 2] = [[],[]]
303 var index:UInt64 = 0
304 var digest:[UInt8] = signature.decodeHex()
305 let numCards = self.cardsPerPack * quantity
306 let numBuckets = UInt64(self.cardTypeWeights.length)
307
308 while index < numCards {
309 let bucketIndex = index % numBuckets
310
311 digest = index == 0 ? digest : HashAlgorithm.KECCAK_256.hash(digest)
312 var n = self.digestToUInt64(digest: digest)
313 let cardID = self.generateSingleCardIDFromDigest(n: n, bucketIndex: bucketIndex)
314 res[0].append(cardID)
315
316 digest = HashAlgorithm.KECCAK_256.hash(digest)
317 n = self.digestToUInt64(digest: digest)
318 res[1].append(MotoGPCardSerialPoolV2.pickSerial(n: n, cardID: cardID))
319
320 index = index + 1
321 }
322 return res
323 }
324
325 access(contract) fun generateSingleCardIDFromDigest(n: UInt64, bucketIndex: UInt64): UInt64 {
326 let cardTypeWeights = self.cardTypeWeights[bucketIndex]
327 let totalWeight = cardTypeWeights[cardTypeWeights.length - 1]
328 let r = n % UInt64(totalWeight)
329 for i, weight in cardTypeWeights {
330 if r <= UInt64(weight) {
331 return self.cardTypes[bucketIndex][i]
332 }
333 }
334 panic("no cardType matched to weight: ".concat(r.toString().concat(" with totalWeight: ").concat(totalWeight.toString())))
335 }
336
337 access(contract) fun calculateSHRDAmount(cardIDs: [UInt64]): UFix64 {
338 if self.mintedSHRD >= self.maxSHRD {
339 return 0.0
340 }
341 var res = 0.0
342 for cardID in cardIDs {
343 let metadata = MotoGPCardMetadata.getMetadataForCardID(cardID: cardID) ?? panic("cardID ".concat(cardID.toString()).concat(" has no matching metadata"))
344 let grade = metadata.data["grade"] ?? panic("cardID ".concat(cardID.toString()).concat(" metadata has no grade"))
345 let shrdAmount = self.shrdPerGrade[grade] ?? 0.0
346 res = res + shrdAmount
347 }
348 if res + self.mintedSHRD > self.maxSHRD {
349 res = self.maxSHRD - self.mintedSHRD
350 }
351 self.mintedSHRD = self.mintedSHRD + res
352 return res
353 }
354 }
355
356 pub resource interface SalesCollectionPublic {
357 pub fun getIDs(): [UInt64]
358 pub fun borrowSales(salesID: UInt64): &Sales
359 pub fun getCardCount(salesID: UInt64): UInt64
360 }
361
362 pub resource SalesCollection: SalesCollectionPublic {
363
364 pub var salesMap: @{UInt64: Sales}
365
366 pub fun addSales(sales: @Sales) {
367 pre {
368 NFTSales.isPaymentReceiverCapSet() == true : "payment receiver is not set"
369 }
370 let salesID = sales.id
371 let oldItem <- self.salesMap[salesID] <- sales
372 // TODO: Add event
373 destroy oldItem
374 }
375 pub fun removeSales(salesID: UInt64): @Sales {
376 let sales <- self.salesMap.remove(key: salesID) ?? panic("missing Sales")
377 // TODO: Add event
378 return <- sales
379 }
380
381 pub fun setSalesOpenOverride(salesID: UInt64, state: NFTSales.SalesOpenOverride) {
382 self.borrowSales(salesID: salesID).setSalesOpenOverride(state: state)
383 }
384 pub fun setMaxSHRD(salesID: UInt64, maxSHRD: UFix64) {
385 self.borrowSales(salesID: salesID).setMaxSHRD(maxSHRD: maxSHRD)
386 }
387 pub fun setPublicKey(salesID: UInt64, publicKey:String, signatureAlgorithm: SignatureAlgorithm) {
388 self.borrowSales(salesID: salesID).setPublicKey(publicKey: publicKey, signatureAlgorithm: signatureAlgorithm)
389 }
390 pub fun setMaxPerWallet(salesID: UInt64, maxPerWallet: UInt64) {
391 self.borrowSales(salesID: salesID).setMaxPerWallet(maxPerWallet: maxPerWallet)
392 }
393 pub fun setCardPerPack(salesID: UInt64, cardsPerPack: UInt64) {
394 self.borrowSales(salesID: salesID).setCardsPerPack(cardsPerPack: cardsPerPack)
395 }
396 pub fun setStartTime(salesID: UInt64, startTime: UFix64) {
397 self.borrowSales(salesID: salesID).setStartTime(startTime: startTime)
398 }
399 pub fun setEndTime(salesID: UInt64, endTime: UFix64) {
400 self.borrowSales(salesID: salesID).setEndTime(endTime: endTime)
401 }
402 pub fun addCardTypeWeights(salesID: UInt64, cardTypeWeights: [UInt32], cardTypes: [UInt64]) {
403 self.borrowSales(salesID: salesID).addCardTypeWeights(cardTypeWeights: cardTypeWeights, cardTypes: cardTypes)
404 }
405 pub fun setSHRDPerGrade(salesID: UInt64, shrdPerGrade: {String: UFix64}) {
406 self.borrowSales(salesID: salesID).setSHRDPerGrade(shrdPerGrade: shrdPerGrade)
407 }
408 pub fun setPrice(salesID: UInt64, price: UFix64) {
409 self.borrowSales(salesID: salesID).setPrice(price: price)
410 }
411 pub fun removeCardTypeWeights(salesID: UInt64, at index: UInt64) {
412 self.borrowSales(salesID: salesID).removeCardTypeWeights(at: index)
413 }
414 pub fun clearCardTypeWeights(salesID: UInt64) {
415 self.borrowSales(salesID: salesID).clearCardTypeWeights()
416 }
417 pub fun getCardCount(salesID: UInt64): UInt64 {
418 return self.borrowSales(salesID: salesID).cardCount
419 }
420 pub fun getNonceForSales(salesID: UInt64, address: Address): UInt64 {
421 return self.borrowSales(salesID: salesID).getNonce(address: address)
422 }
423 pub fun getIDs(): [UInt64] {
424 return self.salesMap.keys
425 }
426 pub fun borrowSales(salesID: UInt64): &Sales {
427 return (&self.salesMap[salesID] as &Sales?)!
428 }
429 init() {
430 self.salesMap <- {}
431 }
432 destroy(){
433 destroy self.salesMap
434 }
435 }
436
437 pub let SalesCollectionStoragePath: StoragePath
438 pub let SalesCollectionPublicPath: PublicPath
439 pub let SalesCollectionPrivatePath: PrivatePath
440 pub let orderIds: {String:UInt64}
441 pub let VaultPurchaseType: UInt8
442 pub let ExternalPaymentPurchaseType: UInt8
443 access(contract) var shrdMintProxyCap: Capability<&SHRDMintAccess.MintProxy{SHRDMintAccess.MintProxyPrivate}>?
444 access(contract) var cardMintProxyCap: Capability<&CardMintAccess.MintProxy{CardMintAccess.MintProxyPrivate}>?
445 access(contract) var paymentReceiverCap: Capability<&{FungibleToken.Receiver}>?
446
447 pub fun isSHRDMintProxyCapSet(): Bool {
448 return self.shrdMintProxyCap != nil
449 }
450
451 pub fun isCardMintProxyCapSet(): Bool {
452 return self.cardMintProxyCap != nil
453 }
454
455 pub fun isPaymentReceiverCapSet(): Bool {
456 return self.paymentReceiverCap != nil
457 }
458
459 pub fun isOrderIdUsed(orderId: String): Bool {
460 return NFTSales.orderIds[orderId] != nil
461 }
462
463 pub fun getBlockHeightForOrderId(orderId: String): UInt64? {
464 return NFTSales.orderIds[orderId]
465 }
466
467 pub fun createSales(
468 adminRef: &MotoGPAdmin.Admin,
469 maxSHRD: UFix64,
470 shrdPerGrade: {String: UFix64},
471 cardsPerPack: UInt64,
472 price: UFix64,
473 startTime: UFix64,
474 endTime: UFix64,
475 maxPerWallet: UInt64,
476 cardTypeWeights: [[UInt32]],
477 cardTypes: [[UInt64]]
478 ): @Sales {
479 pre {
480 adminRef != nil : "adminRef is nil"
481 }
482 return <- create Sales(
483 maxSHRD: maxSHRD,
484 shrdPerGrade: shrdPerGrade,
485 cardsPerPack: cardsPerPack,
486 price: price,
487 startTime: startTime,
488 endTime: endTime,
489 maxPerWallet: maxPerWallet,
490 cardTypeWeights: cardTypeWeights,
491 cardTypes: cardTypes
492 )
493 }
494
495 pub fun createSalesCollection(adminRef: &MotoGPAdmin.Admin): @SalesCollection {
496 pre {
497 adminRef != nil : "adminRef is nil"
498 }
499 return <- create SalesCollection()
500 }
501
502 pub fun setCardMintProxyCapability(adminRef: &MotoGPAdmin.Admin, capability: Capability<&CardMintAccess.MintProxy{CardMintAccess.MintProxyPrivate}>) {
503 pre {
504 adminRef != nil : "adminRef is nil"
505 capability.check() == true : "capability.check() is false"
506 capability!.borrow() != nil : "can't borrow capability"
507 }
508 self.cardMintProxyCap = capability
509 }
510
511 pub fun setSHRDMintProxyCapability(adminRef: &MotoGPAdmin.Admin, capability: Capability<&SHRDMintAccess.MintProxy{SHRDMintAccess.MintProxyPrivate}>) {
512 pre {
513 adminRef != nil : "adminRef is nil"
514 capability.check() == true : "capability.check() is false"
515 capability!.borrow() != nil : "can't borrow capability"
516 }
517 self.shrdMintProxyCap = capability
518 }
519
520 pub fun setPaymentReceiverCapability(adminRef: &MotoGPAdmin.Admin, capability: Capability<&{FungibleToken.Receiver}>) {
521 pre {
522 adminRef != nil : "adminRef is nil"
523 }
524 self.paymentReceiverCap = capability
525 }
526
527 access(contract) fun isEqual2DArrayLengths(_ array1: [[AnyStruct]], _ array2: [[AnyStruct]]): Bool {
528 if (array1.length != array2.length) {
529 return false
530 }
531
532 for i, innerArray1 in array1 {
533 if (innerArray1.length != array2[i].length) {
534 return false
535 }
536 }
537
538 return true
539 }
540
541 access(contract) fun isValidSignature(
542 publicKey: String,
543 signatureAlgorithm: SignatureAlgorithm,
544 hashAlgorithm: HashAlgorithm,
545 signature: String,
546 message: String): Bool {
547 let pk = PublicKey(
548 publicKey: publicKey.decodeHex(),
549 signatureAlgorithm: signatureAlgorithm
550 )
551
552 let isValid = pk.verify(
553 signature: signature.decodeHex(),
554 signedData: message.utf8,
555 domainSeparationTag: "FLOW-V0.0-user",
556 hashAlgorithm: hashAlgorithm
557 )
558 return isValid
559 }
560
561 access(contract) fun mintSHRD(amount: UFix64): @SHRD.Vault {
562 return <- self.shrdMintProxyCap!.borrow()!.mint(amount: amount)
563 }
564
565 access(contract) fun mintCards(cardIDs: [UInt64], serials: [UInt64]): @MotoGPCard.Collection {
566 pre {
567 cardIDs.length == serials.length : "Inconsistent array lengths"
568 }
569
570 let collection <- MotoGPCard.createEmptyCollection()
571 for index, cardID in cardIDs {
572 let card <- self.cardMintProxyCap!.borrow()!.mint(cardID: cardID, serial: serials[index])
573 collection.deposit(token: <- card)
574 }
575 return <- collection
576 }
577
578 init() {
579 self.orderIds = {}
580 self.shrdMintProxyCap = nil
581 self.cardMintProxyCap = nil
582 self.paymentReceiverCap = nil
583 self.ExternalPaymentPurchaseType = 1
584 self.VaultPurchaseType = 2
585 self.SalesCollectionStoragePath = /storage/salesCollection
586 self.SalesCollectionPublicPath = /public/salesCollection
587 self.SalesCollectionPrivatePath = /private/salesCollection
588 }
589
590}