butr
Core concepts

Connectors and wallets

Every wallet is a WalletAdapter — the Connector half is what butr calls, the Wallet half is what your app calls.

A WalletAdapter is Connector & Wallet. The split is documentary: it makes the orchestration / capability seam explicit.

Connector — what butr calls

butr's runtime calls these during connect / disconnect / hydrate. You rarely touch them directly.

type Connector = {
  capabilities: WalletCapabilities;
  chainPlatform: "evm" | "svm";
  connect(): Promise<void>;
  disconnect?(): Promise<void>;
  getAccount(): Promise<Account | null>;
  getAccounts?(): Promise<Array<Account>>;
  icon?: string;
  id: string; // stable key: "metamask", "phantom" — pool entries keyed by this
  name: string; // UI-facing: "MetaMask", "Phantom"
  requestAccounts?(): Promise<void>;
  subscribe?(listener: (event: ConnectorEvent) => void): () => void;
};

subscribe is how wallet-side events (accountChanged, disconnected) get bridged into butr's store; you never wire accountsChanged / chainChanged yourself.

Wallet — what your app calls

These exist purely to give you a typed surface to a connected wallet. butr never invokes them itself.

type Wallet = {
  getBalance(mint?: string): Promise<Balance>;
  getSigner(): Promise<unknown>; // cast to viem WalletClient, WalletStandardWallet, …
  getTransactionReceipt(tx: string): Promise<{ status: "Success" | "Error" | "Pending" }>;
  sendTx(tx: unknown, account?: Account): Promise<string>;
  sendTxToChain(
    tx: unknown,
    targetChainId: string,
    account?: Account,
    cb?: () => void,
  ): Promise<string>;
  signMessage(
    msg: Uint8Array,
    account?: Account,
  ): Promise<{ signature: Uint8Array; signedMessage: Uint8Array }>;
  switchAccount?(address: string): Promise<void>;
  switchChain(chain: ChainBase): Promise<void>;
};

signMessage returns both signature and signedMessage. Solana Wallet Standard wallets may prefix or re-encode the message internally. Verify the signature against signedMessage, not your input bytes. EVM wallets echo the input.

The seam: discovery and createConnector

WalletManagerProvider accepts two orthogonal ways to supply adapters, and they compose: when an id is requested, discovered adapters are checked first, then createConnector.

// WalletManagerProvider props (simplified)
type WalletManagerProviderProps = {
  /** Auto-discovery source. Omit to skip auto-discovery. */
  discovery?: WalletSource;
  /** Explicit adapter factory — resolved after `discovery`. */
  createConnector?: (id: string) => WalletAdapter | null;
  connectors?: Array<ConnectorMeta>;
  // …lifecycle callbacks
};

Pass discovery to let the provider subscribe to wallet announcements automatically. Pass createConnector to register explicit adapters such as WalletConnect or Ledger. Both can be present at the same time. A hand-written adapter supplements the discovered ones, and an id resolves from the discovered set first.

connectors is an optional array of ConnectorMeta{ id, name, chainPlatform, icon?, url?, available? } — that registers display metadata for explicit adapters so the UI can list a wallet before it has connected; discovered adapters populate their own metadata and don't need an entry here.

This is why "how do I add wallet X" always has the same shape: injected, WalletConnect, Ledger, or a hand-written adapter all arrive through the same createConnector seam.

ConnectedWallet

What the hooks hand back:

type ConnectedWallet = {
  account: Account; // currently-active account
  accounts: Array<Account>; // every account this wallet exposes (≥ 1)
  connector: WalletAdapter;
};

Account is { chain: ChainBase; id: string; walletAddress: string }. The chain travels inside the account. Switching chains re-wraps the address with new chain data.

Source: packages/core/src/types/wallet.ts.

On this page