Skip to content

Commit 702ca6f

Browse files
committed
docs(skills): split conventions.md into focused references; dedupe cross-cutting blocks
Reduce documentation drift surface by giving each topic a single canonical home, per the Agent Skills guidance (focused reference files, TOCs >300 lines) and mirroring MetaMask/smart-accounts-kit#264's domain-split refs. - Split the 610-line conventions.md into 7 focused, domain-organized references (evm, events, multichain, solana, react-native, csp, testing), each with its own table of contents. conventions.md is now a 93-line always-on core (import paths, config, supportedNetworks, singleton, error handling, connection state) plus a topic index routing to the rest. - Repoint the CSP cross-links in setup-evm-browser, setup-solana-browser, setup-multichain, and troubleshooting at the new canonical csp.md. - Point the two React Native setup workflows at react-native.md as the canonical polyfill/Metro reference (keeping their runnable recipes). - Update SKILL.md's always-on pointer to describe the core + focused refs. All skills/**/*.md pass prettier (3.6.2) and every internal link/anchor resolves.
1 parent c1952d1 commit 702ca6f

15 files changed

Lines changed: 620 additions & 549 deletions

skills/metamask-connect/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Pick the client for your integration:
3030

3131
## Always-on conventions
3232

33-
Before writing or reviewing **any** MetaMask Connect code, read [references/conventions.md](references/conventions.md)hex chain IDs, `supportedNetworks` validation, EIP-1193 provider events, multichain session lifecycle, Solana constraints, React Native polyfills, and testing patterns. Apply it alongside every workflow below.
33+
Before writing or reviewing **any** MetaMask Connect code, read [references/conventions.md](references/conventions.md)the always-on core guardrails (import paths, required config, `supportedNetworks`, singleton behavior, error handling, connection state) plus a topic index into focused references. Then load the focused reference(s) for your task: [evm.md](references/evm.md) (chain IDs / `switchChain`), [events.md](references/events.md), [multichain.md](references/multichain.md), [solana.md](references/solana.md), [react-native.md](references/react-native.md) (polyfills / Metro), [csp.md](references/csp.md), [testing.md](references/testing.md). Each topic has a single canonical home, so apply the relevant reference alongside every workflow below.
3434

3535
## Set up (choose your stack)
3636

skills/metamask-connect/references/conventions.md

