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
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)