butr
Get started

Quickstart

A working multi-chain wallet UI in five minutes with WalletManagerProvider.

This walks through the smallest useful app: discover wallets, connect one, show it, disconnect. It mirrors the demo-vite reference app, which uses the batteries-included @usebutr/wallets package.

1. Wrap your app in the provider

WalletManagerProvider from @usebutr/react is the single provider. It takes a config whose sources say where adapters come from. autoDiscovery() from @usebutr/wallets discovers EVM (EIP-6963), Solana / Sui / Bitcoin (Wallet Standard), and Polkadot (injectedWeb3) wallets in one source. storageKeyPrefix namespaces the storage keys.

The provider reads config once, at mount, so define it at module scope:

wallet-provider.tsx
import type { WalletManagerConfig } from "@usebutr/core";
import { WalletManagerProvider } from "@usebutr/react";
import { autoDiscovery } from "@usebutr/wallets";
import type { ReactNode } from "react";

const config: WalletManagerConfig = {
  sources: [autoDiscovery()],
  storageKeyPrefix: "butr-demo",
};

const WalletProvider = ({ children }: { children: ReactNode }) => (
  <WalletManagerProvider config={config}>{children}</WalletManagerProvider>
);

export { WalletProvider };

Mount it above your tree:

main.tsx
import ReactDOM from "react-dom/client";
import { App } from "./app";
import { WalletProvider } from "./wallet-provider";

ReactDOM.createRoot(document.querySelector("#root")!).render(
  <WalletProvider>
    <App />
  </WalletProvider>,
);

2. Wait for hydration

butr restores the previous session asynchronously. Gate your UI on useIsHydrated() so you don't flash a "not connected" state on reload.

import { useIsHydrated } from "@usebutr/react";

const Content = () => {
  const isHydrated = useIsHydrated();
  if (!isHydrated) return <p>Loading…</p>;
  // …
};

3. List discovered wallets and connect

useDiscoveredWallets() is the live list of adapters that have announced themselves. useConnect() returns connect, which takes an adapter id, plus the state of the latest attempt. connect never throws: a failure lands in error.

import { useConnect, useConnectedWallets, useDiscoveredWallets } from "@usebutr/react";

const WalletPicker = () => {
  const { connect, connectingId, error } = useConnect();
  const discovered = useDiscoveredWallets();
  const connected = useConnectedWallets();

  const available = discovered.filter((d) => !connected.some((c) => c.connector.id === d.id));

  return (
    <>
      <ul>
        {available.map((wallet) => (
          <li key={wallet.id}>
            <button
              disabled={connectingId !== null}
              type="button"
              onClick={() => connect(wallet.id)}
            >
              Connect {wallet.name} ({wallet.chainPlatform})
            </button>
          </li>
        ))}
      </ul>
      {error ? <p>{error.message}</p> : null}
    </>
  );
};

4. Show connected wallets and disconnect

Actions come from useWalletManager(); its methods are stable references.

import { useConnectedWallets, useWalletManager } from "@usebutr/react";

const Connected = () => {
  const wallets = useConnectedWallets();
  const { disconnect } = useWalletManager();

  return (
    <ul>
      {wallets.map((wallet) => (
        <li key={wallet.connector.id}>
          {wallet.connector.name} — {wallet.account.walletAddress}
          <button type="button" onClick={() => disconnect(wallet.connector.id)}>
            Disconnect
          </button>
        </li>
      ))}
    </ul>
  );
};

That is a complete multi-chain connect flow. What else wallet.connector can do depends on the wallet: signMessage, sendTx, and switchChain exist only when they work, and getSigner always does. See capabilities and the guides.

Source: apps/demo-vite in the butr repository. Run it locally with pnpm dev --filter=demo-vite (serves on https://usebutr.demo-vite.localhost). Hosted at demo.usebutr.com.

Next steps