Skip to main content
Every Passport user has an embedded wallet on Immutable Chain. This guide covers wallet operations for developers integrating Passport into games and applications.
For end users: Players can manage their wallet, view balances, and browse transaction history at Immutable Play. Direct your users there for wallet management features.

Overview

This page covers essential wallet operations including retrieving wallet addresses, checking native IMX and ERC-20 token balances, sending transactions (transfers, contract interactions, NFTs), signing messages (ERC-191 personal sign and EIP-712 typed data), and error handling patterns.

Prerequisites

Get Wallet Address

// First, initialize the zkEVM provider
const provider = await connectWallet({ auth });

// Then get the wallet address
const accounts = await provider.request({ method: 'eth_requestAccounts' });
const address = accounts[0];

console.log('Wallet address:', address);

Linked Addresses

Users can link external wallets (like MetaMask, WalletConnect, etc.) to their Passport account, allowing them to use the same identity across multiple wallets.
import { getLinkedAddresses } from '@imtbl/wallet';
import { config } from '@imtbl/sdk';

// Initialize API client
const apiClient = new config.ImmutableConfiguration({
  environment: config.Environment.SANDBOX,
}).getMultiRollupApiClients();

// Get all wallets linked to the current Passport account
const linkedAddresses = await getLinkedAddresses(auth, apiClient);

console.log('Linked addresses:', linkedAddresses);
// Returns: ['0x123...', '0x456...']

Check Balances

Native IMX and ERC-20 Token Balances
import { BrowserProvider, formatEther, Contract } from 'ethers';

const provider = connectWallet({ auth });

const accounts = await provider.request({ method: 'eth_requestAccounts' });
const address = accounts[0];

const balance = await provider.request({
  method: 'eth_getBalance',
  params: [address, 'latest']
});

console.log('IMX Balance:', formatEther(balance), 'IMX');

// Get ERC-20 token balance
const erc20Abi = [
  'function balanceOf(address owner) view returns (uint256)',
  'function decimals() view returns (uint8)',
  'function symbol() view returns (string)',
];

const tokenAddress = '0x...'; // Your ERC-20 token contract address
const ethersProvider = new BrowserProvider(provider);
const tokenContract = new Contract(tokenAddress, erc20Abi, ethersProvider);

const tokenBalance = await tokenContract.balanceOf(address);
const decimals = await tokenContract.decimals();
const symbol = await tokenContract.symbol();

console.log(`Token Balance: ${formatEther(tokenBalance)} ${symbol}`);

Send Transactions

Wei Conversion: 1 IMX = 10^18 wei. For TypeScript, use parseUnits('1', 18) from ethers or viem. Unity/Unreal require string values in wei.

Send Transaction with Confirmation

Recommended for critical operations - Waits for blockchain confirmation before returning.
import { BrowserProvider } from 'ethers';

// Get the provider from Passport
const provider = await connectWallet({ auth });

// Wrap provider in ethers BrowserProvider
const browserProvider = new BrowserProvider(provider);

// Get the signer (represents the user's wallet)
const signer = await browserProvider.getSigner();

// Send transaction using signer
const tx = await signer.sendTransaction({
  to: '0xRecipient...',
  value: '1500000000000000000', // 1.5 IMX in wei
});

// Wait for blockchain confirmation
const receipt = await tx.wait();

console.log('Transaction confirmed!');
console.log('Hash:', receipt.hash);
console.log('Status:', receipt.status === 1 ? 'Success' : 'Failed');
When to use: Critical operations like transfers, mints, or state changes. Ensures transaction succeeded before updating game state.
import { BrowserProvider, ethers } from 'ethers';

// Setup provider and signer
const provider = await connectWallet({ auth });
const browserProvider = new BrowserProvider(provider);
const signer = await browserProvider.getSigner();

// Define contract ABI (just the functions you need)
const abi = [
  'function safeTransferFrom(address from, address to, uint256 tokenId)'
];

// Create contract instance
const contract = new ethers.Contract(
  '0xYourNFTContract...', // Contract address
  abi,
  signer // Signer enables sending transactions
);

// Call contract function - automatically sends transaction
const tx = await contract.safeTransferFrom(
  userAddress,
  recipientAddress,
  tokenId
);

// Wait for confirmation
const receipt = await tx.wait();

console.log('NFT transferred!', receipt.hash);

Send Transaction without Confirmation

For fire-and-forget operations - Returns transaction hash immediately without waiting.
import { BrowserProvider } from 'ethers';

// Get the provider from Passport
const provider = await connectWallet({ auth });

// Wrap provider in ethers BrowserProvider
const browserProvider = new BrowserProvider(provider);

