diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..da44e6d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Cache directory used by Cadence dependency manager +imports \ No newline at end of file diff --git a/README.md b/README.md index 812267c..69c7cfa 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,12 @@ and then start the development command by running: ``` After the command is started it will automatically watch any changes you make to Cadence files and make sure to continiously sync those changes on the emulator network. If you make any mistakes it will report the errors as well. Read more [about the command here](https://developers.flow.com/tools/flow-cli/super-commands) +**Using external dependencies** + +If you wanna use external contract dependencies (like NonFungibleToken, FlowToken, FungibleToken,..) you can install them using Cadence dependency manager: https://developers.flow.com/tools/flow-cli/dependency-manager + +Use [ContractBrowser](https://contractbrowser.com/) to explore available 3rd party contracts in the Flow ecosystem. + ## 🏎️ Interacting with ExampleNFT ### Initializing an Account @@ -62,10 +68,16 @@ flow transactions send \ With `user1` account initialized to receive `ExampleNFT`, we can now mint into the `user1` account. The minting transaction should be signed by the account that's storing the `ExampleNFT` contract (admin). We need to pass the recipient account's address to the mint transaction. You can grab it from the initialization step above, or `flow.json`. -``` +```bash flow transactions send \ cadence/transactions/mint.cdc \ - 0xUser1Address \ + 0xe03daebed8ca0615 \ + "Hello World" \ + "Hello from Cadence" \ + "https://example.com/my-nft-thumbnail.png" \ + "[]" \ + "[]" \ + "[]" \ --signer exampleNFT ``` @@ -76,7 +88,8 @@ Fetch the `id`s of `ExampleNFT` NFTs stored in a given account: ``` flow scripts execute \ cadence/scripts/get_nft_ids.cdc \ - 0xUser1Address + 0xUser1Address \ + /public/exampleNFTCollection ``` ### Get NFT Metadata @@ -87,7 +100,7 @@ Fetch metadata for given `ExampleNFT` id in an account: flow scripts execute \ cadence/scripts/get_nft_metadata.cdc \ 0xUser1Address \ - nft_id + your_nft_id ``` ### Tranferring NFT to Another Account @@ -104,8 +117,10 @@ Transfer `ExampleNFT` with given `nft_id` to `user2`: ``` flow transactions send \ cadence/transactions/transfer.cdc \ - 0xUser2Address \ - nft_id \ + your_contract_address \ + ExampleNFT \ + user2_address \ + your_nft_id \ --signer user1 ``` diff --git a/cadence/contracts/exampleNFT/ExampleNFT.cdc b/cadence/contracts/exampleNFT/ExampleNFT.cdc index 2627420..f4b066a 100644 --- a/cadence/contracts/exampleNFT/ExampleNFT.cdc +++ b/cadence/contracts/exampleNFT/ExampleNFT.cdc @@ -1,6 +1,7 @@ /* * * This is an example implementation of a Flow Non-Fungible Token +* using the V2 standard. * It is not part of the official standard but it assumed to be * similar to how many NFTs would implement the core functionality. * @@ -10,53 +11,44 @@ */ import "NonFungibleToken" -import "MetadataViews" import "ViewResolver" +import "MetadataViews" -pub contract ExampleNFT: NonFungibleToken, ViewResolver { - - /// Total supply of ExampleNFTs in existence - pub var totalSupply: UInt64 - - /// The event that is emitted when the contract is created - pub event ContractInitialized() +access(all) contract ExampleNFT: NonFungibleToken { - /// The event that is emitted when an NFT is withdrawn from a Collection - pub event Withdraw(id: UInt64, from: Address?) + /// Standard Paths + access(all) let CollectionStoragePath: StoragePath + access(all) let CollectionPublicPath: PublicPath - /// The event that is emitted when an NFT is deposited to a Collection - pub event Deposit(id: UInt64, to: Address?) + /// 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 - /// Storage and Public Paths - pub let CollectionStoragePath: StoragePath - pub let CollectionPublicPath: PublicPath - pub let MinterStoragePath: StoragePath + /// 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 { - /// The core resource that represents a Non Fungible Token. - /// New instances will be created using the NFTMinter resource - /// and stored in the Collection resource - /// - pub resource NFT: NonFungibleToken.INFT, MetadataViews.Resolver { + access(all) let id: UInt64 - /// The unique ID that each NFT has - pub let id: UInt64 + /// From the Display metadata view + access(all) let name: String + access(all) let description: String + access(all) let thumbnail: String - /// Metadata fields - pub let name: String - pub let description: String - pub 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( - id: UInt64, name: String, description: String, thumbnail: String, royalties: [MetadataViews.Royalty], metadata: {String: AnyStruct}, ) { - self.id = id + self.id = self.uuid self.name = name self.description = description self.thumbnail = thumbnail @@ -64,12 +56,14 @@ pub contract ExampleNFT: NonFungibleToken, ViewResolver { self.metadata = metadata } - /// 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. - /// - pub fun getViews(): [Type] { + /// 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(), @@ -78,16 +72,12 @@ pub contract ExampleNFT: NonFungibleToken, ViewResolver { Type(), Type(), Type(), - Type() + Type(), + Type() ] } - /// Function that resolves a metadata view for this token. - /// - /// @param view: The Type of the desired view. - /// @return A structure representing the requested view. - /// - pub fun resolveView(_ view: Type): AnyStruct? { + access(all) fun resolveView(_ view: Type): AnyStruct? { switch view { case Type(): return MetadataViews.Display( @@ -116,34 +106,9 @@ pub contract ExampleNFT: NonFungibleToken, ViewResolver { case Type(): return MetadataViews.ExternalURL("https://example-nft.onflow.org/".concat(self.id.toString())) case Type(): - return MetadataViews.NFTCollectionData( - storagePath: ExampleNFT.CollectionStoragePath, - publicPath: ExampleNFT.CollectionPublicPath, - providerPath: /private/exampleNFTCollection, - publicCollection: Type<&ExampleNFT.Collection{ExampleNFT.ExampleNFTCollectionPublic}>(), - publicLinkedType: Type<&ExampleNFT.Collection{ExampleNFT.ExampleNFTCollectionPublic,NonFungibleToken.CollectionPublic,NonFungibleToken.Receiver,MetadataViews.ResolverCollection}>(), - providerLinkedType: Type<&ExampleNFT.Collection{ExampleNFT.ExampleNFTCollectionPublic,NonFungibleToken.CollectionPublic,NonFungibleToken.Provider,MetadataViews.ResolverCollection}>(), - createEmptyCollectionFunction: (fun (): @NonFungibleToken.Collection { - return <-ExampleNFT.createEmptyCollection() - }) - ) + return ExampleNFT.resolveContractView(resourceType: Type<@ExampleNFT.NFT>(), viewType: Type()) 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") - } - ) + 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"] @@ -159,162 +124,215 @@ pub contract ExampleNFT: NonFungibleToken, ViewResolver { 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? + ?? panic("Could not resolve contract-level EVMBridgedMetadata") + // 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: contractLevel.name, + symbol: contractLevel.symbol, + uri: MetadataViews.URI( + baseURI: baseURI, // defining baseURI results in a concatenation of baseURI and value + value: self.id.toString().concat(".json") + ) + ) } return nil } } - /// Defines the methods that are particular to this NFT contract collection - /// - pub resource interface ExampleNFTCollectionPublic { - pub fun deposit(token: @NonFungibleToken.NFT) - pub fun getIDs(): [UInt64] - pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT - pub fun borrowExampleNFT(id: UInt64): &ExampleNFT.NFT? { - post { - (result == nil) || (result?.id == id): - "Cannot borrow ExampleNFT reference: the ID of the returned reference is incorrect" - } - } - } + // Deprecated: Only here for backward compatibility. + access(all) resource interface ExampleNFTCollectionPublic {} - /// The resource that will be holding the NFTs inside any account. - /// In order to be able to manage NFTs any account will need to create - /// an empty collection first - /// - pub resource Collection: ExampleNFTCollectionPublic, NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection { - // dictionary of NFT conforming tokens - // NFT is a resource type with an `UInt64` ID field - pub var ownedNFTs: @{UInt64: NonFungibleToken.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}} init () { self.ownedNFTs <- {} } - /// Removes an NFT from the collection and moves it to the caller - /// - /// @param withdrawID: The ID of the NFT that wants to be withdrawn - /// @return The NFT resource that has been taken out of the collection - /// - pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT { - let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("missing NFT") + /// 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>() + } - emit Withdraw(id: token.id, from: self.owner?.address) + /// 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 NFT with the provided ID from the collection") return <-token } - /// Adds an NFT to the collections dictionary and adds the ID to the id array - /// - /// @param token: The NFT resource to be included in the collection - /// - pub fun deposit(token: @NonFungibleToken.NFT) { + /// 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: UInt64 = token.id + let id = token.id // add the new token to the dictionary which removes the old one - let oldToken <- self.ownedNFTs[id] <- token - - emit Deposit(id: id, to: self.owner?.address) + 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) } - /// Helper method for getting the collection IDs - /// - /// @return An array containing the IDs of the NFTs in the collection - /// - pub fun getIDs(): [UInt64] { + /// getIDs returns an array of the IDs that are in the collection + access(all) view fun getIDs(): [UInt64] { return self.ownedNFTs.keys } - /// Gets a reference to an NFT in the collection so that - /// the caller can read its metadata and call its methods - /// - /// @param id: The ID of the wanted NFT - /// @return A reference to the wanted NFT resource - /// - pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT { - return (&self.ownedNFTs[id] as &NonFungibleToken.NFT?)! + /// Gets the amount of NFTs stored in the collection + access(all) view fun getLength(): Int { + return self.ownedNFTs.length } - /// Gets a reference to an NFT in the collection so that - /// the caller can read its metadata and call its methods - /// - /// @param id: The ID of the wanted NFT - /// @return A reference to the wanted NFT resource - /// - pub fun borrowExampleNFT(id: UInt64): &ExampleNFT.NFT? { - if self.ownedNFTs[id] != nil { - // Create an authorized reference to allow downcasting - let ref = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)! - return ref as! &ExampleNFT.NFT - } + access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? { + return (&self.ownedNFTs[id] as &{NonFungibleToken.NFT}?) + } + /// 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 } - /// Gets a reference to the NFT only conforming to the `{MetadataViews.Resolver}` - /// interface so that the caller can retrieve the views that the NFT - /// is implementing and resolve them - /// - /// @param id: The ID of the wanted NFT - /// @return The resource reference conforming to the Resolver interface - /// - pub fun borrowViewResolver(id: UInt64): &AnyResource{MetadataViews.Resolver} { - let nft = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)! - let exampleNFT = nft as! &ExampleNFT.NFT - return exampleNFT as &AnyResource{MetadataViews.Resolver} + /// 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>()) } + } - destroy() { - destroy self.ownedNFTs - } + /// 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() } - /// Allows anyone to create a new empty collection + /// Function that returns all the Metadata Views implemented by a Non Fungible Token /// - /// @return The new Collection resource + /// @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. /// - pub fun createEmptyCollection(): @NonFungibleToken.Collection { - return <- create Collection() + access(all) view fun getContractViews(resourceType: Type?): [Type] { + return [ + Type(), + Type(), + Type() + ] + } + + /// 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. + + // 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 /// - pub resource NFTMinter { - - /// Mints a new NFT with a new ID and deposit it in the - /// recipients collection using their collection reference - /// - /// @param recipient: A capability to the collection where the new NFT will be deposited - /// @param name: The name for the NFT metadata - /// @param description: The description for the NFT metadata - /// @param thumbnail: The thumbnail for the NFT metadata - /// @param royalties: An array of Royalty structs, see MetadataViews docs - /// - pub fun mintNFT( - recipient: &{NonFungibleToken.CollectionPublic}, + 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 - metadata["minter"] = recipient.owner!.address // 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( - id: ExampleNFT.totalSupply, name: name, description: description, thumbnail: thumbnail, @@ -322,58 +340,11 @@ pub contract ExampleNFT: NonFungibleToken, ViewResolver { metadata: metadata, ) - // deposit it in the recipient's account using their reference - recipient.deposit(token: <-newNFT) - - ExampleNFT.totalSupply = ExampleNFT.totalSupply + UInt64(1) - } - } - - /// Function that resolves a metadata view for this contract. - /// - /// @param view: The Type of the desired view. - /// @return A structure representing the requested view. - /// - pub fun resolveView(_ view: Type): AnyStruct? { - switch view { - case Type(): - return MetadataViews.NFTCollectionData( - storagePath: ExampleNFT.CollectionStoragePath, - publicPath: ExampleNFT.CollectionPublicPath, - providerPath: /private/exampleNFTCollection, - publicCollection: Type<&ExampleNFT.Collection{ExampleNFT.ExampleNFTCollectionPublic}>(), - publicLinkedType: Type<&ExampleNFT.Collection{ExampleNFT.ExampleNFTCollectionPublic,NonFungibleToken.CollectionPublic,NonFungibleToken.Receiver,MetadataViews.ResolverCollection}>(), - providerLinkedType: Type<&ExampleNFT.Collection{ExampleNFT.ExampleNFTCollectionPublic,NonFungibleToken.CollectionPublic,NonFungibleToken.Provider,MetadataViews.ResolverCollection}>(), - createEmptyCollectionFunction: (fun (): @NonFungibleToken.Collection { - return <-ExampleNFT.createEmptyCollection() - }) - ) - 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 <-newNFT } - return nil - } - - /// 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. - /// - pub fun getViews(): [Type] { - return [ - Type(), - Type() - ] } init() { - // Initialize the total supply - self.totalSupply = 0 // Set the named paths self.CollectionStoragePath = /storage/exampleNFTCollection @@ -382,18 +353,14 @@ pub contract ExampleNFT: NonFungibleToken, ViewResolver { // Create a Collection resource and save it to storage let collection <- create Collection() - self.account.save(<-collection, to: self.CollectionStoragePath) + self.account.storage.save(<-collection, to: self.CollectionStoragePath) // create a public capability for the collection - self.account.link<&ExampleNFT.Collection{NonFungibleToken.CollectionPublic, ExampleNFT.ExampleNFTCollectionPublic, MetadataViews.ResolverCollection}>( - self.CollectionPublicPath, - target: self.CollectionStoragePath - ) + 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.save(<-minter, to: self.MinterStoragePath) - - emit ContractInitialized() + self.account.storage.save(<-minter, to: self.MinterStoragePath) } -} +} \ No newline at end of file diff --git a/cadence/contracts/standards/ViewResolver.cdc b/cadence/contracts/standards/ViewResolver.cdc deleted file mode 100644 index be59741..0000000 --- a/cadence/contracts/standards/ViewResolver.cdc +++ /dev/null @@ -1,25 +0,0 @@ -// Taken from the NFT Metadata standard, this contract exposes an interface to let -// anyone borrow a contract and resolve views on it. -// -// This will allow you to obtain information about a contract without necessarily knowing anything about it. -// All you need is its address and name and you're good to go! -pub contract interface ViewResolver { - /// Function that returns all the Metadata Views implemented by the resolving contract - /// - /// @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. - /// - pub fun getViews(): [Type] { - return [] - } - - /// Function that resolves a metadata view for this token. - /// - /// @param view: The Type of the desired view. - /// @return A structure representing the requested view. - /// - pub fun resolveView(_ view: Type): AnyStruct? { - return nil - } -} - \ No newline at end of file diff --git a/cadence/scripts/get_nft_ids.cdc b/cadence/scripts/get_nft_ids.cdc index 3526f54..1c9aba3 100644 --- a/cadence/scripts/get_nft_ids.cdc +++ b/cadence/scripts/get_nft_ids.cdc @@ -1,15 +1,14 @@ +/// Script to get NFT IDs in an account's collection + import "NonFungibleToken" import "ExampleNFT" -/// Script to get NFT IDs in an account's collection -/// -pub fun main(address: Address): [UInt64] { +access(all) fun main(address: Address, collectionPublicPath: PublicPath): [UInt64] { let account = getAccount(address) - let collectionRef = account - .getCapability(ExampleNFT.CollectionPublicPath) - .borrow<&{NonFungibleToken.CollectionPublic}>() - ?? panic("Could not borrow capability from public collection at specified path") + let collectionRef = account.capabilities.borrow<&{NonFungibleToken.Collection}>( + collectionPublicPath + ) ?? panic("Could not borrow capability from collection at specified path") return collectionRef.getIDs() } \ No newline at end of file diff --git a/cadence/scripts/get_nft_metadata.cdc b/cadence/scripts/get_nft_metadata.cdc index e2924f7..4ecea5e 100644 --- a/cadence/scripts/get_nft_metadata.cdc +++ b/cadence/scripts/get_nft_metadata.cdc @@ -1,33 +1,35 @@ -import "MetadataViews" -import "ExampleNFT" - /// This script gets all the view-based metadata associated with the specified NFT /// and returns it as a single struct -pub struct NFT { - pub let name: String - pub let description: String - pub let thumbnail: String - pub let owner: Address - pub let type: String - pub let royalties: [MetadataViews.Royalty] - pub let externalURL: String - pub let serialNumber: UInt64 - pub let collectionPublicPath: PublicPath - pub let collectionStoragePath: StoragePath - pub let collectionProviderPath: PrivatePath - pub let collectionPublic: String - pub let collectionPublicLinkedType: String - pub let collectionProviderLinkedType: String - pub let collectionName: String - pub let collectionDescription: String - pub let collectionExternalURL: String - pub let collectionSquareImage: String - pub let collectionBannerImage: String - pub let collectionSocials: {String: String} - pub let edition: MetadataViews.Edition - pub let traits: MetadataViews.Traits - pub let medias: MetadataViews.Medias? - pub let license: MetadataViews.License? + +import "ExampleNFT" +import "MetadataViews" + +access(all) struct NFT { + access(all) let name: String + access(all) let description: String + access(all) let thumbnail: String + access(all) let owner: Address + access(all) let type: String + access(all) let royalties: [MetadataViews.Royalty] + access(all) let externalURL: String + access(all) let serialNumber: UInt64 + access(all) let collectionPublicPath: PublicPath + access(all) let collectionStoragePath: StoragePath + access(all) let collectionPublic: String + access(all) let collectionPublicLinkedType: String + access(all) let collectionName: String + access(all) let collectionDescription: String + access(all) let collectionExternalURL: String + access(all) let collectionSquareImage: String + access(all) let collectionBannerImage: String + access(all) let collectionSocials: {String: String} + access(all) let edition: MetadataViews.Edition + access(all) let traits: MetadataViews.Traits + access(all) let medias: MetadataViews.Medias? + access(all) let license: MetadataViews.License? + access(all) let bridgedName: String + access(all) let symbol: String + access(all) let tokenURI: String init( name: String, @@ -40,10 +42,8 @@ pub struct NFT { serialNumber: UInt64, collectionPublicPath: PublicPath, collectionStoragePath: StoragePath, - collectionProviderPath: PrivatePath, collectionPublic: String, collectionPublicLinkedType: String, - collectionProviderLinkedType: String, collectionName: String, collectionDescription: String, collectionExternalURL: String, @@ -52,8 +52,11 @@ pub struct NFT { collectionSocials: {String: String}, edition: MetadataViews.Edition, traits: MetadataViews.Traits, - medias:MetadataViews.Medias?, - license:MetadataViews.License? + medias:MetadataViews.Medias?, + license:MetadataViews.License?, + bridgedName: String, + symbol: String, + tokenURI: String ) { self.name = name self.description = description @@ -65,10 +68,8 @@ pub struct NFT { self.serialNumber = serialNumber self.collectionPublicPath = collectionPublicPath self.collectionStoragePath = collectionStoragePath - self.collectionProviderPath = collectionProviderPath self.collectionPublic = collectionPublic self.collectionPublicLinkedType = collectionPublicLinkedType - self.collectionProviderLinkedType = collectionProviderLinkedType self.collectionName = collectionName self.collectionDescription = collectionDescription self.collectionExternalURL = collectionExternalURL @@ -77,20 +78,26 @@ pub struct NFT { self.collectionSocials = collectionSocials self.edition = edition self.traits = traits - self.medias=medias - self.license=license + self.medias = medias + self.license = license + self.bridgedName = bridgedName + self.symbol = symbol + self.tokenURI = tokenURI } } -pub fun main(address: Address, id: UInt64): NFT { +access(all) fun main(address: Address, id: UInt64): NFT { let account = getAccount(address) - let collection = account - .getCapability(ExampleNFT.CollectionPublicPath) - .borrow<&{ExampleNFT.ExampleNFTCollectionPublic}>() - ?? panic("Could not borrow a reference to the collection") + let collectionData = ExampleNFT.resolveContractView(resourceType: nil, viewType: Type()) as! MetadataViews.NFTCollectionData? + ?? panic("ViewResolver does not resolve NFTCollectionData view") + + let collection = account.capabilities.borrow<&ExampleNFT.Collection>( + collectionData.publicPath + ) ?? panic("Could not borrow a reference to the collection") - let nft = collection.borrowExampleNFT(id: id)! + let nft = collection.borrowNFT(id) + ?? panic("Could not borrow a reference to an NFT with the given ID") // Get the basic display information for this NFT let display = MetadataViews.getDisplay(nft)! @@ -114,10 +121,12 @@ pub fun main(address: Address, id: UInt64): NFT { collectionSocials[key] = collectionDisplay.socials[key]!.url } - let traits = MetadataViews.getTraits(nft)! + let traits = MetadataViews.getTraits(nft)! - let medias=MetadataViews.getMedias(nft) - let license=MetadataViews.getLicense(nft) + let medias = MetadataViews.getMedias(nft) + let license = MetadataViews.getLicense(nft) + + let bridgedMetadata = MetadataViews.getEVMBridgedMetadata(nft)! return NFT( name: display.name, @@ -130,10 +139,8 @@ pub fun main(address: Address, id: UInt64): NFT { serialNumber: serialNumberView.number, collectionPublicPath: nftCollectionView.publicPath, collectionStoragePath: nftCollectionView.storagePath, - collectionProviderPath: nftCollectionView.providerPath, collectionPublic: nftCollectionView.publicCollection.identifier, collectionPublicLinkedType: nftCollectionView.publicLinkedType.identifier, - collectionProviderLinkedType: nftCollectionView.providerLinkedType.identifier, collectionName: collectionDisplay.name, collectionDescription: collectionDisplay.description, collectionExternalURL: collectionDisplay.externalURL.url, @@ -142,7 +149,10 @@ pub fun main(address: Address, id: UInt64): NFT { collectionSocials: collectionSocials, edition: nftEditionView.infoList[0], traits: traits, - medias:medias, - license:license + medias: medias, + license: license, + bridgedName: bridgedMetadata.name, + symbol: bridgedMetadata.symbol, + tokenURI: bridgedMetadata.uri.uri() ) -} +} \ No newline at end of file diff --git a/cadence/transactions/init.cdc b/cadence/transactions/init.cdc index 28e01c3..86919da 100644 --- a/cadence/transactions/init.cdc +++ b/cadence/transactions/init.cdc @@ -1,28 +1,31 @@ -import "NonFungibleToken" -import "MetadataViews" -import "ExampleNFT" - /// This transaction is what an account would run /// to set itself up to receive NFTs +import "NonFungibleToken" +import "ExampleNFT" +import "MetadataViews" + transaction { - prepare(signer: AuthAccount) { + prepare(signer: auth(BorrowValue, IssueStorageCapabilityController, PublishCapability, SaveValue, UnpublishCapability) &Account) { + + let collectionData = ExampleNFT.resolveContractView(resourceType: nil, viewType: Type()) as! MetadataViews.NFTCollectionData? + ?? panic("ViewResolver does not resolve NFTCollectionData view") + // Return early if the account already has a collection - if signer.borrow<&ExampleNFT.Collection>(from: ExampleNFT.CollectionStoragePath) != nil { + if signer.storage.borrow<&ExampleNFT.Collection>(from: collectionData.storagePath) != nil { return } // Create a new empty collection - let collection <- ExampleNFT.createEmptyCollection() + let collection <- ExampleNFT.createEmptyCollection(nftType: Type<@ExampleNFT.NFT>()) // save it to the account - signer.save(<-collection, to: ExampleNFT.CollectionStoragePath) + signer.storage.save(<-collection, to: collectionData.storagePath) // create a public capability for the collection - signer.link<&{NonFungibleToken.CollectionPublic, ExampleNFT.ExampleNFTCollectionPublic, MetadataViews.ResolverCollection}>( - ExampleNFT.CollectionPublicPath, - target: ExampleNFT.CollectionStoragePath - ) + signer.capabilities.unpublish(collectionData.publicPath) + let collectionCap = signer.capabilities.storage.issue<&ExampleNFT.Collection>(collectionData.storagePath) + signer.capabilities.publish(collectionCap, at: collectionData.publicPath) } } \ No newline at end of file diff --git a/cadence/transactions/mint.cdc b/cadence/transactions/mint.cdc index 25e478d..57de7f8 100644 --- a/cadence/transactions/mint.cdc +++ b/cadence/transactions/mint.cdc @@ -1,48 +1,83 @@ +/// This script uses the NFTMinter resource to mint a new NFT +/// It must be run with the account that has the minter resource +/// stored in /storage/NFTMinter +/// +/// The royalty arguments indicies must be aligned + import "NonFungibleToken" import "ExampleNFT" +import "MetadataViews" +import "FungibleToken" -/// Mints a new ExampleNFT into recipient's account +transaction( + recipient: Address, + name: String, + description: String, + thumbnail: String, + cuts: [UFix64], + royaltyDescriptions: [String], + royaltyBeneficiaries: [Address] +) { -transaction(recipient: Address) { /// local variable for storing the minter reference let minter: &ExampleNFT.NFTMinter /// Reference to the receiver's collection - let recipientCollectionRef: &{NonFungibleToken.CollectionPublic} - - /// Previous NFT ID before the transaction executes - let mintingIDBefore: UInt64 + let recipientCollectionRef: &{NonFungibleToken.Receiver} - prepare(signer: AuthAccount) { - self.mintingIDBefore = ExampleNFT.totalSupply + prepare(signer: auth(BorrowValue) &Account) { - // Borrow a reference to the NFTMinter resource in storage - self.minter = signer.borrow<&ExampleNFT.NFTMinter>(from: ExampleNFT.MinterStoragePath) - ?? panic("signer does not store ExampleNFT.NFTMinter") + let collectionData = ExampleNFT.resolveContractView(resourceType: nil, viewType: Type()) as! MetadataViews.NFTCollectionData? + ?? panic("ViewResolver does not resolve NFTCollectionData view") + + // borrow a reference to the NFTMinter resource in storage + self.minter = signer.storage.borrow<&ExampleNFT.NFTMinter>(from: ExampleNFT.MinterStoragePath) + ?? panic("Account does not store an object at the specified path") // Borrow the recipient's public NFT collection reference - self.recipientCollectionRef = getAccount(recipient) - .getCapability(ExampleNFT.CollectionPublicPath) - .borrow<&{NonFungibleToken.CollectionPublic}>() - ?? panic("Could not get receiver reference to the recipient's NFT Collection") + self.recipientCollectionRef = getAccount(recipient).capabilities.borrow<&{NonFungibleToken.Receiver}>( + collectionData.publicPath + ) ?? panic("Could not get receiver reference to the NFT Collection") + } + + pre { + cuts.length == royaltyDescriptions.length && cuts.length == royaltyBeneficiaries.length: "Array length should be equal for royalty related details" } execute { - let currentIDString = self.mintingIDBefore.toString() + // Create the royalty details + var count = 0 + var royalties: [MetadataViews.Royalty] = [] + while royaltyBeneficiaries.length > count { + let beneficiary = royaltyBeneficiaries[count] + let beneficiaryCapability = getAccount(beneficiary).capabilities.get<&{FungibleToken.Receiver}>( + MetadataViews.getRoyaltyReceiverPublicPath() + ) + + if !beneficiaryCapability.check() { + panic("Beneficiary does not have Receiver configured at RoyaltyReceiverPublicPath") + } + + royalties.append( + MetadataViews.Royalty( + receiver: beneficiaryCapability, + cut: cuts[count], + description: royaltyDescriptions[count] + ) + ) + count = count + 1 + } + // Mint the NFT and deposit it to the recipient's collection - self.minter.mintNFT( - recipient: self.recipientCollectionRef, - name: "Example NFT #".concat(currentIDString), - description: "Example description for #".concat(currentIDString), - thumbnail: "https://robohash.org/".concat(currentIDString), - royalties: [] + let mintedNFT <- self.minter.mintNFT( + name: name, + description: description, + thumbnail: thumbnail, + royalties: royalties ) + self.recipientCollectionRef.deposit(token: <-mintedNFT) } - post { - self.recipientCollectionRef.getIDs().contains(self.mintingIDBefore): "The next NFT ID should have been minted and delivered" - ExampleNFT.totalSupply == self.mintingIDBefore + 1: "The total supply should have been increased by 1" - } -} +} \ No newline at end of file diff --git a/cadence/transactions/transfer.cdc b/cadence/transactions/transfer.cdc index 9a4e9a3..2588cad 100644 --- a/cadence/transactions/transfer.cdc +++ b/cadence/transactions/transfer.cdc @@ -1,44 +1,51 @@ +/// This transaction is for transferring an ExampleNFT from one account to another + +import "ViewResolver" +import "MetadataViews" import "NonFungibleToken" -import "ExampleNFT" -/// This transaction is for transferring and NFT from -/// one account to another -transaction(recipient: Address, withdrawID: UInt64) { +transaction(contractAddress: Address, contractName: String, recipient: Address, withdrawID: UInt64) { /// Reference to the withdrawer's collection - let withdrawRef: &ExampleNFT.Collection + let withdrawRef: auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Collection} /// Reference of the collection to deposit the NFT to - let depositRef: &{NonFungibleToken.CollectionPublic} + let receiverRef: &{NonFungibleToken.Receiver} + + prepare(signer: auth(BorrowValue) &Account) { + + // borrow the NFT contract as ViewResolver reference + let viewResolver = getAccount(contractAddress).contracts.borrow<&{ViewResolver}>(name: contractName) + ?? panic("Could not borrow ViewResolver of given name from address") + + // resolve the NFT collection data from the NFT contract + let collectionData = viewResolver.resolveContractView(resourceType: nil, viewType: Type()) as! MetadataViews.NFTCollectionData? + ?? panic("ViewResolver does not resolve NFTCollectionData view") - prepare(signer: AuthAccount) { // borrow a reference to the signer's NFT collection - self.withdrawRef = signer - .borrow<&ExampleNFT.Collection>(from: ExampleNFT.CollectionStoragePath) - ?? panic("Account does not store an object at the specified path") + self.withdrawRef = signer.storage.borrow( + from: collectionData.storagePath + ) ?? panic("Account does not store an object at the specified path") // get the recipients public account object let recipient = getAccount(recipient) // borrow a public reference to the receivers collection - self.depositRef = recipient - .getCapability(ExampleNFT.CollectionPublicPath) - .borrow<&{NonFungibleToken.CollectionPublic}>() - ?? panic("Could not borrow a reference to the receiver's collection") + let receiverCap = recipient.capabilities.get<&{NonFungibleToken.Receiver}>(collectionData.publicPath) + + self.receiverRef = receiverCap.borrow() + ?? panic("Could not borrow reference to the recipient's receiver") } execute { - // withdraw the NFT from the owner's collection let nft <- self.withdrawRef.withdraw(withdrawID: withdrawID) + self.receiverRef.deposit(token: <-nft) - // Deposit the NFT in the recipient's collection - self.depositRef.deposit(token: <-nft) } post { !self.withdrawRef.getIDs().contains(withdrawID): "Original owner should not have the NFT anymore" - self.depositRef.getIDs().contains(withdrawID): "The reciever should now own the NFT" } } \ No newline at end of file diff --git a/flow.json b/flow.json index fbcd410..773c2be 100644 --- a/flow.json +++ b/flow.json @@ -1,48 +1,75 @@ { "contracts": { - "ExampleNFT": "cadence/contracts/exampleNFT/ExampleNFT.cdc", + "ExampleNFT": "cadence/contracts/exampleNFT/ExampleNFT.cdc" + }, + "dependencies": { + "Burner": { + "source": "mainnet://f233dcee88fe0abe.Burner", + "hash": "71af18e227984cd434a3ad00bb2f3618b76482842bae920ee55662c37c8bf331", + "aliases": { + "mainnet": "f233dcee88fe0abe" + } + }, "FungibleToken": { - "source": "", + "source": "mainnet://f233dcee88fe0abe.FungibleToken", + "hash": "1410889b47fef8b02f6867eef3d67a75288a56a651b67a7e815ce273ad301cff", "aliases": { - "emulator": "ee82856bf20e2aa6" + "emulator": "ee82856bf20e2aa6", + "mainnet": "f233dcee88fe0abe", + "previewnet": "a0225e7000ac82a9", + "testnet": "9a0766d93b6608b7" } }, "MetadataViews": { - "source": "", + "source": "mainnet://1d7e57aa55817448.MetadataViews", + "hash": "be26ea7959d7cbc06ac69fe00926b812c4da67984ea2d1bde1029141ae091378", "aliases": { - "emulator": "f8d6e0586b0a20c7" + "emulator": "f8d6e0586b0a20c7", + "mainnet": "1d7e57aa55817448", + "previewnet": "b6763b4399a888c8", + "testnet": "631e88ae7f1d7c20" } }, "NonFungibleToken": { - "source": "", + "source": "mainnet://1d7e57aa55817448.NonFungibleToken", + "hash": "49a58b950afdaf0728fdb7d4eb47cf4f2ec3077d655f274b7fdeb504c742f528", "aliases": { - "emulator": "f8d6e0586b0a20c7" + "emulator": "f8d6e0586b0a20c7", + "mainnet": "1d7e57aa55817448", + "previewnet": "b6763b4399a888c8", + "testnet": "631e88ae7f1d7c20" } }, - "ViewResolver": "cadence/contracts/standards/ViewResolver.cdc" + "ViewResolver": { + "source": "mainnet://1d7e57aa55817448.ViewResolver", + "hash": "374a1994046bac9f6228b4843cb32393ef40554df9bd9907a702d098a2987bde", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "1d7e57aa55817448", + "previewnet": "b6763b4399a888c8", + "testnet": "631e88ae7f1d7c20" + } + } }, "networks": { "emulator": "127.0.0.1:3569", "mainnet": "access.mainnet.nodes.onflow.org:9000", + "previewnet": "access.previewnet.nodes.onflow.org:9000", "sandboxnet": "access.sandboxnet.nodes.onflow.org:9000", "testnet": "access.devnet.nodes.onflow.org:9000" }, "accounts": { "default": { - "address": "f669cb8d41ce0c74", - "key": "2e09fac1b3b8e128c67b2e57ba59ec8506ff8326053812135a954b4d3291096f" + "address": "179b6b1cb6755e31", + "key": "d2c3686da84d61c13627bdf2127866fe358165734f5470be792e6771901d2856" }, "emulator-account": { "address": "f8d6e0586b0a20c7", "key": "d2c3686da84d61c13627bdf2127866fe358165734f5470be792e6771901d2856" }, "exampleNFT": { - "address": "192440c99cb17282", - "key": "58cfc56dcbbfa8bfb7b9898586793b0710dfebe8a0358774044968abc9acc8c2" - }, - "standards": { - "address": "fd43f9148d4b725d", - "key": "28946711cc802634ca4520c0f8b113e065c519dc02306a05ecb204a896ee80ed" + "address": "f3fcd2c1a78f5eee", + "key": "d2c3686da84d61c13627bdf2127866fe358165734f5470be792e6771901d2856" } }, "deployments": { @@ -50,9 +77,6 @@ "default": [], "exampleNFT": [ "ExampleNFT" - ], - "standards": [ - "ViewResolver" ] } }