Smart Contract
Redeemables
A.e81193c424cfd3fb.Redeemables
1import FungibleToken from 0xf233dcee88fe0abe
2import NonFungibleToken from 0x1d7e57aa55817448
3import MetadataViews from 0x1d7e57aa55817448
4import ViewResolver from 0x1d7e57aa55817448
5
6access(all) contract Redeemables: NonFungibleToken {
7 access(all) event ContractInitialized()
8 access(all) event Minted(id: UInt64, address: Address, setId: UInt64, templateId: UInt64, name: String)
9 access(all) event Redeemed(id: UInt64, address: Address, setId: UInt64, templateId: UInt64, name: String)
10 access(all) event Burned(id: UInt64, address: Address, setId: UInt64, templateId: UInt64, name: String)
11 access(all) event SetCreated(id: UInt64, name: String)
12 access(all) event TemplateCreated(id: UInt64, setId: UInt64, name: String)
13
14 access(all) let CollectionStoragePath: StoragePath
15 access(all) let CollectionPublicPath: PublicPath
16 access(all) let AdminStoragePath: StoragePath
17
18 access(all) var totalSupply: UInt64
19 access(all) let sets: {UInt64 : Set}
20 access(all) let templates: {UInt64 : Template}
21 access(all) let templateNextSerialNumber: {UInt64 : UInt64}
22
23 access(self) let extra: {String: AnyStruct}
24
25 access(all) struct Set {
26 access(all) let id: UInt64
27 access(all) var name: String
28 access(all) var canRedeem: Bool
29 access(all) var redeemLimitTimestamp : UFix64
30 access(all) var active: Bool
31 access(contract) let ownersRecord: [Address]
32
33 access(self) let extra: {String: AnyStruct}
34
35 init(name: String, canRedeem: Bool, redeemLimitTimestamp: UFix64, active: Bool) {
36 self.id = UInt64(Redeemables.sets.keys.length) + 1
37 self.name = name
38 self.canRedeem = canRedeem
39 self.redeemLimitTimestamp = redeemLimitTimestamp
40 self.active = active
41 self.ownersRecord = []
42 self.extra = {}
43 }
44
45 access(all) fun getName() : String {
46 return self.name
47 }
48
49 access(all) fun isRedeemLimitExceeded() : Bool {
50 return self.redeemLimitTimestamp < getCurrentBlock().timestamp
51 }
52
53 access(contract) fun setActive(_ active: Bool) {
54 self.active = active
55 }
56
57 access(contract) fun setCanRedeem(_ canRedeem: Bool) {
58 self.canRedeem = canRedeem
59 }
60
61 access(contract) fun setRedeemLimitTimestamp(_ redeemLimitTimestamp: UFix64) {
62 self.redeemLimitTimestamp = redeemLimitTimestamp
63 }
64
65 access(contract) fun addOwnerRecord(_ address: Address): Bool {
66 if self.ownersRecord.contains(address) {
67 return false
68 }
69 self.ownersRecord.append(address)
70 return true
71 }
72 }
73
74 access(all) struct Template {
75 access(all) let id: UInt64
76 access(all) let setId: UInt64
77 access(all) var name: String
78 access(all) var description: String
79 access(all) var brand: String
80 access(all) var royalties: [MetadataViews.Royalty]
81 access(all) var type: String
82 access(all) var thumbnail: MetadataViews.Media
83 access(all) var image: MetadataViews.Media
84 access(all) var active: Bool
85
86 access(self) let extra: {String : AnyStruct}
87
88 init(setId: UInt64, name: String, description: String, brand: String, royalties: [MetadataViews.Royalty], type: String, thumbnail: MetadataViews.Media, image: MetadataViews.Media, active: Bool) {
89 pre {
90 Redeemables.sets.containsKey(setId) : "Set does not exist. Id: ".concat(setId.toString())
91 }
92 self.id = UInt64(Redeemables.templates.keys.length) + 1
93 self.setId = setId
94 self.name = name
95 self.description = description
96 self.brand = brand
97 self.royalties = royalties
98 self.type = type
99 self.thumbnail = thumbnail
100 self.image = image
101 self.active = active
102 self.extra = {}
103 }
104
105 access(all) fun getSet() : Redeemables.Set {
106 return Redeemables.sets[self.setId]!
107 }
108
109 access(contract) fun setActive(_ active: Bool) {
110 self.active = active
111 }
112 }
113
114 access(all) resource NFT: NonFungibleToken.NFT, ViewResolver.Resolver {
115 access(all) let id:UInt64
116 access(all) let templateId: UInt64
117 access(all) let serialNumber: UInt64
118
119 access(self) let extra: {String : AnyStruct}
120
121 init(templateId: UInt64) {
122 self.id = self.uuid
123 self.templateId = templateId
124 self.serialNumber = Redeemables.templateNextSerialNumber[templateId] ?? 1
125 self.extra = {}
126
127 Redeemables.templateNextSerialNumber[templateId] = self.serialNumber + 1
128 Redeemables.totalSupply = Redeemables.totalSupply + 1
129 }
130
131 access(all) view fun getViews(): [Type] {
132 return [
133 Type<MetadataViews.Display>(),
134 Type<MetadataViews.Royalties>(),
135 Type<MetadataViews.ExternalURL>(),
136 Type<MetadataViews.NFTCollectionData>(),
137 Type<MetadataViews.NFTCollectionDisplay>(),
138 Type<MetadataViews.Traits>(),
139 Type<MetadataViews.Editions>()
140 ]
141 }
142
143 access(all) fun getTemplate() : Template {
144 return Redeemables.templates[self.templateId]!
145 }
146
147 access(all) fun getSet() : Set {
148 return self.getTemplate().getSet()
149 }
150
151 access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
152 return <-Redeemables.createEmptyCollection(nftType: Type<@Redeemables.NFT>())
153 }
154
155 access(all) fun resolveView(_ view: Type): AnyStruct? {
156 switch view {
157 case Type<MetadataViews.Display>():
158 let template = self.getTemplate()
159 return MetadataViews.Display(
160 name: template.name,
161 description: template.description,
162 thumbnail: template.thumbnail.file,
163 )
164
165 case Type<MetadataViews.ExternalURL>():
166 return MetadataViews.ExternalURL("https://doodles.app")
167
168 case Type<MetadataViews.Traits>():
169 let template=self.getTemplate()
170 let traits : [MetadataViews.Trait]= []
171
172 traits.append(MetadataViews.Trait(
173 name: "Name",
174 value: template.name,
175 displayType: "string",
176 rarity: nil
177 ))
178
179 traits.append(MetadataViews.Trait(
180 name: "Brand",
181 value: template.brand,
182 displayType: "string",
183 rarity: nil
184 ))
185
186 traits.append(MetadataViews.Trait(
187 name: "Set",
188 value: template.getSet().name,
189 displayType: "string",
190 rarity: nil
191 ))
192
193 traits.append(MetadataViews.Trait(
194 name: "Type",
195 value: template.type,
196 displayType: "string",
197 rarity: nil
198 ))
199
200 traits.append(MetadataViews.Trait(
201 name: "Redeem Limit Date",
202 value: template.getSet().redeemLimitTimestamp,
203 displayType: "Date",
204 rarity: nil
205 ))
206 return MetadataViews.Traits(traits)
207
208 case Type<MetadataViews.Royalties>():
209 let royalties = self.getTemplate().royalties
210 let royalty=royalties[0]
211
212 let doodlesMerchantAccountMainnet="0x014e9ddc4aaaf557"
213 //royalties if we sell on something else then DapperWallet cannot go to the address stored in the contract, and Dapper will not allow us to setup forwarders for Flow/USDC
214 if royalty.receiver.address.toString() == doodlesMerchantAccountMainnet {
215
216 //this is an account that have setup a forwarder for DUC/FUT to the merchant account of Doodles.
217 let royaltyAccountWithDapperForwarder = getAccount(0x12be92985b852cb8)
218 let cap = royaltyAccountWithDapperForwarder.capabilities.get<&{FungibleToken.Receiver}>(/public/fungibleTokenSwitchboardPublic)!
219 return MetadataViews.Royalties([MetadataViews.Royalty(receiver:cap, cut: royalty.cut, description:royalty.description)])
220 }
221
222 let doodlesMerchanAccountTestnet="0xd5b1a1553d0ed52e"
223 if royalty.receiver.address.toString() == doodlesMerchanAccountTestnet {
224 //on testnet we just send this to the main vault, it is not important
225 let cap = Redeemables.account.capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)!
226 return MetadataViews.Royalties([MetadataViews.Royalty(receiver:cap, cut: royalty.cut, description:royalty.description)])
227 }
228
229 return royalties
230 }
231
232 return Redeemables.resolveContractView(resourceType: Type<@Redeemables.NFT>(), viewType: view)
233 }
234 }
235
236 access(all) resource interface RedeemablesCollectionPublic {
237 access(all) fun deposit(token: @{NonFungibleToken.NFT})
238 access(all) fun getIDs(): [UInt64]
239 access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}?
240 access(all) fun borrowRedeemable(id: UInt64): &Redeemables.NFT? {
241 post {
242 (result == nil) || (result?.id == id):
243 "Cannot borrow RedeemNFT reference: the ID of the returned reference is incorrect"
244 }
245 }
246 access(NonFungibleToken.Withdraw) fun redeem(id: UInt64)
247 access(all) fun burnUnredeemedSet(set: Redeemables.Set)
248 }
249
250 access(all) resource Collection: RedeemablesCollectionPublic, NonFungibleToken.Collection {
251 access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}}
252
253 init () {
254 self.ownedNFTs <- {}
255 }
256
257 access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} {
258 let token <- (self.ownedNFTs.remove(key: withdrawID) ?? panic("missing NFT")) as! @NFT
259
260 assert(!token.getSet().isRedeemLimitExceeded(), message: "Set redeem limit timestamp reached")
261
262 return <-token
263 }
264
265 access(all) fun deposit(token: @{NonFungibleToken.NFT}) {
266 let token <- token as! @NFT
267
268 assert(!token.getSet().isRedeemLimitExceeded(), message: "Set redeem limit timestamp reached")
269
270 let set = token.getSet()
271 set.addOwnerRecord(self.owner!.address)
272 Redeemables.sets[set.id] = set
273
274 let id: UInt64 = token.id
275
276 let oldToken <- self.ownedNFTs[id] <- token
277
278 destroy oldToken
279 }
280
281 access(all) view fun getIDs(): [UInt64] {
282 return self.ownedNFTs.keys
283 }
284
285 access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? {
286 return &self.ownedNFTs[id]
287 }
288
289 access(all) fun borrowRedeemable(id: UInt64) : &Redeemables.NFT? {
290 if self.ownedNFTs[id] != nil {
291 return &self.ownedNFTs[id] as &{NonFungibleToken.NFT}? as! &Redeemables.NFT?
292 }
293 return nil
294 }
295
296 access(all) view fun getLength(): Int {
297 return self.ownedNFTs.keys.length
298 }
299
300 access(all) view fun getSupportedNFTTypes(): {Type: Bool} {
301 let supportedTypes: {Type: Bool} = {}
302 supportedTypes[Type<@Redeemables.NFT>()] = true
303 return supportedTypes
304 }
305
306 access(all) view fun isSupportedNFTType(type: Type): Bool {
307 if type == Type<@Redeemables.NFT>() {
308 return true
309 } else {
310 return false
311 }
312 }
313
314 access(all) view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver}? {
315 if let nft = &self.ownedNFTs[id] as &{NonFungibleToken.NFT}? as! &Redeemables.NFT? {
316 return nft as &{ViewResolver.Resolver}
317 }
318 return nil
319 }
320
321 access(NonFungibleToken.Withdraw) fun redeem(id: UInt64) {
322 let nft <- (self.ownedNFTs.remove(key: id) ?? panic("missing NFT")) as! @NFT
323 let template = nft.getTemplate()
324 let set = template.getSet()
325
326 assert(set.canRedeem, message: "Set not available to redeem: ".concat(set.name))
327 assert(!set.isRedeemLimitExceeded(), message: "Set redeem limit timestamp reached: ".concat(set.name))
328
329 emit Redeemed(id: id, address: self.owner!.address, setId: set.id, templateId: template.id, name: template.name)
330 emit Burned(id: id, address: self.owner!.address, setId: set.id, templateId: template.id, name: template.name)
331
332 destroy nft
333 }
334
335 access(all) fun burnUnredeemedSet(set: Redeemables.Set) {
336 assert(set.isRedeemLimitExceeded(), message: "Set redeem limit timestamp not reached: ".concat(set.name))
337
338 let ids = self.ownedNFTs.keys
339 for id in ids {
340 let nft = self.borrowRedeemable(id: id)!
341 let template = nft.getTemplate()
342 if template.getSet().id == set.id {
343 emit Burned(id: id, address: self.owner!.address, setId: set.id, templateId: template.id, name: template.name)
344 destroy <- self.ownedNFTs.remove(key: id)!
345 }
346 }
347 }
348
349 access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
350 return <-Redeemables.createEmptyCollection(nftType: Type<@Redeemables.NFT>())
351 }
352 }
353
354 access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} {
355 return <- create Collection()
356 }
357
358 access(all) resource Admin {}
359
360 access(account) fun createSet(name: String, canRedeem: Bool, redeemLimitTimestamp: UFix64, active: Bool) {
361 let set = Set(name: name, canRedeem: canRedeem, redeemLimitTimestamp: redeemLimitTimestamp, active: active)
362 emit SetCreated(id: set.id, name: set.name)
363 Redeemables.sets[set.id] = set
364 }
365
366 access(account) fun updateSetActive(setId: UInt64, active: Bool) {
367 pre {
368 Redeemables.sets.containsKey(setId) : "Set does not exist. Id: ".concat(setId.toString())
369 }
370 let set = Redeemables.sets[setId]!
371 set.setActive(active)
372 Redeemables.sets[setId] = set
373 }
374
375 access(account) fun updateSetCanRedeem(setId: UInt64, canRedeem: Bool) {
376 pre {
377 Redeemables.sets.containsKey(setId) : "Set does not exist. Id: ".concat(setId.toString())
378 }
379 let set = Redeemables.sets[setId]!
380 set.setCanRedeem(canRedeem)
381 Redeemables.sets[setId] = set
382 }
383
384 access(account) fun updateSetRedeemLimitTimestamp(setId: UInt64, redeemLimitTimestamp: UFix64) {
385 pre {
386 Redeemables.sets.containsKey(setId) : "Set does not exist. Id: ".concat(setId.toString())
387 }
388 let set = Redeemables.sets[setId]!
389 set.setRedeemLimitTimestamp(redeemLimitTimestamp)
390 Redeemables.sets[setId] = set
391 }
392
393 access(account) fun createTemplate(setId: UInt64, name: String, description: String, brand: String, royalties: [MetadataViews.Royalty], type: String, thumbnail: MetadataViews.Media, image: MetadataViews.Media, active: Bool) {
394 pre {
395 Redeemables.sets.containsKey(setId) : "Set does not exist. Id: ".concat(setId.toString())
396 }
397 let template = Template(setId: setId, name: name, description: description, brand: brand, royalties: royalties, type: type, thumbnail: thumbnail, image: image, active: active)
398 emit TemplateCreated(id: template.id, setId: setId, name: name)
399 Redeemables.templates[template.id] = template
400 }
401
402 access(account) fun updateTemplateActive(templateId: UInt64, active: Bool) {
403 pre {
404 Redeemables.templates.containsKey(templateId) : "Template does not exist. Id: ".concat(templateId.toString())
405 }
406 let template = Redeemables.templates[templateId]!
407 template.setActive(active)
408 Redeemables.templates[templateId] = template
409 }
410
411 access(account) fun mintNFT(recipient: &{NonFungibleToken.Receiver}, templateId: UInt64){
412 pre {
413 recipient.owner != nil : "Recipients NFT collection is not owned"
414 Redeemables.templates.containsKey(templateId) : "Template does not exist. Id: ".concat(templateId.toString())
415 }
416
417 let template = Redeemables.templates[templateId] ?? panic("Template does not exist. Id: ".concat(templateId.toString()))
418 let set = Redeemables.sets[template.setId] ?? panic("Set does not exist. Id: ".concat(template.setId.toString()))
419
420 assert(!set.isRedeemLimitExceeded(), message: "Set redeem limit timestamp reached: ".concat(set.name))
421 assert(set.active, message: "Set not active: ".concat(set.name))
422 assert(template.active, message: "Template not active: ".concat(template.name))
423
424 var newNFT <- create NFT(templateId: templateId)
425
426 emit Minted(id: newNFT.id, address:recipient.owner!.address, setId: set.id, templateId: template.id, name: template.name)
427
428 recipient.deposit(token: <-newNFT)
429 }
430
431 access(account) fun burnUnredeemedSet(setId: UInt64) {
432 let set = Redeemables.sets[setId] ?? panic("Set does not exist. Id: ".concat(setId.toString()))
433
434 assert(set.isRedeemLimitExceeded(), message: "Set redeem limit timestamp not reached: ".concat(set.name))
435
436 let addresses = set.ownersRecord
437
438 for address in addresses {
439 let collection =
440 getAccount(address).capabilities.get<&{Redeemables.RedeemablesCollectionPublic}>(Redeemables.CollectionPublicPath)!.borrow()
441 if collection != nil {
442 collection!.burnUnredeemedSet(set: set)
443 }
444 }
445 }
446
447 access(all) view fun getContractViews(resourceType: Type?): [Type] {
448 return [
449 Type<MetadataViews.NFTCollectionDisplay>(),
450 Type<MetadataViews.NFTCollectionData>()
451 ]
452 }
453
454 access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
455 switch viewType {
456 case Type<MetadataViews.NFTCollectionDisplay>():
457 return MetadataViews.NFTCollectionDisplay(
458 name: "Redeemables",
459 description: "Doodles 2 lets anyone create a uniquely personalized and endlessly customizable character in a one-of-a-kind style. Wearables and other collectibles can easily be bought, traded, or sold. Doodles 2 will also incorporate collaborative releases with top brands in fashion, music, sports, gaming, and more. Redeemables are a part of the Doodles ecosystem that will allow you to turn in this NFT within a particular period of time to receive a physical collectible.",
460 externalURL: MetadataViews.ExternalURL("https://doodles.app"),
461 squareImage: MetadataViews.Media(file: MetadataViews.IPFSFile(cid: "QmVpAiutpnzp3zR4q2cUedMxsZd8h5HDeyxs9x3HibsnJb", path: nil), mediaType:"image/png"),
462 bannerImage: MetadataViews.Media(file: MetadataViews.IPFSFile(cid: "QmVoTikzygffMaPcacyTjF8mQ71Eg3zsMF4p4fbsAtGQmQ", path: nil), mediaType:"image/png"),
463 socials: {
464 "instagram": MetadataViews.ExternalURL("https://www.instagram.com/thedoodles"),
465 "discord": MetadataViews.ExternalURL("https://discord.gg/doodles"),
466 "twitter": MetadataViews.ExternalURL("https://twitter.com/doodles")
467 }
468 )
469 case Type<MetadataViews.NFTCollectionData>():
470 return MetadataViews.NFTCollectionData(
471 storagePath: Redeemables.CollectionStoragePath,
472 publicPath: Redeemables.CollectionPublicPath,
473 publicCollection: Type<&Redeemables.Collection>(),
474 publicLinkedType: Type<&Redeemables.Collection>(),
475 createEmptyCollectionFunction: (fun(): @{NonFungibleToken.Collection} {
476 return <- Redeemables.createEmptyCollection(nftType: Type<@Redeemables.NFT>())
477 })
478 )
479 }
480 return nil
481 }
482
483 init() {
484 self.totalSupply = 0
485
486 self.sets = {}
487 self.templates = {}
488 self.templateNextSerialNumber = {}
489 self.extra = {}
490
491 self.CollectionStoragePath = /storage/redeemables
492 self.CollectionPublicPath = /public/redeemabless
493
494 self.AdminStoragePath = /storage/redeemablesAdmin
495
496 self.account.storage.save<@{NonFungibleToken.Collection}>(<- Redeemables.createEmptyCollection(nftType: Type<@Redeemables.NFT>()), to: Redeemables.CollectionStoragePath)
497 let cap = self.account.capabilities.storage.issue<&Redeemables.Collection>(Redeemables.CollectionStoragePath)
498 self.account.capabilities.publish(cap, at: Redeemables.CollectionPublicPath)
499
500 self.account.storage.save(<-create Admin(), to: self.AdminStoragePath)
501
502 emit ContractInitialized()
503 }
504}
505