Skip to main content

Transfer and burn assets

Estimate time to complete: 20 minutes

Minting, burning and transferring assets are important to power your gaming mechanisms such as creating and destroying in-game items such as weapons or skins and complex crafting mechanics. You will learn how to do all these in this tutorial.

displaying player inventoriesdisplaying player inventories

💡What are assets?
For the purpose of this article, "assets" are NFTs you use to power either in-game currencies, or in-game items such as weapons, skins or powerups. There may be circumstances where not all of your in-game items are on-chain, for the purpose of this tutorial, we will only cover on-chain items as "assets".

This is a beginners guide, lots of the helpful context, important links and preparation material are in the blue collapsed sections. For those more confident, you can skip those.

To start, you may want to first familiarise yourself with these concepts
ConceptsDefinition
CraftingCrafting assets is a broad term for the process of combining various in-game resources or items to create new and unique items or equipment. At its core, crafting consists of receiving existing assets, combining them, and then burning them to create a new asset.
BurningBurning is the process of removing a NFT from circulation. It is identical to a transfer except the receiving address will always be the null address 0x000000000000000000000000000000000000000. (This null address is also the address from which a new token is minted - sender or from variable). Typically within the game building context, you burn assets as the item gets used in crafting.
TransferringAssets can be transferred to power a number of in-game mechanics from simple trading and rewarding to more complex crafting mechanics. Transferring can happen between:
  • Player to player
  • Player and the master application wallet
Put simply, asset transfer involves transferring the ownership of an asset from one wallet to another. This must be authorised by the sending wallet. This transaction requires a small amount of gas, which is paid by the transferring wallet.

Task 1: Set up and prerequisites

If you have not installed the Immutable SDK, see instructions on how here:

Prerequisites

Node Version 20 or later
Immutable's Typescript SDK requires **Node v20** (Active LTS version) or **higher**. Node v20 can be installed via `nvm`.

To install nvm follow these instructions. Once installed, run:

nvm install --lts
  • (Optional) To enable Code Splitting (importing only the SDK modules you need) there are additional prerequisites.

Install the Immutable SDK

Run the following command in your project root directory.

npm install -D @imtbl/sdk
# if necessary, install dependencies
npm install -D typescript ts-node
Troubleshooting

The Immutable SDK is still in early development. If experiencing complications, use the following commands to ensure the most recent release of the SDK is correctly installed:

rm -Rf node_modules
npm cache clean --force
npm i

1.1 Have your NFTs assets ready

If you'd like to test the transfer and burn functionality below with your own wallet, it will need to contain NFTs. Learn how to create and deploy assets to your wallet here.

1.2. Create signer from user wallet

As referenced in the concepts section above, transfers must be authorised by the token collection owner (deployer of the smart contract) for security reasons. To exercise this authority to initiate a transfer or burn, the owner "signs" these transactons.

💡What is a Signer?

A signer is a representation of a user's wallet (account) that is used to authorise transactions. These transactions typically involve transferring the ownership of assets (currencies or NFTs) from one account to another, hence why the user is required to "sign" these transactions. Applications use signers to obtain user signatures to execute payments, asset transfers, and more. For more information, see the Signers definition from the ethers library.

In order to create a signer, you must first establish a connection to the user's wallet. This is how you connect to different user wallets:

To initialise the passport module, please see the Passport Setup guide.

import { Web3Provider } from '@ethersproject/providers';

const passportWallet = passport.connectEvm(); // returns an EIP-1193 provider
const ethersProvider = new Web3Provider(passportWallet);
const signer = ethersProvider.getSigner();

Task 2: Transfer assets

To transfer an asset, you first create a contract instance then call a transfer method on it.

Creating a contract instance is the initial step to interacting with smart contracts. This establishes a connection between your application and the contract's address on the network which allows you execute contract functions.

The following are two methods of facilitating asset transfers:

  • Immutable SDK - Use the ERC721 class, which simplifies methods like token transfers and minting operations with predefined functions.
  • Ethers.js - Directly interface with the contract's ABI, which provides flexibility and control over method calls.

The following parameters will be required:

ParameterDescription
signerFrom the step above
CONTRACT_ADDRESSThe address of the contract
RECIPIENTAddress of the wallet that will be the new owner of the NFT when the transfer is completed
TOKEN_IDUnique ID of the NFT

2.1 Transfer a single asset

💡info

Find a more detailed explanation about how to interact with your deployed collection here.

import { getContract, http, createWalletClient, defineChain } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { ImmutableERC721Abi } from '@imtbl/contracts';

const PRIVATE_KEY = 'YOUR_PRIVATE_KEY'; // should be read from environment variable
const CONTRACT_ADDRESS = '0xYOUR_CONTRACT_ADDRESS'; // should be of type `0x${string}`
const RECIPIENT = '0xRECIPIENT_ADDRESS'; // should be of type `0x${string}`
const TOKEN_ID = BigInt(1);

const immutableTestnet = defineChain({
id: 13473,
name: 'imtbl-zkevm-testnet',
nativeCurrency: { name: 'IMX', symbol: 'IMX', decimals: 18 },
rpcUrls: {
default: {
http: ['https://rpc.testnet.immutable.com'],
},
},
});

