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.
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
Concepts | Definition |
---|---|
Crafting | Crafting 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. |
Burning | Burning 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. |
Transferring | Assets 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:
|
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
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
- yarn
npm install -D @imtbl/sdk
# if necessary, install dependencies
npm install -D typescript ts-node
yarn add --dev @imtbl/sdk
# if necessary, install dependencies
yarn add --dev typescript ts-node
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:
- npm
- yarn
rm -Rf node_modules
npm cache clean --force
npm i
rm -Rf node_modules
yarn cache clean
yarn install
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.
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.
- Passport
- Browser
- Private key
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();
import {ethers} from "ethers"; // ethers v5
import CodeBlock from '@theme/CodeBlock';
const browserWallet = new ethers.providers.Web3Provider(window.ethereum);
Then, create a signer from the wallet connection:
const signer = browserWallet.getSigner();
import { getDefaultProvider, Wallet } from "ethers"; // ethers v5
const rpcProvider = getDefaultProvider("https://rpc.testnet.immutable.com/")
const signer = new Wallet(PRIVATE_KEY, rpcProvider);
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:
Parameter | Description |
---|---|
signer | From the step above |
CONTRACT_ADDRESS | The address of the contract |
RECIPIENT | Address of the wallet that will be the new owner of the NFT when the transfer is completed |
TOKEN_ID | Unique ID of the NFT |
2.1 Transfer a single asset
Find a more detailed explanation about how to interact with your deployed collection here.
- Viem
- Ethers.js
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);
import { ethers, Wallet, getDefaultProvider } from 'ethers'; // ethers v5
import 'dotenv/config';
async function main() {
const private_key = process.env.PRIVATE_KEY;
const rpcProvider = getDefaultProvider("https://rpc.testnet.immutable.com/");
const wallet = new Wallet(private_key!, rpcProvider);
// Transfer details
const CONTRACT_ADDRESS = '';
const RECIPIENT = '';
const TOKEN_ID = '';
const SENDER = await wallet.getAddress();
const contract = new ethers.Contract(
CONTRACT_ADDRESS,
[
'function safeTransferFrom(address from, address to, uint256 tokenId)',
],
wallet
);
// The network has a minimum gas price of 100 Gwei to protect it against SPAM traffic
const gasOverrides = {
maxPriorityFeePerGas: 10e9, // 100 Gwei
maxFeePerGas: 15e9,
gasLimit: 200000, // Set an appropriate gas limit for your transaction
};
try {
const tx = await contract.safeTransferFrom(SENDER, RECIPIENT, TOKEN_ID, gasOverrides);
await tx.wait();
console.log('Token transferred successfully:', tx);
} catch (error) {
console.error('Error transferring token:', error);
}
}
main();
Under the hood, ethers
will build a eth_sendTransaction
RPC call to the Passport provider:
{
"data": "0x42842e0e...the rest of encoded the data",
"to": "<address of the ERC-721 contract>",
"from": "<your wallet address>"
}
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.
Find a more detailed explanation about how to interact with your deployed collection here.
- Viem
- Ethers.js
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);
import { ethers, Signer } from 'ethers'; // ethers v5
const contractInstance = async (signer: Signer) => {
const CONTRACT_ADDRESS = '';
return new ethers.Contract(
CONTRACT_ADDRESS,
[
'function safeTransferFromBatch(tuple(address from, address[] tos, uint256[] tokenIds))',
],
signer,
);
};
const batchTransfer = async (signer: Signer) => {
const RECIPIENTS = [''];
const TOKEN_IDS = [''];
const sender = await signer.getAddress()
const contract = await contractInstance(signer);
const transfers = {
from: sender,
tos: RECIPIENTS,
tokenIds: TOKEN_IDS,
};
const transaction = contract.safeTransferFromBatch(transfers);
return transaction.wait();
}
Under the hood, ethers
will build a eth_sendTransaction
RPC call to the Passport provider:
{
"data": "0x42842e0e...the rest of encoded the data",
"to": "<address of the ERC-721 contract>",
"from": "<your wallet address>"
}
Task 3. Burn assets
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:
Parameter | Description |
---|---|
signer | From the step above |
CONTRACT_ADDRESS | The address of the contract |
TOKEN_IDS | Unique IDs of the NFTs to burn |
Find a more detailed explanation about how to interact with your deployed collection here.
- Viem
- Ethers.js
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);
import { ethers, Signer } from 'ethers'; // ethers v5
const contractInstance = async (signer: Signer) => {
const CONTRACT_ADDRESS = '';
return new ethers.Contract(
CONTRACT_ADDRESS,
[
'function burnBatch(uint256[] tokenIds)',
],
signer,
);
};
const batchTransfer = async (signer: Signer) => {
const TOKEN_IDS = ['', ''];
const sender = await signer.getAddress()
const contract = await contractInstance(signer);
const transaction = contract.burnBatch(TOKEN_IDS);
return transaction.wait();
}
Under the hood, ethers
will build a eth_sendTransaction
RPC call to the Passport provider:
{
"data": "0x42842e0e...the rest of encoded the data",
"to": "<address of the ERC-721 contract>",
"from": "<your wallet address>"
}
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);
};