butr
Integrations

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

butr-adapter-bridge.ts
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.

On this page