> ## 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.
</Tip>

## 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 title="User Authentication" icon="user-lock" href="/docs/products/passport/authentication">
  User must be authenticated with Passport before using wallet operations
</Card>

<Warning>
  **Web vs Native**: Some features like [pre-approved transactions](/docs/products/passport/pre-approved-transactions) require Unity/Unreal native clients and cannot be used in web browsers.
</Warning>

## Wallet Connect

Connect the EVM provider and request account access. Required for all wallet operations including [Checkout](/docs/products/checkout/overview) wallet funding and [Orderbook](/docs/products/orderbook/overview) trading operations.

<Tabs>
  <Tab title="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.
    </Note>
  </Tab>

  <Tab title="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/).
    </Note>
  </Tab>

  <Tab title="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.
    </Note>
  </Tab>

  <Tab title="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.
    </Note>
  </Tab>
</Tabs>

<Warning>
  **Important**: Before connecting the wallet, ensure you have [initialized Passport and logged in the user](/docs/products/passport/authentication). Then complete these steps:

  1. Connect EVM provider (`connectEvm()` / `ConnectEvm()`)
  2. Request accounts (`eth_requestAccounts` / `ZkEvmRequestAccounts()`)
</Warning>

<Info>
  **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.
</Info>

## 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

<Tabs>
  <Tab title="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>

  <Tab title="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>

  <Tab title="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();
        }
    ```
  </Tab>
</Tabs>

## 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.

<Tabs>
  <Tab title="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>

  <Tab title="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>

  <Tab title="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.
    </Note>
  </Tab>
</Tabs>

## 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

<Tabs>
  <Tab title="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>

  <Tab title="Unity">
    ```csharp theme={null}
    // Get native IMX balance using Passport's zkEVM provider
    string balance = await passport.ZkEvmGetBalance(playerAddress);
    Debug.Log($"Balance: {balance} wei");
    ```

    <Info>
      The `ZkEvmGetBalance` method returns balance in wei as a string.
    </Info>
  </Tab>

  <Tab title="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]);
            }
        }
    ```

    <Info>
      The `ZkEvmGetBalance` method returns balance in wei as a hex string.
    </Info>
  </Tab>
</Tabs>

## 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.
</Tip>

### Send Transaction with Confirmation

**Recommended for critical operations** - Waits for blockchain confirmation before returning.

<Tabs>
  <Tab title="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>

  <Tab title="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>

  <Tab title="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);
                }
            }));
        }
    ```
  </Tab>
</Tabs>

<Info>
  **When to use:** Critical operations like transfers, mints, or state changes. Ensures transaction succeeded before updating game state.
</Info>

<Accordion title="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);
  ```
</Accordion>

***

### Send Transaction without Confirmation

**For fire-and-forget operations** - Returns transaction hash immediately without waiting.

<Tabs>
  <Tab title="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
    ```

    <Info>
      **Fire-and-Forget:** Returns immediately without waiting for blockchain confirmation. Use for non-critical operations where you want responsive UI (cosmetic purchases, achievements, analytics).
    </Info>
  </Tab>

  <Tab title="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>

  <Tab title="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>
  </Tab>
</Tabs>

<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.
</Warning>

<Info>
  **When to use:** Non-critical operations or when you need custom polling/retry logic. Most games should use "with confirmation" variant for critical operations.
</Info>

### NFT Transfer

<Tabs>
  <Tab title="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>

  <Tab title="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>

  <Tab title="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);
                }
            }));
        }
    ```
  </Tab>
</Tabs>

## Sign Messages

### Personal Sign (ERC-191)

For authentication or simple message signing:

<Tabs>
  <Tab title="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>

  <Tab title="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.
    </Warning>
  </Tab>

  <Tab title="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.
    </Warning>
  </Tab>
</Tabs>

### Typed Data (EIP-712)

For structured data signing (used by protocols like Seaport):

<Tabs>
  <Tab title="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>

  <Tab title="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>

  <Tab title="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();
    ```
  </Tab>
</Tabs>

## 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

<Tabs>
  <Tab title="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>

  <Tab title="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>

  <Tab title="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);
        }
    ```
  </Tab>
</Tabs>

## Next Steps

<CardGroup cols={2}>
  <Card title="Gas Sponsorship" icon="gas-pump" href="/docs/products/passport/gas-sponsorship">
    Gas sponsorship and transaction costs
  </Card>

  <Card title="Pre-Approved Transactions" icon="bolt" href="/docs/products/passport/pre-approved-transactions">
    Instant transactions without popups (Unity/Unreal)
  </Card>

  <Card title="Immutable Play" icon="play" href="/docs/products/play">
    Where users manage their wallet
  </Card>

  <Card title="Architecture" icon="sitemap" href="/docs/products/passport/architecture">
    Understand the security model
  </Card>

  <Card title="Minting NFTs" icon="wand-magic-sparkles" href="/docs/products/asset-contracts/erc721">
    Mint NFTs to user wallets
  </Card>

  <Card title="Operator Allowlist" icon="list-check" href="/docs/products/asset-contracts/operator-allowlist">
    Contract allowlisting and verification
  </Card>
</CardGroup>
