Smart Contract
FlovatarPack
A.921ea449dffec68a.FlovatarPack
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