Smart Contract

DoodlePacks

A.e81193c424cfd3fb.DoodlePacks

Deployed

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

Dependents

0 imports
1import FungibleToken from 0xf233dcee88fe0abe
2import NonFungibleToken from 0x1d7e57aa55817448
3import MetadataViews from 0x1d7e57aa55817448
4import DoodlePackTypes from 0xe81193c424cfd3fb
5import OpenDoodlePacks from 0xe81193c424cfd3fb
6import Burner from 0xf233dcee88fe0abe
7import ViewResolver from 0x1d7e57aa55817448
8
9access(all) contract DoodlePacks: NonFungibleToken {
10	access(all) event ContractInitialized()
11	access(all) event Withdraw(id: UInt64, from: Address?)
12	access(all) event Deposit(id: UInt64, to: Address?)
13	access(all) event Minted(id: UInt64, typeId:UInt64)
14	access(all) event Opened(id: UInt64, address: Address)
15
16	access(all) let CollectionStoragePath: StoragePath
17	access(all) let CollectionPublicPath: PublicPath
18
19	access(all) var totalSupply: UInt64
20	access(all) var currentPackTypesSerialNumber: {UInt64: UInt64} // packTypeId => currentPackTypeSerialNumber
21	access(all) var packTypesCurrentSupply: {UInt64: UInt64} // packTypeId => currentSupply
22
23	access(self) let extra: {String: AnyStruct}
24
25	access(all) resource NFT: NonFungibleToken.NFT, ViewResolver.Resolver, Burner.Burnable {
26		access(all) let id: UInt64
27		access(all) let serialNumber: UInt64
28		access(all) let typeId: UInt64
29
30		init(typeId: UInt64) {
31			if (DoodlePackTypes.getPackType(id: typeId) == nil) {
32                panic("Invalid pack type")
33            }
34			DoodlePacks.totalSupply = DoodlePacks.totalSupply + 1
35			DoodlePacks.currentPackTypesSerialNumber[typeId] = (DoodlePacks.currentPackTypesSerialNumber[typeId] ?? 0) + 1
36			DoodlePacks.packTypesCurrentSupply[typeId] = (DoodlePacks.packTypesCurrentSupply[typeId] ?? 0) + 1
37
38			self.id = self.uuid
39			self.serialNumber = DoodlePacks.currentPackTypesSerialNumber[typeId]!
40			self.typeId = typeId
41		}
42
43		access(all) fun getPackType(): DoodlePackTypes.PackType {
44			return DoodlePackTypes.getPackType(id: self.typeId)!
45		}
46
47		access(contract) fun burnCallback() {
48            DoodlePacks.packTypesCurrentSupply[self.typeId] = DoodlePacks.packTypesCurrentSupply[self.typeId]! - 1
49		}
50
51		access(all) view fun getViews(): [Type] {
52			return [
53				Type<MetadataViews.Display>(),
54				Type<MetadataViews.Royalties>(),
55				Type<MetadataViews.ExternalURL>(),
56				Type<MetadataViews.NFTCollectionDisplay>(),
57				Type<MetadataViews.NFTCollectionData>(),
58				Type<MetadataViews.Traits>(),
59				Type<MetadataViews.Editions>()
60			]
61		}
62
63		access(all) fun resolveView(_ view: Type): AnyStruct? {
64			let packType: DoodlePackTypes.PackType = DoodlePackTypes.getPackType(id: self.typeId)!
65			switch view {
66				case Type<MetadataViews.Display>():
67					return MetadataViews.Display(
68						name: packType.name,
69						description: packType.description,
70						thumbnail: packType.thumbnail.file,
71					)
72				case Type<MetadataViews.ExternalURL>():
73					return MetadataViews.ExternalURL("https://doodles.app")
74				case Type<MetadataViews.Royalties>():
75					// let cut: UFix64 = packType.saleInfo.secondaryMarketCut
76					// let dapperReceiver = DoodlePackTypes.getPaymentReceiver(paymentToken: DoodlePackTypes.PaymentToken.DUC)!
77					// let dapperReceiverAddress = dapperReceiver.address
78					let dapperReceiverAddress = 0x014e9ddc4aaaf557
79					let cut = 0.05
80
81					let description: String = "Doodle Royalty"
82
83					let doodlesMerchantAccountMainnet="0x014e9ddc4aaaf557"
84					//royalties if we sell on something else then DapperWallet cannot go to the address stored in the contract, and Dapper will not allow us to setup forwarders for Flow/USDC
85					if dapperReceiverAddress.toString() == doodlesMerchantAccountMainnet {
86						//this is an account that have setup a forwarder for DUC/FUT to the merchant account of Doodles.
87						let royaltyAccountWithDapperForwarder = getAccount(0x12be92985b852cb8)
88						let cap = royaltyAccountWithDapperForwarder.capabilities.get<&{FungibleToken.Receiver}>(/public/fungibleTokenSwitchboardPublic)!
89						return MetadataViews.Royalties([MetadataViews.Royalty(receiver:cap, cut: cut, description: description)])
90					}
91					let doodlesMerchanAccountTestnet="0xd5b1a1553d0ed52e"
92					if dapperReceiverAddress.toString() == doodlesMerchanAccountTestnet {
93						//on testnet we just send this to the main vault, it is not important
94						let cap = DoodlePacks.account.capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)!
95						return MetadataViews.Royalties([MetadataViews.Royalty(receiver:cap, cut: cut, description: description)])
96					}
97				case Type<MetadataViews.Editions>():
98					return MetadataViews.Editions([
99						MetadataViews.Edition(
100							name: packType.name,
101							number: self.serialNumber,
102							max: nil
103						)
104					])
105				case Type<MetadataViews.Traits>():
106					return MetadataViews.Traits([
107						MetadataViews.Trait(
108							name: "name",
109							value: packType.name,
110							displayType: "string",
111							rarity: nil,
112						),
113						MetadataViews.Trait(
114							name: "pack_type_id",
115							value: packType.id.toString(),
116							displayType: "string",
117							rarity: nil,
118						)
119					])
120			}
121			return DoodlePacks.resolveContractView(resourceType: Type<@DoodlePacks.NFT>(), viewType: view)
122		}
123
124		access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
125			return <- DoodlePacks.createEmptyCollection(nftType: Type<@DoodlePacks.NFT>())
126		}
127	}
128
129	access(all) resource interface CollectionPublic {
130	    access(all) fun deposit(token: @{NonFungibleToken.NFT})
131        access(all) view fun getIDs(): [UInt64]
132        access(all) fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}?
133		access(all) fun contains(_ id: UInt64): Bool
134		access(all) fun getPacksLeft(): Int
135		access(all) fun borrowDoodlePack(id: UInt64): &DoodlePacks.NFT? 
136	}
137
138	access(all) resource Collection: CollectionPublic, NonFungibleToken.Collection{
139		access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}}
140
141		access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} {
142			let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("Could not withdraw nft")
143
144			let nft <- token
145
146			emit Withdraw(id: nft.id, from: self.owner?.address)
147
148			return <-nft
149		}
150
151		access(all) fun deposit(token: @{NonFungibleToken.NFT}) {
152			let token <- token as! @DoodlePacks.NFT
153
154			let id: UInt64 = token.id
155
156			let oldToken <- self.ownedNFTs[id] <- token
157
158			emit Deposit(id: id, to: self.owner?.address)
159
160			destroy oldToken
161		}
162
163		access(all) view fun getIDs(): [UInt64] {
164			return self.ownedNFTs.keys
165		}
166
167		access(all) fun contains(_ id: UInt64): Bool {
168			return self.ownedNFTs.containsKey(id)
169		}
170
171		access(all) fun getPacksLeft(): Int {
172			return self.ownedNFTs.length
173		}
174
175        access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? {
176			return &self.ownedNFTs[id]
177		}
178		
179		access(all) fun borrowDoodlePack(id: UInt64): &DoodlePacks.NFT? {
180			return &self.ownedNFTs[id] as &{NonFungibleToken.NFT}? as! &DoodlePacks.NFT?
181		}
182
183		access(all) view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver} {
184			let nft = (&self.ownedNFTs[id] as &{NonFungibleToken.NFT}?)!
185            let doodlePack: &DoodlePacks.NFT = nft as! &DoodlePacks.NFT
186            return doodlePack
187		}
188
189		access(all) view fun getSupportedNFTTypes(): {Type: Bool} {
190            let supportedTypes: {Type: Bool} = {}
191            supportedTypes[Type<@DoodlePacks.NFT>()] = true
192            return supportedTypes
193        }
194
195        access(all) view fun isSupportedNFTType(type: Type): Bool {
196           if type == Type<@DoodlePacks.NFT>() {
197        	return true
198           } else {
199        	return false
200           }
201		}
202
203        access(all) view fun getLength(): Int {
204            return self.ownedNFTs.keys.length
205        }
206
207        access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
208			return <- DoodlePacks.createEmptyCollection(nftType: Type<@DoodlePacks.NFT>())
209		}
210
211		init () {
212			self.ownedNFTs <- {}
213		}
214	}
215
216	access(account) fun mintNFT(recipient: &{NonFungibleToken.Receiver}, typeId: UInt64) {
217		let packType = DoodlePackTypes.getPackType(id: typeId) ?? panic("Invalid pack type")
218		assert(packType.maxSupply == nil || DoodlePackTypes.getPackTypesMintedCount(typeId: packType.id) < packType.maxSupply!, message: "Max supply reached")
219		
220		let pack: @DoodlePacks.NFT <- create DoodlePacks.NFT(typeId: typeId)
221
222		DoodlePackTypes.addMintedCountToPackType(typeId: typeId, amount: 1)
223
224		emit Minted(id: pack.id, typeId: typeId)
225		
226		recipient.deposit(token: <-pack)
227	}
228
229    access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} {
230        return <- create Collection()
231    }
232
233	access(all) fun open(collection: auth(NonFungibleToken.Withdraw) &{DoodlePacks.CollectionPublic, NonFungibleToken.Provider}, packId: UInt64): @OpenDoodlePacks.NFT {
234		let pack = collection.borrowDoodlePack(id: packId) ?? panic("Could not borrow a reference to the pack")
235
236		let openPack <- OpenDoodlePacks.mintNFT(id: pack.id, serialNumber: pack.serialNumber, typeId: pack.typeId)
237		emit Opened(id: packId, address: collection.owner!.address)
238		Burner.burn(<- collection.withdraw(withdrawID: packId))
239		return <- openPack
240	}
241
242    access(all) view fun getContractViews(resourceType: Type?): [Type] {
243        return [
244            Type<MetadataViews.NFTCollectionDisplay>(),
245            Type<MetadataViews.NFTCollectionData>()
246        ]
247    }
248
249    access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
250        switch viewType {
251            case Type<MetadataViews.NFTCollectionDisplay>():
252				return MetadataViews.NFTCollectionDisplay(
253					name: "Doodle Packs",
254					description: "Doodle Packs can be open to obtain multiple NFTs!",
255					externalURL: MetadataViews.ExternalURL("https://doodles.app"),
256					squareImage: MetadataViews.Media(file: MetadataViews.IPFSFile(cid: "", path: nil), mediaType:"image/png"),
257					bannerImage: MetadataViews.Media(file: MetadataViews.IPFSFile(cid: "", path: nil), mediaType:"image/png"),
258					socials: {
259						"instagram": MetadataViews.ExternalURL("https://www.instagram.com/thedoodles"),
260						"discord": MetadataViews.ExternalURL("https://discord.gg/doodles"),
261						"twitter": MetadataViews.ExternalURL("https://twitter.com/doodles")
262					}
263				)
264			case Type<MetadataViews.NFTCollectionData>():
265				return MetadataViews.NFTCollectionData(
266					storagePath: DoodlePacks.CollectionStoragePath,
267					publicPath: DoodlePacks.CollectionPublicPath,
268					publicCollection: Type<&Collection>(),
269					publicLinkedType: Type<&Collection>(),
270					createEmptyCollectionFunction: (fun(): 
271						@{NonFungibleToken.Collection} {return <- DoodlePacks.createEmptyCollection(nftType: Type<@DoodlePacks.NFT>())})
272				)
273        }
274        return nil
275    }
276
277	init() {
278		self.CollectionStoragePath = /storage/DoodlePacksCollection
279		self.CollectionPublicPath = /public/DoodlePacksCollection
280
281		self.totalSupply = 0
282		self.currentPackTypesSerialNumber = {}
283		self.packTypesCurrentSupply = {}
284
285		self.extra = {}
286
287        self.account.storage.save<@{NonFungibleToken.Collection}>(<- DoodlePacks.createEmptyCollection(nftType: Type<@DoodlePacks.NFT>()), to: DoodlePacks.CollectionStoragePath)
288		let cap = self.account.capabilities.storage.issue<&Collection>(DoodlePacks.CollectionStoragePath)
289		self.account.capabilities.publish(cap, at: DoodlePacks.CollectionPublicPath)
290
291		emit ContractInitialized()
292	}
293}
294
295
296