Next

Devnet

Create Non-Fungible Token with CLI

This tutorial will guide you through the process of using the assetnft module to create and manage Non-Fungible Tokens.

Please note that each subsequent section depends on the previous one.

Prerequisites

  • cored binary installed

  • We are going to interact with testnet, so we need to set --node and --chain-id flag in each request.

    export CHAIN_ID="coreum-testnet-1"
    export RPC_URL="https://full-node.testnet-1.coreum.dev:26657"
    

    If you want to use other network, find relevant values at network variables page.

  • Two accounts at your local keychain. If you don't have them, follow these steps - go to faucet page and generate two accounts there. Then, having the mnemonics, import them with the following commands

    cored keys add nft-issuer-wallet --recover --chain-id=$CHAIN_ID
    # put first mnemonic here
    cored keys add nft-receiver-wallet --recover --chain-id=$CHAIN_ID
    # put second mnemonic here
    
  • Also, since network operates with raw addresses, not your local names, we are going to bind raw addresses to env vars. We will export the following values:

    export NFT_ISSUER_ADDRESS=$(cored keys show nft-issuer-wallet --address --chain-id=$CHAIN_ID)
    export NFT_RECEIVER_ADDRESS=$(cored keys show nft-receiver-wallet --address --chain-id=$CHAIN_ID)
    

Create your first NFT

  • We will export one more environment variable to store NFT class id, which is a uniquely named group of NFT objects. It can be defined with different characteristics (features) that are common for all NFTs belonging to the class.
    export NFT_CLASS_ID=$(echo "puppysmartnft1-"$NFT_ISSUER_ADDRESS)
    

Create NFT class

  • Let's create new NFT class:

    # cored tx assetnft issue-class [symbol] [name] [description] [uri] [uri_hash] --from [issuer] --features=burning,freezing,whitelisting,disable_sending [flags]
    cored tx assetnft issue-class puppysmartnft1 "Puppy NFTs" "A collection of awesome puppy NFTs" "http://puppy-nfts.com" "somehash" --from $NFT_ISSUER_ADDRESS --features=burning,freezing,whitelisting --node=$RPC_URL --chain-id=$CHAIN_ID
    # confirm transaction before signing and broadcasting [y/N]: y
    

    As an output, you should receive tx hash, copy it and go to Block explorer to see the tx status. Features: burning, freezing and whitelisting are explained later in this guide.

  • Now, let's check what NFT classes reside on the Coreum blockchain:

    cored q nft classes --node=$RPC_URL --chain-id=$CHAIN_ID
    #classes:
    #- data: null
    #  description: A collection of awesome puppy NFTs
    #  id: puppysmartnft1-testcore105hmczwh0tkha2h5lu9rr07xtegzsm49d3hxq7
    #  name: Puppy NFTs
    #  symbol: puppysmartnft1
    #  uri: http://puppy-nfts.com
    #  uri_hash: somehash
    

    As you see from the output, your new token has got unique class id which consists of the symbol provided and issuer account address.

Mint NFTs

  • We will now mint 2 NFTs within created class

    #cored tx assetnft mint [class-id] [id] [uri] [uri_hash] --from [sender] [flags]
    cored tx assetnft mint $NFT_CLASS_ID puppysmartnft-1 "http://puppy-nfts.com/puppynft-1" "somehash" --from $NFT_ISSUER_ADDRESS --node=$RPC_URL --chain-id=$CHAIN_ID
    
    cored tx assetnft mint $NFT_CLASS_ID puppysmartnft-2 "http://puppy-nfts.com/puppynft-2" "somehash" --from $NFT_ISSUER_ADDRESS --node=$RPC_URL --chain-id=$CHAIN_ID
    

    Again, we can check transaction status by providing transaction hashes on the Block explorer page

  • We can query all NFTs of a given class with the following command

cored q nft nfts --class-id=$NFT_CLASS_ID --node=$RPC_URL --chain-id=$CHAIN_ID
#nfts:
#- class_id: puppysmartnft1-testcore105hmczwh0tkha2h5lu9rr07xtegzsm49d3hxq7
#  data: null
#  id: puppysmartnft-1
#  uri: http://puppy-nfts.com/puppynft-1
#  uri_hash: somehash
#- class_id: puppysmartnft1-testcore105hmczwh0tkha2h5lu9rr07xtegzsm49d3hxq7
#  data: null
#  id: puppysmartnft-2
#  uri: http://puppy-nfts.com/puppynft-2
#  uri_hash: somehash

As you can see, 2 NFTs were properly minted.

Whitelisting

