Smart Contract
OpenDoodlePacks
A.e81193c424cfd3fb.OpenDoodlePacks
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