Creating and Updating ARC19 NFTs with IPFS Metadata

This tutorial demonstrates how to create an ARC19-compliant NFT with mutable metadata hosted on IPFS. The metadata can be updated by changing the reserve address, which points to a new IPFS hash for the metadata. Follow along with the code examples and steps below.

Generating an Account

Use the following Python code to generate a new account that will be used for managing the ARC19 NFT:

from algosdk.account import generate_account

private_key, address = generate_account()
print(private_key, address)

Funding the Account

To submit transactions, you need to fund the generated account. Use the Algorand Testnet Faucet to add funds.

ARC19 Overview

With ARC19 NFTs, the metadata is mutable. The reserve address serves as a pointer to metadata stored on IPFS. By updating this address, the NFT’s metadata can change, allowing for mutable properties while the image and other data are hosted on IPFS.

Image Display

Charmander Image Charmander Updated Image

Minting an ARC19 NFT

The code snippet below uploads an image to IPFS via Pinata, then creates an ARC19-compliant NFT on Algorand, linking to the image metadata:

from algosdk.v2client.algod import AlgodClient
from algosdk.transaction import wait_for_confirmation, AssetConfigTxn
from dotenv import load_dotenv
from pinata import Pinata
from algosdk.account import address_from_private_key
import os
import json
from PIL import Image
from cid import make_cid
import multihash
import hashlib
from algosdk.encoding import encode_address

load_dotenv()

# Set up Pinata client for IPFS
pinata_api_key = os.getenv('api_key')
pinata_api_secret = os.getenv('api_secret')
pinata_jwt = os.getenv('jwt')
pinata_client = Pinata(api_key=pinata_api_key, secret_key=pinata_api_secret, access_token=pinata_jwt)

# Set up Algorand client
algod_token = os.getenv('algod_token')
algod_server = os.getenv('algod_server')
algod_client = AlgodClient(algod_token, algod_server)

private_key = os.getenv('private_key')
address = address_from_private_key(private_key)

# Mint ARC19 NFT
image_path = './charmander.png'
response = pinata_client.pin_file(image_path)
image_ipfs_hash = response['data']['IpfsHash']
viewable_hash = f'https://gateway.pinata.cloud/ipfs/' + image_ipfs_hash

img = Image.open(image_path)

metadata = {
    "standard": "arc19",
    "image": viewable_hash,
    "image_mime_type": "image/png",
    "image_integrity": "sha256-" + str(hashlib.sha256(img.tobytes()).hexdigest()),
    "properties": {
        "Type": "Fire",
        "Description": "A fire pokemon with a fiery tail",
        "Level": "1",
        "Experience": "0",
    }
}

file_name = './metadata.json'
with open(file_name, 'w') as f:
    json.dump(metadata, f)

json_pin_hash = pinata_client.pin_file(file_name)['data']['IpfsHash']
digest = multihash.decode(make_cid(json_pin_hash).multihash).digest
arc19_algorand_address = encode_address(digest)
print(f'Original ARC19 address: {arc19_algorand_address}')

params = algod_client.suggested_params()

asset_config_transaction = AssetConfigTxn(
    sender=address,
    sp=params,
    total=1,
    decimals=0,
    default_frozen=False,
    manager=address,
    reserve=arc19_algorand_address,
    asset_name="Test Charmander",
    unit_name="TC#1",
    url="template-ipfs://{ipfscid:0:dag-pb:reserve:sha2-256}",
    strict_empty_address_check=False,
)

signed_ac_tx = asset_config_transaction.sign(private_key)
tx_id = algod_client.send_transaction(signed_ac_tx)
print(tx_id)

wait_for_confirmation(algod_client, tx_id)
asset_index = algod_client.pending_transaction_info(tx_id)['asset-index']
print(asset_index)

Updating ARC19 Metadata

The following code example demonstrates updating the ARC19 metadata by pointing the reserve address to a new IPFS link, representing updated metadata:

image_path = './charmander2.0.png'
response = pinata_client.pin_file(image_path)
image_ipfs_hash = response['data']['IpfsHash']
viewable_hash = f'https://gateway.pinata.cloud/ipfs/' + image_ipfs_hash

img = Image.open(image_path)

metadata = {
    "standard": "arc19",
    "image": viewable_hash,
    "image_mime_type": "image/png",
    "image_integrity": "sha256-" + str(hashlib.sha256(img.tobytes()).hexdigest()),
    "properties": {
        "Type": "Fire",
        "Description": "A fire pokemon with a fiery tail",
        "Level": "4",
        "Experience": "15",
    }
}

file_name = './metadata.json'
with open(file_name, 'w') as f:
    json.dump(metadata, f)

json_pin_hash = pinata_client.pin_file(file_name)['data']['IpfsHash']
digest = multihash.decode(make_cid(json_pin_hash).multihash).digest
arc19_algorand_address = encode_address(digest)
print(f'New ARC19 address: {arc19_algorand_address}')

params = algod_client.suggested_params()

asset_config_transaction = AssetConfigTxn(
    index=asset_index,
    sender=address,
    sp=params,
    default_frozen=False,
    manager=address,
    reserve=arc19_algorand_address,
    url="template-ipfs://{ipfscid:0:dag-pb:reserve:sha2-256}",
    strict_empty_address_check=False,
)

signed_ac_tx = asset_config_transaction.sign(private_key)
tx_id = algod_client.send_transaction(signed_ac_tx)
print(tx_id)

Code Editor