Smart Contract
Mneme
A.67c649092019e032.Mneme
1// MADE BY: Noah_Overflow
2
3// This contract is for Mneme, a proof of support platform
4// on Flow.
5
6// Mneme (Μνήμη) is one of the original three Muses in pre-Homeric Greek mythology.
7// Before the more famous Nine Muses were standardized by Hesiod (daughters of Zeus and Mnemosyne)
8// there were three older Muses:
9
10// Melete – Muse of Practice/Contemplation
11// Mneme – Muse of Memory
12// Aoide – Muse of Song
13
14// Mneme herself was seen as the preserver of knowledge and inspiration, responsible for the ability of poets, orators
15// and artists to recall what came before and give it form. She represents the thread that links art to time and culture
16// Literally, the memory of humanity encoded in creative work.
17
18import NonFungibleToken from 0x1d7e57aa55817448
19import FungibleToken from 0xf233dcee88fe0abe
20import FlowToken from 0x1654653399040a61
21import ViewResolver from 0x1d7e57aa55817448
22import MetadataViews from 0x1d7e57aa55817448
23import Pistis from 0x67c649092019e032
24import RandomConsumer from 0x45caec600164c9e6
25import Xorshift128plus from 0x45caec600164c9e6
26import Burner from 0xf233dcee88fe0abe
27import FlowTransactionScheduler from 0xe467b9dd11fa00df
28// import "CrossVMMetadataViews"
29// import "EVM"
30
31access(all)
32contract Mneme: NonFungibleToken {
33 // -----------------------------------------------------------------------
34 // Mneme contract-level fields.
35 // These contain actual values that are stored in the smart contract.
36 // -----------------------------------------------------------------------
37 // Dictionary to hold general collection information
38 access(self) let collectionInfo: {String: AnyStruct}
39 access(self) var artistOriginals: {Address: [Int64]}
40 access(self) var artistEditions: {Address: [Int64]}
41 access(self) var totalOriginals: UInt64
42 access(self) var totalEditions: UInt64
43 access(self) var totalCertificates: UInt64
44 access(all) let address: Address
45 /// The RandomConsumer.Consumer resource used to request & fulfill randomness
46 access(self) let consumer: @RandomConsumer.Consumer
47 // -----------------------------------------------------------------------
48 // Mneme account paths
49 // -----------------------------------------------------------------------
50 access(all) let CollectionStoragePath: StoragePath
51 access(all) let CollectionPublicPath: PublicPath
52 /// Path where the minter should be stored
53 /// The standard paths for the collection are stored in the collection resource type
54 access(all) let ArtDropStoragePath: StoragePath
55 access(all) let ArtDropPublicPath: PublicPath
56 access(all) let AdministratorStoragePath: StoragePath
57 access(all) let ArtistStoragePath: StoragePath
58 access(all) let HandlerStoragePath: StoragePath
59 // -----------------------------------------------------------------------
60
61 // Mneme Entitlements
62 // -----------------------------------------------------------------------
63 access(all) entitlement Admin
64 access(all) entitlement AddArtist
65 access(all) entitlement Editions
66
67 /// Event to show when an NFT is minted
68 access(all) event Minted(
69 type: String,
70 id: UInt64,
71 uuid: UInt64,
72 name: String,
73 description: String
74 )
75 access(all) event OriginalCreated(artistAddress: Address, originalId: UInt64)
76 access(all) event EditionCreated(artistAddress: Address, editionId: UInt64)
77 access(all) event MultipliersUpdated(artistAddress: Address, editionId: UInt64)
78 access(all) event RNGReceiptPopped(id: UInt64, multiplier: UFix64)
79 // -----------------------------------------------------------------------
80 // Mneme contract-level Composite Type definitions
81 // -----------------------------------------------------------------------
82 // These are just *definitions* for Types that this contract
83 // and other accounts can use. These definitions do not contain
84 // actual stored values, but an instance (or object) of one of these Types
85 // can be created by this contract that contains stored values.
86 // -----------------------------------------------------------------------
87 access(all) resource Original: Pistis.Pool {
88 access(all) let id: UInt64
89 access(all) var name: String
90 access(all) var description: String
91 access(all) var thumbnail: String
92 access(all) let artistAddress: Address
93 access(all) var editions: {UInt64: UInt64}
94 access(all) var totalMinted: Int64
95 access(all) var price: UFix64
96 access(all) var metadata: {String: String}
97 access(all) var extra: {String: AnyStruct}
98 // Pistis fields
99 access(all) var vaultsDict: @{Type: {FungibleToken.Vault}}
100 access(all) var vaultReceiverPath: {Type: PublicPath}
101
102 init (
103 id: UInt64,
104 name: String,
105 description: String,
106 thumbnail: String,
107 artistAddress: Address,
108 price: UFix64,
109 metadata: {String: String},
110 ) {
111 self.id = id
112 self.name = name
113 self.description = description
114 self.thumbnail = thumbnail
115 self.artistAddress = artistAddress
116 self.editions = {}
117 self.totalMinted = 0
118 self.price = price
119 self.vaultsDict <- {}
120 self.vaultReceiverPath = {}
121 self.metadata = metadata
122 self.extra = {}
123 }
124
125 access(all) fun addEdition(editionId: UInt64) {
126 self.editions[editionId] = 0
127 }
128 access(all) fun increaseTotalMinted() {
129 self.totalMinted = self.totalMinted + 1
130 }
131 // helper funct to return metadata without ref
132 access(all) fun getMetadata(): {String: String} {
133 return self.metadata
134 }
135 }
136 // Edition resource represents the Art's metadata
137 // and its rewards rules
138 // Attachment
139
140 access(all) resource Edition {
141 access(all) let originalId: UInt64
142 access(all) let id: UInt64
143 access(all) var name: String
144 access(all) var price: UFix64
145 access(all) var type: String
146 access(all) var story: String
147 access(all) var dimensions: {String: String}
148 access(all) var reprintLimit: Int64
149 access(all) let artistAddress: Address
150 access(all) var totalMinted: Int64
151 access(all) var profitSplit: {Address: UFix64} // the address and the percentage of the profit
152 // ArtDrop, Original Painting, Charity, CopyrightsHolder/Reseller, communityPool. (Reseller Address is dynamic)
153 // the total of the percentages must be 100%
154 // distribute after claim/point of no return.
155
156 access(all) var multipliers: [UFix64]
157 // Map of multiplier to the id of the CertificateNFT
158 access(all) var usedMultipliers: {UFix64: UInt64}
159 access(all) let rngReceipts: @{UInt64: Receipt}
160 access(all) let revealReceipts: @{UInt64: FlowTransactionScheduler.ScheduledTransaction}
161
162 init(
163 originalId: UInt64,
164 id: UInt64,
165 name: String,
166 price: UFix64,
167 type: String,
168 story: String,
169 dimensions: {String: String},
170 reprintLimit: Int64,
171 artistAddress: Address,
172 multipliers: [UFix64],
173 profitSplit: {Address: UFix64}) {
174
175 self.originalId = originalId
176 self.name = name
177 self.price = price
178 self.id = id
179 self.type = type
180 self.story = story
181 self.dimensions = dimensions
182 self.reprintLimit = reprintLimit
183 self.artistAddress = artistAddress
184 self.multipliers = multipliers
185 self.usedMultipliers = {}
186 self.totalMinted = 0
187 self.rngReceipts <- {}
188 self.revealReceipts <- {}
189 self.profitSplit = profitSplit
190
191 // The number of multipliers should be equal to
192 // the reprint limit
193 if multipliers.length != Int(reprintLimit) {
194 panic("The number of multipliers should be equal to the reprint limit")
195 }
196
197 }
198 // helper funct to return dimension without ref
199 access(all) fun getDimensions(): {String: String} {
200 return self.dimensions
201 }
202
203 access(Editions) fun editEdition(
204 name: String?,
205 price: UFix64?,
206 type: String?,
207 story: String?,
208 dimensions: {String: String}?,
209 reprintLimit: Int64?) {
210 pre {
211 self.totalMinted == 0: "This edition has already been minted"
212 }
213 self.name = name ?? self.name
214 self.price = price ?? self.price
215 self.type = type ?? self.type
216 self.story = story ?? self.story
217 self.dimensions = dimensions ?? self.dimensions
218 self.reprintLimit = reprintLimit ?? self.reprintLimit
219 }
220 // update multipliers
221 access(Editions) fun updateMultipliers(multipliers: [UFix64]) {
222 // The number of multipliers should be equal to
223 // the reprint limit
224 if multipliers.length != Int(self.reprintLimit) {
225 panic("The number of multipliers should be equal to the reprint limit")
226 }
227 self.multipliers = multipliers
228 }
229 /// mintNFT mints a new NFT with a new ID
230 /// and returns it to the calling context
231 access(Editions)
232 fun mintCertificateNFT(thumbnail: String) {
233 // Get the original path
234 let originalStorageIdentifier = "\(Mneme.account.address)_ArtDrop_Original_\(self.artistAddress)_\(self.originalId)"
235 let originalStoragePath = StoragePath(identifier: originalStorageIdentifier)!
236 let originalRef = Mneme.account.storage.borrow< &Mneme.Original>(from: originalStoragePath)!
237 if originalRef == nil {
238 panic("Original not found")
239 }
240 // Check if the edition has a reprint limit
241 // If it does, check if the total minted count has reached the reprint limit
242 if self.reprintLimit != 0 {
243 if self.totalMinted >= self.reprintLimit {
244 panic("This edition has reached the reprint limit")
245 }
246 }
247 let metadata: {String: AnyStruct} = {}
248 let currentBlock = getCurrentBlock()
249 metadata["mintedBlock"] = currentBlock.height
250 metadata["mintedTime"] = currentBlock.timestamp
251 // this piece of metadata will be used to show embedding rarity into a trait
252 metadata["foo"] = "bar"
253 // [2x. 3x, 4x, 5x, 6x, 7x, 8x, 9x, 10x]
254
255 // increase the total minted count for this Edition
256 self.totalMinted = self.totalMinted + 1
257 // increase the total certificates count
258 Mneme.totalCertificates = Mneme.totalCertificates + 1
259 // request randomness
260 let request <- Mneme.consumer.requestRandomness()
261 let receipt <- create Receipt(id: Mneme.totalCertificates, request: <-request)
262 // Safe receipt linked to this Edition resource
263 self.rngReceipts[Mneme.totalCertificates] <-! receipt
264
265 // create a new NFT
266 var newNFT <- create CertificateNFT(
267 id: Mneme.totalCertificates,
268 serial: UInt64(self.totalMinted),
269 name: self.name,
270 description: self.story,
271 thumbnail: thumbnail,
272 metadata: metadata,
273 artistAddress: self.artistAddress
274 )
275 // Schedule a transaction to reveal the Receipt
276 let future = getCurrentBlock().timestamp + 5.0
277 let priority = FlowTransactionScheduler.Priority.Medium
278 let executionEffort: UInt64 = 1000
279 let estimate = FlowTransactionScheduler.estimate(
280 data: "",
281 timestamp: future,
282 priority: priority,
283 executionEffort: executionEffort
284 )
285 assert(
286 estimate.timestamp != nil || priority == FlowTransactionScheduler.Priority.Medium,
287 message: estimate.error ?? "estimation failed"
288 )
289 if Mneme.account.storage.borrow<&AnyResource>(from: Mneme.HandlerStoragePath) == nil {
290 let handler <- create Handler()
291 Mneme.account.storage.save(<-handler, to: Mneme.HandlerStoragePath)
292 }
293 // Withdraw FLOW fees from this contract's account vault
294 let vaultRef = Mneme.account.storage
295 .borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(from: /storage/flowTokenVault)
296 ?? panic("missing FlowToken vault on contract account")
297 let fees <- vaultRef.withdraw(amount: estimate.flowFee ?? 0.0) as! @FlowToken.Vault
298 let handlerCap = Mneme.account.capabilities.storage
299 .issue<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}>(Mneme.HandlerStoragePath)
300
301 let revealReceipt <- FlowTransactionScheduler.schedule(
302 handlerCap: handlerCap,
303 data: "",
304 timestamp: future,
305 priority: priority,
306 executionEffort: executionEffort,
307 fees: <-fees
308 )
309 // deposit fees to stake
310
311 self.revealReceipts[Mneme.totalCertificates] <-! revealReceipt
312 // Increase total minted count for the Original
313 originalRef.increaseTotalMinted()
314 // emit the Minted event
315 emit Minted(type: newNFT.getType().identifier,
316 id: newNFT.id,
317 uuid: newNFT.uuid,
318 name: newNFT.name,
319 description: newNFT.description
320 )
321 // send the new NFT to the artist's collection
322 let artistAccount = getAccount(self.artistAddress)
323 let artistCollection = artistAccount.capabilities.borrow<&{NonFungibleToken.Receiver}>(Mneme.CollectionPublicPath)!
324 artistCollection.deposit(token: <- newNFT)
325 }
326 // Pop the rng receipt
327 access(Editions) fun popRNGReceipt(id: UInt64) {
328 pre {
329 self.rngReceipts[id] != nil: "RNG receipt not found"
330 }
331 // get the receipt
332 let receipt <- self.rngReceipts.remove(key: id)!
333 // get a random number within the reprint limit
334 // of this edition
335 let randomNumber = Mneme._randomWithinRange(request: <- receipt.popRequest(), max: Int(self.reprintLimit))
336 // use this random number to get the multiplier from the multipliers array
337 let multiplier = self.multipliers[randomNumber]
338 // assign the multiplier to the id of the receipt/NFT
339 self.usedMultipliers[multiplier] = receipt.id
340 // return the used receipt
341 self.rngReceipts[id] <-! receipt
342
343 emit RNGReceiptPopped(id: id, multiplier: multiplier)
344 }
345 }
346
347
348 /// We choose the name NFT here, but this type can have any name now
349 /// because the interface does not require it to have a specific name any more
350 access(all) resource CertificateNFT: NonFungibleToken.NFT, Pistis.Pool {
351 access(all) let id: UInt64
352 access(all) let serial: UInt64
353 access(all) let artistAddress: Address
354 access(all) var vaultsDict: @{Type: {FungibleToken.Vault}}
355 access(all) var vaultReceiverPath: {Type: PublicPath}
356 access(all) let name: String
357 access(all) let description: String
358 access(all) let thumbnail: String
359 access(all) let royalties: MetadataViews.Royalty
360 access(all) let metadata: {String: AnyStruct}
361
362 init(
363 id: UInt64,
364 serial: UInt64,
365 name: String,
366 description: String,
367 thumbnail: String,
368 metadata: {String: AnyStruct},
369 artistAddress: Address
370 ) {
371 self.id = id
372 self.serial = serial
373 self.artistAddress = artistAddress
374 self.name = name
375 self.description = description
376 self.thumbnail = thumbnail
377 self.royalties = MetadataViews.Royalty(
378 receiver: getAccount(Mneme.account.address).capabilities.get<&FlowToken.Vault>(/public/flowTokenReceiver),
379 cut: 0.5,
380 description: "The deployer gets 5% of every secondary sale."
381 )
382 self.metadata = metadata
383 self.vaultsDict <- {}
384 self.vaultReceiverPath = {}
385 }
386
387 view access(all)
388 fun getTraits(): {String: AnyStruct} {
389 let metadata: {String: AnyStruct} = {"name": self.name}
390 metadata["editionID"] = self.id
391 metadata["editionName"] = self.name
392 metadata["editionDescription"] = self.description
393 metadata["editionThumbnail"] = self.thumbnail
394 metadata["editionMetadata"] = self.metadata
395 metadata["editionArtistAddress"] = self.artistAddress
396 return metadata
397 }
398 /// createEmptyCollection creates an empty Collection
399 /// and returns it to the caller so that they can own NFTs
400 /// @{NonFungibleToken.Collection}
401 access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
402 return <-Mneme.createEmptyCollection(nftType: Type<@Mneme.CertificateNFT>())
403
404
405 }
406
407 access(all) view fun getViews(): [Type] {
408 return [
409 Type<MetadataViews.Display>(),
410 Type<MetadataViews.Royalties>(),
411 Type<MetadataViews.Editions>(),
412 Type<MetadataViews.ExternalURL>(),
413 Type<MetadataViews.NFTCollectionData>(),
414 Type<MetadataViews.NFTCollectionDisplay>(),
415 Type<MetadataViews.Serial>(),
416 Type<MetadataViews.Traits>(),
417 Type<MetadataViews.EVMBridgedMetadata>()
418 ]
419 }
420
421 access(all) fun resolveView(_ view: Type): AnyStruct? {
422 switch view {
423 case Type<MetadataViews.Display>():
424 return MetadataViews.Display(
425 name: self.name,
426 description: self.description,
427 thumbnail: MetadataViews.HTTPFile(
428 url: self.thumbnail
429 )
430 )
431 case Type<MetadataViews.Editions>():
432 // There is no max number of NFTs that can be minted from this contract
433 // so the max edition field value is set to nil
434 let editionInfo = MetadataViews.Edition(name: "Example NFT Edition", number: self.id, max: nil)
435 let editionList: [MetadataViews.Edition] = [editionInfo]
436 return MetadataViews.Editions(
437 editionList
438 )
439 case Type<MetadataViews.Serial>():
440 return MetadataViews.Serial(
441 self.serial
442 )
443 case Type<MetadataViews.Royalties>():
444 return MetadataViews.Royalties(
445 [self.royalties]
446 )
447 case Type<MetadataViews.ExternalURL>():
448 return MetadataViews.ExternalURL("https://example-nft.onflow.org/".concat(self.id.toString()))
449 case Type<MetadataViews.NFTCollectionData>():
450 return Mneme.resolveContractView(resourceType: Type<@Mneme.CertificateNFT>(), viewType: Type<MetadataViews.NFTCollectionData>())
451 case Type<MetadataViews.NFTCollectionDisplay>():
452 return Mneme.resolveContractView(resourceType: Type<@Mneme.CertificateNFT>(), viewType: Type<MetadataViews.NFTCollectionDisplay>())
453 case Type<MetadataViews.Traits>():
454 // exclude mintedTime and foo to show other uses of Traits
455 let excludedTraits = self.getTraits()
456 return MetadataViews.dictToTraits(dict: self.getTraits(), excludedNames: [])
457 case Type<MetadataViews.EVMBridgedMetadata>():
458 // Implementing this view gives the project control over how the bridged NFT is represented as an
459 // ERC721 when bridged to EVM on Flow via the public infrastructure bridge.
460 // NOTE: If your NFT is a cross-VM NFT, meaning you control both your Cadence & EVM contracts and
461 // registered your custom association with the VM bridge, it's recommended you use the
462 // CrossVMMetadata.EVMBytesMetadata view to define and pass metadata as EVMBytes into your
463 // EVM contract at the time of bridging into EVM. For more information about cross-VM NFTs,
464 // see FLIP-318: https://github.com/onflow/flips/issues/318
465
466 // Get the contract-level name and symbol values
467 let contractLevel = Mneme.resolveContractView(
468 resourceType: nil,
469 viewType: Type<MetadataViews.EVMBridgedMetadata>()
470 ) as! MetadataViews.EVMBridgedMetadata?
471
472 if let contractMetadata = contractLevel {
473 // Compose the token-level URI based on a base URI and the token ID, pointing to a JSON file. This
474 // would be a file you've uploaded and are hosting somewhere - in this case HTTP, but this could be
475 // IPFS, S3, a data URL containing the JSON directly, etc.
476 let baseURI = "https://example-nft.onflow.org/token-metadata/"
477 let uriValue = self.id.toString().concat(".json")
478
479 return MetadataViews.EVMBridgedMetadata(
480 name: contractMetadata.name,
481 symbol: contractMetadata.symbol,
482 uri: MetadataViews.URI(
483 baseURI: baseURI, // defining baseURI results in a concatenation of baseURI and value
484 value: self.id.toString().concat(".json")
485 )
486 )
487 } else {
488 return nil
489 }
490/* case Type<CrossVMMetadataViews.EVMPointer>():
491 // This view is intended for NFT projects with corresponding NFT implementations in both Cadence and
492 // EVM. Resolving EVMPointer indicates the associated EVM implementation. Fully validating the
493 // cross-VM association would involve inspecting the associated EVM contract and ensuring that
494 // contract also points to the resolved Cadence type and contract address. For more information
495 // about cross-VM NFTs, see FLIP-318: https://github.com/onflow/flips/issues/318
496
497 return Mneme.resolveContractView(resourceType: self.getType(), viewType: view)
498 case Type<CrossVMMetadataViews.EVMBytesMetadata>():
499 // This view is intended for Cadence-native NFTs with corresponding ERC721 implementations. By
500 // resolving, you're able to pass arbitrary metadata into your EVM contract whenever an NFT is
501 // bridged which can be useful for Cadence NFTs with dynamic metadata values.
502 // See FLIP-318 for more information about cross-VM NFTs: https://github.com/onflow/flips/issues/318
503
504 // Here we encoded the EVMBridgedMetadata URI and encode the string as EVM bytes, but you could pass any
505 // Cadence values that can be abi encoded and decode them in your EVM contract as you wish. Within
506 // your EVM contract, you can abi decode the bytes and update metadata in your ERC721 contract as
507 // you see fit.
508 let bridgedMetadata = (self.resolveView(Type<MetadataViews.EVMBridgedMetadata>()) as! MetadataViews.EVMBridgedMetadata?)!
509 let uri = bridgedMetadata.uri.uri()
510 let encodedURI = EVM.encodeABI([uri])
511 let evmBytes = EVM.EVMBytes(value: encodedURI)
512 return CrossVMMetadataViews.EVMBytesMetadata(bytes: evmBytes) */
513 }
514 return nil
515 }
516 }
517
518 access(all) resource Collection: NonFungibleToken.Collection, Pistis.Loyalty {
519 /// dictionary of NFT conforming tokens
520 /// NFT is a resource type with an `UInt64` ID field
521 access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}}
522 access(all) var loyaltyPoints: {Address: UFix64}
523 access(all) var supportedArtists: {Address: [Int64]}
524
525 init () {
526 self.ownedNFTs <- {}
527 self.loyaltyPoints = {}
528 self.loyaltyPoints[Mneme.account.address] = 0.0
529 self.supportedArtists = {}
530 }
531
532 // Function to calculate loyalty points
533 access(all) view fun calculateLoyaltyPoints(artistAddress: Address): Int {
534 // get the array of editions
535 let editions = self.supportedArtists[artistAddress]!
536 // loyalty points is the of editions multiplied by 10
537 let loyaltyPoints: Int = editions.length * 10
538
539
540 return loyaltyPoints
541 }
542
543 access(all) fun addVault(vaultType: Type, vault: @{FungibleToken.Vault}, id: UInt64, vaultReceiverPath: PublicPath) {
544 let nft <- self.ownedNFTs.remove(key: id) as! @Mneme.CertificateNFT
545 nft.addVault(vaultType: vaultType, vault: <- vault, vaultReceiverPath: vaultReceiverPath)
546 self.ownedNFTs[id] <-! nft
547 }
548
549 access(all) fun depositToVault(id: UInt64, vaultType: Type, vaultDeposit: @{FungibleToken.Vault}) {
550 let nft <- self.ownedNFTs.remove(key: id) as! @Mneme.CertificateNFT
551 nft.depositToVault(vaultType: vaultType, vaultDeposit: <- vaultDeposit)
552 self.ownedNFTs[id] <-! nft
553 }
554
555 // Withdraw from a vault
556 access(all) fun withdrawFromVault(id: UInt64, vaultType: Type) {
557 let nft <- self.ownedNFTs.remove(key: id) as! @Mneme.CertificateNFT
558 let newVault <- nft.withdrawFromVault(id: id, vaultType: vaultType)
559 let account = getAccount(self.owner!.address)
560 let vault <- newVault.remove(key: newVault.keys[0])!
561 let receiverRef = account.capabilities.borrow<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)!
562 receiverRef.deposit(from: <- vault.withdraw(amount: vault.balance))
563 destroy newVault
564 destroy vault
565 self.ownedNFTs[id] <-! nft
566 }
567
568 /// getSupportedNFTTypes returns a list of NFT types that this receiver accepts
569 access(all) view fun getSupportedNFTTypes(): {Type: Bool} {
570 let supportedTypes: {Type: Bool} = {}
571 supportedTypes[Type<@Mneme.CertificateNFT>()] = true
572 return supportedTypes
573 }
574
575 /// Returns whether or not the given type is accepted by the collection
576 /// A collection that can accept any type should just return true by default
577 access(all) view fun isSupportedNFTType(type: Type): Bool {
578 return type == Type<@Mneme.CertificateNFT>()
579 }
580
581 /// withdraw removes an NFT from the collection and moves it to the caller
582 access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} {
583 let token <- self.ownedNFTs.remove(key: withdrawID)
584 ?? panic("Mneme.Collection.withdraw: Could not withdraw an NFT with ID "
585 .concat(withdrawID.toString())
586 .concat(". Check the submitted ID to make sure it is one that this collection owns."))
587
588 // Remove loyalty points from the collector
589 self.substractLoyalty(address: self.owner?.address!, loyaltyPoints: 10.0)
590
591
592 return <-token
593 }
594
595 /// deposit takes a NFT and adds it to the collections dictionary
596 /// and adds the ID to the id array
597 access(all) fun deposit(token: @{NonFungibleToken.NFT}) {
598 let token <- token as! @Mneme.CertificateNFT
599 let id = token.id
600 let artistAddress = token.artistAddress
601
602 // add the new token to the dictionary which removes the old one
603 let oldToken <- self.ownedNFTs[token.id] <- token
604 if self.supportedArtists[artistAddress] == nil {
605 self.supportedArtists[artistAddress] = []
606 }
607 self.supportedArtists[artistAddress]!.append(Int64(id))
608
609 let loyaltyPoints = self.calculateLoyaltyPoints(artistAddress: artistAddress)
610 // Based on NFT's edition and other factors, add loyalty points to the collector
611 self.addLoyalty(address: self.owner?.address!, loyaltyPoints: UFix64(loyaltyPoints))
612 destroy oldToken
613
614 // This code is for testing purposes only
615 // Do not add to your contract unless you have a specific
616 // reason to want to emit the NFTUpdated event somewhere
617 // in your contract
618 let authTokenRef = (&self.ownedNFTs[id] as auth(NonFungibleToken.Update) &{NonFungibleToken.NFT}?)!
619 //authTokenRef.updateTransferDate(date: getCurrentBlock().timestamp)
620 Mneme.emitNFTUpdated(authTokenRef)
621 }
622
623 /// getIDs returns an array of the IDs that are in the collection
624 access(all) view fun getIDs(): [UInt64] {
625 return self.ownedNFTs.keys
626 }
627
628 /// Gets the amount of NFTs stored in the collection
629 access(all) view fun getLength(): Int {
630 return self.ownedNFTs.length
631 }
632
633 access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? {
634 return &self.ownedNFTs[id]
635 }
636
637 /// Borrow the view resolver for the specified NFT ID
638 access(all) view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver}? {
639 if let nft = &self.ownedNFTs[id] as &{NonFungibleToken.NFT}? {
640 return nft as &{ViewResolver.Resolver}
641 }
642 return nil
643 }
644
645 /// createEmptyCollection creates an empty Collection of the same type
646 /// and returns it to the caller
647 /// @return A an empty collection of the same type
648 access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
649 return <-Mneme.createEmptyCollection(nftType: Type<@Mneme.CertificateNFT>())
650 }
651 }
652 // -----------------------------------------------------------------------
653 // ArtDrop Scheduled Transaction Handler
654 // -----------------------------------------------------------------------
655 // Struct for the scheduled transaction data
656 access(all) struct revealData {
657 access(all) let id: UInt64
658 access(all) let artistAddress: Address
659 access(all) let editionId: UInt64
660
661 init(id: UInt64, artistAddress: Address, editionId: UInt64) {
662 self.id = id
663 self.artistAddress = artistAddress
664 self.editionId = editionId
665 }
666 }
667 /// Handler resource that implements the Scheduled Transaction interface
668 access(all) resource Handler: FlowTransactionScheduler.TransactionHandler {
669
670
671 access(FlowTransactionScheduler.Execute) fun executeTransaction(id: UInt64, data: AnyStruct?) {
672 // Extract reveal data from transaction data
673 let revealData = data as! revealData? ?? panic("revealData data is required")
674 // Get a ref to the Edition resource
675 // using the artistAddress and editionId
676 let storageIdentifier = "ArtDrop_Edition_\(revealData.artistAddress)_\(revealData.editionId)"
677 let storagePath = StoragePath(identifier: storageIdentifier)!
678 let editionRef = Mneme.account.storage.borrow<auth(Mneme.Editions) &Mneme.Edition>(from: storagePath)!
679 if editionRef == nil {
680 panic("Edition not found")
681 }
682 // get the rng receipts for the edition
683 // with this the revealData.id
684 let rngReceipt = editionRef.popRNGReceipt(id: revealData.id)
685
686
687
688 }
689 }
690 // -----------------------------------------------------------------------
691 // ArtDrop Receipt Resource
692 // -----------------------------------------------------------------------
693 /// The Receipt resource is used to store the associated randomness request. By listing the
694 /// RandomConsumer.RequestWrapper conformance, this resource inherits all the default implementations of the
695 /// interface. This is why the Receipt resource has access to the getRequestBlock() and popRequest() functions
696 /// without explicitly defining them.
697 ///
698 access(all) resource Receipt : RandomConsumer.RequestWrapper {
699 /// The associated randomness request which contains the block height at which the request was made
700 // The setID of the intended pack
701 access(all) let id: UInt64
702
703 /// and whether the request has been fulfilled.
704 access(all) var request: @RandomConsumer.Request?
705
706 init(id: UInt64, request: @RandomConsumer.Request) {
707 self.id = id
708 self.request <- request
709 }
710 }
711 /// Returns a random number between 0 and 1 using the RandomConsumer.Consumer resource contained in the contract.
712 /// For the purposes of this contract, a simple modulo operation could have been used though this is not the case
713 /// for all ranges. Using the Consumer.fulfillRandomInRange function ensures that we can get a random number
714 /// within any range without a risk of bias.
715 ///
716 access(self)
717 fun _randomWithinRange(request: @RandomConsumer.Request, max: Int): UInt64 {
718 return self.consumer.fulfillRandomInRange(request: <-request, min: 0, max: UInt64(max))
719 }
720 // -----------------------------------------------------------------------
721 // Mneme public functions
722 // -----------------------------------------------------------------------
723 // Get all the Artists and their Editions
724 access(all) view fun getAllArtists(): {Address: [Int64]} {
725 return self.artistEditions
726 }
727 // Struct for Original metadata
728 access(all) struct OriginalMetadata {
729 access(all) let name: String
730 access(all) let description: String
731 access(all) let thumbnail: String
732 access(all) let metadata: {String: String}
733 init(name: String, description: String, thumbnail: String, metadata: {String: String}) {
734 self.name = name
735 self.description = description
736 self.thumbnail = thumbnail
737 self.metadata = metadata
738 }
739 }
740 // Get all the Originals for an artist
741 access(all) fun getOriginalMetadata(artistAddress: Address, originalId: UInt64): OriginalMetadata? {
742 pre {
743 self.artistOriginals[artistAddress] != nil: "This artist does not exist"
744 }
745 let storageIdentifier = "\(Mneme.address)_ArtDrop_Original_\(artistAddress)_\(originalId)"
746 let publicPath = PublicPath(identifier: storageIdentifier)!
747
748 let originalRef = Mneme.account.capabilities.borrow<&Mneme.Original>(publicPath)!
749 let originalMetadata = originalRef.getMetadata()
750 let metadata = OriginalMetadata(name: originalRef.name, description: originalRef.description, thumbnail: originalRef.thumbnail, metadata: originalMetadata)
751 return metadata
752 }
753 // Struct for Edition metadata
754 access(all) struct EditionMetadata {
755 access(all) let originalId: UInt64
756 access(all) let id: UInt64
757 access(all) var name: String
758 access(all) var price: UFix64
759 access(all) var type: String
760 access(all) var story: String
761 access(all) var dimensions: {String: String}
762 access(all) var reprintLimit: Int64
763 access(all) let artistAddress: Address
764 access(all) var totalMinted: Int64
765 init(originalId: UInt64, id: UInt64, name: String, price: UFix64, type: String, story: String, dimensions: {String: String}, reprintLimit: Int64, artistAddress: Address, totalMinted: Int64) {
766 self.originalId = originalId
767 self.id = id
768 self.name = name
769 self.price = price
770 self.type = type
771 self.story = story
772 self.dimensions = dimensions
773 self.reprintLimit = reprintLimit
774 self.artistAddress = artistAddress
775 self.totalMinted = totalMinted
776 }
777 }
778 // Get an Edition's metadata
779 // parameters: artistAddress: Address, editionId: UInt64
780 access(all) fun getEditionMetadata(artistAddress: Address, editionId: UInt64): EditionMetadata? {
781 pre {
782 self.artistEditions[artistAddress] != nil: "This artist does not exist"
783 }
784 // Check if editionId exists in the array
785 let editionsArray = self.artistEditions[artistAddress]!
786 var editionExists = false
787 for edition in editionsArray {
788 if edition == Int64(editionId) {
789 editionExists = true
790 break
791 }
792 }
793 if !editionExists {
794 return nil
795 }
796 let storageIdentifier = "\(Mneme.account.address)_ArtDrop_Edition_\(artistAddress)_\(editionId)"
797 let storagePath = StoragePath(identifier: storageIdentifier)!
798
799 let editionRef = Mneme.account.storage.borrow<&Mneme.Edition>(from: storagePath)!
800 let dimensions = editionRef.getDimensions()
801 let metadata = EditionMetadata(originalId: editionRef.originalId, id: editionRef.id, name: editionRef.name, price: editionRef.price, type: editionRef.type, story: editionRef.story, dimensions: dimensions, reprintLimit: editionRef.reprintLimit, artistAddress: editionRef.artistAddress, totalMinted: editionRef.totalMinted)
802 return metadata
803 }
804 /// createEmptyCollection creates an empty Collection for the specified NFT type
805 /// and returns it to the caller so that they can own NFTs
806 access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} {
807 return <- create Collection()
808 }
809
810 /// Function that returns all the Metadata Views implemented by a Non Fungible Token
811 ///
812 /// @return An array of Types defining the implemented views. This value will be used by
813 /// developers to know which parameter to pass to the resolveView() method.
814 ///
815 access(all) view fun getContractViews(resourceType: Type?): [Type] {
816 return [
817 Type<MetadataViews.NFTCollectionData>(),
818 Type<MetadataViews.NFTCollectionDisplay>(),
819 Type<MetadataViews.EVMBridgedMetadata>()
820 ]
821 }
822
823 /// Function that resolves a metadata view for this contract.
824 ///
825 /// @param view: The Type of the desired view.
826 /// @return A structure representing the requested view.
827 ///
828 access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
829 switch viewType {
830 case Type<MetadataViews.NFTCollectionData>():
831 let collectionData = MetadataViews.NFTCollectionData(
832 storagePath: self.CollectionStoragePath,
833 publicPath: self.CollectionPublicPath,
834 publicCollection: Type<&Mneme.Collection>(),
835 publicLinkedType: Type<&Mneme.Collection>(),
836 createEmptyCollectionFunction: (fun(): @{NonFungibleToken.Collection} {
837 return <-Mneme.createEmptyCollection(nftType: Type<@Mneme.CertificateNFT>())
838 })
839 )
840 return collectionData
841 case Type<MetadataViews.NFTCollectionDisplay>():
842 let media = MetadataViews.Media(
843 file: MetadataViews.HTTPFile(
844 url: "https://assets.website-files.com/5f6294c0c7a8cdd643b1c820/5f6294c0c7a8cda55cb1c936_Flow_Wordmark.svg"
845 ),
846 mediaType: "image/svg+xml"
847 )
848 return MetadataViews.NFTCollectionDisplay(
849 name: "The Example Collection",
850 description: "This collection is used as an example to help you develop your next Flow NFT.",
851 externalURL: MetadataViews.ExternalURL("https://example-nft.onflow.org"),
852 squareImage: media,
853 bannerImage: media,
854 socials: {
855 "twitter": MetadataViews.ExternalURL("https://twitter.com/flow_blockchain")
856 }
857 )
858 case Type<MetadataViews.EVMBridgedMetadata>():
859 // Implementing this view gives the project control over how the bridged NFT is represented as an ERC721
860 // when bridged to EVM on Flow via the public infrastructure bridge.
861
862 // Compose the contract-level URI. In this case, the contract metadata is located on some HTTP host,
863 // but it could be IPFS, S3, a data URL containing the JSON directly, etc.
864 return MetadataViews.EVMBridgedMetadata(
865 name: "Mneme",
866 symbol: "XMPL",
867 uri: MetadataViews.URI(
868 baseURI: nil, // setting baseURI as nil sets the given value as the uri field value
869 value: "https://example-nft.onflow.org/contract-metadata.json"
870 )
871 )
872/* case Type<CrossVMMetadataViews.EVMPointer>():
873 // This view is intended for NFT projects with corresponding NFT implementations in both Cadence and
874 // EVM. Resolving EVMPointer indicates the associated EVM implementation. Fully validating the
875 // cross-VM association would involve inspecting the associated EVM contract and ensuring that contract
876 // also points to the resolved Cadence type and contract address. For more information about cross-VM
877 // NFTs, see FLIP-318: https://github.com/onflow/flips/issues/318
878
879 // Assigning a dummy EVM address and deserializing. Implementations would want to declare the actual
880 // EVM address corresponding to their corresponding ERC721. If using a proxy in your EVM contracts, this
881 // address should be your proxy's address.
882 let evmContractAddress = EVM.addressFromString(
883 "0x1234565789012345657890123456578901234565"
884 )
885 // Since this NFT is distributed in Cadence, it's declared as Cadence-native
886 let nativeVM = CrossVMMetadataViews.VM.Cadence
887 return CrossVMMetadataViews.EVMPointer(
888 cadenceType: Type<@Mneme.CertificateNFT>(),
889 cadenceContractAddress: self.account.address,
890 evmContractAddress: evmContractAddress,
891 nativeVM: nativeVM
892 ) */
893 }
894 return nil
895 }
896
897 // Administrator resource
898 access(all) resource Administrator {
899 // Function to create a new Original resource
900 access(all) fun createOriginal(
901 name: String,
902 description: String,
903 thumbnail: String,
904 artistAddress: Address,
905 price: UFix64,
906 metadata: {String: String}
907 ) {
908 // increase the total originals count
909 Mneme.totalOriginals = Mneme.totalOriginals + 1
910 // make this path dynamic based on contract's address
911 let storageIdentifier = "\(Mneme.account.address)_ArtDrop_Original_\(artistAddress)_\(Mneme.totalOriginals)"
912 let storagePath = StoragePath(identifier: storageIdentifier)!
913 let publicPath = PublicPath(identifier: storageIdentifier)!
914
915 // create a new original resource
916 let newOriginal <- create Original(id: Mneme.totalOriginals, name: name, description: description, thumbnail: thumbnail, artistAddress: artistAddress, price: price, metadata: metadata)
917 // save the new original to storage
918 Mneme.account.storage.save(<-newOriginal, to: storagePath)
919 // create a public capability for the original
920 let originalCap: Capability<&Mneme.Original> = Mneme.account.capabilities.storage.issue<&Mneme.Original>(storagePath)
921 Mneme.account.capabilities.publish(originalCap, at: publicPath)
922 // add the new original to the artist's originals
923 // add a new key to the artist's originals dictionary
924 if Mneme.artistOriginals[artistAddress] == nil {
925 Mneme.artistOriginals[artistAddress] = []
926 }
927 Mneme.artistOriginals[artistAddress]!.append(Int64(Mneme.totalOriginals))
928 // emit OriginalCreated event
929 emit OriginalCreated(artistAddress: artistAddress, originalId: Mneme.totalOriginals)
930 }
931 // Function to create a new Edition resource
932 access(all) fun createEdition(
933 originalId: UInt64,
934 name: String,
935 price: UFix64,
936 type: String,
937 story: String,
938 dimensions: {String: String},
939 reprintLimit: Int64,
940 artistAddress: Address,
941 multipliers: [UFix64],
942 profitSplit: {Address: UFix64}) {
943 if Mneme.artistEditions[artistAddress] == nil {
944 Mneme.artistEditions[artistAddress] = []
945 }
946 // increase the total editions count
947 Mneme.totalEditions = Mneme.totalEditions + 1
948 // make this path dynamic based on contract's address
949 let storageIdentifier = "\(Mneme.account.address)_ArtDrop_Edition_\(artistAddress)_\(Mneme.totalEditions)"
950 let storagePath = StoragePath(identifier: storageIdentifier)!
951 let publicPath = PublicPath(identifier: storageIdentifier)!
952
953 // create a new edition resource
954 let newEdition <- create Edition(originalId: originalId, id: Mneme.totalEditions, name: name, price: price, type: type, story: story, dimensions: dimensions, reprintLimit: reprintLimit, artistAddress: artistAddress, multipliers: multipliers, profitSplit: profitSplit)
955 // add the new edition to the original's editions
956 // get the original path
957 let originalStorageIdentifier = "\(Mneme.account.address)_ArtDrop_Original_\(artistAddress)_\(originalId)"
958 let originalStoragePath = StoragePath(identifier: originalStorageIdentifier)!
959 let originalRef = Mneme.account.storage.borrow< &Mneme.Original>(from: originalStoragePath)!
960 if originalRef == nil {
961 panic("Original not found")
962 }
963 originalRef.addEdition(editionId: newEdition.id)
964
965 // save the new edition to storage
966 Mneme.account.storage.save(<- newEdition, to: storagePath)
967 // create a public capability for the edition
968 let editionCap: Capability<&Mneme.Edition> = Mneme.account.capabilities.storage.issue<&Mneme.Edition>(storagePath)
969 ////////
970 // DO WE NEED THIS PUBLIC CAP? //
971 ////////
972 // Mneme.account.capabilities.publish(editionCap, at: publicPath)
973 // add the new edition to the artist's editions
974 Mneme.artistEditions[artistAddress]!.append(Int64(Mneme.totalEditions))
975 // return <- newEdition
976
977 // publish an authorized capability to the
978 // stored edition resource to the artist
979 let artistCap: Capability<auth(Mneme.Editions) &Mneme.Edition> = Mneme.account.capabilities.storage.issue<auth(Editions) &Mneme.Edition>(storagePath)
980 Mneme.account.inbox.publish(artistCap, name: storageIdentifier, recipient: artistAddress)
981
982 // Emit an event to the artist
983 emit EditionCreated(artistAddress: artistAddress, editionId: Mneme.totalEditions)
984
985 }
986 // Function to update an edition's multipliers
987 access(all) fun updateMultipliers(
988 artistAddress: Address,
989 editionId: UInt64,
990 multipliers: [UFix64]) {
991 let storageIdentifier = "\(Mneme.account.address)_ArtDrop_Edition_\(artistAddress)_\(Mneme.totalEditions)"
992 let editionRef = Mneme.account.storage.borrow<auth(Editions) &Mneme.Edition>(from: StoragePath(identifier: storageIdentifier)!)!
993 editionRef.updateMultipliers(multipliers: multipliers)
994 emit MultipliersUpdated(artistAddress: artistAddress, editionId: editionId)
995 }
996
997 // Function to mint a new certificate NFT
998 access(all) fun mintCertificateNFT(
999 artistAddress: Address,
1000 editionId: UInt64,
1001 thumbnail: String
1002 ) {
1003 let storageIdentifier = "\(Mneme.account.address)_ArtDrop_Edition_\(artistAddress)_\(editionId)"
1004 let editionRef = Mneme.account.storage.borrow<auth(Editions) &Mneme.Edition>(from: StoragePath(identifier: storageIdentifier)!)!
1005 if editionRef == nil {
1006 panic("Edition not found")
1007 }
1008 editionRef.mintCertificateNFT(thumbnail: thumbnail)
1009 // return <- newCertificateNFT
1010 }
1011
1012 }
1013
1014
1015 init() {
1016 self.collectionInfo = {}
1017 self.artistOriginals = {}
1018 self.artistEditions = {}
1019 self.totalOriginals = 0
1020 self.totalCertificates = 0
1021 self.totalEditions = 0
1022 let identifier = "Mneme_\(self.account.address))"
1023 // Create a RandomConsumer.Consumer resource
1024 self.consumer <-RandomConsumer.createConsumer()
1025 self.address = self.account.address
1026 // Set the named paths
1027 self.ArtDropStoragePath = StoragePath(identifier: identifier)!
1028 self.ArtDropPublicPath = PublicPath(identifier: identifier)!
1029 self.AdministratorStoragePath = StoragePath(identifier: "\(identifier)_Administrator")!
1030 self.CollectionStoragePath = StoragePath(identifier: identifier)!
1031 self.CollectionPublicPath = PublicPath(identifier: identifier)!
1032 self.ArtistStoragePath = StoragePath(identifier: "\(identifier)_Artist")!
1033 self.HandlerStoragePath = StoragePath(identifier: "\(identifier)_Handler")!
1034
1035 // Create a Collection resource and save it to storage
1036 let collection <- create Collection()
1037 self.account.storage.save(<-collection, to: self.CollectionStoragePath)
1038 // create a public capability for the collection
1039 let collectionCap = self.account.capabilities.storage.issue<&Mneme.Collection>(self.CollectionStoragePath)
1040 self.account.capabilities.publish(collectionCap, at: self.CollectionPublicPath)
1041 // Create an Administrator resource and save it to storage
1042 let administrator <- create Administrator()
1043 self.account.storage.save(<-administrator, to: self.AdministratorStoragePath)
1044 // Create a Handler resource and save it to storage
1045 let handler <- create Handler()
1046 self.account.storage.save(<-handler, to: self.HandlerStoragePath)
1047 }
1048}