butr
Connectors

Solana Wallet Standard

Wallet Standard discovery for Solana wallets, via @usebutr/svm.

@usebutr/svm discovers Solana wallets (Phantom, Solflare, Backpack, Magic Eden, …) through the Wallet Standard.

Register it

autoDiscovery() from @usebutr/wallets includes Solana. For a Solana-only app, discoverSvmAdapters is a source as-is:

import type { WalletManagerConfig } from "@usebutr/core";
import { WalletManagerProvider } from "@usebutr/react";
import { discoverSvmAdapters } from "@usebutr/svm";

const config: WalletManagerConfig = {
  sources: [discoverSvmAdapters],
  storageKeyPrefix: "my-app",
};

export const Providers = ({ children }: { children: React.ReactNode }) => (
  <WalletManagerProvider config={config}>{children}</WalletManagerProvider>
);

The adapter id is wallet-standard:svm-<wallet name>, e.g. wallet-standard:svm-phantom.

@usebutr/svm lazily imports @wallet-standard/app, an optional peer dependency. That asynchronous warmup is one reason a restored Solana wallet can sit in pendingIds for a moment; see hydration.

What the adapter defines

Each method exists only when the wallet advertises the matching feature:

MemberWallet Standard featureBehaviour
sendTxsolana:signAndSendTransactionTakes serialized transaction bytes; resolves the base58 signature.
signMessagesolana:signMessageVerify against the returned signedMessage: wallets may prefix or re-encode it.
signTransactionsolana:signTransactionResolves the signed transaction bytes, for your own RPC client to send.
signInsolana:signInSign In With Solana; the wallet composes the message.
disconnectstandard:disconnect
switchChain2+ advertised Solana clustersRe-points butr's view; the wallet is not asked to move.
subscribealways, when discoveredAccount and chain changes, and the wallet unregistering.

There is no requestAccounts: Wallet Standard wallets expose all their accounts at once. There is no getBalance or getTransactionReceipt: they need an RPC client, which butr does not ship, so useBalance stays idle.

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

const wallet = useSelectedWallet("svm");

const send = async (serializedTx: Uint8Array) => {
  if (!wallet?.connector.sendTx) {
    throw new Error("This wallet cannot send Solana transactions");
  }
  return wallet.connector.sendTx(serializedTx, { chain: SVM_CHAINS.devnet });
};

Chains and accounts

Wallet Standard carries the chain in every call, so options.chain routes one transaction without moving the wallet. The chain must be one the wallet advertises, or the call rejects. The initial chain is Solana mainnet when advertised, else the wallet's first Solana chain. account must be one the wallet exposes; an unknown one rejects.

Signer

getSigner() resolves { kind: "wallet-standard", wallet }. Reach a feature butr does not wrap with getFeature, typed by the feature shapes @usebutr/svm exports:

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

const signAndSend = async (wallet: ConnectedWallet<"svm">, serializedTx: Uint8Array) => {
  const signer = await wallet.connector.getSigner();
  if (signer.kind !== "wallet-standard") {
    throw new Error(`${wallet.connector.name} is not a Wallet Standard wallet`);
  }
  const feature = getFeature<SolanaSignAndSendTransactionFeature>(
    signer.wallet,
    "solana:signAndSendTransaction",
    "signAndSendTransaction",
  );
  const account = findAccount(signer.wallet.accounts, wallet.account.walletAddress);
  if (feature === undefined || account === undefined) {
    throw new Error(`${wallet.connector.name} cannot sign and send for this account`);
  }
  const [output] = await feature.signAndSendTransaction({
    account,
    chain: "solana:devnet",
    transaction: serializedTx,
  });
  return output?.signature;
};

See the Solana integrations for building transactions with framework-kit, gill, @solana/kit or @solana/web3.js.

Source: packages/svm/src. See the API reference.