@solana/wallet-adapter-react
A thin bridge wraps a butr wallet as a BaseMessageSignerWalletAdapter so the standard Solana adapter hooks work unchanged.
If your app (or a dependency) already speaks @solana/wallet-adapter-react,
you don't have to rewrite it. A small bridge wraps the selected butr wallet as
a BaseMessageSignerWalletAdapter, so useWallet() / useConnection() and
anything consuming them work without changes.
The bridge
ButrAdapterBridge extends BaseMessageSignerWalletAdapter and signs through
butr's own adapter, so it needs no signer handoff: signMessage and
sendTransaction map onto the SvmAdapter's signMessage and sendTx,
each checked for presence first.
import type { Account, SvmAdapter } from "@usebutr/core";
import { SVM_CHAINS } from "@usebutr/core";
class ButrAdapterBridge extends BaseMessageSignerWalletAdapter {
readonly supportedTransactionVersions = new Set<0>([0]);
constructor(
public readonly butr: SvmAdapter,
private readonly account: Account,
) {
super();
this._publicKey = new PublicKey(account.walletAddress);
}
async signMessage(message: Uint8Array): Promise<Uint8Array> {
if (!this.butr.signMessage) {
throw new Error(`${this.butr.name} cannot sign messages`);
}
const { signature } = await this.butr.signMessage(message, { account: this.account });
return signature;
}
async sendTransaction(
transaction: Transaction | VersionedTransaction,
_connection: Connection,
): Promise<string> {
if (!this.butr.sendTx) {
throw new Error(`${this.butr.name} cannot send Solana transactions`);
}
const serialized =
transaction instanceof Transaction
? transaction.serialize({ requireAllSignatures: false })
: transaction.serialize();
// Wallet Standard routes the chain per call: devnet here never moves the
// wallet off whatever cluster it is showing.
return this.butr.sendTx(new Uint8Array(serialized), {
account: this.account,
chain: SVM_CHAINS.devnet,
});
}
}See the full file for name, icon, readyState and the
connect/disconnect lifecycle: butr already did the real handshake, so the
bridge's connect() only emits the lifecycle event.
Use it
Build the bridge from the selected Solana wallet, memoised so the adapter provider does not re-run its setup on every render, then mount the standard providers:
import {
ConnectionProvider,
WalletProvider as SolanaWalletProvider,
} from "@solana/wallet-adapter-react";
import { useSelectedWallet } from "@usebutr/react";
import { useMemo } from "react";
const wallet = useSelectedWallet("svm");
const wallets = useMemo(
() => (wallet ? [new ButrAdapterBridge(wallet.connector, wallet.account)] : []),
[wallet],
);
<ConnectionProvider endpoint={DEVNET}>
<SolanaWalletProvider autoConnect wallets={wallets}>
<YourExistingApp />
</SolanaWalletProvider>
</ConnectionProvider>;Inside, useWallet() and useConnection() work unchanged; butr is invisible
to consumer code. useSelectedWallet("svm") already types wallet.connector
as an SvmAdapter, so the bridge takes it without a cast.
The demo bridge implements signMessage and sendTransaction; signTransaction rejects.
butr.signTransaction exists when the wallet advertises solana:signTransaction and resolves the
signed bytes, which a full bridge would deserialize back into a Transaction or
VersionedTransaction.
Source: apps/demo-with-solana-wallet-adapter/src/butr-adapter-bridge.ts and app.tsx in the
butr
repository.
Targets Solana devnet. Run pnpm dev --filter=demo-with-solana-wallet-adapter →
https://usebutr.demo-with-solana-wallet-adapter.localhost.