butr

Caveats & Troubleshooting

The sharp edges: connect timeouts, silent storage, best-effort errors, rejected accounts and chains, missing methods, Ledger browser support, hydration races.

Connect hangs, then fails after 90 seconds

butr enforces a 90-second connect timeout. If connector.connect() neither resolves nor rejects in that window, the attempt fails with a ConnectionError of kind "Timeout". This catches wallets that hang without cleanup. Separately, onSlowConnect fires once if a connect exceeds slowConnectThresholdMs (default 5000); use it for a "still trying, check your wallet" hint. A silent reconnect on page load gives up after 15 seconds.

connect rejects with WalletNotFound

No source announced an adapter with that id. Discovery is asynchronous, so connect from the ids useDiscoveredWallets() lists rather than hard-coded ones. A WalletConnect or Ledger id appears only once its fromAdapters source has resolved; a factory that rejected is logged with [butr] fromAdapters and contributes nothing.

A signature or transaction rejects for an account the wallet shows

An account passed in the options must be one the wallet exposes on that chain, and no adapter falls back to its first account. Pass wallet.account, or an entry of wallet.accounts from the same pool entry. Unisat-style Bitcoin wallets sign with their active account only, Xverse sends from its payment address only, and a Ledger exposes only the accountCount addresses it read.

A transaction rejects with ChainMismatch

The transport cannot reach the chain you passed. Wallet Standard and WalletConnect route a chain per call but only to chains the wallet advertises or the session approved; OKX's Bitcoin provider and most window.btc wallets cannot switch networks; a Ledger adapter signs for its one chainId. Check error.kind === "ChainMismatch" and ask the user to switch in the wallet, or build one Ledger adapter per chain. See chains and accounts.

A method is undefined

A method exists only when it works for that wallet, so sendTx, signMessage, signTransaction, signIn, switchChain, requestAccounts and the rest can be absent. Check before calling (if (wallet.connector.signMessage)) and hide the button otherwise. On a Polkadot injectedWeb3 wallet, signMessage appears only after connect(), so check the connected wallet, not the discovered adapter. The connectors overview lists what each transport defines.

useBalance stays "idle"

Only EVM adapters (injected and WalletConnect) define getBalance. On Solana, Sui, Bitcoin, Polkadot and Ledger wallets there is nothing to call, so the hook stays idle instead of reporting a zero balance. Read those balances with your chain client: @solana/kit, @mysten/sui, an Esplora client, polkadot-api. useBalance is also idle without a wallet and while the wallet is reconnecting.

signer.kind narrows to never, or a case is missing

Each transport package registers its signer kind when it is part of your program. An app that imports neither @usebutr/evm nor @usebutr/wallets has no eip1193 variant. Import the package that provides the wallet: a discover*Adapters or autoDiscovery() in your config is enough, and @usebutr/wallets loads every browser wallet's kinds.

getSigner() rejects with ShadowConnectorError

The wallet was seeded from initialState and is still reconnecting: its shadow adapter rejects connect, getAccounts and getSigner until the live adapter replaces it. Check useIsReconnecting(id) first, or use useSigner(wallet), which stays idle until then.

State isn't persisting and nothing errors

Persistence is fire-and-forget by design: a failed write (quota exceeded, cross-tab conflict, cookie size limit) does not throw. You'll only see it if you set onStorageError; with no callback, butr logs a console.warn. See persistence.

An error shows up as Unknown

Error normalisation is best-effort. A wallet error whose code and message toConnectionError doesn't recognise lands in kind: "Unknown". Inspect error.cause for the raw value. butr never retries; you decide based on kind.

"Request more accounts" does nothing on Solana

Wallet Standard wallets expose all their accounts at once and have no account picker, so their adapters have no requestAccounts, and useWalletManager().requestAccounts(id) is a no-op for them. Render the button only when wallet.connector.requestAccounts is defined.

No Solana, Sui, Bitcoin, or Polkadot wallets show up

Wallet Standard discovery (@usebutr/svm, @usebutr/sui, @usebutr/bitcoin, @usebutr/polkadot) lazily imports the optional peer dependency @wallet-standard/app. When it isn't installed, discovery is disabled and butr logs a single console.warn pointing at the missing install; no Wallet Standard wallets will be detected. Install it to fix:

npm install @wallet-standard/app

See peer dependencies.

Ledger doesn't connect in Firefox/Safari

@usebutr/ledger uses WebUSB, which only exists in Chromium browsers (Chrome, Edge, Brave, Arc). It's also signing-only: its adapters have no sendTx, getBalance or getTransactionReceipt, and a Ledger never reconnects silently on reload. See the Ledger connector.

A previously-connected wallet doesn't come back on reload

Hydration is asynchronous because adapters announce asynchronously. A persisted wallet whose adapter has not been announced yet sits in pendingIds, and the manager restores it the moment a source announces it: nothing to call. It stays away when its silent reconnect failed (it is in dropped, and stays persisted for the next load) or when no source ever announces it. Inspect onHydrated's HydrationOutcome to see which bucket each wallet fell into.

It auto-reconnects when I don't want it to (or won't)

A user disconnect forgets that wallet, and disconnecting the last one (or disconnectAll) forgets every persisted connection, so none of them restores on the next load. A wallet the wallet disconnected (locked, extension removed) stays persisted and is retried. isUserDisconnected (useWalletState((s) => s.isUserDisconnected)) is session-scoped: set by a user disconnect, cleared by the next connect attempt.

Two apps on the same origin clobber each other's wallet state

Storage keys are prefixed ({prefix}-pool, {prefix}-selection, {prefix}-active, {prefix}-user-disconnected) with the default prefix butr. Set a distinct storageKeyPrefix per app sharing an origin, and pass the same prefix to readWalletSnapshot.

Signature verification fails on Solana

Verify against the signedMessage returned by signMessage, not your input bytes; Solana Wallet Standard wallets may prefix or re-encode the message. Polkadot injected signing wraps it in <Bytes>…</Bytes>. See signing.

UI flashes "not connected" on every reload

You're rendering before hydration finishes. Gate the first render on useIsHydrated(), or seed the provider with initialState from readWalletSnapshot; see SSR without a flash.