Smart Contract

TouchstoneSnowGlobez

A.c4b1f4387748f389.TouchstoneSnowGlobez

Deployed

1d ago
Feb 26, 2026, 11:04:48 PM UTC

Dependents

0 imports
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