const walletClient = createWalletClient({
chain: immutableTestnet,
transport: http(),
account: privateKeyToAccount(`0x${PRIVATE_KEY}`),
});

const contract = getContract({
address: CONTRACT_ADDRESS,
abi: ImmutableERC721Abi,
client: walletClient,
});

const transfer = async (
sender: `0x${string}`,
recipient: `0x${string}`,
tokenId: bigint
) => {
const txHash = await contract.write.safeTransferFrom([
sender,
recipient,
tokenId,
]);

console.log(`txHash: ${txHash}`);
return txHash;
};

transfer(walletClient.account.address, RECIPIENT, TOKEN_ID);

2.2 Transfer multiple assets (batch transfers)

The preset ERC721 contract includes functionality for transferring multiple NFTs in one transaction. Batch transfers, like batch minting, are more gas efficient than sending multiple single transfer requests.

💡info

Find a more detailed explanation about how to interact with your deployed collection here.

import { getContract, http, createWalletClient, defineChain } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { ImmutableERC721Abi } from '@imtbl/contracts';

const PRIVATE_KEY = 'YOUR_PRIVATE_KEY'; // should be read from environment variable
const CONTRACT_ADDRESS = '0xYOUR_CONTRACT_ADDRESS'; // should be of type `0x${string}`

const RECIPIENTS: `0x${string}`[] = ['0xRECIPIENT_ADDRESS_1', '0xRECIPIENT_ADDRESS_2'] as `0x${string}`[];
const TOKEN_IDS = [BigInt(1), BigInt(2)];

const immutableTestnet = defineChain({
id: 13473,
name: 'imtbl-zkevm-testnet',
nativeCurrency: { name: 'IMX', symbol: 'IMX', decimals: 18 },
rpcUrls: {
default: {
http: ['https://rpc.testnet.immutable.com'],
},
},
});

const walletClient = createWalletClient({
chain: immutableTestnet,
transport: http(),
account: privateKeyToAccount(`0x${PRIVATE_KEY}`),
});

const contract = getContract({
address: CONTRACT_ADDRESS,
abi: ImmutableERC721Abi,
client: walletClient,
});

const transferBatch = async (
recipients: `0x${string}`[],
tokenIds: bigint[]
): Promise<string> => {
const transfers = {
from: walletClient.account.address,
tos: recipients,
tokenIds: tokenIds,
};

const txHash = await contract.write.safeTransferFromBatch([transfers]);

console.log(`txHash: ${txHash}`);
return txHash;
};

transferBatch(RECIPIENTS, TOKEN_IDS);

Task 3. Burn assets

tip

Burning multiple assets in a single transaction, like batch minting, is more gas efficient than sending multiple single burn requests. This powerful feature enables game studios to reduce their operating costs when performing actions like crafting that burn multiple assets at the same time.

The following parameters will be required to burn an asset:

ParameterDescription
signerFrom the step above
CONTRACT_ADDRESSThe address of the contract
TOKEN_IDSUnique IDs of the NFTs to burn
💡info

Find a more detailed explanation about how to interact with your deployed collection here.

import { getContract, http, createWalletClient, defineChain } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { ImmutableERC721Abi } from '@imtbl/contracts';

const PRIVATE_KEY = 'YOUR_PRIVATE_KEY'; // should be read from environment variable
const CONTRACT_ADDRESS = '0xYOUR_CONTRACT_ADDRESS'; // should be of type `0x${string}`
const TOKEN_IDS = [BigInt(1), BigInt(2)];

const immutableTestnet = defineChain({
id: 13473,
name: 'imtbl-zkevm-testnet',
nativeCurrency: { name: 'IMX', symbol: 'IMX', decimals: 18 },
rpcUrls: {
default: {
http: ['https://rpc.testnet.immutable.com'],
},
},
});

const walletClient = createWalletClient({
chain: immutableTestnet,
transport: http(),
account: privateKeyToAccount(`0x${PRIVATE_KEY}`),
});

const contract = getContract({
address: CONTRACT_ADDRESS,
abi: ImmutableERC721Abi,
client: walletClient,
});

const burnBatch = async (tokenIds: bigint[]): Promise<string> => {
const txHash = await contract.write.burnBatch([tokenIds]);

console.log(`txHash: ${txHash}`);
return txHash;
};

burnBatch(TOKEN_IDS);

Task 4: Obtain type safety using the TypeChain library

Constructing the contract interface using the ABI is not type-safe. To make it type-safe, we can use Typechain to generate typescript interfaces from the contract ABI. You should do this following both a transfer and a burn process.

The contract ABI could be stored or exported to a file and then used to generate the typescript interfaces.

typechain --target=ethers-v5 -out-dir=app/contracts abis/ERC721.json

Here's how you create a contract instance to use type-safe methods. This function returns a contract factory on which you can call the safeTransferFrom method:

import { Signer } from 'ethers'; // ethers v5
import { ERC721_factory, ERC721 } from './contracts';

const contractInstance = async (signer: Signer) => {
const CONTRACT_ADDRESS = '';

const contract: ERC721 = ERC721_factory.connect(
CONTRACT_ADDRESS,
signer,
);

return contract;

// You can call the `safeTransferFrom` method on this contract:
// contract.safeTransferFrom(sender, RECIPIENT, TOKEN_ID);
};