First NFT
In this guide, we will learn how to create an NFT collection that complies with the Non-fungible Token Standard.
Prerequisites:
- Write code in Typescript
- Understand how to compile and deploy contracts in a project using Typescript SDK
You can clone the example in this guide and run it like this:
git clone https://github.com/alephium/ralph-example.git
cd ralph-example/your-firsts
npm install
npm compile
npx ts-node src/non-fungible-token.ts
Create the NFT contract
An NFT contract that complies with the Non-fungible Token
Standard needs to implement
the INFT
interface, which defines the following two methods:
getTokenUri
, which returns the URI to NFT's metadatagetCollectionIndex
, which returns the contract ID of the NFT collection to which this NFT belongs, as well as the index of this specific NFT within the collection
Here is an example of the AwesomeNFT contract:
import "std/nft_interface"
Contract AwesomeNFT(
collectionId: ByteVec,
nftIndex: U256,
uri: ByteVec
) implements INFT {
pub fn getTokenUri() -> ByteVec {
return uri
}
pub fn getCollectionIndex() -> (ByteVec, U256) {
return collectionId, nftIndex
}
}
The NFT metadata pointed to by the token URI should return a json file with the following schema:
{
"title": "NFT Metadata",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of the NFT"
},
"description": {
"type": "string",
"description": "General description of the NFT",
"nullable": true
},
"image": {
"type": "string",
"description": "A URI to the image that represents the NFT"
},
"attributes": {
"type": "array",
"description": "An array of attributes for the NFT",
"items": {
"type": "object",
"properties": {
"trait_type": {
"type": "string",
"description": "The type of trait"
},
"value": {
"type": ["string", "number", "boolean"],
"description": "The value of the trait"
}
}
},
"nullable": true
}
}
}
Here is a concrete example of the NFT metadata:
{
"name": "#0001",
"image": "https://ipfs.io/ipfs/QmaEdvjx5gRBU3m6jxJT4DDc4DDZsbcKLqgS6rmeqHH9pq/0002.png",
"attributes": [
{
"trait_type": "Background",
"value": "Aged leather"
},
{
"trait_type": "Intricate Processes",
"value": "deep texture"
},
{
"trait_type": "Behavioral Patterns",
"value": "worn patina"
}
]
}
That is all for creating a NFT contract!
Create NFT collection contract
An NFT Collection is also represented as a contract. It needs to
implement the INFTCollection
interface to comply with the
Non-fungible Token
Standard. NFTs within an NFT
collection are created as
sub-contracts from
the NFT collection contract. Following is an example of an NFT
collection contract for the AwesomeNFT
contract that we created in
the previous section:
import "std/nft_collection_interface"
Contract AwesomeNFTCollection(
nftTemplateId: ByteVec,
collectionUri: ByteVec,
mut totalSupply: U256
) implements INFTCollection {
enum ErrorCodes {
IncorrectTokenIndex = 0
NFTNotFound = 1
NFTNotPartOfCollection = 2
}
pub fn getCollectionUri() -> ByteVec {
return collectionUri
}
pub fn totalSupply() -> U256 {
return totalSupply
}
pub fn nftByIndex(index: U256) -> INFT {
checkCaller!(index < totalSupply(), ErrorCodes.IncorrectTokenIndex)
let nftTokenId = subContractId!(toByteVec!(index))
assert!(contractExists!(nftTokenId), ErrorCodes.NFTNotFound)
return INFT(nftTokenId)
}
pub fn validateNFT(nftId: ByteVec, nftIndex: U256) -> () {
let expectedTokenContract = nftByIndex(nftIndex)
assert!(nftId == contractId!(expectedTokenContract), ErrorCodes.NFTNotPartOfCollection)
}
@using(preapprovedAssets = true, updateFields = true, checkExternalCaller = false)
pub fn mint(nftUri: ByteVec) -> (ByteVec) {
let minter = callerAddress!()
let (initialImmState, initialMutState) = AwesomeNFT.encodeFields!(selfContractId!(), totalSupply, nftUri)
let contractId = copyCreateSubContractWithToken!{minter -> ALPH: 1 alph}(
toByteVec!(totalSupply),
nftTemplateId,
initialImmState,
initialMutState,
1,
minter
)
totalSupply = totalSupply + 1
return contractId
}
}
The INFTCollection
interface defines four methods:
getCollectionUri
, which returns the URI to NFT collection's metadatatotalSupply
, which defines the total supply of NFTs for this collectionnftByIndex
, which returns the NFT given its index in the collectionvalidateNFT
, which checks if an NFT is part of this NFT collection
The metadata of the NFT collection pointed to by collection URI should return a json file with the following schema:
{
"title": "NFT Collection Metadata",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of the NFT collection"
},
"description": {
"type": "string",
"description": "General description of the NFT collection"
},
"image": {
"type": "string",
"description": "A URI to the image that represents the NFT collection"
}
}
}
Here is a concrete example of the NFT collection metadata:
{
"name": "ALEPHIUM EVERYWHERE",
"description": "ALPH is everywhere,\nWill you know how to find its trace?\nIn your heart, it’s there.",
"image": "https://alephium-nft.infura-ipfs.io/ipfs/Qmb8kEPPW4E17nzyZ691bgQAMbMEgRcNZxuH3wXgByfzvt"
}
The mint
function is responsible for minting an NFT in this
collection. It takes the metadata URI for the new NFT as parameter and
creates a
sub-contract using
the AwesomeNFT
template and issued exactly 1
token for it. It also
increments the total supply of the collection by one.
That's all for creating a NFT collection contract!
Deploy NFT collection and mint NFT
First, we should deploy a dummy version of the AwesomeNFT
contract
as a template contract. This template will be used by the
AwesomeNFTCollection
contract to create AwesomeNFT
sub-contracts
more efficiently.
After AwesomeNFTCollection
is deployed, we can use the
nftCollection.transact.mint
function to mint new AwesomeNFT
s:
async function nonFungibleToken() {
web3.setCurrentNodeProvider('http://127.0.0.1:22973', undefined, fetch)
const signer = await testNodeWallet()
// Deploy a template contract for AwesomeNFT, we need to use its id in the
// NFT collection contract to efficiently create AwesomeNFT sub-contracts
const { contractInstance: awesomeNFTTemplate } = await AwesomeNFT.deploy(
signer,
{ initialFields: { collectionId: '', nftIndex: 0n, uri: '' } }
)
// Create the NFT collection contract
const { contractInstance: nftCollection } = await AwesomeNFTCollection.deploy(
signer,
{
initialFields: {
nftTemplateId: awesomeNFTTemplate.contractId,
collectionUri: stringToHex('https://alephium-nft.infura-ipfs.io/ipfs/QmdobfsES5tx6tdgiyiXiC5pqwyd7WQRZ8gJcM3eMHenYJ'),
totalSupply: 0n
},
}
)
// Mint the NFT
await nftCollection.transact.mint({
signer,
args: { nftUri: stringToHex('https://ipfs.io/ipfs/QmSeS5DQgu7Nwm5cmwhnPnRjGgA4YZYUoJJ1vRVwB3Z8iA/1') },
attoAlphAmount: ONE_ALPH / 10n + DUST_AMOUNT
})
}
Interact with the NFT
For contracts that implement INFTCollection
and INFT
interfaces,
Typescript SDK offers a canonical way to fetch
their respective metadata:
// NFT Collection Metadata
const collectionMetadata = await web3.getCurrentNodeProvider().fetchNFTCollectionMetaData(nftCollection.contractId)
console.log("NFT Collection URI, totalSupply", collectionMetadata.collectionUri, collectionMetadata.totalSupply)
// NFT Metadata
const nftMetadata = await web3.getCurrentNodeProvider().fetchNFTMetaData(address)
console.log('NFT Token URI, collection address', nftMetadata.tokenUri, nftMetadata.collectionId)
These interfaces also enable SDK to guess their types, so that dApps and wallets can handle them respectively:
// Guess NFT token type
const nftTokenType = await web3.getCurrentNodeProvider().guessStdTokenType(nft.contractId)
expect(nftTokenType).toEqual('non-fungible')
// Check if a contract is a NFT collection
const isNFTCollection = await web3.getCurrentNodeProvider().guessFollowsNFTCollectionStd(nftCollection.contractId)
console.log("Is NFT collection", isNFTCollection)
What's Next
Congratulations, now you know how to mint your first NFT! If you are interested in understanding more about how to create NFT collections and trade them in a marketplace, you can find an example NFT marketplace here.