Smart Contract
SturdyTokens
A.427ceada271aa0b1.SturdyTokens
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}