Smart Contract
TouchstoneSnowGlobez
A.c4b1f4387748f389.TouchstoneSnowGlobez
1// CREATED BY: Touchstone (https://touchstone.city/), a platform crafted by your best friends at Emerald City DAO (https://ecdao.org/).
2// STATEMENT: This contract promises to keep the 5% royalty off of primary sales and 2.5% off of secondary sales to Emerald City DAO or risk permanent suspension from participation in the DAO and its tools.
3
4import NonFungibleToken from 0x1d7e57aa55817448
5import MetadataViews from 0x1d7e57aa55817448
6import FungibleToken from 0xf233dcee88fe0abe
7import FlowToken from 0x1654653399040a61
8import MintVerifiers from 0x7a696d6136e1dce2
9import FUSD from 0x3c5959b568896393
10import EmeraldPass from 0x6a07dbeb03167a13
11
12pub contract TouchstoneSnowGlobez: NonFungibleToken {
13
14 // Collection Information
15 access(self) let collectionInfo: {String: AnyStruct}
16
17 // Contract Information
18 pub var nextEditionId: UInt64
19 pub var nextMetadataId: UInt64
20 pub var totalSupply: UInt64
21
22 // Events
23 pub event ContractInitialized()
24 pub event Withdraw(id: UInt64, from: Address?)
25 pub event Deposit(id: UInt64, to: Address?)
26 pub event TouchstonePurchase(id: UInt64, recipient: Address, metadataId: UInt64, name: String, description: String, image: MetadataViews.IPFSFile, price: UFix64)
27 pub event Minted(id: UInt64, recipient: Address, metadataId: UInt64)
28 pub event MintBatch(metadataIds: [UInt64], recipients: [Address])
29
30 // Paths
31 pub let CollectionStoragePath: StoragePath
32 pub let CollectionPublicPath: PublicPath
33 pub let CollectionPrivatePath: PrivatePath
34 pub let AdministratorStoragePath: StoragePath
35
36 // Maps metadataId of NFT to NFTMetadata
37 access(account) let metadatas: {UInt64: NFTMetadata}
38
39 // Maps the metadataId of an NFT to the primary buyer
40 access(account) let primaryBuyers: {Address: {UInt64: [UInt64]}}
41
42 access(account) let nftStorage: @{Address: {UInt64: NFT}}
43
44 pub struct NFTMetadata {
45 pub let metadataId: UInt64
46 pub let name: String
47 pub let description: String
48 // The main image of the NFT
49 pub let image: MetadataViews.IPFSFile
50 // An optional thumbnail that can go along with it
51 // for easier loading
52 pub let thumbnail: MetadataViews.IPFSFile?
53 // If price is nil, defaults to the collection price
54 pub let price: UFix64?
55 pub var extra: {String: AnyStruct}
56 pub let supply: UInt64
57 pub let purchasers: {UInt64: Address}
58
59 access(account) fun purchased(serial: UInt64, buyer: Address) {
60 self.purchasers[serial] = buyer
61 }
62
63 init(_name: String, _description: String, _image: MetadataViews.IPFSFile, _thumbnail: MetadataViews.IPFSFile?, _price: UFix64?, _extra: {String: AnyStruct}, _supply: UInt64) {
64 self.metadataId = TouchstoneSnowGlobez.nextMetadataId
65 self.name = _name
66 self.description = _description
67 self.image = _image
68 self.thumbnail = _thumbnail
69 self.price = _price
70 self.extra = _extra
71 self.supply = _supply
72 self.purchasers = {}
73 }
74 }
75
76 pub resource NFT: NonFungibleToken.INFT, MetadataViews.Resolver {
77 // The 'id' is the same as the 'uuid'
78 pub let id: UInt64
79 // The 'metadataId' is what maps this NFT to its 'NFTMetadata'
80 pub let metadataId: UInt64
81 pub let serial: UInt64
82
83 pub fun getMetadata(): NFTMetadata {
84 return TouchstoneSnowGlobez.getNFTMetadata(self.metadataId)!
85 }
86
87 pub fun getViews(): [Type] {
88 return [
89 Type<MetadataViews.Display>(),
90 Type<MetadataViews.ExternalURL>(),
91 Type<MetadataViews.NFTCollectionData>(),
92 Type<MetadataViews.NFTCollectionDisplay>(),
93 Type<MetadataViews.Royalties>(),
94 Type<MetadataViews.Serial>(),
95 Type<MetadataViews.Traits>(),
96 Type<MetadataViews.NFTView>()
97 ]
98 }
99
100 pub fun resolveView(_ view: Type): AnyStruct? {
101 switch view {
102 case Type<MetadataViews.Display>():
103 let metadata = self.getMetadata()
104 return MetadataViews.Display(
105 name: metadata.name,
106 description: metadata.description,
107 thumbnail: metadata.thumbnail ?? metadata.image
108 )
109 case Type<MetadataViews.NFTCollectionData>():
110 return MetadataViews.NFTCollectionData(
111 storagePath: TouchstoneSnowGlobez.CollectionStoragePath,
112 publicPath: TouchstoneSnowGlobez.CollectionPublicPath,
113 providerPath: TouchstoneSnowGlobez.CollectionPrivatePath,
114 publicCollection: Type<&Collection{NonFungibleToken.CollectionPublic, NonFungibleToken.Receiver, MetadataViews.ResolverCollection}>(),
115 publicLinkedType: Type<&Collection{NonFungibleToken.CollectionPublic, NonFungibleToken.Receiver, MetadataViews.ResolverCollection}>(),
116 providerLinkedType: Type<&Collection{NonFungibleToken.CollectionPublic, NonFungibleToken.Receiver, MetadataViews.ResolverCollection, NonFungibleToken.Provider}>(),
117 createEmptyCollectionFunction: (fun (): @NonFungibleToken.Collection {
118 return <- TouchstoneSnowGlobez.createEmptyCollection()
119 })
120 )
121 case Type<MetadataViews.ExternalURL>():
122 return MetadataViews.ExternalURL("https://touchstone.city/discover/".concat(self.owner!.address.toString()).concat("/TouchstoneSnowGlobez"))
123 case Type<MetadataViews.NFTCollectionDisplay>():
124 let squareMedia = MetadataViews.Media(
125 file: TouchstoneSnowGlobez.getCollectionAttribute(key: "image") as! MetadataViews.IPFSFile,
126 mediaType: "image"
127 )
128
129 // If a banner image exists, use it
130 // Otherwise, default to the main square image
131 var bannerMedia: MetadataViews.Media? = nil
132 if let bannerImage = TouchstoneSnowGlobez.getOptionalCollectionAttribute(key: "bannerImage") as! MetadataViews.IPFSFile? {
133 bannerMedia = MetadataViews.Media(
134 file: bannerImage,
135 mediaType: "image"
136 )
137 }
138 return MetadataViews.NFTCollectionDisplay(
139 name: TouchstoneSnowGlobez.getCollectionAttribute(key: "name") as! String,
140 description: TouchstoneSnowGlobez.getCollectionAttribute(key: "description") as! String,
141 externalURL: MetadataViews.ExternalURL("https://touchstone.city/discover/".concat(self.owner!.address.toString()).concat("/TouchstoneSnowGlobez")),
142 squareImage: squareMedia,
143 bannerImage: bannerMedia ?? squareMedia,
144 socials: TouchstoneSnowGlobez.getCollectionAttribute(key: "socials") as! {String: MetadataViews.ExternalURL}
145 )
146 case Type<MetadataViews.Royalties>():
147 return MetadataViews.Royalties([
148 // This is for Emerald City in favor of producing Touchstone, a free platform for our users. Failure to keep this in the contract may result in permanent suspension from Emerald City.
149 MetadataViews.Royalty(
150 recepient: getAccount(0x5643fd47a29770e7).getCapability<&FlowToken.Vault{FungibleToken.Receiver}>(/public/flowTokenReceiver),
151 cut: 0.025, // 2.5% royalty on secondary sales
152 description: "Emerald City DAO receives a 2.5% royalty from secondary sales because this collection was created using Touchstone (https://touchstone.city/), a tool for creating your own NFT collections, crafted by Emerald City DAO."
153 )
154 ])
155 case Type<MetadataViews.Serial>():
156 return MetadataViews.Serial(
157 self.serial
158 )
159 case Type<MetadataViews.Traits>():
160 return MetadataViews.dictToTraits(dict: self.getMetadata().extra, excludedNames: nil)
161 case Type<MetadataViews.NFTView>():
162 return MetadataViews.NFTView(
163 id: self.id,
164 uuid: self.uuid,
165 display: self.resolveView(Type<MetadataViews.Display>()) as! MetadataViews.Display?,
166 externalURL: self.resolveView(Type<MetadataViews.ExternalURL>()) as! MetadataViews.ExternalURL?,
167 collectionData: self.resolveView(Type<MetadataViews.NFTCollectionData>()) as! MetadataViews.NFTCollectionData?,
168 collectionDisplay: self.resolveView(Type<MetadataViews.NFTCollectionDisplay>()) as! MetadataViews.NFTCollectionDisplay?,
169 royalties: self.resolveView(Type<MetadataViews.Royalties>()) as! MetadataViews.Royalties?,
170 traits: self.resolveView(Type<MetadataViews.Traits>()) as! MetadataViews.Traits?
171 )
172 }
173 return nil
174 }
175
176 init(_metadataId: UInt64, _serial: UInt64, _recipient: Address) {
177 pre {
178 TouchstoneSnowGlobez.metadatas[_metadataId] != nil:
179 "This NFT does not exist yet."
180 _serial < TouchstoneSnowGlobez.getNFTMetadata(_metadataId)!.supply:
181 "This serial does not exist for this metadataId."
182 !TouchstoneSnowGlobez.getNFTMetadata(_metadataId)!.purchasers.containsKey(_serial):
183 "This serial has already been purchased."
184 }
185 self.id = self.uuid
186 self.metadataId = _metadataId
187 self.serial = _serial
188
189 // Update the buyers list so we keep track of who is purchasing
190 if let buyersRef = &TouchstoneSnowGlobez.primaryBuyers[_recipient] as &{UInt64: [UInt64]}? {
191 if let metadataIdMap = &buyersRef[_metadataId] as &[UInt64]? {
192 metadataIdMap.append(_serial)
193 } else {
194 buyersRef[_metadataId] = [_serial]
195 }
196 } else {
197 TouchstoneSnowGlobez.primaryBuyers[_recipient] = {_metadataId: [_serial]}
198 }
199
200 // Update who bought this serial inside NFTMetadata so it cannot be purchased again.
201 let metadataRef = (&TouchstoneSnowGlobez.metadatas[_metadataId] as &NFTMetadata?)!
202 metadataRef.purchased(serial: _serial, buyer: _recipient)
203
204 TouchstoneSnowGlobez.totalSupply = TouchstoneSnowGlobez.totalSupply + 1
205 emit Minted(id: self.id, recipient: _recipient, metadataId: _metadataId)
206 }
207 }
208
209 pub resource Collection: NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection {
210 // dictionary of NFT conforming tokens
211 // NFT is a resource type with an 'UInt64' ID field
212 pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT}
213
214 // withdraw removes an NFT from the collection and moves it to the caller
215 pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT {
216 let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("missing NFT")
217
218 emit Withdraw(id: token.id, from: self.owner?.address)
219
220 return <-token
221 }
222
223 // deposit takes a NFT and adds it to the collections dictionary
224 // and adds the ID to the id array
225 pub fun deposit(token: @NonFungibleToken.NFT) {
226 let token <- token as! @NFT
227
228 let id: UInt64 = token.id
229
230 // add the new token to the dictionary
231 self.ownedNFTs[id] <-! token
232
233 emit Deposit(id: id, to: self.owner?.address)
234 }
235
236 // getIDs returns an array of the IDs that are in the collection
237 pub fun getIDs(): [UInt64] {
238 return self.ownedNFTs.keys
239 }
240
241 // borrowNFT gets a reference to an NFT in the collection
242 // so that the caller can read its metadata and call its methods
243 pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT {
244 return (&self.ownedNFTs[id] as &NonFungibleToken.NFT?)!
245 }
246
247 pub fun borrowViewResolver(id: UInt64): &AnyResource{MetadataViews.Resolver} {
248 let token = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)!
249 let nft = token as! &NFT
250 return nft as &AnyResource{MetadataViews.Resolver}
251 }
252
253 pub fun claim() {
254 if let storage = &TouchstoneSnowGlobez.nftStorage[self.owner!.address] as &{UInt64: NFT}? {
255 for id in storage.keys {
256 self.deposit(token: <- storage.remove(key: id)!)
257 }
258 }
259 }
260
261 init () {
262 self.ownedNFTs <- {}
263 }
264
265 destroy() {
266 destroy self.ownedNFTs
267 }
268 }
269
270 // A function to mint NFTs.
271 // You can only call this function if minting
272 // is currently active.
273 pub fun mintNFT(metadataId: UInt64, recipient: &{NonFungibleToken.Receiver}, payment: @FlowToken.Vault, serial: UInt64): UInt64 {
274 pre {
275 self.canMint(): "Minting is currently closed by the Administrator!"
276 payment.balance == self.getPriceOfNFT(metadataId):
277 "Payment does not match the price. You passed in ".concat(payment.balance.toString()).concat(" but this NFT costs ").concat(self.getPriceOfNFT(metadataId)!.toString())
278 }
279 let price: UFix64 = self.getPriceOfNFT(metadataId)!
280
281 // Confirm recipient passes all verifiers
282 for verifier in self.getMintVerifiers() {
283 let params = {"minter": recipient.owner!.address}
284 if let error = verifier.verify(params) {
285 panic(error)
286 }
287 }
288
289 // Handle Emerald City DAO royalty (5%)
290 let EmeraldCityTreasury = getAccount(0x5643fd47a29770e7).getCapability(/public/flowTokenReceiver)
291 .borrow<&FlowToken.Vault{FungibleToken.Receiver}>()!
292 let emeraldCityCut: UFix64 = 0.05 * price
293
294 // Handle royalty to user that was configured upon creation
295 if let royalty = TouchstoneSnowGlobez.getOptionalCollectionAttribute(key: "royalty") as! MetadataViews.Royalty? {
296 royalty.receiver.borrow()!.deposit(from: <- payment.withdraw(amount: price * royalty.cut))
297 }
298
299 EmeraldCityTreasury.deposit(from: <- payment.withdraw(amount: emeraldCityCut))
300
301 // Give the rest to the collection owner
302 let paymentRecipient = self.account.getCapability(/public/flowTokenReceiver)
303 .borrow<&FlowToken.Vault{FungibleToken.Receiver}>()!
304 paymentRecipient.deposit(from: <- payment)
305
306 // Mint the nft
307 let nft <- create NFT(_metadataId: metadataId, _serial: serial, _recipient: recipient.owner!.address)
308 let nftId: UInt64 = nft.id
309 let metadata = self.getNFTMetadata(metadataId)!
310 self.collectionInfo["profit"] = (self.getCollectionAttribute(key: "profit") as! UFix64) + price
311
312 // Emit event
313 emit TouchstonePurchase(id: nftId, recipient: recipient.owner!.address, metadataId: metadataId, name: metadata.name, description: metadata.description, image: metadata.image, price: price)
314
315 // Deposit nft
316 recipient.deposit(token: <- nft)
317
318 return nftId
319 }
320
321 pub resource Administrator {
322 pub fun createNFTMetadata(name: String, description: String, imagePath: String, thumbnailPath: String?, ipfsCID: String, price: UFix64?, extra: {String: AnyStruct}, supply: UInt64) {
323 TouchstoneSnowGlobez.metadatas[TouchstoneSnowGlobez.nextMetadataId] = NFTMetadata(
324 _name: name,
325 _description: description,
326 _image: MetadataViews.IPFSFile(
327 cid: ipfsCID,
328 path: imagePath
329 ),
330 _thumbnail: thumbnailPath == nil ? nil : MetadataViews.IPFSFile(cid: ipfsCID, path: thumbnailPath),
331 _price: price,
332 _extra: extra,
333 _supply: supply
334 )
335 TouchstoneSnowGlobez.nextMetadataId = TouchstoneSnowGlobez.nextMetadataId + 1
336 }
337
338 // mintNFT mints a new NFT and deposits
339 // it in the recipients collection
340 pub fun mintNFT(metadataId: UInt64, serial: UInt64, recipient: Address) {
341 pre {
342 EmeraldPass.isActive(user: TouchstoneSnowGlobez.account.address): "You must have an active Emerald Pass subscription to airdrop NFTs. You can purchase Emerald Pass at https://pass.ecdao.org/"
343 }
344 let nft <- create NFT(_metadataId: metadataId, _serial: serial, _recipient: recipient)
345 if let recipientCollection = getAccount(recipient).getCapability(TouchstoneSnowGlobez.CollectionPublicPath).borrow<&TouchstoneSnowGlobez.Collection{NonFungibleToken.CollectionPublic}>() {
346 recipientCollection.deposit(token: <- nft)
347 } else {
348 if let storage = &TouchstoneSnowGlobez.nftStorage[recipient] as &{UInt64: NFT}? {
349 storage[nft.id] <-! nft
350 } else {
351 TouchstoneSnowGlobez.nftStorage[recipient] <-! {nft.id: <- nft}
352 }
353 }
354 }
355
356 pub fun mintBatch(metadataIds: [UInt64], serials: [UInt64], recipients: [Address]) {
357 pre {
358 metadataIds.length == recipients.length: "You need to pass in an equal number of metadataIds and recipients."
359 }
360 var i = 0
361 while i < metadataIds.length {
362 self.mintNFT(metadataId: metadataIds[i], serial: serials[i], recipient: recipients[i])
363 i = i + 1
364 }
365
366 emit MintBatch(metadataIds: metadataIds, recipients: recipients)
367 }
368
369 // create a new Administrator resource
370 pub fun createAdmin(): @Administrator {
371 return <- create Administrator()
372 }
373
374 // change piece of collection info
375 pub fun changeField(key: String, value: AnyStruct) {
376 TouchstoneSnowGlobez.collectionInfo[key] = value
377 }
378 }
379
380 // public function that anyone can call to create a new empty collection
381 pub fun createEmptyCollection(): @NonFungibleToken.Collection {
382 return <- create Collection()
383 }
384
385 // Get information about a NFTMetadata
386 pub fun getNFTMetadata(_ metadataId: UInt64): NFTMetadata? {
387 return self.metadatas[metadataId]
388 }
389
390 pub fun getNFTMetadatas(): {UInt64: NFTMetadata} {
391 return self.metadatas
392 }
393
394 pub fun getPrimaryBuyers(): {Address: {UInt64: [UInt64]}} {
395 return self.primaryBuyers
396 }
397
398 pub fun getCollectionInfo(): {String: AnyStruct} {
399 let collectionInfo = self.collectionInfo
400 collectionInfo["metadatas"] = self.metadatas
401 collectionInfo["primaryBuyers"] = self.primaryBuyers
402 collectionInfo["totalSupply"] = self.totalSupply
403 collectionInfo["nextMetadataId"] = self.nextMetadataId
404 collectionInfo["version"] = 1
405 return collectionInfo
406 }
407
408 pub fun getCollectionAttribute(key: String): AnyStruct {
409 return self.collectionInfo[key] ?? panic(key.concat(" is not an attribute in this collection."))
410 }
411
412 pub fun getOptionalCollectionAttribute(key: String): AnyStruct? {
413 return self.collectionInfo[key]
414 }
415
416 pub fun getMintVerifiers(): [{MintVerifiers.IVerifier}] {
417 return self.getCollectionAttribute(key: "mintVerifiers") as! [{MintVerifiers.IVerifier}]
418 }
419
420 pub fun canMint(): Bool {
421 return self.getCollectionAttribute(key: "minting") as! Bool
422 }
423
424 // Returns nil if an NFT with this metadataId doesn't exist
425 pub fun getPriceOfNFT(_ metadataId: UInt64): UFix64? {
426 if let metadata: TouchstoneSnowGlobez.NFTMetadata = self.getNFTMetadata(metadataId) {
427 let defaultPrice: UFix64 = self.getCollectionAttribute(key: "price") as! UFix64
428 if self.getCollectionAttribute(key: "lotteryBuying") as! Bool {
429 return defaultPrice
430 }
431 return metadata.price ?? defaultPrice
432 }
433 // If the metadataId doesn't exist
434 return nil
435 }
436
437 // Returns an mapping of `id` to NFTMetadata
438 // for the NFTs a user can claim
439 pub fun getClaimableNFTs(user: Address): {UInt64: NFTMetadata} {
440 let answer: {UInt64: NFTMetadata} = {}
441 if let storage = &TouchstoneSnowGlobez.nftStorage[user] as &{UInt64: NFT}? {
442 for id in storage.keys {
443 let nftRef = (&storage[id] as &NFT?)!
444 answer[id] = self.getNFTMetadata(nftRef.metadataId)
445 }
446 }
447 return answer
448 }
449
450 init(
451 _name: String,
452 _description: String,
453 _imagePath: String,
454 _bannerImagePath: String?,
455 _minting: Bool,
456 _royalty: MetadataViews.Royalty?,
457 _defaultPrice: UFix64,
458 _paymentType: String,
459 _ipfsCID: String,
460 _lotteryBuying: Bool,
461 _socials: {String: MetadataViews.ExternalURL},
462 _mintVerifiers: [{MintVerifiers.IVerifier}]
463 ) {
464 // Collection Info
465 self.collectionInfo = {}
466 self.collectionInfo["name"] = _name
467 self.collectionInfo["description"] = _description
468 self.collectionInfo["image"] = MetadataViews.IPFSFile(
469 cid: _ipfsCID,
470 path: _imagePath
471 )
472 if let bannerImagePath = _bannerImagePath {
473 self.collectionInfo["bannerImage"] = MetadataViews.IPFSFile(
474 cid: _ipfsCID,
475 path: _bannerImagePath
476 )
477 }
478 self.collectionInfo["ipfsCID"] = _ipfsCID
479 self.collectionInfo["socials"] = _socials
480 self.collectionInfo["minting"] = _minting
481 self.collectionInfo["lotteryBuying"] = _lotteryBuying
482 if let royalty = _royalty {
483 assert(royalty.receiver.check(), message: "The passed in royalty receiver is not valid. The royalty account must set up the intended payment token.")
484 assert(royalty.cut <= 0.95, message: "The royalty cut cannot be bigger than 95% because 5% goes to Emerald City treasury for primary sales.")
485 self.collectionInfo["royalty"] = royalty
486 }
487 self.collectionInfo["price"] = _defaultPrice
488 self.collectionInfo["paymentType"] = _paymentType
489 self.collectionInfo["dateCreated"] = getCurrentBlock().timestamp
490 self.collectionInfo["mintVerifiers"] = _mintVerifiers
491 self.collectionInfo["profit"] = 0.0
492
493 self.nextEditionId = 0
494 self.nextMetadataId = 0
495 self.totalSupply = 0
496 self.metadatas = {}
497 self.primaryBuyers = {}
498 self.nftStorage <- {}
499
500 // Set the named paths
501 // We include the user's address in the paths.
502 // This is to prevent clashing with existing
503 // Collection paths in the ecosystem.
504 self.CollectionStoragePath = /storage/TouchstoneSnowGlobezCollection_0xc4b1f4387748f389
505 self.CollectionPublicPath = /public/TouchstoneSnowGlobezCollection_0xc4b1f4387748f389
506 self.CollectionPrivatePath = /private/TouchstoneSnowGlobezCollection_0xc4b1f4387748f389
507 self.AdministratorStoragePath = /storage/TouchstoneSnowGlobezAdministrator_0xc4b1f4387748f389
508
509 // Create a Collection resource and save it to storage
510 let collection <- create Collection()
511 self.account.save(<- collection, to: self.CollectionStoragePath)
512
513 // create a public capability for the collection
514 self.account.link<&Collection{NonFungibleToken.CollectionPublic, NonFungibleToken.Receiver, MetadataViews.ResolverCollection}>(
515 self.CollectionPublicPath,
516 target: self.CollectionStoragePath
517 )
518
519 // Create a Administrator resource and save it to storage
520 let administrator <- create Administrator()
521 self.account.save(<- administrator, to: self.AdministratorStoragePath)
522
523 emit ContractInitialized()
524 }
525}
526