viem
Narrow butr's signer to its EIP-1193 provider and hand it to viem's createWalletClient.
butr owns wallet discovery and connection state. viem owns chain abstraction
and RPC. The bridge is the signer: on an injected EVM wallet, getSigner()
resolves { kind: "eip1193", provider }, and provider is exactly what
viem's custom() transport takes.
Wire the wallet client
useSigner resolves the signer and re-resolves it when the wallet changes,
so the client is a memo over it. Narrowing on kind types the provider; a
Ledger or another non-EIP-1193 wallet yields a different kind and no client.
import { useSelectedWallet, useSigner } from "@usebutr/react";
import { useMemo } from "react";
import { createWalletClient, custom, getAddress } from "viem";
import { sepolia } from "viem/chains";
export const useWalletClient = () => {
const wallet = useSelectedWallet("evm");
const signer = useSigner(wallet);
return useMemo(() => {
if (wallet === undefined || signer.data?.kind !== "eip1193") {
return null;
}
return createWalletClient({
account: getAddress(wallet.account.walletAddress),
chain: sepolia,
transport: custom(signer.data.provider),
});
}, [signer.data, wallet]);
};Outside React, narrow the same way after await wallet.connector.getSigner().
Read, sign, send
Reads go through viem's own public client, over your RPC rather than the wallet's:
import { createPublicClient, http } from "viem";
const publicClient = createPublicClient({ chain: sepolia, transport: http() });
const wei = await publicClient.getBalance({ address: walletClient.account.address });Signing and sending go through the wallet client, so the real wallet prompts:
const signature = await walletClient.signMessage({ message: "Hello from butr + viem" });
const hash = await walletClient.sendTransaction({ to: BURN_ADDRESS, value: 0n });viem compares the wallet's current chain with the client's chain and throws
on a mismatch. Move the wallet first with butr:
import { EVM_CHAINS } from "@usebutr/core";
await wallet.connector.switchChain?.(EVM_CHAINS.sepolia);For a plain transfer you do not need viem at all: wallet.connector.sendTx({ to, value: 0n }, { chain: EVM_CHAINS.sepolia }) switches the wallet and sends in one call.
The split is the whole point: butr handles discovery and connection; viem handles chain reads, signing, and transaction submission against the provider butr exposes.
Source: apps/demo-with-viem/src/app.tsx in the butr
repository. Targets
Sepolia. Run pnpm dev --filter=demo-with-viem → https://usebutr.demo-with-viem.localhost.