Capabilities
An adapter method exists only when it works for that wallet, so checking for the method is the capability check.
There are no capability flags. An adapter defines an optional method only when calling it can succeed for that wallet, so the method's presence is the capability, and TypeScript enforces the check:
if (wallet.connector.signMessage) {
await wallet.connector.signMessage(bytes, { account: wallet.account });
}No adapter ships a placeholder: no zero balance from a wallet that cannot read
one, no receipt that stays pending forever, no subscribe that never fires, no
switchChain that does nothing.
What is always there
Every adapter has chainPlatform, id, name, icon?, connect,
getAccounts, and getSigner. Everything else is optional:
| Method | Platforms | Present when |
|---|---|---|
disconnect | all | the wallet has a teardown call |
subscribe | all | the wallet emits account or chain events |
requestAccounts | all | the wallet has an account picker (EVM wallet_requestPermissions) |
switchChain | all | the wallet can move to another chain, or advertises more than one |
signMessage | all | the wallet can sign bytes |
sendTx | EVM, SVM, Sui, Bitcoin | the wallet can sign and broadcast |
signTransaction | SVM, Sui, Bitcoin | the wallet can sign without broadcasting |
signIn | SVM | the wallet advertises solana:signIn (Sign In With Solana) |
getBalance | EVM | the adapter can read through the wallet's provider |
getTransactionReceipt | EVM | the adapter can read through the wallet's provider |
getBalance and getTransactionReceipt need an RPC. EVM wallets expose one
through their provider; butr ships none for Wallet Standard chains, so those
adapters leave both out. Read balances there with your own chain client.
Gate UI on presence
Render an affordance only when its method is there:
import type { ConnectedWallet } from "@usebutr/core";
import { useWalletManager } from "@usebutr/react";
const WalletActions = ({ wallet }: { wallet: ConnectedWallet }) => {
const { requestAccounts } = useWalletManager();
return (
<>
{wallet.connector.requestAccounts ? (
<button type="button" onClick={() => void requestAccounts(wallet.connector.id)}>
Request more accounts
</button>
) : null}
{wallet.connector.switchChain ? <ChainPicker wallet={wallet} /> : null}
{wallet.connector.signMessage ? (
<SignButton account={wallet.account} wallet={wallet} />
) : null}
</>
);
};The hooks follow the same rule. useBalance(wallet) stays "idle" for an adapter
without getBalance, and requestAccounts(id) on the manager does nothing for
an adapter without requestAccounts.
Narrow on chainPlatform for platform methods
signIn and signTransaction exist only on some platforms, and sendTx takes a
different transaction type on each. ConnectedWallet is a union, so narrow it
to a platform before you reach for those:
import type { ConnectedWallet } from "@usebutr/core";
const signSolanaTx = async (wallet: ConnectedWallet, tx: Uint8Array) => {
const { connector } = wallet;
if (connector.chainPlatform !== "svm" || !connector.signTransaction) {
throw new Error(`${connector.name} cannot sign Solana transactions`);
}
return connector.signTransaction(tx, { account: wallet.account });
};Three ways to get a narrowed wallet:
wallet.connector.chainPlatform === "svm", as above.isPlatformWallet(wallet, "svm")from@usebutr/core, which narrows the wholeConnectedWallet.useSelectedWallet("svm")from@usebutr/react, which returnsConnectedWallet<"svm"> | undefined.
See Send a transaction for each platform's transaction type.
What decides presence
- Wallet Standard adapters (Solana, Sui, Bitcoin, Polkadot) check the
wallet's advertised features when discovery builds the adapter:
signMessageneedssolana:signMessage(or its Sui, Bitcoin, Polkadot equivalent),sendTxneedssolana:signAndSendTransaction, andswitchChainneeds more than one advertised chain in the namespace. A wallet that adds features later keeps the adapter it was built with. - EVM adapters define every optional method: EIP-1193 has a call for each. The wallet can still reject one it doesn't implement.
- WalletConnect and Ledger have fixed sets per namespace or device app. A
Ledger signs and never sends; WalletConnect has no
requestAccounts. See WalletConnect and Ledger. - Injected Bitcoin adapters define
switchChainonly when the provider can switch networks (Unisat, Xverse). - Injected Polkadot adapters gain
signMessageandsubscribeonceconnect()has enabled the extension, becauseinjectedWeb3only reveals them then.
Read presence where you call the method, on the connected wallet, rather than caching it from the discovered adapter.
A wallet that is still reconnecting from a
server-rendered snapshot is backed by a placeholder with none of the optional methods. They appear
once the live adapter lands. To keep layout stable, render those controls disabled while
useIsReconnecting(id) is true instead of hiding them.
Source: packages/core/src/types/wallet.ts, packages/core/src/types/connector.ts,
ADR 0004.