Your First NFT
This tutorial describes how to create and transfer non-fungible assets on the Aptos blockchain. The Aptos no-code implementation for non-fungible digital assets can be found in the aptos_token.move Move module.
Step 1: Pick an SDK
Install your preferred SDK from the below list:
Step 2: Run the example
- Typescript
- Python
Clone the aptos-ts-sdk
repo:
git clone [email protected]:aptos-labs/aptos-ts-sdk.git
Navigate to the Typescript SDK examples directory:
cd aptos-ts-sdk/examples/typescript-esm
Install the necessary dependencies:
pnpm install
Run the Typescript simple_digital_asset
example:
pnpm run simple_digital_asset
Clone the aptos-core
repo:
git clone https://github.com/aptos-labs/aptos-core.git
Navigate to the Python SDK directory:
cd ~/aptos-core/ecosystem/python/sdk
Install the necessary dependencies:
curl -sSL https://install.python-poetry.org | python3
poetry install
Run the Python simple_aptos_token
example:
poetry run python -m examples.simple_aptos_token
Step 3: Understand the output
- Typescript
- Python
The following output should appear after executing the simple_digital_asset
example, though some values will be different:
=== Addresses ===
Alice's address is: 0x770dbeb6101056eac5a19de9a73ad72fac512e0de909e7bcb13a9d9241d1d162
=== Create the collection ===
Alice's collection: {
"collection_id": "0x23ece6c35415f5c5a720dc4de2820cabece0a6f1768095db479f657ad2c05753",
"collection_name": "Example Collection",
"creator_address": "0x770dbeb6101056eac5a19de9a73ad72fac512e0de909e7bcb13a9d9241d1d162",
"current_supply": 0,
"description": "Example description.",
"last_transaction_timestamp": "2023-11-29T21:26:03.204874",
"last_transaction_version": 8001101,
"max_supply": 18446744073709552000,
"mutable_description": true,
"mutable_uri": true,
"table_handle_v1": null,
"token_standard": "v2",
"total_minted_v2": 0,
"uri": "aptos.dev"
}
=== Alice Mints the digital asset ===
Alice's digital assets balance: 1
Alice's digital asset: {
"token_standard": "v2",
"token_properties_mutated_v1": null,
"token_data_id": "0x9f4460e29a66b4e41cef1671767dc8a5e8c52a2291e36f84b8596e0d1205fd8c",
"table_type_v1": null,
"storage_id": "0x9f4460e29a66b4e41cef1671767dc8a5e8c52a2291e36f84b8596e0d1205fd8c",
"property_version_v1": 0,
"owner_address": "0x770dbeb6101056eac5a19de9a73ad72fac512e0de909e7bcb13a9d9241d1d162",
"last_transaction_version": 8001117,
"last_transaction_timestamp": "2023-11-29T21:26:04.521624",
"is_soulbound_v2": false,
"is_fungible_v2": false,
"amount": 1,
"current_token_data": {
"collection_id": "0x23ece6c35415f5c5a720dc4de2820cabece0a6f1768095db479f657ad2c05753",
"description": "Example asset description.",
"is_fungible_v2": false,
"largest_property_version_v1": null,
"last_transaction_timestamp": "2023-11-29T21:26:04.521624",
"last_transaction_version": 8001117,
"maximum": null,
"supply": 0,
"token_data_id": "0x9f4460e29a66b4e41cef1671767dc8a5e8c52a2291e36f84b8596e0d1205fd8c",
"token_name": "Example Asset",
"token_properties": {},
"token_standard": "v2",
"token_uri": "aptos.dev/asset",
"current_collection": {
"collection_id": "0x23ece6c35415f5c5a720dc4de2820cabece0a6f1768095db479f657ad2c05753",
"collection_name": "Example Collection",
"creator_address": "0x770dbeb6101056eac5a19de9a73ad72fac512e0de909e7bcb13a9d9241d1d162",
"current_supply": 1,
"description": "Example description.",
"last_transaction_timestamp": "2023-11-29T21:26:04.521624",
"last_transaction_version": 8001117,
"max_supply": 18446744073709552000,
"mutable_description": true,
"mutable_uri": true,
"table_handle_v1": null,
"token_standard": "v2",
"total_minted_v2": 1,
"uri": "aptos.dev"
}
}
}
=== Transfer the digital asset to Bob ===
Alice's digital assets balance: 0
Bob's digital assets balance: 1
This example demonstrates:
Details
- Initializing the Aptos client.
- The creation of two accounts: Alice and Bob.
- The funding and creation of Alice and Bob's accounts.
- The creation of a collection and a token using Alice's account.
- Alice sending a token to Bob.
The following output should appear after executing the simple_aptos_token
example, though some values will be different:
=== Addresses ===
Alice: 0x391f8b07439768674023fb87ae5740e90fb8508600486d8ee9cc411b4365fe89
Bob: 0xfbca055c91d12989dc6a2c1a5e41ae7ba69a35454b04c69f03094bbccd5210b4
=== Initial Coin Balances ===
Alice: 100000000
Bob: 100000000
=== Creating Collection and Token ===
Collection data: {
"address": "0x38f5310a8f6f3baef9a54daea8a356d807438d3cfe1880df563fb116731b671c",
"creator": "0x391f8b07439768674023fb87ae5740e90fb8508600486d8ee9cc411b4365fe89",
"name": "Alice's",
"description": "Alice's simple collection",
"uri": "https://aptos.dev"
}
Token owner: Alice
Token data: {
"address": "0x57710a3887eaa7062f96967ebf966a83818017b8f3a8a613a09894d8465e7624",
"owner": "0x391f8b07439768674023fb87ae5740e90fb8508600486d8ee9cc411b4365fe89",
"collection": "0x38f5310a8f6f3baef9a54daea8a356d807438d3cfe1880df563fb116731b671c",
"description": "Alice's simple token",
"name": "Alice's first token",
"uri": "https://doc.alcove.pro/img/nyan.jpeg",
"index": "1"
}
=== Transferring the token to Bob ===
Token owner: Bob
=== Transferring the token back to Alice ===
Token owner: Alice
This example demonstrates:
Details
- Initializing the REST and faucet clients.
- The creation of two accounts: Alice and Bob.
- The funding and creation of Alice and Bob's accounts.
- The creation of a collection and a token using Alice's account.
- Alice sending a token to Bob.
- Bob sending the token back to Alice.
Step 4: The SDK in depth
- Typescript
- Python
See simple_digital_asset
for the complete code as you follow the below steps.
See simple_aptos_token
for the complete code as you follow the below steps.
Step 4.1: Initializing the clients
- Typescript
- Python
In the first step, the simple_digital_asset
example initializes the Aptos client:
const APTOS_NETWORK: Network =
NetworkToNetworkName[process.env.APTOS_NETWORK] || Network.DEVNET;
const config = new AptosConfig({ network: APTOS_NETWORK });
const aptos = new Aptos(config);
By default, the Aptos client points to Aptos devnet services. However, it can be configured with the network
input argument
In the first step, the example initializes both the API and faucet clients.
- The API client interacts with the REST API.
- The faucet client interacts with the devnet Faucet service for creating and funding accounts.
rest_client = RestClient(NODE_URL)
faucet_client = FaucetClient(FAUCET_URL, rest_client)
Using the API client we can create a TokenClient
that we use for common token operations such as creating collections and tokens, transferring them, claiming them, and so on.
token_client = AptosTokenClient(rest_client)
common.py
initializes these values as follows:
NODE_URL = os.getenv("APTOS_NODE_URL", "https://fullnode.devnet.aptoslabs.com/v1")
FAUCET_URL = os.getenv(
"APTOS_FAUCET_URL",
"https://faucet.devnet.aptoslabs.com",
)
By default, the URLs for both the services point to Aptos devnet services. However, they can be configured with the following environment variables:
APTOS_NODE_URL
APTOS_FAUCET_URL
Step 4.2: Creating local accounts
The next step is to create two accounts locally. Accounts consist of a public address and the public/private key pair used to authenticate ownership of the account. This step demonstrates how to generate an Account and store its key pair and address in a variable.
- Typescript
- Python
const alice = Account.generate();
const bob = Account.generate();
alice = Account.generate()
bob = Account.generate()
Note that this only generates the local keypair. After generating the keypair and public address, the account still does not exist on-chain.
Step 4.3: Creating blockchain accounts
In order to actually instantiate the Account on-chain, it must be explicitly created somehow. On the devnet network, you can request free coins with the Faucet API to use for testing purposes. This example leverages the faucet to fund and inadvertently create Alice and Bob's accounts:
- Typescript
- Python
await aptos.fundAccount({
accountAddress: alice.accountAddress,
amount: 100_000_000,
});
await aptos.faucet.fundAccount({
accountAddress: bob.accountAddress,
amount: 100_000_000,
});
bob_fund = faucet_client.fund_account(alice.address(), 100_000_000)
alice_fund = faucet_client.fund_account(bob.address(), 100_000_000)
Step 4.4: Creating a collection
Now begins the process of creating the digital, non-fungible assets. First, as the creator, you must create a collection that groups the assets. A collection can contain zero, one, or many distinct fungible or non-fungible assets within it. The collection is simply a container, intended only to group assets for a creator.
- Typescript
- Python
Your application will call createCollectionTransaction
and then signAndSubmitTransaction
to chain:
const createCollectionTransaction = await aptos.createCollectionTransaction({
creator: alice,
description: collectionDescription,
name: collectionName,
uri: collectionURI,
});
const committedTxn = await aptos.signAndSubmitTransaction({
signer: alice,
transaction: createCollectionTransaction,
});
This is the function signature of createCollectionTransaction
. It returns a SingleSignerTransaction
that can be simulated or submitted to chain:
export async function createCollectionTransaction(
args: {
creator: Account;
description: string;
name: string;
uri: string;
options?: InputGenerateTransactionOptions;
} & CreateCollectionOptions,
): Promise<SingleSignerTransaction>;
Your application will call create_collection
:
txn_hash = await token_client.create_collection(
alice,
"Alice's simple collection",
1,
collection_name,
"https://aptos.dev",
True,
True,
True,
True,
True,
True,
True,
True,
True,
0,
1,
)
This is the function signature of create_collection
. It returns a transaction hash:
async def create_collection(
self,
creator: Account,
description: str,
max_supply: int,
name: str,
uri: str,
mutable_description: bool,
mutable_royalty: bool,
mutable_uri: bool,
mutable_token_description: bool,
mutable_token_name: bool,
mutable_token_properties: bool,
mutable_token_uri: bool,
tokens_burnable_by_creator: bool,
tokens_freezable_by_creator: bool,
royalty_numerator: int,
royalty_denominator: int,
) -> str:
Step 4.5: Creating a token
To create a token, the creator must specify an associated collection. A token must be associated with a collection, and that collection must have remaining tokens that can be minted. There are many attributes associated with a token, but the helper API exposes only the minimal amount required to create static content.
- Typescript
- Python
Your application will call mintTokenTransaction
:
const mintTokenTransaction = await aptos.mintTokenTransaction({
creator: alice,
collection: collectionName,
description: tokenDescription,
name: tokenName,
uri: tokenURI,
});
const committedTxn = await aptos.signAndSubmitTransaction({
signer: alice,
transaction: mintTokenTransaction,
});
This is the function signature of mintTokenTransaction
. It returns a SingleSignerTransaction
that can be simulated or submitted to chain:
async mintTokenTransaction(args: {
creator: Account;
collection: string;
description: string;
name: string;
uri: string;
options?: InputGenerateTransactionOptions;
}): Promise<SingleSignerTransaction>
Your application will call mint_token
:
txn_hash = await token_client.mint_token(
alice,
collection_name,
"Alice's simple token",
token_name,
"https://doc.alcove.pro/img/nyan.jpeg",
PropertyMap([]),
)
This is the function signature of mint_token
. It returns a transaction hash:
async def mint_token(
self,
creator: Account,
collection: str,
description: str,
name: str,
uri: str,
properties: PropertyMap,
) -> str:
Step 4.6: Reading token and collection metadata
Both the collection and token assets are Objects on-chain with unique addresses. Their metadata is stored at the object address. The SDKs provide convenience wrappers around querying this data:
- Typescript
- Python
To read a collection's metadata:
const alicesCollection = await aptos.getCollectionData({
creatorAddress: alice.accountAddress,
collectionName,
minimumLedgerVersion: BigInt(pendingTxn.version),
});
console.log(`Alice's collection: ${JSON.stringify(alicesCollection, null, 4)}`);
To read an owned token's metadata:
const alicesDigitalAsset = await aptos.getOwnedTokens({
ownerAddress: alice.accountAddress,
minimumLedgerVersion: BigInt(pendingTxn.version),
});
console.log(
`Alice's digital asset: ${JSON.stringify(alicesDigitalAsset[0], null, 4)}`,
);
To read a collection's metadata:
async def get_collection_data(
token_client: AptosTokenClient, collection_addr: AccountAddress
) -> dict[str, str]:
collection = (await token_client.read_object(collection_addr)).resources[Collection]
return {
"creator": str(collection.creator),
"name": str(collection.name),
"description": str(collection.description),
"uri": str(collection.uri),
}
To read a token's metadata:
async def get_token_data(
token_client: AptosTokenClient, token_addr: AccountAddress
) -> dict[str, str]:
token = (await token_client.read_object(token_addr)).resources[Token]
return {
"collection": str(token.collection),
"description": str(token.description),
"name": str(token.name),
"uri": str(token.uri),
"index": str(token.index),
}
Step 4.7: Reading an object's owner
Each object created from the aptos_token.move
contract is a distinct asset. The assets owned by a user are stored separately from the user's account. To check if a user owns an object, check the object's owner:
- Typescript
- Python
const alicesDigitalAsset = await aptos.getOwnedTokens({
ownerAddress: alice.accountAddress,
minimumLedgerVersion: BigInt(pendingTxn.version),
});
console.log(
`Alice's digital asset: ${JSON.stringify(alicesDigitalAsset[0], null, 4)}`,
);
export async function getOwnedTokens(args: {
aptosConfig: AptosConfig;
ownerAddress: AccountAddressInput;
options?: PaginationArgs & OrderByArg<GetTokenActivityResponse[0]>;
}): Promise<GetOwnedTokensResponse> {
const { aptosConfig, ownerAddress, options } = args;
const whereCondition: CurrentTokenOwnershipsV2BoolExp = {
owner_address: { _eq: AccountAddress.from(ownerAddress).toStringLong() },
amount: { _gt: 0 },
};
const graphqlQuery = {
query: GetCurrentTokenOwnership,
variables: {
where_condition: whereCondition,
offset: options?.offset,
limit: options?.limit,
order_by: options?.orderBy,
},
};
const data = await queryIndexer<GetCurrentTokenOwnershipQuery>({
aptosConfig,
query: graphqlQuery,
originMethod: "getOwnedTokens",
});
return data.current_token_ownerships_v2;
}
obj_resources = await token_client.read_object(token_addr)
owner = str(get_owner(obj_resources))
print(f"\nToken owner: {owners[owner]}")
owners = {str(alice.address()): "Alice", str(bob.address()): "Bob"}
Step 4.8: Transfer the object back and forth
Each object created from the aptos_token.move
contract is a distinct asset. The assets owned by a user are stored separately from the user's account. To check if a user owns an object, check the object's owner:
- Typescript
- Python
const alicesDigitalAsset = await aptos.getOwnedTokens({
ownerAddress: alice.accountAddress,
minimumLedgerVersion: BigInt(pendingTxn.version),
});
console.log(
`Alice's digital asset: ${JSON.stringify(alicesDigitalAsset[0], null, 4)}`,
);
const transferTransaction = await aptos.transferDigitalAsset({
sender: alice,
digitalAssetAddress: alicesDigitalAsset[0].token_data_id,
recipient: bob.accountAddress,
});
const committedTxn = await aptos.signAndSubmitTransaction({
signer: alice,
transaction: transferTransaction,
});
const pendingTxn = await aptos.waitForTransaction({
transactionHash: committedTxn.hash,
});
const alicesDigitalAssetsAfter = await aptos.getOwnedTokens({
ownerAddress: alice.accountAddress,
minimumLedgerVersion: BigInt(pendingTxn.version),
});
console.log(
`Alices's digital assets balance: ${alicesDigitalAssetsAfter.length}`,
);
const bobDigitalAssetsAfter = await aptos.getOwnedTokens({
ownerAddress: bob.accountAddress,
minimumLedgerVersion: BigInt(pendingTxn.version),
});
console.log(`Bob's digital assets balance: ${bobDigitalAssetsAfter.length}`);
print("\n=== Transferring the token to Bob ===")
txn_hash = await token_client.transfer_token(
alice,
token_addr,
bob.address(),
)
await rest_client.wait_for_transaction(txn_hash)
async def transfer_token(
self, owner: Account, token: AccountAddress, to: AccountAddress
) -> str:
return await self.client.transfer_object(owner, token, to)
obj_resources = await token_client.read_object(token_addr)
print(f"Token owner: {owners[str(get_owner(obj_resources))]}")
print("\n=== Transferring the token back to Alice ===")
txn_hash = await token_client.transfer_token(
bob,
token_addr,
alice.address(),
)
await rest_client.wait_for_transaction(txn_hash)
obj_resources = await token_client.read_object(token_addr)
print(f"Token owner: {owners[str(get_owner(obj_resources))]}\n")