butr
Connectors

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, and window.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

MemberWallet StandardUnisat, OKX, window.btc (unisat)Xverse (sats-connect)
sendTxwith bitcoin:sendTransferwhen the provider has sendBitcoin✓ (sendTransfer)
signMessagewith bitcoin:signMessage✓✓, payment or ordinals address
signTransactionwith bitcoin:signPsbt✓ (signPsbt)✓ (signPsbt)
switchChain2+ advertised networks; re-points butr's viewwhen the provider has switchNetwork✓ (wallet_changeNetwork)
subscribe✓when the provider has on✓, reports butr's own network moves
disconnectwith standard:disconnectnone✓
  • sendTx takes a BitcoinTransfer, { amount, recipient } with amount in satoshis. The wallet builds, signs and broadcasts; it resolves the txid.
  • signTransaction takes 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 getBalance or getTransactionReceipt: 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.chain per 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 without switchNetwork (OKX, usually window.btc) cannot switch at all, and signet is never a switch target, so those calls reject with a ChainMismatch ConnectionError. These wallets sign with their active account only: any other account rejects.
  • 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.