> ## Documentation Index
> Fetch the complete documentation index at: https://docs.immutable.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Wallet

Every Passport user has an embedded wallet on Immutable Chain. This guide covers wallet operations for developers integrating Passport into games and applications.

> [!TIP]
>
> **For end users**: Players can manage their wallet, view balances, and browse transaction history at [Immutable Play](https://play.immutable.com). 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

> [card] [User Authentication](https://docs.immutable.com/docs/products/passport/authentication.md)
>
> User must be authenticated with Passport before using wallet operations

> [!WARNING]
>
> **Web vs Native**: Some features like [pre-approved transactions](https://docs.immutable.com/docs/products/passport/pre-approved-transactions.md) 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](https://docs.immutable.com/docs/products/checkout/overview.md) wallet funding and [Orderbook](https://docs.immutable.com/docs/products/orderbook/overview.md) trading operations.

> [tab] Next.js
>
> Pass `getUser` from `useImmutableSession` to `connectWallet` so the wallet uses your server-managed session instead of its own OAuth flow.
>
> ```tsx theme={null}
> '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>
>   );
> }
> ```
>
> > [!NOTE]
> >
> > 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.

> [tab] TypeScript
>
> ```typescript theme={null}
> // 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);
> ```
>
> > [!NOTE]
> >
> > The `connectEvm()` method returns an [EIP-1193](https://eips.ethereum.org/EIPS/eip-1193) compatible provider that can be used with libraries like [ethers.js](https://docs.ethers.org/).

> [tab] Unity
>
> ```csharp theme={null}
> // 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}");
> ```
>
> > [!NOTE]
> >
> > Call `ConnectEvm()` once after login, then use `ZkEvmRequestAccounts()` to get the wallet address. This initializes the zkEVM provider for all subsequent wallet operations.

> [tab] Unreal
>
> ```cpp theme={null}
> // 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();
>     }
> ```
>
> > [!NOTE]
> >
> > Follow the sequence: ConnectEvm → ZkEvmRequestAccounts. Use `FImmutablePassportZkEvmRequestAccountsData::FromJsonObject` to parse the response.

> [!WARNING]
>
> **Important**: Before connecting the wallet, ensure you have [initialized Passport and logged in the user](https://docs.immutable.com/docs/products/passport/authentication.md). Then complete these steps:
>
> 1. Connect EVM provider (`connectEvm()` / `ConnectEvm()`)
> 2. Request accounts (`eth_requestAccounts` / `ZkEvmRequestAccounts()`)

> [!NOTE]
>
> **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](https://eips.ethereum.org/EIPS/eip-1193) compatible `ZkEvmProvider`. Use the TypeScript tab code with your Next.js provider.

## Configuration

The `connectWallet` function accepts configuration options to customize the wallet provider.

### Custom Client ID

```typescript theme={null}
const provider = await connectWallet({
  clientId: 'your-client-id',
});
```

### Chain Selection

```typescript theme={null}
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

```typescript theme={null}
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 the `Auth` class in a `getUser` function:

```typescript theme={null}
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

```typescript theme={null}
import {
  IMMUTABLE_ZKEVM_MAINNET_CHAIN_ID,  // 13371
  IMMUTABLE_ZKEVM_TESTNET_CHAIN_ID,  // 13473
} from '@imtbl/wallet';
```

### Presets

```typescript theme={null}
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,
});
```

Spread-friendly presets are also available:

```typescript theme={null}
import {
  IMMUTABLE_ZKEVM_MAINNET,
  IMMUTABLE_ZKEVM_TESTNET,
  IMMUTABLE_ZKEVM_MULTICHAIN,
} from '@imtbl/wallet';

const provider = await connectWallet({
  ...IMMUTABLE_ZKEVM_MAINNET,
});
```

### Custom Chain Configuration

```typescript theme={null}
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

> [tab] TypeScript
>
> ```typescript theme={null}
> // 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);
> ```

> [tab] Unity
>
> ```csharp theme={null}
> // 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}");
> ```

> [tab] Unreal
>
> ```cpp theme={null}
> // 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.

> [tab] TypeScript
>
> ```typescript theme={null}
> 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...']
> ```

> [tab] Unity
>
> ```csharp theme={null}
> 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");
> }
> ```

> [tab] Unreal
>
> ```cpp theme={null}
> // 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();
>     }
> ```
>
> > [!NOTE]
> >
> > **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.

```typescript theme={null}
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

> [tab] TypeScript
>
> ```typescript theme={null}
> 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}`);
> ```

> [tab] Unity
>
> ```csharp theme={null}
> // Get native IMX balance using Passport's zkEVM provider
> string balance = await passport.ZkEvmGetBalance(playerAddress);
> Debug.Log($"Balance: {balance} wei");
> ```
>
> > [!NOTE]
> >
> > The `ZkEvmGetBalance` method returns balance in wei as a string.

> [tab] Unreal
>
> ```cpp theme={null}
> // 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]);
>         }
>     }
> ```
>
> > [!NOTE]
> >
> > The `ZkEvmGetBalance` method returns balance in wei as a hex string.

## Send Transactions

> [!TIP]
>
> **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.

> [tab] TypeScript
>
> ```typescript theme={null}
> 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');
> ```

> [tab] Unity
>
> ```csharp theme={null}
> 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;
> }
> ```

> [tab] Unreal
>
> ```cpp theme={null}
> // 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);
>             }
>         }));
>     }
> ```

> [!NOTE]
>
> **When to use:** Critical operations like transfers, mints, or state changes. Ensures transaction succeeded before updating game state.

> [accordion] Example: Interacting with Smart Contracts
>
> ```typescript theme={null}
> 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.

