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 a Connector intersected with a platform wallet surface
(EvmWallet, SvmWallet, SuiWallet, BitcoinWallet, or PolkadotWallet).
The split is documentary: it names which half butr drives and which half your
app calls. In both halves, an optional member is defined only when it works for
that wallet; see capabilities.
Connector: what butr calls
The manager calls these during connect, disconnect, and hydration. You rarely touch them directly.
type Connector<P extends ChainPlatform = ChainPlatform> = {
chainPlatform: P;
/** `silent` is hydration's non-interactive reconnect; an adapter that
* can't honour it rejects instead of prompting. */
connect: (options?: { silent?: boolean }) => Promise<void>;
disconnect?: () => Promise<void>;
/** Every exposed account, active first. Empty when not connected. */
getAccounts: () => Promise<ReadonlyArray<Account>>;
icon?: string;
id: string; // stable key: "io.metamask", "wallet-standard:svm-phantom"
name: string; // UI-facing: "MetaMask", "Phantom"
requestAccounts?: () => Promise<void>;
subscribe?: (listener: (event: ConnectorEvent) => void) => () => void;
};
type ConnectorEvent =
{ accounts: ReadonlyArray<Account>; type: "accountsChanged" } | { type: "disconnected" };subscribe is how wallet-side events reach butr: an account switch or a chain
switch arrives as accountsChanged with the new list, active first, and an
empty list or disconnected ends the session. You never wire
accountsChanged / chainChanged yourself.
Wallet: what your app calls
butr never invokes these itself, apart from getSigner in useSigner() and
getBalance in useBalance(). Tx is the platform's transaction type.
type WalletBase<Tx> = {
getBalance?: (options?: { account?: Account; token?: string }) => Promise<Balance>;
getSigner: () => Promise<WalletSigner>;
getTransactionReceipt?: (hash: string) => Promise<TransactionReceipt>;
sendTx?: (tx: Tx, options?: { account?: Account; chain?: ChainBase }) => Promise<string>;
signMessage?: (message: Uint8Array, options?: { account?: Account }) => Promise<SignedMessage>;
switchChain?: (chain: ChainBase) => Promise<void>;
};The platform types add their own members and fix Tx:
| Type | Tx | Adds |
|---|---|---|
EvmWallet | EvmTransactionRequest | nothing: EVM wallets sign and send in one eth_sendTransaction |
SvmWallet | Uint8Array | signIn (SIWS), signTransaction → signed transaction bytes |
SuiWallet | SuiTransactionInput | signTransaction → { bytes, signature } |
BitcoinWallet | BitcoinTransfer | signTransaction on PSBT bytes → signed PSBT bytes |
PolkadotWallet | none | no sendTx: extrinsics go through getSigner() and polkadot-api |
account must be one the wallet exposes; omit it to use the active account.
An unknown account rejects rather than silently signing with another. chain
targets one transaction; see Send a transaction for how
each transport routes it.
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: sources
Every adapter reaches the manager through WalletManagerConfig.sources, a list
of WalletSource functions. Discovery and built adapters use the same seam:
import type { WalletManagerConfig } from "@usebutr/core";
import { fromAdapters } from "@usebutr/core";
import { autoDiscovery } from "@usebutr/wallets";
import { createWalletConnectAdapters } from "@usebutr/walletconnect";
const config: WalletManagerConfig = {
sources: [
autoDiscovery(), // injected and Wallet Standard wallets
fromAdapters(createWalletConnectAdapters({ … })), // built adapters
],
};fromAdapters takes one adapter, several, or a promise of either, and a
rejected promise is logged and contributes nothing. Every adapter from every source lands in
useDiscoveredWallets(), so a picker lists WalletConnect and Ledger next to
MetaMask with no extra metadata. The first adapter announced for an id wins.
This is why "how do I add wallet X" always has the same shape: injected, WalletConnect, Ledger, or a hand-written adapter all arrive as a source.
ConnectedWallet
What the hooks hand back:
type ConnectedWallet<P extends ChainPlatform = ChainPlatform> = {
account: Account; // the active account; always one of `accounts`
accounts: ReadonlyArray<Account>; // every account the wallet exposed
connector: WalletAdapterFor<P>;
};Account is { chain: ChainBase; id: string; walletAddress: string }, built
with buildAccount(address, chain). The chain travels inside the account, so
a chain switch arrives as the same address with new chain data.
ConnectedWallet<"svm"> narrows connector to SvmAdapter; get one from
useSelectedWallet("svm") or isPlatformWallet(wallet, "svm").
Source: packages/core/src/types/connector.ts, packages/core/src/types/wallet.ts,
packages/core/src/wallet-source.ts.