diff --git a/cadence/contracts/ExampleNFT.cdc b/cadence/contracts/ExampleNFT.cdc index 8df0aec..c21d32c 100644 --- a/cadence/contracts/ExampleNFT.cdc +++ b/cadence/contracts/ExampleNFT.cdc @@ -1,118 +1,371 @@ -/// ExampleNFT.cdc -/// -/// This is a complete version of the ExampleNFT contract -/// that includes withdraw and deposit functionalities, as well as a -/// collection resource that can be used to bundle NFTs together. -/// -/// Learn more about non-fungible tokens in this tutorial: https://developers.flow.com/cadence/tutorial/non-fungible-tokens-1 +/* +* +* This is an example implementation of a Flow Non-Fungible Token +* using the V2 standard: https://github.com/onflow/flow-nft/blob/master/contracts/ExampleNFT.cdc +* It is not part of the official standard but it assumed to be +* similar to how many NFTs would implement the core functionality. +* +* This contract does not implement any sophisticated classification +* system for its NFTs. It defines a simple NFT with minimal metadata. +* +*/ -access(all) contract ExampleNFT { +import "NonFungibleToken" +import "ViewResolver" +import "MetadataViews" - // Declare Path constants so paths do not have to be hardcoded - // in transactions and scripts +access(all) contract ExampleNFT: NonFungibleToken { + /// Standard Paths access(all) let CollectionStoragePath: StoragePath access(all) let CollectionPublicPath: PublicPath + + /// Path where the minter should be stored + /// The standard paths for the collection are stored in the collection resource type access(all) let MinterStoragePath: StoragePath - // Tracks the unique IDs of the NFTs - access(all) var idCount: UInt64 + /// We choose the name NFT here, but this type can have any name now + /// because the interface does not require it to have a specific name any more + access(all) resource NFT: NonFungibleToken.NFT { - // Declare the NFT resource type - access(all) resource NFT { - // The unique ID that differentiates each NFT access(all) let id: UInt64 - // Initialize both fields in the initializer - init(initID: UInt64) { - self.id = initID + /// From the Display metadata view + access(all) let name: String + access(all) let description: String + access(all) let thumbnail: String + + /// For the Royalties metadata view + access(self) let royalties: [MetadataViews.Royalty] + + /// Generic dictionary of traits the NFT has + access(self) let metadata: {String: AnyStruct} + + init( + name: String, + description: String, + thumbnail: String, + royalties: [MetadataViews.Royalty], + metadata: {String: AnyStruct}, + ) { + self.id = self.uuid + self.name = name + self.description = description + self.thumbnail = thumbnail + self.royalties = royalties + self.metadata = metadata + } + + /// createEmptyCollection creates an empty Collection + /// and returns it to the caller so that they can own NFTs + /// @{NonFungibleToken.Collection} + access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} { + return <-ExampleNFT.createEmptyCollection(nftType: Type<@ExampleNFT.NFT>()) + } + + access(all) view fun getViews(): [Type] { + return [ + Type(), + Type(), + Type(), + Type(), + Type(), + Type(), + Type(), + Type(), + Type() + ] + } + + access(all) fun resolveView(_ view: Type): AnyStruct? { + switch view { + case Type(): + return MetadataViews.Display( + name: self.name, + description: self.description, + thumbnail: MetadataViews.HTTPFile( + url: self.thumbnail + ) + ) + case Type(): + // There is no max number of NFTs that can be minted from this contract + // so the max edition field value is set to nil + let editionInfo = MetadataViews.Edition(name: "Example NFT Edition", number: self.id, max: nil) + let editionList: [MetadataViews.Edition] = [editionInfo] + return MetadataViews.Editions( + editionList + ) + case Type(): + return MetadataViews.Serial( + self.id + ) + case Type(): + return MetadataViews.Royalties( + self.royalties + ) + case Type(): + return MetadataViews.ExternalURL("https://example-nft.onflow.org/".concat(self.id.toString())) + case Type(): + return ExampleNFT.resolveContractView(resourceType: Type<@ExampleNFT.NFT>(), viewType: Type()) + case Type(): + return ExampleNFT.resolveContractView(resourceType: Type<@ExampleNFT.NFT>(), viewType: Type()) + case Type(): + // exclude mintedTime and foo to show other uses of Traits + let excludedTraits = ["mintedTime", "foo"] + let traitsView = MetadataViews.dictToTraits(dict: self.metadata, excludedNames: excludedTraits) + + // mintedTime is a unix timestamp, we should mark it with a displayType so platforms know how to show it. + let mintedTimeTrait = MetadataViews.Trait(name: "mintedTime", value: self.metadata["mintedTime"]!, displayType: "Date", rarity: nil) + traitsView.addTrait(mintedTimeTrait) + + // foo is a trait with its own rarity + let fooTraitRarity = MetadataViews.Rarity(score: 10.0, max: 100.0, description: "Common") + let fooTrait = MetadataViews.Trait(name: "foo", value: self.metadata["foo"], displayType: nil, rarity: fooTraitRarity) + traitsView.addTrait(fooTrait) + + return traitsView + case Type(): + // Implementing this view gives the project control over how the bridged NFT is represented as an + // ERC721 when bridged to EVM on Flow via the public infrastructure bridge. + + // Get the contract-level name and symbol values + let contractLevel = ExampleNFT.resolveContractView( + resourceType: nil, + viewType: Type() + ) as! MetadataViews.EVMBridgedMetadata? + + if let contractMetadata = contractLevel { + // Compose the token-level URI based on a base URI and the token ID, pointing to a JSON file. This + // would be a file you've uploaded and are hosting somewhere - in this case HTTP, but this could be + // IPFS, S3, a data URL containing the JSON directly, etc. + let baseURI = "https://example-nft.onflow.org/token-metadata/" + let uriValue = self.id.toString().concat(".json") + + return MetadataViews.EVMBridgedMetadata( + name: contractMetadata.name, + symbol: contractMetadata.symbol, + uri: MetadataViews.URI( + baseURI: baseURI, // defining baseURI results in a concatenation of baseURI and value + value: self.id.toString().concat(".json") + ) + ) + } else { + return nil + } + } + return nil } } - access(all) entitlement Withdraw + // Deprecated: Only here for backward compatibility. + access(all) resource interface ExampleNFTCollectionPublic {} - // The definition of the Collection resource that - // holds the NFTs that a user owns - access(all) resource Collection { - // dictionary of NFT conforming tokens - // NFT is a resource type with an `UInt64` ID field - access(all) var ownedNFTs: @{UInt64: NFT} + access(all) resource Collection: NonFungibleToken.Collection, ExampleNFTCollectionPublic { + /// dictionary of NFT conforming tokens + /// NFT is a resource type with an `UInt64` ID field + access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}} - // Initialize the NFTs field to an empty collection init () { self.ownedNFTs <- {} } - // withdraw - // - // Function that removes an NFT from the collection - // and moves it to the calling context - access(Withdraw) fun withdraw(withdrawID: UInt64): @NFT { - // If the NFT isn't found, the transaction panics and reverts + /// getSupportedNFTTypes returns a list of NFT types that this receiver accepts + access(all) view fun getSupportedNFTTypes(): {Type: Bool} { + let supportedTypes: {Type: Bool} = {} + supportedTypes[Type<@ExampleNFT.NFT>()] = true + return supportedTypes + } + + /// Returns whether or not the given type is accepted by the collection + /// A collection that can accept any type should just return true by default + access(all) view fun isSupportedNFTType(type: Type): Bool { + return type == Type<@ExampleNFT.NFT>() + } + + /// withdraw removes an NFT from the collection and moves it to the caller + access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} { let token <- self.ownedNFTs.remove(key: withdrawID) - ?? panic("Could not withdraw an ExampleNFT.NFT with id=" - .concat(withdrawID.toString()) - .concat("Verify that the collection owns the NFT ") - .concat("with the specified ID first before withdrawing it.")) + ?? panic("ExampleNFT.Collection.withdraw: Could not withdraw an NFT with ID " + .concat(withdrawID.toString()) + .concat(". Check the submitted ID to make sure it is one that this collection owns.")) return <-token } - // deposit - // - // Function that takes a NFT as an argument and - // adds it to the collections dictionary - access(all) fun deposit(token: @NFT) { - // add the new token to the dictionary with a force assignment - // if there is already a value at that key, it will fail and revert - self.ownedNFTs[token.id] <-! token - } + /// deposit takes a NFT and adds it to the collections dictionary + /// and adds the ID to the id array + access(all) fun deposit(token: @{NonFungibleToken.NFT}) { + let token <- token as! @ExampleNFT.NFT + let id = token.id - // idExists checks to see if a NFT - // with the given ID exists in the collection - access(all) view fun idExists(id: UInt64): Bool { - return self.ownedNFTs[id] != nil + // add the new token to the dictionary which removes the old one + let oldToken <- self.ownedNFTs[token.id] <- token + + destroy oldToken + + // This code is for testing purposes only + // Do not add to your contract unless you have a specific + // reason to want to emit the NFTUpdated event somewhere + // in your contract + let authTokenRef = (&self.ownedNFTs[id] as auth(NonFungibleToken.Update) &{NonFungibleToken.NFT}?)! + //authTokenRef.updateTransferDate(date: getCurrentBlock().timestamp) + ExampleNFT.emitNFTUpdated(authTokenRef) } - // getIDs returns an array of the IDs that are in the collection + /// getIDs returns an array of the IDs that are in the collection access(all) view fun getIDs(): [UInt64] { return self.ownedNFTs.keys } + + /// Gets the amount of NFTs stored in the collection + access(all) view fun getLength(): Int { + return self.ownedNFTs.length + } + + access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? { + return &self.ownedNFTs[id] + } + + /// Borrow the view resolver for the specified NFT ID + access(all) view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver}? { + if let nft = &self.ownedNFTs[id] as &{NonFungibleToken.NFT}? { + return nft as &{ViewResolver.Resolver} + } + return nil + } + + /// createEmptyCollection creates an empty Collection of the same type + /// and returns it to the caller + /// @return A an empty collection of the same type + access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} { + return <-ExampleNFT.createEmptyCollection(nftType: Type<@ExampleNFT.NFT>()) + } } - // creates a new empty Collection resource and returns it - access(all) fun createEmptyCollection(): @Collection { + /// createEmptyCollection creates an empty Collection for the specified NFT type + /// and returns it to the caller so that they can own NFTs + access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} { return <- create Collection() } - // mintNFT - // - // Function that mints a new NFT with a new ID - // and returns it to the caller - access(all) fun mintNFT(): @NFT { + /// Function that returns all the Metadata Views implemented by a Non Fungible Token + /// + /// @return An array of Types defining the implemented views. This value will be used by + /// developers to know which parameter to pass to the resolveView() method. + /// + access(all) view fun getContractViews(resourceType: Type?): [Type] { + return [ + Type(), + Type(), + Type() + ] + } - // create a new NFT - var newNFT <- create NFT(initID: self.idCount) + /// Function that resolves a metadata view for this contract. + /// + /// @param view: The Type of the desired view. + /// @return A structure representing the requested view. + /// + access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? { + switch viewType { + case Type(): + let collectionData = MetadataViews.NFTCollectionData( + storagePath: self.CollectionStoragePath, + publicPath: self.CollectionPublicPath, + publicCollection: Type<&ExampleNFT.Collection>(), + publicLinkedType: Type<&ExampleNFT.Collection>(), + createEmptyCollectionFunction: (fun(): @{NonFungibleToken.Collection} { + return <-ExampleNFT.createEmptyCollection(nftType: Type<@ExampleNFT.NFT>()) + }) + ) + return collectionData + case Type(): + let media = MetadataViews.Media( + file: MetadataViews.HTTPFile( + url: "https://assets.website-files.com/5f6294c0c7a8cdd643b1c820/5f6294c0c7a8cda55cb1c936_Flow_Wordmark.svg" + ), + mediaType: "image/svg+xml" + ) + return MetadataViews.NFTCollectionDisplay( + name: "The Example Collection", + description: "This collection is used as an example to help you develop your next Flow NFT.", + externalURL: MetadataViews.ExternalURL("https://example-nft.onflow.org"), + squareImage: media, + bannerImage: media, + socials: { + "twitter": MetadataViews.ExternalURL("https://twitter.com/flow_blockchain") + } + ) + case Type(): + // Implementing this view gives the project control over how the bridged NFT is represented as an ERC721 + // when bridged to EVM on Flow via the public infrastructure bridge. - // change the id so that each ID is unique - self.idCount = self.idCount + 1 + // Compose the contract-level URI. In this case, the contract metadata is located on some HTTP host, + // but it could be IPFS, S3, a data URL containing the JSON directly, etc. + return MetadataViews.EVMBridgedMetadata( + name: "ExampleNFT", + symbol: "XMPL", + uri: MetadataViews.URI( + baseURI: nil, // setting baseURI as nil sets the given value as the uri field value + value: "https://example-nft.onflow.org/contract-metadata.json" + ) + ) + } + return nil + } + + /// Resource that an admin or something similar would own to be + /// able to mint new NFTs + /// + access(all) resource NFTMinter { + + /// mintNFT mints a new NFT with a new ID + /// and returns it to the calling context + access(all) fun mintNFT( + name: String, + description: String, + thumbnail: String, + royalties: [MetadataViews.Royalty] + ): @ExampleNFT.NFT { + + let metadata: {String: AnyStruct} = {} + let currentBlock = getCurrentBlock() + metadata["mintedBlock"] = currentBlock.height + metadata["mintedTime"] = currentBlock.timestamp - return <-newNFT + // this piece of metadata will be used to show embedding rarity into a trait + metadata["foo"] = "bar" + + // create a new NFT + var newNFT <- create NFT( + name: name, + description: description, + thumbnail: thumbnail, + royalties: royalties, + metadata: metadata, + ) + + return <-newNFT + } } - init() { - self.CollectionStoragePath = /storage/nftTutorialCollection - self.CollectionPublicPath = /public/nftTutorialCollection - self.MinterStoragePath = /storage/nftTutorialMinter + init() { - // initialize the ID count to one - self.idCount = 1 + // Set the named paths + self.CollectionStoragePath = /storage/exampleNFTCollection + self.CollectionPublicPath = /public/exampleNFTCollection + self.MinterStoragePath = /storage/exampleNFTMinter - // store an empty NFT Collection in account storage - self.account.storage.save(<-self.createEmptyCollection(), to: self.CollectionStoragePath) + // Create a Collection resource and save it to storage + let collection <- create Collection() + self.account.storage.save(<-collection, to: self.CollectionStoragePath) - // publish a capability to the Collection in storage - let cap = self.account.capabilities.storage.issue<&Collection>(self.CollectionStoragePath) - self.account.capabilities.publish(cap, at: self.CollectionPublicPath) - } + // create a public capability for the collection + let collectionCap = self.account.capabilities.storage.issue<&ExampleNFT.Collection>(self.CollectionStoragePath) + self.account.capabilities.publish(collectionCap, at: self.CollectionPublicPath) + + // Create a Minter resource and save it to storage + let minter <- create NFTMinter() + self.account.storage.save(<-minter, to: self.MinterStoragePath) + } } \ No newline at end of file diff --git a/cadence/contracts/Recipe.cdc b/cadence/contracts/Recipe.cdc index bff8346..85d6f56 100644 --- a/cadence/contracts/Recipe.cdc +++ b/cadence/contracts/Recipe.cdc @@ -2,50 +2,32 @@ import "ExampleNFT" import "NonFungibleToken" access(all) contract Recipe { - access(all) let MinterStoragePath: StoragePath - access(all) let CollectionPublicPath: PublicPath + access(all) let ExampleNFTMinterPath: StoragePath - // Initialize the paths and the NFTMinter resource init() { - self.MinterStoragePath = /storage/RecipeNFTMinter - self.CollectionPublicPath = /public/RecipeNFTCollection + // Reference the storage path where ExampleNFT stores its NFTMinter + self.ExampleNFTMinterPath = /storage/exampleNFTMinter + } - // Create and store an NFTMinter resource in storage - let minter <- create NFTMinter() - self.account.storage.save(<-minter, to: self.MinterStoragePath) + access(all) fun mintNFT( + recipient: &{NonFungibleToken.CollectionPublic}, + name: String, + description: String, + thumbnail: String + ) { + // Borrow the ExampleNFT.NFTMinter resource + let minterRef = self.account.storage.borrow<&ExampleNFT.NFTMinter>(from: self.ExampleNFTMinterPath) + ?? panic("Could not borrow reference to the ExampleNFT NFTMinter") - // Create a Collection and publish its capability - let collection <- ExampleNFT.createEmptyCollection() - self.account.storage.save(<-collection, to: /storage/RecipeNFTCollection) - let cap = self.account.capabilities.storage.issue<&{NonFungibleToken.CollectionPublic}>( - /storage/RecipeNFTCollection + // Call the ExampleNFT.NFTMinter's mintNFT function + let newNFT <- minterRef.mintNFT( + name: name, + description: description, + thumbnail: thumbnail, + royalties: [] ) - self.account.capabilities.publish(cap, at: self.CollectionPublicPath) - } - - access(all) resource NFTMinter { - /// Mint and deposit a new NFT - access(all) - fun mintNFT( - recipient: &{NonFungibleToken.CollectionPublic}, - name: String, - description: String, - thumbnail: String, - power: String, - will: String, - determination: String - ) { - let newNFT <- ExampleNFT.mintNFT() - let metadata: {String: String} = { - "name": name, - "description": description, - "thumbnail": thumbnail, - "power": power, - "will": will, - "determination": determination - } - recipient.deposit(token: <-newNFT) - } + // Deposit the minted NFT into the recipient's collection + recipient.deposit(token: <-newNFT) } } diff --git a/cadence/transactions/mint_nft.cdc b/cadence/transactions/mint_nft.cdc index 0e15159..b945169 100644 --- a/cadence/transactions/mint_nft.cdc +++ b/cadence/transactions/mint_nft.cdc @@ -2,43 +2,30 @@ import "NonFungibleToken" import "Recipe" transaction { - - let minter: auth(Storage, Capabilities) &Recipe.NFTMinter + let minter: &Recipe let receiver: &{NonFungibleToken.CollectionPublic} prepare(signer: auth(Storage, Capabilities) &Account) { - // Borrow the NFTMinter resource from the Recipe contract - self.minter = signer.capabilities.storage.borrow<&Recipe.NFTMinter>( - from: Recipe.MinterStoragePath - ) ?? panic("Could not borrow a reference to the NFTMinter") + // Borrow the Recipe contract as a reference + self.minter = signer.capabilities.storage.borrow<&Recipe>( + from: Recipe.ExampleNFTMinterPath + ) ?? panic("Could not borrow a reference to the Recipe contract") - // Borrow the signer's own NFT collection reference - self.receiver = signer.capabilities.borrow<&{NonFungibleToken.CollectionPublic}>( - Recipe.CollectionPublicPath - ) ?? panic("Could not borrow the signer's NFT collection reference") + // Borrow the signer's NFT collection reference + self.receiver = signer.capabilities.storage.borrow<&{NonFungibleToken.CollectionPublic}>( + from: Recipe.CollectionPublicPath + ) ?? panic("Could not borrow a reference to the signer's NFT collection") } execute { - // Mint and deposit the NFT into the signer's own collection + // Call mintNFT from the Recipe contract self.minter.mintNFT( recipient: self.receiver, name: "Hardcoded NFT Name", description: "This is a hardcoded description of the NFT.", - thumbnail: "https://example.com/hardcoded-thumbnail.png", - power: "Unlimited Power", - will: "Unyielding Will", - determination: "Unstoppable Determination" - ) - - log( - "Minted NFT with ID: ".concat(newNFT.id.toString()) - .concat(" and metadata: ") - .concat("{name: Hardcoded NFT Name, description: This is a hardcoded description}") + thumbnail: "https://example.com/hardcoded-thumbnail.png" ) - log( - "Deposited NFT with ID: ".concat(newNFT.id.toString()) - .concat(" into the signer's collection.") - ) + log("Minted and deposited an NFT into the signer's collection.") } } \ No newline at end of file diff --git a/flow.json b/flow.json index 8ac9445..be5845f 100644 --- a/flow.json +++ b/flow.json @@ -105,8 +105,9 @@ "deployments": { "emulator": { "emulator-account": [ - "Recipe", - "ExampleNFT" + "ExampleNFT", + "Recipe" + ] } }