Whitelisting functionality allows transferring NFTs of a certain class only to specific addresses. Since we enabled that feature for our NFT class, in order to send one of our minted NFTs to a receiver, we need to first whitelist them.

cored tx assetnft whitelist $NFT_CLASS_ID puppysmartnft-1 $NFT_RECEIVER_ADDRESS --from $NFT_ISSUER_ADDRESS --node=$RPC_URL --chain-id=$CHAIN_ID

Sending and querying balance

Our NFT is now ready to be transferred to the receiving account.

Send

cored tx nft send $NFT_CLASS_ID puppysmartnft-1 $NFT_RECEIVER_ADDRESS --from $NFT_ISSUER_ADDRESS --node=$RPC_URL --chain-id=$CHAIN_ID

Query the balance

We can query NFT balances in multiple ways. These could be as follows

cored q nft balance $NFT_RECEIVER_ADDRESS $NFT_CLASS_ID --node=$RPC_URL --chain-id=$CHAIN_ID
# amount: "1"

That way, we can query the number of NFTs of a given class owned by the owner. Our receiving account owns 1 NFT of this class.

We can also query the owner of the NFT based on its class and id.

cored q nft owner  $NFT_CLASS_ID puppysmartnft-1  --node=$RPC_URL --chain-id=$CHAIN_ID
# owner: testcore1k9575m9egrlmymnyd29p5g0p5e94d930tg67sv

Freezing

Freezing, if enabled for NFT class, allows issuer to freeze specific NFT of a given class. A frozen token cannot be transferred until it is unfrozen by the issuer.

Freeze

cored tx assetnft freeze $NFT_CLASS_ID puppysmartnft-1 --from $NFT_ISSUER_ADDRESS --node=$RPC_URL --chain-id=$CHAIN_ID

Our NFT of this class is now frozen. Let's verify if this is actually the case

  • Query frozen

Using the following command, we can check frozen flag set for an NFT in a given class

cored q assetnft frozen $NFT_CLASS_ID puppysmartnft-1 --node=$RPC_URL --chain-id=$CHAIN_ID
#frozen: true
  • Let's now try to send the NFT from the receiver back to the issuer. Such transaction should not succeed.
cored tx nft send $NFT_CLASS_ID puppysmartnft-1 $NFT_ISSUER_ADDRESS --from $NFT_RECEIVER_ADDRESS --node=$RPC_URL --chain-id=$CHAIN_ID -b=block
#at the end of the raw_log section, notice an error: "raw_log: 'failed to execute message; message index: 0: nft with classID:puppysmartnft1-testcore105hmczwh0tkha2h5lu9rr07xtegzsm49d3hxq7 and ID:puppysmartnft-1 is frozen: unauthorized'

Notice we used -b=block argument which waits for the tx to pass/fail checks and be committed in a block.

Unfreeze

  • We can unfreeze the token and make it transferable again
cored tx assetnft unfreeze $NFT_CLASS_ID puppysmartnft-1 --from $NFT_ISSUER_ADDRESS --node=$RPC_URL --chain-id=$CHAIN_ID
  • Sending the token back to the issuer succeeds this time
cored tx nft send $NFT_CLASS_ID puppysmartnft-1 $NFT_ISSUER_ADDRESS --from $NFT_RECEIVER_ADDRESS --node=$RPC_URL --chain-id=$CHAIN_ID -b=block
  • Let's query the current owner of the token
cored q nft owner $NFT_CLASS_ID puppysmartnft-1  --node=$RPC_URL --chain-id=$CHAIN_ID
#owner: testcore105hmczwh0tkha2h5lu9rr07xtegzsm49d3hxq7

The address shown belongs to the issuer.

Class Freeze

  • Let's send the NFT back to receiver first.
cored tx nft send $NFT_CLASS_ID puppysmartnft-1 $NFT_RECEIVER_ADDRESS --from $NFT_ISSUER_ADDRESS --node=$RPC_URL --chain-id=$CHAIN_ID
  • Now we can freeze all the NFTs of a class held by an account
cored tx assetnft class-freeze $NFT_CLASS_ID $NFT_RECEIVER_ADDRESS --from $NFT_ISSUER_ADDRESS --chain-id=$CHAIN_ID -b=block
  • Let's now try to send the NFT from the issuer to the receiver. Such transaction should not succeed.
cored tx nft send $NFT_CLASS_ID puppysmartnft-1 $NFT_ISSUER_ADDRESS --from $NFT_RECEIVER_ADDRESS --node=$RPC_URL --chain-id=$CHAIN_ID -b=block
#at the end of the raw_log section, notice an error: "raw_log: 'failed to execute message; message index: 0: nft with classID:puppysmartnft1-testcore105hmczwh0tkha2h5lu9rr07xtegzsm49d3hxq7 and ID:puppysmartnft-1 is frozen: unauthorized'

