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
User Authentication
User must be authenticated with Passport before using wallet operations
Web vs Native: Some features like pre-approved transactions require Unity/Unreal native clients and cannot be used in web browsers.
Wallet Connect
Connect the EVM provider and request account access. Required for all wallet operations including Checkout wallet funding and Orderbook trading operations.- Next.js
- TypeScript
- Unity
- Unreal
Pass
getUser from useImmutableSession to connectWallet so the wallet uses your server-managed session instead of its own OAuth flow.Copy
Ask AI
'use client';
import { useImmutableSession } from '@imtbl/auth-next-client';
import { connectWallet, type ZkEvmProvider } from '@imtbl/wallet';
import { useState } from 'react';
function WalletConnect() {
const { isAuthenticated, getUser } = useImmutableSession();
const [address, setAddress] = useState<string | null>(null);
async function handleConnect() {
const provider = await connectWallet({
getUser: isAuthenticated ? getUser : undefined,
clientId: process.env.NEXT_PUBLIC_IMMUTABLE_CLIENT_ID!,
}) as ZkEvmProvider;
const accounts = await provider.request({ method: 'eth_requestAccounts' });
setAddress(accounts[0]);
}
return (
<div>
{address ? (
<p>Wallet connected: {address}</p>
) : (
<button onClick={handleConnect}>Connect Wallet</button>
)}
</div>
);
}
When
getUser is provided, the wallet skips its own OAuth flow and relies on the server-managed session. This avoids duplicate login prompts and keeps token refresh in one place. Pass undefined when the user is not authenticated to allow the wallet to use its own login flow.Copy
Ask AI
// Assuming Passport is initialized and user is logged in
// 1. Connect EVM provider
const provider = passportInstance.connectEvm();
// 2. Request accounts (connect wallet)
const accounts = await provider.request({ method: 'eth_requestAccounts' });
const address = accounts[0];
console.log('Wallet connected:', address);
Copy
Ask AI
// Assuming Passport is initialized and user is logged in
// 1. Connect EVM provider
await passport.ConnectEvm();
// 2. Request accounts (connect wallet)
var accounts = await passport.ZkEvmRequestAccounts();
string address = accounts[0];
Debug.Log($"Wallet connected: {address}");
Call
ConnectEvm() once after login, then use ZkEvmRequestAccounts() to get the wallet address. This initializes the zkEVM provider for all subsequent wallet operations.Copy
Ask AI
// Add to your subsystem:
public:
void ConnectWallet()
{
auto Passport = GetPassport();
if (!Passport)
{
return;
}
// 1. Connect EVM provider
Passport->ConnectEvm(UImmutablePassport::FImtblPassportResponseDelegate::CreateUObject(this, &UPassportQuickStartSubsystem::OnWalletEvmConnected));
}
private:
void OnWalletEvmConnected(FImmutablePassportResult Result)
{
if (!Result.Success)
{
UE_LOG(LogTemp, Error, TEXT("ConnectEvm Failed: %s"), *Result.Error);
return;
}
auto Passport = GetPassport();
if (!Passport)
{
return;
}
// 2. Request accounts (connect wallet)
Passport->ZkEvmRequestAccounts(UImmutablePassport::FImtblPassportResponseDelegate::CreateUObject(this, &UPassportQuickStartSubsystem::OnWalletAccountsReceived));
}
void OnWalletAccountsReceived(FImmutablePassportResult Result)
{
if (!Result.Success)
{
UE_LOG(LogTemp, Error, TEXT("Request Accounts Failed: %s"), *Result.Error);
return;
}
const auto& RequestAccountsData = FImmutablePassportZkEvmRequestAccountsData::FromJsonObject(Result.Response.JsonObject);
if (!RequestAccountsData.IsSet())
{
UE_LOG(LogTemp, Error, TEXT("Failed to parse accounts data"));
return;
}
const auto& Accounts = RequestAccountsData->accounts;
for (int Index = 0; Index < Accounts.Num(); Index++)
{
UE_LOG(LogTemp, Log, TEXT("Account[%d]: %s"), Index, *Accounts[Index]);
}
}
void OnLoginComplete(FImmutablePassportResult Result)
{
if (!Result.Success) return;
UE_LOG(LogTemp, Log, TEXT("Login Successful"));
ConnectWallet();
}
Follow the sequence: ConnectEvm → ZkEvmRequestAccounts. Use
FImmutablePassportZkEvmRequestAccountsData::FromJsonObject to parse the response.Important: Before connecting the wallet, ensure you have initialized Passport and logged in the user. Then complete these steps:
- Connect EVM provider (
connectEvm()/ConnectEvm()) - Request accounts (
eth_requestAccounts/ZkEvmRequestAccounts())
Next.js: Once you have the provider from
connectWallet({ getUser }) (shown in the Next.js tab above), all wallet operations below work the same way as the TypeScript examples — the provider is the same EIP-1193 compatible ZkEvmProvider. Use the TypeScript tab code with your Next.js provider.Configuration
TheconnectWallet function accepts configuration options to customize the wallet provider.
Custom Client ID
Copy
Ask AI
const provider = await connectWallet({
clientId: 'your-client-id',
});
Chain Selection
Copy
Ask AI
import {
connectWallet,
IMMUTABLE_ZKEVM_MAINNET_CHAIN,
IMMUTABLE_ZKEVM_TESTNET_CHAIN,
} from '@imtbl/wallet';
const provider = await connectWallet({
chains: [IMMUTABLE_ZKEVM_MAINNET_CHAIN],
});
Popup Overlay Options
Copy
Ask AI
const provider = await connectWallet({
popupOverlayOptions: {
disableGenericPopupOverlay: true,
disableBlockedPopupOverlay: true,
},
});
All Options
| Option | Type | Default | Description |
|---|---|---|---|
clientId | string | Auto-detected | Immutable client ID |
chains | ChainConfig[] | [testnet, mainnet] | Chain configurations |
initialChainId | number | First chain | Initial chain to connect to |
popupOverlayOptions | PopupOverlayOptions | - | Options for login popup overlays |
announceProvider | boolean | true | Whether to announce via EIP-6963 |
feeTokenSymbol | string | 'IMX' | Preferred token symbol for relayer fees |
getUser | GetUserFunction | Internal | Custom function for external auth integration |
Using @imtbl/auth (Direct)
For applications that manage authentication outside of Next.js, wrap theAuth class in a getUser function:
Copy
Ask AI
import { connectWallet } from '@imtbl/wallet';
import { Auth } from '@imtbl/auth';
const auth = new Auth({
clientId: 'your-client-id',
redirectUri: 'https://your-app.com/callback',
});
const getUser = async (forceRefresh?: boolean) => {
if (forceRefresh) {
return auth.forceUserRefresh();
}
return auth.getUser();
};
const provider = await connectWallet({ getUser });
Chain Configuration
Chain Constants
Copy
Ask AI
import {
IMMUTABLE_ZKEVM_MAINNET_CHAIN_ID, // 13371
IMMUTABLE_ZKEVM_TESTNET_CHAIN_ID, // 13473
} from '@imtbl/wallet';
Presets
Copy
Ask AI
import {
IMMUTABLE_ZKEVM_MAINNET_CHAIN,
IMMUTABLE_ZKEVM_TESTNET_CHAIN,
DEFAULT_CHAINS,
} from '@imtbl/wallet';
// Testnet only
const provider = await connectWallet({
chains: [IMMUTABLE_ZKEVM_TESTNET_CHAIN],
});
// Mainnet only
const provider = await connectWallet({
chains: [IMMUTABLE_ZKEVM_MAINNET_CHAIN],
});
// Both (default)
const provider = await connectWallet({
chains: DEFAULT_CHAINS,
});
Copy
Ask AI
import {
IMMUTABLE_ZKEVM_MAINNET,
IMMUTABLE_ZKEVM_TESTNET,
IMMUTABLE_ZKEVM_MULTICHAIN,
} from '@imtbl/wallet';
const provider = await connectWallet({
...IMMUTABLE_ZKEVM_MAINNET,
});
Custom Chain Configuration
Copy
Ask AI
import type { ChainConfig } from '@imtbl/wallet';
const customChain: ChainConfig = {
chainId: 13473,
name: 'Immutable zkEVM Testnet',
rpcUrl: 'https://rpc.testnet.immutable.com',
relayerUrl: 'https://api.sandbox.immutable.com/relayer-mr',
apiUrl: 'https://api.sandbox.immutable.com',
passportDomain: 'https://passport.sandbox.immutable.com',
};
Get Wallet Address
- TypeScript
- Unity
- Unreal
Copy
Ask AI
// 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);
Copy
Ask AI
// First, initialize the zkEVM provider (call once after login)
await passport.ConnectEvm();
// Then get the wallet address
var accounts = await passport.ZkEvmRequestAccounts();
string address = accounts[0];
Debug.Log($"Wallet address: {address}");
Copy
Ask AI
// Add to your subsystem:
public:
void GetWalletAddress()
{
auto Passport = GetPassport();
if (!Passport)
{
return;
}
// First, initialize the zkEVM provider (call once after login)
Passport->ConnectEvm(UImmutablePassport::FImtblPassportResponseDelegate::CreateUObject(this, &UPassportQuickStartSubsystem::OnEvmConnected));
}
private:
void OnEvmConnected(FImmutablePassportResult Result)
{
if (!Result.Success)
{
UE_LOG(LogTemp, Error, TEXT("ConnectEvm Failed: %s"), *Result.Error);
return;
}
auto Passport = GetPassport();
if (!Passport)
{
return;
}
// Then get the wallet address
Passport->ZkEvmRequestAccounts(UImmutablePassport::FImtblPassportResponseDelegate::CreateUObject(this, &UPassportQuickStartSubsystem::OnAccountsReceived));
}
void OnAccountsReceived(FImmutablePassportResult Result)
{
if (!Result.Success)
{
UE_LOG(LogTemp, Error, TEXT("Request Accounts Failed: %s"), *Result.Error);
return;
}
const auto& RequestAccountsData = FImmutablePassportZkEvmRequestAccountsData::FromJsonObject(Result.Response.JsonObject);
if (!RequestAccountsData.IsSet())
{
UE_LOG(LogTemp, Error, TEXT("Failed to parse accounts data"));
return;
}
if (RequestAccountsData->accounts.Num() > 0)
{
FString Address = RequestAccountsData->accounts[0];
UE_LOG(LogTemp, Log, TEXT("Wallet Address: %s"), *Address);
}
}
void OnWalletAccountsReceived(FImmutablePassportResult Result)
{
if (!Result.Success) return;
UE_LOG(LogTemp, Log, TEXT("Wallet Connected"));
GetWalletAddress();
}
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.- TypeScript
- Unity
- Unreal
Copy
Ask AI
import { getLinkedAddresses } from '@imtbl/wallet';
import { ImmutableConfiguration } from '@imtbl/config';
import { Environment } from '@imtbl/config';
// Initialize API client
const apiClient = new ImmutableConfiguration({
environment: 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...']
Copy
Ask AI
List<string> addresses = await Passport.Instance.GetLinkedAddresses();
if (addresses.Count > 0)
{
Debug.Log("Linked addresses:");
foreach (string address in addresses)
{
Debug.Log($" {address}");
}
}
else
{
Debug.Log("No linked addresses");
}
Copy
Ask AI
// Add to your subsystem:
public:
void GetLinkedAddresses()
{
auto Passport = GetPassport();
if (!Passport)
{
return;
}
Passport->GetLinkedAddresses(UImmutablePassport::FImtblPassportResponseDelegate::CreateUObject(this, &UPassportQuickStartSubsystem::OnLinkedAddressesReceived));
}
private:
void OnLinkedAddressesReceived(FImmutablePassportResult Result)
{
if (!Result.Success)
{
UE_LOG(LogTemp, Error, TEXT("Get Linked Addresses Failed: %s"), *Result.Error);
return;
}
TArray<FString> LinkedAddresses = UImmutablePassport::GetResponseResultAsStringArray(Result.Response);
if (LinkedAddresses.Num() > 0)
{
UE_LOG(LogTemp, Log, TEXT("Found %d Linked Address(es)"), LinkedAddresses.Num());
for (int Index = 0; Index < LinkedAddresses.Num(); Index++)
{
UE_LOG(LogTemp, Log, TEXT(" Linked[%d]: %s"), Index, *LinkedAddresses[Index]);
}
}
else
{
UE_LOG(LogTemp, Log, TEXT("No Linked Addresses Found"));
}
}
void OnWalletAccountsReceived(FImmutablePassportResult Result)
{
if (!Result.Success) return;
UE_LOG(LogTemp, Log, TEXT("Wallet Connected"));
GetLinkedAddresses();
}
Important:
ConnectEvm() must be called once after login to initialize the zkEVM provider before calling ZkEvmRequestAccounts() or any other wallet operations.Link External Wallet
Link an external wallet (such as MetaMask or a hardware wallet) to the user’s Passport account. This requires a signature from the external wallet to prove ownership.Copy
Ask AI
import { linkExternalWallet } from '@imtbl/wallet';
import type { LinkWalletParams, LinkedWallet } from '@imtbl/wallet';
const walletAddress = '0x...';
const nonce = 'unique-nonce-123';
const message = `Link wallet ${walletAddress} with nonce ${nonce}`;
// Sign the message with the external wallet (e.g., using ethers.js or viem)
const signature = await externalWallet.signMessage(message);
const params: LinkWalletParams = {
type: 'metamask',
walletAddress,
signature,
nonce,
};
const linkedWallet: LinkedWallet = await linkExternalWallet(
auth,
apiClient,
params
);
console.log('Linked wallet:', linkedWallet.address);
Linking Errors
| Error Code | Description |
|---|---|
ALREADY_LINKED | This wallet is already linked to an account |
MAX_WALLETS_LINKED | Maximum number of linked wallets reached |
DUPLICATE_NONCE | The nonce has already been used |
VALIDATION_ERROR | Invalid signature or parameters |
Check Balances
Native IMX and ERC-20 Token Balances- TypeScript
- Unity
- Unreal
Copy
Ask AI
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}`);
Copy
Ask AI
// Get native IMX balance using Passport's zkEVM provider
string balance = await passport.ZkEvmGetBalance(playerAddress);
Debug.Log($"Balance: {balance} wei");
The
ZkEvmGetBalance method returns balance in wei as a string.Copy
Ask AI
// Add to your subsystem:
public:
void GetWalletBalance(const FString& WalletAddress)
{
auto Passport = GetPassport();
if (!Passport)
{
return;
}
FImmutablePassportZkEvmGetBalanceData BalanceData;
BalanceData.address = WalletAddress;
BalanceData.blockNumberOrTag = TEXT("latest"); // Optional: defaults to "latest"
Passport->ZkEvmGetBalance(BalanceData, UImmutablePassport::FImtblPassportResponseDelegate::CreateUObject(this, &UPassportQuickStartSubsystem::OnBalanceReceived));
}
private:
void OnBalanceReceived(FImmutablePassportResult Result)
{
if (!Result.Success)
{
UE_LOG(LogTemp, Error, TEXT("Get Balance Failed: %s"), *Result.Error);
return;
}
FString Balance = UImmutablePassport::GetResponseResultAsString(Result.Response);
UE_LOG(LogTemp, Log, TEXT("Wallet Balance (wei): %s"), *Balance);
}
void OnWalletAccountsReceived(FImmutablePassportResult Result)
{
if (!Result.Success) return;
const auto& Data = FImmutablePassportZkEvmRequestAccountsData::FromJsonObject(Result.Response.JsonObject);
if (Data.IsSet() && Data->accounts.Num() > 0)
{
GetWalletBalance(Data->accounts[0]);
}
}
The
ZkEvmGetBalance method returns balance in wei as a hex string.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.- TypeScript
- Unity
- Unreal
Copy
Ask AI
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');
Copy
Ask AI
using Immutable.Passport.Model;
TransactionRequest request = new TransactionRequest()
{
to = address,
value = amount,
data = data
}
TransactionReceiptResponse response = await passport.ZkEvmSendTransactionWithConfirmation(request);
switch (response.status)
{
case "1":
// Successful
break;
case "0":
// Failed
break;
}
Copy
Ask AI
// Add to your subsystem:
public:
void SendTransactionWithConfirmation(const FString& ToAddress, const FString& ValueInWei, const FString& Data = TEXT("0x"))
{
auto Passport = GetPassport();
if (!Passport)
{
UE_LOG(LogTemp, Error, TEXT("Passport not available"));
return;
}
FImtblTransactionRequest TransactionRequest;
TransactionRequest.to = ToAddress;
TransactionRequest.value = ValueInWei;
TransactionRequest.data = Data;
Passport->ZkEvmSendTransactionWithConfirmation(TransactionRequest, UImmutablePassport::FImtblPassportResponseDelegate::CreateWeakLambda(this, [this](FImmutablePassportResult Result)
{
if (Result.Success)
{
UE_LOG(LogTemp, Log, TEXT("Transaction Confirmed"));
}
else
{
UE_LOG(LogTemp, Error, TEXT("Transaction Confirmation Failed: %s"), *Result.Error);
}
}));
}
When to use: Critical operations like transfers, mints, or state changes. Ensures transaction succeeded before updating game state.
Example: Interacting with Smart Contracts
Example: Interacting with Smart Contracts
Copy
Ask AI
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.- TypeScript
- Unity
- Unreal
Copy
Ask AI
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).
Copy
Ask AI
using Cysharp.Threading.Tasks;
using Immutable.Passport.Model;
using System.Threading;
async void GetTransactionReceiptStatus()
{
TransactionRequest request = new TransactionRequest()
{
to = address,
value = amount,
data = data
}
string transactionHash = await passport.ZkEvmSendTransaction(request);
string? status = await PollStatus(
passport,
transactionHash,
TimeSpan.FromSeconds(1), // Poll every one second
TimeSpan.FromSeconds(10) // Stop polling after 10 seconds
);
switch (status)
{
case "0x1":
// Successful
break;
case "0x0":
// Failed
break;
}
}
static async UniTask<string?> PollStatus(Passport passport, string transactionHash, TimeSpan pollInterval, TimeSpan timeout)
{
var cancellationTokenSource = new CancellationTokenSource(timeout);
try
{
while (!cancellationTokenSource.Token.IsCancellationRequested)
{
TransactionReceiptResponse response = await passport.ZkEvmGetTransactionReceipt(transactionHash);
if (response.status == null)
{
// The transaction is still being processed, poll for status again
await UniTask.Delay(delayTimeSpan: pollInterval, cancellationToken: cancellationTokenSource.Token);
}
else
{
return response.status;
}
}
}
catch (OperationCanceledException)
{
// Task was canceled due to timeout
}
return null; // Timeout or could not get transaction receipt
}
Copy
Ask AI
// Add to your subsystem:
public:
void SendTransaction(const FString& ToAddress, const FString& ValueInWei, const FString& Data = TEXT("0x"))
{
auto Passport = GetPassport();
if (!Passport)
{
UE_LOG(LogTemp, Error, TEXT("SendTransaction: Passport not available"));
return;
}
FImtblTransactionRequest TransactionRequest;
TransactionRequest.to = ToAddress;
TransactionRequest.value = ValueInWei;
TransactionRequest.data = Data;
Passport->ZkEvmSendTransaction(TransactionRequest, UImmutablePassport::FImtblPassportResponseDelegate::CreateWeakLambda(this, [this](FImmutablePassportResult Result)
{
if (Result.Success)
{
FString TxHash = UImmutablePassport::GetResponseResultAsString(Result.Response);
UE_LOG(LogTemp, Log, TEXT("Transaction Hash: %s"), *TxHash);
}
else
{
UE_LOG(LogTemp, Error, TEXT("Send Transaction Failed: %s"), *Result.Error);
}
}));
}
Important: Don’t update game state until transaction is confirmed. Use this pattern only for non-critical operations or when you have custom polling logic.
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
- TypeScript
- Unity
- Unreal
Copy
Ask AI
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;
}
Copy
Ask AI
public class NFTTransfer : MonoBehaviour
{
public async void Execute()
{
try
{
string receiverAddress = "0x..."; // Receiver's wallet address
string tokenId = "123"; // NFT Token ID
string tokenAddress = "0x..."; // NFT Contract Address
UnsignedTransferRequest transferRequest = UnsignedTransferRequest.ERC721(receiverAddress, tokenId, tokenAddress);
CreateTransferResponseV1 response = await Passport.Instance.ImxTransfer(transferRequest);
Debug.Log($"NFT transferred successfully! Transfer ID: {response.transfer_id}");
}
catch (System.Exception ex)
{
Debug.LogError($"Transfer failed: {ex.Message}");
}
}
}
Copy
Ask AI
// Add to your subsystem:
public:
void ImxTransferNFT(const FString& ReceiverAddress, const FString& TokenId, const FString& TokenAddress)
{
auto Passport = GetPassport();
if (!Passport)
{
UE_LOG(LogTemp, Error, TEXT("ImxTransfer: Passport not available"));
return;
}
FImxTransferRequest ImxTransfer;
ImxTransfer.receiver = ReceiverAddress;
ImxTransfer.type = TEXT("ERC721");
ImxTransfer.tokenId = TokenId;
ImxTransfer.tokenAddress = TokenAddress;
Passport->ImxTransfer(ImxTransfer, UImmutablePassport::FImtblPassportResponseDelegate::CreateWeakLambda(this, [this](FImmutablePassportResult Result)
{
if (Result.Success)
{
UE_LOG(LogTemp, Log, TEXT("IMX NFT Transfer Successful"));
}
else
{
UE_LOG(LogTemp, Error, TEXT("IMX NFT Transfer Failed: %s"), *Result.Error);
}
}));
}
Sign Messages
Personal Sign (ERC-191)
For authentication or simple message signing:- TypeScript
- Unity
- Unreal
Copy
Ask AI
// 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);
Unity SDK Limitation: Unity SDK does not support ERC-191 personal sign. Only EIP-712 typed data signing is supported via
ZkEvmSignTypedDataV4(). See the Typed Data section below for message signing in Unity.Unreal SDK Limitation: Unreal SDK does not support ERC-191 personal sign. Only EIP-712 typed data signing is supported via
ZkEvmSignTypedDataV4(). See the Typed Data section below for message signing in Unreal.Typed Data (EIP-712)
For structured data signing (used by protocols like Seaport):- TypeScript
- Unity
- Unreal
Copy
Ask AI
// 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);
Copy
Ask AI
// Construct EIP-712 typed data as JSON string
string typedDataJson = @"{
""domain"": {
""name"": ""My Game"",
""version"": ""1"",
""chainId"": 13473,
""verifyingContract"": ""0xYourContract...""
},
""types"": {
""Trade"": [
{ ""name"": ""itemId"", ""type"": ""uint256"" },
{ ""name"": ""price"", ""type"": ""uint256"" }
]
},
""primaryType"": ""Trade"",
""message"": {
""itemId"": 123,
""price"": ""10000000000000000000""
}
}";
var signature = await passport.ZkEvmSignTypedDataV4(typedDataJson);
Debug.Log($"Signature: {signature}");
Copy
Ask AI
// Add to your subsystem:
public:
void SignTypedData()
{
auto Passport = GetPassport();
if (!Passport)
{
return;
}
FString TypedDataJson = TEXT(R"({
"domain": {
"name": "My Game",
"version": "1",
"chainId": 13473,
"verifyingContract": "0xYourContract..."
},
"types": {
"Trade": [
{ "name": "itemId", "type": "uint256" },
{ "name": "price", "type": "uint256" }
]
},
"primaryType": "Trade",
"message": {
"itemId": "123",
"price": "10000000000000000000"
}
})");
Passport->ZkEvmSignTypedDataV4(TypedDataJson, UImmutablePassport::FImtblPassportResponseDelegate::CreateUObject(this, &UPassportQuickStartSubsystem::OnTypedDataSigned));
}
private:
void OnTypedDataSigned(FImmutablePassportResult Result)
{
if (!Result.Success)
{
UE_LOG(LogTemp, Error, TEXT("Sign Typed Data Failed: %s"), *Result.Error);
return;
}
FString Signature = UImmutablePassport::GetResponseResultAsString(Result.Response);
UE_LOG(LogTemp, Log, TEXT("Signature: %s"), *Signature);
}
// Call when user needs to sign typed data:
SignTypedData();
Provider Events
The provider emits standard EIP-1193 events that you can subscribe to:Copy
Ask AI
provider.on('accountsChanged', (accounts: string[]) => {
console.log('Accounts changed:', accounts);
});
provider.on('chainChanged', (chainId: string) => {
console.log('Chain changed:', chainId);
});
provider.on('disconnect', (error: Error) => {
console.log('Disconnected:', error);
});
// Remove a specific listener
provider.removeListener('accountsChanged', handler);
EIP-6963 Provider Discovery
The wallet automatically announces itself via EIP-6963 for wallet discovery by dApps. Disable this if needed:Copy
Ask AI
const provider = await connectWallet({
announceProvider: false,
});
Copy
Ask AI
import { announceProvider, passportProviderInfo } from '@imtbl/wallet';
announceProvider({
info: passportProviderInfo,
provider: yourProvider,
});
Error Handling
- TypeScript
- Unity
- Unreal
Use
WalletError and WalletErrorType for typed error handling:Copy
Ask AI
import { connectWallet, WalletError, WalletErrorType } from '@imtbl/wallet';
try {
const provider = await connectWallet();
const accounts = await provider.request({ method: 'eth_requestAccounts' });
} catch (error) {
if (error instanceof WalletError) {
switch (error.type) {
case WalletErrorType.NOT_LOGGED_IN_ERROR:
console.log('User is not logged in');
break;
case WalletErrorType.WALLET_CONNECTION_ERROR:
console.log('Failed to connect wallet:', error.message);
break;
case WalletErrorType.TRANSACTION_REJECTED:
console.log('User rejected the transaction');
break;
case WalletErrorType.UNAUTHORIZED:
console.log('Unauthorized - call eth_requestAccounts first');
break;
case WalletErrorType.GUARDIAN_ERROR:
console.log('Guardian validation failed');
break;
case WalletErrorType.INVALID_CONFIGURATION:
console.log('Invalid wallet configuration');
break;
case WalletErrorType.SERVICE_UNAVAILABLE_ERROR:
console.log('Service temporarily unavailable');
break;
default:
console.error('Wallet error:', error.message);
}
} else {
console.error('Unexpected error:', error);
}
}
Error Types
| Error Type | Description |
|---|---|
NOT_LOGGED_IN_ERROR | User is not authenticated |
WALLET_CONNECTION_ERROR | Failed to connect or link wallet |
TRANSACTION_REJECTED | User rejected a transaction |
UNAUTHORIZED | Operation requires authentication |
GUARDIAN_ERROR | Guardian (security) validation failed |
INVALID_CONFIGURATION | Invalid wallet configuration |
SERVICE_UNAVAILABLE_ERROR | Backend service unavailable |
JSON-RPC Error Codes
Copy
Ask AI
import { ProviderErrorCode, RpcErrorCode } from '@imtbl/wallet';
// Standard provider error codes (EIP-1193)
ProviderErrorCode.USER_REJECTED_REQUEST // 4001
ProviderErrorCode.UNAUTHORIZED // 4100
ProviderErrorCode.UNSUPPORTED_METHOD // 4200
ProviderErrorCode.DISCONNECTED // 4900
// Standard RPC error codes
RpcErrorCode.INVALID_REQUEST // -32600
RpcErrorCode.METHOD_NOT_FOUND // -32601
RpcErrorCode.INVALID_PARAMS // -32602
RpcErrorCode.INTERNAL_ERROR // -32603
Copy
Ask AI
try
{
var response = await passport.ZkEvmSendTransactionWithConfirmation(request);
Debug.Log($"Success: {response.transactionHash}");
}
catch (Exception e)
{
Debug.LogError($"Transaction failed: {e.Message}");
}
Copy
Ask AI
// Add to your subsystem:
public:
void SendTransactionWithErrorHandling(const FString& ToAddress, const FString& Data)
{
auto Passport = GetPassport();
if (!Passport)
{
UE_LOG(LogTemp, Error, TEXT("Failed to get Passport instance"));
return;
}
FImtblTransactionRequest Request;
Request.to = ToAddress;
Request.data = Data;
Request.value = TEXT("0");
Passport->ZkEvmSendTransactionWithConfirmation(Request, UImmutablePassport::FImtblPassportResponseDelegate::CreateUObject(this, &UPassportQuickStartSubsystem::OnTransactionComplete));
}
private:
void OnTransactionComplete(FImmutablePassportResult Result)
{
if (!Result.Success)
{
// Handle different error cases
if (Result.Error.Contains(TEXT("User rejected")))
{
UE_LOG(LogTemp, Warning, TEXT("User rejected the transaction"));
}
else if (Result.Error.Contains(TEXT("insufficient funds")))
{
UE_LOG(LogTemp, Error, TEXT("Insufficient balance"));
}
else
{
UE_LOG(LogTemp, Error, TEXT("Transaction error: %s"), *Result.Error);
}
return;
}
TOptional<FZkEvmTransactionReceipt> Receipt = JsonObjectToUStruct<FZkEvmTransactionReceipt>(Result.Response.JsonObject);
if (!Receipt.IsSet())
{
UE_LOG(LogTemp, Error, TEXT("Failed to parse transaction receipt"));
return;
}
UE_LOG(LogTemp, Log, TEXT("Transaction Hash: %s"), *Receipt->hash);
UE_LOG(LogTemp, Log, TEXT("Status: %s"), *Receipt->status);
}
Next Steps
Gas Sponsorship
Gas sponsorship and transaction costs
Pre-Approved Transactions
Instant transactions without popups (Unity/Unreal)
Immutable Play
Where users manage their wallet
Architecture
Understand the security model
Minting NFTs
Mint NFTs to user wallets
Operator Allowlist
Contract allowlisting and verification