butr
Guides

Send a transaction

One sendTx(tx, { account, chain }) per wallet, typed per platform, with fixed chain-routing rules. Build the transaction with your chain library.

butr is deliberately not a transaction builder. You build the transaction with your chain library (viem, @solana/kit, @mysten/sui, bitcoinjs-lib) and either hand it to the adapter's sendTx, or take the signer from getSigner() and submit through that library.

sendTx

sendTx?: (tx: Tx, options?: { account?: Account; chain?: ChainBase }) => Promise<string>;

It asks the wallet to sign and broadcast, and resolves the transaction hash, signature, digest, or txid. It exists only when the wallet can do that, so check for it first:

import type { ConnectedWallet } from "@usebutr/core";
import { EVM_CHAINS } from "@usebutr/core";

const pay = async (wallet: ConnectedWallet<"evm">, to: string) => {
  if (!wallet.connector.sendTx) {
    throw new Error(`${wallet.connector.name} cannot send transactions`);
  }
  return wallet.connector.sendTx(
    { to, value: 10_000_000_000_000_000n },
    { account: wallet.account, chain: EVM_CHAINS.sepolia },
  );
};
  • account must be one the wallet exposes; omit it to send from the active account. An unknown account rejects instead of sending from another one.
  • chain targets this one transaction; omit it to use the wallet's current chain. See chain routing.

Transaction types

Tx is different on every platform:

PlatformTxResolves
EVMEvmTransactionRequest: an eth_sendTransaction object; bigint quantities are hex-encoded for you, and from is always the resolved accounttransaction hash
SVMUint8Array: the serialized transactionbase58 signature
SuiSuiTransactionInput: a @mysten/sui Transaction, its JSON string, or BCS bytestransaction digest
BitcoinBitcoinTransfer: { amount: bigint; recipient: string }, amount in satoshistxid
Polkadotno sendTx: build and submit extrinsics with polkadot-api through getSigner()—

A ConnectedWallet from useWallet() or useConnectedWallets() could be any platform, so narrow it before calling sendTx:

import { isPlatformWallet } from "@usebutr/core";

if (isPlatformWallet(wallet, "svm") && wallet.connector.sendTx) {
  const signature = await wallet.connector.sendTx(serializedTx, { account: wallet.account });
}

wallet.connector.chainPlatform === "svm" narrows the same way, and useSelectedWallet("svm") hands you a wallet that is already narrowed.

Chain routing

What chain does depends on the transport, and no adapter ignores it:

TransportWith chain
Wallet Standard (Solana, Sui, Bitcoin)routes this one call; the wallet stays where it is. Rejects a chain the wallet doesn't advertise.
EVM (injected, and WalletConnect's EVM namespace)an EVM wallet has one global network, so it switches first (wallet_switchEthereumChain, which may prompt), then sends. The wallet stays switched.
WalletConnect Solana, Sui, Bitcoinroutes this one call. Rejects a chain the session didn't approve.
Injected BitcoinUnisat and Xverse switch the wallet's network first; OKX and window.btc reject with ChainMismatch.
Ledgersigns for the chain it was built for; any other chain rejects with ChainMismatch.

The same rules apply to signTransaction's chain, which is the only one a Ledger has. A chain from another platform's namespace always rejects.

Sign without sending

When you broadcast yourself, use signTransaction. It takes the same options and exists only when the wallet supports it:

PlatformsignTransaction inputResolves
SVMserialized transactionthe signed transaction, ready for your RPC
SuiSuiTransactionInput{ bytes, signature } for executeTransactionBlock
BitcoinPSBT bytes (psbt.toBuffer())the signed PSBT, to finalise and broadcast

EVM adapters have none: EVM wallets sign and send in one eth_sendTransaction. On EVM, getTransactionReceipt(hash) reports "Pending", "Success", or "Error" through the wallet's provider; other platforms read receipts with your own client.

Through your chain library

getSigner() hands back the object the adapter drives, tagged by transport. Narrow on kind and give it to your library. With viem:

import { createWalletClient, custom, getAddress, parseEther } from "viem";
import { sepolia } from "viem/chains";

const signer = await wallet.connector.getSigner();
if (signer.kind !== "eip1193") {
  throw new Error(`${wallet.connector.name} has no EIP-1193 provider`);
}
const account = getAddress(wallet.account.walletAddress);
const walletClient = createWalletClient({
  account,
  chain: sepolia,
  transport: custom(signer.provider),
});

const hash = await walletClient.sendTransaction({
  account,
  chain: sepolia,
  to: BURN_ADDRESS,
  value: parseEther("0"),
});

In a component, useSigner(wallet) resolves the same signer as { status, data, error } and refetches when the wallet changes. Pass it the entry from useWallet() or useSelectedWallet("evm"); with no wallet it stays "idle".

The kind follows the transport, not the platform. An EVM wallet behind a Ledger hands back ledger-evm, so code that only handles eip1193 should say so rather than cast. Solana, Sui, and Bitcoin Wallet Standard wallets hand back { kind: "wallet-standard", wallet }; read their features with getFeature (see signing).

The viem, wagmi, gill, @solana/kit, framework-kit, Sui, and Bitcoin integrations show the full build-and-send flow for each library.

Source: packages/core/src/types/wallet.ts; apps/demo-with-viem, apps/demo-with-wagmi, apps/demo-with-gill, apps/demo-with-solana-kit, apps/demo-with-sui, and apps/demo-with-bitcoin in the butr repository. These target testnets.