Class Unfreeze

  • We can remove the class freeze of an account
cored tx assetnft class-unfreeze $NFT_CLASS_ID $NFT_RECEIVER_ADDRESS --from $NFT_ISSUER_ADDRESS --chain-id=$CHAIN_ID -b=block
  • Sending the token back to the issuer succeeds this time
cored tx nft send $NFT_CLASS_ID puppysmartnft-1 $NFT_ISSUER_ADDRESS --from $NFT_RECEIVER_ADDRESS --node=$RPC_URL --chain-id=$CHAIN_ID -b=block

Burning

If burning feature is enabled on NFT class level, it allows an owner of a token to burn it.

  • First, let's query an nft we want to burn
cored q nft nft $NFT_CLASS_ID puppysmartnft-2 --node=$RPC_URL --chain-id=$CHAIN_ID
#nft:
#  class_id: puppysmartnft1-testcore105hmczwh0tkha2h5lu9rr07xtegzsm49d3hxq7
#  data: null
#  id: puppysmartnft-2
#  uri: http://puppy-nfts.com/puppynft-2
#  uri_hash: somehash
  • Now, then token can be burnt
cored tx assetnft burn $NFT_CLASS_ID puppysmartnft-2 --from $NFT_ISSUER_ADDRESS --node=$RPC_URL --chain-id=$CHAIN_ID
  • As a verification step, let's check if the token still exists
cored q nft nft $NFT_CLASS_ID puppysmartnft-2 --node=$RPC_URL --chain-id=$CHAIN_ID
#notice an error at the end of the output
#not found nft: class: puppysmartnft1-testcore105hmczwh0tkha2h5lu9rr07xtegzsm49d3hxq7, id: puppysmartnft-2: nft does not exist: invalid request

Update Data

update the data of the specified NFT with the information provided in the data file

cored tx assetnft update-data $NFT_CLASS_ID puppysmartnft-1 --from $NFT_ISSUER_ADDRESS --data-file /path/to/your/data-file.json --node=$RPC_URL --chain-id=$CHAIN_ID

FAQ

Can a token be always burnt by an owner?

No, when a token is frozen, it cannot be burnt. Also, burning feature needs to be enabled on NFT class id level.

In order to burn a token, does burning feature always need to be enabled?

In general yes. However, an issuer of a token, can always burn it (while they own it), regardless of the feature setting.

Can all tokens within an NFT class be frozen/unfrozen at once?

No. There's no single command allowing to achieve that. It can, though, be done programmatically (for example, by utilizing the output returned by cored q nft nfts --class-id=$NFT_CLASS_ID --node=$RPC_URL --chain-id=$CHAIN_ID command).

What is next?

You can read more about Non-Fungible Tokens at Smart Token Overview and assetnft spec pages

Create Non-Fungible Token with Golang

This document gives instructions and examples on how to use our pkg/client package to create and manage non-fungible token.

Complete code

Complete code with go.mod file you can find here

P.S. If you have issues with go mod tidy command, just copy go.mod file from the example above.

!!!include(/docs/next/tutorials/get-started/golang)!!!

Creating NFT class

First we create class which is a container for a set of NFTs having the same purpose:

senderAddress, err := senderInfo.GetAddress()
if err != nil {
	panic(err)
}
const classSymbol = "NFTClass"
msgIssueClass := &assetnfttypes.MsgIssueClass{
	Issuer:      senderAddress.String(),
	Symbol:      classSymbol,
	Name:        "NFT Class",
	Description: "NFT Class",
	Features:    []assetnfttypes.ClassFeature{assetnfttypes.ClassFeature_freezing},
}

ctx := context.Background()
_, err = client.BroadcastTx(
	ctx,
	clientCtx.WithFromAddress(senderAddress),
	txFactory,
	msgIssueClass,
)
if err != nil {
	panic(err)
}

Minting NFT

Then we mint new NFT for that class:

classID := assetnfttypes.BuildClassID(classSymbol, senderAddress)
const nftID = "myNFT"
msgMint := &assetnfttypes.MsgMint{
	Sender:  senderAddress.String(),
	ClassID: classID,
	ID:      nftID,
}

_, err = client.BroadcastTx(
	ctx,
	clientCtx.WithFromAddress(senderAddress),
	txFactory,
	msgMint,
)
if err != nil {
	panic(err)
}

Querying the owner

We query the owner of the NFT to verify that it is owned by the creator:

