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:
| Member | Wallet Standard feature | Behaviour |
|---|---|---|
sendTx | solana:signAndSendTransaction | Takes serialized transaction bytes; resolves the base58 signature. |
signMessage | solana:signMessage | Verify against the returned signedMessage: wallets may prefix or re-encode it. |
signTransaction | solana:signTransaction | Resolves the signed transaction bytes, for your own RPC client to send. |
signIn | solana:signIn | Sign In With Solana; the wallet composes the message. |
disconnect | standard:disconnect | |
switchChain | 2+ advertised Solana clusters | Re-points butr's view; the wallet is not asked to move. |
subscribe | always, when discovered | Account 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.
packages/svm/src. See the API reference.