butr
Guides

Multi-wallet and chain switching

Hold several wallets at once, choose the active one, act on each platform, and switch a wallet's chain.

Several wallets at once

The pool holds every connected wallet. A user can connect MetaMask and Phantom together; useConnectedWallets() returns both. Mark one the active wallet with setActive from useWalletManager():

import { useWallet, useWalletManager } from "@usebutr/react";

const active = useWallet();
const { setActive } = useWalletManager();

const isActive = active?.connector.id === wallet.connector.id;
{
  isActive ? null : (
    <button type="button" onClick={() => setActive(wallet.connector.id)}>
      Make active
    </button>
  );
}

See pool, selection, active for why these are separate.

One wallet per platform

When your app acts on several platforms at once, read each platform's selected wallet. useSelectedWallet is typed to that platform, so its sendTx takes that platform's transaction with no narrowing:

import { useSelectedWallet } from "@usebutr/react";

const evm = useSelectedWallet("evm"); // ConnectedWallet<"evm"> | undefined
const svm = useSelectedWallet("svm"); // ConnectedWallet<"svm"> | undefined

Switch which wallet serves a platform with setSelection(platform, id) from useWalletManager(). useConnectedWalletsByPlatform() groups the pool for a per-platform picker.

Switch a wallet's chain

wallet.connector.switchChain(chain) takes a ChainBase. Get the right chain list for the wallet's platform from CHAINS_BY_PLATFORM in @usebutr/core:

import type { ConnectedWallet } from "@usebutr/core";
import { CHAINS_BY_PLATFORM } from "@usebutr/core";

const ChainPicker = ({ wallet }: { wallet: ConnectedWallet }) => {
  const chains = CHAINS_BY_PLATFORM[wallet.connector.chainPlatform];
  return (
    <select
      value={wallet.account.chain.id}
      onChange={(event) => {
        const target = chains.find((chain) => chain.id === event.target.value);
        if (target) {
          void wallet.connector.switchChain?.(target);
        }
      }}
    >
      {chains.map((chain) => (
        <option key={chain.id} value={chain.id}>
          {chain.name}
        </option>
      ))}
    </select>
  );
};

Render the picker only when the wallet can switch; switchChain exists only then:

{
  wallet.connector.switchChain ? <ChainPicker wallet={wallet} /> : null;
}

What switching means depends on the transport:

  • EVM wallets switch for real via wallet_switchEthereumChain, which may prompt the user.
  • Wallet Standard wallets (Solana, Sui, Bitcoin) have no switch call. switchChain re-points butr's view of the wallet, and every later call's chain input, to the new chain. It exists only when the wallet advertises more than one chain.
  • Injected Bitcoin wallets switch their network when the provider can (Unisat, Xverse).
  • Ledger and injected Polkadot adapters have no switchChain.

Either way the new chain arrives as an accountsChanged event, bridged automatically into wallet.account.chain. To send one transaction on another chain without switching the UI, pass chain to sendTx instead; see Send a transaction.

A single-platform app imports only its own list, such as EVM_CHAINS_LIST from @usebutr/core; demo-next does exactly this.

Source: ChainPicker in apps/demo-vite/src/app.tsx (uses CHAINS_BY_PLATFORM) and apps/demo-next/src/app/page.tsx (uses EVM_CHAINS_LIST).

The Wormhole USDC demo connects EVM and Solana wallets together for CCTP bridging on testnets.