Smart Contract
Doodles
A.e81193c424cfd3fb.Doodles
1import NonFungibleToken from 0x1d7e57aa55817448
2import FungibleToken from 0xf233dcee88fe0abe
3import MetadataViews from 0x1d7e57aa55817448
4import Wearables from 0xe81193c424cfd3fb
5import Templates from 0xe81193c424cfd3fb
6import DoodleNames from 0xe81193c424cfd3fb
7import FindUtils from 0x097bafa4e0b48eef
8import Debug from 0xe81193c424cfd3fb
9import ViewResolver from 0x1d7e57aa55817448
10import Burner from 0xf233dcee88fe0abe
11import FlowtyViews from 0x3cdbb3d569211ff3
12
13access(all) contract Doodles: NonFungibleToken {
14
15 access(all) var totalSupply: UInt64
16
17 access(all) event ContractInitialized()
18 access(all) event Minted(id:UInt64, address:Address, set: String, setNumber: UInt64, name: String, context: {String : String})
19 access(all) event Equipped(id: UInt64, subId:UInt64, subSlot: String, resourceType: String, address:Address, tags: {String: String} , context: {String: String})
20 access(all) event Unequipped(id: UInt64, subId:UInt64, subSlot:String, resourceType: String, address:Address, tags: {String: String} , context: {String: String})
21 access(all) event SetRegistered(id: UInt64, name:String, royalties:[Templates.Royalty])
22 access(all) event SetRetired(id:UInt64, name:String)
23 access(all) event SpeciesRegistered(id: UInt64, name:String)
24 access(all) event SpeciesRetired(id: UInt64, name:String)
25 access(all) event BaseCharacterRegistered(id: UInt64, species:String, name:String, baseCharacterTraits: {String: String}, image: String)
26 access(all) event BaseCharacterRetired(id: UInt64, name:String)
27 access(all) event DoodleUpdated(id: UInt64, owner: Address)
28
29 access(all) let CollectionStoragePath: StoragePath
30 access(all) let CollectionPublicPath: PublicPath
31
32 access(all) let sets: {UInt64: Set}
33 access(all) let species : {UInt64: Species}
34 access(all) let baseCharacters : {UInt64 : BaseCharacter}
35
36 access(all) struct Set : Templates.Retirable, Templates.Editionable, Templates.RoyaltyHolder {
37 access(all) let id:UInt64
38 access(all) let name: String
39 access(all) var active:Bool
40 access(all) let royalties: [Templates.Royalty]
41 access(self) let extra: {String : AnyStruct}
42
43 init(id:UInt64, name:String, royalties: [Templates.Royalty]) {
44 self.id=id
45 self.name=name
46 self.active=true
47 self.royalties=royalties
48 self.extra={}
49 }
50
51 access(all) fun getClassifier() : String{
52 return "set"
53 }
54
55 access(all) fun getCounterSuffix() : String {
56 return self.name
57 }
58
59 access(all) fun getContract() : String {
60 return "doodles"
61 }
62
63 }
64
65 access(all) struct Species : Templates.Retirable, Templates.Editionable {
66 access(all) let id: UInt64
67 access(all) let name: String
68 access(all) var active:Bool
69 access(self) let extra: {String : AnyStruct}
70
71 init(id: UInt64, name:String) {
72 self.id=id
73 self.name=name
74 self.active=true
75 self.extra={}
76 }
77
78 access(all) fun getClassifier() :String {
79 return "species"
80 }
81
82 access(all) fun getCounterSuffix() : String {
83 return self.name
84 }
85
86 access(all) fun getContract() : String {
87 return "doodles"
88 }
89 }
90
91 access(all) struct BaseCharacter : Templates.Retirable, Templates.Editionable{
92 access(all) let id: UInt64
93 access(all) let name: String
94 access(all) let species: UInt64
95 access(all) let set: UInt64
96 //TODO: What kind of traits exsist? Do we not want to have control over that?
97 access(all) let baseCharacterTraits: {String: BaseCharacterTrait}
98 access(all) var thumbnail: {MetadataViews.File}
99 access(all) let image: {MetadataViews.File}
100 access(all) var active:Bool
101 access(self) let extra: {String : AnyStruct}
102
103 init(id: UInt64, name:String, species:UInt64, set: UInt64, baseCharacterTraits: {String: BaseCharacterTrait}, thumbnail: {MetadataViews.File}, image: {MetadataViews.File}) {
104 self.id=id
105 self.species=species
106 self.name=name
107 self.set=set
108 self.baseCharacterTraits=baseCharacterTraits
109 self.thumbnail=thumbnail
110 self.image=image
111 self.active=true
112 self.extra={}
113 }
114
115 access(all) fun getClassifier() :String {
116 return "doodles"
117 }
118
119 access(all) fun getCounterSuffix() : String {
120 return self.name
121 }
122
123 access(all) fun getContract() : String {
124 return "doodles"
125 }
126
127 access(all) fun getSpecies() : Doodles.Species {
128 return Doodles.species[self.species]!
129 }
130
131 access(all) fun getSet() : Doodles.Set {
132 return Doodles.sets[self.set]!
133 }
134
135 access(all) fun getTraits() : [MetadataViews.Trait] {
136 let t : [MetadataViews.Trait] = []
137 for v in self.baseCharacterTraits.values {
138 if v.value != "" {
139 t.append(v.getTrait())
140 }
141 }
142 return t
143 }
144
145 access(all) fun getTraitsAsMap() : {String : String} {
146 let t : {String : String} = {}
147 for key in self.baseCharacterTraits.keys {
148 t[key] = self.baseCharacterTraits[key]!.value
149 }
150 return t
151 }
152
153 access(account) fun updateTrait(name:String, value:String) {
154 if let curr = self.baseCharacterTraits[name] {
155 if curr.value!=value{
156 self.baseCharacterTraits[name]= BaseCharacterTrait(name:name, value:value)
157 }
158 } else {
159 self.baseCharacterTraits[name]= BaseCharacterTrait(name:name, value:value)
160 }
161 }
162
163 }
164
165 access(account) fun addSet(_ set: Doodles.Set) {
166 emit SetRegistered(id: set.id, name:set.name, royalties:set.royalties)
167 self.sets[set.id] = set
168 }
169
170 access(account) fun retireSet(_ id: UInt64) {
171 pre{
172 self.sets.containsKey(id) : "Set does not exist. Id : ".concat(id.toString())
173 }
174 emit SetRetired(id: id, name:self.sets[id]!.name)
175 self.sets[id]!.enable(false)
176 }
177
178 access(account) fun addSpecies(_ species: Doodles.Species) {
179 emit SpeciesRegistered(id: species.id, name: species.name)
180 self.species[species.id] = species
181 }
182
183 access(account) fun retireSpecies(_ id: UInt64) {
184 pre{
185 self.species.containsKey(id) : "Species does not exist. Id : ".concat(id.toString())
186 }
187 emit SpeciesRetired(id: id, name:self.species[id]!.name)
188 self.species[id]!.enable(false)
189 }
190
191 access(account) fun setBaseCharacter(_ bc: Doodles.BaseCharacter) {
192 // we do not check here because baseCharacterTraits are updatable and it can be overwritten
193 emit BaseCharacterRegistered(id: bc.id, species:bc.getSpecies().name, name:bc.name, baseCharacterTraits: bc.getTraitsAsMap(), image: bc.image.uri())
194 self.baseCharacters[bc.id] = bc
195 }
196
197 access(account) fun retireBaseCharacter(_ id: UInt64) {
198 pre{
199 self.baseCharacters.containsKey(id) : "Base Character does not exist. Id : ".concat(id.toString())
200 }
201 emit BaseCharacterRetired(id: id, name:self.baseCharacters[id]!.name)
202 self.baseCharacters[id]!.enable(false)
203 }
204
205 access(all) struct BaseCharacterTrait {
206 access(all) let name: String
207 access(all) let value: String
208 access(all) let tag: {String : String}
209 access(all) let extra: {String : AnyStruct}
210
211 init(name: String , value: String) {
212 self.name = name
213 self.value = value
214 self.tag = {}
215 self.extra = {}
216 }
217
218 access(all) fun getTrait() : MetadataViews.Trait {
219 return MetadataViews.Trait(name: "trait_".concat(self.name), value: self.value, displayType: "string", rarity: nil)
220 }
221
222 access(all) fun getTraitAsMap() : {String : String} {
223 let t = self.tag
224 t["name"] = self.name
225 t["value"] = self.value
226 return t
227 }
228 }
229
230 access(all) resource NFT: NonFungibleToken.NFT, ViewResolver.Resolver, Burner.Burnable, ViewResolver.ResolverCollection {
231
232 access(all) let id:UInt64
233 // At the moment this will only store one name. A map of {id : name resource}
234 access(all) var name: @{UInt64 : DoodleNames.NFT}
235 access(all) var nounce:UInt64
236 access(all) let editions: [Templates.EditionInfo]
237 access(all) let baseCharacter: BaseCharacter
238
239 //This is resourceId to index for the item
240 access(all) let positionIndex: {UInt64 : UInt64}
241 // mapping of wearable ids to wearables resources
242 access(all) let wearables: @{UInt64: Wearables.NFT}
243
244 access(all) let royalties: MetadataViews.Royalties
245 access(all) let extra: {String : AnyStruct}
246
247 access(all) let context: { String: String}
248 init(
249 baseCharacter: BaseCharacter,
250 editions: [Templates.EditionInfo],
251 context: {String:String}
252
253 ) {
254 self.nounce=0
255 self.id=self.uuid
256 self.name<-{}
257 self.editions=editions
258 self.baseCharacter=baseCharacter
259 self.wearables <- {}
260 self.positionIndex={}
261 let s = baseCharacter.getSet()
262 self.royalties=MetadataViews.Royalties(s.getRoyalties())
263 self.context=context
264 self.extra ={}
265 }
266
267 /// implemente ResolverCollection
268 access(all) view fun getIDs() : [UInt64] {
269 var keys=self.wearables.keys
270 keys = keys.concat(self.name.keys)
271 return keys
272 }
273
274 access(all) fun getSet() : Set {
275 return self.baseCharacter.getSet()
276 }
277
278 access(all) view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver} {
279 if self.name.containsKey(id) {
280 return (&self.name[id] as &DoodleNames.NFT?)!
281 }
282 return (&self.wearables[id] as &Wearables.NFT?)!
283 }
284
285 access(all) fun getContext() : {String:String} {
286 return self.context
287 }
288
289 access(contract) fun burnCallback() {
290 // Cannot move nested resources?
291 // destroy self.wearables
292 // destroy self.name
293 }
294
295 access(all) view fun getViews(): [Type] {
296 return [
297 Type<MetadataViews.Display>(),
298 Type<MetadataViews.Medias>(),
299 Type<MetadataViews.Royalties>(),
300 Type<MetadataViews.ExternalURL>(),
301 Type<MetadataViews.NFTCollectionData>(),
302 Type<MetadataViews.NFTCollectionDisplay>(),
303 Type<MetadataViews.Editions>(),
304 Type<MetadataViews.Traits>(),
305 Type<FlowtyViews.DNA>()
306 ]
307 }
308
309
310 access(all) fun resolveView(_ view: Type): AnyStruct? {
311
312 var fullMediaType="image/png"
313 let description="This Doodle is a uniquely personalized customizable character in a one-of-a-kind style."
314
315 let imageFile = self.baseCharacter.thumbnail
316
317 let fullMedia=MetadataViews.Media(file:self.baseCharacter.image, mediaType: fullMediaType)
318 let set = self.getSet()
319
320 var name =""
321 if let ownedName = self.getName() {
322 name = ownedName
323 }
324
325 switch view {
326 case Type<MetadataViews.Display>():
327 return MetadataViews.Display(
328 name: name,
329 description: description,
330 thumbnail: imageFile
331 )
332 case Type<MetadataViews.ExternalURL>():
333 return MetadataViews.ExternalURL("https://doodles.app")
334 case Type<MetadataViews.Royalties>():
335 return self.royalties
336 case Type<MetadataViews.Medias>():
337 let medias: [MetadataViews.Media] = []
338 for key in self.wearables.keys{
339 let w = self.borrowWearable(key)
340 medias.append(w.getTemplate().thumbnail)
341 }
342 return MetadataViews.Medias(medias)
343 case Type<MetadataViews.Traits>():
344 return self.getTraitsAsTraits()
345 case Type<FlowtyViews.DNA>():
346 return FlowtyViews.DNA(self.calculateDNA())
347 case Type<MetadataViews.Editions>():
348 let edition=self.editions[0]
349 let active = self.getActive(edition.name)
350 let editions : [MetadataViews.Edition] =[
351 edition.getAsMetadataEdition(active)
352 ]
353 return MetadataViews.Editions(editions)
354 }
355 return Doodles.resolveContractView(resourceType: Type<@Doodles.NFT>(), viewType: view)
356 }
357
358 access(all) fun getSetActive() : Bool {
359 let t = Doodles.sets[self.baseCharacter.set]!
360 return t.active
361 }
362
363 access(all) fun getSpeciesActive() : Bool {
364 let t = Doodles.species[self.baseCharacter.species]!
365 return t.active
366 }
367
368 access(all) fun getBaseCharacterActive() : Bool {
369 let t = Doodles.baseCharacters[self.baseCharacter.id]!
370 return t.active
371 }
372
373 access(all) fun getActive(_ classifier: String) : Bool {
374 switch classifier {
375 case "baseCharacter" :
376 return self.getBaseCharacterActive()
377
378 case "species" :
379 return self.getSpeciesActive()
380
381 case "set" :
382 return self.getSetActive()
383 }
384 return true
385 }
386
387 access(all) fun getName() : String? {
388 if let id = self.getNameId() {
389 let ref = self.borrowName(id)
390 return ref.name
391 }
392 return nil
393 }
394
395 access(all) fun getNameId() : UInt64? {
396 if self.name.length > 0 {
397 return self.name.keys[0]
398 }
399 return nil
400 }
401
402 access(all) fun getRoyalties() : [MetadataViews.Royalty] {
403 return self.royalties.getRoyalties()
404 }
405
406 //This needs to change
407 access(all) fun getWearablesAt(_ position: UInt64) : [UInt64] {
408 //map from index to resourceId
409 let ids : { UInt64 : UInt64} = {}
410
411 for key in self.wearables.keys {
412 let w = self.borrowWearable(key)
413 if w.template.position == position {
414 ids[self.positionIndex[key]!]=key
415 }
416 }
417
418 var i=0
419 let idArrays : [UInt64]=[]
420 while(i < ids.length) {
421 idArrays.append(ids[UInt64(i)]!)
422 i=i+1
423 }
424 return idArrays
425 }
426
427 //todo: collapse
428 access(NonFungibleToken.Update) fun updateDoodle(wearableCollection: auth(NonFungibleToken.Withdraw) &Wearables.Collection, equipped: [UInt64],quote: String, expression: String, mood: String, background: String, hairStyle: String, hairColor: String, facialHair: String, facialHairColor: String, skinTone: String, pose: String, stage: String, location: String) {
429
430 let existingIds = self.wearables.keys
431
432 for wId in existingIds {
433 if equipped.contains(wId) {
434 //if an id is already equipped skip it
435 continue
436 }
437 let nft <- self.wearables.remove(key: wId)!
438 let p = nft.template.getPosition()
439 emit Unequipped(id:self.id, subId: wId, subSlot: p.name, resourceType: nft.getType().identifier, address:self.owner!.address, tags: {"wearableId": wId.toString()}, context: {"wearablePosition" : p.name, "wearableName" : nft.template.name})
440 wearableCollection.deposit(token: <- nft)
441 }
442
443 for wId in equipped {
444 if existingIds.contains(wId) {
445 //we already have it on
446 continue
447 }
448
449 let wearableNFT <- wearableCollection.withdraw(withdrawID: wId)
450 let nft <- wearableNFT as! @Wearables.NFT
451 nft.equipped(owner: self.owner!.address, characterId: self.id)
452 let p = nft.template.getPosition()
453 let t = nft.getType()
454 emit Equipped(id:self.id, subId:wId, subSlot: p.name, resourceType: t.identifier, address:self.owner!.address, tags: {"wearableId": wId.toString()}, context: {"wearablePosition" : p.name, "wearableName" : nft.getName()})
455 self.wearables[wId] <-! nft
456
457 }
458
459 self.baseCharacter.updateTrait(name:"quote",value: quote)
460 self.baseCharacter.updateTrait(name:"expression",value: expression)
461 self.baseCharacter.updateTrait(name:"mood",value: mood)
462 self.baseCharacter.updateTrait(name:"background",value: background)
463 self.baseCharacter.updateTrait(name:"hair_style",value: hairStyle)
464 self.baseCharacter.updateTrait(name:"hair_color",value: hairColor)
465 self.baseCharacter.updateTrait(name:"facial_hair",value: facialHair)
466 self.baseCharacter.updateTrait(name:"facial_hair_color",value: facialHairColor)
467 self.baseCharacter.updateTrait(name:"skin_tone",value: skinTone)
468 self.baseCharacter.updateTrait(name:"pose",value: pose)
469 self.baseCharacter.updateTrait(name:"stage",value: stage)
470 self.baseCharacter.updateTrait(name:"location",value: location)
471
472 emit DoodleUpdated(id: self.id, owner: self.owner!.address)
473 Doodles.emitNFTUpdated(&self as auth(NonFungibleToken.Update) &{NonFungibleToken.NFT})
474 }
475
476 //expand
477 access(NonFungibleToken.Update) fun editDoodle(wearableCollection: auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Collection, NonFungibleToken.Provider}, equipped: [UInt64],quote: String, expression: String, mood: String, background: String, hairStyle: String, hairColor: String, facialHair: String, facialHairColor: String, skinTone: String, pose: String, stage: String, location: String, hairPinched:Bool, hideExpression:Bool) {
478 self.internalEditDoodle(
479 wearableReceiver: wearableCollection,
480 wearableProviders: [wearableCollection],
481 equipped: equipped,
482 quote: quote,
483 expression: expression,
484 mood: mood,
485 background: background,
486 hairStyle: hairStyle,
487 hairColor: hairColor,
488 facialHair: facialHair,
489 facialHairColor: facialHairColor,
490 skinTone: skinTone,
491 pose: pose,
492 stage: stage,
493 location: location,
494 hairPinched: hairPinched,
495 hideExpression: hideExpression
496 )
497 }
498
499 access(NonFungibleToken.Update) fun editDoodleWithMultipleCollections(receiverWearableCollection: &{NonFungibleToken.Receiver}, wearableCollections: [auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Collection, NonFungibleToken.Provider}], equipped: [UInt64],quote: String, expression: String, mood: String, background: String, hairStyle: String, hairColor: String, facialHair: String, facialHairColor: String, skinTone: String, pose: String, stage: String, location: String, hairPinched:Bool, hideExpression:Bool) {
500 self.internalEditDoodle(
501 wearableReceiver: receiverWearableCollection,
502 wearableProviders: wearableCollections,
503 equipped: equipped,
504 quote: quote,
505 expression: expression,
506 mood: mood,
507 background: background,
508 hairStyle: hairStyle,
509 hairColor: hairColor,
510 facialHair: facialHair,
511 facialHairColor: facialHairColor,
512 skinTone: skinTone,
513 pose: pose,
514 stage: stage,
515 location: location,
516 hairPinched: hairPinched,
517 hideExpression: hideExpression
518 )
519 }
520
521 access(contract) fun internalEditDoodle(wearableReceiver: &{NonFungibleToken.Receiver}, wearableProviders: [auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Collection, NonFungibleToken.Provider}], equipped: [UInt64],quote: String, expression: String, mood: String, background: String, hairStyle: String, hairColor: String, facialHair: String, facialHairColor: String, skinTone: String, pose: String, stage: String, location: String, hairPinched:Bool, hideExpression:Bool) {
522 let existingIds = self.wearables.keys
523
524 for wId in existingIds {
525 if equipped.contains(wId) {
526 //if an id is already equipped skip it
527 continue
528 }
529 let nft <- self.wearables.remove(key: wId)!
530 let p = nft.template.getPosition()
531 emit Unequipped(id:self.id, subId: wId, subSlot: p.name, resourceType: nft.getType().identifier, address:self.owner!.address, tags: {"wearableId": wId.toString()}, context: {"wearablePosition" : p.name, "wearableName" : nft.template.name})
532 wearableReceiver.deposit(token: <- nft)
533 }
534
535 for wearableProvider in wearableProviders {
536 for wId in equipped {
537 if !wearableProvider.getIDs().contains(wId) {
538 continue
539 }
540
541 let wearableNFT <- wearableProvider.withdraw(withdrawID: wId)
542 let nft <- wearableNFT as! @Wearables.NFT
543 nft.equipped(owner: self.owner!.address, characterId: self.id)
544 let p = nft.template.getPosition()
545 let t = nft.getType()
546 emit Equipped(id:self.id, subId:wId, subSlot: p.name, resourceType: t.identifier, address:self.owner!.address, tags: {"wearableId": wId.toString()}, context: {"wearablePosition" : p.name, "wearableName" : nft.getName()})
547 self.wearables[wId] <-! nft
548
549 }
550 }
551
552 var hairPinchedValue=""
553 if hairPinched {
554 hairPinchedValue="true"
555 }
556
557 var hideExpressionValue=""
558 if hideExpression {
559 hideExpressionValue="true"
560 }
561
562 self.baseCharacter.updateTrait(name:"quote",value: quote)
563 self.baseCharacter.updateTrait(name:"expression",value: expression)
564 self.baseCharacter.updateTrait(name:"expression_hide",value: hideExpressionValue)
565 self.baseCharacter.updateTrait(name:"mood",value: mood)
566 self.baseCharacter.updateTrait(name:"background",value: background)
567 self.baseCharacter.updateTrait(name:"hair_style",value: hairStyle)
568 self.baseCharacter.updateTrait(name:"hair_color",value: hairColor)
569 self.baseCharacter.updateTrait(name:"hair_pinched",value: hairPinchedValue)
570 self.baseCharacter.updateTrait(name:"facial_hair",value: facialHair)
571 self.baseCharacter.updateTrait(name:"facial_hair_color",value: facialHairColor)
572 self.baseCharacter.updateTrait(name:"skin_tone",value: skinTone)
573 self.baseCharacter.updateTrait(name:"pose",value: pose)
574 self.baseCharacter.updateTrait(name:"stage",value: stage)
575 self.baseCharacter.updateTrait(name:"location",value: location)
576
577 emit DoodleUpdated(id: self.id, owner: self.owner!.address)
578 Doodles.emitNFTUpdated(&self as auth(NonFungibleToken.Update) &{NonFungibleToken.NFT})
579 }
580
581 access(account) fun addName(_ nft: @DoodleNames.NFT, owner:Address) {
582 pre{
583 self.name.length == 0 : "This doodles have name equipped ID : ".concat(self.name.keys[0].toString())
584 }
585
586 nft.deposited(owner: owner, characterId: self.id)
587 self.name[nft.id] <-! nft
588 }
589
590 access(NonFungibleToken.Update) fun equipName(_ nft: @DoodleNames.NFT) {
591 pre{
592 self.name.length == 0 : "This doodles have name equipped ID : ".concat(self.name.keys[0].toString())
593 }
594
595 let action = "equipName"
596 Templates.assertFeatureEnabled(action)
597
598 let resourceId = nft.id
599 let t = nft.getType()
600 let characterName = nft.name
601 nft.deposited(owner: self.owner!.address, characterId: self.id)
602 self.name[nft.id] <-! nft
603 emit Equipped(id:self.id, subId: resourceId, subSlot: "name", resourceType: t.identifier, address:self.owner!.address, tags: {}, context: {"Name" : characterName})
604 }
605
606 access(contract) fun unequipName() : @DoodleNames.NFT {
607 pre{
608 self.name.length > 0 : "This character does not have name equipped ID : ".concat(self.id.toString())
609 }
610
611 let action = "unequipName"
612 Templates.assertFeatureEnabled(action)
613
614 let resourceId = self.getNameId()!
615 let nft <- self.name.remove(key: resourceId)!
616 nft.withdrawn()
617 let characterName = nft.name
618 let t = nft.getType()
619 emit Unequipped(id:self.id, subId: resourceId, subSlot: "name", resourceType: t.identifier, address:self.owner!.address, tags: {}, context: {"Name" : characterName})
620 return <- nft
621 }
622
623 access(contract) fun equipWearable(_ nft: @Wearables.NFT, index: UInt64) {
624 let action = "equipWearable"
625 Templates.assertFeatureEnabled(action)
626
627 let resourceId = nft.id
628 let t = nft.getType()
629 let name = nft.template.name
630 let p = nft.template.getPosition()
631 let positionCount = p.getPositionCount()
632 if UInt64(positionCount - 1) < index {
633 panic("Position index does not exist. Maximum index : ".concat((positionCount - 1).toString()))
634 }
635 let equipped = self.getWearablesAt(p.id)
636
637 //loop over these and order them by the value in positionIndex
638
639 for w in equipped {
640 if self.positionIndex[w]! == index {
641 panic("Position with index is already equipped. index : ".concat(index.toString()) )
642 }
643 }
644 self.positionIndex[resourceId] = index
645 equipped.append(resourceId)
646 assert(equipped.length <= positionCount, message : "You already equipped more wearables than you can at this position : ".concat(p.name).concat(" Max number of wearables : ".concat(positionCount.toString())))
647 nft.equipped(owner: self.owner!.address, characterId: self.id)
648 self.wearables[resourceId] <-! nft
649 assert(equipped.length <= positionCount, message : "You already equipped more wearables than you can at this position : ".concat(p.name).concat(" Max number of wearables : ".concat(positionCount.toString())))
650 emit Equipped(id:self.id, subId: resourceId, subSlot: p.name, resourceType: t.identifier, address:self.owner!.address, tags: {"wearableId": resourceId.toString()}, context: {"wearablePosition" : p.name, "wearableName" : name})
651 }
652
653 access(contract) fun unequipWearable(_ resourceId: UInt64) : @Wearables.NFT {
654 pre{
655 self.wearables.containsKey(resourceId) : "Wearables with id is not equipped : ".concat(resourceId.toString())
656 }
657
658 self.positionIndex.remove(key: resourceId)!
659 let action = "unequipWearable"
660 Templates.assertFeatureEnabled(action)
661
662
663 let nft <- self.wearables.remove(key: resourceId)!
664 let p = nft.template.getPosition()
665 emit Unequipped(id:self.id, subId: resourceId, subSlot: p.name, resourceType: nft.getType().identifier, address:self.owner!.address, tags: {"wearableId": resourceId.toString()}, context: {"wearablePosition" : p.name, "wearableName" : nft.template.name})
666 return <- nft
667 }
668
669 access(all) fun borrowWearable(_ id: UInt64) : &Wearables.NFT {
670 return (&self.wearables[id])!
671 }
672
673 access(all) fun borrowName(_ id: UInt64) : &DoodleNames.NFT {
674 return (&self.name[id])!
675 }
676
677 access(all) fun borrowWearableViewResolver(_ id: UInt64) : &{ViewResolver.Resolver}? {
678 if let nft = &self.wearables[id] as &Wearables.NFT? {
679 return nft
680 }
681 return nil
682 }
683
684 access(contract) fun increaseNounce() {
685 self.nounce=self.nounce+1
686 }
687
688 access(all) fun getTraitsAsTraits() : MetadataViews.Traits {
689 let traits=self.getAllTraitsMetadata()
690 let res : [MetadataViews.Trait] = []
691 for t in traits.values {
692 res.appendAll(t)
693 }
694 return MetadataViews.Traits(res)
695 }
696
697 access(all) fun getAllTraitsMetadata() : {String : [MetadataViews.Trait]} {
698 var traitMetadata : [MetadataViews.Trait] = self.baseCharacter.getTraits()
699 let wearableTraits = self.getWearableTraits()
700 wearableTraits["doodles"] = traitMetadata
701 if let nameTrait = self.getNameTrait() {
702 wearableTraits["name"] = [nameTrait]
703 }
704
705 wearableTraits["dna"] = [MetadataViews.Trait(name:"dna", value:self.calculateDNA(), displayType:"string", rarity:nil)]
706
707 let ctx = self.getContext()
708 for key in ctx.keys{
709 let traitKey ="context_".concat(key)
710 wearableTraits[traitKey]=[MetadataViews.Trait(name:traitKey, value: ctx[key], displayType:"string", rarity:nil)]
711 }
712 return wearableTraits
713 }
714
715 access(all) fun getNameTrait() : MetadataViews.Trait? {
716 if let name = self.getName() {
717 return MetadataViews.Trait(name: "doodle_name", value: name, displayType:"string", rarity:nil)
718 }
719 return nil
720 }
721
722 //b64(<doodleName>-<pose value>-<expression value>-<skinTone value>-<hairStyle value>-<hairColor value>-<id of doodle>-[<array of equipped NFT Ids>])
723 access(all) fun calculateDNA() : String {
724 return self.calculateDNACustom(sep: "|", encode: true)
725 }
726
727 access(all) fun calculateDNACustom(sep: String, encode: Bool) : String {
728 let seperator=sep
729 let traits =self.baseCharacter.getTraitsAsMap()
730 var dna=""
731 dna=dna.concat(self.getName() ?? "").concat(seperator)
732 dna=dna.concat(traits["pose"] ?? "").concat(seperator)
733 dna=dna.concat(traits["expression"]?? "").concat(seperator)
734 dna=dna.concat(traits["skin_tone"] ?? "").concat(seperator)
735 dna=dna.concat(traits["hair_style"]?? "").concat(seperator)
736 dna=dna.concat(traits["hair_color"] ?? "").concat(seperator)
737 dna=dna.concat(self.id.toString()).concat(seperator)
738 dna=dna.concat("[")
739
740 //https://www.geeksforgeeks.org/insertion-sort/
741 var ids = self.wearables.keys
742 for i, key in ids {
743 var j=i
744 while j >0 && ids[j-1] > ids[j] {
745 ids[j] <-> ids[j-1]
746 j=j-1
747 }
748 }
749
750 for id in ids {
751 dna = dna.concat(id.toString()).concat(seperator)
752 }
753 //remove the last seperator
754 dna=dna.slice(from: 0, upTo: dna.length - seperator.length)
755 dna=dna.concat("]")
756
757 if !encode {
758 return dna
759 }
760 return Doodles.base64encode(dna)
761 }
762
763 access(all) fun getWearableTraits() : {String : [MetadataViews.Trait]} {
764 let traitsToKeep = ["name", "position", "set", "layer"]
765 let mvt : {String : [MetadataViews.Trait]} = {}
766 let counter : {UInt64:Int} = {}
767 for key in self.wearables.keys{
768 let w = self.borrowWearable(key)
769
770 let position=w.getTemplate().id.toString()
771
772 let trait = MetadataViews.getTraits(w)
773 if trait != nil {
774 let cleanedTraits : [MetadataViews.Trait] = []
775 let traitIdentifer = "wearable_".concat(position).concat("_")
776
777 let idTrait=MetadataViews.Trait(name: traitIdentifer.concat("id"), value: w.id, displayType:"Number", rarity:nil)
778 cleanedTraits.append(idTrait)
779
780 let templateIdTrait=MetadataViews.Trait(name: traitIdentifer.concat("template_id"), value: w.getTemplate().id, displayType:"Number", rarity:nil)
781 cleanedTraits.append(templateIdTrait)
782
783 for t in trait!.traits {
784 if !traitsToKeep.contains(t.name) {
785 continue
786 }
787 let traitName = FindUtils.trimSuffix(traitIdentifer.concat(t.name), suffix: "_name")
788 let newTrait=MetadataViews.Trait(name: traitName, value: t.value, displayType:t.displayType, rarity:t.rarity)
789 cleanedTraits.append(newTrait)
790 }
791
792 let array = mvt[position] ?? []
793 array.appendAll(cleanedTraits)
794 mvt[position] = array
795 }
796 }
797 return mvt
798 }
799
800 /// Gets all the NFTs that this NFT directly owns
801 /// @return A dictionary of all subNFTS keyed by type
802 access(all) view fun getAvailableSubNFTS(): {Type: [UInt64]} {
803 let ownedNFTs: {Type: [UInt64]} = {}
804 if self.wearables.length > 0 {
805 let t = Type<@Wearables.NFT>()
806 var ids : [UInt64] = []
807 for key in self.wearables.keys {
808 ids = ids.concat([key])
809 }
810 ownedNFTs[t] = ids
811 }
812 if self.name.length > 0 {
813 let t = Type<@DoodleNames.NFT>()
814 ownedNFTs[t] = [self.name.keys[0]]
815 }
816 return ownedNFTs
817 }
818
819 /// Get a reference to an NFT that this NFT owns
820 /// Both arguments are optional to allow the NFT to choose
821 /// how it returns sub NFTs depending on what arguments are provided
822 /// For example, if `type` has a value, but `id` doesn't, the NFT
823 /// can choose which NFT of that type to return if there is a "default"
824 /// If both are `nil`, then NFTs that only store a single NFT can just return
825 /// that. This helps callers who aren't sure what they are looking for
826 ///
827 /// @param type: The Type of the desired NFT
828 /// @param id: The id of the NFT to borrow
829 ///
830 /// @return A structure representing the requested view.
831 access(all) fun getSubNFT(type: Type, id: UInt64) : &{NonFungibleToken.NFT}? {
832 switch type {
833 case Type<@Wearables.NFT>():
834 return &self.wearables[id]
835 case Type<@DoodleNames.NFT>():
836 return &self.name[id]
837 default:
838 return nil
839 }
840 }
841
842 access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
843 return <- Doodles.createEmptyCollection(nftType: Type<@Doodles.NFT>())
844 }
845 }
846
847 access(all) resource interface DoodlesCollectionPublic {
848 access(all) fun deposit(token: @{NonFungibleToken.NFT})
849 access(all) view fun getIDs(): [UInt64]
850 access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}?
851 access(all) view fun borrowDoodlesNFT(id: UInt64) : &NFT?
852 access(NonFungibleToken.Update) fun borrowDoodlesNFTUpdate(id: UInt64): auth(NonFungibleToken.Update) &NFT?
853 }
854
855 access(all) resource Collection: DoodlesCollectionPublic, NonFungibleToken.Collection {
856 // dictionary of NFT conforming tokens
857 // NFT is a resource type with an `UInt64` ID field
858 access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}}
859
860 init () {
861 self.ownedNFTs <- {}
862 }
863
864 // withdraw removes an NFT from the collection and moves it to the caller
865 access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} {
866 let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("missing NFT")
867 return <-token
868 }
869
870 // deposit takes a NFT and adds it to the collections dictionary
871 // and adds the ID to the id array
872 access(all) fun deposit(token: @{NonFungibleToken.NFT}) {
873 let token <- token as! @NFT
874
875 let id: UInt64 = token.id
876
877 token.increaseNounce()
878 // add the new token to the dictionary which removes the old one
879 let oldToken <- self.ownedNFTs[id] <- token
880
881
882 destroy oldToken
883 }
884
885 // getIDs returns an array of the IDs that are in the collection
886 access(all) view fun getIDs(): [UInt64] {
887 return self.ownedNFTs.keys
888 }
889
890 // borrowNFT gets a reference to an NFT in the collection
891 // so that the caller can read its metadata and call its methods
892 access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? {
893 return &self.ownedNFTs[id]
894 }
895
896 access(all) view fun borrowDoodlesNFT(id: UInt64) : &Doodles.NFT? {
897 return &self.ownedNFTs[id] as &{NonFungibleToken.NFT}? as! &Doodles.NFT?
898 }
899
900 access(NonFungibleToken.Update) fun borrowDoodlesNFTUpdate(id: UInt64) : auth(NonFungibleToken.Update) &Doodles.NFT? {
901 return &self.ownedNFTs[id] as auth(NonFungibleToken.Update) &{NonFungibleToken.NFT}? as! auth(NonFungibleToken.Update) &Doodles.NFT?
902 }
903
904 access(all) view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver}? {
905 if let nft = &self.ownedNFTs[id] as &{NonFungibleToken.NFT}? as! &Doodles.NFT? {
906 return nft
907 }
908 return nil
909 }
910
911 access(all) view fun getLength(): Int {
912 return self.ownedNFTs.keys.length
913 }
914
915 access(all) view fun getSupportedNFTTypes(): {Type: Bool} {
916 let supportedTypes: {Type: Bool} = {}
917 supportedTypes[Type<@Doodles.NFT>()] = true
918 return supportedTypes
919 }
920
921 access(all) view fun isSupportedNFTType(type: Type): Bool {
922 if type == Type<@Doodles.NFT>() {
923 return true
924 } else {
925 return false
926 }
927 }
928
929 access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
930 return <- Doodles.createEmptyCollection(nftType: Type<@Doodles.NFT>())
931 }
932 }
933
934 // public function that anyone can call to create a new empty collection
935 access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} {
936 return <- create Collection()
937 }
938
939 access(account) fun mintDoodle(
940 recipient: &{NonFungibleToken.Receiver},
941 betaPass: @Wearables.NFT,
942 doodleName: String,
943 baseCharacter: UInt64
944 ){
945 pre {
946 recipient.owner != nil : "Recipients NFT collection is not owned"
947 }
948
949 let template: Wearables.Template = betaPass.getTemplate()
950 assert(template.id == 244, message: "Not a valid beta pass")
951 let context: {String: String} = betaPass.context
952 destroy <- betaPass
953
954 let newNFT <- self.adminMintDoodle(
955 recipientAddress: recipient.owner!.address,
956 doodleName: doodleName,
957 baseCharacter: baseCharacter,
958 context: context
959 )
960
961 recipient.deposit(token: <-newNFT)
962 }
963
964 // mintNFT mints a new NFT with a new ID
965 // and deposit it in the recipients collection using their collection reference
966 //The distinction between sending in a reference and sending in a capability is that when you send in a reference it cannot be stored. So it can only be used in this method
967 //while a capability can be stored and used later. So in this case using a reference is the right choice, but it needs to be owned so that you can have a good event
968 access(account) fun adminMintDoodle(
969 recipientAddress: Address,
970 doodleName: String,
971 baseCharacter: UInt64,
972 context: {String : String}
973 ): @Doodles.NFT {
974 pre {
975 self.baseCharacters.containsKey(baseCharacter) : "Base Character does not exist. Id : ".concat(baseCharacter.toString())
976 }
977
978 Doodles.totalSupply = Doodles.totalSupply + 1
979 let baseCharacter = Doodles.borrowBaseCharacter(baseCharacter)
980 let set = Doodles.borrowSet(baseCharacter.set)
981 let species = Doodles.borrowSpecies(baseCharacter.species)
982
983 assert(set.active, message: "Set Retired : ".concat(set.name))
984 assert(species.active, message: "Species Retired : ".concat(species.name))
985 assert(baseCharacter.active, message: "BaseCharacter Retired : ".concat(baseCharacter.name))
986
987 let name <- DoodleNames.mintName(name:doodleName, context:context, address: recipientAddress)
988
989 let editions=[
990 baseCharacter.createEditionInfo(nil),
991 species.createEditionInfo(nil),
992 set.createEditionInfo(nil)
993 ]
994
995 // create a new NFT
996 var newNFT <- create NFT(
997 baseCharacter: Doodles.baseCharacters[baseCharacter.id]!,
998 editions: editions,
999 context:context
1000 )
1001 newNFT.addName(<- name, owner: recipientAddress)
1002
1003 emit Minted(id:newNFT.id, address: recipientAddress, set: set.name, setNumber: set.id, name: doodleName, context: context)
1004
1005 return <- newNFT
1006 }
1007
1008 access(account) fun borrowSet(_ id: UInt64) : &Doodles.Set {
1009 pre{
1010 self.sets.containsKey(id) : "Set does not exist. Id : ".concat(id.toString())
1011 }
1012 return &Doodles.sets[id]!
1013 }
1014
1015 access(account) fun borrowSpecies(_ id: UInt64) : &Doodles.Species {
1016 pre{
1017 self.species.containsKey(id) : "Species does not exist. Id : ".concat(id.toString())
1018 }
1019 return &Doodles.species[id]!
1020 }
1021
1022 access(account) fun borrowBaseCharacter(_ id: UInt64) : &Doodles.BaseCharacter {
1023 pre{
1024 self.baseCharacters.containsKey(id) : "BaseCharacter does not exist. Id : ".concat(id.toString())
1025 }
1026 return &Doodles.baseCharacters[id]!
1027 }
1028
1029 access(all) view fun getContractViews(resourceType: Type?): [Type] {
1030 return [
1031 Type<MetadataViews.NFTCollectionDisplay>(),
1032 Type<MetadataViews.NFTCollectionData>()
1033 ]
1034 }
1035
1036 access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
1037 let description="This Doodle is a uniquely personalized customizable character in a one-of-a-kind style."
1038 switch viewType {
1039 case Type<MetadataViews.NFTCollectionDisplay>():
1040 let externalURL = MetadataViews.ExternalURL("https://doodles.app")
1041 let squareImage = MetadataViews.Media(file: MetadataViews.IPFSFile(cid: "QmVpAiutpnzp3zR4q2cUedMxsZd8h5HDeyxs9x3HibsnJb", path:nil), mediaType:"image/png")
1042 let bannerImage = MetadataViews.Media(file: MetadataViews.HTTPFile(url: "https://res.cloudinary.com/hxn7xk7oa/image/upload/v1675121458/doodles2_banner_ee7a035d05.jpg"), mediaType: "image/jpeg")
1043 return MetadataViews.NFTCollectionDisplay(
1044 name: "Doodles",
1045 description: description,
1046 externalURL: externalURL,
1047 squareImage: squareImage,
1048 bannerImage: bannerImage,
1049 socials: {
1050 "discord": MetadataViews.ExternalURL("https://discord.gg/doodles"),
1051 "twitter" : MetadataViews.ExternalURL("https://twitter.com/Doodles")
1052 }
1053 )
1054 case Type<MetadataViews.NFTCollectionData>():
1055 return MetadataViews.NFTCollectionData(
1056 storagePath: Doodles.CollectionStoragePath,
1057 publicPath: Doodles.CollectionPublicPath,
1058 publicCollection: Type<&Collection>(),
1059 publicLinkedType: Type<&Collection>(),
1060 createEmptyCollectionFunction: (fun():
1061 @{NonFungibleToken.Collection} {return <- Doodles.createEmptyCollection(nftType: Type<@Doodles.NFT>())})
1062 )
1063 }
1064 return nil
1065 }
1066
1067 //MOVE TO UTIL
1068 //https://forum.onflow.org/t/base64-encode-in-cadence/1915/6
1069 access(all) fun base64encode(_ input: String): String {
1070 let data = input.utf8
1071
1072 let baseChars: [String] = [
1073 "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
1074 "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
1075 "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "+", "/"
1076 ]
1077
1078 var encoded = ""
1079 var padding = ""
1080 var padCount = data.length % 3
1081
1082 // Add a right zero pad to make the input a multiple of 3 characters
1083 if padCount > 0 {
1084 while padCount < 3 {
1085 padding = padding.concat("=")
1086 data.append(0)
1087 padCount = padCount + 1
1088 }
1089 }
1090
1091 // Increment over the length of the input, three bytes at a time
1092 var i = 0
1093 while i < data.length {
1094 // Each three bytes become one 24-bit number
1095 let n = (UInt32(data[i]) << 16)
1096 + (UInt32(data[i + 1]) << 8)
1097 + UInt32(data[i + 2])
1098
1099 // This 24-bit number gets separated into four 6-bit numbers
1100 let n1 = (n >> 18) & 63
1101 let n2 = (n >> 12) & 63
1102 let n3 = (n >> 6) & 63
1103 let n4 = n & 63
1104
1105 // Those four 6-bit numbers are used as indices into the base64 character list
1106 encoded = encoded
1107 .concat(baseChars[n1])
1108 .concat(baseChars[n2])
1109 .concat(baseChars[n3])
1110 .concat(baseChars[n4])
1111
1112 i = i + 3
1113 }
1114
1115 return encoded
1116 .slice(from: 0, upTo: encoded.length - padding.length)
1117 .concat(padding)
1118 }
1119
1120 init() {
1121 // Initialize the total supply
1122 self.totalSupply = 0
1123
1124 self.sets = {}
1125 self.species = {}
1126 self.baseCharacters = {}
1127
1128 // Set the named paths
1129 self.CollectionStoragePath = /storage/doodles
1130 self.CollectionPublicPath = /public/doodles
1131
1132 self.account.storage.save<@{NonFungibleToken.Collection}>(<- Doodles.createEmptyCollection(nftType: Type<@Doodles.NFT>()), to: Doodles.CollectionStoragePath)
1133 let cap = self.account .capabilities.storage.issue<&Collection>(Doodles.CollectionStoragePath)
1134 self.account.capabilities.publish(cap, at: Doodles.CollectionPublicPath)
1135
1136 emit ContractInitialized()
1137 }
1138}