NFT Transactions
Non-Fungible Tokens (NFTs) are unique digital assets on the ArcBlock blockchain, representing everything from digital art to real-world property rights. This section details how to manage NFTs using the @arcblock/graphql-client
library, covering their creation, updates, and interactions with NFT Factories.
Before diving into the transactions, it is recommended to familiarize yourself with the core concepts of Non-Fungible Tokens (NFTs) and Wallets and Accounts.
Create an NFT#
The CreateAssetTx
transaction allows you to create a new NFT on the ArcBlock chain. The @arcblock/graphql-client
provides a convenient createAsset
helper method to simplify this process. When creating an NFT, you define its unique properties, such as its moniker
(human-readable name), whether it's readonly
or transferrable
, and any associated data
, display
, or endpoint
information.
Parameters for client.createAsset
#
Name | Type | Description |
---|---|---|
|
| Human-readable name for the NFT. |
|
| Optional. NFT Factory identifier if created from a factory. |
|
| Optional. Time-to-live after first consumption, in seconds (default: 0). |
|
| The data that makes the NFT unique. This can be any serializable data. |
|
| Optional. If |
|
| Optional. If |
|
| Optional. Defines how the NFT will look like. |
|
| Optional. Defines dynamic attributes and actions of the NFT. |
|
| Optional. List of tags for later filtering. |
|
| Optional. Address of the wallet that delegated permissions to the |
|
| The wallet (signer) of the initial owner of the asset. |
Example: Creating a Basic NFT#
import { fromRandom } = require('@ocap/wallet');
import GraphQLClient = require('@ocap/client');
const endpoint = process.env.OCAP_API_HOST || 'http://127.0.0.1:4000';
const client = new GraphQLClient(`${endpoint}/api`);
async function createBasicNFT() {
try {
const owner = fromRandom();
console.log(`Owner address: ${owner.address}`);
// 1. Declare the owner account (required before creating assets)
let declareHash = await client.declare({ moniker: 'nft-owner', wallet: owner });
console.log(`Declare owner transaction hash: ${declareHash}`);
// 2. Create the NFT
const [createHash, assetAddress] = await client.createAsset({
moniker: 'MyUniqueNFT',
readonly: false, // Allow updates later
transferrable: true,
data: {
type: 'json',
value: {
description: 'A unique digital collectible.',
creator: owner.address,
serial: Math.random(),
},
},
wallet: owner,
});
console.log(`Create NFT transaction hash: ${createHash}`);
console.log(`New NFT address: ${assetAddress}`);
console.log(`View NFT state: ${endpoint}/explorer/assets/${assetAddress}`);
} catch (err) {
console.error('Error creating NFT:', err);
}
}
createBasicNFT();
This example first declares an owner account on the chain, then uses the client.createAsset
method to create a new NFT. The moniker
is set to "MyUniqueNFT", readonly
is false
to allow future updates, and data
contains custom JSON information about the NFT. The method returns the transaction hash and the newly generated NFT address.
Update an NFT#
Existing NFTs can be updated using the UpdateAssetTx
transaction, provided their readonly
property was set to false
during creation. The client.updateAsset
helper method simplifies this operation.
Parameters for client.updateAsset
#
Name | Type | Description |
---|---|---|
|
| The unique identifier (address) of the NFT to update. |
|
| Optional. New human-readable name for the NFT. |
|
| Optional. New data payload for the NFT. |
|
| The wallet (signer) of the current owner of the asset. |
|
| Optional. Extra parameters for the underlying client implementation. |
Example: Updating an NFT's Moniker and Data#
import { fromRandom } = require('@ocap/wallet');
import GraphQLClient = require('@ocap/client');
const endpoint = process.env.OCAP_API_HOST || 'http://127.0.0.1:4000';
const client = new GraphQLClient(`${endpoint}/api`);
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
async function updateExistingNFT() {
try {
const owner = fromRandom();
await client.declare({ moniker: 'upd-owner', wallet: owner });
const [createHash, assetAddress] = await client.createAsset({
moniker: 'InitialNFT',
readonly: false,
transferrable: true,
data: { type: 'json', value: { version: 1 } },
wallet: owner,
});
console.log(`Created NFT at: ${assetAddress}, tx: ${createHash}`);
await sleep(5000); // Wait for the asset state to consolidate
// Read initial state
const { state: initialState } = await client.getAssetState({ address: assetAddress });
console.log('Initial NFT state:', initialState.moniker, initialState.data.value);
// Update the NFT
const updateHash = await client.updateAsset({
address: assetAddress,
moniker: 'UpdatedNFT',
data: {
type: 'json',
value: {
version: 2,
status: 'updated',
},
},
wallet: owner,
});
console.log(`Update NFT transaction hash: ${updateHash}`);
console.log(`View updated NFT state: ${endpoint}/explorer/assets/${assetAddress}`);
await sleep(5000); // Wait for the asset state to consolidate
// Read updated state
const { state: updatedState } = await client.getAssetState({ address: assetAddress });
console.log('Updated NFT state:', updatedState.moniker, updatedState.data.value);
} catch (err) {
console.error('Error updating NFT:', err);
}
}
updateExistingNFT();
This example first creates a mutable NFT, then waits for a short period to ensure the state is consolidated. Afterward, it updates the NFT's moniker
to "UpdatedNFT" and modifies its data
payload. The client.updateAsset
method returns the transaction hash.
NFT Factories#
NFT Factories act as vending machines on the blockchain, allowing developers to define standard NFT formats and enable users to acquire them, often with payment. This mechanism streamlines the creation and distribution of large quantities of similar NFTs, such as tickets or certificates.
The core transaction for creating a factory is CreateFactoryTx
. The @arcblock/graphql-client
provides the createAssetFactory
helper method.
NFT Factory Workflow#
NFT factories consume certain inputs (tokens or other NFTs) to produce standardized NFTs. The process can be visualized as follows:
Parameters for client.createAssetFactory
#
Name | Type | Description |
---|---|---|
|
| An object defining the factory's properties. |
|
| Name of the NFT factory. |
|
| Description of the NFT factory. |
|
| Optional. Settlement type (e.g., |
|
| Optional. Maximum number of NFTs this factory can mint (0 for unlimited). |
|
| Optional. List of DID addresses of trusted issuers. |
|
| Defines the required inputs (tokens, assets, variables) for acquiring an NFT from this factory. |
|
| Defines the structure of the NFTs produced by this factory, often a mustache template. |
|
| Optional. Additional data attached to the factory. |
|
| Optional. Custom logic (contracts) to execute when an NFT is acquired. |
|
| Optional. Defines how the factory itself will look like. |
|
| The wallet (signer) of the factory owner. |
Example: Creating an NFT Factory#
import { fromRandom } = require('@ocap/wallet');
import GraphQLClient = require('@ocap/client');
const endpoint = process.env.OCAP_API_HOST || 'http://127.0.0.1:4000';
const client = new GraphQLClient(`${endpoint}/api`);
async function createNFTFactory() {
try {
const factoryOwner = fromRandom();
await client.declare({ moniker: 'factory-owner', wallet: factoryOwner });
const [createFactoryHash, factoryAddress] = await client.createAssetFactory({
factory: {
name: 'EventTicketFactory',
description: 'Factory for generating event tickets.',
settlement: 'instant',
limit: 1000, // Max 1000 tickets
input: {
tokens: [
{ address: 'z35nNRvYxBoHitx9yZ5ATS88psfShzPPBLxYD', value: client.fromTokenToUnit(10) }, // Requires 10 ABT
],
assets: [],
variables: [
{ name: 'eventName', description: 'Name of the event', required: true },
{ name: 'ticketType', description: 'Type of ticket (e.g., VIP, General)', required: true },
],
},
output: {
moniker: '{{input.eventName}} Ticket ({{input.ticketType}})',
data: {
type: 'json',
value: {
event: '{{input.eventName}}',
type: '{{input.ticketType}}',
issuedAt: '{{ctx.date}}',
ticketId: '{{ctx.id}}',
},
},
readonly: true,
transferrable: true,
parent: '{{ctx.factory}}',
issuer: '{{ctx.issuer.id}}',
tags: ['event-ticket', '{{input.eventName}}'],
},
hooks: [
{
name: 'mint',
type: 'contract',
hook: "transferToken('z35nNRvYxBoHitx9yZ5ATS88psfShzPPBLxYD', 'z1gShFYDsiMfGtaerBTh2ydu75768xMYPQU', '1000000000000000000');" // Example: transfer 1 token to another address
}
],
},
wallet: factoryOwner,
});
console.log(`Created NFT Factory at: ${factoryAddress}, tx: ${createFactoryHash}`);
console.log(`View NFT Factory state: ${endpoint}/explorer/factories/${factoryAddress}`);
} catch (err) {
console.error('Error creating NFT Factory:', err);
}
}
createNFTFactory();
This example creates an EventTicketFactory
that requires 10 ABT tokens and two variables (eventName
, ticketType
) to mint a new ticket NFT. The output defines the structure of the ticket NFT, using mustache templates to inject dynamic values from the input and factory context (ctx
). It also includes a simple mint
hook to demonstrate post-acquisition logic.
NFT Factory Inputs#
Factory inputs define what users must provide to acquire an NFT. They fall into three categories:
Field | Type | Description |
---|---|---|
|
| Cryptocurrencies required to acquire the NFT. Each entry specifies |
|
| NFTs to be consumed to acquire the new NFT. These are identified by their address or factory address and are marked as consumed upon successful acquisition. |
|
| Extra information required, usually collected through a form before acquisition. Each |
Example of a factory input structure:
{
"tokens": [
{
"address": "z35nNRvYxBoHitx9yZ5ATS88psfShzPPBLxYD",
"value": "299000000000000000000"
}
],
"assets": [],
"variables": [
{
"name": "plan",
"value": "",
"description": "",
"required": true
},
{
"name": "tag",
"value": "",
"description": "",
"required": true
}
]
}
NFT Factory Output#
Factory output defines the structure of the NFTs produced. It is a mustache template that generates a CreateAssetTx
structure upon rendering (acquiring). This ensures all NFTs from a factory share a predefined format.
Data that can be consumed in NFT factory output templates includes:
Variable | Description |
---|---|
| Values from the factory input variables. |
| Immutable data object attached to the factory upon creation. |
| The factory's address. |
| The current mint sequence number (factory.numMinted + 1). |
| The address of the NFT issuer. |
| The public key of the NFT issuer. |
| The moniker (name) of the NFT issuer's account. |
| The address of the NFT owner. |
Example of a factory output structure:
{
"moniker": "BlockletServerOwnershipNFT",
"data": {
"type": "json",
"value": {
"purchased": {
"plan": "{{input.plan}}",
"sku": {
"name": "{{data.name}}",
"type": "{{data.type}}",
"period": "{{data.period}}"
}
}
}
},
"readonly": false,
"transferrable": true,
"ttl": 0,
"parent": "{{ctx.factory}}",
"address": "",
"issuer": "{{ctx.issuer.id}}",
"endpoint": {
"id": "http://1322c65c-znkqyck3vfnye4cyk3yrwfnydgvvkshr9cur.did.abtnet.io/api/nft/status",
"scope": "public"
},
"display": {
"type": "url",
"content": "http://1322c65c-znkqyck3vfnye4cyk3yrwfnydgvvkshr9cur.did.abtnet.io/api/nft/display"
},
"tags": ["BlockletServerOwnershipNFT", "{{input.tag}}"]
}
NFT Factory Hooks#
Factory hooks allow developers to add custom logic that executes when a new NFT is acquired from a factory. These hooks are set during CreateFactoryTx
and are immutable thereafter.
Example of a mint
hook:
[
{
"name": "mint",
"type": "contract",
"hook": "transferToken('z35nNRvYxBoHitx9yZ5ATS88psfShzPPBLxYD','z1gShFYDsiMfGtaerBTh2ydu75768xMYPQU','4662000000000000000');\ntransferToken('z35nNRvYxBoHitx9yZ5ATS88psfShzPPBLxYD','zNKXtdqz6Jbw5mKpojK2nP5gRNiEGJY3mNFF','1998000000000000000')"
}
]
This mint
hook demonstrates an experimental chain contract that performs token transfers. In this specific case, it splits revenue from the acquisition between two different addresses.
Token transfers within factory hooks have the following restrictions:
- Revenue split must not exceed the factory input token amount.
- Token receivers must exist in the ledger.
- Token addresses must exist in the ledger.
Acquire an NFT from a Factory#
Users can acquire NFTs from a factory by sending an AcquireAssetV2Tx
or AcquireAssetV3Tx
transaction, which typically involves providing the required inputs (tokens, assets, or variables) defined by the factory. The @arcblock/graphql-client
offers preMintAsset
to prepare the transaction and acquireAsset
to send it.
Parameters for client.preMintAsset
#
Name | Type | Description |
---|---|---|
|
| The address of the NFT factory. |
|
| An object containing the required inputs (tokens, assets, variables) for the acquisition, matching the factory's |
|
| The DID address of the account that will own the acquired NFT. |
|
| The wallet (signer) of the NFT issuer (often the factory owner or a trusted issuer). |
|
| Optional. Extra parameters to merge into the inner transaction. |
Parameters for client.acquireAsset
#
Name | Type | Description |
---|---|---|
|
| The prepared inner transaction object, typically from |
|
| Optional. Address of the wallet that delegated permissions to the |
|
| The wallet (signer) of the initial owner of the asset (the buyer). |
|
| Optional. Extra parameters for the underlying client implementation. |
Example: Acquiring an NFT#
import { fromRandom } = require('@ocap/wallet');
import GraphQLClient = require('@ocap/client');
const endpoint = process.env.OCAP_API_HOST || 'http://127.0.0.1:4000';
const client = new GraphQLClient(`${endpoint}/api`);
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
async function acquireNFTFromFactory() {
try {
const factoryOwner = fromRandom();
await client.declare({ moniker: 'factory-owner-acq', wallet: factoryOwner });
// Create a dummy token for payment if needed (replace with real token address)
const [createTokenHash, tokenAddress] = await client.createToken({
name: 'Test Token',
description: 'Token for testing NFT factory',
symbol: 'TST',
unit: 'tst',
decimal: 18,
totalSupply: 1000000000000000000000n, // 1000 tokens
initialSupply: 1000000000000000000000n,
wallet: factoryOwner,
});
console.log(`Created test token: ${tokenAddress}, tx: ${createTokenHash}`);
await sleep(5000);
// Create the factory
const [createFactoryHash, factoryAddress] = await client.createAssetFactory({
factory: {
name: 'SimpleNFTFactory',
description: 'Factory for simple NFTs',
input: {
tokens: [
{ address: tokenAddress, value: client.fromTokenToUnit(1) }, // Requires 1 TST
],
assets: [],
variables: [
{ name: 'itemName', description: 'Name of the item', required: true },
],
},
output: {
moniker: '{{input.itemName}} NFT',
data: { type: 'json', value: { item: '{{input.itemName}}', acquiredAt: '{{ctx.date}}' } },
readonly: true,
transferrable: true,
parent: '{{ctx.factory}}',
issuer: '{{ctx.issuer.id}}',
},
},
wallet: factoryOwner,
});
console.log(`Created factory: ${factoryAddress}, tx: ${createFactoryHash}`);
await sleep(5000);
const buyer = fromRandom();
await client.declare({ moniker: 'nft-buyer', wallet: buyer });
// Fund the buyer with some test tokens for acquisition
await client.transfer({
token: 10, // Transfer 10 TST to buyer
to: buyer.address,
wallet: factoryOwner, // Assuming factoryOwner has tokens
tokens: [{ address: tokenAddress, value: 10 }],
});
console.log(`Transferred tokens to buyer: ${buyer.address}`);
await sleep(5000);
// Prepare the acquisition transaction
const itx = await client.preMintAsset({
factory: factoryAddress,
inputs: {
itemName: 'DigitalArtPiece',
tokens: [{ address: tokenAddress, value: 1 }], // Must match factory input
},
owner: buyer.address,
issuer: { wallet: factoryOwner, name: 'factory-owner-acq' }, // Issuer context
wallet: factoryOwner, // Wallet that signs the preMintAsset call (issuer)
});
console.log('Prepared acquisition ITX:', itx);
// Acquire the NFT
const acquireHash = await client.acquireAsset({
itx,
wallet: buyer, // The buyer signs the final acquire transaction
});
console.log(`Acquire NFT transaction hash: ${acquireHash}`);
await sleep(5000);
const { state: acquiredAssetState } = await client.getAssetState({ address: itx.asset.address });
console.log('Acquired NFT state:', acquiredAssetState);
} catch (err) {
console.error('Error acquiring NFT from factory:', err);
}
}
acquireNFTFromFactory();
This example demonstrates a full cycle of creating a token, creating a factory that accepts that token, funding a buyer, and then having the buyer acquire an NFT from the factory. The preMintAsset
function prepares the necessary data for the transaction, and acquireAsset
sends the signed transaction to the blockchain.
Mint an NFT from a Factory#
MintAssetTx
is similar to AcquireAssetTx
but typically implies that the NFT is created by the factory owner (or a trusted issuer) without requiring a payment or consumption of other assets from the recipient. This is useful for scenarios like issuing free certificates or promotional items. The @arcblock/graphql-client
provides the mintAsset
helper method.
Parameters for client.mintAsset
#
Name | Type | Description |
---|---|---|
|
| The prepared inner transaction object, typically from |
|
| The wallet (signer) of the issuer/factory owner who is minting the NFT. |
|
| Optional. Extra parameters for the underlying client implementation. |
Example: Minting an NFT#
import { fromRandom } = require('@ocap/wallet');
import GraphQLClient = require('@ocap/client');
const endpoint = process.env.OCAP_API_HOST || 'http://127.0.0.1:4000';
const client = new GraphQLClient(`${endpoint}/api`);
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
async function mintNFTFromFactory() {
try {
const minter = fromRandom(); // The minter is also the factory owner
await client.declare({ moniker: 'nft-minter', wallet: minter });
// Create the factory for minting (no token/asset inputs needed for simplicity in this example)
const [createFactoryHash, factoryAddress] = await client.createAssetFactory({
factory: {
name: 'CertificateFactory',
description: 'Factory for issuing course completion certificates',
input: {
tokens: [],
assets: [],
variables: [
{ name: 'courseName', description: 'Name of the course', required: true },
{ name: 'studentDID', description: 'DID of the student', required: true },
],
},
output: {
moniker: 'Certificate: {{input.courseName}}',
data: {
type: 'json',
value: {
course: '{{input.courseName}}',
student: '{{input.studentDID}}',
issuedDate: '{{ctx.date}}',
},
},
readonly: true,
transferrable: false, // Certificates are usually not transferrable
parent: '{{ctx.factory}}',
issuer: '{{ctx.issuer.id}}',
},
},
wallet: minter,
});
console.log(`Created factory: ${factoryAddress}, tx: ${createFactoryHash}`);
await sleep(5000);
const student = fromRandom();
await client.declare({ moniker: 'student-did', wallet: student });
// Prepare the mint transaction for the student
const itx = await client.preMintAsset({
factory: factoryAddress,
inputs: {
courseName: 'Blockchain Fundamentals',
studentDID: student.address,
},
owner: student.address, // The student will be the owner
issuer: { wallet: minter, name: 'nft-minter' }, // The minter is the issuer
wallet: minter, // The minter signs the preMintAsset call
});
console.log('Prepared mint ITX:', itx);
// Mint the NFT to the student
const mintHash = await client.mintAsset({
itx,
wallet: minter, // The minter signs the final mint transaction
});
console.log(`Mint NFT transaction hash: ${mintHash}`);
await sleep(5000);
const { state: mintedAssetState } = await client.getAssetState({ address: itx.asset.address });
console.log('Minted NFT state:', mintedAssetState);
} catch (err) {
console.error('Error minting NFT from factory:', err);
}
}
mintNFTFromFactory();
This example demonstrates how a minter
(who also owns the factory) can issue a course certificate NFT to a student
without any payment from the student. The preMintAsset
prepares the transaction, and mintAsset
executes it, assigning the newly minted NFT to the specified owner.
This section provided a detailed overview of NFT-related transactions, including creating, updating, acquiring, and minting NFTs, along with the powerful concept of NFT Factories. Understanding these operations is crucial for building applications that leverage unique digital assets on ArcBlock. For details on managing fungible tokens, proceed to the Token Transactions section.