Lines changed: 27 additions & 544 deletions
Large diffs are not rendered by default.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# MetaMask Connect — Content Security Policy (browser)
2+
3+
The canonical reference for CSP origins the SDK needs in the browser. Not relevant in Node.js or React Native. For always-on core guardrails see [conventions.md](conventions.md).
4+
5+
A host page with a strict CSP must allow MetaMask Connect's origins, or browser integrations fail in ways that look like other bugs — a blocked relay socket presents exactly like "connection hangs".
6+
7+
**Required:**
8+
9+
- `connect-src wss://mm-sdk-relay.api.cx.metamask.io` — the relay WebSocket used for remote (mobile / no-extension) connections. It cannot be proxied or deferred from within the library, so remote connections fail without it.
10+
- `img-src data:` — the install/QR modal in `@metamask/multichain-ui` embeds the MetaMask fox as a `data:` URI inside the generated QR code (it sets `saveAsBlob: false`), so the QR will not render without it.
11+
12+
**Also consider:**
13+
14+
- `connect-src https://mm-sdk-analytics.api.cx.metamask.io` — the `@metamask/analytics` telemetry endpoint, used when analytics are enabled (the default). Unnecessary if you set `analytics: { enabled: false }`.
15+
- `style-src 'unsafe-inline'``@metamask/multichain-ui` is built with Stencil, which injects component styles at runtime into Shadow DOM. Strict CSPs without `'unsafe-inline'` (or an equivalent nonce/hash strategy) may break modal styling.
16+
- RPC endpoints you pass to `supportedNetworks` (e.g. `https://*.infura.io` or your own node provider) — add the matching `connect-src` entries for whatever you configure.
17+
- `https://metamask.app.link` and `metamask://` — mobile deeplinks / universal links. These are top-level navigations and not normally subject to `connect-src`, but strict policies using `navigate-to` / `form-action` may need to allow them.
18+
19+
Minimal example (default analytics endpoint + Infura + install modal):
20+
21+
```
22+
connect-src 'self' wss://mm-sdk-relay.api.cx.metamask.io https://mm-sdk-analytics.api.cx.metamask.io https://*.infura.io;
23+
img-src 'self' data:;
24+
style-src 'self' 'unsafe-inline';
25+
```
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# MetaMask Connect — Event Handling
2+
3+
EVM provider and `connect-evm` event handling: EIP-1193 events, SDK `eventHandlers`, payload types, `display_uri` timing, EIP-6963 announcement, cached state, and listener best practices. For multichain `wallet_sessionChanged` see [multichain.md](multichain.md); for always-on core guardrails see [conventions.md](conventions.md).
4+
5+
## Contents
6+
7+
- [EIP-1193 Events (EVM Provider)](#eip-1193-events-evm-provider)
8+
- [chainChanged Payload Type](#chainchanged-payload-type)
9+
- [SDK eventHandlers (Client Options)](#sdk-eventhandlers-client-options)
10+
- [display_uri Timing](#display_uri-timing)
11+
- [Multichain stateChanged Event](#multichain-statechanged-event)
12+
- [Transport Events](#transport-events)
13+
- [EIP-6963 Provider Announcement](#eip-6963-provider-announcement)
14+
- [Cached State Methods](#cached-state-methods)
15+
- [Client Status Property](#client-status-property)
16+
- [Event Listener Best Practices](#event-listener-best-practices)
17+
18+
## EIP-1193 Events (EVM Provider)
19+
20+
- **`connect`** — fired when the provider establishes a connection; payload: `{ chainId: Hex; accounts: Address[] }`
21+
- **`disconnect`** — fired when the provider loses connection; **no payload**
22+
- **`accountsChanged`** — fired when the user's accounts change; payload: `string[]` (array of addresses)
23+
- **`chainChanged`** — fired when the active chain changes; payload: `string` (**hex** chain ID, not decimal)
24+
- **`message`** — part of the EIP-1193 provider event _type_ (payload: `{ type: string; data: unknown }`), but **not currently emitted** by `@metamask/connect-evm`; don't rely on it for subscription delivery
25+
26+
```typescript
27+
const provider = client.getProvider();
28+
29+
provider.on('accountsChanged', (accounts: string[]) => {
30+
console.log('New accounts:', accounts);
31+
});
32+
33+
provider.on('chainChanged', (chainId: string) => {
34+
// chainId is HEX (e.g., '0x1'), NOT decimal
35+
console.log('New chain:', chainId);
36+
});
37+
38+
provider.on(
39+
'connect',
40+
({ chainId, accounts }: { chainId: string; accounts: string[] }) => {
41+
console.log('Connected to chain:', chainId, 'accounts:', accounts);
42+
},
43+
);
44+
45+
provider.on('disconnect', () => {
46+
// No payload — the event itself is the signal
47+
console.log('Disconnected');
48+
});
49+
```
50+
51+
## chainChanged Payload Type
52+
53+
- `chainChanged` emits a **hex string** (e.g., `'0x1'`, `'0x89'`), **not a decimal number**
54+
- Never compare directly with decimal numbers: `chainId === 1` will always be false
55+
- Convert if needed: `parseInt(chainId, 16)` to get the decimal chain ID
56+
- This is a common source of bugs — always treat chainChanged payload as a hex string
57+
58+
## SDK eventHandlers (Client Options)
59+
60+
- Configure event callbacks directly in client options via `eventHandlers`:
61+
- `connect` — same as EIP-1193 connect
62+
- `disconnect` — same as EIP-1193 disconnect
63+
- `accountsChanged` — same as EIP-1193 accountsChanged
64+
- `chainChanged` — same as EIP-1193 chainChanged
65+
- `displayUri` — fires with the connection URI string for QR code rendering
66+
- `connectAndSign` — fires with the signature result from `connectAndSign` flow
67+
- `connectWith` — fires with the result from `connectWith` flow
68+
69+
```typescript
70+
const client = await createEVMClient({
71+
dapp: { name: 'My DApp' },
72+
eventHandlers: {
73+
accountsChanged: (accounts) => updateUI(accounts),
74+
chainChanged: (chainId) => updateChain(chainId),
75+
displayUri: (uri) => renderQrCode(uri),
76+
},
77+
});
78+
```
79+
80+
## display_uri Timing
81+
82+
- `display_uri` only fires during the `'connecting'` state — between calling `connect()` and the connection resolving
83+
- Register the `display_uri` listener **before** calling `connect()` — registering after may miss the event
84+
- The URI is a one-time-use pairing token; once used or expired, it cannot be reused
85+
- On connection error, do not attempt to regenerate or reuse the QR — call `connect()` again for a new URI
86+
- In non-headless mode, the SDK renders its own QR modal; `display_uri` is mainly useful in headless mode
87+
88+
## Multichain stateChanged Event
89+
90+
- The multichain core client emits `stateChanged` whenever the connection status changes
91+
- Listen via `client.on('stateChanged', (status) => ...)` on the multichain client, where `status` is a `ConnectionStatus` string
92+
- This is available on the multichain client (`createMultichainClient`) and on the Solana client's public `.core` property. The EVM client does **not** expose `.core` (it is private) — use `client.status` / provider events there
93+
94+
## Transport Events
95+
96+
- For the Mobile Wallet Protocol (MWP) transport, the SDK attempts to resume an interrupted session — including a reconnection check when the browser tab regains focus — so you generally don't need to wire this up manually. This resumption logic is MWP-specific; the browser-extension transport does not use it.
97+
- The provider's `disconnect` event carries no error payload — treat the event itself as the signal, and do not expect legacy json-rpc-engine codes (e.g. `1013`) from the connect-\* packages
98+
99+
## EIP-6963 Provider Announcement
100+
101+
- Since `@metamask/connect-evm` 2.0.0, the MMConnect-managed EIP-1193 provider is announced through **EIP-6963** (`eip6963:announceProvider`) **by default** when native MetaMask has not already announced its own provider — so wallet-discovery UIs (RainbowKit, ConnectKit, Web3Modal, wagmi's `injected`/`metaMask` discovery, etc.) can surface the MMConnect provider automatically
102+
- The auto-announce is suppressed when native MetaMask (extension) has already announced, and EIP-6963 extension detection is restricted to native MetaMask RDNS values so MMConnect announcements do not get mistaken for — or select — the browser-extension transport
103+
- Pass `skipAutoAnnounce: true` to `createEVMClient()` to opt out of the automatic announcement (e.g. when you want to control discovery manually or avoid a duplicate entry alongside another integration)
104+
- Call `client.announceProvider()` to re-announce on demand — useful after `skipAutoAnnounce`, or to re-emit in response to a late `eip6963:requestProvider` event from a discovery library that mounted after the SDK initialized
105+
106+
## Cached State Methods
107+
108+
- `eth_accounts` and `eth_chainId` return locally cached state from the SDK rather than making RPC calls
109+
- The cached values are kept in sync via `accountsChanged` and `chainChanged` events, so they reflect the current state after connection
110+
- Use `client.getChainId()` to get the current hex chain ID (returns `Hex | undefined`)
111+
- Use `client.getAccount()` to get the current account address (returns `Address | undefined`)
112+
- Since `@metamask/connect-evm` 1.3.1, the intercepted EIP-1193 account requests return method-specific shapes that match the spec: `provider.request({ method: 'eth_requestAccounts' })` resolves to an accounts array (`Address[]`), and `provider.request({ method: 'eth_coinbase' })` resolves to the **currently selected account** (`Address`), **not** the full accounts array. Do not destructure `eth_coinbase` as an array (`const [acct] = await provider.request({ method: 'eth_coinbase' })`) — treat it as a single address string
113+
- Since `@metamask/connect-evm` 2.0.0, more intercepted EIP-1193 requests return spec-compatible values: `provider.request({ method: 'wallet_requestPermissions' })` resolves to the **requested permissions** array, while successful `wallet_switchEthereumChain` and `wallet_addEthereumChain` requests resolve to **`null`** (per EIP-3326 / EIP-3085). Do not expect a truthy value back from a successful switch/add — branch on the absence of a thrown error, not on the resolved value
114+
115+
## Client Status Property
116+
117+
- On the EVM client (`createEVMClient`), `client.status` is `ConnectEvmStatus`: `'connecting'`, `'connected'`, or `'disconnected'` (since `@metamask/connect-evm` 0.11.0 it no longer proxies `MultichainClient.status`)
118+
- On the multichain client (`createMultichainClient`), `client.status` is the 5-value `ConnectionStatus`: `'loaded'`, `'pending'`, `'connecting'`, `'connected'`, or `'disconnected'`
119+
- Use this for UI state management instead of tracking connection state manually
120+
121+
## Event Listener Best Practices
122+
123+
- Register event listeners before calling `connect()` to catch all events including initial state
124+
- Remove listeners on component unmount to prevent memory leaks: `provider.removeListener('event', handler)`
125+
- Do not register duplicate listeners — check if a listener is already registered before adding
126+
- In React, use `useEffect` cleanup to remove listeners:
127+
128+
```typescript
129+
useEffect(() => {
130+
const provider = client.getProvider();
131+
const handler = (accounts: string[]) => setAccounts(accounts);
132+
provider.on('accountsChanged', handler);
133+
return () => provider.removeListener('accountsChanged', handler);
134+
}, [client]);
135+
```
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# MetaMask Connect — EVM Chain ID & Switching
2+
3+
EVM chain ID formatting rules and `switchChain` behavior for `@metamask/connect-evm` and the wagmi `metaMask()` connector. For EVM provider events see [events.md](events.md); for always-on core guardrails (config, error handling, `supportedNetworks`) see [conventions.md](conventions.md).
4+
5+
## Contents
6+
7+
- [Hex String Requirement](#hex-string-requirement)
8+
- [Common Chain IDs](#common-chain-ids)
9+
- [CAIP-2 Conversion](#caip-2-conversion)
10+
- [Auto-Included Chain](#auto-included-chain)
11+
- [Wagmi Connector](#wagmi-connector)
12+
- [Switch Chain Fallback](#switch-chain-fallback)
13+
- [Validation Error](#validation-error)
14+
15+
## Hex String Requirement
16+
17+
- Chain IDs in MetaMask Connect must always be hex strings: `'0x1'` not `1` or `'1'`
18+
- All `chainIds` arrays, `supportedNetworks` keys, and `switchChain` parameters expect hex format
19+
- Passing a number or decimal string will cause silent failures or runtime errors
20+
- Use `'0x' + chainId.toString(16)` to convert from decimal to hex
21+
22+
## Common Chain IDs
23+
24+
| Network | Decimal | Hex | CAIP-2 Scope |
25+
| ----------------- | -------- | ---------- | ----------------- |
26+
| Ethereum Mainnet | 1 | `0x1` | `eip155:1` |
27+
| Sepolia | 11155111 | `0xaa36a7` | `eip155:11155111` |
28+
| Polygon | 137 | `0x89` | `eip155:137` |
29+
| Arbitrum One | 42161 | `0xa4b1` | `eip155:42161` |
30+
| Optimism | 10 | `0xa` | `eip155:10` |
31+
| Base | 8453 | `0x2105` | `eip155:8453` |
32+
| Avalanche C-Chain | 43114 | `0xa86a` | `eip155:43114` |
33+
| BNB Smart Chain | 56 | `0x38` | `eip155:56` |
34+
| Celo | 42220 | `0xa4ec` | `eip155:42220` |
35+
| Linea | 59144 | `0xe708` | `eip155:59144` |
36+
37+
## CAIP-2 Conversion
38+
39+
- EVM CAIP-2 format is `eip155:<decimal-chainId>` — always uses decimal, not hex
40+
- EVM RPC / EIP-1193 format uses hex strings (`0x1`)
41+
- Multichain `invokeMethod` scope uses CAIP-2 (`eip155:1`)
42+
- EVM client `connect({ chainIds })` uses hex strings (`['0x1']`)
43+
- Convert: hex `0x89` → decimal `137` → CAIP-2 `eip155:137`
44+
45+
## Auto-Included Chain
46+
47+
- `0x1` (Ethereum mainnet) is automatically included in the EVM client's `connect()` **permission request** even if you don't pass it in `chainIds`
48+
- It is **not** injected into `api.supportedNetworks` — that map must explicitly contain every chain you use (including mainnet), and `createEVMClient` throws if it is empty
49+
- All chains need valid RPC URLs in `supportedNetworks`
50+
- If you use Infura RPC URLs, make sure the needed chains are enabled for your Infura project/API key
51+
52+
## Wagmi Connector
53+
54+
- The wagmi MetaMask connector is imported from `wagmi/connectors`: `import { metaMask } from 'wagmi/connectors'` — it requires `@metamask/connect-evm` as a peer dependency
55+
- Use `getInfuraRpcUrls({ infuraApiKey: 'API_KEY', chainIds?: Hex[] })` from `@metamask/connect-evm` to populate `supportedNetworks` — returns a hex-chain-ID-keyed map of Infura RPC URLs (e.g. `{ '0x1': 'https://...', '0x89': 'https://...' }`); `chainIds` is optional and filters to specific hex chain IDs
56+
- The multichain equivalent in `@metamask/connect-multichain` is `getInfuraRpcUrls({ infuraApiKey: 'API_KEY', caipChainIds?: string[] })` — returns a CAIP-2-keyed map (e.g. `{ 'eip155:1': 'https://...' }`) and accepts CAIP-2 IDs for filtering
57+
58+
## Switch Chain Fallback
59+
60+
- Use `client.switchChain({ chainId, chainConfiguration? })` to switch the active EVM chain
61+
- If the chain is not already added in MetaMask, `wallet_switchEthereumChain` can fail
62+
- Pass `chainConfiguration` directly to `client.switchChain()` as the `wallet_addEthereumChain` fallback payload
63+
- In wagmi flows, the connector passes the same fallback config through to the underlying SDK `switchChain()` call
64+
- Since `@metamask/connect-evm` 1.2.0, calling `switchChain({ chainId })` without a `chainConfiguration` now surfaces the wallet's **original** `Unrecognized chain ID` error (EIP-1193 code `4902`) instead of the previous `No chain configuration found.` wrapper. Catch the raw code in your `catch` block and either retry with a `chainConfiguration` fallback, call `wallet_addEthereumChain` explicitly, or prompt the user to add the chain — do not pattern-match on the legacy `"No chain configuration found"` message string
65+
- Since `@metamask/connect-evm` 2.0.0, MWP-backed (Mobile Wallet Protocol) EIP-1193 requests reject with the wallet's error consistently with the default transport, so `switchChain()` no longer inspects returned error payloads — wallet errors (including `4902`) always arrive as a **rejected promise**. Handle switch-chain failures purely in `catch`; do not check for an error object in the resolved value of `switchChain()` or a `provider.request({ method: 'wallet_switchEthereumChain' })` call
66+
67+
## Validation Error
68+
69+
- Making an RPC request whose **active** chain's CAIP scope is missing from `supportedNetworks` throws `Chain eip155:<id> is not configured in supportedNetworks. Requests cannot be made to chains not explicitly configured in supportedNetworks.`
70+
- This check lives in the EIP-1193 provider's `request()` path — **not** in `connect()`. `connect()` only validates that `chainIds` is a non-empty array, and `wallet_switchEthereumChain` is forwarded to the wallet (it is not gated by `supportedNetworks`).
71+
- Fix: add every chain the dApp reads from to `supportedNetworks` with a valid RPC URL before selecting it

0 commit comments

Comments
 (0)