Use this file to discover all available pages before exploring further.
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.
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.
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.
// Assuming Passport is initialized and user is logged in// 1. Connect EVM providerconst 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);
The connectEvm() method returns an EIP-1193 compatible provider that can be used with libraries like ethers.js.
// Assuming Passport is initialized and user is logged in// 1. Connect EVM providerawait 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.
// 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.
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.
// First, initialize the zkEVM providerconst provider = await connectWallet({ auth });// Then get the wallet addressconst accounts = await provider.request({ method: 'eth_requestAccounts' });const address = accounts[0];console.log('Wallet address:', address);
// First, initialize the zkEVM provider (call once after login)await passport.ConnectEvm();// Then get the wallet addressvar accounts = await passport.ZkEvmRequestAccounts();string address = accounts[0];Debug.Log($"Wallet address: {address}");
// 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(); }
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
import { getLinkedAddresses } from '@imtbl/wallet';import { ImmutableConfiguration } from '@imtbl/config';import { Environment } from '@imtbl/config';// Initialize API clientconst apiClient = new ImmutableConfiguration({ environment: Environment.SANDBOX,}).getMultiRollupApiClients();// Get all wallets linked to the current Passport accountconst linkedAddresses = await getLinkedAddresses(auth, apiClient);console.log('Linked addresses:', linkedAddresses);// Returns: ['0x123...', '0x456...']
// 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 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.
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);
Recommended for critical operations - Waits for blockchain confirmation before returning.
TypeScript
Unity
Unreal
import { BrowserProvider } from 'ethers';// Get the provider from Passportconst provider = await connectWallet({ auth });// Wrap provider in ethers BrowserProviderconst browserProvider = new BrowserProvider(provider);// Get the signer (represents the user's wallet)const signer = await browserProvider.getSigner();// Send transaction using signerconst tx = await signer.sendTransaction({ to: '0xRecipient...', value: '1500000000000000000', // 1.5 IMX in wei});// Wait for blockchain confirmationconst receipt = await tx.wait();console.log('Transaction confirmed!');console.log('Hash:', receipt.hash);console.log('Status:', receipt.status === 1 ? 'Success' : 'Failed');
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;}
// 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
import { BrowserProvider, ethers } from 'ethers';// Setup provider and signerconst 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 instanceconst contract = new ethers.Contract( '0xYourNFTContract...', // Contract address abi, signer // Signer enables sending transactions);// Call contract function - automatically sends transactionconst tx = await contract.safeTransferFrom( userAddress, recipientAddress, tokenId);// Wait for confirmationconst receipt = await tx.wait();console.log('NFT transferred!', receipt.hash);
For fire-and-forget operations - Returns transaction hash immediately without waiting.
TypeScript
Unity
Unreal
import { BrowserProvider } from 'ethers';// Get the provider from Passportconst provider = await connectWallet({ auth });// Wrap provider in ethers BrowserProviderconst browserProvider = new BrowserProvider(provider);// Get the signer (represents the user's wallet)const signer = await browserProvider.getSigner();// Send transaction - returns immediately without waitingconst 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).
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}
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.
// 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.