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
| Option | Type | Notes |
|---|---|---|
projectId | string (required) | From Reown Cloud. |
namespaces | Partial<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) => void | Fires when a QR code or deep link must be shown. |
id | string | Adapter id base. Default "walletconnect". |
name | string | Display name. Default "WalletConnect". |
icon | string | Default 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
| EVM | Solana, 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
eip155side, and behaves like it:sendTx({ chain })switches the wallet first. There is norequestAccounts: more accounts means re-pairing.subscribeignores the other namespaces' events. - Solana
sendTxissolana_signAndSendTransactionand resolves the signature.signTransactionresolves 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.signTransactionresolves{ bytes, signature }. - Bitcoin
sendTx({ amount, recipient })issendTransfer, withamountin satoshis.signTransaction(psbt)issignPsbtwithout broadcasting. Reown'sbip122methods still drift between wallets; see its reference. subscribeon Solana, Sui and Bitcoin reports the new chain's accounts afterswitchChain, anddisconnectedwhen 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.