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/appSee 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.