butr
Integrations

@solana/kit

Build the transaction message with kit's functional pipeline, sign and send through the wallet's Wallet Standard features.

@solana/kit is the modular Solana SDK. butr owns the wallet; kit owns the RPC client and the functional message builder; the wallet's Wallet Standard features own signing.

Get the wallet

useSelectedWallet("svm") types the wallet to Solana, and useSigner resolves its signer. On a Wallet Standard wallet the signer carries the raw wallet, whose features you call directly:

import { useSelectedWallet, useSigner } from "@usebutr/react";

const wallet = useSelectedWallet("svm");
const signer = useSigner(wallet);
// `null` for a WalletConnect or Ledger wallet, which resolve another kind.
const walletStd = signer.data?.kind === "wallet-standard" ? signer.data.wallet : null;

Read state

import { address, createSolanaRpc } from "@solana/kit";

const rpc = createSolanaRpc("https://api.devnet.solana.com");

const { value } = await rpc.getBalance(address(wallet.account.walletAddress)).send();
const sol = `${Number(value) / 1_000_000_000} SOL`;

Sign a message

getFeature reads a feature off the wallet, typed by the feature shape @usebutr/svm exports, and findAccount finds the Wallet Standard account for butr's address. Neither needs a cast:

import type { ConnectedWallet } from "@usebutr/core";
import type { SolanaSignMessageFeature } from "@usebutr/svm";
import type { WalletStandardWallet } from "@usebutr/wallet-standard-shared";
import { findAccount, getFeature } from "@usebutr/wallet-standard-shared";

const signMessage = async (
  walletStd: WalletStandardWallet,
  wallet: ConnectedWallet<"svm">,
  message: Uint8Array,
) => {
  const feature = getFeature<SolanaSignMessageFeature>(
    walletStd,
    "solana:signMessage",
    "signMessage",
  );
  const account = findAccount(walletStd.accounts, wallet.account.walletAddress);
  if (feature === undefined || account === undefined) {
    throw new Error("This wallet cannot sign messages for the active account");
  }
  const [output] = await feature.signMessage({ account, message });
  return output; // verify `output.signature` against `output.signedMessage`
};

Build and send a transaction

Build the message with kit's pipe() and compile it to wire bytes:

import type { Address } from "@solana/kit";
import {
  appendTransactionMessageInstruction,
  compileTransaction,
  createTransactionMessage,
  getBase64EncodedWireTransaction,
  pipe,
  setTransactionMessageFeePayer,
  setTransactionMessageLifetimeUsingBlockhash,
} from "@solana/kit";
import { base64ToBytes } from "@usebutr/core";

const buildTransfer = async (payer: Address) => {
  const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
  const transactionMessage = pipe(
    createTransactionMessage({ version: 0 }),
    (m) => setTransactionMessageFeePayer(payer, m),
    (m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),
    (m) => appendTransactionMessageInstruction(transferIx, m),
  );
  return base64ToBytes(getBase64EncodedWireTransaction(compileTransaction(transactionMessage)));
};

Then hand the bytes to solana:signAndSendTransaction. Its chain input routes this one call to devnet without moving the wallet:

import { bytesToBase58 } from "@usebutr/core";
import type { SolanaSignAndSendTransactionFeature } from "@usebutr/svm";

const signAndSend = async (
  walletStd: WalletStandardWallet,
  wallet: ConnectedWallet<"svm">,
  transaction: Uint8Array,
) => {
  const feature = getFeature<SolanaSignAndSendTransactionFeature>(
    walletStd,
    "solana:signAndSendTransaction",
    "signAndSendTransaction",
  );
  const account = findAccount(walletStd.accounts, wallet.account.walletAddress);
  if (feature === undefined || account === undefined) {
    throw new Error("This wallet cannot send transactions for the active account");
  }
  const [output] = await feature.signAndSendTransaction({
    account,
    chain: "solana:devnet",
    transaction,
  });
  if (output === undefined) {
    throw new Error("signAndSendTransaction returned no output");
  }
  return bytesToBase58(output.signature);
};

butr's adapter wraps the same two features: wallet.connector.sendTx(bytes, { chain: SVM_CHAINS.devnet }) resolves the base58 signature, and wallet.connector.signMessage(message) the signature. Both also work on WalletConnect wallets, which have no Wallet Standard handle. Check for the method first; it exists only when the wallet can back it.

The demo hand-rolls the System Program transfer instruction (a 12-byte buffer: u32 variant 2 + u64 lamports) rather than depending on @solana-program/system, whose codec imports drift between @solana/kit versions. Either approach works.

Source: apps/demo-with-solana-kit/src/app.tsx in the butr repository. Targets Solana devnet. Run pnpm dev --filter=demo-with-solana-kit → https://usebutr.demo-with-solana-kit.localhost.