butr
Connectors

Injected EVM

EIP-6963 discovery with an EIP-1193 injected fallback, via @usebutr/evm.

@usebutr/evm discovers browser EVM wallets (MetaMask, Rabby, Brave, Coinbase, …) through EIP-6963, where wallets announce themselves with window events instead of fighting over window.ethereum.

Register it

autoDiscovery() from @usebutr/wallets includes EVM. For an EVM-only app, discoverEvmAdapters is a source as-is:

import type { WalletManagerConfig } from "@usebutr/core";
import { discoverEvmAdapters } from "@usebutr/evm";
import { WalletManagerProvider } from "@usebutr/react";

const config: WalletManagerConfig = {
  sources: [discoverEvmAdapters],
  storageKeyPrefix: "my-app",
};

export const Providers = ({ children }: { children: React.ReactNode }) => (
  <WalletManagerProvider config={config}>{children}</WalletManagerProvider>
);

The adapter id is the wallet's reverse-DNS name (io.metamask, io.rabby).

Injected fallback

Some wallets expose only window.ethereum and never announce through EIP-6963. discoverInjectedAdapter waits 150 ms, then emits one "Browser wallet" adapter with the id injected:legacy. On its own it would list a wallet a second time next to its EIP-6963 adapter, so pair the two through autoDiscovery(["evm"]), which runs the fallback only when EIP-6963 announced nothing. autoDiscovery(["evm"], { fallbacks: false }) skips it.

What the adapter defines

Every injected EVM adapter defines the same members:

MemberBehaviour
signMessagepersonal_sign; resolves the signature bytes and your message as signedMessage.
sendTxeth_sendTransaction; resolves the hash. bigint quantities are encoded as hex; from is always the resolved account.
switchChainwallet_switchEthereumChain. Rejects a chain outside eip155.
requestAccountswallet_requestPermissions, falling back to eth_requestAccounts where unsupported. Call it through useWalletManager().requestAccounts(id) so the pool entry refreshes.
getBalanceeth_getBalance, or an ERC-20 balanceOf when token is set. The native balance is labelled "ETH" on every chain.
getTransactionReceipteth_getTransactionReceipt; "Pending" until mined.
subscribeBridges accountsChanged, chainChanged, connect and disconnect.
disconnectwallet_revokePermissions. Many wallets ignore it, so their own auto-reconnect may outlive it.

There is no signTransaction and no signIn on EVM: wallets sign and send in one step, and SIWE is app-level.

Chains and accounts

An EVM wallet has one global network. sendTx(tx, { chain }) switches it to chain first, then sends:

import { EVM_CHAINS } from "@usebutr/core";

const wallet = useSelectedWallet("evm");

const pay = async (to: string) => {
  if (!wallet?.connector.sendTx) {
    return;
  }
  return wallet.connector.sendTx(
    { to, value: 10_000_000_000_000_000n },
    { account: wallet.account, chain: EVM_CHAINS.base },
  );
};

butr does not call wallet_addEthereumChain, so a chain the wallet does not know rejects with the wallet's own error. Chain registries (EVM_CHAINS, EVM_CHAINS_LIST) come from @usebutr/core; see multi-chain.

account must be one the wallet exposes, compared case-insensitively; an unknown one rejects.

Signer

getSigner() resolves { kind: "eip1193", provider }, the wallet's own provider, ready for viem or wagmi. See the viem integration.

Source: packages/evm/src. See the API reference.