Bitcoin
Wallet Standard discovery for Bitcoin wallets plus injected fallbacks (Unisat, OKX, Xverse), via @usebutr/bitcoin.
@usebutr/bitcoin discovers Bitcoin wallets two ways:
- Wallet Standard: Phantom, Magic Eden, Leather, …
- Injected fallback:
window.unisat,window.okxwallet.bitcoin, Xverse's sats-connect provider, andwindow.btc.
Register it
autoDiscovery() from @usebutr/wallets includes both channels and runs the
injected one only when Wallet Standard found no Bitcoin wallet, so a wallet
on both channels lists once. For a Bitcoin-only app, keep that pairing:
import type { WalletManagerConfig } from "@usebutr/core";
import { WalletManagerProvider } from "@usebutr/react";
import { autoDiscovery } from "@usebutr/wallets";
const config: WalletManagerConfig = {
sources: [autoDiscovery(["bitcoin"])],
storageKeyPrefix: "my-app",
};
export const Providers = ({ children }: { children: React.ReactNode }) => (
<WalletManagerProvider config={config}>{children}</WalletManagerProvider>
);discoverBitcoinAdapters alone is Wallet Standard only; add
discoverInjectedBitcoinAdapter yourself only if you accept that a wallet on
both channels can list twice.
Wallet Standard discovery lazily imports @wallet-standard/app, an optional peer dependency. A
restored Bitcoin wallet can sit in pendingIds for a moment during that warmup; see
hydration.
What each adapter defines
| Member | Wallet Standard | Unisat, OKX, window.btc (unisat) | Xverse (sats-connect) |
|---|---|---|---|
sendTx | with bitcoin:sendTransfer | when the provider has sendBitcoin | ✓ (sendTransfer) |
signMessage | with bitcoin:signMessage | ✓ | ✓, payment or ordinals address |
signTransaction | with bitcoin:signPsbt | ✓ (signPsbt) | ✓ (signPsbt) |
switchChain | 2+ advertised networks; re-points butr's view | when the provider has switchNetwork | ✓ (wallet_changeNetwork) |
subscribe | ✓ | when the provider has on | ✓, reports butr's own network moves |
disconnect | with standard:disconnect | none | ✓ |
sendTxtakes aBitcoinTransfer,{ amount, recipient }withamountin satoshis. The wallet builds, signs and broadcasts; it resolves the txid.signTransactiontakes PSBT bytes (psbt.toBuffer()) and resolves the signed PSBT, for you to finalise and broadcast through your own Esplora or Electrum client.- There is no
getBalanceorgetTransactionReceipt: butr ships no Bitcoin RPC.
import { BITCOIN_CHAINS } from "@usebutr/core";
const wallet = useSelectedWallet("bitcoin");
const pay = async (recipient: string) => {
if (!wallet?.connector.sendTx) {
throw new Error("This wallet cannot send Bitcoin");
}
return wallet.connector.sendTx(
{ amount: 10_000n, recipient },
{ account: wallet.account, chain: BITCOIN_CHAINS.testnet },
);
};Chains and accounts
- Wallet Standard routes
options.chainper call; the wallet must advertise it. The initial chain is Bitcoin mainnet when advertised. - Unisat-style wallets have one network for the whole wallet. A call
targeting another chain switches it first through
switchNetwork, which reaches mainnet and testnet only. A provider withoutswitchNetwork(OKX, usuallywindow.btc) cannot switch at all, and signet is never a switch target, so those calls reject with aChainMismatchConnectionError. These wallets sign with their active account only: any otheraccountrejects. - Xverse switches its network through
wallet_changeNetwork(mainnet, testnet,BITCOIN_CHAINS.testnet4, signet, regtest), then re-reads its addresses, since a Bitcoin address encodes its network. It sends from its payment address only.
import type { ConnectedWallet } from "@usebutr/core";
import { BITCOIN_CHAINS, ConnectionError } from "@usebutr/core";
const signOnSignet = async (wallet: ConnectedWallet<"bitcoin">, psbt: Uint8Array) => {
try {
return await wallet.connector.signTransaction?.(psbt, { chain: BITCOIN_CHAINS.signet });
} catch (error) {
if (error instanceof ConnectionError && error.kind === "ChainMismatch") {
// The wallet cannot move there itself: ask the user to switch networks.
}
throw error;
}
};Address formats vary by wallet. Phantom exposes native SegWit only; Xverse exposes a payment
address as the account and signs with its ordinals address too. Read account.walletAddress and
detect the format from its prefix.
Signers
getSigner() resolves the object the adapter drives, tagged by transport:
const signer = await wallet.connector.getSigner();
switch (signer.kind) {
case "wallet-standard": {
// signer.wallet: read features with `getFeature`
break;
}
case "unisat": {
// signer.provider: the UniSat-style provider
break;
}
case "sats-connect": {
// signer.provider: Xverse's `request(method, params)` provider
break;
}
}Wallet Standard features butr does not wrap are reachable with getFeature
from @usebutr/wallet-standard-shared and the feature types
@usebutr/bitcoin exports. See the bitcoinjs-lib integration.
Source: packages/bitcoin/src (wallet-standard-adapter.ts, injected/). See the API
reference.