> [tab] TypeScript
>
> ```typescript theme={null}
> 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
> ```
>
> > [!NOTE]
> >
> > **Fire-and-Forget:** Returns immediately without waiting for blockchain confirmation. Use for non-critical operations where you want responsive UI (cosmetic purchases, achievements, analytics).

> [tab] Unity
>
> ```csharp theme={null}
> 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
> }
> ```

> [tab] Unreal
>
> ```cpp theme={null}
> // 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);
>             }
>         }));
>     }
> ```
>
> > [!WARNING]
> >
> > **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.

> [!WARNING]
>
> **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.

> [!NOTE]
>
> **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

> [tab] TypeScript
>
> ```typescript theme={null}
> 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;
> }
> ```

> [tab] Unity
>
> ```csharp theme={null}
> 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}");
>         }
>     }
> }
> ```

> [tab] Unreal
>
> ```cpp theme={null}
> // 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:

> [tab] TypeScript
>
> ```typescript theme={null}
> // 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);
> ```

> [tab] Unity
>
> > [!WARNING]
> >
> > **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.

> [tab] Unreal
>
> > [!WARNING]
> >
> > **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):

> [tab] TypeScript
>
> ```typescript theme={null}
> // 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);
> ```

> [tab] Unity
>
> ```csharp theme={null}
> // 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}");
> ```

> [tab] Unreal
>
> ```cpp theme={null}
> // 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](https://eips.ethereum.org/EIPS/eip-1193) events that you can subscribe to:

```typescript theme={null}
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](https://eips.ethereum.org/EIPS/eip-6963) for wallet discovery by dApps. Disable this if needed:

```typescript theme={null}
const provider = await connectWallet({
  announceProvider: false,
});
```

For manual announcement:

```typescript theme={null}
import { announceProvider, passportProviderInfo } from '@imtbl/wallet';

announceProvider({
  info: passportProviderInfo,
  provider: yourProvider,
});
```

## Error Handling

> [tab] TypeScript
>
> Use `WalletError` and `WalletErrorType` for typed error handling:
>
> ```typescript theme={null}
> 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
>
> ```typescript theme={null}
> 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
> ```

> [tab] Unity
>
> ```csharp theme={null}
> try
> {
>     var response = await passport.ZkEvmSendTransactionWithConfirmation(request);
>     Debug.Log($"Success: {response.transactionHash}");
> }
> catch (Exception e)
> {
>     Debug.LogError($"Transaction failed: {e.Message}");
> }
> ```

> [tab] Unreal
>
> ```cpp theme={null}
> // 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

> [card] [Gas Sponsorship](https://docs.immutable.com/docs/products/passport/gas-sponsorship.md)
>
> Gas sponsorship and transaction costs

> [card] [Pre-Approved Transactions](https://docs.immutable.com/docs/products/passport/pre-approved-transactions.md)
>
> Instant transactions without popups (Unity/Unreal)

> [card] [Immutable Play](https://docs.immutable.com/docs/products/play/overview.md)
>
> Where users manage their wallet

> [card] [Architecture](https://docs.immutable.com/docs/products/passport/architecture.md)
>
> Understand the security model

> [card] [Minting NFTs](https://docs.immutable.com/docs/products/asset-contracts/erc721.md)
>
> Mint NFTs to user wallets

> [card] [Operator Allowlist](https://docs.immutable.com/docs/products/asset-contracts/operator-allowlist.md)
>
> Contract allowlisting and verification


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.