Skip to content

Custom Wallet Adapter ​

TokenFlight widgets don't bundle a wallet — they talk to wallets through an adapter interface. Official adapters exist for AppKit, wagmi, and ethers, but you can build your own for any provider.

Should you read this? ​

Read this page if users should connect a wallet inside your app, or if your app already has wallet state and the widget must reuse it.

If you only want a first widget on screen, start with Getting Started. If you already use a supported wallet library, you can skip to AppKit, wagmi, or ethers.

Before you start ​

  • A walletAdapter connects the widget to a wallet provider.
  • The widget builds wallet actions, but the wallet signs them.
  • The adapter decides which chains and addresses are available.
  • Missing wallet chains can make balances look empty even when the API supports the chain.

Using a Built-in Adapter ​

js
import { TokenFlightWidget } from '@tokenflight/swap';
import { createAppKitAdapter } from '@tokenflight/adapter-appkit';
import { mainnet, base } from '@reown/appkit/networks';

const { adapter: walletAdapter } = await createAppKitAdapter({
  projectId: 'YOUR_REOWN_PROJECT_ID',
  networks: [mainnet, base],
  metadata: {
    name: 'TokenFlight Example',
    description: 'TokenFlight payment flow',
    url: window.location.origin,
    icons: [],
  },
});

