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