Smart Contract

SturdyTokens

A.427ceada271aa0b1.SturdyTokens

Deployed

19h ago
Feb 27, 2026, 10:12:42 PM UTC

Dependents

0 imports
1import FungibleToken from 0xf233dcee88fe0abe
2import NonFungibleToken from 0x1d7e57aa55817448
3import MetadataViews from 0x1d7e57aa55817448
4
5pub contract SturdyTokens: NonFungibleToken {
6
7  pub event ContractInitialized()
8  pub event AccountInitialized()
9  pub event SetCreated(setID: UInt64)
10  pub event NFTTemplateCreated(templateID: UInt64, metadata: {String: String})
11  pub event Withdraw(id: UInt64, from: Address?)
12  pub event Deposit(id: UInt64, to: Address?)
13  pub event Minted(id: UInt64, templateID: UInt64)
14  pub event TemplateAddedToSet(setID: UInt64, templateID: UInt64)
15  pub event TemplateLockedFromSet(setID: UInt64, templateID: UInt64)
16  pub event TemplateUpdated(template: SturdyTokensTemplate)
17  pub event SetLocked(setID: UInt64)
18  pub event SetUnlocked(setID: UInt64)
19  pub event Burned(owner: Address?, id: UInt64, templateID: UInt64, setID: UInt64)
20  
21  pub let CollectionStoragePath: StoragePath
22  pub let CollectionPublicPath: PublicPath
23  pub let AdminStoragePath: StoragePath
24
25  pub var totalSupply: UInt64
26  pub var initialNFTID: UInt64
27  pub var nextNFTID: UInt64
28  pub var nextTemplateID: UInt64
29  pub var nextSetID: UInt64
30
31  access(self) var sturdyTokensTemplates: {UInt64: SturdyTokensTemplate}
32  access(self) var sets: @{UInt64: Set}
33
34  pub resource interface SturdyTokensCollectionPublic {
35    pub fun deposit(token: @NonFungibleToken.NFT)
36    pub fun getIDs(): [UInt64]
37    pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT
38    pub fun borrowSturdyToken(id: UInt64): &SturdyTokens.NFT? {
39      post {
40        (result == nil) || (result?.id == id):
41          "Cannot borrow SturdyTokens reference: The ID of the returned reference is incorrect"
42      }
43    }
44  }
45
46  pub struct SturdyTokensTemplate {
47    pub let templateID: UInt64
48    pub var name: String
49    pub var description: String
50    pub var locked: Bool
51    pub var addedToSet: UInt64
52    access(self) var metadata: {String: String}
53
54    pub fun getMetadata(): {String: String} {
55      return self.metadata
56    }
57
58    pub fun lockTemplate() {      
59      self.locked = true
60    }
61
62    pub fun updateMetadata(newMetadata: {String: String}) {
63      pre {
64        newMetadata.length != 0: "New Template metadata cannot be empty"
65      }
66      self.metadata = newMetadata
67    }
68    
69    pub fun markAddedToSet(setID: UInt64) {
70      self.addedToSet = setID
71    }
72
73    init(templateID: UInt64, name: String, description: String, metadata: {String: String}){
74      pre {
75        metadata.length != 0: "New Template metadata cannot be empty"
76      }
77
78      self.templateID = templateID
79      self.name = name
80      self.description= description
81      self.metadata = metadata
82      self.locked = false
83      self.addedToSet = 0
84
85      emit NFTTemplateCreated(templateID: self.templateID, metadata: self.metadata)
86    }
87  }
88
89  pub struct Royalty {
90    pub let address: Address
91    pub let description: String
92
93    init(address: Address, primaryCut: UFix64, secondaryCut: UFix64, description: String) {
94      pre {
95          primaryCut >= 0.0 && primaryCut <= 1.0 : "primaryCut value should be in valid range i.e [0,1]"
96          secondaryCut >= 0.0 && secondaryCut <= 1.0 : "secondaryCut value should be in valid range i.e [0,1]"
97      }
98      self.address = address
99      self.description = description
100    }
101  }
102
103  pub resource NFT: NonFungibleToken.INFT, MetadataViews.Resolver {
104    pub let id: UInt64
105    pub let templateID: UInt64
106    pub var serialNumber: UInt64
107    
108    pub fun getViews(): [Type] {
109      return [
110        Type<MetadataViews.ExternalURL>(),
111        Type<MetadataViews.NFTCollectionData>(),
112        Type<MetadataViews.NFTCollectionDisplay>(),
113        Type<MetadataViews.Display>(),
114        Type<MetadataViews.Medias>(),
115        Type<MetadataViews.Royalties>()
116      ]
117    }
118
119     pub fun resolveView(_ view: Type): AnyStruct? {
120      let metadata = SturdyTokens.sturdyTokensTemplates[self.templateID]!.getMetadata()
121      let thumbnailCID = metadata["thumbnailCID"] != nil ? metadata["thumbnailCID"]! : metadata["imageCID"]!
122      switch view {
123        case Type<MetadataViews.ExternalURL>():
124          return MetadataViews.ExternalURL("https://ipfs.io/ipfs/".concat(thumbnailCID))
125        case Type<MetadataViews.NFTCollectionData>():
126          return MetadataViews.NFTCollectionData(
127              storagePath: SturdyTokens.CollectionStoragePath,
128              publicPath: SturdyTokens.CollectionPublicPath,
129              providerPath: /private/SturdyTokensCollection,
130              publicCollection: Type<&SturdyTokens.Collection{SturdyTokens.SturdyTokensCollectionPublic}>(),
131              publicLinkedType: Type<&SturdyTokens.Collection{SturdyTokens.SturdyTokensCollectionPublic,NonFungibleToken.CollectionPublic,NonFungibleToken.Receiver,MetadataViews.ResolverCollection}>(),
132              providerLinkedType: Type<&SturdyTokens.Collection{SturdyTokens.SturdyTokensCollectionPublic,NonFungibleToken.CollectionPublic,NonFungibleToken.Provider,MetadataViews.ResolverCollection}>(),
133              createEmptyCollectionFunction: (fun (): @NonFungibleToken.Collection {
134                  return <-SturdyTokens.createEmptyCollection()
135              })
136          )
137        case Type<MetadataViews.NFTCollectionDisplay>():
138          let media = MetadataViews.Media(
139            file: MetadataViews.HTTPFile(url: "https://ipfs.io/ipfs/bafkreigzbmx5vrynlnau2bchis76gz2jp7fylcs3kh6aqbfzhky22sko3y"),
140            mediaType: "image/jpeg"
141          )
142          return MetadataViews.NFTCollectionDisplay(
143            name: "Sturdy Exchange",
144            description: "",
145            externalURL: MetadataViews.ExternalURL("https://sturdy.exchange/"),
146            squareImage: media,
147            bannerImage: media,
148            socials: {}
149          )
150        case Type<MetadataViews.Display>():
151          return MetadataViews.Display(
152            name: SturdyTokens.sturdyTokensTemplates[self.templateID]!.name,
153            description: SturdyTokens.sturdyTokensTemplates[self.templateID]!.description,
154            thumbnail: MetadataViews.HTTPFile(
155              url: "https://ipfs.io/ipfs/".concat(SturdyTokens.sturdyTokensTemplates[self.templateID]!.getMetadata()["imageCID"]!)
156            )
157          )
158        case Type<MetadataViews.Medias>():
159          let medias: [MetadataViews.Media] = [];
160          let videoCID = SturdyTokens.sturdyTokensTemplates[self.templateID]!.getMetadata()["videoCID"]
161          let imageCID = SturdyTokens.sturdyTokensTemplates[self.templateID]!.getMetadata()["imageCID"]
162          if videoCID != nil {
163            medias.append(
164              MetadataViews.Media(
165                file: MetadataViews.HTTPFile(
166                  url: "https://ipfs.io/ipfs/".concat(videoCID!)
167                ),
168                mediaType: "video/mp4"
169              )
170            )
171          }
172          else if imageCID != nil {
173            medias.append(
174              MetadataViews.Media(
175                file: MetadataViews.HTTPFile(
176                  url: "https://ipfs.io/ipfs/".concat(imageCID!)
177                ),
178                mediaType: "image/jpeg"
179              )
180            )
181          }
182          return MetadataViews.Medias(medias)
183        case Type<MetadataViews.Royalties>():
184          let setID = SturdyTokens.sturdyTokensTemplates[self.templateID]!.addedToSet
185          let setRoyalties = SturdyTokens.getSetRoyalties(setID: setID)
186          let royalties: [MetadataViews.Royalty] = []
187          for royalty in setRoyalties {
188            royalties.append(
189              MetadataViews.Royalty(
190                receiver: getAccount(royalty.address)
191                    .getCapability<&{FungibleToken.Receiver}>(/public/dapperUtilityCoinReceiver),
192                cut: 0.05,
193                description: royalty.description
194              )
195            )
196          }
197          return MetadataViews.Royalties(royalties)
198      }
199      return nil
200    }
201
202    pub fun getNFTMetadata(): {String: String} {
203      return SturdyTokens.sturdyTokensTemplates[self.templateID]!.getMetadata()
204    }
205
206    pub fun getSetID(): UInt64 {
207      return SturdyTokens.sturdyTokensTemplates[self.templateID]!.addedToSet
208    }
209
210    init(initID: UInt64, initTemplateID: UInt64, serialNumber: UInt64) {
211      self.id = initID
212      self.templateID = initTemplateID
213      self.serialNumber = serialNumber
214    }
215  }
216
217  pub resource Collection: SturdyTokensCollectionPublic, NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection {
218    pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT}
219
220    pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT {
221      let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("missing NFT")
222      emit Withdraw(id: token.id, from: self.owner?.address)
223      return <-token
224    }
225
226    pub fun deposit(token: @NonFungibleToken.NFT) {
227      let token <- token as! @SturdyTokens.NFT
228      let id: UInt64 = token.id
229      let oldToken <- self.ownedNFTs[id] <- token
230      emit Deposit(id: id, to: self.owner?.address)
231      destroy oldToken
232    }
233
234    pub fun batchDeposit(collection: @Collection) {
235      let keys = collection.getIDs()
236      for key in keys {
237        self.deposit(token: <-collection.withdraw(withdrawID: key))
238      }
239      destroy collection
240    }
241
242    pub fun getIDs(): [UInt64] {
243      return self.ownedNFTs.keys
244    }
245
246    pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT {
247      return (&self.ownedNFTs[id] as &NonFungibleToken.NFT?)!
248    }
249
250    pub fun borrowSturdyToken(id: UInt64): &SturdyTokens.NFT? {
251      if self.ownedNFTs[id] != nil {
252        let ref = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)!
253        return ref as! &SturdyTokens.NFT
254      } else {
255        return nil
256      }
257    }
258
259    pub fun borrowViewResolver(id: UInt64): &AnyResource{MetadataViews.Resolver} {
260      let nft = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)!
261      let exampleNFT = nft as! &SturdyTokens.NFT
262      return exampleNFT as &AnyResource{MetadataViews.Resolver}
263    }
264
265    pub fun burn(burnID: UInt64) {
266      let token <- self.withdraw(withdrawID: burnID) as! @SturdyTokens.NFT
267      let templateID = token.templateID
268      let setID = SturdyTokens.sturdyTokensTemplates[templateID]!.addedToSet
269      destroy token;
270      emit Burned(owner: self.owner?.address, id: burnID, templateID: templateID, setID: setID)
271    }
272
273    destroy() {
274      destroy self.ownedNFTs
275    }
276
277    init () {
278      self.ownedNFTs <- {}
279    }
280  }
281  
282  pub fun createEmptyCollection(): @NonFungibleToken.Collection {
283    emit AccountInitialized()
284    return <- create Collection()
285  }
286
287  pub resource Set {
288    pub let setID: UInt64
289    pub let name: String
290    access(self) var templateIDs: [UInt64]
291    access(self) var availableTemplateIDs: [UInt64]
292    access(self) var lockedTemplates: {UInt64: Bool}
293    pub var locked: Bool
294    pub var nextSetSerialNumber: UInt64
295    pub var isPublic: Bool
296    pub var artistRoyalties: [Royalty]
297
298
299    init(name: String, sturdyRoyaltyAddress: Address, sturdyRoyaltySecondaryCut: UFix64) {
300      self.name = name
301      self.setID = SturdyTokens.nextSetID
302      self.templateIDs = []
303      self.lockedTemplates = {}
304      self.locked = false
305      self.availableTemplateIDs = []
306      self.nextSetSerialNumber = 1
307      self.isPublic = false
308      self.artistRoyalties = []
309      
310      SturdyTokens.nextSetID = SturdyTokens.nextSetID + 1
311      emit SetCreated(setID: self.setID)
312    }
313
314    pub fun getAvailableTemplateIDs(): [UInt64] {
315      return self.availableTemplateIDs
316    }
317
318    pub fun makeSetPublic() {
319      self.isPublic = true
320    }
321
322    pub fun makeSetPrivate() {
323      self.isPublic = false
324    }
325
326    pub fun addArtistRoyalty(royalty: Royalty) {
327      self.artistRoyalties.append(royalty)
328    }
329
330    pub fun addTemplate(templateID: UInt64, available: Bool) {
331      pre {
332        SturdyTokens.sturdyTokensTemplates[templateID] != nil:
333          "Template doesn't exist"
334        !self.locked:
335          "Cannot add template - set is locked"
336        !self.templateIDs.contains(templateID):
337          "Cannot add template - template is already added to the set"
338        !(SturdyTokens.sturdyTokensTemplates[templateID]!.addedToSet != 0):
339          "Cannot add template - template is already added to another set"
340      }
341
342      self.templateIDs.append(templateID)
343      if available {
344        self.availableTemplateIDs.append(templateID)
345      }
346      self.lockedTemplates[templateID] = !available
347      SturdyTokens.sturdyTokensTemplates[templateID]!.markAddedToSet(setID: self.setID)
348
349      emit TemplateAddedToSet(setID: self.setID, templateID: templateID)
350    }
351
352    pub fun addTemplates(templateIDs: [UInt64], available: Bool) {
353      for template in templateIDs {
354        self.addTemplate(templateID: template, available: available)
355      }
356    }
357
358    pub fun lockTemplate(templateID: UInt64) {
359      pre {
360        self.lockedTemplates[templateID] != nil:
361          "Cannot lock the template: Template is locked already!"
362        !self.availableTemplateIDs.contains(templateID):
363          "Cannot lock a not yet minted template!"
364      }
365
366      if !self.lockedTemplates[templateID]! {
367        self.lockedTemplates[templateID] = true
368        emit TemplateLockedFromSet(setID: self.setID, templateID: templateID)
369      }
370    }
371
372    pub fun lockAllTemplates() {
373      for template in self.templateIDs {
374        self.lockTemplate(templateID: template)
375      }
376    }
377
378    pub fun lock() {
379      if !self.locked {
380          self.locked = true
381          emit SetLocked(setID: self.setID)
382      }
383    }
384
385    pub fun unlock() {
386      if self.locked {
387          self.locked = false
388          emit SetUnlocked(setID: self.setID)
389      }
390    }
391
392    pub fun mintNFT(): @NFT {
393      let templateID = self.availableTemplateIDs[0]
394      if (SturdyTokens.sturdyTokensTemplates[templateID]!.locked) {
395        panic("template is locked")
396      }
397
398      let newNFT: @NFT <- create SturdyTokens.NFT(initID: SturdyTokens.nextNFTID, initTemplateID: templateID, serialNumber: self.nextSetSerialNumber)
399      
400      SturdyTokens.totalSupply = SturdyTokens.totalSupply + 1
401      SturdyTokens.nextNFTID = SturdyTokens.nextNFTID + 1
402      self.nextSetSerialNumber = self.nextSetSerialNumber + 1
403      self.availableTemplateIDs.remove(at: 0)
404
405      emit Minted(id: newNFT.id, templateID: newNFT.templateID)
406
407      return <-newNFT
408    }
409
410    pub fun mintNFTByTemplateID(templateID: UInt64): @NFT {
411      let newNFT: @NFT <- create SturdyTokens.NFT(initID: templateID, initTemplateID: templateID, serialNumber: self.nextSetSerialNumber)
412      
413      SturdyTokens.totalSupply = SturdyTokens.totalSupply + 1
414      self.nextSetSerialNumber = self.nextSetSerialNumber + 1
415      self.lockTemplate(templateID: templateID)
416
417      emit Minted(id: newNFT.id, templateID: newNFT.templateID)
418
419      return <-newNFT
420    }
421
422    pub fun updateTemplateMetadata(templateID: UInt64, newMetadata: {String: String}):SturdyTokensTemplate {
423      pre {
424        SturdyTokens.sturdyTokensTemplates[templateID] != nil:
425          "Template doesn't exist"
426        !self.locked:
427          "Cannot edit template - set is locked"
428      }
429
430      SturdyTokens.sturdyTokensTemplates[templateID]!.updateMetadata(newMetadata: newMetadata)
431      emit TemplateUpdated(template: SturdyTokens.sturdyTokensTemplates[templateID]!)
432      return SturdyTokens.sturdyTokensTemplates[templateID]!
433    }
434  }
435
436  pub fun getSetName(setID: UInt64): String {
437    pre {
438      SturdyTokens.sets[setID] != nil: "Cannot borrow Set: The Set doesn't exist"
439    }
440      
441    let set = (&SturdyTokens.sets[setID] as &Set?)!
442    return set.name
443  }
444
445  pub fun getSetRoyalties(setID: UInt64): [Royalty] {
446    pre {
447      SturdyTokens.sets[setID] != nil: "Cannot borrow Set: The Set doesn't exist"
448    }
449      
450    let set = (&SturdyTokens.sets[setID] as &Set?)!
451    var sturdyRoyaltyPrimaryCut: UFix64 = 1.00
452    // for royalty in set.artistRoyalties {
453    //   sturdyRoyaltyPrimaryCut = sturdyRoyaltyPrimaryCut - royalty.primaryCut
454    // }
455    let royalties = [
456      Royalty(
457        address: 0xd43cf319894f9662,
458        primaryCut: sturdyRoyaltyPrimaryCut,
459        secondaryCut: 0.10,
460        description: "Sturdy Royalty"
461      )
462    ]
463    royalties.appendAll(set.artistRoyalties)
464    return royalties
465  }
466
467  pub resource Admin {
468
469    pub fun mintNFT(recipient: &{NonFungibleToken.CollectionPublic}, setID: UInt64) {
470      let set = self.borrowSet(setID: setID)
471      if (set.getAvailableTemplateIDs()!.length == 0){
472        panic("Set is empty")
473      }
474      if (set.locked) {
475        panic("Set is locked")
476      }
477      recipient.deposit(token: <- set.mintNFT())
478    }
479
480    pub fun createAndMintNFT(recipient: &{NonFungibleToken.CollectionPublic}, templateID: UInt64, setID: UInt64, name: String, description: String, metadata: {String: String}) {
481      if SturdyTokens.sturdyTokensTemplates[templateID] != nil {
482        panic("Template already exists")
483      }
484      SturdyTokens.sturdyTokensTemplates[templateID] = SturdyTokensTemplate(
485        templateID: templateID,
486        name: name,
487        description: description,
488        metadata: metadata
489      )
490      let set = self.borrowSet(setID: setID)
491      set.addTemplate(templateID: templateID, available: false)
492      recipient.deposit(token: <- set.mintNFTByTemplateID(templateID: templateID))
493    }
494
495    pub fun createSturdyTokensTemplate(name: String, description: String, metadata: {String: String}) {
496      SturdyTokens.sturdyTokensTemplates[SturdyTokens.nextTemplateID] = SturdyTokensTemplate(
497        templateID: SturdyTokens.nextTemplateID,
498        name: name,
499        description: description,
500        metadata: metadata
501      )
502      SturdyTokens.nextTemplateID = SturdyTokens.nextTemplateID + 1
503    }
504
505    pub fun createSet(name: String, sturdyRoyaltyAddress: Address, sturdyRoyaltySecondaryCut: UFix64): UInt64 {
506      var newSet <- create Set(name: name, sturdyRoyaltyAddress: sturdyRoyaltyAddress, sturdyRoyaltySecondaryCut: sturdyRoyaltySecondaryCut)
507      let setID = newSet.setID
508      SturdyTokens.sets[setID] <-! newSet
509      return setID
510    }
511
512    pub fun borrowSet(setID: UInt64): &Set {
513      pre {
514        SturdyTokens.sets[setID] != nil:
515          "Cannot borrow Set: The Set doesn't exist"
516      }
517      
518      return (&SturdyTokens.sets[setID] as &Set?)!
519    }
520
521    pub fun updateSturdyTokensTemplate(templateID: UInt64, newMetadata: {String: String}) {
522      pre {
523        SturdyTokens.sturdyTokensTemplates.containsKey(templateID) != nil:
524          "Template does not exists."
525      }
526      SturdyTokens.sturdyTokensTemplates[templateID]!.updateMetadata(newMetadata: newMetadata)
527    }
528
529    pub fun setInitialNFTID(initialNFTID: UInt64) {
530      pre {
531        SturdyTokens.initialNFTID == 0:
532          "initialNFTID is already initialized"
533      }
534      SturdyTokens.initialNFTID = initialNFTID
535      SturdyTokens.nextNFTID = initialNFTID
536      SturdyTokens.nextTemplateID = initialNFTID
537    }
538
539  }
540
541  pub fun getSturdyTokensTemplateByID(templateID: UInt64): SturdyTokens.SturdyTokensTemplate {
542    return SturdyTokens.sturdyTokensTemplates[templateID]!
543  }
544
545  pub fun getSturdyTokensTemplates(): {UInt64: SturdyTokens.SturdyTokensTemplate} {
546    return SturdyTokens.sturdyTokensTemplates
547  }
548
549  pub fun getAvailableTemplateIDsInSet(setID: UInt64): [UInt64] {
550    pre {
551        SturdyTokens.sets[setID] != nil:
552        "Cannot borrow Set: The Set doesn't exist"
553    }
554    let set = (&SturdyTokens.sets[setID] as &Set?)!
555    return set.getAvailableTemplateIDs()
556  }
557
558  init() {
559    self.CollectionStoragePath = /storage/SturdyTokensCollection
560    self.CollectionPublicPath = /public/SturdyTokensCollection
561    self.AdminStoragePath = /storage/SturdyTokensAdmin
562
563    self.totalSupply = 0
564    self.nextSetID = 1
565    self.initialNFTID = 0
566    self.nextNFTID = 0
567    self.nextTemplateID = 0
568    self.sets <- {}
569
570    self.sturdyTokensTemplates = {}
571
572    let admin <- create Admin()
573    self.account.save(<-admin, to: self.AdminStoragePath)
574
575    emit ContractInitialized()
576  }
577}