Smart Contract

OpenDoodlePacks

A.e81193c424cfd3fb.OpenDoodlePacks

Deployed

2d ago
Feb 26, 2026, 01:49:14 PM UTC

Dependents

0 imports
1import NonFungibleToken from 0x1d7e57aa55817448
2import MetadataViews from 0x1d7e57aa55817448
3import Random from 0xe81193c424cfd3fb
4import DoodlePackTypes from 0xe81193c424cfd3fb
5import Wearables from 0xe81193c424cfd3fb
6import Redeemables from 0xe81193c424cfd3fb
7import ViewResolver from 0x1d7e57aa55817448
8import Burner from 0xf233dcee88fe0abe
9
10access(all) contract OpenDoodlePacks: NonFungibleToken {
11    access(all) event ContractInitialized()
12    access(all) event Withdraw(id: UInt64, from: Address?)
13    access(all) event Deposit(id: UInt64, to: Address?)
14    access(all) event Burned(id: UInt64)
15    access(all) event Minted(id: UInt64, typeId: UInt64, block: UInt64)
16	access(all) event Revealed(id: UInt64)
17
18    access(all) let CollectionStoragePath: StoragePath
19    access(all) let CollectionPublicPath: PublicPath
20	access(all) let CollectionPrivatePath: PrivatePath
21
22    access(all) var totalSupply: UInt64
23	access(all) var packTypesCurrentSupply: {UInt64: UInt64} // packTypeId => currentSupply
24	access(all) var packTypesTotalBurned: {UInt64: UInt64} // packTypeId => totalBurned
25
26    // Amount of blocks that need to pass before a pack can be revealed.
27    // This is part of the commit-reveal scheme to prevent transaction rollbacks.
28	access(all) var revealBlocks: UInt64
29
30    access(self) let extra: {String: AnyStruct}
31
32    access(all) resource NFT: NonFungibleToken.NFT, ViewResolver.Resolver, Burner.Burnable {
33        access(all) let id: UInt64
34		access(all) let serialNumber: UInt64
35		access(all) let typeId: UInt64
36		access(all) let openedBlock: UInt64
37    
38        init(id: UInt64, serialNumber: UInt64, typeId: UInt64) {
39            if (DoodlePackTypes.getPackType(id: typeId) == nil) {
40                panic("Invalid pack type")
41            }
42            self.id = id
43            self.serialNumber = serialNumber
44            self.typeId = typeId
45            self.openedBlock = getCurrentBlock().height
46
47	        OpenDoodlePacks.totalSupply = OpenDoodlePacks.totalSupply + 1
48            OpenDoodlePacks.packTypesCurrentSupply[typeId] = (OpenDoodlePacks.packTypesCurrentSupply[typeId] ?? 0) + 1
49        }
50
51        access(all) fun getPackType(): DoodlePackTypes.PackType {
52			return DoodlePackTypes.getPackType(id: self.typeId)!
53		}
54
55        access(all) fun canReveal(): Bool {
56            return getCurrentBlock().height >= self.openedBlock + OpenDoodlePacks.revealBlocks
57        }
58
59        access(all) view fun getViews(): [Type] {
60			return [
61				Type<MetadataViews.Display>(),
62				Type<MetadataViews.Royalties>(),
63				Type<MetadataViews.ExternalURL>(),
64				Type<MetadataViews.NFTCollectionDisplay>(),
65				Type<MetadataViews.NFTCollectionData>(),
66				Type<MetadataViews.Traits>(),
67				Type<MetadataViews.Editions>()
68			]
69		}
70
71        access(all) fun resolveView(_ view: Type): AnyStruct? {
72			let packType: DoodlePackTypes.PackType = DoodlePackTypes.getPackType(id: self.typeId)!
73			switch view {
74				case Type<MetadataViews.Display>():
75					return MetadataViews.Display(
76						name: packType.name,
77						description: packType.description,
78						thumbnail: packType.thumbnail.file,
79					)
80				case Type<MetadataViews.ExternalURL>():
81					return MetadataViews.ExternalURL("https://doodles.app")
82				case Type<MetadataViews.Royalties>():
83					return []
84				case Type<MetadataViews.Editions>():
85					return MetadataViews.Editions([
86						MetadataViews.Edition(
87							name: packType.name,
88							number: self.serialNumber,
89							max: nil
90						)
91					])
92				case Type<MetadataViews.Traits>():
93					return MetadataViews.Traits([
94						MetadataViews.Trait(
95							name: "name",
96							value: packType.name,
97							displayType: "string",
98							rarity: nil,
99						),
100                        MetadataViews.Trait(
101							name: "pack_type_id",
102							value: packType.id.toString(),
103							displayType: "string",
104							rarity: nil,
105						)
106					])
107			}
108			return OpenDoodlePacks.resolveContractView(resourceType: Type<@OpenDoodlePacks.NFT>(), viewType: view)
109		}
110
111        access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
112			return <- OpenDoodlePacks.createEmptyCollection(nftType: Type<@OpenDoodlePacks.NFT>())
113		}
114    
115        access(contract) fun burnCallback() {
116            OpenDoodlePacks.packTypesCurrentSupply[self.typeId] = OpenDoodlePacks.packTypesCurrentSupply[self.typeId]! - 1
117            OpenDoodlePacks.packTypesTotalBurned[self.typeId] = (OpenDoodlePacks.packTypesTotalBurned[self.typeId] ?? 0) + 1
118        }
119    }
120
121    access(all) resource interface CollectionPublic {
122        access(all) fun deposit(token: @{NonFungibleToken.NFT})
123        access(all) view fun getIDs(): [UInt64]
124        access(all) fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}?
125        access(all) fun borrowOpenDoodlePack(id: UInt64): &OpenDoodlePacks.NFT? {
126            post {
127                (result == nil) || (result?.id == id):
128                    "Cannot borrow OpenDoodlePacks reference: the ID of the returned reference is incorrect"
129            }
130        }
131    }
132
133    access(all) resource Collection: CollectionPublic, NonFungibleToken.Collection {
134        access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}}
135
136        init () {
137            self.ownedNFTs <- {}
138        }
139
140        access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} {
141            let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("missing NFT")
142
143			emit Withdraw(id: token.id, from: self.owner?.address)
144
145			return <-token
146        }
147
148        access(all) fun deposit(token: @{NonFungibleToken.NFT}) {
149            let token <- token as! @OpenDoodlePacks.NFT
150
151            let id: UInt64 = token.id
152
153            let oldToken <- self.ownedNFTs[id] <- token
154
155            emit Deposit(id: id, to: self.owner?.address)
156
157            destroy oldToken
158        }
159
160        access(all) view fun getIDs(): [UInt64] {
161            return self.ownedNFTs.keys
162        }
163
164        access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? {
165			return &self.ownedNFTs[id]
166		}
167 
168        access(all) fun borrowOpenDoodlePack(id: UInt64): &OpenDoodlePacks.NFT? {
169            if self.ownedNFTs[id] != nil {
170                let ref = (&self.ownedNFTs[id] as &{NonFungibleToken.NFT}?)!
171                return ref as! &OpenDoodlePacks.NFT
172            }
173
174            return nil
175        }
176
177        access(all) view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver} {
178            let nft = (&self.ownedNFTs[id] as &{NonFungibleToken.NFT}?)!
179            let openDoodlePack = nft as! &OpenDoodlePacks.NFT
180            return openDoodlePack
181        }
182
183        access(all) view fun getLength(): Int {
184            return self.ownedNFTs.keys.length
185        }
186
187        access(all) view fun getSupportedNFTTypes(): {Type: Bool} {
188            let supportedTypes: {Type: Bool} = {}
189            supportedTypes[Type<@OpenDoodlePacks.NFT>()] = true
190            return supportedTypes
191        }
192
193        access(all) view fun isSupportedNFTType(type: Type): Bool {
194           if type == Type<@OpenDoodlePacks.NFT>() {
195        	return true
196           } else {
197        	return false
198           }
199		}
200
201        access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
202			return <- OpenDoodlePacks.createEmptyCollection(nftType: Type<@OpenDoodlePacks.NFT>())
203		}
204    }
205
206    access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} {
207        return <- create Collection()
208    }
209
210    access(account) fun mintNFT(id: UInt64, serialNumber: UInt64, typeId: UInt64): @OpenDoodlePacks.NFT {
211        let openPack <- create NFT(id: id, serialNumber: serialNumber, typeId: typeId)
212        emit Minted(id: id, typeId: typeId, block: openPack.openedBlock)
213        return <- openPack
214    }
215
216    access(account) fun updateRevealBlocks(revealBlocks: UInt64) {
217        OpenDoodlePacks.revealBlocks = revealBlocks
218    }
219
220    access(all) struct MintableTemplateDistribution {
221        access(all) let packType: DoodlePackTypes.PackType
222        access(all) var templateDistribution: DoodlePackTypes.TemplateDistribution
223        access(all) let totalProbability: UFix64
224        access(all) var mintedAmount: UInt8
225        access(all) let ponderation: UFix64
226
227        init(packType: DoodlePackTypes.PackType, templateDistribution: DoodlePackTypes.TemplateDistribution) {
228            self.packType = packType
229            self.templateDistribution = templateDistribution
230            self.mintedAmount = 0
231
232            // Calculate total probability of the distribution based on the probability of each template.
233            var totalProbability = 0.0
234            for distribution in templateDistribution.templateProbabilities {
235                totalProbability = totalProbability + distribution.probability
236            }
237            self.totalProbability = totalProbability
238
239            // Only ponderate if there is a limited supply (at distribution and pack levels)
240            if templateDistribution.maxMint == nil || packType.maxSupply == nil {
241                self.ponderation = 1.0
242            } else {
243                // Ponderation will ensure that very low probability templates (like 1:1) are not minted in the last pack,
244                // because fixed probabilities will always benefit the distributions with highest probability.
245                // In an scenario of limited supply, this could lead to situations where a template with 1:1 probability
246                // is minted not because it was selected, but rather because the other templates didn't have any supply left.
247                
248                // Ponderation will ensure that the probability of a template is not fixed, but rather it is adjusted
249                // based on the remaining supply of the distribution.
250                self.ponderation = 1.0 - UFix64(
251                    DoodlePackTypes.getTemplateDistributionMintedCount(
252                        typeId: packType.id,
253                        templateDistributionId: templateDistribution.id
254                    )
255                ) / UFix64(templateDistribution.maxMint!)
256            }
257        }
258
259        access(contract) fun addMinted() {
260            self.mintedAmount = self.mintedAmount + 1
261        }
262    }
263
264    access(all) fun reveal(collection: auth(NonFungibleToken.Withdraw) &{OpenDoodlePacks.CollectionPublic, NonFungibleToken.Provider}, packId: UInt64, receiverAddress: Address) {
265        let pack = collection.borrowOpenDoodlePack(id: packId) ?? panic ("This pack is not in your collection")
266        if !pack.canReveal() {
267            panic("You can only reveal the pack after ".concat(OpenDoodlePacks.revealBlocks.toString()).concat(" blocks"))
268        }
269        let seedHeight = pack.openedBlock + OpenDoodlePacks.revealBlocks
270
271        let packType = pack.getPackType()
272
273        var randomNumbers: [UFix64] = self.getRandomNumbersFromPack(pack: pack, amount: packType.amountOfTokens)
274
275        var remainingDistributions: [OpenDoodlePacks.MintableTemplateDistribution] = []
276        var minAmountToMint: UInt64 = 0
277        for templateDistribution in packType.templateDistributions {
278            if templateDistribution.maxMint != nil
279                && DoodlePackTypes.getTemplateDistributionMintedCount(typeId: packType.id, templateDistributionId: templateDistribution.id) >= templateDistribution.maxMint!
280            {
281                continue
282            }
283            remainingDistributions.append(
284                OpenDoodlePacks.MintableTemplateDistribution(packType: packType, templateDistribution: templateDistribution)
285            )
286            minAmountToMint = minAmountToMint + UInt64(templateDistribution.minAmount)
287        }
288
289        if (minAmountToMint > 0) {
290            remainingDistributions = OpenDoodlePacks.revealRemainingDistributionsMinAmount(
291                receiverAddress: receiverAddress,
292                randomNumbers: randomNumbers,
293                packId: packId,
294                packType: packType,
295                remainingDistributions: remainingDistributions
296            )
297            randomNumbers = randomNumbers.slice(from: Int(minAmountToMint), upTo: randomNumbers.length)
298        }
299
300        while randomNumbers.length > 0 {
301            let randomNumber = randomNumbers.removeFirst()
302            remainingDistributions = OpenDoodlePacks.revealRemainingDistributions(
303                receiverAddress: receiverAddress,
304                packId: packId,
305                packType: packType,
306                randomNumber: randomNumber,
307                remainingDistributions: remainingDistributions
308            )
309        }
310
311        emit Revealed(id: packId)
312
313        destroy <- collection.withdraw(withdrawID: packId)
314        emit Burned(id: packId)
315    }
316
317    access(contract) fun revealRemainingDistributionsMinAmount(
318        receiverAddress: Address,
319        randomNumbers: [UFix64],
320        packId: UInt64,
321        packType: DoodlePackTypes.PackType,
322        remainingDistributions: [OpenDoodlePacks.MintableTemplateDistribution]
323    ): [OpenDoodlePacks.MintableTemplateDistribution] {
324        let completedDistributionIndexes: [Int] = []
325        for index, remainingDistribution in remainingDistributions {
326            let templateDistribution = remainingDistribution.templateDistribution
327            while remainingDistribution.mintedAmount < templateDistribution.minAmount
328                && (templateDistribution.maxMint == nil || DoodlePackTypes.getTemplateDistributionMintedCount(typeId: packType.id, templateDistributionId: templateDistribution.id) < templateDistribution.maxMint!)
329            {
330                var accumulatedProbability: UFix64 = 0.0
331                let random = randomNumbers.remove(at: 0)
332                let adaptedProbability: UFix64 = random * remainingDistribution.totalProbability
333                for templateProbability in templateDistribution.templateProbabilities {
334                    accumulatedProbability = accumulatedProbability + templateProbability.probability
335                    if accumulatedProbability >= adaptedProbability {
336                        OpenDoodlePacks.mintNFTFromPack(
337                            collection: templateProbability.collection,
338                            receiverAddress: receiverAddress,
339                            templateId: templateProbability.templateId,
340                            packId: packId,
341                            packType: packType
342                        )
343                        remainingDistribution.addMinted()
344                        DoodlePackTypes.addMintedCountToTemplateDistribution(
345                            typeId: packType.id,
346                            templateDistributionId: templateDistribution.id,
347                            amount: 1
348                        )
349
350                        if remainingDistribution.mintedAmount == templateDistribution.maxAmount
351                            || (templateDistribution.maxMint != nil && DoodlePackTypes.getTemplateDistributionMintedCount(typeId: packType.id, templateDistributionId: templateDistribution.id) == templateDistribution.maxMint!)
352                        {
353                            completedDistributionIndexes.append(index)
354                        }
355                        break
356                    }
357                }
358            }
359        }
360        let updatedRemainingDistributions: [OpenDoodlePacks.MintableTemplateDistribution] = []
361        for index, remainingDistribution in remainingDistributions {
362            if !completedDistributionIndexes.contains(index) {
363                updatedRemainingDistributions.append(remainingDistribution)
364            }
365        }
366        return updatedRemainingDistributions
367    }
368
369    access(contract) fun revealRemainingDistributions(
370        receiverAddress: Address,
371        packId: UInt64,
372        packType: DoodlePackTypes.PackType,
373        randomNumber: UFix64,
374        remainingDistributions: [OpenDoodlePacks.MintableTemplateDistribution]
375    ): [OpenDoodlePacks.MintableTemplateDistribution] {
376        var remainingPackProbability: UFix64 = 0.0
377        var accumulatedPackProbability: UFix64 = 0.0
378
379        for distribution in remainingDistributions {
380            remainingPackProbability = remainingPackProbability + distribution.totalProbability * distribution.ponderation
381        }
382
383        let adaptedProbability = randomNumber * remainingPackProbability
384
385        var accumulatedProbability: UFix64 = 0.0
386        for index, remainingDistribution in remainingDistributions {
387            let templateDistribution = remainingDistribution.templateDistribution
388            for templateProbability in templateDistribution.templateProbabilities {
389                accumulatedProbability = accumulatedProbability + templateProbability.probability * remainingDistribution.ponderation
390                if accumulatedProbability >= adaptedProbability {
391                    OpenDoodlePacks.mintNFTFromPack(
392                        collection: templateProbability.collection,
393                        receiverAddress: receiverAddress,
394                        templateId: templateProbability.templateId,
395                        packId: packId,
396                        packType: packType
397                    )
398                    remainingDistribution.addMinted()
399                    DoodlePackTypes.addMintedCountToTemplateDistribution(
400                        typeId: packType.id,
401                        templateDistributionId: templateDistribution.id,
402                        amount: 1
403                    )
404                    remainingDistributions.insert(at: index, remainingDistribution)
405                    remainingDistributions.remove(at: index + 1)
406                    if remainingDistribution.mintedAmount == templateDistribution.maxAmount
407                        || (templateDistribution.maxMint != nil  && DoodlePackTypes.getTemplateDistributionMintedCount(typeId: packType.id, templateDistributionId: templateDistribution.id) == templateDistribution.maxMint!)
408                    {
409                        remainingDistributions.remove(at: index)
410                    }
411                    return remainingDistributions
412                }
413            }
414        }
415        return remainingDistributions
416    }
417
418    access(self) fun mintNFTFromPack(
419        collection: DoodlePackTypes.Collection,
420        receiverAddress: Address,
421        templateId: UInt64,
422        packId: UInt64,
423        packType: DoodlePackTypes.PackType
424    ) {
425        switch collection {
426            case DoodlePackTypes.Collection.Wearables:
427                let recipient = getAccount(receiverAddress).capabilities.get<&{NonFungibleToken.Receiver}>(Wearables.CollectionPublicPath)!.borrow()
428                    ?? panic("Could not borrow wearables receiver capability")
429                Wearables.mintNFT(
430                    recipient: recipient,
431                    template: templateId,
432                    context: {
433                        "pack_id": packId.toString(),
434                        "pack_name": packType.name
435                    },
436                )
437            case DoodlePackTypes.Collection.Redeemables:
438                let recipient = getAccount(receiverAddress).capabilities.get<&{NonFungibleToken.Receiver}>(Redeemables.CollectionPublicPath)!.borrow()
439                    ?? panic("Could not borrow redeemables receiver capability")
440                Redeemables.mintNFT(recipient: recipient, templateId: templateId)
441        }
442        
443    }
444
445    access(self) fun getRandomNumbersFromPack(pack: &OpenDoodlePacks.NFT, amount: UInt8): [UFix64] {
446        let hash: [UInt8] = HashAlgorithm.SHA3_256.hash(pack.id.toBigEndianBytes())
447        let blockHash = getBlock(at: pack.openedBlock + OpenDoodlePacks.revealBlocks)!.id
448        for bit in blockHash  {
449			hash.append(bit)
450		}
451        return Random.generateWithBytesSeed(seed: hash, amount: amount)
452    }
453
454    access(all) view fun getContractViews(resourceType: Type?): [Type] {
455        return [
456            Type<MetadataViews.NFTCollectionDisplay>(),
457            Type<MetadataViews.NFTCollectionData>()
458        ]
459    }
460
461    access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
462        switch viewType {
463            case Type<MetadataViews.NFTCollectionDisplay>():
464                return MetadataViews.NFTCollectionDisplay(
465                    name: "Open Doodle Packs",
466                    description: "",
467                    externalURL: MetadataViews.ExternalURL("https://doodles.app"),
468                    squareImage: MetadataViews.Media(file: MetadataViews.IPFSFile(cid: "", path: nil), mediaType:"image/png"),
469                    bannerImage: MetadataViews.Media(file: MetadataViews.IPFSFile(cid: "", path: nil), mediaType:"image/png"),
470                    socials: {
471                        "instagram": MetadataViews.ExternalURL("https://www.instagram.com/thedoodles"),
472                        "discord": MetadataViews.ExternalURL("https://discord.gg/doodles"),
473                        "twitter": MetadataViews.ExternalURL("https://twitter.com/doodles")
474                    }
475                )
476            case Type<MetadataViews.NFTCollectionData>():
477                return MetadataViews.NFTCollectionData(
478             		storagePath: OpenDoodlePacks.CollectionStoragePath,
479					publicPath: OpenDoodlePacks.CollectionPublicPath,
480					publicCollection: Type<&Collection>(),
481					publicLinkedType: Type<&Collection>(),
482					createEmptyCollectionFunction: (fun(): 
483						@{NonFungibleToken.Collection} {return <- OpenDoodlePacks.createEmptyCollection(nftType: Type<@OpenDoodlePacks.NFT>())})
484                )
485        }
486        return nil
487    }
488
489    init() {
490        self.CollectionStoragePath = /storage/OpenDoodlePacksCollection
491        self.CollectionPublicPath = /public/OpenDoodlePacksCollection
492		self.CollectionPrivatePath = /private/OpenDoodlePacksCollection
493
494        self.totalSupply = 0
495		self.packTypesCurrentSupply = {}
496		self.packTypesTotalBurned = {}
497		self.revealBlocks = 1
498
499        self.extra = {}
500
501        self.account.storage.save<@{NonFungibleToken.Collection}>(<- OpenDoodlePacks.createEmptyCollection(nftType: Type<@OpenDoodlePacks.NFT>()), to: OpenDoodlePacks.CollectionStoragePath)
502		let cap = self.account.capabilities.storage.issue<&Collection>(OpenDoodlePacks.CollectionStoragePath)
503		self.account.capabilities.publish(cap, at: OpenDoodlePacks.CollectionPublicPath)
504
505        emit ContractInitialized()
506    }
507}
508