// Get the signer (represents the user's wallet)
const signer = await browserProvider.getSigner();

// Send transaction - returns immediately without waiting
const tx = await signer.sendTransaction({
  to: '0xRecipient...',
  value: '1500000000000000000', // 1.5 IMX in wei
});

console.log('Transaction sent:', tx.hash);
// User can continue - transaction confirms in background
Fire-and-Forget: Returns immediately without waiting for blockchain confirmation. Use for non-critical operations where you want responsive UI (cosmetic purchases, achievements, analytics).
Transaction Hash ≠ Success: Obtaining the transaction hash does not guarantee a successful transaction. To determine the transaction’s status, use ZkEvmGetTransactionReceipt along with the transaction hash received. Follow best practices for client-side polling: set maximum attempts, polling intervals, and implement timeout handling.
When to use: Non-critical operations or when you need custom polling/retry logic. Most games should use “with confirmation” variant for critical operations.

NFT Transfer

const ERC721_ABI = [{
  name: 'transferFrom',
  type: 'function',
  inputs: [
    { name: 'from', type: 'address' },
    { name: 'to', type: 'address' },
    { name: 'tokenId', type: 'uint256' },
  ],
  outputs: [],
  stateMutability: 'nonpayable',
}] as const;

export async function transferNFT(
  nftContract: `0x${string}`,
  from: `0x${string}`,
  to: `0x${string}`,
  tokenId: bigint
) {
  // highlight-start
  const hash = await walletClient.writeContract({
    account: from,
    address: nftContract,
    abi: ERC721_ABI,
    functionName: 'transferFrom',
    args: [from, to, tokenId],
  });
  // highlight-end

  const receipt = await publicClient.waitForTransactionReceipt({ hash });
  console.log('NFT transferred! Block:', receipt.blockNumber);
  return receipt;
}

Sign Messages

Personal Sign (ERC-191)

For authentication or simple message signing:
// Assuming you have the Passport provider from connectEvm()
const provider = connectWallet({ auth });

const accounts = await provider.request({ method: 'eth_requestAccounts' });
const address = accounts[0];
const message = 'Hello from Immutable!';

// Sign the message using personal_sign (ERC-191)
const signature = await provider.request({
  method: 'personal_sign',
  params: [message, address],
});

console.log('Signature:', signature);

Typed Data (EIP-712)

For structured data signing (used by protocols like Seaport):
// Assuming you have the Passport provider from connectEvm()
const provider = connectWallet({ auth });

const accounts = await provider.request({ method: 'eth_requestAccounts' });
const address = accounts[0];

// Get chain ID
const chainIdHex = await provider.request({ method: 'eth_chainId' });
const chainId = parseInt(chainIdHex, 16);

// Define EIP-712 typed data structure
const typedData = {
  domain: {
    name: 'My Game',
    version: '1',
    chainId,
    verifyingContract: address, // Your contract address
  },
  message: {
    itemId: 123,
    price: '1000000000000000000', // 1 token in wei
  },
  primaryType: 'Trade',
  types: {
    EIP712Domain: [
      { name: 'name', type: 'string' },
      { name: 'version', type: 'string' },
      { name: 'chainId', type: 'uint256' },
      { name: 'verifyingContract', type: 'address' },
    ],
    Trade: [
      { name: 'itemId', type: 'uint256' },
      { name: 'price', type: 'uint256' },
    ],
  },
};

// Sign typed data using eth_signTypedData_v4
const signature = await provider.request({
  method: 'eth_signTypedData_v4',
  params: [address, JSON.stringify(typedData)],
});

console.log('Typed data signature:', signature);

Error Handling

// Assuming you have the Passport provider from connectEvm()
const provider = connectWallet({ auth });

async function sendTransactionWithErrorHandling() {
  try {
    const accounts = await provider.request({ method: 'eth_requestAccounts' });
    const fromAddress = accounts[0];

    const hash = await provider.request({
      method: 'eth_sendTransaction',
      params: [{
        from: fromAddress,
        to: '0xRecipient...',
        value: '1000000000000000000', // 1 IMX in wei
      }],
    });

    console.log('Transaction sent:', hash);
    return { success: true, hash };
  } catch (error) {
    // Handle different error cases
    if (error instanceof Error) {
      if (error.message.includes('User rejected')) {
        console.log('User rejected the transaction');
        return { success: false, error: 'User rejected' };
      }
      if (error.message.includes('insufficient funds')) {
        console.error('Insufficient balance');
        return { success: false, error: 'Insufficient funds' };
      }
      console.error('Transaction error:', error.message);
      return { success: false, error: error.message };
    }
    throw error;
  }
}

Next Steps