const widget = new TokenFlightWidget({
  container: '#widget',
  config: {
    toToken: { chainId: 8453, address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' },
    tradeType: 'EXACT_OUTPUT',
    amount: '100',
    theme: 'dark',
  },
  walletAdapter,
});
widget.initialize();

Next step ​

PackageProvider
@tokenflight/adapter-appkitWalletConnect / AppKit
@tokenflight/adapter-wagmiwagmi
@tokenflight/adapter-ethersethers

One-Call Helpers with RPC Overrides ​

Both the AppKit and wagmi adapter packages ship an async factory that wires custom per-chain RPC URLs into the wallet stack for you. Pass a map keyed by decimal chain ID; any chain without an override falls back to its default public RPC.

ts
import { createAppKitAdapter } from '@tokenflight/adapter-appkit';
import { mainnet, arbitrum, base } from '@reown/appkit/networks';
import { VersionedTransaction } from '@solana/web3.js';

const { adapter, appkit } = await createAppKitAdapter({
  projectId: 'YOUR_REOWN_PROJECT_ID',
  networks: [mainnet, arbitrum, base],
  rpcOverrides: {
    '1': 'https://ethereum-rpc.publicnode.com',
    '42161': 'https://arb1.arbitrum.io/rpc',
  },
  solana: { VersionedTransaction },
});
ts
import { createWagmiAdapter } from '@tokenflight/adapter-wagmi';
import { mainnet, arbitrum } from 'viem/chains';

const adapter = await createWagmiAdapter({
  chains: [mainnet, arbitrum],
  rpcOverrides: {
    '1': 'https://ethereum-rpc.publicnode.com',
  },
});

Both helpers return a ready-to-use adapter that you pass straight to the widget via the walletAdapter option. If your app already loads RPC overrides from its own config endpoint, pass that map into the helper:

ts
import { createAppKitAdapter } from '@tokenflight/adapter-appkit';

const appConfig = await fetch('/tokenflight-config.json').then((res) => res.json());
const { adapter } = await createAppKitAdapter({
  projectId: 'YOUR_REOWN_PROJECT_ID',
  networks: [mainnet, arbitrum, base],
  rpcOverrides: appConfig.rpcOverrides,
});

Example Projects ​

The IWalletAdapter Interface ​

Implement this interface to connect any wallet:

ts
import type {
  IWalletAdapter,
  WalletAction,
  WalletActionResult,
  WalletActionType,
  WalletEvent,
  WalletEventType,
  ChainType,
} from '@tokenflight/swap';

class MyAdapter implements IWalletAdapter {
  readonly name = 'My Wallet';
  readonly icon = 'https://example.com/icon.svg'; // optional
  readonly supportedActionTypes: WalletActionType[] = [
    'eip1193_request',                  // EVM JSON-RPC
    'solana_signTransaction',           // Solana sign-only action
    'solana_signAndSendTransaction',    // Solana sign and send action
  ];
  // Optional: restrict the widget to only show tokens on these chains
  readonly supportedChainIds = [1, 8453, 42161]; // Ethereum, Base, Arbitrum

  async connect(chainType?: ChainType): Promise<void> { /* ... */ }
  async disconnect(): Promise<void> { /* ... */ }
  isConnected(chainType?: ChainType): boolean { /* ... */ }
  async getAddress(chainType?: ChainType): Promise<string | null> { /* ... */ }
  async executeWalletAction(action: WalletAction): Promise<WalletActionResult> { /* ... */ }

  // Optional
  async signMessage?(message: string, chainType?: ChainType): Promise<string> { /* ... */ }
  async openAccountModal?(): Promise<void> { /* opens the wallet's native account modal */ }
  destroy?(): void { /* clean up internal subscriptions / watchers */ }

  on(event: WalletEventType, handler: (e: WalletEvent) => void): void { /* ... */ }
  off(event: WalletEventType, handler: (e: WalletEvent) => void): void { /* ... */ }
}

Key Concepts ​

Chain Types ​

ts
type ChainType = 'evm' | 'solana' | 'bitcoin' | 'tron';

The widget tells the adapter which chain family it needs. If your wallet only supports one chain type, ignore the parameter and throw on unsupported types.

bitcoin and tron are optional. An adapter that holds accounts on several families returns the matching address from getAddress(chainType) (and null for a family it has no account on), and tags its connect / disconnect events with the family, e.g. { address, chainType: 'tron' }. The widget keeps one address per family, so a Tron account can be connected alongside an EVM one.

By default the widget derives the supported families from supportedActionTypes (eip1193_* → evm, solana_* → solana, bitcoin_* → bitcoin, tron_* → tron). Set readonly supportedChainTypes to declare them explicitly.

⚠️ Coming soon: Zcash is not supported yet.

Supported Chain IDs ​

supportedChainIds tells the widget which chains to show tokens, balances, and search results for. It does not control the order they are displayed in — the chain filter always follows the order GET {apiBase}/v1/chains returns (chain id ascending), so the widget reads the same way for every integrator and does not reshuffle when you reorder your wallet config. Behavior differs by adapter:

AdapterDefaultHow to set
WagmiWalletAdapterAuto — derived from wagmiConfig.chainsPass { supportedChainIds } to override
AppKitWalletAdapterAuto — derived from appkit.getCaipNetworks() (Solana mapped)Pass { supportedChainIds } to override
EthersWalletAdapterOptional — EIP-1193 has no chain listPass { supportedChainIds } in constructor when you want chain filtering
Custom adapterOptional — all API chains shown if omittedSet readonly supportedChainIds on the class
ts
// Custom adapter
readonly supportedChainIds = [1, 8453, 42161]; // Ethereum, Base, Arbitrum

// ethers (recommended when you want chain filtering)
new EthersWalletAdapter(window.ethereum, { supportedChainIds: [1, 8453, 42161] });

When set, the widget filters the token list, balance queries, search, and the chain-filter dropdown to just those chains.

Common pitfall: If the widget shows zero balance on a chain that the API supports, the chain is likely missing from your wallet library's network configuration. The auto-derived supportedChainIds only includes chains your wallet is configured for. Add the missing chain to your AppKit/wagmi config, or pass supportedChainIds explicitly. See Troubleshooting → Wallet shows zero balance.

Wallet Actions ​

The widget sends transaction requests as typed actions:

ts
// EVM — a standard JSON-RPC request
interface EvmWalletAction {
  type: 'eip1193_request';
  chainId: number;
  method: string;       // e.g. 'eth_sendTransaction'
  params: unknown[];
}

// Solana — sign only (host submits)
interface SolanaSignTransactionAction {
  type: 'solana_signTransaction';
  transaction: string;  // base64
}

// Solana — sign and send in one call
interface SolanaSignAndSendAction {
  type: 'solana_signAndSendTransaction';
  transaction: string;  // base64
}

// Bitcoin — build, sign and broadcast a payment; return the txid in `txHash`
interface BitcoinSendTransferAction {
  type: 'bitcoin_sendTransfer';
  chainId: number;
  recipient: string;
  amount: string;       // satoshis, base-10 integer string
}

// Tron — build, sign and broadcast a transfer; return the txid in `txHash`
interface TronSendTransferAction {
  type: 'tron_sendTransfer';
  chainId: number;
  recipient: string;    // base58 "T…" address
  amount: string;       // token base units, base-10 integer string
  token?: string;       // TRC-20 contract (base58); omit for native TRX
}

Bitcoin and Tron deposits are plain transfers to a deposit address: the API returns the address and amount, not an unsigned transaction. An adapter that lists bitcoin_sendTransfer / tron_sendTransfer in supportedActionTypes lets the connected wallet send that deposit, and the widget submits the returned transaction hash for the user. This requires the transfer option to be enabled. Without such an adapter — or if the wallet request fails — the user transfers manually and pastes the transaction hash.

Action Results ​

Return a standardized result:

ts
interface WalletActionResult {
  success: boolean;
  data?: unknown;     // provider-specific payload
  error?: string;     // human-readable error
  txHash?: string;    // if a transaction was sent
}

Events ​

Emit lifecycle events so the widget reacts to wallet state changes:

ts
type WalletEventType = 'connect' | 'disconnect' | 'chainChanged' | 'accountsChanged';

Full Example: EVM-Only Adapter ​

ts
import type {
  IWalletAdapter,
  WalletAction,
  WalletActionResult,
  WalletActionType,
  WalletEvent,
  WalletEventType,
  ChainType,
} from '@tokenflight/swap';

interface EIP1193Provider {
  request(args: { method: string; params?: unknown[] }): Promise<unknown>;
}

export class EthersAdapter implements IWalletAdapter {
  readonly name = 'Ethers.js';
  readonly supportedActionTypes: WalletActionType[] = ['eip1193_request'];

  private provider: import('ethers').BrowserProvider | null = null;
  private signer: import('ethers').Signer | null = null;
  private address: string | null = null;
  private listeners = new Map<WalletEventType, Set<(e: WalletEvent) => void>>();

  constructor(private ethereum: EIP1193Provider) {}

  async connect(): Promise<void> {
    const { BrowserProvider } = await import('ethers');
    this.provider = new BrowserProvider(this.ethereum);
    this.signer = await this.provider.getSigner();
    this.address = await this.signer.getAddress();
    this.emit('connect', { address: this.address });
  }

  async disconnect(): Promise<void> {
    this.address = null;
    this.signer = null;
    this.emit('disconnect');
  }

  isConnected(): boolean {
    return this.address !== null;
  }

  async getAddress(): Promise<string | null> {
    return this.address;
  }

  async executeWalletAction(action: WalletAction): Promise<WalletActionResult> {
    if (action.type !== 'eip1193_request') {
      return { success: false, error: 'Unsupported action type' };
    }

    try {
      const result = await this.ethereum.request({
        method: action.method,
        params: action.params,
      });
      return { success: true, data: result, txHash: typeof result === 'string' ? result : undefined };
    } catch (err: unknown) {
      return { success: false, error: (err as Error).message };
    }
  }

  on(event: WalletEventType, handler: (e: WalletEvent) => void): void {
    if (!this.listeners.has(event)) this.listeners.set(event, new Set());
    this.listeners.get(event)!.add(handler);
  }

  off(event: WalletEventType, handler: (e: WalletEvent) => void): void {
    this.listeners.get(event)?.delete(handler);
  }

  private emit(type: WalletEventType, data?: unknown): void {
    for (const h of this.listeners.get(type) ?? []) h({ type, data });
  }
}

Usage:

js
import { TokenFlightWidget } from '@tokenflight/swap';
import { EthersAdapter } from './ethers-adapter';

const widget = new TokenFlightWidget({
  container: '#widget',
  config: {
    toToken: { chainId: 8453, address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' },
    tradeType: 'EXACT_OUTPUT',
    amount: '100',
    theme: 'dark',
  },
  walletAdapter: new EthersAdapter(window.ethereum),
});
widget.initialize();