Minting NFTs on Algorand using IPFS
Overview
Algorand NFTs with IPFS
IPFS is a decentralized p2p system for storing and accessing files, websites, applications, and data.
NFTs being a hot and progressive trend for blockchains and booming of IPFS usage to host digital assets referenced by those NFTs, demands increase of community contributions on this direction as it happens with pioneer chains like Algorand. To that purpose, the current solution code and tutorial intend to enable developers to use IPFS in conjunction with the Algorand NFT creation process, simply and practically.
NFTs on Algorand
Algorand: non-fungible tokens, or NFTs, are a type of digital asset where each token represents a unique digital or physical asset.
Very good resources to start reading on NFTs on Algorand resources:
Non-Fungible tokens are a configurable subset of Algorand Standard Assets (ASA) to represent regular or decentralized digital assets. These NFT tokens include content integrity controls but not the content itself, mostly because of volume size considerations.
To represent a tokenized digital asset then, the content needs to be available in an immutable and decentralized way and then the non-fungible token should be issued over this tamper-proof resource to include resource and metadata identifiers as well as their integrity proofs on-chain.
Simply put, the original file (binary) resources for NFT have to be available globally, better in a decentralized way. The NFT needs to somehow include both URI and checksum hash (for content integrity check) of that original binary (file) resource.
What is Algorand ARC3 (ARC0003)?
ARC3 is Algorand Standard Asset Parameters Conventions for Fungible and Non-Fungible Tokens.
ARC stands for Algorand Request for Comments, an initiative by Algorand foundation’s scientists to put their thoughts of Algorand features required conventions into the public discussion via the GitHub system. Algorand being one of the most democratic technology communities on the planet, always does things by consensus! From the depth of the chain operations and each block’s finalization up to the heights of higher-level literal conventions on layer one features such as ARC3, Algorand does it to make sure every voice is heard and every idea expressed. Regarding standards, conventions, and regulations for the technical community and subjects ARCs are the way to go and they eventually graduate as standards.
ARC3 (aka ARC0003) refers to one of these ARCs that addresses NFT conventions. Here you can find the ARC3 body of document as well as discussion threads covering the history of ARC3 in Algorand Foundation’s ARCs repository.
Important highlights from Algorand ARC3 recommendations:
-
Clients recognize ARC-3 ASAs by looking at the Asset Name and Asset URL. If the Asset Name is arc3 or ends with @arc3, or if the Asset URL ends with #arc3, the ASA is to be considered an ARC-3 ASA.
-
Asset URL (au) points to a JSON Metadata file URI. In the case of using IPFS , only standard IPFS URI (
ipfs://...
) must be used and not gateway format (https://ipfs.io/ipfs/...
). Nohttp
URI is allowed for web security standards reasons. If Asset Name does not end with@arc3
then the Asset URL has to end with#arc3
. -
Asset Metadata Hash (am) is defined as the SHA-256 digest of the JSON Metadata file as a 32-byte string (as defined in NIST FIPS 180-4), when no extra_metadata is defined in JSON Metadata file (most of use cases).
-
The JSON Medata File schema follows the Ethereum Improvement Proposal ERC-1155 Metadata URI JSON Schema with the following main differences:
- Support for integrity fields for any file pointed by any URI field as well as for localized JSON Metadata files.
- Support for mimetype fields for any file pointed by any URI field.
- Support for extra metadata that is hashed as part of the Asset Metadata Hash (am) of the ASA.
- Adding the fields external_url, background_color, animation_url used by OpenSea metadata format.
-
Similarly to ERC-1155, the URI does support ID substitution. If the URI contains {id}, clients MUST substitute it by the asset ID in decimal.
-
Contrary to ERC-1155, the ID is represented in decimal (instead of hexadecimal) to match what current APIs and block explorers use on the Algorand blockchain.
-
An example of an ARC-3 JSON Metadata file:
{ "name": "My Song", "description": "This is my song!", "image": "mysong.png", "image_integrity": "sha256-47DEQpj8HBSa+/TImW+5JCeuQeR...", "image_mimetype": "image/png", "external_url": "https://mysongs.com/song/mysong", "animation_url": "mysong.ogg", "animation_url_integrity": "sha256-LwArA6xMdnFF...", "animation_url_mimetype": "audio/ogg", "properties": { "file_url": "https://s3.amazonaws.com/your-bucket/song/full/mysong.ogg", "file_url_integrity": "sha256-7IGatqxLhUYkru...", "file_url_mimetype": "audio/ogg" } }
-
If the Asset URL is ipfs://QmWS1VA…/metadata.json:
Theimage
URI isipfs://QmWS1VA.../mysong.png
and theanimation_url
URI isipfs://QmWS1VA.../mysong.ogg
.
What needs to be done for NFT using IPFS?
As illustrated in the following diagram, this solution considers two scenarios during which you may want to use IPFS integration with Algorand NFTs:
-
Scenario 1: NFT’s digital file and metadata JSON separately and orderly, uploaded and pinned to IPFS
-
Scenario 2: A directory containing NFT’s digital file and metadata JSON file next to each other, uploaded and pinned to IPFS.
Pinning services
Unless you run a production-level IPFS node, it’s recommended to use IPFS pining services, which are simply service providers for the IPFS network and let you upload and pin your content while offering partial IPFS API endpoints as well.
For sake of this solution, we used Pinata which is free to start and very well integrated and bounded with crypto community and NFT requirement sets. We use Pinata for our NFT’s files global placement and pinning.
All codes here can be found in this solution’s code repository on GitHub, in more complete form.
First Scenario:
- Step 1 Instatiate Pinata client from Pinata SDK using your own Pinata cloud API key and secret:
const pinataSDK = require('@pinata/sdk');
const pinata = pinataSDK(pinataApiKey, pinataApiSecret);
Results should look like this:
SC1: The NFT original digital asset pinned to IPFS via Pinata: {
IpfsHash: 'QmNMwakRUvwjWK82azDYFBwrKvUYXryhfJEpzqpxxw2Yj7',
PinSize: 216160,
Timestamp: '2021-09-20T17:33:22.917Z'
}
- Step 2 Now pin the file via Pinata API:
const resultFile = await pinata.pinFileToIPFS(nftFile, options);
console.log('SC1: The NFT original digital asset pinned to IPFS via Pinata: ', resultFile);
- Step 3 Constructing metadata JSON:
let integrity = convertIpfsCidV0ToByte32(resultFile.IpfsHash)
const metadata = {
"name": assetName,
"description": assetDesc,
"image": `ipfs://${resultFile.IpfsHash}`,
"image_integrity": `sha256-${integrity.base64}`,
"image_mimetype": `${fileCat}/${fileExt}`,
"properties": {
"file_url": `${fileName}`,
"file_url_integrity": `sha256-${integrity.base64}`,
"file_url_mimetype": `${fileCat}/${fileExt}`,
}
}
Metadata JSON object with actual data from variables would look like this before sending:
{
name: 'NFT::ARC3::IPFS::1@arc3',
description: 'This is a Scenario1 NFT created with metadata JSON in ARC3 compliance and using IPFS via Pinata API',
image: 'ipfs://QmQp7k5JrwQJ2XLfmvEDzmwsTgKaLxzNUNh5eZWmXEAHSe',
image_integrity: 'sha256-48DEQpj8HBSa...',
image_mimetype: 'image/png',
external_url: 'https://github.com/emg110/arc3ipfs',
properties: {
file_url: 'arc3-asa',
file_url_integrity: 'sha256-48DEQpj8HBSa...',
file_url_mimetype: 'image/png'
}
}
- Step 4 Pinning the metadata JSON, containing all authenticity, identity, and integrity proofs of our original NFT digital asset:
const resultMeta = await pinata.pinJSONToIPFS(metadata, options);
console.log('SC1: The NFT metadata JSON file pinned to IPFS via Pinata: ', resultMeta);
Results should look like this:
SC1: The NFT metadata JSON file pinned to IPFS via Pinata: {
IpfsHash: 'Qmchxvzg3qFVhJedoNxS8BK8qptWvLxXeM1zAS8cYXT32B',
PinSize: 461,
Timestamp: '2021-09-20T17:33:25.123Z'
}
Second Scenario:
For this scenario there is an IPFS node connection needed and if you do not have access to one, just:
npm install -g ipfs
and then npm run ipfs
to get your local IPFS up & running on port 5002.
- Step 1 Instatiate pinata client from pinata SDK using your own Pinata cloud API key and secret:
const pinataSDK = require('@pinata/sdk');
const pinata = pinataSDK(pinataApiKey, pinataApiSecret);
- Step 2 Instatiate IPFS client either from http-client or IPFS embedded:
The first method, using a local IPFS node and IPFS HTTP client is more recommended becauseipfs-core
package is a bit behind track.
/*From IPFS http client (Recommended way)*/
const { create, CID } = require('ipfs-http-client')
const ipfs = create("http://localhost:5002")
/* localhost:5002 is the endpoint of your IPFS node, change it to your configurations accordingly if required*/
/*OR*/
/*From IPFS embedded (simpler way but not guaranteed to work on all systems)*/
const IPFS = require('ipfs-core');
const ipfs = await IPFS.create();
- Step 3 Now add & pin the file via Pinata API:
let resultFile = await pinata.pinFileToIPFS(nftFile, options)
console.log('SC2: The NFT original digital asset pinned to IPFS via Pinata: ', resultFile);
Results should look like this:
SC2: The NFT original digital asset pinned to IPFS via Pinata: {
IpfsHash: 'QmNMwakRUvwjWK82azDYFBwrKvUYXryhfJEpzqpxxw2Yj7',
PinSize: 216160,
Timestamp: '2021-09-20T17:33:22.917Z',
isDuplicate: false
}
- Step 4 Constructing metadata JSON:
let integrity = convertIpfsCidV0ToByte32(resultFile.IpfsHash)
const metadata = {
"name": assetName,
"description": assetDesc,
"image": `ipfs://${resultFile.IpfsHash}`,
"image_integrity": `sha256-${integrity.base64}`,
"image_mimetype": `${fileCat}/${fileExt}`,
"properties": {
"file_url": `${fileName}`,
"file_url_integrity": `sha256-${integrity.base64}`,
"file_url_mimetype": `${fileCat}/${fileExt}`,
}
}
Metadata JSON object with actual data from variables would look like this before sending:
{
name: 'NFT::ARC3::IPFS::2@arc3',
description: 'This is a Scenario2 NFT created with metadata JSON in ARC3 compliance and using IPFS via Pinata API',
image: 'ipfs://QmNMwakRUvwjWK82azD...',
image_integrity: 'sha256-48DEQpj8HBSa...',
image_mimetype: 'image/png',
properties: {
file_url: 'arc3-asa',
file_url_integrity: 'sha256-48DEQpj8HBSa...',
file_url_mimetype: 'image/png'
}
}
- Step 5 Adding & pinning the metadata JSON, containing all authenticity, identity, and integrity proofs of our original NFT digital assets:
let resultMeta = await pinata.pinJSONToIPFS(metadata, options);
console.log('SC2: The NFT metadata pinned to IPFS via Pinata: ', resultMeta);
Results should look like this:
SC2: The NFT metadata pinned to IPFS via Pinata: {
IpfsHash: 'QmXcUsZrYZotT9NKXsjQbGVQNu4QdSUdL3Z7DVMXrDQa8U',
PinSize: 461,
Timestamp: '2021-09-20T17:41:00.131Z',
isDuplicate: true
}
- Step 6 Creating a directory which is in fact a Directed Acyclic Graph (DAG) node on IPFS which can have many included items under it as linked nodes (seen as files under directory being the DAG node):
const folderCid = await ipfs.object.new({ template: 'unixfs-dir' });
console.log('SC2: The NFT folder CID: ', folderCid);
Results should look like this:
SC2: The NFT folder CID: CID(QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn)
- Step 7 Adding created digital asset (in step 3) as a link (item under directory or folder) under the created directory (step 6) as a linked node:
const finResJson = await ipfs.object.patch.addLink(folderCid, {
name: `${nftFileNameSplit[0]}.json`,
size: resultMeta.size,
cid: resultMeta.IpfsHash,
});
console.log('SC2: The NFT folder CID after added metadata JSON link: ', finResJson);
Results should look like this:
SC2: The NFT folder CID after added metadata JSON link: CID(QmarphdSVN2zQnfejFVv3qzABDh2MpsYwWMmP39crp7GWU)
- Step 8 Adding created digital asset (in step 5) as a link (item under directory or folder) under the created directory (step 6) as a linked:
const finResNft = await ipfs.object.patch.addLink(finResJson, {
name: nftFileName,
size: resultFile.size,
cid: resultFile.IpfsHash,
});
console.log('SC2: The NFT folder CID after added original NFT file link: ', finResNft);
Results should look like this:
SC2: The NFT folder CID after added original NFT file link: CID(QmPiMrU9E7KqTSAmXQwees8ZfsXmoN8t2kEZr9TPxzHrA9)
- Step 9 Adding & pinning the created directory (DAG) to local IPFS:
let finPin = await ipfs.pin.add(finResNft);
console.log('SC2: The NFT folder CID pinned locally on IPFS: ', finPin);
Results should look like this:
SC2: The NFT folder CID pinned locally on IPFS: CID(QmPiMrU9E7KqTSAmXQwees8ZfsXmoN8t2kEZr9TPxzHrA9)
- Step 10 Pinning the added directory via Pinata API (the CID multi-hash will be pinned only and content synced from your local so that you can remove your local node and shut it down without damaging or risking your IPFS content):
const finPinataPin = await pinata.pinByHash(finPin.toString());
console.log('SC2: The NFT folder CID pinned to Pinata: ', finPinataPin);
Results should look like this:
SC2: The NFT folder CID pinned to Pinata: {
id: '6b51061d-b6c0-4990-980f-25bce53f7fd6',
ipfsHash: 'QmPiMrU9E7KqTSAmXQwees8ZfsXmoN8t2kEZr9TPxzHrA9',
status: 'prechecking',
name: null
}
Use this solution’s code
In Algorand standard asset (ASA) creation documentation, how to generate an new ASA (NFT or FT) is fully covered for multiple programming languages as well as API calls. In this article here on this portal, as well , you can find a practical guide to create a new Algorand Standard Asset. There are as well Algorand Replits (Online IDE and code examples) and especially Generate NFT Replit to get a hang on how to create an NFT on Algorand, using JS code.
This solution is available on Replit as well if you like to fork and get to code right away!
To run the example code demo for both scenarios and the NFT generation using IPFS and complying to ARC3, you should:
Clone the solution’s GitHub repository project and then add your Pinata API Key
and Pinata API Secret
to config.js:
pinataApiKey: "111111",
pinataApiSecret: "1111111",
And then run:
npm install
then install the IPFS engine with
npm install -g ipfs
then in a separate terminal or command prompt , run the IPFS engine with
npm run ipfs
and then run the complete demo using
npm run start` or `node index.js
This will demo automatically and in order:
- Scenario 1
- Scenario 2
- Create NFT based on Scenario 1
The output final logs for successful NFT generation will be like this:
== > CREATE NFT
Created account balance: 10000000 microAlgos
ASA MetaDataHash: Uint8Array(32)[
54, 27, 38, 157, 143, 29, 142, 18,
13, 70, 91, 131, 138, 83, 90, 31,
95, 194, 92, 111, 121, 95, 177, 126,
146, 100, 181, 36, 207, 249, 34, 206
]
ASA MetaDataHash Length: 32
nft_integrity: NhsmnY8djhINRluDilNaH1 / CXG95X7F + kmS1JM / 5Is4 =
Transaction EGQPCWKBA6L3CRO3T6CFZBTQEVQKITRV2K2VL6R2J5GODTR4L3WA confirmed in round 17728149
Account: UUJM6DHLKMHIYX4TO5J6MWICGVLQA3VMPZV2ADQYIVIU7MGMMA4AYDMJZQ Has created ASA with ID: 43445923
Congratulations!You created your IPFS supporting, ARC3 complying NFT on Algorand!Check it by link below:
https: //testnet.algoexplorer.io/asset/43445923
The link for asset ID 43445923
at the end of logs is your generated NFT link on algoexplorer, you can check the metadata inside that NFT here on IPFS, the IPFS CID for the example generated NFT here is :
QmRyrjjQFHjejwTfE9Gf89hhy5DbJtihQqqo6sAFZFsTLh
Use Asset’s URL & MetaDataHash for NFT digital assets
-
For scenario 1:
Asset’s Metadata JSON’s IPFS address (ipfs://DIGITAL_ASSET_METADATA_JSON_CID) goes in Asset’s URL field and Metadata JSON’s hash part of CID, gets converted to a 32 bytes Uint8Array and then goes in Asset’smetadatahash
field. -
For scenario 2:
Same as scenario1 but with folder-based, Asset’s Metadata JSON’s IPFS address.
For your own use cases, you can use ipfs2bytes32 repository’s code snippets in TypeScript, JavaScript, and Python to convert IPFS CID Version 0 (default) to 32 bytes hash string and back, to be able to fit in metadatahash field, which is 32 bytes long.
Transaction Note field use cases:
This case has been explored in detail by valued dev community member Diego Said Anaya Mancilla. What’s worth adding is, now that new methods and conventions are defined and presented, methods presented by Diego can be used to attach transactional metadata to the ASA transaction note field (instead of using it to store and retrieve the IPFS CIDs). This will be detailed in a future solution by this author.