Skip to main content

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

Clone the aptos-ts-sdk repo and build it:

git clone https://github.com/aptos-labs/aptos-ts-sdk.git
cd aptos-ts-sdk
pnpm install
pnpm build

Navigate to the Typescript ESM examples directory:

cd examples/typescript-esm

Install the necessary dependencies and build it:

pnpm install
pnpm build

Run the Typescript simple_digital_asset example:

pnpm run simple_digital_asset

Step 3: Understand the output

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.

Step 4: The SDK in depth

See the full code

See simple_digital_asset for the complete code as you follow the below steps.


Step 4.1: Initializing the clients

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

By default, the Aptos client points to Aptos devnet services. However, it can be configured with the network input argument


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.

const alice = Account.generate();
const bob = Account.generate();
info

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:

await aptos.fundAccount({
accountAddress: alice.accountAddress,
amount: 100_000_000,
});
await aptos.faucet.fundAccount({
accountAddress: bob.accountAddress,
amount: 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.

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

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.

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>

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:

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.getOwnedDigitalAssets({
ownerAddress: alice.accountAddress,
minimumLedgerVersion: BigInt(pendingTxn.version),
});

console.log(
`Alice's digital asset: ${JSON.stringify(alicesDigitalAsset[0], null, 4)}`,
);

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:

const alicesDigitalAsset = await aptos.getOwnedDigitalAssets({
ownerAddress: alice.accountAddress,
minimumLedgerVersion: BigInt(pendingTxn.version),
});

console.log(
`Alice's digital asset: ${JSON.stringify(alicesDigitalAsset[0], null, 4)}`,
);
Making the query to get the data
export async function getOwnedDigitalAssets(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: "getOwnedDigitalAssets",
});

return data.current_token_ownerships_v2;
}

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:

const alicesDigitalAsset = await aptos.getOwnedDigitalAssets({
ownerAddress: alice.accountAddress,
minimumLedgerVersion: BigInt(pendingTxn.version),
});

console.log(
`Alice's digital asset: ${JSON.stringify(alicesDigitalAsset[0], null, 4)}`,
);
Transfer the token from Alice to Bob
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,
});
Print each user's queried token amount
const alicesDigitalAssetsAfter = await aptos.getOwnedDigitalAssets({
ownerAddress: alice.accountAddress,
minimumLedgerVersion: BigInt(pendingTxn.version),
});
console.log(
`Alices's digital assets balance: ${alicesDigitalAssetsAfter.length}`,
);

const bobDigitalAssetsAfter = await aptos.getOwnedDigitalAssets({
ownerAddress: bob.accountAddress,
minimumLedgerVersion: BigInt(pendingTxn.version),
});
console.log(`Bob's digital assets balance: ${bobDigitalAssetsAfter.length}`);

Supporting documentation