Smart Contract

FlovatarPack

A.921ea449dffec68a.FlovatarPack

Deployed

1w ago
Feb 16, 2026, 11:20:41 PM UTC

Dependents

8 imports
1import FungibleToken from 0xf233dcee88fe0abe
2import NonFungibleToken from 0x1d7e57aa55817448
3import FlowToken from 0x1654653399040a61
4import FlovatarComponentTemplate from 0x921ea449dffec68a
5import FlovatarComponent from 0x921ea449dffec68a
6import Crypto
7import FlowUtilityToken from 0xead892083b3e2c6c
8import FlovatarDustToken from 0x921ea449dffec68a
9
10/*
11
12 This contract defines the Flovatar Packs and a Collection to manage them.
13
14 Each Pack will contain one item for each required Component (body, hair, eyes, nose, mouth, clothing),
15 and two other Components that are optional (facial hair, accessory, hat, eyeglasses, background).
16
17 Packs will be pre-minted and can be purchased from the contract owner's account by providing a
18 verified signature that is different for each Pack (more info in the purchase function).
19
20 Once purchased, packs cannot be re-sold and users will only be able to open them to receive
21 the contained Components into their collection.
22
23 */
24
25access(all)
26contract FlovatarPack{
27
28    access(all) entitlement WithdrawEnt
29
30	access(all)
31	let CollectionStoragePath: StoragePath
32	
33	access(all)
34	let CollectionPublicPath: PublicPath
35	
36	// Counter for all the Packs ever minted
37	access(all)
38	var totalSupply: UInt64
39	
40	// Standard events that will be emitted
41	access(all)
42	event ContractInitialized()
43	
44	access(all)
45	event Withdraw(id: UInt64, from: Address?)
46	
47	access(all)
48	event Deposit(id: UInt64, to: Address?)
49	
50	access(all)
51	event Created(id: UInt64, prefix: String)
52	
53	access(all)
54	event Opened(id: UInt64)
55	
56	access(all)
57	event Purchased(id: UInt64)
58	
59	// The public interface contains only the ID and the price of the Pack
60	access(all)
61	resource interface Public{ 
62		access(all)
63		let id: UInt64
64		
65		access(all)
66		let price: UFix64
67		
68		access(all)
69		let sparkCount: UInt32
70		
71		access(all)
72		let series: UInt32
73		
74		access(all)
75		let name: String
76	}
77	
78	// The Pack resource that implements the Public interface and that contains
79	// different Components in a Dictionary
80	access(all)
81	resource Pack: Public{ 
82		access(all)
83		let id: UInt64
84		
85		access(all)
86		let price: UFix64
87		
88		access(all)
89		let sparkCount: UInt32
90		
91		access(all)
92		let series: UInt32
93		
94		access(all)
95		let name: String
96		
97		access(account)
98		let components: @[FlovatarComponent.NFT]
99		
100		access(account)
101		var randomString: String
102		
103		// Initializes the Pack with all the Components.
104		// It receives also the price and a random String that will signed by
105		// the account owner to validate the purchase process.
106		init(components: @[FlovatarComponent.NFT], randomString: String, price: UFix64, sparkCount: UInt32, series: UInt32, name: String){ 
107			
108			// Makes sure that if it's set to have a spark component, this one is present in the array
109			var sparkCountCheck: UInt32 = 0
110			if sparkCount > 0{ 
111				var i: Int = 0
112				while i < components.length{ 
113					if components[i].getCategory() == "spark"{ 
114						sparkCountCheck = sparkCountCheck + 1
115					}
116					i = i + 1
117				}
118			}
119			if sparkCount != sparkCountCheck{ 
120				panic("There is a mismatch in the spark count")
121			}
122			
123			// Increments the total supply counter
124			FlovatarPack.totalSupply = FlovatarPack.totalSupply + 1
125			self.id = FlovatarPack.totalSupply
126			
127			// Moves all the components into the array
128			self.components <- []
129			while components.length > 0{ 
130				self.components.append(<-components.remove(at: 0))
131			}
132			destroy components
133			
134			// Sets the randomString text and the price
135			self.randomString = randomString
136			self.price = price
137			self.sparkCount = sparkCount
138			self.series = series
139			self.name = name
140		}
141		
142		// This function is used to retrieve the random string to match it
143		// against the signature passed during the purchase process
144		access(contract)
145		fun getRandomString(): String{ 
146			return self.randomString
147		}
148		
149		// This function reset the randomString so that after the purchase nobody
150		// will be able to re-use the verified signature
151		access(contract)
152		fun setRandomString(randomString: String){ 
153			self.randomString = randomString
154		}
155		
156		access(all)
157		fun removeComponent(at: Int): @FlovatarComponent.NFT{ 
158			return <-self.components.remove(at: at)
159		}
160	}
161	
162	//Pack CollectionPublic interface that allows users to purchase a Pack
163	access(all)
164	resource interface CollectionPublic{ 
165		access(all)
166		fun getIDs(): [UInt64]
167		
168		access(all)
169		fun deposit(token: @FlovatarPack.Pack)
170		
171		access(all)
172		fun purchase(
173			tokenId: UInt64,
174			recipientCap: Capability<&{FlovatarPack.CollectionPublic}>,
175			buyTokens: @{FungibleToken.Vault},
176			signature: String
177		)
178		
179		access(all)
180		fun purchaseWithDust(
181			tokenId: UInt64,
182			recipientCap: Capability<&{FlovatarPack.CollectionPublic}>,
183			buyTokens: @{FungibleToken.Vault},
184			signature: String
185		)
186		
187		access(all)
188		fun purchaseDapper(
189			tokenId: UInt64,
190			recipientCap: Capability<&{FlovatarPack.CollectionPublic}>,
191			buyTokens: @{FungibleToken.Vault},
192			signature: String,
193			expectedPrice: UFix64
194		)
195	}
196	
197	// Main Collection that implements the Public interface and that
198	// will handle the purchase transactions
199	access(all)
200	resource Collection: CollectionPublic{ 
201		// Dictionary of all the Packs owned
202		access(account)
203		let ownedPacks: @{UInt64: FlovatarPack.Pack}
204		
205		// Capability to send the FLOW tokens to the owner's account
206		access(account)
207		let ownerVault: Capability<&{FungibleToken.Receiver}>
208		
209		// Initializes the Collection with the vault receiver capability
210		init(ownerVault: Capability<&{FungibleToken.Receiver}>){ 
211			self.ownedPacks <-{} 
212			self.ownerVault = ownerVault
213		}
214		
215		// getIDs returns an array of the IDs that are in the collection
216		access(all)
217		fun getIDs(): [UInt64]{ 
218			return self.ownedPacks.keys
219		}
220		
221		// deposit takes a Pack and adds it to the collections dictionary
222		// and adds the ID to the id array
223		access(all)
224		fun deposit(token: @FlovatarPack.Pack){ 
225			let id: UInt64 = token.id
226			
227			// add the new token to the dictionary which removes the old one
228			let oldToken <- self.ownedPacks[id] <- token
229			emit Deposit(id: id, to: self.owner?.address)
230			destroy oldToken
231		}
232		
233		// withdraw removes a Pack from the collection and moves it to the caller
234		access(WithdrawEnt)
235		fun withdraw(withdrawID: UInt64): @FlovatarPack.Pack{ 
236			let token <- self.ownedPacks.remove(key: withdrawID) ?? panic("Missing Pack")
237			emit Withdraw(id: token.id, from: self.owner?.address)
238			return <-token
239		}
240		
241		// This function allows any Pack owner to open the pack and receive its content
242		// into the owner's Component Collection.
243		// The pack is destroyed after the Components are delivered.
244		access(WithdrawEnt)
245		fun openPack(id: UInt64){ 
246			
247			// Gets the Component Collection Public capability to be able to
248			// send there the Components contained in the Pack
249			let recipientCap = (self.owner!).capabilities.get<&{FlovatarComponent.CollectionPublic}>(FlovatarComponent.CollectionPublicPath)
250			let recipient = recipientCap.borrow()!
251			
252			// Removed the pack from the collection
253			let pack <- self.withdraw(withdrawID: id)
254			
255			// Removes all the components from the Pack and deposits them to the
256			// Component Collection of the owner
257			while pack.components.length > 0{ 
258				recipient.deposit(token: <-pack.removeComponent(at: 0))
259			}
260			
261			// Emits the event to notify that the pack was opened
262			emit Opened(id: pack.id)
263			destroy pack
264		}
265		
266		// Gets the price for a specific Pack
267		access(account)
268		view fun getPrice(id: UInt64): UFix64{ 
269			let pack: &FlovatarPack.Pack = (&self.ownedPacks[id])!
270			return pack.price
271		}
272		
273		// Gets the random String for a specific Pack
274		access(account)
275		fun getRandomString(id: UInt64): String{ 
276			let pack: &FlovatarPack.Pack = (&self.ownedPacks[id])!
277			return pack.getRandomString()
278		}
279		
280		// Sets the random String for a specific Pack
281		access(account)
282		fun setRandomString(id: UInt64, randomString: String){ 
283			let pack: &FlovatarPack.Pack = (&self.ownedPacks[id])!
284			pack.setRandomString(randomString: randomString)
285		}
286		
287		// This function provides the ability for anyone to purchase a Pack
288		// It receives as parameters the Pack ID, the Pack Collection Public capability to receive the pack,
289		// a vault containing the necessary FLOW token, and finally a signature to validate the process.
290		// The signature is generated off-chain by the smart contract's owner account using the Crypto library
291		// to generate a hash from the original random String contained in each Pack.
292		// This will guarantee that the contract owner will be able to decide which user can buy a pack, by
293		// providing them the correct signature.
294		//
295		access(all)
296		fun purchase(tokenId: UInt64, recipientCap: Capability<&{FlovatarPack.CollectionPublic}>, buyTokens: @{FungibleToken.Vault}, signature: String){ 
297			
298			// Checks that the pack is still available and that the FLOW tokens are sufficient
299			pre{ 
300				self.ownedPacks.containsKey(tokenId) == true:
301					"Pack not found!"
302				self.getPrice(id: tokenId) <= buyTokens.balance:
303					"Not enough tokens to buy the Pack!"
304				buyTokens.isInstance(Type<@FlowToken.Vault>()):
305					"Vault not of the right Token Type"
306			}
307			
308			// Gets the Crypto.KeyList and the public key of the collection's owner
309			let keyList = Crypto.KeyList()
310			let accountKey = ((self.owner!).keys.get(keyIndex: 0)!).publicKey
311			
312			// Adds the public key to the keyList
313			keyList.add(PublicKey(publicKey: accountKey.publicKey, signatureAlgorithm: accountKey.signatureAlgorithm), hashAlgorithm: HashAlgorithm.SHA3_256, weight: 1.0)
314			
315			// Creates a Crypto.KeyListSignature from the signature provided in the parameters
316			let signatureSet: [Crypto.KeyListSignature] = []
317			signatureSet.append(Crypto.KeyListSignature(keyIndex: 0, signature: signature.decodeHex()))
318			
319
320
321			// Verifies that the signature is valid and that it was generated from the
322			// owner of the collection
323			if !keyList.verify(signatureSet: signatureSet, signedData: self.getRandomString(id: tokenId).utf8, domainSeparationTag: "FLOW-V0.0-user"){ 
324				panic("Unable to validate the signature for the pack!")
325			}
326			
327			
328			// Borrows the recipient's capability and withdraws the Pack from the collection.
329			// If this fails the transaction will revert but the signature will be exposed.
330			// For this reason in case it happens, the randomString will be reset when the purchase
331			// reservation timeout expires by the web server back-end.
332			let recipient = recipientCap.borrow()!
333			let pack <- self.withdraw(withdrawID: tokenId)
334			
335			// Borrows the owner's capability for the Vault and deposits the FLOW tokens
336			let vaultRef = self.ownerVault.borrow() ?? panic("Could not borrow reference to owner pack vault")
337			vaultRef.deposit(from: <-buyTokens)
338			
339			// Resets the randomString so that the provided signature will become useless
340			let packId: UInt64 = pack.id
341			pack.setRandomString(randomString: revertibleRandom<UInt64>().toString())
342			
343			// Deposits the Pack to the recipient's collection
344			recipient.deposit(token: <-pack)
345			
346			// Emits an even to notify about the purchase
347			emit Purchased(id: packId)
348		}
349		
350		//
351		access(all)
352		fun purchaseDapper(tokenId: UInt64, recipientCap: Capability<&{FlovatarPack.CollectionPublic}>, buyTokens: @{FungibleToken.Vault}, signature: String, expectedPrice: UFix64){ 
353			
354			// Checks that the pack is still available and that the FLOW tokens are sufficient
355			pre{ 
356				self.ownedPacks.containsKey(tokenId) == true:
357					"Pack not found!"
358				self.getPrice(id: tokenId) <= buyTokens.balance:
359					"Not enough tokens to buy the Pack!"
360				self.getPrice(id: tokenId) == expectedPrice:
361					"Price not set as expected!"
362				buyTokens.isInstance(Type<@FlowUtilityToken.Vault>()):
363					"Vault not of the right Token Type"
364			}
365			
366			// Gets the Crypto.KeyList and the public key of the collection's owner
367			let keyList = Crypto.KeyList()
368			let accountKey = ((self.owner!).keys.get(keyIndex: 0)!).publicKey
369			
370			// Adds the public key to the keyList
371			keyList.add(PublicKey(publicKey: accountKey.publicKey, signatureAlgorithm: accountKey.signatureAlgorithm), hashAlgorithm: HashAlgorithm.SHA3_256, weight: 1.0)
372			
373			// Creates a Crypto.KeyListSignature from the signature provided in the parameters
374			let signatureSet: [Crypto.KeyListSignature] = []
375			signatureSet.append(Crypto.KeyListSignature(keyIndex: 0, signature: signature.decodeHex()))
376			
377
378
379			// Verifies that the signature is valid and that it was generated from the
380			// owner of the collection
381			if !keyList.verify(signatureSet: signatureSet, signedData: self.getRandomString(id: tokenId).utf8, domainSeparationTag: "FLOW-V0.0-user"){ 
382				panic("Unable to validate the signature for the pack!")
383			}
384			
385			
386			// Borrows the recipient's capability and withdraws the Pack from the collection.
387			// If this fails the transaction will revert but the signature will be exposed.
388			// For this reason in case it happens, the randomString will be reset when the purchase
389			// reservation timeout expires by the web server back-end.
390			let recipient = recipientCap.borrow()!
391			let pack <- self.withdraw(withdrawID: tokenId)
392			
393			// Borrows the owner's capability for the Vault and deposits the FLOW tokens
394			let dapperMarketVault = getAccount(0x8a86f18e0e05bd9f).capabilities.get<&{FungibleToken.Receiver}>(/public/flowUtilityTokenReceiver)
395			let vaultRef = dapperMarketVault.borrow() ?? panic("Could not borrow reference to owner pack vault")
396			vaultRef.deposit(from: <-buyTokens)
397			
398			// Resets the randomString so that the provided signature will become useless
399			let packId: UInt64 = pack.id
400			pack.setRandomString(randomString: revertibleRandom<UInt64>().toString())
401			
402			// Deposits the Pack to the recipient's collection
403			recipient.deposit(token: <-pack)
404			
405			// Emits an even to notify about the purchase
406			emit Purchased(id: packId)
407		}
408		
409		access(all)
410		fun purchaseWithDust(tokenId: UInt64, recipientCap: Capability<&{FlovatarPack.CollectionPublic}>, buyTokens: @{FungibleToken.Vault}, signature: String){ 
411			
412			// Checks that the pack is still available and that the FLOW tokens are sufficient
413			pre{ 
414				self.ownedPacks.containsKey(tokenId) == true:
415					"Pack not found!"
416				self.getPrice(id: tokenId) <= buyTokens.balance:
417					"Not enough tokens to buy the Pack!"
418				buyTokens.isInstance(Type<@FlovatarDustToken.Vault>()):
419					"Vault not of the right Token Type"
420			}
421			
422			// Gets the Crypto.KeyList and the public key of the collection's owner
423			let keyList = Crypto.KeyList()
424			let accountKey = ((self.owner!).keys.get(keyIndex: 0)!).publicKey
425			
426			// Adds the public key to the keyList
427			keyList.add(PublicKey(publicKey: accountKey.publicKey, signatureAlgorithm: accountKey.signatureAlgorithm), hashAlgorithm: HashAlgorithm.SHA3_256, weight: 1.0)
428			
429			// Creates a Crypto.KeyListSignature from the signature provided in the parameters
430			let signatureSet: [Crypto.KeyListSignature] = []
431			signatureSet.append(Crypto.KeyListSignature(keyIndex: 0, signature: signature.decodeHex()))
432			
433
434			// Verifies that the signature is valid and that it was generated from the
435			// owner of the collection
436			if !keyList.verify(signatureSet: signatureSet, signedData: self.getRandomString(id: tokenId).utf8, domainSeparationTag: "FLOW-V0.0-user"){ 
437				panic("Unable to validate the signature for the pack!")
438			}
439			
440			
441			// Borrows the recipient's capability and withdraws the Pack from the collection.
442			// If this fails the transaction will revert but the signature will be exposed.
443			// For this reason in case it happens, the randomString will be reset when the purchase
444			// reservation timeout expires by the web server back-end.
445			let recipient = recipientCap.borrow()!
446			let pack <- self.withdraw(withdrawID: tokenId)
447			if pack.name != "Dust Flobit Pack"{ 
448				panic("Wrong type of Pack selected")
449			}
450			
451			// Burn the DUST Tokens
452			destroy buyTokens
453			
454			// Resets the randomString so that the provided signature will become useless
455			let packId: UInt64 = pack.id
456			pack.setRandomString(randomString: revertibleRandom<UInt64>().toString())
457			
458			// Deposits the Pack to the recipient's collection
459			recipient.deposit(token: <-pack)
460			
461			// Emits an even to notify about the purchase
462			emit Purchased(id: packId)
463		}
464	}
465	
466	// public function that anyone can call to create a new empty collection
467	access(all)
468	fun createEmptyCollection(
469		ownerVault: Capability<&{FungibleToken.Receiver}>
470	): @FlovatarPack.Collection{ 
471		return <-create Collection(ownerVault: ownerVault)
472	}
473	
474	// Get all the packs from a specific account
475	access(all)
476	fun getPacks(address: Address): [UInt64]?{ 
477		let account = getAccount(address)
478
479		if let packCollection = account.capabilities.borrow<&FlovatarPack.Collection>(FlovatarPack.CollectionPublicPath){ 
480			return packCollection.getIDs()
481		}
482		
483		return nil
484	}
485	
486	// This method can only be called from another contract in the same account (The Flovatar Admin resource)
487	// It creates a new pack from a list of Components, the random String and the price.
488	// Some Components are required and others are optional
489	access(account)
490	fun createPack(
491		components: @[
492			FlovatarComponent.NFT
493		],
494		randomString: String,
495		price: UFix64,
496		sparkCount: UInt32,
497		series: UInt32,
498		name: String
499	): @FlovatarPack.Pack{ 
500		var newPack <-
501			create Pack(
502				components: <-components,
503				randomString: randomString,
504				price: price,
505				sparkCount: sparkCount,
506				series: series,
507				name: name
508			)
509		
510		// Emits an event to notify that a Pack was created.
511		// Sends the first 4 digits of the randomString to be able to sync the ID with the off-chain DB
512		// that will store also the signatures once they are generated
513		emit Created(id: newPack.id, prefix: randomString.slice(from: 0, upTo: 4))
514		return <-newPack
515	}
516	
517	init(){ 
518		let wallet = self.account.capabilities.get<&FlowToken.Vault>(/public/flowTokenReceiver)
519		self.CollectionPublicPath = /public/FlovatarPackCollection
520		self.CollectionStoragePath = /storage/FlovatarPackCollection
521		
522		// Initialize the total supply
523		self.totalSupply = 0
524		self.account.storage.save<@FlovatarPack.Collection>(
525			<-FlovatarPack.createEmptyCollection(ownerVault: wallet),
526			to: FlovatarPack.CollectionStoragePath
527		)
528		var capability_1 =
529			self.account.capabilities.storage.issue<&{FlovatarPack.CollectionPublic}>(
530				FlovatarPack.CollectionStoragePath
531			)
532		self.account.capabilities.publish(capability_1, at: FlovatarPack.CollectionPublicPath)
533		emit ContractInitialized()
534	}
535}
536