@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.