butr
Guides

Sign a message

Per-account message signing, and why you verify against signedMessage on Solana.

signMessage lives on the wallet half of the adapter. It takes the message bytes and an optional account, which must be one the wallet exposes:

signMessage?: (
  message: Uint8Array,
  options?: { account?: Account },
) => Promise<{ signature: Uint8Array; signedMessage: Uint8Array }>;

Omit account to sign with the active account. An account the wallet does not expose rejects; no adapter falls back to another account.

Sign with a connected wallet

signMessage exists only when the wallet can sign, so the check that gates the button is the same one TypeScript wants before the call. The demo-vite reference signs per account straight off the adapter:

import type { Account, ConnectedWallet } from "@usebutr/core";

const SignButton = ({ account, wallet }: { account: Account; wallet: ConnectedWallet }) => {
  const handleSign = async () => {
    const bytes = new TextEncoder().encode("Hello from the butr demo");
    const signed = await wallet.connector.signMessage?.(bytes, { account });
    // send `signed.signature` and `signed.signedMessage` to whatever verifies them
  };

  // No signMessage, no button.
  return wallet.connector.signMessage ? (
    <button type="button" onClick={() => void handleSign()}>
      Sign
    </button>
  ) : null;
};

What comes back depends on the platform: an EIP-191 personal_sign signature on EVM, a Solana, Sui, or Bitcoin Signed Message signature on those, a signRaw result on Polkadot. See platforms.

Verify the signature against the returned signedMessage, not your input bytes. Solana Wallet Standard wallets may prefix or re-encode the message internally. EVM wallets echo the input, so there it's the same bytes, but writing verification against signedMessage works for both.

For a full nonce-and-verify login, use createSignInFlow.

Calling a Wallet Standard feature directly

When you need a feature butr doesn't wrap, reach the wallet through getSigner(). Narrow on kind, then read the feature with getFeature from @usebutr/wallet-standard-shared, which checks the method exists before handing it back:

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

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<SolanaSignMessageFeature>(
  signer.wallet,
  "solana:signMessage",
  "signMessage",
);
const account = findAccount(signer.wallet.accounts, wallet.account.walletAddress);
if (!feature || !account) {
  throw new Error("Wallet cannot sign Solana messages for this account");
}
const message = new TextEncoder().encode("Hello");
const [output] = await feature.signMessage({ account, message });

No casts and no structural guards: signer.kind narrows the union, getFeature returns the typed feature or undefined, and findAccount returns the wallet's own account object for a butr address.

Signing through an integration library

If you've wired viem / wagmi (see Integrations), sign through that library's API instead; it uses the same provider butr handed you:

// viem
const sig = await walletClient.signMessage({ account, message: "Hello" });
// wagmi
const sig = await signMessage(wagmiConfig, { message: "Hello" });

Source: apps/demo-vite/src/app.tsx; handleSign in apps/demo-with-solana-kit/src/app.tsx and apps/demo-with-viem/src/app.tsx.