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