Smart Contract

NFTSales

A.a49cc0ee46c54bfb.NFTSales

Deployed

1d ago
Feb 26, 2026, 09:43:52 PM UTC

Dependents

0 imports
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}