nftClient := nft.NewQueryClient(clientCtx)
resp, err := nftClient.Owner(ctx, &nft.QueryOwnerRequest{
	ClassId: classID,
	Id:      nftID,
})
if err != nil {
	panic(err)
}
fmt.Printf("Owner: %s\n", resp.Owner)

Sending NFT

Now we send NFT to someone else:

recipientInfo, _, err := clientCtx.Keyring().NewMnemonic(
	"recipient",
	keyring.English,
	sdk.GetConfig().GetFullBIP44Path(),
	"",
	hd.Secp256k1,
)
if err != nil {
	panic(err)
}

recipientAddress, err := recipientInfo.GetAddress()
if err != nil {
	panic(err)
}
msgSend := &nft.MsgSend{
	Sender:   senderAddress.String(),
	Receiver: recipientAddress.String(),
	Id:       nftID,
	ClassId:  classID,
}

_, err = client.BroadcastTx(
	ctx,
	clientCtx.WithFromAddress(senderAddress),
	txFactory,
	msgSend,
)
if err != nil {
	panic(err)
}

Let's verify that recipient is the owner now:

resp, err = nftClient.Owner(ctx, &nft.QueryOwnerRequest{
	ClassId: classID,
	Id:      nftID,
})
if err != nil {
	panic(err)
}
fmt.Printf("Owner: %s\n", resp.Owner)

Freezing

Because issuer enabled freezing feature during class issuance, he/she might freeze the NFT:

msgFreeze := &assetnfttypes.MsgFreeze{
	Sender:  senderAddress.String(),
	ClassID: classID,
	ID:      nftID,
}

_, err = client.BroadcastTx(
	ctx,
	clientCtx.WithFromAddress(senderAddress),
	txFactory,
	msgFreeze,
)
if err != nil {
	panic(err)
}

Update Data

update the data of the specified NFT with the information provided in the data file

dataDynamic := assetnfttypes.DataDynamic{
		Items: []assetnfttypes.DataDynamicItem{
			{
				Editors: []assetnfttypes.DataEditor{
					assetnfttypes.DataEditor_owner,
				},
				Data: jsonData,
			},
		},
	}
	data, err = codectypes.NewAnyWithValue(&dataDynamic)
	if err != nil {
		panic(err)
	}

	// Broadcast transaction minting new dynamic nft
	classID = assetnfttypes.BuildClassID(classSymbol, senderAddress)
	const dynamicNftID = "myDynamicNFT"
	msgMint = &assetnfttypes.MsgMint{
		Sender:  senderAddress.String(),
		ClassID: classID,
		ID:      dynamicNftID,
		Data:    data,
	}

	_, err = client.BroadcastTx(
		ctx,
		clientCtx.WithFromAddress(senderAddress),
		txFactory,
		msgMint,
	)
	if err != nil {
		panic(err)
	}

	// Query the nft
	storedNFT, err := nftClient.NFT(ctx, &nft.QueryNFTRequest{
		ClassId: classID,
		Id:      dynamicNftID,
	})
	if err != nil {
		panic(err)
	}

	var storedDataDynamic assetnfttypes.DataDynamic
	err = storedDataDynamic.Unmarshal(storedNFT.Nft.Data.Value)
	if err != nil {
		panic(err)
	}
	fmt.Printf("Data: %s\n", string(storedDataDynamic.Items[0].Data))

	// update stored NFT
	msgUpdateData := &assetnfttypes.MsgUpdateData{
		Sender:  senderAddress.String(),
		ClassID: classID,
		ID:      dynamicNftID,
		Items: []assetnfttypes.DataDynamicIndexedItem{
			{
				Index: 0,
				Data:  []byte(`{"name": "Updated Name", "description": "Updated Description"}`),
			},
		},
	}

	_, err = client.BroadcastTx(
		ctx,
		clientCtx.WithFromAddress(senderAddress),
		txFactory,
		msgUpdateData,
	)
	if err != nil {
		panic(err)
	}

	storedNFT, err = nftClient.NFT(ctx, &nft.QueryNFTRequest{
		ClassId: classID,
		Id:      dynamicNftID,
	})
	if err != nil {
		panic(err)
	}

	var storedDataDynamic2 assetnfttypes.DataDynamic
	err = storedDataDynamic2.Unmarshal(storedNFT.Nft.Data.Value)
	if err != nil {
		panic(err)
	}
	fmt.Printf("Data: %s\n", string(storedDataDynamic2.Items[0].Data))

After doing this, recipient is not allowed to transfer the NFT from its account.

All the other features may be used in a similar fashion. More info is available in the documentation