Batch Minting
On this page, we explore the ways minting in batches reduces gas fees and network congestion. You will learn what game design scenarios are best suited to minting in batches and how to perform a batch mints using the tools provided by Immutable's preset contracts.
What is batch minting?
Batch minting refers to the process of creating multiple non-fungible tokens (NFTs) or semi-fungible tokens (SFTs) in a single transaction on the blockchain. Traditionally, minting each token_id
requires a separate transaction, which can be time-consuming and costly in terms of gas fees. Batch minting streamlines this process by allowing the simultaneous creation of multiple token_ids in one transaction, thereby reducing both the number of transactions and the associated costs.
This approach is especially beneficial for game studios that need to mint large quantities of tokens at once, such as when launching a new collection, distributing rewards, or provisioning in-game assets to multiple users. By batching these operations, studios can minimize the costs and time related to gas fees and transaction confirmations, leading to a more efficient and scalable process.
Batch minting functions require all assets within a batch to belong to the same collection.
How Does Batch Minting Save Gas?
Immutable’s batch minting techniques are designed to optimize gas usage by reducing the computational and storage costs associated with blockchain transactions.
ERC721 & ERC1155: Reducing Transactional Overheads
Every blockchain transaction incurs computational and storage costs. Batch minting reduces these costs by consolidating multiple minting actions into a single transaction, which significantly lowers gas fees compared to minting each asset individually. This method leverages economies of scale, allowing the creation of multiple assets with different token_id
s at a lower collective gas fee.
See below for the batch functions offered by Immutable's preset contracts designed to reduce transactional overheads.
ERC1155: Efficient Blockchain Storage for SFTs
In Web3 gaming, where assets are often semi-fungible, ERC1155 collections are typically more suitable. In scenarios where a player might hold multiple identical assets, ERC1155 consumes less storage because a single token_id
can represent more than a single asset.
For example, if a player owns five identical swords, ERC1155 would record this with one token_id
and a quantity of five, whereas ERC721 would require five separate entries, each with its own token_id
. This makes ERC1155 more gas-efficient for handling semi-fungible assets in games.
ERC721: Minimizing Blockchain Data Storage
Each NFT minting operation adds data to the blockchain, which incurs gas costs. The mintBatchByQuantity()
function is designed to require less on-chain data storage, so requires less gas.
For example, traditional ERC721 contracts store data for each NFT's identity and ownership. In a standard ERC721 mint operation, each NFT's token_id
and account_address
(owner's wallet address) must be recorded on-chain.
Immutable's recommended ERC721 Preset contains the mintBatchByQuantity()
function, which reduces blockchain data usage by attributing one account_address
to multiple NFTs minted to the same wallet. In this method, the first NFT of a batch has a defined account_address
, while subsequent NFTs in the batch show a null value for account_address
, indicating they belong to the same owner as the first. The next populated account_address
signals the beginning of a new batch of NFTs owned by a different wallet. This approach reduces on-chain data and, consequently, lowers gas costs.
This structure is illustrated below:
While the explanation above conceptually illustrates how the data structure works, Immutable's recommended ERC721 Preset actually uses a more efficient method involving bitmaps and a de Bruijn sequence for locating account_address
data. This approach is optimized beyond the sequential checking of previous NFTs.
When to use batch minting functions
Batch minting is the recommended approach when a player or game studio plans to create multiple assets. In all cases, batch minting multiple assets with different token_id
incurs lower gas costs than individually minting each asset in separate transactions.
The following gaming scenarios are suited to batch minting:
- Primary Sales: Primary sales refer to the initial offerings of digital assets, often in the form of NFTs/SFTs, directly from the creator or issuer to buyers. When a player buys more than 1 item from a collection, batch minting can lower the cost of the transaction.
- New Player Assets: When a new player signs up to a game they are often allocated a starter kit of assets that can engage with the game. Batch minting these assets to the player wallet can lower client acquisition costs, whilst still giving new players the opportunity to own Web3 assets.
- Event Giveaways: Event giveaways involve players completing studio-dictated tasks for rewards. These range from in-game tournaments to signing up for services by specific dates. Batch minting can be used to efficiently allocate assets to a large user group at predetermined point in time (i.e. when a tournament ends at Sunday midnight), curbing costs for game studios.
- Player Engagement: Games often motivate player engagement through asset rewards for achievements like winning matches or completing levels. When these rewards involve minting multiple assets from the same collection, batch minting becomes a cost-effective approach. This empowers studios to provide more assets, fostering increased player engagement.
- Loot boxes: Loot boxes are purchasable or earnable virtual items in video games. When opened, they yield randomized in-game items like characters, weapons, cosmetic skins, and more. While loot boxes are often minted individually, opening a loot box grants players multiple assets simultaneously, making it an ideal scenario for leveraging the efficiency of batch minting functions.
The amount of gas saving by using batch minting techniques can be further increased by strategically delaying NFT/SFT minting to enable the minting of larger batches.
How to batch mint
A game can batch mint either via a direct function call or through Immutable's Minting API.
Minting by function call is the most gas-efficient method and will be discussed in more detail in this section.
Minting via API is significantly easier to execute at scale but comes with additional cost overheads for the convenience (the Minting API contract requires additional gas for the services it performs). Currently, all Minting API requests are gas-free, though this policy may change in the future.
Immutable's ERC1155 Preset Contract
ERC1155 standard contract includes batch mining with the safeMintBatch()
function.
To use this function, the mint requestor must include a token_id
to specify the new token they wish to create, or add additional tokens to an existing token_id
.
To mint a batch of assets perform the following steps:
Get some test $IMX. To get test $IMX you will need to create a Immutable Hub account and go to this faucet. Enter your wallet's address in the text field requesting it and then click the Receive Test-IMX button. It may take a few minutes for the test $IMX to arrive, after which, your test $IMX balance will be visible in your wallet.
Deploy Immutable's ERC1155 preset contract.
- Technical details of the contract can be found here
- Deploy the ERC1155 contract manually via the command line, or use Immutable's Hub(https://hub.immutable.com/) to deploy a ERC1155 collection.
Ensure you have enough test $IMX in your wallet to deploy the intended batch of SFTs.
Create a file called
mintBatch1155.ts
Add the following code to your file with the following variables:
CONTRACT_ADDRESS
- The address of the deployedERC1155
contractPRIVATE_KEY
- The private key of the account used for signing the mint requestACCOUNT_ADDRESS1
- The first address of the wallet that will own the SFTs being minted in the batchACCOUNT_ADDRESS2
- The second address of the wallet that will own the SFTs being minted in the batchTOKEN_ID1
- A free token ID in the collectionTOKEN_ID2
- A free token ID in the collection
Find a more detailed explanation about how to interact with your deployed collection here.
import { getContract, http, createWalletClient, defineChain, ByteArray, toBytes } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { ImmutableERC1155Abi } 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 RECEIVER = '0xRECEIVER_ADDRESS'; // should be of type `0x${string}`
const TOKEN_IDS: bigint[] = [BigInt(1), BigInt(2)]; // should be of type BigInt
const TOKEN_VALUES: bigint[] = [BigInt(1), BigInt(1)]; // should be of type BigInt
const DATA = '0x'; // should be of type `0x${string}`
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: ImmutableERC1155Abi,
client: walletClient,
});
const safeMintBatch = async (): Promise<void> => {
await contract.write.safeMintBatch([RECEIVER, TOKEN_IDS, TOKEN_VALUES, DATA]);
};
safeMintBatch();
- Run the following script:
./node_modules/.bin/ts-node mintBatch1155.ts
Immutable's ERC721 Preset Contract
Immutable's recommended ERC721 Preset offers a versatile solution for batch minting, allowing game studios to choose the most suitable minting strategy for each use-case, while keeping all assets within the same collection. The contract contains the following batch minting functions:
- mintBatch(): Facilitates gas-efficient batch minting where the minter can specify
token_id
for new assets. - mintBatchByQuantity(): Enables sequential batch minting where the system automatically generates the next
token_id
, offering the most gas-efficient option.
To support these batch minting functions, the contract reserves distinct token_id
ranges for each function. By default, token_id
s below 2^128 (up to 340,282,366,920,938,463,463,374,607,431,768,211,455) are reserved for the mintBatch()
or mint()
functions. The mintBatchByQuantity()
function uses token_id
s higher than this threshold. The attribute mintBatchByQuantityThreshold
controls the token_id
range reserved for each minting function.
Batch Minting with Immutable's ERC721 Preset Contract
- ERC721: mintBatch()
- ERC721: mintBatchByQuantity()
- ERC721: Changing the mintBatchByQuantityThreshold
Immutable's preset contracts enables one to mint multiple NFTs to multiple wallets.
The mintBatch()
function does not need to be called separately for each receiving wallet.
This function is most gas efficent when minting 1-2 NFTs to each wallet in a batch.
Good For
- Migrations: Ideal for migrating NFTs across chains or collections where the
token_id
needs to remain consistent. - Crafting: Useful for player-initiated crafting requests, where specifying the
token_id
upfront can aid in preparing metadata and managing error handling. - Token ID Categorization: Necessary when the
token_id
must represent specific asset types (e.g., Token ID: 1-10,000 = Swords; Token ID: 10,001-20,000 = Shields).
Supported Preset Contracts
How to Batch Mint with mintBatch()
To mint a batch of assets perform the following steps:
Get some test $IMX.
To get test $IMX you will need to create a Immutable Hub account and go to this faucet. Enter your wallet's address in the text field requesting it and then click the Receive Test-IMX button. It may take a few minutes for the test $IMX to arrive, after which, your test $IMX balance will be visible in your wallet.
Ensure you have enough test $IMX in your wallet to deploy the intended batch of NFTs.
Create a file called
mintNFTsByID.ts
Add the following code to your file with the following variables:
CONTRACT_ADDRESS
- The address of the deployedERC721
contractPRIVATE_KEY
- The private key of the account used for signing the mint requestACCOUNT_ADDRESS1
- The first address of the wallet that will own the NFTs being minted in the batchACCOUNT_ADDRESS2
- The second address of the wallet that will own the NFTs being minted in the batchTOKEN_ID1
- A free token ID in the collection that is less than 18,446,744,073,709,551,616TOKEN_ID2
- A free token ID in the collection that is less than 18,446,744,073,709,551,616TOKEN_ID3
- A free token ID in the collection that is less than 18,446,744,073,709,551,616TOKEN_ID4
- A free token ID in the collection that is less than 18,446,744,073,709,551,616
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 = '0xYOUR_PRIVATE_KEY'; // should be read from environment variable, should be of type `0x${string}`
const CONTRACT_ADDRESS = '0xYOUR_CONTRACT_ADDRESS'; // should be of type `0x${string}`
const TOKEN_ID_1 = BigInt(1);
const TOKEN_ID_2 = BigInt(2);
const TOKEN_ID_3 = BigInt(3);
const TOKEN_ID_4 = BigInt(4);
const ACCOUNT_ADDRESS_1: `0x${string}` = '0xACCOUNT_ADDRESS_1'; // should be of type `0x${string}`
const ACCOUNT_ADDRESS_2: `0x${string}` = '0xACCOUNT_ADDRESS_2'; // should be of type `0x${string}`
const REQUESTS = [
{
to: ACCOUNT_ADDRESS_1,
tokenIds: [TOKEN_ID_1, TOKEN_ID_2],
},
{
to: ACCOUNT_ADDRESS_2,
tokenIds: [TOKEN_ID_3, TOKEN_ID_4],
},
];
export const batchMintERC721ByID = async (
privateKey: `0x${string}`,
contractAddress: `0x${string}`,
requests: {
to: `0x${string}`;
tokenIds: bigint[];
}[],
): Promise<string> => {
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(privateKey),
});
// Bound contract instance
const contract = getContract({
address: contractAddress,
abi: ImmutableERC721Abi,
client: walletClient,
});
// We can use the read function hasRole to check if the intended signer
// has sufficient permissions to mint before we send the transaction
const minterRole = await contract.read.MINTER_ROLE();
const hasMinterRole = await contract.read.hasRole([
minterRole,
walletClient.account.address,
]);
if (!hasMinterRole) {
// Handle scenario without permissions...
console.log('Account doesnt have permissions to mint.');
return Promise.reject(
new Error('Account doesnt have permissions to mint.'),
);
}
const txHash = await contract.write.mintBatch([requests]);
console.log(`txHash: ${txHash}`);
return txHash;
};
batchMintERC721ByID(PRIVATE_KEY, CONTRACT_ADDRESS, REQUESTS);
- Run the following script:
./node_modules/.bin/ts-node mintNFTsByID.ts
The mintBatchByQuantity()
method is most gas-efficient when minting more than 2 NFTs to each wallet.
Due to its design features, utilising mintBatchByQuantity()
for minting over approximately 80,000 tokens within the collection will result in incomplete results from the balanceOf()
function.
Should you require concurrent use of both mintBatchByQuantity()
and the balanceOf()
function, please contact your Immutable representative.
Good For
For use cases where gas efficiency is a priority, mintBatchByQuantity() is highly advantageous, offering up to 65% more gas efficiency than single mint requests when minting multiple NFTs to a single address:
- Minting Multiple NFTs Per Wallet: Achieve significant gas savings when batch minting multiple NFTs to each wallet (e.g., 500 NFTs to 5 addresses).
- Primary Sales: Selling 10 NFTs to a player in a primary sale.
- New Player Assets: Providing 20 basic NFTs to new players.
- Event Giveaways: Awarding many assets to many players as a result of player's performance in set tasks.
- Player Engagement: Granting players 2 or more NFTs upon level or match completion.
- Loot boxes: Opening loot boxes to receive multiple items (e.g. 5 items) to a single wallet.
Supported Preset Contracts
How to Batch Mint with mintBatchByQuantity()
To mint a batch of assets perform the following steps:
Get some test $IMX.
To get test $IMX you will need to create a Immutable Hub account and go to this faucet. Enter your wallet's address in the text field requesting it and then click the Receive Test-IMX button. It may take a few minutes for the test $IMX to arrive, after which, your test $IMX balance will be visible in your wallet.
Ensure you have enough test $IMX in your wallet to deploy the intended batch of NFTs.
Create a file called
mintNFTsByQuantity.ts
Add the following code to your file with the following variables:
CONTRACT_ADDRESS
- The address of the deployedERC721
contractPRIVATE_KEY
- The private key of the account used for signing the mint requestACCOUNT_ADDRESS1
- The first address of the wallet that will own the NFTs being minted in the batchACCOUNT_ADDRESS2
- The second address of the wallet that will own the NFTs being minted in the batch
import { getContract, http, createWalletClient, defineChain } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { ImmutableERC721Abi } from '@imtbl/contracts';
const PRIVATE_KEY = '0xYOUR_PRIVATE_KEY'; // should be read from environment variable, should be of type `0x${string}`
const CONTRACT_ADDRESS = '0xYOUR_CONTRACT_ADDRESS'; // should be of type `0x${string}`
const ACCOUNT_ADDRESS_1: `0x${string}` = '0xACCOUNT_ADDRESS_1'; // should be of type `0x${string}`
const ACCOUNT_ADDRESS_2: `0x${string}` = '0xACCOUNT_ADDRESS_2'; // should be of type `0x${string}`
const MINTS = [
{
to: ACCOUNT_ADDRESS_1,
quantity: BigInt(3),
},
{
to: ACCOUNT_ADDRESS_2,
quantity: BigInt(3),
},
];
export const batchMintERC721ByQuantity = async (
privateKey: `0x${string}`,
contractAddress: `0x${string}`,
mints: {
to: `0x${string}`;
quantity: bigint;
}[],
): Promise<string> => {
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(privateKey),
});
// Bound contract instance
const contract = getContract({
address: contractAddress,
abi: ImmutableERC721Abi,
client: walletClient,
});
// We can use the read function hasRole to check if the intended signer
// has sufficient permissions to mint before we send the transaction
const minterRole = await contract.read.MINTER_ROLE();
const hasMinterRole = await contract.read.hasRole([
minterRole,
walletClient.account.address,
]);
if (!hasMinterRole) {
// Handle scenario without permissions...
console.log('Account doesnt have permissions to mint.');
return Promise.reject(
new Error('Account doesnt have permissions to mint.'),
);
}
const txHash = await contract.write.mintBatchByQuantity([mints]);
console.log(`txHash: ${txHash}`);
return txHash;
};
batchMintERC721ByQuantity(PRIVATE_KEY, CONTRACT_ADDRESS, MINTS);
- Run the following script:
./node_modules/.bin/ts-node mintNFTsByQuantity.ts
Partner's who are able to deploy their own smart contracts on Immutable's zkEVM will be able to override mintBatchByQuantityThreshold
method to adjust the Token ID threshold reserved for mintBatchByQuantity()
.
This can be achieved through deploying the below contract.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import '@imtbl/zkevm-contracts/contracts/token/erc721/preset/ImmutableERC721.sol';
contract MyERC721Contract is ImmutableERC721 {
constructor(
address owner,
string memory name,
string memory symbol,
string memory baseURI,
string memory contractURI,
address operatorAllowlist,
address receiver,
uint96 feeNumerator
)
ImmutableERC721(
owner,
name,
symbol,
baseURI,
contractURI,
operatorAllowlist,
receiver,
feeNumerator
)
{}
// Bulk mint threshold will start from ID 1
function mintBatchByQuantityThreshold() public pure override returns (uint256) {
return 1;
}
}
Custom Contracts
Batch minting is possible with custom contacts, but it is essential to ensure that the contract is compatible with Immutable's batch minting methods and the Operator Allowlist is implemented correctly.
To make sure that creators receive their hard earned loyalties, Immutable requires that all collections of ERC-721/1155s deployed on Immutable zkEVM implement the Operator Allowlist. Not implementing will have these effects for your collection (i.e. ERC-721/1155s):
- You may forfeit any token grants you may have received.
- Passport users will receive a warnings that your collection is likely to be counterfeit.
- Your collection will not appear on any of the 3rd party marketplaces in our ecosystem.
For more information on the Operator Allowlist, please see this guide.
Verify successful mints
Blockscout's Immutable zkEVM testnet explorer can be used to verify your contract has been deployed to the network.
Entering the CONTRACT_ADDRESS
in the search bar will allow you to view transactions by collection. The mint requests you generated will produce a Contract Call
record.
Using the below code example you can verify whether the minting request was successful by checking for mint activities belonging to that CONTRACT_ADDRESS
on the listActivities
API endpoint.
The code example relies on the following variable:
CONTRACT_ADDRESS
- The address of the deployedERC721
contract
import { blockchainData } from '@imtbl/sdk';
import { client } from '../lib';
export async function verifySuccessfulMints(
contractAddress: string,
): Promise<blockchainData.Types.ListActivitiesResult> {
return await client.listActivities({
chainName: 'imtbl-zkevm-testnet',
contractAddress,
activityType: blockchainData.Types.ActivityType.Mint,
});
}
Other Batch Transactions
Batching other collection transactions can also lead to significant gas savings, similar to those achieved with batch minting.
Immutable's ERC721 & ERC1155 preset contracts include built-in functionality for transferring or burning multiple assets within a single transaction. This approach not only reduces gas costs but also streamlines operations for game studios managing large numbers of assets.
For detailed instructions on how to perform these operations in batches and maximize gas efficiency, check out our transfer and burn tutorials.