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.