35
loading...
This website collects cookies to deliver better user experience
-$4.00
) to buy a cup of coffee from Mr. Peet, the bank needs to be consistent and update the ledgers on both sides to reflect the ownership change (Peet has +$4.00
and you have -$4.00
). Because both ledgers are not openly visible to both of Peet and you and the the currency is likely digital, there is no guarantee that the bank won't mistakenly or intentionally update either ledger with the incorrect value.💡 Your bank probably owes you
If you have a saving account with some money in it, you might be loaning your money to your bank. You are trusting it to have your money for you when you want to withdraw. This is why if you had a billion dollars in your account and you want to withdraw today, your teller will freak out. Your money is just part of the stream of digital currency your bank is free to do anything with.
resource
.NFT
resource and an Ownable
interface (à la ERC721) in a separate PetShop
contract:pub contract PetShop {
// A map recording owners of NFTs
pub var owners: {UInt64 : Address}
// A Transferrable interface declaring some methods or "capabilities"
pub resource interface Transferrable {
pub fun owner(): Address
pub fun transferTo(recipient: Address)
}
// NFT resource implements Transferrable
pub resource NFT: Transferrable {
// Unique id for each NFT.
pub let id: UInt64
// Constructor method
init(initId: UInt64) {
self.id = initId
}
pub fun owner(): Address {
return owners[self.id]!
}
pub fun transferTo(recipient: Address) {
// Code to transfer this NFT resource to the recipient's address.
}
}
}
pub
before each definition. This declares public access for all user accounts. Writing a Cadence contract revolves around designing access control.transaction {
// Takes the signing account as a single argument.
prepare(acc: AuthAccount) {
// This is where you write code that requires a
// signature, such as withdrawing a token from the
// signing account.
}
execute {
// This is where you write code that does not require
// a signature.
log("Hello, transaction")
}
}
owners
map by the token's ID:// Takes a target token ID as a parameter and returns an
// address of the current owner.
pub fun main(id: UInt64) : Address {
return PetStore.owner[id]!
}
npx create-react-app petstore; cd petstore
flow init
flow.json
configuration file inside. This file is important as it tells the command line tool and the FCL library where to find things in the project. Let's take a closer look at the newly created directory and add some configurations to the project.flow.json
file under the root directory. This configuration file was created when we typed the command flow init
and tells Flow that this is a Flow project. We will leave most of the initial settings as they were, but make sure it contains these fields by adding or changing them accordingly:{
// ...
"contracts": {
"PetStore": "./src/flow/contracts/PetStore.cdc"
},
"deployments": {
"emulator": {
"emulator-account": ["PetStore"]
}
},
// ...
}
flow
under src
directory, and create three more subdirectories named contract
, transaction
, and script
under flow
, respectively. This can be combined into a single command (make sure your current directory is petstore
before running this):mkdir -p src/flow/{contract,transaction,script}
contract/PetStore.cdc
, transaction/MintToken.cdc
, and script/GetTokenIds.cdc
.src
directory should now look like this:.
|— flow
| |— contract
| | |
| | `— PetStore.cdc
| |— script
| | |
| | `— GetTokenIds.cdc
| `— transaction
| |
| `— MintToken.cdc
|
...
NFT
resource within:pub contract PetStore {
// This dictionary stores token owners' addresses.
pub var owners: {UInt64: Address}
pub resource NFT {
// The Unique ID for each token, starting from 1.
pub let id: UInt64
// String -> String dictionary to hold
// token's metadata.
pub var metadata: {String: String}
// The NFT's constructor. All declared variables are
// required to be initialized here.
init(id: UInt64, metadata: {String: String}) {
self.id = id
self.metadata = metadata
}
}
}
owners
. This dictionary has the type {UInt64: Address}
which maps unsigned 64-bit integers to users' Addresses. We will use owners
to keep track of all the current owners of all tokens globally.owners
variable is prepended by a var
keyword, while the id
variable is prepended by a let
keyword. In Cadence, a mutable variable is defined using var
while an immutable one is defined with let
.💡 Immutable vs mutable
In Cadence, a variable stores a mutable variable that can be changed later in the program while a binding binds an immutable value that cannot be changed.
NFT
resource, we declare id
field and a constructor method to assign the id
to the NFT
instance.NFTReceiver
interface to define the capabilities of a receiver of NFTs. What this means is only the accounts with these capabilities can receive tokens from another addresses.NFTReceiver
code to the existing PetStore
contract. I will begin the comment for each method with "can" to make this clear that we are talking about a capability. Moreover, we won't be displaying all the code written previously. Instead, Comments with ellipses ...
will be used to notate these truncated code.pub contract PetStore {
// ...
pub resource interface NFTReceiver {
// Can withdraw a token by its ID and returns
// the token.
pub fun withdraw(id: UInt64): @NFT
// Can deposit an NFT to this NFTReceiver.
pub fun deposit(token: @NFT)
// Can fetch all NFT IDs belonging to this
// NFTReceiver.
pub fun getTokenIds(): [UInt64]
// Can fetch the metadata of an NFT instance
// by its ID.
pub fun getTokenMetadata(id: UInt64) : {String: String}
// Can update the metadata of an NFT.
pub fun updateTokenMetadata(id: UInt64, metadata: {String: String})
}
}
withdraw(id: UInt64): @NFT
method takes an NFT's id
, withdraws a token of type @NFT
, which is prepended with a @
to indicate a reference to a resource.deposit(token: @NFT)
method takes a token reference and deposits to the current NFTReceiver
.getTokenIds(): [UInt64]
method accesses all token IDs owned by the current NFTReceiver
.getTokenMetadata(id: UInt64) : {String : String}
method takes a token ID, reads the metadata, and returns it as a dictionary.updateTokenMetadata(id: UInt64, metadata: {String: String})
method takes an ID of an NFT
and a metadata dictionary to update the target NFT's metadata.NFTCollection
resource to implement the NFTReceiver
interface. Think of this as a "vault" where NFTs can be deposited or withdrawn.pub contract PetStore {
// ... The @NFT code ...
// ... The @NFTReceiver code ...
pub resource NFTCollection: NFTReceiver {
// Keeps track of NFTs this collection.
access(account) var ownedNFTs: @{UInt64: NFT}
// Constructor
init() {
self.ownedNFTs <- {}
}
// Destructor
destroy() {
destroy self.ownedNFTs
}
// Withdraws and return an NFT token.
pub fun withdraw(id: UInt64): @NFT {
let token <- self.ownedNFTs.remove(key: id)
return <- token!
}
// Deposits a token to this NFTCollection instance.
pub fun deposit(token: @NFT) {
self.ownedNFTs[token.id] <-! token
}
// Returns an array of the IDs that are in this collection.
pub fun getTokenIds(): [UInt64] {
return self.ownedNFTs.keys
}
// Returns the metadata of an NFT based on the ID.
pub fun getTokenMetadata(id: UInt64): {String : String} {
let metadata = self.ownedNFTs[id]?.metadata
return metadata!
}
// Updates the metadata of an NFT based on the ID.
pub fun updateTokenMetadata(id: UInt64, metadata: {String: String}) {
for key in metadata.keys {
self.ownedNFTs[id]?.metadata?.insert(key: key, metadata[key]!)
}
}
}
// Public factory method to create a collection
// so it is callable from the contract scope.
pub fun createNFTCollection(): @NFTCollection {
return <- create NFTCollection()
}
}
ownedNFTs
. Note the new access modifier pub(set)
, which gives public write access to the users.@NFT
resources, we prepend the type with @
, making itself a resource too.init()
, we instantiate the ownedNFTs
with an empty dictionary. A resource also needs a destroy()
destructor method to make sure it is being freed.💡 Nested Resource
A composite structure including a dictionary can store resources, but when they do they will be treated as resources. Which means they need to be moved rather than assigned and their type will be annotated with @
.
withdraw(id: UInt64): @NFT
method removes an NFT from the collection's ownedNFTs
array and return it.<-
is known as a move symbol, and we use it to move a resource around. Once a resource has been moved, it can no longer be used from the old variable.!
symbol after the token
variable. It force-unwraps the Optional
value. If the value turns out to be nil
, the program panics and crashes.@
to make them explicit. For instance, @NFT
and @NFTCollection
are two resource types.deposit(token: @NFT)
function takes the @NFT
resource as a parameter and stores it in the ownedNFTs
array in this @NFTCollection
instance.!
symbol reappears here, but now it's after the move arrow <-!
. This is called a force-move or force-assign operator, which only moves a resource to a variable if the variable is nil
. Otherwise, the program panics.getTokenIds(): [UInt64]
method simply reads all the UInt64
keys of the ownedNFTs
dictionary and returns them as an array.getTokenMetadata(id: UInt64): {String : String}
method reads the metadata
field of an @NFT
stored by its ID in the ownedNFTs
dictionary and returns it.updateTokenMetadata(id: UInt64, metadata: {String: String})
method is a bit more involved.for key in metadata.keys {
self.ownedNFTs[id]?.metadata?.insert(key: key, metadata[key]!)
}
?
in the call chain. It is used with Optional
s values to keep going down the call chain only if the value is not nil
.@NFTReceiver
interface for the @NFTCollection
resource.PetStore
contract is @NFTMinter
resource, which will contain an exclusive code for the contract owner to mint all the tokens. Without it, our store will not be able to mint any pet tokens. It is very simplistic though, since we have already blazed through the more complex components. Its only mint(): @NFT
method creates an @NFT
resource, gives it an ID, saves the address of the first owner to the contract (which is the address of the contract owner, although you could change it to mint and transfer to the creator's address in one step), increments the universal ID counter, and returns the new token.pub contract PetStore {
// ... NFT code ...
// ... NFTReceiver code ...
// ... NFTCollection code ...
pub resource NFTMinter {
// Declare a global variable to count ID.
pub var idCount: UInt64
init() {
// Instantialize the ID counter.
self.idCount = 1
}
pub fun mint(_ metadata: {String: String}): @NFT {
// Create a new @NFT resource with the current ID.
let token <- create NFT(id: self.idCount, metadata: metadata)
// Save the current owner's address to the dictionary.
PetStore.owners[self.idCount] = PetStore.account.address
// Increment the ID
self.idCount = self.idCount + 1 as UInt64
return <-token
}
}
}
@NFTCollection
instance for the deployer of the contract (you) so it is possible for the contract owner to mint and store NFTs from the contract. As we go over this last hurdle, we will also learn about the other important concept in Cadence—Storage and domains.pub contract PetStore {
// ... @NFT code ...
// ... @NFTReceiver code ...
// ... @NFTCollection code ...
// This contract constructor is called once when the contract is deployed.
// It does the following:
//
// - Creating an empty Collection for the deployer of the collection so
// the owner of the contract can mint and own NFTs from that contract.
//
// - The `Collection` resource is published in a public location with reference
// to the `NFTReceiver` interface. This is how we tell the contract that the functions defined
// on the `NFTReceiver` can be called by anyone.
//
// - The `NFTMinter` resource is saved in the account storage for the creator of
// the contract. Only the creator can mint tokens.
init() {
// Set `owners` to an empty dictionary.
self.owners = {}
// Create a new `@NFTCollection` instance and save it in `/storage/NFTCollection` domain,
// which is only accessible by the contract owner's account.
self.account.save(<-create NFTCollection(), to: /storage/NFTCollection)
// "Link" only the `@NFTReceiver` interface from the `@NFTCollection` stored at `/storage/NFTCollection` domain to the `/public/NFTReceiver` domain, which is accessible to any user.
self.account.link<&{NFTReceiver}>(/public/NFTReceiver, target: /storage/NFTCollection)
// Create a new `@NFTMinter` instance and save it in `/storage/NFTMinter` domain, accesible
// only by the contract owner's account.
self.account.save(<-create NFTMinter(), to: /storage/NFTMinter)
}
}
@NFTCollection
instance for our own account and saved it to the /storage/NFTCollection
namespace. The path following the first namespace is arbitrary, so we could have named it /storage/my/nft/collection
. Then, something odd happened as we "link" a reference to the @NFTReceiver
capability from the /storage
domain to /public
. The caret pair <
and >
was used to explicitly annotate the type of the reference being linked, &{NFTReceiver}
, with the &
and the wrapping brackets {
and }
to define the unauthorized reference type (see References to learn more). Last but not least, we created the @NFTMinter
instance and saved it to our account's /storage/NFTMinter
domain.For a deep dive into storages, check out Account Storage.
PetStore
contract, let's try to deploy it to the Flow emulator to verify the contract. Start the emulator by typing flow emulator
in your shell.flow emulator
> INFO[0000] ⚙️ Using service account 0xf8d6e0586b0a20c7 serviceAddress=f8d6e0586b0a20c7 serviceHashAlgo=SHA3_256 servicePrivKey=bd7a891abd496c9cf933214d2eab26b2a41d614d81fc62763d2c3f65d33326b0 servicePubKey=5f5f1442afcf0c817a3b4e1ecd10c73d151aae6b6af74c0e03385fb840079c2655f4a9e200894fd40d51a27c2507a8f05695f3fba240319a8a2add1c598b5635 serviceSigAlgo=ECDSA_P256
> INFO[0000] 📜 Flow contracts FlowFees=0xe5a8b7f23e8b548f FlowServiceAccount=0xf8d6e0586b0a20c7 FlowStorageFees=0xf8d6e0586b0a20c7 FlowToken=0x0ae53cb6e3f42a79 FungibleToken=0xee82856bf20e2aa6
> INFO[0000] 🌱 Starting gRPC server on port 3569 port=3569
> INFO[0000] 🌱 Starting HTTP server on port 8080 port=8080
0xf8d6e0586b0a20c7
(In fact, these numbers are so ubiquitous in Flow that it has its own Address
type). This is the address of the contract on the emulator.flow project deploy
to deploy our first contract. You should see an output similar to this if it was successful:flow project deploy
> Deploying 1 contracts for accounts: emulator-account
>
> PetStore -> 0xf8d6e0586b0a20c7 (11e3afe90dc3a819ec9736a0a36d29d07a2f7bca856ae307dcccf4b455788710)
>
>
> ✨ All contracts deployed successfully
⚠️ Oops! That didn't work
Check flow.json
configuration and make sure the path to the contract is correct.
// MintToken.cdc
// Import the `PetStore` contract instance from the master account address.
// This is a fixed address for used with the emulator only.
import PetStore from 0xf8d6e0586b0a20c7
transaction(metadata: {String: String}) {
// Declare an "unauthorized" reference to `NFTReceiver` interface.
let receiverRef: &{PetStore.NFTReceiver}
// Declare an "authorized" reference to the `NFTMinter` interface.
let minterRef: &PetStore.NFTMinter
// `prepare` block always take one or more `AuthAccount` parameter(s) to indicate
// who are signing the transaction.
// It takes the account info of the user trying to execute the transaction and
// validate. In this case, the contract owner's account.
// Here we try to "borrow" the capabilities available on `NFTMinter` and `NFTReceiver`
// resources, and will fail if the user executing this transaction does not have access
// to these resources.
prepare(account: AuthAccount) {
// Note that we have to call `getCapability(_ domain: Domain)` on the account
// object before we can `borrow()`.
self.receiverRef = account.getCapability<&{PetStore.NFTReceiver}>(/public/NFTReceiver)
.borrow()
?? panic("Could not borrow receiver reference")
// With an authorized reference, we can just `borrow()` it.
// Note that `NFTMinter` is borrowed from `/storage` domain namespace, which
// means it is only accessible to this account.
self.minterRef = account.borrow<&PetStore.NFTMinter>(from: /storage/NFTMinter)
?? panic("Could not borrow minter reference")
}
// `execute` block executes after the `prepare` block is signed and validated.
execute {
// Mint the token by calling `mint(metadata: {String: String})` on `@NFTMinter` resource, which returns an `@NFT` resource, and move it to a variable `newToken`.
let newToken <- self.minterRef.mint(metadata)
// Call `deposit(token: @NFT)` on the `@NFTReceiver` resource to deposit the token.
// Note that this is where the metadata can be changed before transferring.
self.receiverRef.deposit(token: <-newToken)
}
}
⚠️ Ambiguous type warning
If you are using VSCode, chances are you might see the editor flagging the
lines referring to PetStore.NFTReceiver
and PetStore.NFTMinter
types
with an "ambiguous type <T> not found". Try to reset the running emulator
by pressing Ctrl+C
in the shell where you ran the emulator to interrupt it
and run it again with flow emulator
and on a different shell, don't forget
to redeploy the contract with flow project deploy
.
PetStore
contract instance.transaction
block takes an arbitrary number of named parameters, which will be provided by the calling program (In Flow CLI, JavaScript, Go, or other language). These parameters are the only channels for the transaction code to interact with the outside world.&{NFTReceiver}
and &NFTMinter
(Note the first is an unauthorized reference).prepare
block, which is responsible for authorizing the transaction. This block takes an argument of type AuthAccount
. This account instance is required to sign and validate the transaction with its key. If it takes more than one AuthAccount
parameters, then the transaction becomes a multi-signature transaction. This is the only place our code can access the account object.getCapability(/public/NFTReceiver)
on the account instance, then borrow()
to borrow the reference to NFTReceiver
and gain the capability for receiverRef
to receive tokens. We also called borrow(from: /storage/NFTMinter)
on the account to enable minterRef
with the superpower to mint tokens into existence.execute
block runs the code within after the prepare
block succeeds. Here, we called mint(metadata: {String: String})
on the minterRef
reference, then moved the newly created @NFT
instance into a newToken
variable. After, we called deposit(token: @NFT)
on the receiverRef
reference, passing <-newToken
(@NFT
resource) as an argument. The newly minted token is now stored in our account's receiverRef
.metadata
of type {String: String}
(string to string dictionary), we will need to pass that argument when sending the command via Flow CLI.flow transactions send src/flow/transaction/MintToken.cdc '{"name": "Max", "breed": "Bulldog"}'
> Transaction ID: b10a6f2a1f1d88f99e562e72b2eb4fa3ae690df591d5a9111318b07b8a72e060
>
> Status ✅ SEALED
> ID b10a6f2a1f1d88f99e562e72b2eb4fa3ae690df591d5a9111318b07b8a72e060
> Payer f8d6e0586b0a20c7
> Authorizers [f8d6e0586b0a20c7]
> ...
flow keys generate
. The output should look similar to the following, while the keys will be different:flow keys generate
> 🔴️ Store private key safely and don't share with anyone!
> Private Key f410328ecea1757efd2e30b6bc692277a51537f30d8555106a3186b3686a2de6
> Public Key be393a6e522ae951ed924a88a70ae4cfa4fd59a7411168ebb8330ae47cf02aec489a7e90f6c694c4adf4c95d192fa00143ea8639ea795e306a27e7398cd57bd9
.keys.json
in the root directory next to flow.json
so we can read them later on:{
"private": "f410328ecea1757efd2e30b6bc692277a51537f30d8555106a3186b3686a2de6",
"public": "be393a6e522ae951ed924a88a70ae4cfa4fd59a7411168ebb8330ae47cf02aec489a7e90f6c694c4adf4c95d192fa00143ea8639ea795e306a27e7398cd57bd9"
}
<PUBLIC_KEY>
with the public key you generated to create a new account:flow accounts create —key <PUBLIC_KEY> —signer emulator-account
> Transaction ID: b19f64d3d6e05fdea5dd2ac75832d16dc61008eeacb9d290f153a7a28187d016
>
> Address 0xf3fcd2c1a78f5eee
> Balance 0.00100000
> Keys 1
>
> ...
emulator-account
(hence, —signer emulator-account
flag).flow.json
configuration file, and at the "accounts" field, add the new account name ("test-account" here, but it could be any name), address, and the private key:{
// ...
"accounts": {
"emulator-account": {
"address": "f8d6e0586b0a20c7",
"key": "bd7a891abd496c9cf933214d2eab26b2a41d614d81fc62763d2c3f65d33326b0"
},
"test-account": {
"address": "0xf3fcd2c1a78f5eee",
"key": <PRIVATE_KEY>
}
}
// ...
}
NFTCollection
in order to receive NFTs./transactions
directory next to MintToken.cdc
, create a new Cadence file named InitCollection.cdc
:// InitCollection.cdc
import PetStore from 0xf8d6e0586b0a20c7
// This transaction will be signed by any user account who wants to receive tokens.
transaction {
prepare(acct: AuthAccount) {
// Create a new empty collection for this account
let collection <- PetStore.NFTCollection.new()
// store the empty collection in this account storage.
acct.save<@PetStore.NFTCollection>(<-collection, to: /storage/NFTCollection)
// Link a public capability for the collection.
// This is so that the sending account can deposit the token to this account's
// collection by calling its `deposit(token: @NFT)` method.
acct.link<&{PetStore.NFTReceiver}>(/public/NFTReceiver, target: /storage/NFTCollection)
}
}
NFTCollection
instance and save it to their own private /storage/NFTCollection
domain (Recall that anything stored in /storage
domain can only be accessible by the current account). In the last step, we linked the NFTCollection
we have just stored to the public domain /public/NFTReceiver
(and in the process, "casting" the collection up to NFTReceiver
) so whoever is sending the token can access this and call deposit(token: @NFT)
on it to deposit the token.flow transactions send src/flow/transaction/InitCollection.cdc —signer test-account
test-account
is the name of the new account we created in the flow.json
file. Hopefully, the new account should now have an NFTCollection
created and ready to receive tokens!TransferToken.cdc
in the /transactions
directory with the following code.// TransferToken.cdc
import PetStore from 0xf8d6e0586b0a20c7
// This transaction transfers a token from one user's
// collection to another user's collection.
transaction(tokenId: UInt64, recipientAddr: Address) {
// The field holds the NFT as it is being transferred to the other account.
let token: @PetStore.NFT
prepare(account: AuthAccount) {
// Create a reference to a borrowed `NFTCollection` capability.
// Note that because `NFTCollection` is publicly defined in the contract, any account can access it.
let collectionRef = account.borrow<&PetStore.NFTCollection>(from: /storage/NFTCollection)
?? panic("Could not borrow a reference to the owner's collection")
// Call the withdraw function on the sender's Collection to move the NFT out of the collection
self.token <- collectionRef.withdraw(id: tokenId)
}
execute {
// Get the recipient's public account object
let recipient = getAccount(recipientAddr)
// This is familiar since we have done this before in the last `MintToken` transaction block.
let receiverRef = recipient.getCapability<&{PetStore.NFTReceiver}>(/public/NFTReceiver)
.borrow()
?? panic("Could not borrow receiver reference")
// Deposit the NFT in the receivers collection
receiverRef.deposit(token: <-self.token)
// Save the new owner into the `owners` dictionary for look-ups.
PetStore.owners[tokenId] = recipientAddr
}
}
MintToken.cdc
code, we were saving the minted token to our account's NFTCollection
reference stored at /storage/NFTCollection
domain.TransferToken.cdc
, we are basically creating a sequel of the minting process. The overall goal is to move the token stored in the sending source account's NFTCollection
to the receiving destination account's NFTCollection
by calling withdraw(id: UInt64)
and deposit(token: @NFT)
on the sending and receiving collections, respectively. Hopefully, by now it shouldn't be too difficult for you to follow along with the comments as you type down each line.execute
block where we call a special built-in function getAccount(_ addr: Address)
, which returns an AuthAccount
instance from an address passed as an argument to this transaction, and the last line, where we update the owners
dictionary on the PetStore
contract with the new address entry to keep track of the current NFT owners.TransferToken.cdc
by typing the command:flow transactions send src/flow/transaction/TransferToken.cdc 1 0xf3fcd2c1a78f5eee
> Transaction ID: 4750f983f6b39d87a1e78c84723b312c1010216ba18e233270a5dbf1e0fdd4e6
>
> Status ✅ SEALED
> ID 4750f983f6b39d87a1e78c84723b312c1010216ba18e233270a5dbf1e0fdd4e6
> Payer f8d6e0586b0a20c7
> Authorizers [f8d6e0586b0a20c7]
>
> ...
transaction
block of TransferToken.cdc
accepts two arguments — A token ID and the recipient's address — which we passed as a list of arguments to the command. Some of you might wonder why we left out --signer
flag for this transaction command, but not the other. Without passing the signing account's name to --signer
flag, the contract owner's account is the signer by default (a.k.a the AuthAccount
argument in the prepare
block).test-account
, it would be nice to confirm that the token was actually transferred.GetTokenOwner.cdc
under the script
directory:// GetTokenOwner.cdc
import PetStore from 0xf8d6e0586b0a20c7
// All scripts start with the `main` function,
// which can take an arbitrary number of argument and return
// any type of data.
//
// This function accepts a token ID and returns an Address.
pub fun main(id: UInt64): Address {
// Access the address that owns the NFT with the provided ID.
let ownerAddress = PetStore.owners[id]!
return ownerAddress
}
main
, which can take any number of arguments and return any data type.main
function accesses the owners
dictionary in the PetStore
contract using the token ID and returns the address of the token's owner, or fails if the value is nil
.flow scripts execute src/flow/script/GetTokenOwner.cdc <TOKEN_ID>
<TOKEN_ID>
is an unsigned integer token ID starting from 1. If you have minted an NFT and transferred it to the test-account
, then replace <TOKEN_ID>
with the token ID. You should get back the address of the test-account
you have created.GetTokenOwner.cdc
script, it takes only a few more steps to create a script that returns a token's metadata.GetTokenMetadata.cdc
which, as the name suggests, gets the metadata of an NFT based on the given ID.metadata
variable in the NFT
resource definition in the contract which stores a {String: String}
dictionary of that NFT
's metadata. Our script will have to query the right NFT
and read the variable.NFTReceiver
capability of the owner's account and call getTokenMetadata(id: UInt64) : {String: String}
on it to get back the NFT's metadata.// GetTokenMetadata.cdc
import PetStore from 0xf8d6e0586b0a20c7
// All scripts start with the `main` function,
// which can take an arbitrary number of argument and return
// any type of data.
//
// This function accepts a token ID and returns a metadata dictionary.
pub fun main(id: UInt64) : {String: String} {
// Access the address that owns the NFT with the provided ID.
let ownerAddress = PetStore.owners[id]!
// We encounter the `getAccount(_ addr: Address)` function again.
// Get the `AuthAccount` instance of the current owner.
let ownerAcct = getAccount(ownerAddress)
// Borrow the `NFTReceiver` capability of the owner.
let receiverRef = ownerAcct.getCapability<&{PetStore.NFTReceiver}>(/public/NFTReceiver)
.borrow()
?? panic("Could not borrow receiver reference")
// Happily delegate this query to the owning collection
// to do the grunt work of getting its token's metadata.
return receiverRef.getTokenMetadata(id: id)
}
flow scripts execute src/flow/script/GetTokenMetadata.cdc <TOKEN_ID>
{"name": "Max", "breed": "Bulldog"}
in the previous minting step, then that is what you will get after running the script.// GetAllTokenIds.cdc
import PetStore from 0xPetStore
pub fun main() : [UInt64] {
// We basically just return all the UInt64 keys of `owners`
// dictionary as an array to get all IDs of all tokens in existence.
return PetStore.owners.keys
}