butr
Connectors

WalletConnect

WalletConnect v2 for EVM, Solana, Sui, and Bitcoin mobile wallets: one Reown project id, one QR scan, one adapter per namespace.

@usebutr/walletconnect connects mobile wallets (Trust, Rainbow, MetaMask Mobile, Phantom, Slush, Magic Eden, …) over WalletConnect v2. It is not injected into the page: a factory builds the adapters, and fromAdapters hands them to the manager. One pairing serves every namespace you request (eip155, solana, sui, bip122), with one adapter per namespace.

Install

npm install @usebutr/walletconnect @walletconnect/universal-provider

@walletconnect/universal-provider is an optional peer dependency, imported on first use.

Register it

import type { WalletManagerConfig } from "@usebutr/core";
import { SVM_CHAINS, fromAdapters } from "@usebutr/core";
import { WalletManagerProvider } from "@usebutr/react";
import { autoDiscovery } from "@usebutr/wallets";
import { createWalletConnectAdapters } from "@usebutr/walletconnect";

const config: WalletManagerConfig = {
  sources: [
    autoDiscovery(),
    fromAdapters(
      createWalletConnectAdapters({
        metadata: { name: "My dapp", url: "https://my-dapp.example" },
        namespaces: {
          bitcoin: [],
          evm: ["eip155:1", "eip155:137"],
          sui: ["sui:mainnet"],
          svm: [SVM_CHAINS.mainnet.id, SVM_CHAINS.devnet.id],
        },
        onPairingUri: showQr,
        projectId: "<your Reown project id>",
      }),
    ),
  ],
  storageKeyPrefix: "my-app",
};

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

The adapters then appear in useDiscoveredWallets() next to the browser wallets: walletconnect-evm, walletconnect-svm and so on, named WalletConnect (EVM). With a single namespace the id and name stay walletconnect and WalletConnect. An empty array uses the namespace's default chain; an omitted key skips the namespace. The first namespace is required at pairing, the rest are optional, and an adapter whose namespace the wallet declined rejects on connect().

createWalletConnectAdapters starts loading the provider as soon as it is called. In a server-rendered app, call it only in the browser: fromAdapters(typeof window === "undefined" ? [] : createWalletConnectAdapters(options)).

Options

OptionTypeNotes
projectIdstring (required)From Reown Cloud.
namespacesPartial<Record<ChainPlatform, ReadonlyArray<string>>>CAIP-2 chains per platform. Polkadot has no namespace.
metadata{ name?; url?; description?; icons? }Shown in the mobile wallet during pairing. Some wallets refuse to pair without name and url.
onPairingUri(uri: string) => voidFires when a QR code or deep link must be shown.
idstringAdapter id base. Default "walletconnect".
namestringDisplay name. Default "WalletConnect".
iconstringDefault WALLETCONNECT_DEFAULT_ICON.

Pairing and reconnecting

Pairing starts when the user connects any of the adapters; the first connect() pairs every namespace, and a second adapter connects without a new QR code. The session ends when the last connected adapter disconnects. On reload, a session still live on the relay restores silently; otherwise the wallet waits for the user to connect again.

butr ships no QR renderer. onPairingUri hands you the URI string; render it with @walletconnect/modal, a qrcode library or your own UI. On mobile, forward it to window.location to open the OS wallet picker.

What each namespace defines

EVMSolana, Sui, Bitcoin
signMessage✓ (personal_sign)✓
sendTx✓✓
signTransaction✓
switchChain✓ (wallet_switchEthereumChain)when 2+ chains are configured
getBalance✓ (eth_getBalance, ERC-20)
getTransactionReceipt✓
subscribe✓✓
requestAccounts
signIn
  • EVM is the injected EVM adapter over the session's eip155 side, and behaves like it: sendTx({ chain }) switches the wallet first. There is no requestAccounts: more accounts means re-pairing. subscribe ignores the other namespaces' events.
  • Solana sendTx is solana_signAndSendTransaction and resolves the signature. signTransaction resolves the full signed transaction, splicing the signature in when the wallet returns only that. There is no Sign In With Solana over WalletConnect.
  • Sui accepts a Transaction, its JSON string or BCS bytes. signTransaction resolves { bytes, signature }.
  • Bitcoin sendTx({ amount, recipient }) is sendTransfer, with amount in satoshis. signTransaction(psbt) is signPsbt without broadcasting. Reown's bip122 methods still drift between wallets; see its reference.
  • subscribe on Solana, Sui and Bitcoin reports the new chain's accounts after switchChain, and disconnected when the wallet deletes the session.

Chains and accounts

Every call names its CAIP-2 chain, so a Solana request never reaches the EVM side of a shared session. On Solana, Sui and Bitcoin, options.chain routes one call without moving anything; it must be a chain the session approved, or the call rejects. switchChain re-points the adapter's later calls to another approved chain.

WalletConnect sessions name Solana clusters by genesis hash (solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp is mainnet), not by the solana:mainnet alias Wallet Standard uses. The Solana adapter translates at its boundary: pass SVM_CHAINS ids in namespaces.svm and SVM_CHAINS entries as options.chain, as on every other transport, and its accounts carry SVM_CHAINS chains. Genesis-hash ids are accepted too.

options.account must be an account the session exposes on that chain. Accounts on other approved chains surface after switchChain: a pool entry holds one chain at a time.

Signers

The EVM adapter resolves { kind: "eip1193", provider }, an EIP-1193 view of the session that viem and wagmi accept. Solana, Sui and Bitcoin resolve { kind: "walletconnect", chainId, provider }, where chainId is the session's own id for the adapter's current chain (the genesis hash on Solana). Pass it as provider.request's second argument, or UniversalProvider routes the call to the session's first namespace:

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

const signer = await wallet.connector.getSigner();
if (signer.kind === "walletconnect") {
  const result = await signer.provider.request(
    {
      method: "solana_signMessage",
      params: { message: bytesToBase58(message), pubkey: wallet.account.walletAddress },
    },
    signer.chainId,
  );
}

Namespace builders

Each platform's RPC shape lives in its own builder, exported so you can compose your own factory: evmNamespace, solanaNamespace, suiNamespace, bitcoinNamespace, and KNOWN_NAMESPACES, the table createWalletConnectAdapters dispatches through. See the API reference for their default chains and methods.

Source: packages/walletconnect/src (adapter.ts, session.ts, namespaces/{evm,svm,sui,bitcoin,caip}.ts) in the butr repository.