From 7495c3703099ef238c846b6bc9b17c44dac6365c Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Fri, 12 Jun 2026 13:21:14 -0500 Subject: [PATCH 1/6] feat: add metamask-connect domain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new `metamask-connect` domain (dApp-builder audience, alongside web3-tools) with 18 skills for building dApps with the MetaMask Connect SDK (@metamask/connect-evm, @metamask/connect-multichain, @metamask/connect-solana) and the wagmi metaMask() connector — EVM/Solana/multichain setup, signing, transactions, migration from @metamask/sdk, and troubleshooting — plus a metamask-connect-conventions guardrails skill. Skills are sourced from the MetaMask Connect Cursor plugin (github.com/MetaMask/metamask-connect-cursor-plugin); the conventions skill folds that plugin's always-on rules so non-Cursor agents get the same correctness guardrails. Updates the README domains table and CHANGELOG. --- CHANGELOG.md | 4 + README.md | 1 + .../metamask-connect-conventions/skill.md | 580 ++++++++++++++++++ .../skills/migrate-from-sdk/skill.md | 357 +++++++++++ .../migrate-wagmi-metamask-connector/skill.md | 279 +++++++++ .../skills/send-evm-transaction/skill.md | 248 ++++++++ .../skills/send-solana-transaction/skill.md | 286 +++++++++ .../skills/setup-evm-browser-app/skill.md | 294 +++++++++ .../skills/setup-evm-react-app/skill.md | 299 +++++++++ .../setup-evm-react-native-app/skill.md | 321 ++++++++++ .../skills/setup-multichain-app/skill.md | 288 +++++++++ .../skills/setup-solana-browser-app/skill.md | 263 ++++++++ .../skills/setup-solana-react-app/skill.md | 195 ++++++ .../setup-solana-react-native-app/skill.md | 382 ++++++++++++ .../skills/setup-wagmi-app/skill.md | 241 ++++++++ .../setup-wagmi-metamask-connector/skill.md | 302 +++++++++ .../skills/sign-evm-message/skill.md | 227 +++++++ .../sign-multichain-evm-transaction/skill.md | 212 +++++++ .../skill.md | 242 ++++++++ .../skills/sign-solana-message/skill.md | 156 +++++ .../skills/troubleshoot-connection/skill.md | 483 +++++++++++++++ 21 files changed, 5660 insertions(+) create mode 100644 domains/metamask-connect/skills/metamask-connect-conventions/skill.md create mode 100644 domains/metamask-connect/skills/migrate-from-sdk/skill.md create mode 100644 domains/metamask-connect/skills/migrate-wagmi-metamask-connector/skill.md create mode 100644 domains/metamask-connect/skills/send-evm-transaction/skill.md create mode 100644 domains/metamask-connect/skills/send-solana-transaction/skill.md create mode 100644 domains/metamask-connect/skills/setup-evm-browser-app/skill.md create mode 100644 domains/metamask-connect/skills/setup-evm-react-app/skill.md create mode 100644 domains/metamask-connect/skills/setup-evm-react-native-app/skill.md create mode 100644 domains/metamask-connect/skills/setup-multichain-app/skill.md create mode 100644 domains/metamask-connect/skills/setup-solana-browser-app/skill.md create mode 100644 domains/metamask-connect/skills/setup-solana-react-app/skill.md create mode 100644 domains/metamask-connect/skills/setup-solana-react-native-app/skill.md create mode 100644 domains/metamask-connect/skills/setup-wagmi-app/skill.md create mode 100644 domains/metamask-connect/skills/setup-wagmi-metamask-connector/skill.md create mode 100644 domains/metamask-connect/skills/sign-evm-message/skill.md create mode 100644 domains/metamask-connect/skills/sign-multichain-evm-transaction/skill.md create mode 100644 domains/metamask-connect/skills/sign-multichain-solana-transaction/skill.md create mode 100644 domains/metamask-connect/skills/sign-solana-message/skill.md create mode 100644 domains/metamask-connect/skills/troubleshoot-connection/skill.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 383d49c..011c124 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `metamask-connect` domain: 18 skills for building dApps with the MetaMask Connect SDK (`@metamask/connect-evm`, `@metamask/connect-multichain`, `@metamask/connect-solana`) across EVM, Solana, multichain, wagmi, and React Native, plus signing, transaction, migration, and troubleshooting skills, and a `metamask-connect-conventions` guardrails skill + ## [0.1.0] ### Added diff --git a/README.md b/README.md index b94fbe3..64d4ac8 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,7 @@ tools/ | Domain | Audience | Examples | | -------------- | ----------------- | ------------------------------------------- | | `web3-tools` | dApp builders | `gator-cli`, `smart-accounts-kit`, `oh-my-opencode` | +| `metamask-connect` | dApp builders | EVM/Solana/multichain setup, signing, wagmi, React Native | | `coding` | MM product eng | Coding guidelines, controller patterns | | `agentic` | MM product eng | Experimental recipe workflows and runtime proof tools | | `general` | All agents | `codex`, `gemini` CLI usage guides | diff --git a/domains/metamask-connect/skills/metamask-connect-conventions/skill.md b/domains/metamask-connect/skills/metamask-connect-conventions/skill.md new file mode 100644 index 0000000..188fb77 --- /dev/null +++ b/domains/metamask-connect/skills/metamask-connect-conventions/skill.md @@ -0,0 +1,580 @@ +--- +name: metamask-connect-conventions +description: Core conventions, constraints, and common mistakes for the MetaMask Connect SDK across EVM, Solana, multichain, wagmi, and React Native. Consult before writing or reviewing any MetaMask Connect integration code — covers hex chain IDs, supportedNetworks validation, EIP-1193 provider events, multichain session lifecycle, Solana constraints, React Native polyfills, and testing patterns. +maturity: stable +--- +# MetaMask Connect — Conventions & Guardrails + +Always-on guardrails for the MetaMask Connect SDK, distilled from the [MetaMask Connect Cursor plugin](https://github.com/MetaMask/metamask-connect-cursor-plugin) rules. Apply these whenever you generate or review MetaMask Connect (`@metamask/connect-evm` / `-multichain` / `-solana`) or wagmi `metaMask()` connector code. + +## MetaMask Connect Best Practices + +> Best practices for MetaMask Connect SDK — import paths, singleton behavior, required config, error handling, and connection state management + +## Import Paths + +- Import EVM client from `@metamask/connect-evm` +- Import multichain client from `@metamask/connect-multichain` +- Import Solana client from `@metamask/connect-solana` +- Never import from internal sub-packages like `@metamask/connect/dist/...` or `@metamask/connect-evm/src/...` +- Use the wagmi connector from the published entrypoint your installed version exposes; do not assume `@metamask/connect-evm/wagmi` exists unless your package version exports it +- `@metamask/connect-multichain` is a **regular dependency** of both `@metamask/connect-evm` and `@metamask/connect-solana` (since 2.1.0) and is installed transitively — you do not need to add it yourself. (Only the 2.0.0 releases briefly made it a peer dependency.) Both clients warn at runtime on duplicate or mismatched `@metamask/connect-multichain` resolutions; if you do depend on it directly (e.g. to use `createMultichainClient`), use `^1.0.0` — it is a stable 1.x package following strict semver + +## Required Configuration + +- `dapp.name` is always required — it appears in the MetaMask connection prompt +- `dapp.url` is required in Node.js and React Native environments (no `window.location` available) +- `dapp.url` in browser can default to `window.location.href` but explicit is safer +- `dapp.iconUrl` is optional — displayed in MetaMask connection UI +- `dapp.base64Icon` is an alternative to `iconUrl` — pass a base64-encoded icon string directly (useful when a hosted URL is unavailable, e.g., in React Native) + +## Supported Networks + +- Every chain the dApp interacts with must be in `api.supportedNetworks` with a reachable RPC URL +- Use `getInfuraRpcUrls({ infuraApiKey: 'API_KEY', chainIds?: Hex[] })` to populate common EVM chains — it returns a hex-keyed map for `createEVMClient` +- Use `getInfuraRpcUrls({ infuraApiKey: 'API_KEY', caipChainIds?: string[] })` to populate CAIP-2 chains for `createMultichainClient` +- Use `getInfuraRpcUrls({ infuraApiKey: 'API_KEY', networks: SolanaNetwork[] })` from `@metamask/connect-solana` to populate a network-name-keyed map for `createSolanaClient` — `networks` is required +- Chain `0x1` (Ethereum mainnet) is auto-included in the EVM `connect()` permission request if not specified — but it is **not** auto-added to `supportedNetworks`, which must list every chain explicitly +- Making an RPC request whose active chain is missing from `supportedNetworks` throws "not configured in supportedNetworks" (the check runs in the provider's `request()` path, not in `connect()`) + +## Singleton Behavior + +- `createMultichainClient` is the singleton shared core instance +- `createEVMClient` and `createSolanaClient` create chain-specific wrappers on top of that shared multichain core +- Repeated client creation still reuses the existing multichain session and merged core options, but EVM/Solana wrappers can attach fresh listeners +- The multichain core keeps the `dapp` object from the first call and does not overwrite it later +- Never call `create*Client` inside a React component render — call it once at app startup +- Do not wrap client creation in `useEffect` or other hooks that may re-run + +## Error Handling + +- Code `4001`: User rejected the request — show retry UI, do not log as application error. On the EVM provider it appears as `err.code`; on the multichain client it appears as `err.rpcCode` (see below) +- Code `-32002` ("request already pending") comes from the **extension transport only** — multichain MWP concurrent `connect()` instead throws a plain `Error` ("Existing connection is pending...") with no numeric code +- Wrap all `connect()`, `invokeMethod()`, and signing calls in try/catch +- Multichain `invokeMethod()` errors are wrapped in `RPCInvokeMethodErr` (its own `code` is `53`); the wallet's original code/message/data are preserved on `rpcCode` / `rpcMessage` / `rpcData`: + ```typescript + import { RPCInvokeMethodErr } from '@metamask/connect-multichain'; + + try { + await client.invokeMethod({ scope, request }); + } catch (err) { + if (err instanceof RPCInvokeMethodErr && err.rpcCode === 4001) { + // user rejection + } + } + ``` +- Other exported error classes: `RPCHttpErr` (code 50), `RPCReadonlyResponseErr` (51), `RPCReadonlyRequestErr` (52) — for RPC-node-routed read calls. (There are no `ProtocolError`/`StorageError`/`RpcError` exports.) + +## Connection State + +- Check connection state before making signing requests +- Listen for `wallet_sessionChanged` to track session state reactively +- Do not call `connect()` on page reload if a session already exists — listen for session restoration via events +- **Multichain client:** `disconnect()` with no arguments revokes all scopes and terminates the session; `disconnect(scopes)` revokes only those scopes +- **EVM client:** `disconnect()` revokes only the `eip155:*` scopes — Solana scopes on the same session survive; full teardown requires the multichain client +- `disconnect(scopes)` with specific scopes only revokes those scopes + +## Unsupported Methods + +- The EVM client **rejects** certain methods with `Method: is not supported by Metamask Connect/EVM` (they are not silently ignored) +- Since `@metamask/connect-evm` 2.0.0, `wallet_requestPermissions` resolves to a spec-shaped requested-permissions array — but `connect()` remains the canonical way to establish permissions + +--- + +## EVM Chain ID Format + +> EVM chain ID formatting rules — hex string requirements, common chain IDs, CAIP-2 conversion, switchChain fallback, and supportedNetworks validation + +## Hex String Requirement + +- Chain IDs in MetaMask Connect must always be hex strings: `'0x1'` not `1` or `'1'` +- All `chainIds` arrays, `supportedNetworks` keys, and `switchChain` parameters expect hex format +- Passing a number or decimal string will cause silent failures or runtime errors +- Use `'0x' + chainId.toString(16)` to convert from decimal to hex + +## Common Chain IDs + +| Network | Decimal | Hex | CAIP-2 Scope | +|---------|---------|-----|-------------| +| Ethereum Mainnet | 1 | `0x1` | `eip155:1` | +| Sepolia | 11155111 | `0xaa36a7` | `eip155:11155111` | +| Polygon | 137 | `0x89` | `eip155:137` | +| Arbitrum One | 42161 | `0xa4b1` | `eip155:42161` | +| Optimism | 10 | `0xa` | `eip155:10` | +| Base | 8453 | `0x2105` | `eip155:8453` | +| Avalanche C-Chain | 43114 | `0xa86a` | `eip155:43114` | +| BNB Smart Chain | 56 | `0x38` | `eip155:56` | +| Celo | 42220 | `0xa4ec` | `eip155:42220` | +| Linea | 59144 | `0xe708` | `eip155:59144` | + +## CAIP-2 Conversion + +- EVM CAIP-2 format is `eip155:` — always uses decimal, not hex +- EVM RPC / EIP-1193 format uses hex strings (`0x1`) +- Multichain `invokeMethod` scope uses CAIP-2 (`eip155:1`) +- EVM client `connect({ chainIds })` uses hex strings (`['0x1']`) +- Convert: hex `0x89` → decimal `137` → CAIP-2 `eip155:137` + +## Auto-Included Chain + +- `0x1` (Ethereum mainnet) is automatically included in the EVM client's `connect()` **permission request** even if you don't pass it in `chainIds` +- 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 +- All chains need valid RPC URLs in `supportedNetworks` +- If you use Infura RPC URLs, make sure the needed chains are enabled for your Infura project/API key + +## Wagmi Connector + +- The wagmi MetaMask connector is imported from `wagmi/connectors`: `import { metaMask } from 'wagmi/connectors'` — it requires `@metamask/connect-evm` as a peer dependency +- 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 +- 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 + +## Switch Chain Fallback + +- Use `client.switchChain({ chainId, chainConfiguration? })` to switch the active EVM chain +- If the chain is not already added in MetaMask, `wallet_switchEthereumChain` can fail +- Pass `chainConfiguration` directly to `client.switchChain()` as the `wallet_addEthereumChain` fallback payload +- In wagmi flows, the connector passes the same fallback config through to the underlying SDK `switchChain()` call +- 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 +- 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 + +## Validation Error + +- Making an RPC request whose **active** chain's CAIP scope is missing from `supportedNetworks` throws `Chain eip155: is not configured in supportedNetworks. Requests cannot be made to chains not explicitly configured in supportedNetworks.` +- 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`). +- Fix: add every chain the dApp reads from to `supportedNetworks` with a valid RPC URL before selecting it + +--- + +## EVM Provider Event Handling + +> EVM provider and connect-evm event handling — EIP-1193 events, SDK eventHandlers, payload types, display_uri timing, and transport events + +## EIP-1193 Events (EVM Provider) + +- **`connect`** — fired when the provider establishes a connection; payload: `{ chainId: Hex; accounts: Address[] }` +- **`disconnect`** — fired when the provider loses connection; **no payload** +- **`accountsChanged`** — fired when the user's accounts change; payload: `string[]` (array of addresses) +- **`chainChanged`** — fired when the active chain changes; payload: `string` (**hex** chain ID, not decimal) +- **`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 + +```typescript +const provider = client.getProvider(); + +provider.on('accountsChanged', (accounts: string[]) => { + console.log('New accounts:', accounts); +}); + +provider.on('chainChanged', (chainId: string) => { + // chainId is HEX (e.g., '0x1'), NOT decimal + console.log('New chain:', chainId); +}); + +provider.on('connect', ({ chainId, accounts }: { chainId: string; accounts: string[] }) => { + console.log('Connected to chain:', chainId, 'accounts:', accounts); +}); + +provider.on('disconnect', () => { + // No payload — the event itself is the signal + console.log('Disconnected'); +}); +``` + +## chainChanged Payload Type + +- `chainChanged` emits a **hex string** (e.g., `'0x1'`, `'0x89'`), **not a decimal number** +- Never compare directly with decimal numbers: `chainId === 1` will always be false +- Convert if needed: `parseInt(chainId, 16)` to get the decimal chain ID +- This is a common source of bugs — always treat chainChanged payload as a hex string + +## SDK eventHandlers (Client Options) + +- Configure event callbacks directly in client options via `eventHandlers`: + - `connect` — same as EIP-1193 connect + - `disconnect` — same as EIP-1193 disconnect + - `accountsChanged` — same as EIP-1193 accountsChanged + - `chainChanged` — same as EIP-1193 chainChanged + - `displayUri` — fires with the connection URI string for QR code rendering + - `connectAndSign` — fires with the signature result from `connectAndSign` flow + - `connectWith` — fires with the result from `connectWith` flow + +```typescript +const client = await createEVMClient({ + dapp: { name: 'My DApp' }, + eventHandlers: { + accountsChanged: (accounts) => updateUI(accounts), + chainChanged: (chainId) => updateChain(chainId), + displayUri: (uri) => renderQrCode(uri), + }, +}); +``` + +## display_uri Timing + +- `display_uri` only fires during the `'connecting'` state — between calling `connect()` and the connection resolving +- Register the `display_uri` listener **before** calling `connect()` — registering after may miss the event +- The URI is a one-time-use pairing token; once used or expired, it cannot be reused +- On connection error, do not attempt to regenerate or reuse the QR — call `connect()` again for a new URI +- In non-headless mode, the SDK renders its own QR modal; `display_uri` is mainly useful in headless mode + +## Multichain stateChanged Event + +- The multichain core client emits `stateChanged` whenever the connection status changes +- Listen via `client.on('stateChanged', (status) => ...)` on the multichain client, where `status` is a `ConnectionStatus` string +- 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 + +## Transport Events + +- 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. +- 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 + +## EIP-6963 Provider Announcement + +- 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 +- 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 +- 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) +- 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 + +## Cached State Methods + +- `eth_accounts` and `eth_chainId` return locally cached state from the SDK rather than making RPC calls +- The cached values are kept in sync via `accountsChanged` and `chainChanged` events, so they reflect the current state after connection +- Use `client.getChainId()` to get the current hex chain ID (returns `Hex | undefined`) +- Use `client.getAccount()` to get the current account address (returns `Address | undefined`) +- 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 +- 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 + +## Client Status Property + +- 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`) +- On the multichain client (`createMultichainClient`), `client.status` is the 5-value `ConnectionStatus`: `'loaded'`, `'pending'`, `'connecting'`, `'connected'`, or `'disconnected'` +- Use this for UI state management instead of tracking connection state manually + +## Event Listener Best Practices + +- Register event listeners before calling `connect()` to catch all events including initial state +- Remove listeners on component unmount to prevent memory leaks: `provider.removeListener('event', handler)` +- Do not register duplicate listeners — check if a listener is already registered before adding +- In React, use `useEffect` cleanup to remove listeners: + +```typescript +useEffect(() => { + const provider = client.getProvider(); + const handler = (accounts: string[]) => setAccounts(accounts); + provider.on('accountsChanged', handler); + return () => provider.removeListener('accountsChanged', handler); +}, [client]); +``` + +--- + +## Multichain Session Lifecycle + +> Multichain session lifecycle rules — singleton merging, concurrent connect guard, session data shape, wallet_sessionChanged events, headless mode, timeouts, and permission handling + +## Singleton Merging + +- `createMultichainClient` is a singleton — calling it multiple times returns the same instance +- On subsequent calls, new options merge into the existing instance +- The `dapp` object from the first call is used for the client's lifetime — it is **excluded from option merging** entirely (later `dapp` values are ignored) +- `api.supportedNetworks` entries merge by spreading the new map over the old — new chains are added and **existing keys are overwritten** by later calls +- Call `createMultichainClient` once at app startup and store the returned client reference + +## Concurrent Connect Guard + +- Only one `connect()` call can be active at a time over MetaMask Wallet Protocol (MWP) +- Calling `connect()` while a previous MWP `connect()` is pending throws a plain `Error` ("Existing connection is pending. Please check your MetaMask Mobile app to continue.") with **no numeric code** — match on the message. (`-32002` is an extension-transport RPC-queue code, not an SDK error code) +- Guard against double-clicks with a loading state or disable the connect button during connection +- The original pending `connect()` promise will resolve once the user acts in MetaMask + +## Session Data Shape + +- Multichain `connect()` resolves with **no value** (`Promise`) — session data arrives via the `wallet_sessionChanged` event or on demand from `client.provider.getSession()` +- Session data is `SessionData`: scopes live under `sessionScopes` (e.g., `session.sessionScopes['eip155:1'].accounts`), and accounts are CAIP-10 strings (`eip155:1:0x...`) +- `sessionProperties` may be present — if empty, it is `undefined` (not an empty object) +- Always null-check `sessionProperties` before accessing its fields +- Since `@metamask/connect-evm` 1.2.0, every `wallet_createSession` request issued by `connect-evm` attaches `sessionProperties: { 'eip1193-compatible': true }`. Sessions established through `createEVMClient` will surface this flag on the resolved session, letting wallets and analytics consumers distinguish EIP-1193-style connections from pure Multichain API connections or other provider types (e.g. Solana Wallet Standard). Do not rely on it being present for sessions created directly via the multichain client + +## dapp.url Requirement + +- In browser environments, `dapp.url` falls back to `window.location.href` if not specified +- In Node.js and React Native, `dapp.url` is **required** — there is no `window.location` to fall back to +- Omitting `dapp.url` in non-browser environments throws `Error: You must provide dapp url` during client creation (in the browser it is auto-filled from `window.location`, which is absent in Node.js / React Native) + +## Multichain Events + +- **`wallet_sessionChanged`** — fires when any part of the multichain session changes (accounts, scopes, permissions) +- Listen on the multichain client directly with `client.on('wallet_sessionChanged', handler)` +- Payload contains the updated session object with all active scopes and accounts +- Fires on: initial connection, account changes, scope additions/removals, session restoration + +```typescript +// Payload is SessionData | undefined — iterate sessionScopes, not the payload itself +client.on('wallet_sessionChanged', (session) => { + for (const [scope, data] of Object.entries(session?.sessionScopes ?? {})) { + console.log(`Scope ${scope}:`, data.accounts); // CAIP-10 account IDs + } +}); +``` + +## Session Persistence and Resumption + +- The SDK persists session state and attempts to resume on subsequent page loads +- Listen for `wallet_sessionChanged` on startup to detect restored sessions +- Do not call `connect()` again if a session already exists — check session state first +- `createEVMClient` and `createSolanaClient` perform an initial session sync before returning, but session state should still be treated as event-driven +- Do not assume a usable session exists unless your startup logic has observed the current session state or a `wallet_sessionChanged` event + +## Headless Mode + +- Set `ui: { headless: true }` to suppress the default QR code modal +- Register a `display_uri` event listener **before** calling `connect()` to receive the connection URI +- `display_uri` only fires during the connecting phase — after connection or on error, it stops +- On connection error in headless mode, do **not** try to regenerate the QR from the old URI — start a new `connect()` call +- The URI is a one-time-use pairing token + +## Timeouts + +- Default request timeout is **60 seconds** +- Mobile Wallet Protocol uses an extended **120 second** connection timeout while waiting for user action in MetaMask Mobile +- Pending-session resumption waits about **10 seconds** before giving up +- These are internal SDK timeouts — do not implement your own shorter timeouts that race against them + +## Bundle / Lazy-loaded Transport + +- Since `@metamask/connect-multichain` 0.13.0, the MWP transport modules — `@metamask/mobile-wallet-protocol-core`, `@metamask/mobile-wallet-protocol-dapp-client`, and `eciesjs` — are dynamically imported only when MWP transport is actually used +- Bundlers (webpack, Vite, Rollup, Metro) can now code-split the entire MWP + crypto dependency tree out of the main chunk for consumers who only use the browser-extension flow +- Do not statically import the MWP modules yourself in app code — that defeats the code-split and re-inflates the bundle +- Since `@metamask/connect-multichain` 0.14.0, the QR-code MWP flow (desktop web and Node.js) omits the initial `wallet_createSession` request from the deeplink URI and sends it as a separate request after the wallet completes the MWP handshake. The result is a shorter deeplink URI and a less dense QR code. The native deeplink (non-QR MWP) flow used on mobile web and React Native is unchanged — no app-side action required + +## Permission Handling + +- Use `connect(scopes, [], undefined, true)` when you need a fresh permission prompt even if permissions already exist — `forceRequest` is the fourth positional argument +- The multichain `connect` signature is `connect(scopes, caipAccountIds, sessionProperties?, forceRequest?)` — all positional arguments, not an options object +- `wallet_requestPermissions` itself does not take a `forceRequest` parameter; the SDK handles that through `connect()` +- Without `forceRequest`, the SDK may reuse an existing compatible session +- `connect()` internally handles the underlying permission request flow, so you rarely need to call `wallet_requestPermissions` directly +- For multichain, `connect(scopes, [])` is the canonical way to request permissions for specific chains + +## Analytics + +- The SDK emits dapp-side analytics events and attaches wallet-correlation metadata by default. To opt out, pass `analytics: { enabled: false }` to the client factory — supported by `createMultichainClient` (`@metamask/connect-multichain` 0.15.0+), `createEVMClient` (`@metamask/connect-evm` 1.4.0+), and `createSolanaClient` (`@metamask/connect-solana` 1.2.0+) +- Setting `analytics.enabled: false` on `createMultichainClient` also omits the `analytics.remote_session_id` field from connection metadata; on the EVM/Solana clients it disables dapp-side events and wallet-correlation metadata +- To disable analytics at runtime after the client exists (rather than at construction), call `analytics.disable()` (`@metamask/analytics` 0.6.0+) — it stops event collection and clears any queued analytics events +- Respect user privacy preferences (e.g. a Do-Not-Track or cookie-consent setting) by wiring them to `analytics.enabled` / `analytics.disable()` rather than trying to intercept or block the network requests yourself + +--- + +## Solana Integration Constraints + +> Constraints and requirements for Solana integration with MetaMask Connect — wallet adapter config, CAIP-2 IDs, network support per platform, RPC routing, and platform limitations + +## Wallet Adapter Configuration + +- The wallet name registered by `createSolanaClient` is `"MetaMask"` (renamed from `"MetaMask Connect"` in `@metamask/connect-solana` 1.0.0). Match on exactly `"MetaMask"` — do not branch on the old `"MetaMask Connect"` literal. +- Since `@metamask/connect-solana` 1.0.0, `createSolanaClient` no longer announces its own wallet-standard provider if an injected Solana provider (e.g. the MetaMask browser extension) is already present. Treat the already-injected provider as MetaMask; your UI should not expect two wallet entries. +- `WalletProvider` must receive `wallets={[]}` — MetaMask uses the wallet-standard auto-discovery protocol +- Never manually add MetaMask to the wallets array — it will not be found and may cause duplicates +- Initialize `createSolanaClient` early in app startup, but it does not need to resolve before the first `WalletProvider` render +- If your UI depends on MetaMask already being registered, gate that UI until `createSolanaClient` resolves +- Since `@metamask/connect-solana` 1.1.0, `createSolanaClient()` eagerly initializes the Solana wallet provider during creation — if the underlying multichain session already contains Solana scopes, the provider's accounts are populated by the time the client resolves. Apps no longer need to wait for a separate `wallet_sessionChanged` event to read accounts on cold start +- Since `@metamask/connect-solana` 1.1.0, `getWallet()` returns the same wallet instance on every call instead of constructing a new one. It is safe to cache the result in a module-level constant, React `useRef`, or `useMemo` — do not call `getWallet()` on every render expecting a fresh instance + +## CAIP-2 Genesis Hash Identifiers + +- Solana mainnet: `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` +- Solana devnet: `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` +- These are genesis hash identifiers, not cluster URLs or chain IDs +- Always use the full CAIP-2 string as the scope in multichain `invokeMethod` and `connect` + +## Devnet and Testnet + +- The SDK and the wallet-standard layer model three Solana scopes — mainnet (`solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp`), devnet (`solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1`), and testnet (`solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z`) +- Non-mainnet availability ultimately depends on the connected MetaMask build/version — don't assume a given cluster is present. Handle `connect()` / `invokeMethod` errors rather than treating devnet/testnet as guaranteed +- For Solana read calls, point a `@solana/web3.js` `Connection` at the matching cluster RPC (the SDK routes signing through the wallet, not reads) + +## RPC Routing + +- **All Solana methods route through the wallet** — there is no RPC node fallback +- Unlike EVM (where read methods like `eth_getBalance` go to Infura), every Solana `invokeMethod` call goes to MetaMask +- This means every Solana call may prompt the user or require wallet availability +- For Solana read operations (balance, account info), use `@solana/web3.js` `Connection` directly against an RPC endpoint + +## Disconnect Scopes Behavior + +- On the Solana client (`createSolanaClient`), `disconnect()` revokes **only** the Solana scopes (mainnet/devnet/testnet) — it does not touch EVM scopes. (Full-session teardown across all scopes is the *multichain* client's `disconnect()` with no arguments.) +- On the multichain client (`createMultichainClient`), `disconnect(['solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'])` revokes only Solana mainnet — EVM scopes stay active +- Disconnecting a Solana scope does not affect any active EVM connections + +## Chrome Android Bug + +- There is a known issue with `@solana/wallet-adapter-react` on Chrome Android when used with the wallet-standard provider from `@metamask/connect-solana` +- The connect monorepo carries a patch for the wallet-adapter behavior in that setup +- Treat Solana wallet-adapter flows on mobile Chrome as fragile until you verify them explicitly +- Test Solana flows on desktop Chrome and MetaMask browser extension wallet before targeting mobile + +## React Native Limitation + +- The Solana wallet adapter (`@solana/wallet-adapter-react`) is **not supported** in React Native +- For Solana in React Native, use the multichain client (`createMultichainClient`) with `invokeMethod` directly +- Do not attempt to import `@solana/wallet-adapter-react` or `@solana/wallet-adapter-react-ui` in RN — they depend on browser APIs + +--- + +## React Native Polyfills for MetaMask Connect + +> Required polyfills and configuration for MetaMask Connect SDK in React Native — import order, Buffer, window, Event/CustomEvent, metro config, and persistence + +## Per-Package Polyfill Requirements + +Different integrations need different polyfills. Do not blindly copy the full set: + +| Polyfill | connect-evm / connect-solana (standalone) | + wagmi | +|---|---|---| +| `react-native-get-random-values` | RN < 0.72 only (see below) | RN < 0.72 only | +| `Buffer` | Safety net only (self-polyfilled by connect-multichain) | Safety net only | +| `window` object | **Required** for correct deeplink/platform detection | **Required** | +| `Event` | Not required | **Required** (wagmi uses DOM events) | +| `CustomEvent` | Not required | **Required** (wagmi uses DOM events) | + +## Import Order (Critical) + +```typescript +// Entry file (_layout.tsx / index.js) — order is critical +import 'react-native-get-random-values'; // MUST be first (if used) +import './polyfills'; // window shim, and Event/CustomEvent if using wagmi +``` + +Incorrect order causes `crypto.getRandomValues is not a function` at runtime. + +## react-native-get-random-values + +- Required only for **React Native < 0.72** — Hermes 0.72+ exposes `globalThis.crypto.getRandomValues` natively +- Still recommended as an explicit safety net — especially if any dependency has its own minimum RN version assumptions +- Must be the **very first import** in the entry file, before anything that touches crypto + +## Buffer Polyfill + +- `@metamask/connect-multichain` self-polyfills `Buffer` via its React Native entry point — not needed for the SDK itself +- Still recommended to set `global.Buffer = Buffer` in `polyfills.ts` as a safety net for peer deps (e.g. `eciesjs`, `@solana/web3.js`) that may load before connect-multichain +- Install: `npm install buffer` + +## window Object Polyfill + +- **Required** for correct platform and deeplink behaviour — `getPlatformType()` in connect-multichain inspects `window` and `global.navigator.product` to decide between the deeplink path and the install-modal path +- All `window.*` accesses inside the SDK are guarded, so code will not crash without it, but `isSecure()` returns the wrong value and deeplinks will not trigger +- Provide at minimum: `location`, `addEventListener`, `removeEventListener`, `dispatchEvent` + +## Event and CustomEvent Polyfills + +- **Not required** by the connect-* packages themselves — the SDK uses `eventemitter3` for all internal eventing; DOM `Event`/`CustomEvent` are never constructed in React Native code paths +- **Required when using wagmi** — wagmi core dispatches DOM events internally +- Add only if your integration uses wagmi: + +```typescript +class EventPolyfill { /* ... */ } +class CustomEventPolyfill extends EventPolyfill { detail: any; /* ... */ } +global.Event = EventPolyfill as any; +global.CustomEvent = CustomEventPolyfill as any; +``` + +## Metro extraNodeModules + +- The MetaMask Connect SDK has transitive dependencies on Node.js built-in modules +- Metro cannot resolve them without explicit shims in `metro.config.js` +- **`stream`** must map to `readable-stream` (not `stream-browserify`) — it is the only built-in that needs a real implementation +- Map every other referenced built-in to an **empty stub module** (`module.exports = {};`) — they are referenced by transitive deps but never called at runtime in React Native (this matches the SDK's own react-native-playground): + +```javascript +// metro.config.js +const path = require('path'); +const emptyModule = path.resolve(__dirname, 'src', 'empty-module.js'); // module.exports = {}; + +resolver: { + extraNodeModules: { + stream: require.resolve('readable-stream'), + crypto: emptyModule, + http: emptyModule, + https: emptyModule, + net: emptyModule, + tls: emptyModule, + zlib: emptyModule, + os: emptyModule, + dns: emptyModule, + assert: emptyModule, + url: emptyModule, + path: emptyModule, + fs: emptyModule, + }, +} +``` + +- Only `readable-stream` needs to be installed — do not install `react-native-crypto`, `@tradle/react-native-http`, `https-browserify`, or `os-browserify`; they are obsolete for this SDK + +## preferredOpenLink (Required) + +- `mobile.preferredOpenLink` must be set in React Native for deeplinks to open MetaMask Mobile +- Pass: `(deeplink: string) => Linking.openURL(deeplink)` +- Without this, connection attempts via MWP will hang — no deeplink is triggered + +## Async Storage for Persistence + +- Browser localStorage is not available in React Native +- Use `@react-native-async-storage/async-storage` for session persistence +- With wagmi: use `createAsyncStoragePersister` from `@tanstack/query-async-storage-persister` +- Without wagmi: the MetaMask Connect SDK handles persistence internally when AsyncStorage is provided + +--- + +## MetaMask Connect Testing Patterns + +> Testing patterns for MetaMask Connect SDK — provider mocking, client mocking, singleton cleanup, and event testing + +## Provider Mocking + +- Mock the EIP-1193 provider's request method for unit tests +- Create a mock provider factory that returns controlled responses +- Example: `const mockProvider = { request: vi.fn(), on: vi.fn(), removeListener: vi.fn() }` +- Mock different responses for different methods (eth_accounts, eth_chainId, etc.) + +## Client Mocking + +- Mock createEVMClient to return a controlled client object +- Mock client.connect(), client.disconnect(), client.getProvider(), client.switchChain() +- For multichain: mock createMultichainClient, client.invokeMethod(), client.on() + +## Singleton Cleanup + +- createMultichainClient is a singleton — tests that create clients will share state +- Clear or reset the singleton between test runs +- Use beforeEach/afterEach to ensure clean state + +## Test Networks + +- Use Sepolia (0xaa36a7) for E2E tests, never mainnet +- For Solana E2E: use devnet — supported in the MetaMask browser extension (mobile supports mainnet only) +- Mock RPC responses for unit tests; use real RPCs only for integration tests + +## Async Client Initialization + +- createEVMClient and createMultichainClient are async — tests must await them +- In React testing, await the client before rendering components that depend on it +- Use act() wrapper for React state updates triggered by SDK events + +## Error Simulation + +- Test user rejection: throw { code: 4001, message: 'User rejected' } +- Test pending connection: throw { code: -32002, message: 'Already pending' } +- Test network errors: simulate RPC failures +- Test disconnect scenarios + +## Event Testing + +- Test that components react to accountsChanged, chainChanged events +- Simulate events by calling the mock provider's event handlers +- Test display_uri event handling for headless mode + +## Solana Testing + +- Mock wallet-standard wallet object +- Mock signMessage, signAndSendTransaction features +- Test wallet discovery with mocked wallet registry diff --git a/domains/metamask-connect/skills/migrate-from-sdk/skill.md b/domains/metamask-connect/skills/migrate-from-sdk/skill.md new file mode 100644 index 0000000..08b695c --- /dev/null +++ b/domains/metamask-connect/skills/migrate-from-sdk/skill.md @@ -0,0 +1,357 @@ +--- +name: migrate-from-sdk +description: Migrate from @metamask/sdk to @metamask/connect-evm, @metamask/connect-multichain, and @metamask/connect-solana with step-by-step package, API, and configuration changes +maturity: stable +--- +# Migrate from @metamask/sdk to @metamask/connect + +## When to use + +Use this skill when: +- Migrating an existing dApp from `@metamask/sdk` or `@metamask/sdk-react` to the new `@metamask/connect-*` packages +- Updating initialization code, provider access, or event handling for the new API +- Converting a wagmi integration to use the new `metaMask()` connector +- Adding multichain or Solana support during the migration + +## Workflow + +### Step 1: Replace packages + +Remove the old packages and install the new ones: + +```bash +# Remove old +npm uninstall @metamask/sdk @metamask/sdk-react + +# Install new — pick the packages you need +npm install @metamask/connect-evm +npm install @metamask/connect-multichain +npm install @metamask/connect-solana +``` + +--- + +### Step 1b: React Native polyfills (if applicable) + +No polyfill configuration is needed for web environments (Vite, Webpack, Next.js, etc.) — `@metamask/connect-*` packages no longer depend on Node.js built-ins in the browser. + +**React Native only:** Polyfills must be imported in a specific order. See the `react-native-polyfills` rule for required import order, window/Event/CustomEvent shims, and metro configuration. Note: `Buffer` is self-polyfilled by `@metamask/connect-multichain` but should still be set early as a safety net for peer deps. + +--- + +### Step 2: Update imports + +**Old:** + +```typescript +import { MetaMaskSDK } from '@metamask/sdk'; +import { MetaMaskProvider, useSDK } from '@metamask/sdk-react'; +``` + +**New (EVM):** + +```typescript +import { createEVMClient, getInfuraRpcUrls } from '@metamask/connect-evm'; +``` + +**New (Multichain):** + +```typescript +import { createMultichainClient } from '@metamask/connect-multichain'; +``` + +**New (Solana):** + +```typescript +import { createSolanaClient } from '@metamask/connect-solana'; +``` + +**New (wagmi connector):** + +```typescript +// Requires wagmi >= 3.6 / @wagmi/connectors >= 8 (the connect-evm-backed +// connector), with @metamask/connect-evm installed at wagmi's declared peer +// range (currently ^1.3.0). On older wagmi, copy the reference connector +// from connect-monorepo/integrations/wagmi/metamask-connector.ts. +import { metaMask } from 'wagmi/connectors'; +``` + +--- + +### Step 3: Update initialization + +**Old:** + +```typescript +const sdk = new MetaMaskSDK({ + dappMetadata: { + name: 'My DApp', + url: window.location.href, + }, + infuraAPIKey: 'YOUR_INFURA_KEY', + readonlyRPCMap: { + '0x89': 'https://polygon-rpc.com', + }, + headless: true, + extensionOnly: false, + openDeeplink: (link) => window.open(link, '_blank'), +}); +await sdk.init(); +``` + +**New:** + +```typescript +const client = await createEVMClient({ + dapp: { + name: 'My DApp', + url: window.location.href, + }, + api: { + supportedNetworks: { + ...getInfuraRpcUrls({ infuraApiKey: 'YOUR_INFURA_KEY', chainIds: ['0x1', '0x89'] }), + '0xa4b1': 'https://arb1.arbitrum.io/rpc', + }, + }, + ui: { + headless: true, + preferExtension: false, + }, + // `mobile` block only needed for React Native + // mobile: { + // preferredOpenLink: (link: string) => Linking.openURL(link), + // }, +}); +``` + +**Key option mappings:** + +| Old (`MetaMaskSDK`) | New (`createEVMClient`) | Notes | +|---|---|---| +| `dappMetadata` | `dapp` | Same shape: `{ name, url, iconUrl }` | +| `dappMetadata.name` | `dapp.name` | Required | +| `dappMetadata.url` | `dapp.url` | Optional | +| `infuraAPIKey` | `api.supportedNetworks` via `getInfuraRpcUrls({ infuraApiKey: key })` | Helper generates URLs for all Infura-supported chains; optional `chainIds` to limit to specific chains | +| `readonlyRPCMap` | `api.supportedNetworks` | Merge into the same object | +| `headless` | `ui.headless` | Same behavior | +| `extensionOnly` | `ui.preferExtension` | `true` prefers extension (default); not the same as "only" | +| `openDeeplink` | `mobile.preferredOpenLink` | Same signature: `(deeplink: string) => void` | +| `useDeeplink` | `mobile.useDeeplink` | Same behavior | +| `timer` | Removed | No longer configurable | +| `enableAnalytics` | `analytics: { enabled: boolean }` | Pass `analytics: { enabled: false }` at client creation. (A runtime `analytics.disable()` exists on the `@metamask/analytics` singleton — `import { analytics } from '@metamask/analytics'` — it is **not** a method on the connect client.) | +| `communicationServerUrl` | Removed | Managed internally | +| `storage` | Removed | Managed internally | + +--- + +### Step 4: Update connection flow + +**Old:** + +```typescript +const accounts = await sdk.connect(); +const chainId = await sdk.getProvider().request({ method: 'eth_chainId' }); +``` + +**New:** + +```typescript +const { accounts, chainId } = await client.connect({ + chainIds: ['0x1'], +}); +``` + +Key differences: +- `connect()` now returns an **object** with both `accounts` and `chainId` — no separate call needed +- `chainIds` parameter specifies which chains to request (hex strings) +- Use `connectAndSign` for connect + personal_sign in one step: + +```typescript +const { accounts, chainId, signature } = await client.connectAndSign({ + chainIds: ['0x1'], + message: 'Sign in to My DApp', +}); +``` + +- Use `connectWith` for connect + arbitrary RPC method: + +```typescript +const { accounts, chainId, result } = await client.connectWith({ + chainIds: ['0x1'], + method: 'eth_sendTransaction', + params: [{ from: '0x...', to: '0x...', value: '0x0' }], +}); +``` + +--- + +### Step 5: Update provider access + +**Old:** + +```typescript +const provider = sdk.getProvider(); // SDKProvider +await provider.request({ method: 'eth_chainId' }); +``` + +**New:** + +```typescript +const provider = client.getProvider(); // EIP1193Provider +await provider.request({ method: 'eth_chainId' }); +``` + +Key differences: +- The provider is now a standard **EIP-1193 provider**, not the custom `SDKProvider` +- The provider is available **immediately** after `createEVMClient` resolves — even before `connect()` +- Before connection, RPC calls that require an account will fail; read-only calls (like `eth_blockNumber`) work against `supportedNetworks` RPCs +- No more `sdk.getProvider()` returning `undefined` — the provider always exists + +--- + +### Step 6: Update event handling + +**Old:** + +```typescript +const provider = sdk.getProvider(); +provider.on('chainChanged', (chainId) => { /* ... */ }); +provider.on('accountsChanged', (accounts) => { /* ... */ }); +provider.on('disconnect', () => { /* ... */ }); +``` + +**New (same EIP-1193 events still work):** + +```typescript +const provider = client.getProvider(); +provider.on('chainChanged', (chainId) => { /* ... */ }); +provider.on('accountsChanged', (accounts) => { /* ... */ }); +provider.on('disconnect', () => { /* ... */ }); +``` + +**New (additional SDK-level events via constructor):** + +```typescript +const client = await createEVMClient({ + dapp: { name: 'My DApp' }, + eventHandlers: { + displayUri: (uri) => { /* render QR code */ }, + }, +}); +``` + +Or subscribe on the EIP-1193 provider after creation: + +```typescript +const provider = client.getProvider(); +provider.on('display_uri', (uri) => { /* ... */ }); +``` + +For `wallet_sessionChanged`, use the multichain client directly: + +```typescript +const client = await createMultichainClient({ /* ... */ }); +client.on('wallet_sessionChanged', (session) => { /* ... */ }); +``` + +--- + +### Step 7: New capabilities to adopt + +These features are **new** in the MetaMask Connect packages and have no old-SDK equivalent: + +| Capability | Description | +|---|---| +| **Multichain client** | `createMultichainClient` supports CAIP-25 scopes across EVM and non-EVM chains | +| **`invokeMethod`** | Call RPC methods on specific CAIP scopes: `client.invokeMethod({ scope: 'eip155:1', request: { method, params } })` | +| **Solana support** | `createSolanaClient` from `@metamask/connect-solana` with wallet-standard adapter | +| **`connectAndSign`** | Connect and sign a message in a single user approval | +| **`connectWith`** | Connect and execute any RPC method in a single user approval | +| **Partial disconnect** | `disconnect(scopes)` is available on the multichain client to revoke specific CAIP scopes while keeping others active | +| **Singleton client** | Subsequent `createMultichainClient` calls merge into the existing instance | +| **`wallet_sessionChanged`** | Multichain client event fired when session state changes or is restored | + +--- + +### Step 8: Wagmi migration + +**Old:** + +```typescript +// Old @metamask/sdk constructor takes flat options (no `options` wrapper): +import { MetaMaskSDK } from '@metamask/sdk'; + +const sdk = new MetaMaskSDK({ + dappMetadata: { name: 'My DApp', url: window.location.href }, +}); +// (or the legacy wagmi `metaMask()` connector that wrapped @metamask/sdk) +``` + +**New:** + +```typescript +import { createConfig, http } from 'wagmi'; +import { mainnet, sepolia } from 'wagmi/chains'; +import { metaMask } from 'wagmi/connectors'; + +export const wagmiConfig = createConfig({ + chains: [mainnet, sepolia], + connectors: [ + metaMask({ + dapp: { + name: 'My DApp', + url: typeof window !== 'undefined' ? window.location.href : undefined, + }, + }), + ], + transports: { + [mainnet.id]: http(), + [sepolia.id]: http(), + }, +}); +``` + +Key differences: +- The connect-evm-backed `metaMask()` connector ships in `wagmi/connectors` from wagmi 3.6 / `@wagmi/connectors` 8 — there is no `@metamask/connect-evm/wagmi` subpath; install `@metamask/connect-evm` at wagmi's declared peer range +- Use `dapp` not `dappMetadata` +- Connector ID is `'metaMaskSDK'` — find it with `connectors.find(c => c.id === 'metaMaskSDK')` +- Most wagmi hooks work unchanged, but note the wagmi v3 renames: `useConnect().connectors` → `useConnectors()`, `connectAsync` → `mutateAsync`, `useAccount` → `useConnection` (see the migrate-wagmi-metamask-connector skill) + +--- + +## Quick Reference: Full Option Mapping + +| Old (`@metamask/sdk`) | New (`@metamask/connect-*`) | Status | +|---|---|---| +| `new MetaMaskSDK(opts)` | `await createEVMClient(opts)` | Renamed, async | +| `sdk.init()` | Not needed | Init happens in `createEVMClient` | +| `sdk.connect()` | `client.connect({ chainIds })` | Returns `{ accounts, chainId }` | +| `sdk.getProvider()` | `client.getProvider()` | Returns EIP-1193 provider | +| `sdk.disconnect()` | `client.disconnect()` | Same for EVM; partial disconnect is multichain-only | +| `sdk.terminate()` | `client.disconnect()` | `terminate` is removed — the EVM client's `disconnect()` revokes the EVM (`eip155:*`) scopes; for full multi-ecosystem teardown call the multichain client's `disconnect()` with no arguments | +| `dappMetadata` | `dapp` | Renamed | +| `infuraAPIKey` | `getInfuraRpcUrls({ infuraApiKey: key })` in `api.supportedNetworks` | Helper function; optional `chainIds` filters to specific chains | +| `readonlyRPCMap` | `api.supportedNetworks` | Merged with Infura URLs | +| `headless` | `ui.headless` | Moved to `ui` namespace | +| `extensionOnly` | `ui.preferExtension` | Renamed, slightly different semantics | +| `openDeeplink` | `mobile.preferredOpenLink` | Moved to `mobile` namespace | +| `useDeeplink` | `mobile.useDeeplink` | Moved to `mobile` namespace | +| `MetaMaskProvider` (React) | No direct equivalent | Use wagmi `WagmiProvider` or call `createEVMClient` directly | +| `useSDK()` hook | No direct equivalent | Use wagmi hooks or manage client state manually | +| `SDKProvider` | `EIP1193Provider` | Standard provider interface | +| `timer` | Removed | — | +| `enableAnalytics` | `analytics: { enabled: boolean }` | — | +| `communicationServerUrl` | Removed | — | +| `storage` | Removed | — | + +## Important Notes + +- **`createEVMClient` is async** — unlike `new MetaMaskSDK()`, it returns a promise. Ensure you `await` it or handle the promise before accessing the client. +- **The multichain core is the singleton** — `createMultichainClient` merges into a shared instance, while EVM/Solana create wrappers on top of that shared core. Do not recreate clients on every render. +- **`connect()` returns an object now** — destructure `{ accounts, chainId }` instead of treating the return value as an accounts array. +- **Chain IDs must be hex strings** — use `'0x1'` not `1` or `'1'` in `chainIds` and `supportedNetworks` keys. +- **No more `sdk.init()`** — initialization is part of `createEVMClient`. There is no separate init step. +- **Provider exists before connection** — `client.getProvider()` never returns `undefined`. But node-routed reads (`eth_blockNumber`, `eth_getBalance`, …) require a **selected chain** and throw `No chain ID selected` until one is set (after `connect()` or a restored session); only the intercepted `eth_chainId` / `eth_accounts` (cached) are safe before connecting. +- **`@metamask/sdk-react` has no 1:1 replacement** — if you were using `MetaMaskProvider` and `useSDK()`, migrate to either wagmi hooks or manage the client instance in your own React context. +- **`sdk.terminate()` is replaced by `disconnect()`** — the EVM client's `disconnect()` revokes EVM (`eip155:*`) scopes only; if the session also has Solana scopes, terminate everything via the multichain client's `disconnect()` with no arguments. There is no separate `terminate` method. +- **Test the migration on both extension and mobile** — the transport layer has changed, and behavior differences may surface in one environment but not the other. diff --git a/domains/metamask-connect/skills/migrate-wagmi-metamask-connector/skill.md b/domains/metamask-connect/skills/migrate-wagmi-metamask-connector/skill.md new file mode 100644 index 0000000..04d9201 --- /dev/null +++ b/domains/metamask-connect/skills/migrate-wagmi-metamask-connector/skill.md @@ -0,0 +1,279 @@ +--- +name: migrate-wagmi-metamask-connector +description: Migrate a wagmi app from @metamask/sdk to the new @metamask/connect-evm connector (wagmi PR #4960) +maturity: stable +--- +# Migrate Wagmi MetaMask Connector to @metamask/connect-evm + +## When to use +- Upgrading a wagmi project from `@wagmi/connectors` v6.x/v7.x (which bundled `@metamask/sdk`) to v8.x+ / wagmi >= 3.6 (which uses `@metamask/connect-evm`) +- You see errors like `Cannot find module '@metamask/sdk'` after updating wagmi +- You want to adopt the new MetaMask Connect SDK in an existing wagmi app +- Consumer is migrating to the latest wagmi version that includes the MetaMask connector refactor (PR [#4960](https://github.com/wevm/wagmi/pull/4960)) + +## Breaking Change Summary + +The MetaMask connector in wagmi has been **completely rewritten**. The underlying SDK changed from `@metamask/sdk` to `@metamask/connect-evm`. The connector now dynamically imports `@metamask/connect-evm` instead of bundling `@metamask/sdk`. + +**Key impacts:** +- New optional peer dependency: `@metamask/connect-evm` must be installed explicitly, at a version inside wagmi's declared peer range (check `npm info @wagmi/connectors peerDependencies` — currently `^1.3.0`) +- Old dependency `@metamask/sdk` should be removed +- Configuration parameter names changed (`dappMetadata` → `dapp`, `useDeeplink` → `mobile.useDeeplink`) +- Several deprecated SDK-specific options are removed entirely +- Internal provider type changed from `SDKProvider` to `EIP1193Provider` + +## Workflow + +### Step 1: Update Dependencies + +Remove the old SDK and install the new one: + +```bash +# npm +npm uninstall @metamask/sdk +npm install @metamask/connect-evm + +# pnpm +pnpm remove @metamask/sdk +pnpm add @metamask/connect-evm + +# yarn +yarn remove @metamask/sdk +yarn add @metamask/connect-evm +``` + +Then update wagmi packages to the latest: + +```bash +npm install wagmi@latest @wagmi/core@latest @wagmi/connectors@latest +``` + +### Step 2: Update MetaMask Connector Configuration + +#### Before (old `@metamask/sdk` options): + +```typescript +import { metaMask } from 'wagmi/connectors' + +metaMask({ + dappMetadata: { + name: 'My Dapp', + url: 'https://mydapp.com', + }, + useDeeplink: true, + logging: { sdk: false }, + // These SDK-specific options are REMOVED: + forceDeleteProvider: false, + forceInjectProvider: false, + injectProvider: false, +}) +``` + +#### After (new `@metamask/connect-evm` options): + +```typescript +import { metaMask } from 'wagmi/connectors' + +metaMask({ + dapp: { + name: 'My Dapp', + url: 'https://mydapp.com', + iconUrl: 'https://mydapp.com/icon.png', // new optional field + }, + debug: false, + // Mobile options are now nested: + mobile: { + useDeeplink: true, + preferredOpenLink: undefined, // required for React Native + }, +}) +``` + +### Step 3: Configuration Parameter Migration Reference + +| Old Parameter (`@metamask/sdk`) | New Parameter (`@metamask/connect-evm`) | Notes | +|---|---|---| +| `dappMetadata: { name, url }` | `dapp: { name, url, iconUrl }` | `dappMetadata` still works but is deprecated | +| `logging: { sdk: true }` | `debug: true` | `logging` still works but is deprecated | +| `useDeeplink: boolean` | `mobile: { useDeeplink: boolean }` | Moved into `mobile` namespace | +| `preferredOpenLink` | `mobile: { preferredOpenLink }` | Moved into `mobile` namespace | +| `forceDeleteProvider` | *(removed)* | No replacement — not needed with new SDK | +| `forceInjectProvider` | *(removed)* | No replacement — not needed with new SDK | +| `injectProvider` | *(removed)* | No replacement — not needed with new SDK | +| `readonlyRPCMap` | *(auto-configured)* | Built automatically from wagmi's chain config | +| `_source` | *(auto-set to 'wagmi')* | Set internally by the connector | + +### Step 4: Update connectAndSign / connectWith Usage (if applicable) + +The `connectAndSign` parameter name changed from `msg` to `message` internally. However, at the wagmi connector level the API is the same — you still pass `connectAndSign: 'message string'` in the `metaMask()` parameters. + +```typescript +// Still works the same at the wagmi config level: +metaMask({ + dapp: { name: 'My Dapp' }, + connectAndSign: 'Please sign this message to verify your identity', +}) +``` + +The `connectWith` API is also unchanged at the wagmi level: + +```typescript +metaMask({ + dapp: { name: 'My Dapp' }, + connectWith: { + method: 'eth_signTypedData_v4', + params: [address, typedData], + }, +}) +``` + +### Step 5: Handle Provider Type Changes + +If your code directly accesses the provider from the connector, the type has changed: + +```typescript +// Before: provider was SDKProvider from @metamask/sdk +// After: provider is EIP1193Provider from @metamask/connect-evm + +// The EIP1193Provider interface is the same standard interface, +// so provider.request() calls remain unchanged. + +// New: You can access the underlying MetamaskConnectEVM instance: +const connector = config.connectors.find(c => c.id === 'metaMaskSDK') +if (connector) { + const instance = await connector.getInstance() + // instance.accounts, instance.getChainId(), instance.switchChain(), etc. +} +``` + +### Step 6: Remove Deprecated Patterns + +The new connector handles event listeners internally. If you had code that manually managed MetaMask SDK event listeners, you can remove it: + +```typescript +// REMOVE any manual SDK event management like: +// sdk.on('accountsChanged', ...) +// sdk.on('chainChanged', ...) +// provider.removeListener(...) + +// Event handlers are now passed to createEVMClient internally. +// Wagmi hooks (useAccount, useChainId, etc.) handle state automatically. +``` + +### Step 7: Additional Wagmi API Renames (same major version) + +This wagmi release also includes several API renames. Deprecated aliases are provided but you should migrate: + +| Old API | New API | Package | +|---|---|---| +| `useAccount()` | `useConnection()` | `wagmi` | +| `useAccountEffect()` | `useConnectionEffect()` | `wagmi` | +| `useSwitchAccount()` | `useSwitchConnection()` | `wagmi` | +| `getAccount()` | `getConnection()` | `@wagmi/core` | +| `switchAccount()` | `switchConnection()` | `@wagmi/core` | +| `watchAccount()` | `watchConnection()` | `@wagmi/core` | +| `WagmiConfig` | `WagmiProvider` | `wagmi` (alias removed) | +| `useToken()` | `useReadContracts()` | `wagmi` (hook removed) | +| `useFeeData()` | `useEstimateFeesPerGas()` | `wagmi` (alias removed) | +| `normalizeChainId()` | *(removed)* | `wagmi` (export removed) | + +### Step 8: Verify the Migration + +After making changes, verify: + +1. **Build succeeds** — `npm run build` or `tsc --noEmit` should pass +2. **No `@metamask/sdk` imports remain** — search your codebase: + ```bash + grep -r "@metamask/sdk" --include="*.ts" --include="*.tsx" --include="*.js" + ``` +3. **Wallet connection works** — test connecting via MetaMask browser extension +4. **Mobile deep-link works** (if applicable) — test QR code / deep-link flow +5. **Chain switching works** — test switching between configured chains +6. **Signing works** — test message signing and transaction signing + +## Complete Before/After Example + +### Before (`@wagmi/connectors` <= 7.x + @metamask/sdk): + +```typescript +import { createConfig, http } from 'wagmi' +import { mainnet, sepolia, optimism } from 'wagmi/chains' +import { metaMask } from 'wagmi/connectors' + +export const config = createConfig({ + chains: [mainnet, sepolia, optimism], + connectors: [ + metaMask({ + dappMetadata: { + name: 'My Dapp', + url: window.location.origin, + }, + useDeeplink: true, + }), + ], + transports: { + [mainnet.id]: http(), + [sepolia.id]: http(), + [optimism.id]: http(), + }, +}) +``` + +### After (wagmi >= 3.6 / `@wagmi/connectors` >= 8 + @metamask/connect-evm): + +```typescript +import { createConfig, http } from 'wagmi' +import { mainnet, sepolia, optimism } from 'wagmi/chains' +import { metaMask } from 'wagmi/connectors' + +export const config = createConfig({ + chains: [mainnet, sepolia, optimism], + connectors: [ + metaMask({ + dapp: { + name: 'My Dapp', + url: window.location.origin, + }, + mobile: { + useDeeplink: true, + }, + }), + ], + transports: { + [mainnet.id]: http(), + [sepolia.id]: http(), + [optimism.id]: http(), + }, +}) +``` + +## React Native Specific Migration + +If you are using wagmi with React Native, the `preferredOpenLink` callback has moved: + +```typescript +// Before: +metaMask({ + dappMetadata: { name: 'My RN App' }, + preferredOpenLink: (link, target) => Linking.openURL(link), + useDeeplink: true, +}) + +// After: +metaMask({ + dapp: { name: 'My RN App' }, + mobile: { + preferredOpenLink: (link, target) => Linking.openURL(link), + useDeeplink: true, + }, +}) +``` + +## Important Notes +- `@metamask/connect-evm` is an **optional peer dependency** of `@wagmi/connectors` — you only need it if you use the `metaMask()` connector +- The connector ID remains `'metaMaskSDK'` and the name remains `'MetaMask'` — no changes to connector identity +- The connector's `rdns` is `['io.metamask', 'io.metamask.mobile']` — unchanged +- The `supportedNetworks` map is now auto-built from wagmi's configured chains and their default RPC URLs — you no longer need to pass `readonlyRPCMap` +- The `dappMetadata` parameter still works (it's mapped to `dapp` internally) but is deprecated — migrate to `dapp` for forward compatibility +- The `logging` parameter still works (mapped to `debug: true`) but is deprecated +- If no `dapp` config is provided, the connector defaults to `{ name: window.location.hostname, url: window.location.href }` in browsers, or `{ name: 'wagmi' }` in Node.js/SSR diff --git a/domains/metamask-connect/skills/send-evm-transaction/skill.md b/domains/metamask-connect/skills/send-evm-transaction/skill.md new file mode 100644 index 0000000..9e44e7c --- /dev/null +++ b/domains/metamask-connect/skills/send-evm-transaction/skill.md @@ -0,0 +1,248 @@ +--- +name: send-evm-transaction +description: Send ETH and contract transactions with MetaMask using eth_sendTransaction via the EIP-1193 provider, gas estimation, receipt polling, and the connectWith shortcut +maturity: stable +--- +# Send EVM Transactions with MetaMask Connect + +## When to use + +Use this skill when: +- Sending ETH transfers via `eth_sendTransaction` +- Calling smart contract functions by encoding `data` in the transaction +- Estimating gas with `eth_estimateGas` before sending +- Polling for transaction confirmation with `eth_getTransactionReceipt` +- Using the `connectWith` shortcut to connect and send in a single approval + +## Workflow + +### Step 1: Get the provider and connected account + +```typescript +import { createEVMClient, getInfuraRpcUrls } from '@metamask/connect-evm'; +import type { Hex, Address } from '@metamask/connect-evm'; + +const client = await createEVMClient({ + dapp: { name: 'My DApp', url: window.location.href }, + api: { + supportedNetworks: { + ...getInfuraRpcUrls({ infuraApiKey: 'YOUR_INFURA_KEY', chainIds: ['0x1', '0x89'] }), + }, + }, +}); + +const { accounts } = await client.connect({ chainIds: ['0x1'] }); +const provider = client.getProvider(); +const from = accounts[0] as Address; +``` + +### Step 2: Convert ETH to hex wei + +All `value` fields in transactions must be hex-encoded wei. 1 ETH = 10^18 wei: + +```typescript +function ethToHexWei(ethAmount: string): Hex { + const wei = BigInt(Math.round(parseFloat(ethAmount) * 1e18)); + return `0x${wei.toString(16)}` as Hex; +} + +// Examples +ethToHexWei('0.01'); // '0x2386f26fc10000' +ethToHexWei('0.001'); // '0x38d7ea4c68000' +ethToHexWei('1'); // '0xde0b6b3a7640000' +``` + +### Step 3: Send an ETH transfer + +Build the transaction params and call `eth_sendTransaction`: + +```typescript +const txParams = { + from: from, + to: '0xRecipientAddress' as Address, + value: ethToHexWei('0.01'), +}; + +try { + const txHash = await provider.request({ + method: 'eth_sendTransaction', + params: [txParams], + }) as Hex; + + console.log('Transaction hash:', txHash); +} catch (err: any) { + if (err.code === 4001) { + console.log('User rejected the transaction'); + return; + } + if (err.code === -32002) { + console.log('A transaction request is already pending'); + return; + } + throw err; +} +``` + +### Step 4: Estimate gas before sending + +Use `eth_estimateGas` to get a gas estimate, then optionally add a buffer: + +```typescript +const txParams = { + from: from, + to: '0xRecipientAddress' as Address, + value: ethToHexWei('0.01'), + data: '0x', // empty for plain ETH transfer +}; + +const estimatedGas = await provider.request({ + method: 'eth_estimateGas', + params: [txParams], +}) as Hex; + +// Add 20% buffer to the estimate +const gasWithBuffer = BigInt(estimatedGas) * 120n / 100n; +const gasHex = `0x${gasWithBuffer.toString(16)}` as Hex; + +const txHash = await provider.request({ + method: 'eth_sendTransaction', + params: [{ + ...txParams, + gas: gasHex, + }], +}) as Hex; +``` + +### Step 5: Send a contract interaction + +Encode the function call as the `data` field. For an ERC-20 `transfer(address,uint256)`: + +```typescript +// ERC-20 transfer function selector: 0xa9059cbb +// Encode: transfer(0xRecipient, 1000000) for USDC (6 decimals) +const recipient = '0xRecipientAddress'.slice(2).padStart(64, '0'); +const amount = (1000000).toString(16).padStart(64, '0'); // 1 USDC + +const data = `0xa9059cbb${recipient}${amount}` as Hex; + +const txHash = await provider.request({ + method: 'eth_sendTransaction', + params: [{ + from: from, + to: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as Address, // USDC contract + data: data, + value: '0x0', // no ETH sent with token transfer + }], +}) as Hex; +``` + +### Step 6: Poll for transaction receipt + +After sending, poll `eth_getTransactionReceipt` until the transaction is confirmed: + +```typescript +async function waitForReceipt( + provider: any, + txHash: Hex, + intervalMs = 2000, + timeoutMs = 120000, +): Promise { + const start = Date.now(); + + while (Date.now() - start < timeoutMs) { + const receipt = await provider.request({ + method: 'eth_getTransactionReceipt', + params: [txHash], + }); + + if (receipt !== null) { + // receipt.status: '0x1' = success, '0x0' = revert + return receipt; + } + + await new Promise((r) => setTimeout(r, intervalMs)); + } + + throw new Error(`Transaction ${txHash} not confirmed within ${timeoutMs}ms`); +} + +// Usage +const txHash = await provider.request({ + method: 'eth_sendTransaction', + params: [txParams], +}) as Hex; + +const receipt = await waitForReceipt(provider, txHash); + +if (receipt.status === '0x1') { + console.log('Transaction confirmed in block:', parseInt(receipt.blockNumber, 16)); +} else { + console.error('Transaction reverted'); +} +``` + +### Step 7: Use connectWith for single-approval flow + +`connectWith` connects the wallet and sends a transaction in one user interaction: + +```typescript +const { accounts, chainId, result } = await client.connectWith({ + method: 'eth_sendTransaction', + // The params function receives the FIRST connected account (a single + // Address), not the accounts array + params: (account: Address) => [ + { + from: account, + to: '0xRecipientAddress' as Address, + value: ethToHexWei('0.01'), + }, + ], + chainIds: ['0x1'], +}); + +// result is the transaction hash +const txHash = result as Hex; +console.log('Connected as:', accounts[0]); +console.log('Transaction hash:', txHash); +``` + +The `params` field accepts a function that receives the first connected account (`(account: Address) => unknown[]`), letting you use the connected address as `from` without knowing it ahead of time. + +### Step 8: Handle errors + +```typescript +try { + const txHash = await provider.request({ + method: 'eth_sendTransaction', + params: [txParams], + }); +} catch (err: any) { + switch (err.code) { + case 4001: + // User rejected the transaction — offer retry + break; + case -32002: + // Request already pending — wait for user action in MetaMask + break; + case -32000: + // Execution error (insufficient funds, gas too low, etc.) + console.error('Execution error:', err.message); + break; + default: + console.error('Transaction failed:', err); + } +} +``` + +## Important Notes + +- **`value` must be hex-encoded wei** — `'0xde0b6b3a7640000'` is 1 ETH. Never pass decimal strings or ETH-denominated numbers directly. +- **`from` must match the connected account** — MetaMask rejects transactions where `from` doesn't match the active account. +- **`eth_sendTransaction` returns a transaction hash, not a receipt** — poll `eth_getTransactionReceipt` to confirm the transaction was mined. +- **Receipt `status` is hex** — `'0x1'` means success, `'0x0'` means the transaction was mined but reverted. +- **`eth_estimateGas` can throw** — if the transaction would revert, estimation fails. Wrap it in a try/catch and show the error to the user. +- **`connectWith` params can be a function** — `params: (account) => [{ from: account, ... }]` — it receives the first connected account (a single `Address`, not an array). +- **Chain IDs are always hex strings in SDK calls** — `'0x1'`, `'0x89'`, `'0xaa36a7'`. The `chainId` in transaction objects follows the same convention when present. +- **Error code 4001** means the user deliberately rejected — handle gracefully. +- **Error code -32002** means a request is pending — do not send another transaction. +- **`0x1` is auto-included** in every `connect()` / `connectWith()` call. diff --git a/domains/metamask-connect/skills/send-solana-transaction/skill.md b/domains/metamask-connect/skills/send-solana-transaction/skill.md new file mode 100644 index 0000000..8a51b72 --- /dev/null +++ b/domains/metamask-connect/skills/send-solana-transaction/skill.md @@ -0,0 +1,286 @@ +--- +name: send-solana-transaction +description: Build and send a Solana transaction using MetaMask Connect. Covers both the React wallet-adapter approach (sendTransaction) and the vanilla browser approach (signAndSendTransaction wallet-standard feature). +maturity: stable +--- +# Send Solana Transaction with MetaMask + +## When to use + +Use this skill when: +- Sending SOL or interacting with Solana programs via MetaMask Connect +- Building a `Transaction` with `@solana/web3.js` and submitting it through the wallet +- Using `sendTransaction` from `useWallet` in a React app +- Using the `solana:signAndSendTransaction` wallet-standard feature in a vanilla browser app + +## Workflow + +### Step 1: Build the transaction + +Use `@solana/web3.js` to construct the transaction. Every transaction needs a recent blockhash and a fee payer. + +```typescript +import { + Connection, + Transaction, + SystemProgram, + PublicKey, + LAMPORTS_PER_SOL, +} from '@solana/web3.js'; + +const connection = new Connection('https://api.mainnet-beta.solana.com'); +const { blockhash } = await connection.getLatestBlockhash(); + +const senderPubkey = new PublicKey('SENDER_PUBLIC_KEY'); +const recipientPubkey = new PublicKey('RECIPIENT_PUBLIC_KEY'); + +const transaction = new Transaction().add( + SystemProgram.transfer({ + fromPubkey: senderPubkey, + toPubkey: recipientPubkey, + lamports: 0.001 * LAMPORTS_PER_SOL, + }), +); +transaction.recentBlockhash = blockhash; +transaction.feePayer = senderPubkey; +``` + +### Step 2a: Send with React wallet-adapter (useWallet) + +**Prerequisites:** `createSolanaClient` has been awaited before rendering, `WalletProvider` is configured with `wallets={[]}`, and the user is connected. See the `setup-solana-react-app` skill. + +```tsx +import { useWallet, useConnection } from '@solana/wallet-adapter-react'; +import { + Transaction, + SystemProgram, + PublicKey, + LAMPORTS_PER_SOL, +} from '@solana/web3.js'; + +function SendTransactionButton() { + const { publicKey, sendTransaction, connected } = useWallet(); + const { connection } = useConnection(); + + const handleSend = async () => { + if (!publicKey || !sendTransaction) return; + + try { + const { blockhash } = await connection.getLatestBlockhash(); + + const transaction = new Transaction().add( + SystemProgram.transfer({ + fromPubkey: publicKey, + toPubkey: new PublicKey('RECIPIENT_PUBLIC_KEY'), + lamports: 0.001 * LAMPORTS_PER_SOL, + }), + ); + transaction.recentBlockhash = blockhash; + transaction.feePayer = publicKey; + + const signature = await sendTransaction(transaction, connection); + console.log('Transaction submitted:', signature); + + const confirmation = await connection.confirmTransaction(signature, 'confirmed'); + console.log('Transaction confirmed:', confirmation); + } catch (err: any) { + if (err.code === 4001) { + console.log('User rejected the transaction'); + return; + } + console.error('Transaction failed:', err); + } + }; + + return ( + + ); +} +``` + +### Step 2b: Send with vanilla browser (wallet-standard feature) + +**Prerequisites:** `createSolanaClient` has been called and the wallet is connected via `standard:connect`. See the `setup-solana-browser-app` skill. + +```typescript +import { createSolanaClient } from '@metamask/connect-solana'; +import { + Connection, + Transaction, + SystemProgram, + PublicKey, + LAMPORTS_PER_SOL, +} from '@solana/web3.js'; + +const solanaClient = await createSolanaClient({ + dapp: { name: 'My DApp', url: window.location.href }, +}); + +const wallet = solanaClient.getWallet(); + +// Connect first +const connectFeature = wallet.features['standard:connect']; +const { accounts } = await connectFeature.connect(); +const account = accounts[0]; +const senderPubkey = new PublicKey(account.address); + +// Build the transaction +const connection = new Connection('https://api.mainnet-beta.solana.com'); +const { blockhash } = await connection.getLatestBlockhash(); + +const transaction = new Transaction().add( + SystemProgram.transfer({ + fromPubkey: senderPubkey, + toPubkey: new PublicKey('RECIPIENT_PUBLIC_KEY'), + lamports: 0.001 * LAMPORTS_PER_SOL, + }), +); +transaction.recentBlockhash = blockhash; +transaction.feePayer = senderPubkey; + +// Serialize and send +const serializedTransaction = transaction.serialize({ + requireAllSignatures: false, +}); + +const signAndSendFeature = wallet.features['solana:signAndSendTransaction']; + +// The `chain` field accepts the wallet-standard short forms ('solana:mainnet', +// 'solana:devnet', 'solana:testnet') or the full genesis-hash CAIP-2 scope +// (e.g. 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'). It is optional and +// defaults to mainnet. Bare 'mainnet' (without the 'solana:' prefix) is +// INVALID here and throws 'Unsupported chainId' — the SolanaNetwork short +// names apply only to createSolanaClient's api.supportedNetworks keys. + +// Requires `npm install bs58` and `import bs58 from 'bs58'` at the top of the file +try { + const [{ signature }] = await signAndSendFeature.signAndSendTransaction({ + account, + transaction: serializedTransaction, + chain: 'solana:mainnet', // wallet-standard short form; full genesis-hash CAIP-2 IDs also accepted + }); + + // signature is a Uint8Array — encode with bs58 ('base58' is NOT a Buffer encoding) + const signatureBase58 = bs58.encode(signature); + console.log('Transaction submitted:', signatureBase58); + + // confirmTransaction expects the base58 signature string + const confirmation = await connection.confirmTransaction(signatureBase58, 'confirmed'); + console.log('Transaction confirmed:', confirmation); +} catch (err: any) { + if (err.code === 4001) { + console.log('User rejected the transaction'); + } else { + console.error('Transaction failed:', err); + } +} +``` + +### Step 3: Sign without sending (sign-only flow) + +If you need to sign a transaction without broadcasting it (e.g., for offline signing or multi-sig): + +**React:** + +```typescript +const { signTransaction } = useWallet(); + +if (signTransaction) { + const signedTransaction = await signTransaction(transaction); + // signedTransaction is now signed but NOT sent — broadcast manually if needed + const rawTransaction = signedTransaction.serialize(); + const signature = await connection.sendRawTransaction(rawTransaction); +} +``` + +**Browser (wallet-standard):** + +```typescript +const signFeature = wallet.features['solana:signTransaction']; + +const [{ signedTransaction }] = await signFeature.signTransaction({ + account, + transaction: transaction.serialize({ requireAllSignatures: false }), + chain: 'solana:mainnet', +}); + +// signedTransaction is serialized and signed — broadcast manually +const signature = await connection.sendRawTransaction(signedTransaction); +``` + +### Step 4: Send multiple transactions (batch) + +There is **no** `solana:signAndSendAllTransactions` feature. The `solana:signAndSendTransaction` feature is **variadic** — pass multiple inputs and it returns one result per input: + +```typescript +const signAndSendFeature = wallet.features['solana:signAndSendTransaction']; + +const results = await signAndSendFeature.signAndSendTransaction( + { account, transaction: serializedTx1, chain: 'solana:mainnet' }, + { account, transaction: serializedTx2, chain: 'solana:mainnet' }, + { account, transaction: serializedTx3, chain: 'solana:mainnet' }, +); + +for (const { signature } of results) { + console.log('Tx signature:', bs58.encode(signature)); +} +``` + +### Step 5: Confirm the transaction + +Always confirm after sending. Use `'confirmed'` commitment for most use cases: + +```typescript +const confirmation = await connection.confirmTransaction(signature, 'confirmed'); + +if (confirmation.value.err) { + console.error('Transaction failed on-chain:', confirmation.value.err); +} else { + console.log('Transaction succeeded'); +} +``` + +### Step 6: Error handling + +| Error | Cause | Action | +|-------|-------|--------| +| Code `4001` | User rejected in MetaMask | Show retry UI | +| Code `-32002` | Request already pending | Wait for user to act in MetaMask | +| `Blockhash not found` | Blockhash expired before submission | Fetch a new blockhash and rebuild | +| `Insufficient funds` | Sender balance too low for transfer + fees | Show balance check UI | +| `Transaction too large` | Transaction exceeds 1232 bytes | Split into multiple transactions | + +```typescript +try { + const signature = await sendTransaction(transaction, connection); + await connection.confirmTransaction(signature, 'confirmed'); +} catch (err: any) { + switch (err.code) { + case 4001: + // User rejected + break; + case -32002: + // Pending — ask user to check MetaMask + break; + default: + if (err.message?.includes('Blockhash not found')) { + // Retry with fresh blockhash + } + console.error('Transaction error:', err); + } +} +``` + +## Important Notes + +- **Always fetch a fresh blockhash** — blockhashes expire after ~60 seconds. Fetch `getLatestBlockhash()` immediately before building the transaction, not at app startup. +- **Set `feePayer` before signing** — the transaction must have `feePayer` and `recentBlockhash` set before it is signed or serialized. +- **Serialize with `requireAllSignatures: false`** — when using wallet-standard features directly, the wallet will add the signature. Serializing with `requireAllSignatures: true` (the default) will throw because the transaction isn't signed yet. +- **`sendTransaction` vs `signAndSendTransaction`** — in the React adapter, `sendTransaction` handles serialization internally. With wallet-standard features, you must serialize the transaction yourself and pass the bytes. +- **`chain` is only honored by `signAndSendTransaction`** — `signAndSendTransaction` reads the input's `chain` to pick the cluster, but `signTransaction`/`signMessage` ignore any `chain` field and use the **connected session scope** instead. Passing `chain` to `signTransaction` is harmless but doesn't switch networks; connect with the scope you want to sign on. +- **Solana networks** — mainnet, devnet, and testnet scopes are all modeled by the SDK; non-mainnet availability depends on the connected MetaMask build/version, so handle connection errors rather than assuming a cluster is present. +- **`disconnect()` only revokes Solana scopes** — EVM sessions remain active. +- **Chrome on Android** — apply the `beforeunload` workaround for the known page-unload bug during wallet interactions. +- **Confirm before reporting success** — a submitted transaction is not finalized until `confirmTransaction` returns. Always confirm before updating the UI. diff --git a/domains/metamask-connect/skills/setup-evm-browser-app/skill.md b/domains/metamask-connect/skills/setup-evm-browser-app/skill.md new file mode 100644 index 0000000..f2a7256 --- /dev/null +++ b/domains/metamask-connect/skills/setup-evm-browser-app/skill.md @@ -0,0 +1,294 @@ +--- +name: setup-evm-browser-app +description: Scaffold a vanilla JS/TS browser app with MetaMask EVM integration using createEVMClient, EIP-1193 provider event listeners, RPC methods, and chain switching with chainConfiguration fallback +maturity: stable +--- +# Setup EVM Browser App with MetaMask Connect + +## When to use + +Use this skill when: +- Building a vanilla JavaScript or TypeScript browser app (no React) with MetaMask +- Integrating `createEVMClient` into a plain HTML page or a bundler-based project +- Wiring up EIP-1193 provider event listeners for account and chain changes +- Performing RPC calls through `provider.request` in a non-framework context + +## Workflow + +### Step 1: Install dependencies + +```bash +npm install @metamask/connect-evm +``` + +`@metamask/connect-multichain` is a regular dependency of `@metamask/connect-evm` and is installed transitively. (Only the 2.0.0 release briefly made it a peer dependency; 2.1.0 reverted that.) Installing it explicitly is harmless but not required. The SDK warns at runtime if duplicate or mismatched copies are resolved. + +Or include via CDN/script tag if not using a bundler. + +### Step 2: Create the EVM client + +```typescript +import { createEVMClient, getInfuraRpcUrls } from '@metamask/connect-evm'; + +const client = await createEVMClient({ + dapp: { + name: 'My Browser DApp', + url: window.location.href, + }, + api: { + supportedNetworks: { + ...getInfuraRpcUrls({ infuraApiKey: 'YOUR_INFURA_KEY', chainIds: ['0x1', '0x89', '0xa4b1', '0xaa36a7'] }), + }, + }, + ui: { + headless: false, + preferExtension: true, + showInstallModal: true, + }, + eventHandlers: { + // Keys are camelCase — `display_uri`/`wallet_sessionChanged` are NOT valid here + displayUri: (uri: string) => { + console.log('QR URI:', uri); + // Render QR code for mobile connection + }, + connect: ({ accounts, chainId }) => { + // Fires on connection and on automatic session restore + updateUI(accounts, chainId); + }, + }, + debug: false, +}); +``` + +There is no `wallet_sessionChanged` handler on the EVM client — session restores surface through the `connect` handler / provider event and `accountsChanged`. (`wallet_sessionChanged` is a multichain-client event.) + +### Step 3: Register provider event listeners + +Set up EIP-1193 event listeners immediately after client creation: + +```typescript +const provider = client.getProvider(); + +provider.on('accountsChanged', (accounts: string[]) => { + if (accounts.length === 0) { + // User disconnected their wallet + updateUI([], null); + return; + } + updateUI(accounts, null); + fetchBalance(accounts[0]); +}); + +provider.on('chainChanged', (chainId: string) => { + // chainId is a hex string, e.g. '0x1' + document.getElementById('chain')!.textContent = `Chain: ${chainId}`; + // Refresh balances since the chain changed + const currentAccount = document.getElementById('account')?.dataset.address; + if (currentAccount) fetchBalance(currentAccount); +}); + +provider.on('disconnect', () => { + // The connect-evm provider emits `disconnect` with no payload + console.log('Disconnected'); + updateUI([], null); +}); +``` + +### Step 4: Connect and update UI + +```typescript +const connectBtn = document.getElementById('connect-btn')!; +const disconnectBtn = document.getElementById('disconnect-btn')!; + +connectBtn.addEventListener('click', async () => { + try { + connectBtn.textContent = 'Connecting...'; + connectBtn.setAttribute('disabled', 'true'); + + const { accounts, chainId } = await client.connect({ + chainIds: ['0x1'], + }); + + updateUI(accounts, chainId); + } catch (err: any) { + if (err.code === 4001) { + showError('Connection rejected. Click Connect to try again.'); + return; + } + if (err.code === -32002) { + showError('A connection request is already pending. Check MetaMask.'); + return; + } + showError(err.message ?? 'Connection failed'); + } finally { + connectBtn.textContent = 'Connect MetaMask'; + connectBtn.removeAttribute('disabled'); + } +}); + +disconnectBtn.addEventListener('click', async () => { + await client.disconnect(); + updateUI([], null); +}); + +function updateUI(accounts: string[], chainId: string | null) { + const accountEl = document.getElementById('account')!; + const chainEl = document.getElementById('chain')!; + const connectedSection = document.getElementById('connected')!; + + if (accounts.length === 0) { + connectedSection.style.display = 'none'; + connectBtn.style.display = 'block'; + return; + } + + accountEl.textContent = `Account: ${accounts[0]}`; + accountEl.dataset.address = accounts[0]; + if (chainId) chainEl.textContent = `Chain: ${chainId}`; + connectedSection.style.display = 'block'; + connectBtn.style.display = 'none'; +} + +function showError(message: string) { + const errorEl = document.getElementById('error')!; + errorEl.textContent = message; + setTimeout(() => { errorEl.textContent = ''; }, 5000); +} +``` + +### Step 5: Make RPC calls via provider.request + +```typescript +const provider = client.getProvider(); + +// Cached/intercepted reads — safe before connect (no chain selection needed) +const chainId = await provider.request({ method: 'eth_chainId' }); +const accounts = await provider.request({ method: 'eth_accounts' }); + +// Node-routed reads need a SELECTED chain — they throw `No chain ID selected` +// until after connect() (or a restored session). Call these post-connect. +const blockNumber = await provider.request({ method: 'eth_blockNumber' }); + +async function fetchBalance(address: string) { + const wei = await provider.request({ + method: 'eth_getBalance', + params: [address, 'latest'], + }) as string; + + const ethBalance = parseInt(wei, 16) / 1e18; + document.getElementById('balance')!.textContent = + `Balance: ${ethBalance.toFixed(6)} ETH`; +} + +// Get gas price +const gasPrice = await provider.request({ method: 'eth_gasPrice' }); + +// Get transaction count (nonce) +const nonce = await provider.request({ + method: 'eth_getTransactionCount', + params: [accounts[0], 'latest'], +}); + +// Call a contract (read-only) +const result = await provider.request({ + method: 'eth_call', + params: [ + { + to: '0xContractAddress', + data: '0xEncodedFunctionSelector', + }, + 'latest', + ], +}); +``` + +### Step 6: Switch chains with chainConfiguration fallback + +```typescript +async function switchChain(targetChainId: string) { + try { + await client.switchChain({ chainId: targetChainId }); + } catch (err: any) { + if (err.code === 4001) { + showError('Chain switch rejected by user.'); + } + } +} + +// Switch to a chain with fallback configuration +// chainConfiguration triggers wallet_addEthereumChain if the chain +// is not already configured in the user's wallet +async function switchToArbitrum() { + try { + await client.switchChain({ + chainId: '0xa4b1', + chainConfiguration: { + chainId: '0xa4b1', // optional in the type, but set it to the target chain — if omitted it falls back to the currently selected chain (likely the wrong chain to add) + chainName: 'Arbitrum One', + nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, + rpcUrls: ['https://arb1.arbitrum.io/rpc'], + blockExplorerUrls: ['https://arbiscan.io'], + }, + }); + } catch (err: any) { + if (err.code === 4001) { + showError('User rejected the chain addition or switch.'); + } + } +} + +// Switch to well-known chains +document.getElementById('switch-mainnet')!.addEventListener('click', + () => switchChain('0x1')); +document.getElementById('switch-polygon')!.addEventListener('click', + () => switchChain('0x89')); +document.getElementById('switch-sepolia')!.addEventListener('click', + () => switchChain('0xaa36a7')); +document.getElementById('switch-arbitrum')!.addEventListener('click', + () => switchToArbitrum()); +``` + +### Step 7: Complete HTML structure + +```html + + + + + MetaMask Connect + + +

MetaMask Connect Demo

+ + +

+ + + + + + +``` + +## Important Notes + +- **Call `createEVMClient` once at app startup** — each call returns a *new* EVM client wrapper, but they all share one underlying multichain core (the core is the singleton whose options merge across calls). Don't recreate the client repeatedly. +- **Chain IDs are always hex strings** — use `'0x1'`, `'0x89'`, `'0xaa36a7'`. Never pass decimal numbers or decimal strings. +- **`0x1` (Ethereum mainnet) is auto-included** in every `connect()` call regardless of the `chainIds` you specify. +- **The provider exists before connection** — `client.getProvider()` always returns a valid EIP-1193 provider. But node-routed reads (`eth_blockNumber`, `eth_getBalance`, `eth_call`, …) require a **selected chain** and throw `No chain ID selected` until one is set (after `connect()` or a restored session). Only `eth_chainId` and `eth_accounts` (intercepted, served from cache) are safe before connecting. +- **Convenience getters** — use `client.getChainId()` (returns `Hex | undefined`) and `client.getAccount()` (returns `Address | undefined`) instead of `provider.request({ method: 'eth_chainId' })` / `eth_accounts` for cached state. +- **Connection status** — `client.status` returns `'connecting'` | `'connected'` | `'disconnected'`. (The 5-value `'loaded'`/`'pending'` union belongs to the multichain core, not the EVM client.) Use this for UI state instead of tracking manually. +- **Register event listeners before connecting** — set up `accountsChanged`, `chainChanged`, and `disconnect` handlers immediately after getting the provider. +- **`chainConfiguration` is a fallback, not a forced add** — it is only used if the wallet doesn't already have the chain configured. If the chain exists, only `wallet_switchEthereumChain` fires. +- **Page reloads restore automatically** — the EVM client syncs any persisted session before `createEVMClient` resolves and re-emits `connect`/`accountsChanged` on the provider. The EVM client has no `.on()` method and no `wallet_sessionChanged` handler — use the provider events (or `eventHandlers.connect`) to restore UI state. +- **Error code 4001** means the user deliberately rejected — show a retry option, not a crash screen. +- **Error code -32002** means a request is already pending — do not send another `connect()`. Wait for the user to respond in MetaMask. diff --git a/domains/metamask-connect/skills/setup-evm-react-app/skill.md b/domains/metamask-connect/skills/setup-evm-react-app/skill.md new file mode 100644 index 0000000..8472802 --- /dev/null +++ b/domains/metamask-connect/skills/setup-evm-react-app/skill.md @@ -0,0 +1,299 @@ +--- +name: setup-evm-react-app +description: Scaffold a React app with MetaMask EVM integration using createEVMClient, useState/useEffect/useRef patterns, provider.request calls, chain switching, and error handling +maturity: stable +--- +# Setup EVM React App with MetaMask Connect + +## When to use + +Use this skill when: +- Creating a new React app that connects to MetaMask via `@metamask/connect-evm` +- Adding wallet connect, sign, or send functionality to an existing React app +- Setting up `createEVMClient` with Infura RPC URLs and event handlers +- Building a React component that tracks accounts, chain, and balance state + +## Workflow + +### Step 1: Install dependencies + +```bash +npm install @metamask/connect-evm @metamask/connect-multichain +``` + +`@metamask/connect-multichain` is a regular dependency of `@metamask/connect-evm` and is installed transitively. (Only the 2.0.0 release briefly made it a peer dependency; 2.1.0 reverted that.) Installing it explicitly is harmless but not required. The SDK warns at runtime if duplicate or mismatched copies are resolved. + +### Step 2: Create the EVM client + +Create a module that initializes the client once and exports a ready promise: + +```typescript +// src/metamask.ts +import { createEVMClient, getInfuraRpcUrls } from '@metamask/connect-evm'; +import type { MetamaskConnectEVM } from '@metamask/connect-evm'; + +let clientPromise: Promise | null = null; + +export function getClient(): Promise { + if (!clientPromise) { + clientPromise = createEVMClient({ + dapp: { + name: 'My React DApp', + url: window.location.href, + }, + api: { + supportedNetworks: { + ...getInfuraRpcUrls({ infuraApiKey: 'YOUR_INFURA_KEY', chainIds: ['0x1', '0x89', '0xaa36a7'] }), + '0xa4b1': 'https://arb1.arbitrum.io/rpc', + }, + }, + ui: { + headless: false, + preferExtension: true, + showInstallModal: true, + }, + eventHandlers: { + displayUri: (uri: string) => { + console.log('QR URI:', uri); + }, + }, + debug: false, + }); + } + return clientPromise; +} +``` + +`getInfuraRpcUrls({ infuraApiKey, chainIds? })` returns a `Record` mapping hex chain IDs to Infura RPC URLs for all Infura-supported EVM chains. Pass an optional `chainIds` array (hex strings, e.g. `['0x1', '0x89']`) to limit the output to specific chains. Spread it into `supportedNetworks` and add custom RPCs for any additional chains. + +### Step 3: Build the wallet component + +Use `useRef` to hold the client instance and `useState` for reactive UI state: + +```tsx +// src/WalletConnect.tsx +import { useEffect, useRef, useState, useCallback } from 'react'; +import { getClient } from './metamask'; +import type { MetamaskConnectEVM } from '@metamask/connect-evm'; +import type { Hex, Address } from '@metamask/connect-evm'; + +export function WalletConnect() { + const clientRef = useRef(null); + const [accounts, setAccounts] = useState([]); + const [chainId, setChainId] = useState(null); + const [balance, setBalance] = useState(''); + const [connecting, setConnecting] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + let mounted = true; + + async function init() { + const client = await getClient(); + if (!mounted) return; + clientRef.current = client; + + const provider = client.getProvider(); + + provider.on('accountsChanged', (accs: Address[]) => { + if (mounted) setAccounts(accs); + }); + + provider.on('chainChanged', (id: Hex) => { + if (mounted) setChainId(id); + }); + + provider.on('disconnect', () => { + if (mounted) { + setAccounts([]); + setChainId(null); + setBalance(''); + } + }); + } + + init(); + return () => { mounted = false; }; + }, []); + + const handleConnect = useCallback(async () => { + const client = clientRef.current; + if (!client) return; + + setConnecting(true); + setError(null); + + try { + const result = await client.connect({ chainIds: ['0x1'] }); + setAccounts(result.accounts as Address[]); + setChainId(result.chainId as Hex); + } catch (err: any) { + if (err.code === 4001) { + setError('Connection rejected. Please try again.'); + return; + } + if (err.code === -32002) { + setError('A connection request is already pending. Check MetaMask.'); + return; + } + setError(err.message ?? 'Connection failed'); + } finally { + setConnecting(false); + } + }, []); + + const handleDisconnect = useCallback(async () => { + const client = clientRef.current; + if (!client) return; + await client.disconnect(); + setAccounts([]); + setChainId(null); + setBalance(''); + }, []); + + const fetchBalance = useCallback(async () => { + const client = clientRef.current; + if (!client || accounts.length === 0) return; + + const provider = client.getProvider(); + const wei = await provider.request({ + method: 'eth_getBalance', + params: [accounts[0], 'latest'], + }) as Hex; + + const ethBalance = parseInt(wei, 16) / 1e18; + setBalance(ethBalance.toFixed(6)); + }, [accounts]); + + // chainConfiguration must match the target chain (and include its chainId) — + // the wallet receives it verbatim as wallet_addEthereumChain params + const handleSwitchToPolygon = useCallback(async () => { + const client = clientRef.current; + if (!client) return; + + try { + await client.switchChain({ + chainId: '0x89', + chainConfiguration: { + chainId: '0x89', + chainName: 'Polygon', + nativeCurrency: { name: 'MATIC', symbol: 'MATIC', decimals: 18 }, + rpcUrls: ['https://polygon-rpc.com'], + blockExplorerUrls: ['https://polygonscan.com'], + }, + }); + } catch (err: any) { + if (err.code === 4001) { + setError('Chain switch rejected by user.'); + } + } + }, []); + + const isConnected = accounts.length > 0; + + if (!isConnected) { + return ( +
+ + {error &&

{error}

} +
+ ); + } + + return ( +
+

Account: {accounts[0]}

+

Chain ID: {chainId}

+

Balance: {balance || '—'} ETH

+ + + + {error &&

{error}

} +
+ ); +} +``` + +### Step 4: Use provider.request for RPC calls + +Once connected, use the EIP-1193 provider for any Ethereum JSON-RPC method: + +```typescript +const provider = client.getProvider(); + +// Get current block number +const blockNumber = await provider.request({ method: 'eth_blockNumber' }); + +// Get chain ID +const chainId = await provider.request({ method: 'eth_chainId' }); + +// Get accounts +const accounts = await provider.request({ method: 'eth_accounts' }); + +// Get balance +const balance = await provider.request({ + method: 'eth_getBalance', + params: [accounts[0], 'latest'], +}); + +// Get transaction count (nonce) +const nonce = await provider.request({ + method: 'eth_getTransactionCount', + params: [accounts[0], 'latest'], +}); +``` + +### Step 5: Switch chains + +Use `client.switchChain` to request a network change. The `chainConfiguration` fallback triggers `wallet_addEthereumChain` if the chain is not already in the user's wallet: + +```typescript +await client.switchChain({ + chainId: '0xaa36a7', // Sepolia +}); + +// With fallback configuration for unknown chains +await client.switchChain({ + chainId: '0xa4b1', // Arbitrum One + chainConfiguration: { + chainId: '0xa4b1', // optional in the type, but set it to the target chain — if omitted it falls back to the currently selected chain (likely the wrong chain to add) + chainName: 'Arbitrum One', + nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, + rpcUrls: ['https://arb1.arbitrum.io/rpc'], + blockExplorerUrls: ['https://arbiscan.io'], + }, +}); +``` + +### Step 6: Handle errors + +Always catch and handle known error codes: + +```typescript +try { + await client.connect({ chainIds: ['0x1'] }); +} catch (err: any) { + switch (err.code) { + case 4001: + // User rejected the request — show retry UI + break; + case -32002: + // Request already pending — tell user to check MetaMask + break; + default: + console.error('Unexpected error:', err); + } +} +``` + +## Important Notes + +- **Call `createEVMClient` once at app startup** — store the promise and reuse it; never call it per render. Each call returns a *new* EVM client wrapper, but they all share one underlying multichain core (the core is the singleton, and its options are merged across calls). +- **Chain IDs are always hex strings** — use `'0x1'` (Ethereum), `'0x89'` (Polygon), `'0xaa36a7'` (Sepolia). Never use decimal numbers. +- **`0x1` (Ethereum mainnet) is always auto-included** in `connect()` regardless of the `chainIds` you pass. +- **The provider exists before connection** — `client.getProvider()` never returns `undefined`. But node-routed reads (`eth_blockNumber`, `eth_getBalance`, `eth_call`, …) require a **selected chain** and throw `No chain ID selected` until one is set (after `connect()` or a restored session). Only the intercepted methods `eth_chainId` and `eth_accounts` (served from cached state) are safe before connecting. +- **Register event listeners early** — set up `accountsChanged`, `chainChanged`, and `disconnect` listeners in `useEffect` before the user connects. +- **Error code 4001 is not an application error** — it means the user deliberately rejected. Handle it gracefully with a retry option. +- **Error code -32002 means a request is pending** — do not fire another `connect()` call. Wait for the user to act in MetaMask. diff --git a/domains/metamask-connect/skills/setup-evm-react-native-app/skill.md b/domains/metamask-connect/skills/setup-evm-react-native-app/skill.md new file mode 100644 index 0000000..805825f --- /dev/null +++ b/domains/metamask-connect/skills/setup-evm-react-native-app/skill.md @@ -0,0 +1,321 @@ +--- +name: setup-evm-react-native-app +description: Scaffold a React Native app with MetaMask EVM integration including required polyfills, metro.config.js shims, import order constraints, mobile deeplinks, and a full component example +maturity: stable +--- +# Setup EVM React Native App with MetaMask Connect + +## When to use + +Use this skill when: +- Creating a React Native app that connects to MetaMask Mobile +- Setting up polyfills for window and other missing globals +- Configuring metro.config.js with Node.js module shims +- Debugging React Native import order or missing polyfill errors + +## Workflow + +### Step 1: Install dependencies + +Install the SDK and all required polyfill/shim packages: + +```bash +npm install @metamask/connect-evm @metamask/connect-multichain react-native-get-random-values buffer @react-native-async-storage/async-storage readable-stream +``` + +`@metamask/connect-multichain` is installed transitively by `@metamask/connect-evm` (only the 2.0.0 release briefly made it a peer dependency; 2.1.0 reverted that) — installing it explicitly is harmless but not required. The SDK warns at runtime on duplicate or mismatched copies. `react-native-get-random-values` provides `crypto.getRandomValues` — strictly required only on React Native < 0.72 (Hermes 0.72+ ships `globalThis.crypto.getRandomValues` natively), but recommended as a safety net on all versions. It **must** be imported before any other SDK-related code. `readable-stream` provides the `stream` shim for Metro. `buffer` is recommended as a safety net for peer dependencies — `@metamask/connect-multichain` self-polyfills Buffer internally, but other deps (e.g. `eciesjs`) may load before it. `@react-native-async-storage/async-storage` is needed for session persistence. + +### Step 2: Create polyfills.ts + +Create `src/polyfills.ts` with all required global shims. This file must be imported before anything else: + +```typescript +// src/polyfills.ts +// IMPORTANT: react-native-get-random-values must be imported in the +// entry file BEFORE this polyfills file. See Step 4. + +import { Buffer } from 'buffer'; + +// Buffer global — connect-multichain self-polyfills this, but set it here +// as a safety net for other deps that may load before connect-multichain. +global.Buffer = Buffer; + +// window object — required for correct platform detection and deeplink behaviour. +// connect-multichain inspects window.navigator.product and window.location to +// determine platform type and whether to use deeplinks vs install modal. +const eventListeners = new Map>(); +if (typeof global.window === 'undefined') { + (global as any).window = { + location: { + hostname: 'my-rn-app', + href: 'https://my-rn-app.local', + }, + navigator: { product: 'ReactNative' }, + addEventListener: (event: string, listener: EventListener) => { + if (!eventListeners.has(event)) eventListeners.set(event, new Set()); + eventListeners.get(event)?.add(listener); + }, + removeEventListener: (event: string, listener: EventListener) => { + eventListeners.get(event)?.delete(listener); + }, + dispatchEvent: (_event: Event) => true, + }; +} + +// NOTE: Event and CustomEvent polyfills are NOT needed for standalone +// @metamask/connect-evm usage — the SDK uses eventemitter3 internally. +// Add them only if you are also using wagmi (wagmi dispatches DOM events). +``` + +### Step 3: Configure metro.config.js + +Metro cannot resolve Node.js built-in modules. Map them to React Native-compatible shims or an empty module: + +```javascript +// metro.config.js +const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); +const path = require('path'); + +// Create a path to an empty module for stubs +const emptyModule = path.resolve(__dirname, 'src/empty-module.js'); + +const config = { + resolver: { + extraNodeModules: { + stream: require.resolve('readable-stream'), + crypto: emptyModule, + http: emptyModule, + https: emptyModule, + net: emptyModule, + tls: emptyModule, + zlib: emptyModule, + os: emptyModule, + dns: emptyModule, + assert: emptyModule, + url: emptyModule, + path: emptyModule, + fs: emptyModule, + }, + }, +}; + +module.exports = mergeConfig(getDefaultConfig(__dirname), config); +``` + +Create the empty module stub: + +```javascript +// src/empty-module.js +module.exports = {}; +``` + +### Step 4: Set up the entry file with correct import order + +The import order is critical. `react-native-get-random-values` **must** be the very first import: + +```typescript +// index.js or App.tsx (entry file) +import 'react-native-get-random-values'; // MUST be first +import './src/polyfills'; // MUST be second +import { AppRegistry } from 'react-native'; +import App from './src/App'; +import { name as appName } from './app.json'; + +AppRegistry.registerComponent(appName, () => App); +``` + +If you import anything from `@metamask/connect-evm` before `react-native-get-random-values`, you will get `crypto.getRandomValues is not a function`. + +### Step 5: Create the EVM client with mobile configuration + +```typescript +// src/metamask.ts +import { createEVMClient, getInfuraRpcUrls } from '@metamask/connect-evm'; +import { Linking } from 'react-native'; +import type { MetamaskConnectEVM } from '@metamask/connect-evm'; + +let clientPromise: Promise | null = null; + +export function getClient(): Promise { + if (!clientPromise) { + clientPromise = createEVMClient({ + dapp: { + name: 'My RN DApp', + url: 'https://mydapp.com', + }, + api: { + supportedNetworks: { + ...getInfuraRpcUrls({ infuraApiKey: 'YOUR_INFURA_KEY', chainIds: ['0x1', '0x89'] }), + }, + }, + ui: { + preferExtension: false, + }, + mobile: { + preferredOpenLink: (deeplink: string) => Linking.openURL(deeplink), + useDeeplink: true, + }, + eventHandlers: { + // Keys are camelCase — `display_uri`/`wallet_sessionChanged` are NOT valid here + displayUri: (uri: string) => { + console.log('Deeplink URI:', uri); + }, + connect: ({ accounts, chainId }) => { + // Fires on connection and on automatic session restore at relaunch + console.log('Connected/restored:', accounts, chainId); + }, + }, + debug: false, + }); + } + return clientPromise; +} +``` + +`mobile.preferredOpenLink` is **required** for React Native — it tells the SDK how to open deeplinks to the MetaMask Mobile app. Without it, the connection flow will hang silently. + +### Step 6: Build the React Native component + +```tsx +// src/WalletScreen.tsx +import React, { useEffect, useRef, useState, useCallback } from 'react'; +import { View, Text, TouchableOpacity, StyleSheet, Alert } from 'react-native'; +import { getClient } from './metamask'; +import type { MetamaskConnectEVM } from '@metamask/connect-evm'; +import type { Hex, Address } from '@metamask/connect-evm'; + +export function WalletScreen() { + const clientRef = useRef(null); + const [accounts, setAccounts] = useState([]); + const [chainId, setChainId] = useState(null); + const [balance, setBalance] = useState(''); + const [connecting, setConnecting] = useState(false); + + useEffect(() => { + let mounted = true; + + async function init() { + const client = await getClient(); + if (!mounted) return; + clientRef.current = client; + + const provider = client.getProvider(); + + provider.on('accountsChanged', (accs: Address[]) => { + if (mounted) setAccounts(accs); + }); + + provider.on('chainChanged', (id: Hex) => { + if (mounted) setChainId(id); + }); + + provider.on('disconnect', () => { + if (mounted) { + setAccounts([]); + setChainId(null); + setBalance(''); + } + }); + } + + init(); + return () => { mounted = false; }; + }, []); + + const handleConnect = useCallback(async () => { + const client = clientRef.current; + if (!client) return; + + setConnecting(true); + try { + const result = await client.connect({ chainIds: ['0x1'] }); + setAccounts(result.accounts as Address[]); + setChainId(result.chainId as Hex); + } catch (err: any) { + if (err.code === 4001) { + Alert.alert('Rejected', 'Connection was rejected. Please try again.'); + return; + } + if (err.code === -32002) { + Alert.alert('Pending', 'A request is already pending. Check MetaMask.'); + return; + } + Alert.alert('Error', err.message ?? 'Connection failed'); + } finally { + setConnecting(false); + } + }, []); + + const handleDisconnect = useCallback(async () => { + const client = clientRef.current; + if (!client) return; + await client.disconnect(); + setAccounts([]); + setChainId(null); + setBalance(''); + }, []); + + const fetchBalance = useCallback(async () => { + const client = clientRef.current; + if (!client || accounts.length === 0) return; + + const provider = client.getProvider(); + const wei = await provider.request({ + method: 'eth_getBalance', + params: [accounts[0], 'latest'], + }) as Hex; + + const ethBalance = parseInt(wei, 16) / 1e18; + setBalance(ethBalance.toFixed(6)); + }, [accounts]); + + const isConnected = accounts.length > 0; + + return ( + + {!isConnected ? ( + + + {connecting ? 'Connecting...' : 'Connect MetaMask'} + + + ) : ( + + Account: {accounts[0]} + Chain: {chainId} + Balance: {balance || '—'} ETH + + Refresh Balance + + + Disconnect + + + )} + + ); +} + +const styles = StyleSheet.create({ + container: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 }, + button: { backgroundColor: '#037DD6', padding: 14, borderRadius: 8, marginVertical: 8 }, + buttonText: { color: '#fff', fontSize: 16, textAlign: 'center' }, + label: { fontSize: 14, marginVertical: 4 }, +}); +``` + +## Important Notes + +- **Import order is critical** — `react-native-get-random-values` must be the very first import in the entry file, followed by `polyfills.ts`, before any SDK or application code. +- **`mobile.preferredOpenLink` is required** — without it, the SDK cannot open deeplinks to MetaMask Mobile and the connection flow will silently fail. +- **`ui.preferExtension` should be `false`** — React Native has no browser extension. Setting this to `false` (or omitting it) ensures the SDK uses the mobile deeplink/QR flow. +- **Chain IDs are always hex strings** — use `'0x1'`, `'0x89'`, `'0xaa36a7'`. Never decimal. +- **`0x1` is auto-included** in every `connect()` call. +- **The empty module stub** (`src/empty-module.js`) is used for Node built-ins the SDK's transitive dependencies reference but never actually call at runtime in React Native. The `stream` module is the exception — it needs a real shim (`readable-stream`). +- **`createEVMClient` is a singleton** — do not call it on every render or in a component body. Initialize once and store the promise. +- **Session restoration** — the EVM client syncs any persisted session before `createEVMClient` resolves; detect restores via the `connect` / `accountsChanged` events (in `eventHandlers` or on the provider). There is no `wallet_sessionChanged` handler on the EVM client — that event belongs to the multichain client. +- **iOS requires `Linking` permissions** — ensure your `Info.plist` includes the `metamask` URL scheme in `LSApplicationQueriesSchemes` so `Linking.openURL` can open the MetaMask app. diff --git a/domains/metamask-connect/skills/setup-multichain-app/skill.md b/domains/metamask-connect/skills/setup-multichain-app/skill.md new file mode 100644 index 0000000..6f4df71 --- /dev/null +++ b/domains/metamask-connect/skills/setup-multichain-app/skill.md @@ -0,0 +1,288 @@ +--- +name: setup-multichain-app +description: Set up a multichain app using createMultichainClient from @metamask/connect-multichain. Covers EVM + Solana scopes, invokeMethod for both ecosystems, session events, headless mode, getInfuraRpcUrls, selective disconnect, and singleton behavior. +maturity: stable +--- +# Setup Multichain App with MetaMask + +## When to use + +Use this skill when: +- Building an app that needs both EVM and Solana wallet connectivity through a single MetaMask session +- Using `createMultichainClient` from `@metamask/connect-multichain` +- Calling `invokeMethod` with CAIP-2 scopes for cross-chain RPC or signing +- Handling `wallet_sessionChanged` events for multichain session state +- Running in headless mode with custom QR rendering via `display_uri` +- Configuring `getInfuraRpcUrls` for EVM and Solana RPC transport + +## Workflow + +### Step 1: Install dependencies + +```bash +npm install @metamask/connect-multichain +``` + +For Solana transaction building (optional): + +```bash +npm install @solana/web3.js +``` + +### Step 2: Create the multichain client + +`createMultichainClient` is a **singleton** — calling it multiple times returns the same instance with merged options. Never recreate it per render. + +```typescript +import { createMultichainClient, getInfuraRpcUrls } from '@metamask/connect-multichain'; + +const client = await createMultichainClient({ + dapp: { + name: 'My Multichain DApp', + url: window.location.href, + iconUrl: 'https://mydapp.com/icon.png', // optional (or use base64Icon for embedded icons) + }, + api: { + supportedNetworks: { + ...getInfuraRpcUrls({ infuraApiKey: 'YOUR_INFURA_API_KEY', caipChainIds: ['eip155:1', 'eip155:137', 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'] }), + }, + }, + ui: { + headless: false, // set true for custom QR rendering + }, +}); +``` + +**`getInfuraRpcUrls({ infuraApiKey, caipChainIds? })`** returns a CAIP-2 keyed map of Infura RPC URLs for supported networks (EVM chains and Solana). Pass `caipChainIds` to limit the output to specific chains. Merge with any custom network RPCs. + +**Singleton behavior:** The `dapp` object from the first call is used for the lifetime of the client — it is ignored on subsequent calls (not merged). Call `createMultichainClient` once at app startup. + +### Step 3: Connect with mixed EVM + Solana scopes + +Scopes use CAIP-2 format: `'eip155:N'` for EVM chains, `'solana:'` for Solana. + +```typescript +// connect() resolves with no value (Promise) +await client.connect( + [ + 'eip155:1', // Ethereum mainnet + 'eip155:137', // Polygon + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', // Solana mainnet + ], + [], // caipAccountIds (empty for initial connection) +); + +// Session data arrives via the wallet_sessionChanged event (see Step 6), +// or read it on demand: +const session = await client.provider.getSession(); +console.log(session?.sessionScopes); // approved scopes with their accounts +``` + +**Solana CAIP-2 identifiers:** + +| Network | CAIP-2 ID | +|----------|-----------| +| Mainnet | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | +| Devnet | `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` | +| Testnet | `solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z` | + +All three Solana scopes are modeled by the SDK; non-mainnet availability depends on the connected MetaMask build/version, so don't assume a cluster is present — handle connection errors. + +You can optionally pass `caipAccountIds` (second argument) to hint at specific accounts: + +```typescript +await client.connect( + ['eip155:1'], + ['eip155:1:0xYourAddress'], +); +``` + +### Step 4: Invoke EVM methods + +Use `invokeMethod` with a CAIP-2 scope and a JSON-RPC request object. + +**EVM read methods** (eth_call, eth_getBalance, eth_blockNumber, etc.) route through the **RPC node**. **Signing methods** (eth_sendTransaction, personal_sign, etc.) route through the **wallet**. + +```typescript +// Read: eth_getBalance via RPC node +const balance = await client.invokeMethod({ + scope: 'eip155:1', + request: { + method: 'eth_getBalance', + params: ['0xYourAddress', 'latest'], + }, +}); + +// Sign: personal_sign via wallet +const signature = await client.invokeMethod({ + scope: 'eip155:1', + request: { + method: 'personal_sign', + params: ['0x48656c6c6f', '0xYourAddress'], + }, +}); + +// Send transaction via wallet +const txHash = await client.invokeMethod({ + scope: 'eip155:137', + request: { + method: 'eth_sendTransaction', + params: [{ + from: '0xYourAddress', + to: '0xRecipient', + value: '0x2386F26FC10000', // 0.01 ETH in wei (hex) + gas: '0x5208', + }], + }, +}); +``` + +### Step 5: Invoke Solana methods + +**All Solana methods route through the wallet** — only EVM read methods are routed to RPC nodes. (The Solana entries in `supportedNetworks` declare which networks the dapp uses; they are not used to route Solana requests.) + +Method names have **no `solana_` prefix**, and params take an `account: { address }` object: + +```typescript +const SOLANA_MAINNET = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'; + +// Sign a message +const signResult = await client.invokeMethod({ + scope: SOLANA_MAINNET, + request: { + method: 'signMessage', + params: { + account: { address: 'YourSolanaAddress' }, + message: btoa('Hello from Solana!'), // base64-encoded message bytes + }, + }, +}); +// signResult: { signature: , signedMessage: , signatureType: 'ed25519' } + +// Sign and send a transaction +const txResult = await client.invokeMethod({ + scope: SOLANA_MAINNET, + request: { + method: 'signAndSendTransaction', + params: { + account: { address: 'YourSolanaAddress' }, + transaction: base64EncodedTransaction, // base64-encoded serialized transaction + scope: SOLANA_MAINNET, + }, + }, +}); +// txResult: { signature: } +``` + +To sign without sending, use `signTransaction` (same params) — it returns `{ signedTransaction: }`. + +### Step 6: Listen for session events + +Register event listeners **before** calling `connect()`. + +```typescript +// Session state changes (accounts added/removed, scopes changed) +// Payload is SessionData | undefined — scopes live under sessionScopes +client.on('wallet_sessionChanged', (session) => { + const scopes = session?.sessionScopes ?? {}; + console.log('Approved scopes:', Object.keys(scopes)); + // Accounts are CAIP-10 strings, e.g. 'eip155:1:0xabc...' — take the last segment for the address +}); + +// Connection status changes +client.on('stateChanged', (status) => { + // status: 'loaded' | 'pending' | 'connecting' | 'connected' | 'disconnected' + console.log('Connection status:', status); +}); + +// display_uri fires during 'connecting' state — headless QR code flow +client.on('display_uri', (uri: string) => { + renderQrCode(uri); +}); +``` + +**`display_uri` timing:** The event only fires during the connecting phase. Register the listener before `connect()`. In headless mode, if an error occurs during connection, do not attempt to regenerate the QR — start a new `connect()` call instead. + +### Step 7: Headless mode + +For full control over the connection UI: + +```typescript +const client = await createMultichainClient({ + dapp: { name: 'My DApp', url: window.location.href }, + api: { + supportedNetworks: getInfuraRpcUrls({ infuraApiKey: 'YOUR_INFURA_API_KEY' }), + }, + ui: { headless: true }, +}); + +client.on('display_uri', (uri: string) => { + // Render your own QR code or deeplink UI + showCustomQrModal(uri); +}); + +await client.connect( + ['eip155:1', 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'], + [], +); + +// Hide QR modal on successful connection +hideCustomQrModal(); +``` + +### Step 8: Selective disconnect + +```typescript +// Disconnect only Solana scope — EVM session stays active +await client.disconnect(['solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp']); + +// Full disconnect — revoke all scopes, terminate session +await client.disconnect(); +``` + +### Step 9: Error handling + +```typescript +import { RPCInvokeMethodErr } from '@metamask/connect-multichain'; + +try { + await client.connect(['eip155:1'], []); +} catch (err: any) { + if (err?.message?.includes('Existing connection is pending')) { + // MWP: a previous connect() is still pending — do NOT retry. + // Show "Check your MetaMask Mobile app to continue" message. + // (This error has no numeric code.) + } else if (err?.code === 4001 || /reject|denied|cancel/i.test(err?.message ?? '')) { + // User rejected the connection — show retry UI + } else { + console.error('Connection error:', err); + } +} + +// invokeMethod errors are wrapped in RPCInvokeMethodErr (err.code === 53). +// The wallet's original EIP-1193 / JSON-RPC code is on err.rpcCode +// (with err.rpcMessage and err.rpcData for revert data). +try { + await client.invokeMethod({ + scope: 'eip155:1', + request: { method: 'personal_sign', params: ['0x48656c6c6f', '0xYourAddress'] }, + }); +} catch (err) { + if (err instanceof RPCInvokeMethodErr && err.rpcCode === 4001) { + // User rejected the signature — not an app error + } else { + throw err; + } +} +``` + +## Important Notes + +- **Singleton:** `createMultichainClient` is a singleton. The `dapp` object from the first call is used for the client's lifetime (later calls' `dapp` is ignored). Call it once at app startup and reuse the returned client. +- **Concurrent connect throws on MWP:** Never call `connect()` while a previous `connect()` is still pending — it throws a plain `Error` ("Existing connection is pending...") with **no numeric code**. (`-32002` only comes from the extension transport's own RPC queue.) +- **EVM read vs sign routing:** EVM read methods (eth_call, eth_getBalance, etc.) go to the RPC node configured in `supportedNetworks`. Signing methods go to the wallet. All Solana methods always go to the wallet. +- **Scope format:** EVM scopes are `'eip155:'` (e.g., `'eip155:1'`). Solana scopes use the genesis hash: `'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'`. +- **`display_uri`:** Only fires during the connecting phase. Register before `connect()`. Do not regenerate QR on connection error — start a fresh `connect()`. +- **Selective disconnect:** Passing specific scopes only revokes those scopes. Omit arguments to fully terminate the session. +- **Node.js / React Native:** `dapp.url` is **required** in non-browser environments (there is no `window.location`). +- **Solana networks:** mainnet, devnet, and testnet scopes are all modeled by the SDK; non-mainnet availability depends on the connected MetaMask build/version, so handle connection errors rather than assuming a cluster is present. diff --git a/domains/metamask-connect/skills/setup-solana-browser-app/skill.md b/domains/metamask-connect/skills/setup-solana-browser-app/skill.md new file mode 100644 index 0000000..0fc1a6c --- /dev/null +++ b/domains/metamask-connect/skills/setup-solana-browser-app/skill.md @@ -0,0 +1,263 @@ +--- +name: setup-solana-browser-app +description: Set up a vanilla browser (non-React) app with @metamask/connect-solana using wallet-standard features directly. Use when integrating MetaMask Solana without a framework or wallet adapter library. +maturity: stable +--- +# Setup Solana Browser App with MetaMask + +## When to use + +Use this skill when: +- Integrating MetaMask with Solana in a vanilla JavaScript or non-React browser app +- Using wallet-standard features directly without `@solana/wallet-adapter-react` +- Building connect, sign, and send flows with the `SolanaClient` API +- Accessing wallet-standard features like `solana:signTransaction` or `solana:signMessage` directly + +## Workflow + +### Step 1: Install dependencies + +```bash +npm install @metamask/connect-solana @metamask/connect-multichain @solana/web3.js +``` + +`@metamask/connect-multichain` is a regular dependency of `@metamask/connect-solana` and is installed transitively. (Only the 2.0.0 release briefly made it a peer dependency; 2.1.0 reverted that.) Installing it explicitly is harmless but not required. The SDK warns at runtime if duplicate or mismatched copies are resolved. + +### Step 2: Create the Solana client + +```typescript +import { createSolanaClient } from '@metamask/connect-solana'; + +const solanaClient = await createSolanaClient({ + dapp: { + name: 'My Solana DApp', + url: window.location.href, + }, + api: { + supportedNetworks: { + mainnet: 'https://api.mainnet-beta.solana.com', + }, + }, +}); +``` + +**`createSolanaClient` returns `Promise`:** + +| Property | Type | Description | +|----------|------|-------------| +| `core` | `MultichainCore` | The underlying multichain client instance | +| `getWallet()` | `() => Wallet` | Returns the wallet-standard `Wallet` (from `@wallet-standard/base`) | +| `registerWallet()` | `() => Promise` | Manually register the wallet (auto-called unless `skipAutoRegister: true`) | +| `disconnect()` | `() => Promise` | Disconnect and revoke Solana scopes | + +### Step 3: Get the wallet and connect + +```typescript +const wallet = solanaClient.getWallet(); + +// The wallet exposes wallet-standard features +console.log('Wallet name:', wallet.name); // "MetaMask" +console.log('Available features:', Object.keys(wallet.features)); +``` + +Connect using the `standard:connect` feature: + +```typescript +const connectFeature = wallet.features['standard:connect']; +const { accounts } = await connectFeature.connect(); + +if (accounts.length > 0) { + const account = accounts[0]; + console.log('Address:', account.address); + console.log('Public key:', account.publicKey); // Uint8Array + console.log('Chains:', account.chains); // e.g. ['solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'] +} +``` + +### Step 4: Access wallet-standard features + +The wallet exposes these wallet-standard features: + +| Feature Key | Description | +|-------------|-------------| +| `standard:connect` | Connect and request accounts | +| `standard:disconnect` | Disconnect the wallet | +| `standard:events` | Subscribe to account/chain change events | +| `solana:signIn` | Sign-In-With-Solana (SIWS) authentication | +| `solana:signTransaction` | Sign a transaction without sending | +| `solana:signAndSendTransaction` | Sign and broadcast a transaction | +| `solana:signMessage` | Sign an arbitrary message | + +There is **no** `solana:signAndSendAllTransactions` feature — to batch, pass multiple inputs to `signAndSendTransaction(...inputs)` (it is variadic and returns one result per input). + +### Step 5: Sign a message + +```typescript +const signMessageFeature = wallet.features['solana:signMessage']; + +const message = new TextEncoder().encode('Hello from MetaMask on Solana!'); + +const [{ signature }] = await signMessageFeature.signMessage({ + account: accounts[0], + message, +}); + +console.log('Signature:', Buffer.from(signature).toString('hex')); +``` + +### Step 6: Sign and send a transaction + +```typescript +import { + Connection, + Transaction, + SystemProgram, + PublicKey, + LAMPORTS_PER_SOL, +} from '@solana/web3.js'; + +const connection = new Connection('https://api.mainnet-beta.solana.com'); +const { blockhash } = await connection.getLatestBlockhash(); + +const senderPubkey = new PublicKey(accounts[0].address); + +const transaction = new Transaction().add( + SystemProgram.transfer({ + fromPubkey: senderPubkey, + toPubkey: new PublicKey('11111111111111111111111111111112'), + lamports: 0.001 * LAMPORTS_PER_SOL, + }), +); +transaction.recentBlockhash = blockhash; +transaction.feePayer = senderPubkey; + +const serializedTransaction = transaction.serialize({ + requireAllSignatures: false, +}); + +const signAndSendFeature = wallet.features['solana:signAndSendTransaction']; + +const [{ signature: txSignature }] = await signAndSendFeature.signAndSendTransaction({ + account: accounts[0], + transaction: serializedTransaction, + chain: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', +}); + +// txSignature is a Uint8Array — encode with bs58 ('base58' is NOT a Buffer encoding). +// Requires `npm install bs58` and `import bs58 from 'bs58'` at the top of the file. +const signatureBase58 = bs58.encode(txSignature); +console.log('Transaction signature:', signatureBase58); + +// confirmTransaction expects the base58 signature string +const confirmation = await connection.confirmTransaction(signatureBase58, 'confirmed'); +console.log('Confirmed:', confirmation); +``` + +### Step 7: Listen for account and chain changes + +```typescript +const eventsFeature = wallet.features['standard:events']; + +eventsFeature.on('change', ({ accounts: newAccounts }) => { + if (newAccounts) { + console.log('Accounts changed:', newAccounts.map((a) => a.address)); + } +}); +``` + +### Step 8: Disconnect + +```typescript +await solanaClient.disconnect(); +``` + +### Step 9: Full working example + +```typescript +import { createSolanaClient } from '@metamask/connect-solana'; +import { + Connection, + Transaction, + SystemProgram, + PublicKey, + LAMPORTS_PER_SOL, +} from '@solana/web3.js'; + +async function main() { + const solanaClient = await createSolanaClient({ + dapp: { + name: 'My Solana DApp', + url: window.location.href, + }, + api: { + supportedNetworks: { + mainnet: 'https://api.mainnet-beta.solana.com', + }, + }, + }); + + const wallet = solanaClient.getWallet(); + + // Connect + const connectFeature = wallet.features['standard:connect']; + const { accounts } = await connectFeature.connect(); + const account = accounts[0]; + console.log('Connected:', account.address); + + // Sign message + const signMessageFeature = wallet.features['solana:signMessage']; + const [{ signature }] = await signMessageFeature.signMessage({ + account, + message: new TextEncoder().encode('Hello Solana!'), + }); + console.log('Message signed'); + + // Send transaction + const connection = new Connection('https://api.mainnet-beta.solana.com'); + const { blockhash } = await connection.getLatestBlockhash(); + const senderPubkey = new PublicKey(account.address); + + const tx = new Transaction().add( + SystemProgram.transfer({ + fromPubkey: senderPubkey, + toPubkey: new PublicKey('11111111111111111111111111111112'), + lamports: 0.001 * LAMPORTS_PER_SOL, + }), + ); + tx.recentBlockhash = blockhash; + tx.feePayer = senderPubkey; + + const signAndSendFeature = wallet.features['solana:signAndSendTransaction']; + const [{ signature: txSig }] = await signAndSendFeature.signAndSendTransaction({ + account, + transaction: tx.serialize({ requireAllSignatures: false }), + chain: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + }); + console.log('Transaction sent'); + + // Disconnect + await solanaClient.disconnect(); +} + +main().catch(console.error); +``` + +## Important Notes + +- **`createSolanaClient` is async** — always `await` it before accessing the wallet. The factory returns a `Promise`. +- **`getInfuraRpcUrls` helper** — use `getInfuraRpcUrls({ infuraApiKey: 'YOUR_KEY', networks: ['mainnet', 'devnet'] })` from `@metamask/connect-solana` to auto-generate `supportedNetworks` from Infura. Returns `SolanaSupportedNetworks`. +- **Wallet name is exactly `"MetaMask"`** — case-sensitive. Use this to identify the wallet if you enumerate registered wallets. +- **Feature keys are string constants** — always access features via bracket notation (e.g., `wallet.features['solana:signTransaction']`), not dot notation. +- **Solana networks** — mainnet, devnet, and testnet scopes are all modeled by the SDK and wallet-standard layer. Non-mainnet availability depends on the connected MetaMask build/version, so handle connection errors rather than assuming a cluster is present; for reads, point a `@solana/web3.js` `Connection` at the matching cluster. +- **`skipAutoRegister` option** — pass `skipAutoRegister: true` to `createSolanaClient` to prevent automatic wallet-standard registration. Useful when you want to control when the wallet becomes discoverable. +- **`analytics.integrationType` option** — pass an `analytics: { integrationType: 'your-integration' }` string to `createSolanaClient` (added in `@metamask/connect-solana` 0.8.0) to tag analytics events with your integration identifier. +- **Injected Solana provider wins** — since `@metamask/connect-solana` 1.0.0, if an injected Solana provider is already present (e.g. the MetaMask browser extension), `createSolanaClient` will not announce its own wallet-standard provider. Don't expect two `"MetaMask"` entries in the registered wallets list. +- **Eager provider initialization + stable `getWallet()`** — since `@metamask/connect-solana` 1.1.0, `createSolanaClient()` eagerly initializes the Solana wallet provider during creation; if the underlying multichain session already contains Solana scopes, the provider's accounts are populated by the time the client resolves (no need to wait for `wallet_sessionChanged` on cold start). `getWallet()` also returns the same wallet instance on every call now — safe to cache in a module-level constant, no need to re-await or recreate on subsequent access. +- **`disconnect()` only revokes Solana scopes** — EVM sessions managed by other clients remain active. +- **Chrome on Android** has a known bug where the page may unload during the connection flow. Add a `beforeunload` listener as a workaround: + ```typescript + window.addEventListener('beforeunload', (e) => { + e.preventDefault(); + e.returnValue = ''; + }); + ``` diff --git a/domains/metamask-connect/skills/setup-solana-react-app/skill.md b/domains/metamask-connect/skills/setup-solana-react-app/skill.md new file mode 100644 index 0000000..530729e --- /dev/null +++ b/domains/metamask-connect/skills/setup-solana-react-app/skill.md @@ -0,0 +1,195 @@ +--- +name: setup-solana-react-app +description: Set up a React app with @metamask/connect-solana and the Solana wallet adapter. Use when integrating MetaMask with Solana in React, configuring WalletProvider, or building connect/sign/send flows with useWallet. +maturity: stable +--- +# Setup Solana React App with MetaMask + +## When to use + +Use this skill when: +- Integrating MetaMask with Solana in a React web application +- Configuring `@solana/wallet-adapter-react` with MetaMask Connect's wallet-standard discovery +- Building connect, sign message, or send transaction flows using `useWallet` +- The MetaMask wallet is not appearing in the Solana wallet adapter + +## Workflow + +### Step 1: Install dependencies + +```bash +npm install @metamask/connect-solana @metamask/connect-multichain @solana/wallet-adapter-react @solana/wallet-adapter-react-ui @solana/wallet-adapter-base @solana/web3.js +``` + +`@metamask/connect-multichain` is a regular dependency of `@metamask/connect-solana` and is installed transitively. (Only the 2.0.0 release briefly made it a peer dependency; 2.1.0 reverted that.) Installing it explicitly is harmless but not required. The SDK warns at runtime if duplicate or mismatched copies are resolved. + +### Step 2: Create the Solana client early in app startup + +Initialize `createSolanaClient` early (e.g. in your bootstrap before rendering). It does **not** need to resolve before the first `WalletProvider` render — wallet-standard supports late registration, and the SDK registers the wallet about 1 second after the factory resolves. The adapter picks it up via the wallet-standard register event whenever it lands. If your UI asserts "MetaMask is available" synchronously, gate that specific UI on the client being ready. + +```typescript +// src/main.tsx (or index.tsx) +import { createSolanaClient } from '@metamask/connect-solana'; +import { createRoot } from 'react-dom/client'; +import App from './App'; + +async function bootstrap() { + await createSolanaClient({ + dapp: { + name: 'My Solana DApp', + url: window.location.href, + }, + }); + + const root = createRoot(document.getElementById('root')!); + root.render(); +} + +bootstrap(); +``` + +**`createSolanaClient` options:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `dapp.name` | `string` | **Required.** Display name of your dApp | +| `dapp.url` | `string` | Optional. dApp URL | +| `dapp.iconUrl` | `string` | Optional. dApp icon | +| `api.supportedNetworks` | `Partial>` | Map network names to RPC URLs | +| `debug` | `boolean` | Reserved — accepted in the options type but **not currently forwarded** by `createSolanaClient` (no effect yet) | +| `skipAutoRegister` | `boolean` | Skip automatic wallet-standard registration | +| `analytics.integrationType` | `string` | Optional. Tag analytics events with an integration identifier (added in `@metamask/connect-solana` 0.8.0) | + +**Solana CAIP chain IDs:** + +| Network | CAIP ID | +|---------|---------| +| Mainnet | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | +| Devnet | `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` | +| Testnet | `solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z` | + +### Step 3: Configure the WalletProvider + +Pass an **empty** `wallets` array. MetaMask registers via the wallet-standard auto-discovery protocol and must not be added manually. + +```tsx +// src/App.tsx +import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react'; +import { WalletModalProvider } from '@solana/wallet-adapter-react-ui'; +import { clusterApiUrl } from '@solana/web3.js'; +import '@solana/wallet-adapter-react-ui/styles.css'; +import { SolanaDemo } from './SolanaDemo'; + +const endpoint = clusterApiUrl('mainnet-beta'); + +export default function App() { + return ( + + + + + + + + ); +} +``` + +### Step 4: Find the MetaMask wallet + +When using `useWallet`, you can verify MetaMask is connected by checking the wallet name. The wallet-standard name is exactly `"MetaMask"` (case-sensitive). + +```typescript +import { useWallet } from '@solana/wallet-adapter-react'; + +const { wallet } = useWallet(); +const isMetaMask = wallet?.adapter.name === 'MetaMask'; +``` + +### Step 5: Build the full component + +```tsx +// src/SolanaDemo.tsx +import { useWallet, useConnection } from '@solana/wallet-adapter-react'; +import { WalletMultiButton } from '@solana/wallet-adapter-react-ui'; +import { + Transaction, + SystemProgram, + PublicKey, + LAMPORTS_PER_SOL, +} from '@solana/web3.js'; + +export function SolanaDemo() { + const { publicKey, sendTransaction, signMessage, connected, wallet } = useWallet(); + const { connection } = useConnection(); + + const handleSignMessage = async () => { + if (!signMessage) return; + const message = new TextEncoder().encode('Hello from MetaMask on Solana!'); + try { + const signature = await signMessage(message); + console.log('Signature:', Buffer.from(signature).toString('hex')); + } catch (err) { + console.error('Sign message failed:', err); + } + }; + + const handleSendTransaction = async () => { + if (!publicKey || !sendTransaction) return; + try { + const { blockhash } = await connection.getLatestBlockhash(); + const transaction = new Transaction().add( + SystemProgram.transfer({ + fromPubkey: publicKey, + toPubkey: new PublicKey('11111111111111111111111111111112'), + lamports: 0.001 * LAMPORTS_PER_SOL, + }), + ); + transaction.recentBlockhash = blockhash; + transaction.feePayer = publicKey; + + const txSignature = await sendTransaction(transaction, connection); + const confirmation = await connection.confirmTransaction(txSignature, 'confirmed'); + console.log('Transaction confirmed:', txSignature); + } catch (err) { + console.error('Transaction failed:', err); + } + }; + + return ( +
+ + {connected && publicKey && ( +
+

Wallet: {wallet?.adapter.name}

+

Address: {publicKey.toBase58()}

+ + +
+ )} +
+ ); +} +``` + +### Step 6: Chrome on Android workaround + +Chrome on Android has a known bug where the page may unload before MetaMask can respond. Add a `beforeunload` patch in your entry file: + +```typescript +window.addEventListener('beforeunload', (e) => { + e.preventDefault(); + e.returnValue = ''; +}); +``` + +## Important Notes + +- **Initialize `createSolanaClient` early, but rendering need not wait** — wallet registration happens shortly *after* the factory resolves (the SDK defers it ~1s), and the wallet adapter discovers late registrations via the wallet-standard register event. Only gate UI that assumes MetaMask is already in the wallet list. +- **`wallets` prop must be `[]`** — MetaMask uses wallet-standard auto-discovery. Passing wallet adapter instances manually will not work and may cause duplicates with other wallets. +- **The wallet name is exactly `"MetaMask"`** — case-sensitive. Use this to identify the MetaMask wallet in the adapter list. Renamed from `"MetaMask Connect"` in `@metamask/connect-solana` 1.0.0; since that release, the client will also defer to an already-injected Solana provider (e.g. the MetaMask browser extension) instead of announcing a second `"MetaMask"` entry. +- **Eager provider initialization + stable `getWallet()`** — since `@metamask/connect-solana` 1.1.0, `createSolanaClient()` eagerly initializes the Solana wallet provider during creation; if the underlying multichain session already contains Solana scopes, the provider's accounts are populated by the time the client resolves (no need to wait for `wallet_sessionChanged` on cold start). `getWallet()` also returns the same wallet instance on every call now — safe to cache in a `useRef` / `useMemo` value, no need to call it on every render. +- **`getInfuraRpcUrls` helper** — use `getInfuraRpcUrls({ infuraApiKey: 'YOUR_KEY', networks: ['mainnet', 'devnet'] })` from `@metamask/connect-solana` to auto-generate `supportedNetworks` from Infura. +- **Solana networks** — mainnet, devnet, and testnet scopes are all modeled by the SDK and wallet-standard layer. Non-mainnet availability depends on the connected MetaMask build/version, so handle connection errors rather than assuming a cluster is present. +- **`disconnect()` only revokes Solana scopes** — if the user also has EVM sessions, those remain active. Each chain family manages its own session lifecycle. +- **Chrome on Android** — a known browser bug can interrupt the connection flow. Apply the `beforeunload` workaround shown in Step 6. diff --git a/domains/metamask-connect/skills/setup-solana-react-native-app/skill.md b/domains/metamask-connect/skills/setup-solana-react-native-app/skill.md new file mode 100644 index 0000000..a7bde3c --- /dev/null +++ b/domains/metamask-connect/skills/setup-solana-react-native-app/skill.md @@ -0,0 +1,382 @@ +--- +name: setup-solana-react-native-app +description: Set up a React Native app with @metamask/connect-solana using polyfills and multichain invokeMethod for Solana operations. Use when building Solana support in React Native where wallet-adapter is not available. +maturity: stable +--- +# Setup Solana React Native App with MetaMask + +## When to use + +Use this skill when: +- Integrating MetaMask with Solana in a React Native application +- Setting up the required polyfills and metro shims for `@metamask/connect-solana` +- Building Solana sign and send flows in React Native using `invokeMethod` +- You need Solana support in React Native where `@solana/wallet-adapter-react` is **not** available + +## Workflow + +### Step 1: Install dependencies + +```bash +npm install @metamask/connect-solana @metamask/connect-multichain @solana/web3.js react-native-get-random-values buffer readable-stream @react-native-async-storage/async-storage +``` + +`@metamask/connect-multichain` is a regular dependency of `@metamask/connect-solana` and is installed transitively — but this skill imports `createMultichainClient` directly (to configure `mobile.preferredOpenLink`, which `createSolanaClient` does not forward), so declare it explicitly to keep strict package managers (pnpm) happy. The SDK warns at runtime if duplicate or mismatched copies are resolved. + +### Step 2: Create the polyfills file + +Create `polyfills.ts` at the root of your project. + +```typescript +// polyfills.ts +import { Buffer } from 'buffer'; + +// Buffer — connect-multichain self-polyfills this, but set early as a safety +// net for peer deps (e.g. @solana/web3.js) that may load first. +global.Buffer = Buffer; + +// window object — required for correct platform detection and deeplink behaviour. +const eventListeners = new Map>(); +if (typeof global.window === 'undefined') { + (global as any).window = { + location: { + hostname: 'my-rn-app', + href: 'https://my-rn-app.local', + }, + navigator: { product: 'ReactNative' }, + addEventListener: (event: string, listener: EventListener) => { + if (!eventListeners.has(event)) eventListeners.set(event, new Set()); + eventListeners.get(event)?.add(listener); + }, + removeEventListener: (event: string, listener: EventListener) => { + eventListeners.get(event)?.delete(listener); + }, + dispatchEvent: (_event: Event) => true, + }; +} + +// NOTE: Event and CustomEvent are NOT needed for standalone connect-solana — +// the SDK uses eventemitter3 internally. Add them only if using wagmi. +``` + +### Step 3: Import polyfills FIRST in the entry file + +`react-native-get-random-values` must be the **very first import** in the entry file — it cannot be inside `polyfills.ts` because Metro may have already touched crypto by the time that file runs. + +```typescript +// index.js or App.tsx — import order is critical +import 'react-native-get-random-values'; // MUST be first (needed for RN < 0.72; safe to include on 0.72+) +import './polyfills'; + +import { AppRegistry } from 'react-native'; +import App from './App'; +import { name as appName } from './app.json'; + +AppRegistry.registerComponent(appName, () => App); +``` + +### Step 4: Configure metro shims + +Add `extraNodeModules` to `metro.config.js` so the bundler can resolve Node.js built-in modules: + +```javascript +const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); +const path = require('path'); + +// src/empty-module.js: `module.exports = {};` +// Only `stream` needs a real shim — the other Node built-ins are referenced +// by transitive deps but never called at runtime in React Native. +const emptyModule = path.resolve(__dirname, 'src', 'empty-module.js'); + +const config = { + resolver: { + extraNodeModules: { + stream: require.resolve('readable-stream'), + crypto: emptyModule, + http: emptyModule, + https: emptyModule, + net: emptyModule, + tls: emptyModule, + zlib: emptyModule, + os: emptyModule, + dns: emptyModule, + assert: emptyModule, + url: emptyModule, + path: emptyModule, + fs: emptyModule, + }, + }, +}; + +module.exports = mergeConfig(getDefaultConfig(__dirname), config); +``` + +### Step 5: Create the Solana client + +`createSolanaClient` does not forward `mobile` options to the underlying multichain core. In React Native, you must call `createMultichainClient` first with `mobile.preferredOpenLink` so the singleton core is configured for deeplinks, then call `createSolanaClient` which reuses that same core. + +```typescript +import { createMultichainClient } from '@metamask/connect-multichain'; +import { createSolanaClient } from '@metamask/connect-solana'; +import { Linking } from 'react-native'; + +// Initialize the multichain singleton with mobile deeplink handling +await createMultichainClient({ + dapp: { + name: 'My Solana RN App', + url: 'https://myapp.com', + }, + api: { + supportedNetworks: {}, + }, + mobile: { + preferredOpenLink: (deeplink: string) => Linking.openURL(deeplink), + }, +}); + +// Create the Solana client — reuses the multichain singleton above +const solanaClient = await createSolanaClient({ + dapp: { + name: 'My Solana RN App', + url: 'https://myapp.com', + }, + api: { + supportedNetworks: { + mainnet: 'https://api.mainnet-beta.solana.com', + }, + }, +}); +``` + +### Step 6: Use multichain invokeMethod for Solana operations + +**There is no `@solana/wallet-adapter-react` in React Native.** Instead, use the `core` multichain client and `invokeMethod` to call Solana RPC methods on specific CAIP scopes. + +#### Connect + +```typescript +await solanaClient.core.connect( + ['solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'], + [], +); +``` + +Listen for `wallet_sessionChanged` to get accounts after connection: + +```typescript +solanaClient.core.on('wallet_sessionChanged', (session) => { + // Scopes live under session.sessionScopes; accounts are CAIP-10 strings + // ('solana::
') — take the last segment for the address + const caipAccounts = + session?.sessionScopes?.['solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp']?.accounts ?? []; + const solanaAccounts = caipAccounts.map((a) => a.split(':')[2]); + console.log('Solana accounts:', solanaAccounts); +}); +``` + +#### Sign a message + +```typescript +const message = new TextEncoder().encode('Hello from React Native!'); +const messageBase64 = Buffer.from(message).toString('base64'); + +// Method names have no `solana_` prefix; the account is passed as account: { address } +const result = await solanaClient.core.invokeMethod({ + scope: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + request: { + method: 'signMessage', + params: { + account: { address: solanaAccounts[0] }, + message: messageBase64, + }, + }, +}); + +// result: { signature: , signedMessage: , signatureType: 'ed25519' } +console.log('Signature:', result.signature); +``` + +#### Sign and send a transaction + +```typescript +import { + Connection, + Transaction, + SystemProgram, + PublicKey, + LAMPORTS_PER_SOL, +} from '@solana/web3.js'; + +const connection = new Connection('https://api.mainnet-beta.solana.com'); +const { blockhash } = await connection.getLatestBlockhash(); +const senderPubkey = new PublicKey(solanaAccounts[0]); + +const tx = new Transaction().add( + SystemProgram.transfer({ + fromPubkey: senderPubkey, + toPubkey: new PublicKey('11111111111111111111111111111112'), + lamports: 0.001 * LAMPORTS_PER_SOL, + }), +); +tx.recentBlockhash = blockhash; +tx.feePayer = senderPubkey; + +const serializedTx = tx.serialize({ requireAllSignatures: false }); +const txBase64 = Buffer.from(serializedTx).toString('base64'); + +const SOLANA_MAINNET = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'; +const sendResult = await solanaClient.core.invokeMethod({ + scope: SOLANA_MAINNET, + request: { + method: 'signAndSendTransaction', + params: { + account: { address: solanaAccounts[0] }, + transaction: txBase64, + scope: SOLANA_MAINNET, + }, + }, +}); + +// sendResult: { signature: } +console.log('Transaction signature:', sendResult.signature); +``` + +#### Disconnect + +```typescript +await solanaClient.disconnect(); +``` + +### Step 7: Full React Native component + +```tsx +import React, { useState, useEffect } from 'react'; +import { View, Text, Button, Alert, Linking } from 'react-native'; +import { createMultichainClient } from '@metamask/connect-multichain'; +import { createSolanaClient, SolanaClient } from '@metamask/connect-solana'; +import { + Connection, + Transaction, + SystemProgram, + PublicKey, + LAMPORTS_PER_SOL, +} from '@solana/web3.js'; + +const MAINNET_SCOPE = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'; +const RPC_URL = 'https://api.mainnet-beta.solana.com'; + +export default function SolanaScreen() { + const [client, setClient] = useState(null); + const [accounts, setAccounts] = useState([]); + + useEffect(() => { + (async () => { + await createMultichainClient({ + dapp: { name: 'My RN App', url: 'https://myapp.com' }, + api: { supportedNetworks: {} }, + mobile: { preferredOpenLink: (dl) => Linking.openURL(dl) }, + }); + const c = await createSolanaClient({ + dapp: { name: 'My RN App', url: 'https://myapp.com' }, + api: { supportedNetworks: { mainnet: RPC_URL } }, + }); + setClient(c); + c.core.on('wallet_sessionChanged', (session) => { + const caipAccounts = session?.sessionScopes?.[MAINNET_SCOPE]?.accounts ?? []; + setAccounts(caipAccounts.map((a) => a.split(':')[2])); + }); + })(); + }, []); + + const handleConnect = async () => { + if (!client) return; + try { + await client.core.connect([MAINNET_SCOPE], []); + } catch (err: any) { + Alert.alert('Connection failed', err.message); + } + }; + + const handleSignMessage = async () => { + if (!client || accounts.length === 0) return; + try { + const message = Buffer.from('Hello from React Native!').toString('base64'); + const result = await client.core.invokeMethod({ + scope: MAINNET_SCOPE, + request: { method: 'signMessage', params: { account: { address: accounts[0] }, message } }, + }); + Alert.alert('Signed', JSON.stringify(result.signature).slice(0, 40) + '...'); + } catch (err: any) { + Alert.alert('Sign failed', err.message); + } + }; + + const handleSendTransaction = async () => { + if (!client || accounts.length === 0) return; + try { + const connection = new Connection(RPC_URL); + const { blockhash } = await connection.getLatestBlockhash(); + const sender = new PublicKey(accounts[0]); + + const tx = new Transaction().add( + SystemProgram.transfer({ + fromPubkey: sender, + toPubkey: new PublicKey('11111111111111111111111111111112'), + lamports: 0.001 * LAMPORTS_PER_SOL, + }), + ); + tx.recentBlockhash = blockhash; + tx.feePayer = sender; + + const txBase64 = Buffer.from( + tx.serialize({ requireAllSignatures: false }), + ).toString('base64'); + + const result = await client.core.invokeMethod({ + scope: MAINNET_SCOPE, + request: { + method: 'signAndSendTransaction', + params: { account: { address: accounts[0] }, transaction: txBase64, scope: MAINNET_SCOPE }, + }, + }); + Alert.alert('Sent', result.signature); + } catch (err: any) { + Alert.alert('Transaction failed', err.message); + } + }; + + const handleDisconnect = async () => { + if (!client) return; + await client.disconnect(); + setAccounts([]); + }; + + if (accounts.length === 0) { + return ( + + + ); + } + + return ( +
+

Address: {address}

+

Balance: {balance ? formatEther(balance.value) : '—'} ETH

+

Chain: {chainId}

+ + + + +
+ ); +} +``` + +**Wagmi hooks used (v3):** +- `useConnection`: `address`, `isConnected`, `status` (renamed from `useAccount` in v3) +- `useBalance`: balance for connected account +- `useConnect`: `mutateAsync` (renamed from `connectAsync`), `status` +- `useConnectors`: standalone hook for connector list (removed from `useConnect` in v3) +- `useDisconnect`: `disconnect` +- `useSendTransaction`: send ETH +- `useSignMessage`: sign messages +- `useSwitchChain`: `switchChainAsync` +- `useChains`: standalone hook for chain list (removed from `useSwitchChain` in v3) +- `useWaitForTransactionReceipt`: tx confirmation +- `useChainId`: current chain +- `useBlockNumber`: current block (`watch: true`) + +### Step 6: Connect flow + +```typescript +// wagmi v3: connectors come from useConnectors(), and useConnect() exposes +// mutateAsync (connectAsync was the v2 name) +const connectors = useConnectors(); +const { mutateAsync: connect } = useConnect(); +const metaMaskConnector = connectors.find((c) => c.id === 'metaMaskSDK'); +await connect({ connector: metaMaskConnector, chainId: 1 }); +``` + +### Step 7: Error handling + +Handle common errors: + +- `UserRejectedRequestError` (code 4001) +- `ResourceUnavailableRpcError` (code -32002) +- `SwitchChainError`, `ChainNotConfiguredError` + +## Important Notes + +- **Connector ID is `'metaMaskSDK'`** — always find it with `connectors.find((c) => c.id === 'metaMaskSDK')`. +- **Wagmi disconnect is separate from multichain disconnect** — disconnecting one does not disconnect the other. +- **CRA/Expo import restriction**: Cannot import from outside `src/` — the connector may need to be copied locally. +- **`isAuthorized` retries on mobile**: The connector wraps `getAccounts()` in `withTimeout` (10ms per attempt) and `withRetry` (3 attempts, ~11ms delay between) because the MetaMask mobile provider sometimes doesn't resolve JSON-RPC requests immediately on page load. It returns `false` on failure (does NOT throw) — it resolves in tens of milliseconds, not seconds. +- **Chains in `wagmiConfig` must match chains you use** — wagmi validates against configured chains. +- **React Native**: Import polyfills before wagmi config; add `mobile.preferredOpenLink`; use `createAsyncStoragePersister` with AsyncStorage. Polyfill requirements: `react-native-get-random-values` first (required for RN < 0.72), then `window` shim (required by connect-multichain for platform detection), then `Event`/`CustomEvent` shims (**wagmi-specific** — wagmi dispatches DOM events; not needed for standalone connect-* usage). Buffer is self-polyfilled by connect-multichain; keep `global.Buffer = Buffer` as a safety net for peer deps. diff --git a/domains/metamask-connect/skills/setup-wagmi-metamask-connector/skill.md b/domains/metamask-connect/skills/setup-wagmi-metamask-connector/skill.md new file mode 100644 index 0000000..eaf9936 --- /dev/null +++ b/domains/metamask-connect/skills/setup-wagmi-metamask-connector/skill.md @@ -0,0 +1,302 @@ +--- +name: setup-wagmi-metamask-connector +description: Set up a wagmi app with the MetaMask Connect EVM connector using @metamask/connect-evm +maturity: stable +--- +# Set Up Wagmi with MetaMask Connect EVM Connector + +## When to use +- Building a new wagmi-based dApp that needs MetaMask wallet connectivity +- Adding the MetaMask connector to an existing wagmi config +- Need a working wagmi + MetaMask setup with connection, chain switching, signing, and transactions +- Integrating MetaMask via the new `@metamask/connect-evm` SDK in a wagmi project + +## Workflow + +### Step 1: Install Dependencies + +```bash +npm install wagmi viem @tanstack/react-query + +# Check which @metamask/connect-evm range wagmi's connector was built against: +npm info @wagmi/connectors peerDependencies +# ... then install a version inside that range (currently ^1.3.0): +npm install @metamask/connect-evm@"^1.3.0" +``` + +The connect-evm-backed `metaMask()` connector ships in **wagmi >= 3.6 / `@wagmi/connectors` >= 8**, which declares `@metamask/connect-evm` as an **optional peer dependency**. Install a version that satisfies wagmi's declared peer range — do **not** install `@metamask/connect-evm@latest` blindly: the current 2.x line does not satisfy `^1.3.0`, and pairing the connector with a major it wasn't built against produces peer warnings and undefined behavior. `@metamask/connect-multichain` is installed transitively by `connect-evm`; you do not need to add it. + +### Step 2: Create Wagmi Config + +```typescript +import { createConfig, http } from 'wagmi' +import { mainnet, sepolia, optimism, polygon } from 'wagmi/chains' +import { metaMask } from 'wagmi/connectors' + +export const config = createConfig({ + chains: [mainnet, sepolia, optimism, polygon], + connectors: [ + metaMask({ + dapp: { + name: 'My Dapp', + url: window.location.href, + iconUrl: 'https://mydapp.com/icon.png', + }, + debug: false, + }), + ], + transports: { + [mainnet.id]: http(), + [sepolia.id]: http(), + [optimism.id]: http(), + [polygon.id]: http(), + }, +}) + +declare module 'wagmi' { + interface Register { + config: typeof config + } +} +``` + +The connector automatically builds `supportedNetworks` from the configured chains and their default RPC URLs. You do not need to pass RPC URLs manually. + +### Step 3: Set Up Providers in React + +```tsx +import { WagmiProvider } from 'wagmi' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { config } from './wagmi' + +const queryClient = new QueryClient() + +function App() { + return ( + + + + + + ) +} +``` + +### Step 4: Connect Wallet + +```tsx +import { useConnect, useConnectors, useConnection, useDisconnect } from 'wagmi' + +function ConnectButton() { + const { mutate: connect, status, error } = useConnect() + const connectors = useConnectors() + const { address, chainId, status: connectionStatus } = useConnection() + const { disconnect } = useDisconnect() + + if (connectionStatus === 'connected') { + return ( +
+

Connected: {address}

+

Chain: {chainId}

+ +
+ ) + } + + return ( +
+ {connectors.map((connector) => ( + + ))} + {status === 'pending' &&

Connecting...

} + {error &&

Error: {error.message}

} +
+ ) +} +``` + +### Step 5: Switch Chains + +```tsx +import { useSwitchChain, useChains, useChainId } from 'wagmi' + +function ChainSwitcher() { + const chainId = useChainId() + const chains = useChains() + const { switchChain, error } = useSwitchChain() + + return ( +
+ {chains.map((chain) => ( + + ))} + {error &&

{error.message}

} +
+ ) +} +``` + +### Step 6: Sign Messages + +```tsx +import { useSignMessage } from 'wagmi' + +function SignMessage() { + const { data, signMessage, error, isPending } = useSignMessage() + + return ( +
+ + {data &&

Signature: {data}

} + {error &&

Error: {error.message}

} +
+ ) +} +``` + +### Step 7: Send Transactions + +```tsx +import { useSendTransaction, useWaitForTransactionReceipt } from 'wagmi' +import { parseEther } from 'viem' + +function SendTransaction() { + const { data: hash, sendTransaction, isPending, error } = useSendTransaction() + const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash }) + + return ( +
+ + {isConfirming &&

Confirming...

} + {isSuccess &&

Confirmed! Hash: {hash}

} + {error &&

Error: {error.message}

} +
+ ) +} +``` + +### Step 8: Connect and Sign (Optional) + +Use `connectAndSign` to prompt the user to connect and sign a message in a single flow: + +```typescript +metaMask({ + dapp: { name: 'My Dapp' }, + connectAndSign: 'By signing this message, you agree to our Terms of Service.', +}) +``` + +The signed message is emitted on the provider as a `'connectAndSign'` event: + +```typescript +const connector = config.connectors[0] +const provider = await connector.getProvider() +provider.on('connectAndSign', ({ accounts, chainId, signature }) => { + console.log('Connected accounts:', accounts) + console.log('Chain ID:', chainId) // hex string e.g. '0x1' + console.log('Signature:', signature) +}) +``` + +### Step 9: ConnectWith (Optional) + +Use `connectWith` to connect and execute an RPC method in a single flow: + +```typescript +metaMask({ + dapp: { name: 'My Dapp' }, + connectWith: { + method: 'eth_signTypedData_v4', + params: [address, JSON.stringify(typedData)], + }, +}) +``` + +## MetaMask Connector Parameters Reference + +```typescript +type MetaMaskParameters = { + dapp?: { + name: string + url?: string + iconUrl?: string + } + debug?: boolean + mobile?: { + preferredOpenLink?: (deeplink: string, target?: string) => void + useDeeplink?: boolean + } + ui?: { + headless?: boolean + preferExtension?: boolean + showInstallModal?: boolean + } + // One of: + connectAndSign?: string + // OR + connectWith?: { method: string; params: unknown[] } + + // Deprecated (still functional): + dappMetadata?: { name: string; url?: string } // use dapp instead + logging?: unknown // use debug instead +} +``` + +## React Native Setup + +For React Native apps using wagmi with MetaMask: + +```typescript +import { Linking } from 'react-native' +import { metaMask } from 'wagmi/connectors' + +metaMask({ + dapp: { + name: 'My RN App', + url: 'https://myapp.com', + }, + mobile: { + preferredOpenLink: (link) => Linking.openURL(link), + useDeeplink: true, + }, +}) +``` + +Ensure React Native polyfills are set up per the `react-native-polyfills` rule. + +## Important Notes +- The connector ID is `'metaMaskSDK'` and the display name is `'MetaMask'` +- The connector RDNS is `['io.metamask', 'io.metamask.mobile']` +- `@metamask/connect-evm` is an optional peer dependency of `@wagmi/connectors` — only needed when you use the `metaMask()` connector, and the installed version must satisfy wagmi's declared peer range (currently `^1.3.0`), not "latest" +- The `supportedNetworks` map is auto-built from wagmi chain config — no manual RPC URL configuration needed +- If no `dapp` config is provided, defaults to `{ name: window.location.hostname, url: window.location.href }` in browsers +- `useAccount()` is deprecated in favor of `useConnection()` — both work but prefer the new name +- `useSwitchAccount()` is deprecated in favor of `useSwitchConnection()` — both work but prefer the new name +- Transport selection is automatic: uses MetaMask extension (postMessage) when available, otherwise MWP (WebSocket relay + QR/deeplinks) +- Error code `4001` = user rejected, `-32002` = request already pending — handle both explicitly diff --git a/domains/metamask-connect/skills/sign-evm-message/skill.md b/domains/metamask-connect/skills/sign-evm-message/skill.md new file mode 100644 index 0000000..751e996 --- /dev/null +++ b/domains/metamask-connect/skills/sign-evm-message/skill.md @@ -0,0 +1,227 @@ +--- +name: sign-evm-message +description: Sign messages with MetaMask using personal_sign and eth_signTypedData_v4 via the EIP-1193 provider, plus the connectAndSign shortcut +maturity: stable +--- +# Sign EVM Messages with MetaMask Connect + +## When to use + +Use this skill when: +- Signing a plaintext message with `personal_sign` for authentication or verification +- Signing structured EIP-712 typed data with `eth_signTypedData_v4` for permits, orders, or typed messages +- Using the `connectAndSign` shortcut to connect and sign in a single user approval +- Handling signature errors and user rejections + +## Workflow + +### Step 1: Get the provider and connected account + +Ensure the client is connected before requesting a signature: + +```typescript +import { createEVMClient, getInfuraRpcUrls } from '@metamask/connect-evm'; + +const client = await createEVMClient({ + dapp: { name: 'My DApp', url: window.location.href }, + api: { + supportedNetworks: { + ...getInfuraRpcUrls({ infuraApiKey: 'YOUR_INFURA_KEY', chainIds: ['0x1'] }), + }, + }, +}); + +const { accounts } = await client.connect({ chainIds: ['0x1'] }); +const provider = client.getProvider(); +const account = accounts[0]; // Address (0x-prefixed hex) +``` + +### Step 2: Sign with personal_sign + +`personal_sign` signs a UTF-8 message. The params order is `[message, account]` where `message` is a hex-encoded string: + +```typescript +// Convert message to hex +const message = 'Hello, MetaMask!'; +const hexMessage = '0x' + Array.from(new TextEncoder().encode(message)) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); + +try { + const signature = await provider.request({ + method: 'personal_sign', + params: [hexMessage, account], + }); + + console.log('Signature:', signature); + // signature is a Hex string: 0x... +} catch (err: any) { + if (err.code === 4001) { + console.log('User rejected the signature request'); + return; + } + throw err; +} +``` + +MetaMask also accepts a raw UTF-8 string for the message parameter, but hex encoding is the canonical format per EIP-191. + +### Step 3: Sign EIP-712 typed data with eth_signTypedData_v4 + +Build the full EIP-712 typed data structure with `types`, `primaryType`, `domain`, and `message`, then pass it as a JSON string: + +```typescript +const typedData = { + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' }, + ], + Mail: [ + { name: 'from', type: 'Person' }, + { name: 'to', type: 'Person' }, + { name: 'contents', type: 'string' }, + ], + Person: [ + { name: 'name', type: 'string' }, + { name: 'wallet', type: 'address' }, + ], + }, + primaryType: 'Mail', + domain: { + name: 'Ether Mail', + version: '1', + chainId: 1, + verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + }, + message: { + from: { name: 'Alice', wallet: '0xAliceAddress' }, + to: { name: 'Bob', wallet: '0xBobAddress' }, + contents: 'Hello Bob!', + }, +}; + +try { + const signature = await provider.request({ + method: 'eth_signTypedData_v4', + params: [account, JSON.stringify(typedData)], + }); + + console.log('Typed data signature:', signature); +} catch (err: any) { + if (err.code === 4001) { + console.log('User rejected the typed data signature'); + return; + } + throw err; +} +``` + +### Step 4: ERC-20 Permit (EIP-2612) example + +A common use case for `eth_signTypedData_v4` is signing ERC-20 permit approvals: + +```typescript +const permitData = { + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' }, + ], + Permit: [ + { name: 'owner', type: 'address' }, + { name: 'spender', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + ], + }, + primaryType: 'Permit', + domain: { + name: 'USD Coin', + version: '2', + chainId: 1, + verifyingContract: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC + }, + message: { + owner: account, + spender: '0xSpenderContractAddress', + value: '1000000', // 1 USDC (6 decimals) + nonce: 0, + deadline: Math.floor(Date.now() / 1000) + 3600, // 1 hour + }, +}; + +const signature = await provider.request({ + method: 'eth_signTypedData_v4', + params: [account, JSON.stringify(permitData)], +}); +``` + +### Step 5: Use connectAndSign for single-approval flow + +`connectAndSign` connects and signs a `personal_sign` message in one user interaction: + +```typescript +// For authentication, never sign a static string — it is replayable. +// Use an EIP-4361 (SIWE) formatted message with a server-issued nonce: +const siweMessage = [ + `${window.location.host} wants you to sign in with your Ethereum account:`, + '', // account is filled by your SIWE library or template + 'Sign in to My DApp', + '', + `URI: ${window.location.origin}`, + 'Version: 1', + `Chain ID: 1`, + `Nonce: ${serverIssuedNonce}`, // fetched from your backend + `Issued At: ${new Date().toISOString()}`, +].join('\n'); + +const { accounts, chainId, signature } = await client.connectAndSign({ + message: siweMessage, + chainIds: ['0x1'], +}); + +console.log('Connected account:', accounts[0]); +console.log('Signature:', signature); +``` + +This is ideal for sign-in-with-Ethereum (SIWE) flows where you want the user to connect and prove ownership in a single step — verify the signature, nonce, and domain server-side. + +### Step 6: Handle errors + +```typescript +try { + const signature = await provider.request({ + method: 'personal_sign', + params: [hexMessage, account], + }); +} catch (err: any) { + switch (err.code) { + case 4001: + // User rejected — show a message, offer retry + break; + case -32002: + // Request pending — another signing request is in progress + break; + default: + // Unexpected error + console.error('Signing failed:', err); + } +} +``` + +## Important Notes + +- **`personal_sign` params order is `[message, account]`** — not `[account, message]`. Getting this wrong will produce an invalid signature or an error. +- **`eth_signTypedData_v4` params are `[account, typedDataJSON]`** — the typed data must be passed as a `JSON.stringify`'d string, not as a raw object. +- **The `EIP712Domain` type must be declared in `types`** even though `primaryType` is never `EIP712Domain`. It defines the domain separator fields. +- **`connectAndSign` only supports `personal_sign`** — for typed data signing during connection, use `connectWith` with `method: 'eth_signTypedData_v4'` instead. +- **Chain IDs in typed data `domain.chainId` are integers** (e.g., `1`), while chain IDs in SDK calls are hex strings (e.g., `'0x1'`). Don't mix them up. +- **Error code 4001 is a deliberate user rejection** — handle gracefully with a retry option. +- **Error code -32002 means a request is pending** — do not fire another sign request until the user responds. +- **Always connect before signing** — `personal_sign` and `eth_signTypedData_v4` require an active account. Call `client.connect()` first or use `connectAndSign`. diff --git a/domains/metamask-connect/skills/sign-multichain-evm-transaction/skill.md b/domains/metamask-connect/skills/sign-multichain-evm-transaction/skill.md new file mode 100644 index 0000000..2d81c1b --- /dev/null +++ b/domains/metamask-connect/skills/sign-multichain-evm-transaction/skill.md @@ -0,0 +1,212 @@ +--- +name: sign-multichain-evm-transaction +description: Sign and send EVM transactions using the multichain client's invokeMethod. Covers eth_sendTransaction, personal_sign, eth_signTypedData_v4, scope selection, and RPC routing for read vs sign operations. +maturity: stable +--- +# Sign EVM Transactions via Multichain Client + +## When to use + +Use this skill when: +- Sending EVM transactions through `invokeMethod` on a multichain client +- Signing messages with `personal_sign` or `eth_signTypedData_v4` +- Understanding which methods route to the RPC node vs the wallet +- Selecting the correct CAIP-2 EVM scope for a target chain + +## Workflow + +### Step 1: Ensure the client is connected with EVM scopes + +```typescript +import { createMultichainClient, getInfuraRpcUrls } from '@metamask/connect-multichain'; + +const client = await createMultichainClient({ + dapp: { name: 'My DApp', url: window.location.href }, + api: { + supportedNetworks: { + ...getInfuraRpcUrls({ infuraApiKey: 'YOUR_INFURA_KEY', caipChainIds: ['eip155:1', 'eip155:137'] }), + }, + }, +}); + +await client.connect( + ['eip155:1', 'eip155:137'], // Ethereum mainnet + Polygon + [], +); +``` + +### Step 2: Understand RPC routing + +The multichain client routes EVM methods based on type: + +| Route | Methods | Transport | +|-------|---------|-----------| +| **RPC node** | `eth_call`, `eth_getBalance`, `eth_blockNumber`, `eth_getTransactionReceipt`, `eth_estimateGas`, `eth_getCode`, `eth_getLogs`, `eth_getTransactionCount` | Infura / custom RPC URL from `supportedNetworks` | +| **Wallet** | `eth_sendTransaction`, `personal_sign`, `eth_signTypedData_v4`, `wallet_switchEthereumChain`, `wallet_addEthereumChain` | MetaMask (extension or MWP) | + +The scope in `invokeMethod` determines which chain the request targets. Use `'eip155:1'` for Ethereum mainnet, `'eip155:137'` for Polygon, etc. + +### Step 3: Send a transaction (eth_sendTransaction) + +```typescript +const txHash = await client.invokeMethod({ + scope: 'eip155:1', + request: { + method: 'eth_sendTransaction', + params: [{ + from: '0xYourAddress', + to: '0xRecipientAddress', + value: '0x2386F26FC10000', // 0.01 ETH in hex wei + gas: '0x5208', // 21000 gas + // gasPrice or maxFeePerGas/maxPriorityFeePerGas optional + }], + }, +}); + +console.log('Transaction hash:', txHash); +``` + +**Estimating gas before sending:** + +```typescript +const gasEstimate = await client.invokeMethod({ + scope: 'eip155:1', + request: { + method: 'eth_estimateGas', + params: [{ + from: '0xYourAddress', + to: '0xRecipientAddress', + value: '0x2386F26FC10000', + }], + }, +}); +``` + +### Step 4: Sign a message (personal_sign) + +The message must be hex-encoded. The signer address is the second parameter. + +```typescript +const message = '0x' + Buffer.from('Hello MetaMask!').toString('hex'); + +const signature = await client.invokeMethod({ + scope: 'eip155:1', + request: { + method: 'personal_sign', + params: [message, '0xYourAddress'], + }, +}); + +console.log('Signature:', signature); +``` + +### Step 5: Sign typed data (eth_signTypedData_v4) + +Pass the signer address as the first parameter and the JSON-stringified typed data as the second. + +```typescript +const typedData = { + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' }, + ], + Mail: [ + { name: 'from', type: 'string' }, + { name: 'to', type: 'string' }, + { name: 'contents', type: 'string' }, + ], + }, + primaryType: 'Mail', + domain: { + name: 'My DApp', + version: '1', + chainId: 1, + verifyingContract: '0xContractAddress', + }, + message: { + from: 'Alice', + to: 'Bob', + contents: 'Hello!', + }, +}; + +const signature = await client.invokeMethod({ + scope: 'eip155:1', + request: { + method: 'eth_signTypedData_v4', + params: ['0xYourAddress', JSON.stringify(typedData)], + }, +}); + +console.log('Typed data signature:', signature); +``` + +### Step 6: Cross-chain scope selection + +Each `invokeMethod` call targets a specific chain via its scope. You do not need to "switch chains" — just use the appropriate scope. + +```typescript +// Send on Polygon +await client.invokeMethod({ + scope: 'eip155:137', + request: { + method: 'eth_sendTransaction', + params: [{ from: '0x...', to: '0x...', value: '0xDE0B6B3A7640000' }], + }, +}); + +// Read balance on Ethereum +const ethBalance = await client.invokeMethod({ + scope: 'eip155:1', + request: { + method: 'eth_getBalance', + params: ['0xYourAddress', 'latest'], + }, +}); + +// Read balance on Polygon +const polyBalance = await client.invokeMethod({ + scope: 'eip155:137', + request: { + method: 'eth_getBalance', + params: ['0xYourAddress', 'latest'], + }, +}); +``` + +### Step 7: Error handling for signing + +```typescript +try { + const sig = await client.invokeMethod({ + scope: 'eip155:1', + request: { + method: 'personal_sign', + params: [hexMessage, signerAddress], + }, + }); +} catch (err) { + // Multichain invokeMethod errors are wrapped in RPCInvokeMethodErr (code 53); + // the wallet's original code is on err.rpcCode + if (err instanceof RPCInvokeMethodErr && err.rpcCode === 4001) { + // User rejected the signing request + return; + } + throw err; +} +``` + +(Import the class with `import { RPCInvokeMethodErr } from '@metamask/connect-multichain';`. Revert reasons / custom error bytes from the wallet are available on `err.rpcData`.) + +## Important Notes + +- **Scope = chain target:** The `scope` field in `invokeMethod` determines which chain the method executes on. Use `'eip155:'` format (e.g., `'eip155:1'`, `'eip155:137'`, `'eip155:42161'`). +- **No chain switching needed:** Unlike single-chain EVM clients, the multichain client does not require `wallet_switchEthereumChain`. Each call specifies its own scope. +- **Read vs sign routing:** Read-only methods go to the RPC node (fast, no user prompt). Signing methods go to the wallet (requires user approval in MetaMask). +- **Hex encoding:** `personal_sign` expects the message as a hex string (`0x...`). `eth_sendTransaction` expects `value`, `gas`, and other numeric fields as hex strings. +- **`eth_signTypedData_v4`:** The typed data parameter must be a JSON **string**, not an object. +- **Gas estimation:** Always estimate gas with `eth_estimateGas` before sending if you don't have a reliable gas value. This routes to the RPC node and does not prompt the user. +- **Connected scopes:** `invokeMethod` will fail if the target scope was not included in the `connect()` call. Ensure you connect with all chains you intend to use. diff --git a/domains/metamask-connect/skills/sign-multichain-solana-transaction/skill.md b/domains/metamask-connect/skills/sign-multichain-solana-transaction/skill.md new file mode 100644 index 0000000..3564e32 --- /dev/null +++ b/domains/metamask-connect/skills/sign-multichain-solana-transaction/skill.md @@ -0,0 +1,242 @@ +--- +name: sign-multichain-solana-transaction +description: Sign and send Solana transactions using the multichain client's invokeMethod. Covers signTransaction, signAndSendTransaction, signMessage, building transactions with @solana/web3.js, base64 encoding, mainnet/devnet scopes, and selective disconnect. +maturity: stable +--- +# Sign Solana Transactions via Multichain Client + +## When to use + +Use this skill when: +- Signing or sending Solana transactions through `invokeMethod` on a multichain client +- Building Solana transactions with `@solana/web3.js` and encoding them for the multichain API +- Signing Solana messages through the multichain client +- Selecting the correct Solana CAIP-2 scope (mainnet, devnet) +- Disconnecting only Solana scopes while keeping EVM sessions active + +## Workflow + +### Step 1: Connect with Solana scopes + +```typescript +import { createMultichainClient, getInfuraRpcUrls } from '@metamask/connect-multichain'; + +const client = await createMultichainClient({ + dapp: { name: 'My DApp', url: window.location.href }, + api: { + supportedNetworks: { + ...getInfuraRpcUrls({ infuraApiKey: 'YOUR_INFURA_KEY', caipChainIds: ['eip155:1', 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'] }), + }, + }, +}); + +await client.connect( + [ + 'eip155:1', + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', // mainnet + ], + [], +); // resolves with no value — read session via client.provider.getSession() +``` + +**Solana CAIP-2 scope identifiers:** + +| Network | CAIP-2 Scope | +|----------|-------------| +| Mainnet | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | +| Devnet | `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` | +| Testnet | `solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z` | + +All three Solana scopes are modeled by the SDK; non-mainnet availability depends on the connected MetaMask build/version, so handle connection errors rather than assuming a cluster is present. + +### Step 2: Understand Solana RPC routing + +**All Solana methods route through the wallet.** Unlike EVM where read calls go to an RPC node, every Solana `invokeMethod` call is handled by MetaMask. There is no RPC node fallback for Solana. + +### Step 3: Sign a message (signMessage) + +Method names have **no `solana_` prefix**. The message must be **base64 encoded**, and the signing account is passed as `account: { address }`. + +```typescript +const message = btoa('Hello from Solana via MetaMask!'); + +const result = await client.invokeMethod({ + scope: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + request: { + method: 'signMessage', + params: { + account: { address: 'YourSolanaAddressBase58' }, + message, + }, + }, +}); + +// result: { signature: , signedMessage: , signatureType: 'ed25519' } +console.log('Signature:', result.signature); +``` + +### Step 4: Build a Solana transaction with @solana/web3.js + +Build the transaction using `@solana/web3.js`, serialize it, then base64-encode for `invokeMethod`. + +```typescript +import { + Connection, + PublicKey, + SystemProgram, + Transaction, + clusterApiUrl, +} from '@solana/web3.js'; + +const connection = new Connection(clusterApiUrl('mainnet-beta')); +const fromPubkey = new PublicKey('YourSolanaPublicKey'); +const toPubkey = new PublicKey('RecipientSolanaPublicKey'); + +const transaction = new Transaction().add( + SystemProgram.transfer({ + fromPubkey, + toPubkey, + lamports: 1_000_000, // 0.001 SOL + }), +); + +transaction.recentBlockhash = (await connection.getLatestBlockhash()).blockhash; +transaction.feePayer = fromPubkey; + +const serialized = transaction.serialize({ + requireAllSignatures: false, + verifySignatures: false, +}); +const base64Transaction = Buffer.from(serialized).toString('base64'); +``` + +### Step 5: Sign a transaction (signTransaction) + +Returns the signed transaction without broadcasting it. + +```typescript +const SOLANA_MAINNET = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'; + +const signResult = await client.invokeMethod({ + scope: SOLANA_MAINNET, + request: { + method: 'signTransaction', + params: { + account: { address: 'YourSolanaAddressBase58' }, + transaction: base64Transaction, + scope: SOLANA_MAINNET, + }, + }, +}); + +// The result field is `signedTransaction` (base64), not `transaction` +console.log('Signed transaction:', signResult.signedTransaction); +``` + +You can then broadcast the signed transaction yourself: + +```typescript +const signedBuffer = Buffer.from(signResult.signedTransaction, 'base64'); +const txId = await connection.sendRawTransaction(signedBuffer); +console.log('Transaction ID:', txId); +``` + +### Step 6: Sign and send a transaction (signAndSendTransaction) + +Signs and broadcasts the transaction in one step. + +```typescript +const sendResult = await client.invokeMethod({ + scope: SOLANA_MAINNET, + request: { + method: 'signAndSendTransaction', + params: { + account: { address: 'YourSolanaAddressBase58' }, + transaction: base64Transaction, + scope: SOLANA_MAINNET, + }, + }, +}); + +// sendResult: { signature: } +console.log('Transaction signature:', sendResult.signature); +``` + +### Step 7: Devnet transactions + +Connect with the devnet scope and point `@solana/web3.js` at the devnet cluster: + +```typescript +const SOLANA_DEVNET = 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1'; + +await client.connect([SOLANA_DEVNET], []); + +const connection = new Connection(clusterApiUrl('devnet')); + +// Build transaction with devnet connection... +const base64Tx = buildAndSerializeTransaction(connection); + +const result = await client.invokeMethod({ + scope: SOLANA_DEVNET, + request: { + method: 'signAndSendTransaction', + params: { + account: { address: 'YourSolanaAddressBase58' }, + transaction: base64Tx, + scope: SOLANA_DEVNET, + }, + }, +}); +``` + +### Step 8: Selective disconnect + +Disconnect only Solana scopes while keeping EVM sessions active: + +```typescript +// Disconnect only Solana mainnet — EVM scopes remain connected +await client.disconnect(['solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp']); + +// Disconnect all scopes (full session teardown) +await client.disconnect(); +``` + +### Step 9: Error handling + +`invokeMethod` errors are wrapped in `RPCInvokeMethodErr` — its own `code` is always `53`, and the wallet's original EIP-1193 / JSON-RPC code (e.g. `4001` user rejection) is on `rpcCode`: + +```typescript +import { RPCInvokeMethodErr } from '@metamask/connect-multichain'; + +try { + await client.invokeMethod({ + scope: SOLANA_MAINNET, + request: { + method: 'signAndSendTransaction', + params: { + account: { address: 'YourSolanaAddressBase58' }, + transaction: base64Tx, + scope: SOLANA_MAINNET, + }, + }, + }); +} catch (err) { + if (err instanceof RPCInvokeMethodErr && err.rpcCode === 4001) { + // User rejected the transaction in MetaMask — not an app error + } else { + console.error('Solana transaction error:', err); + } +} +``` + +## Important Notes + +- **All Solana methods go to the wallet.** There is no RPC node routing for Solana — every `invokeMethod` call with a Solana scope prompts MetaMask. +- **Base64 encoding required.** Transactions and messages must be base64-encoded strings, not raw buffers or hex. +- **Use `@solana/web3.js` to build transactions.** Construct `Transaction` objects, set `recentBlockhash` and `feePayer`, serialize with `requireAllSignatures: false`, then base64-encode. +- **CAIP-2 genesis hash IDs.** Mainnet is `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp`. Devnet is `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1`. These are not cluster URLs — they are genesis hash identifiers. +- **Solana networks.** Mainnet, devnet, and testnet scopes are all modeled by the SDK; non-mainnet availability depends on the connected MetaMask build/version, so handle connection errors rather than assuming a cluster is present. +- **Selective disconnect preserves other scopes.** Passing specific Solana scopes to `disconnect()` only revokes those scopes. EVM scopes remain active. +- **Connected scopes required.** `invokeMethod` fails if the Solana scope was not included in the original `connect()` call. +- **Method names have no `solana_` prefix.** The MetaMask Multichain API methods are `signMessage`, `signTransaction`, and `signAndSendTransaction`, each taking `account: { address }` in params. (`solana_*`-prefixed names are WalletConnect's schema, not MetaMask's.) +- **`signTransaction` vs `signAndSendTransaction`:** Use `signTransaction` when you need to inspect or modify the signed output before broadcasting (result field: `signedTransaction`, base64). Use `signAndSendTransaction` for the common case where you want a single atomic operation (result field: `signature`, base58). diff --git a/domains/metamask-connect/skills/sign-solana-message/skill.md b/domains/metamask-connect/skills/sign-solana-message/skill.md new file mode 100644 index 0000000..f326b93 --- /dev/null +++ b/domains/metamask-connect/skills/sign-solana-message/skill.md @@ -0,0 +1,156 @@ +--- +name: sign-solana-message +description: Sign an arbitrary message on Solana using MetaMask Connect. Covers both the React wallet-adapter approach (useWallet) and the vanilla browser approach (wallet-standard features). +maturity: stable +--- +# Sign Solana Message with MetaMask + +## When to use + +Use this skill when: +- Signing an arbitrary message on Solana with MetaMask Connect +- Implementing sign-in-with-Solana or message verification flows +- Using `useWallet().signMessage` in a React app +- Using the `solana:signMessage` wallet-standard feature in a vanilla browser app + +## Workflow + +### Step 1: Encode the message + +Solana message signing requires a `Uint8Array`. Use `TextEncoder` to convert a string: + +```typescript +const message = new TextEncoder().encode('Sign this message to verify your identity'); +``` + +### Step 2a: Sign with React wallet-adapter (useWallet) + +**Prerequisites:** `createSolanaClient` has been awaited before rendering, `WalletProvider` is configured with `wallets={[]}`, and the user is connected. See the `setup-solana-react-app` skill. + +```tsx +import { useWallet } from '@solana/wallet-adapter-react'; + +function SignMessageButton() { + const { signMessage, publicKey, connected } = useWallet(); + + const handleSign = async () => { + if (!signMessage || !publicKey) { + console.error('Wallet does not support signMessage or is not connected'); + return; + } + + try { + const message = new TextEncoder().encode('Hello from MetaMask on Solana!'); + const signature = await signMessage(message); + console.log('Signature (bytes):', signature); + console.log('Signature (hex):', Buffer.from(signature).toString('hex')); + console.log('Signer:', publicKey.toBase58()); + } catch (err: any) { + if (err.code === 4001) { + console.log('User rejected the signature request'); + return; + } + console.error('signMessage failed:', err); + } + }; + + return ( + + ); +} +``` + +### Step 2b: Sign with vanilla browser (wallet-standard feature) + +**Prerequisites:** `createSolanaClient` has been called and the wallet is connected via `standard:connect`. See the `setup-solana-browser-app` skill. + +```typescript +import { createSolanaClient } from '@metamask/connect-solana'; + +const solanaClient = await createSolanaClient({ + dapp: { name: 'My DApp', url: window.location.href }, +}); + +const wallet = solanaClient.getWallet(); + +// Connect first +const connectFeature = wallet.features['standard:connect']; +const { accounts } = await connectFeature.connect(); +const account = accounts[0]; + +// Sign the message +const signMessageFeature = wallet.features['solana:signMessage']; + +try { + const message = new TextEncoder().encode('Hello from MetaMask on Solana!'); + + const [{ signature }] = await signMessageFeature.signMessage({ + account, + message, + }); + + console.log('Signature (hex):', Buffer.from(signature).toString('hex')); + console.log('Signer:', account.address); +} catch (err: any) { + if (err.code === 4001) { + console.log('User rejected the signature request'); + } else { + console.error('signMessage failed:', err); + } +} +``` + +### Step 3: Verify the signature (optional) + +Use `tweetnacl` or `@noble/ed25519` to verify the signature off-chain: + +```typescript +import nacl from 'tweetnacl'; + +const message = new TextEncoder().encode('Hello from MetaMask on Solana!'); +const isValid = nacl.sign.detached.verify( + message, + signature, // Uint8Array from signMessage + publicKey.toBytes(), // Uint8Array of the signer's public key +); +console.log('Signature valid:', isValid); +``` + +### Step 4: Error handling + +Handle these common error scenarios: + +| Error | Cause | Action | +|-------|-------|--------| +| Code `4001` | User rejected the request in MetaMask | Show retry UI, do not treat as app error | +| `signMessage` is `undefined` | Wallet does not support message signing | Check `signMessage` exists before calling | +| `publicKey` is `null` | Wallet not connected | Prompt user to connect first | +| Network error | MetaMask Mobile connection interrupted | Retry or reconnect | + +```typescript +try { + const signature = await signMessage(message); +} catch (err: any) { + switch (err.code) { + case 4001: + // User rejected — show retry button + break; + case -32002: + // Request already pending — wait for user to act in MetaMask + break; + default: + console.error('Unexpected error:', err); + } +} +``` + +## Important Notes + +- **Messages must be `Uint8Array`** — use `new TextEncoder().encode(string)` to convert. Do not pass raw strings to `signMessage`. +- **`signMessage` may be `undefined`** — always check that `signMessage` exists on the wallet adapter before calling it. Not all wallets support arbitrary message signing. +- **The signature is Ed25519** — Solana uses Ed25519 signatures. The returned `Uint8Array` is 64 bytes. +- **User rejection is code `4001`** — handle it gracefully with a retry option. Do not log it as an error. +- **Wallet name is `"MetaMask"`** — case-sensitive, used to identify the MetaMask wallet in the adapter list. +- **Solana networks** — mainnet, devnet, and testnet scopes are all modeled by the SDK; non-mainnet availability depends on the connected MetaMask build/version, so handle connection errors rather than assuming a cluster is present. diff --git a/domains/metamask-connect/skills/troubleshoot-connection/skill.md b/domains/metamask-connect/skills/troubleshoot-connection/skill.md new file mode 100644 index 0000000..f217cee --- /dev/null +++ b/domains/metamask-connect/skills/troubleshoot-connection/skill.md @@ -0,0 +1,483 @@ +--- +name: troubleshoot-connection +description: Diagnose and fix common MetaMask Connect SDK connection failures, transport issues, and runtime errors +maturity: stable +--- +# Troubleshoot MetaMask Connect Issues + +## When to use + +Use this skill when: +- A connection attempt hangs, fails, or produces an unexpected error +- React Native apps crash on import or at runtime with missing polyfill errors +- QR codes don't appear or deeplinks don't open MetaMask Mobile +- Solana wallet adapter doesn't detect MetaMask +- Sessions are lost after page reload or disconnect behaves unexpectedly +- You need a systematic checklist to verify a MetaMask Connect integration + +## Symptom -> Cause -> Fix Reference + +--- + +### 1. Connection hangs / nothing happens after `connect()` + +**Cause A:** Extension not detected but `preferExtension` is `true` (the default). The SDK falls through to MetaMask Wallet Protocol (MWP) but no QR code is rendered because headless mode is on and there is no `display_uri` listener. + +**Fix:** Register a `display_uri` event listener to render the QR code URI before calling `connect()` + +**Cause B:** A concurrent `connect()` call is already in progress over MWP. + +**Fix:** Guard against double-clicking. Wrap `connect()` in a loading-state check and match on the `"Existing connection is pending"` error message — on MWP this error has **no numeric code**. (`-32002` only appears on the extension transport.) + +--- + +### 2. User rejected request (code `4001`) + +**Cause:** The user clicked "Reject" in MetaMask. This is normal behavior. + +**Fix:** Handle gracefully — show a retry button. Do not treat this as an application error or log it to error-tracking services: + +```typescript +try { + await client.connect({ chainIds: ['0x1'] }); +} catch (err) { + if (err.code === 4001) { + // User rejected — show retry UI + return; + } + throw err; +} +``` + +--- + +### 3. Connection already pending (code `-32002`) + +**Cause:** A previous `connect()` call has not yet resolved (the user may still have the MetaMask approval dialog open on mobile). + +**Fix:** Show a message like "Check MetaMask Mobile to approve the connection." Do **not** call `connect()` again — the original promise will resolve once the user acts. + +--- + +### 4. Chain not configured in `supportedNetworks` + +**Cause:** An RPC request was made on a chain whose CAIP scope is missing from `api.supportedNetworks`. This error is thrown by the EIP-1193 provider's `request()` path for the *active* chain's node-routed reads — not by `connect()` (which only checks `chainIds` is non-empty) and not by `wallet_switchEthereumChain` (forwarded to the wallet). + +**Fix:** Add every chain the dApp needs to `supportedNetworks` with a valid RPC URL: + +```typescript +const client = await createEVMClient({ + dapp: { name: 'My DApp' }, + api: { + supportedNetworks: { + ...getInfuraRpcUrls({ infuraApiKey: 'YOUR_INFURA_KEY', chainIds: ['0x1', '0x89', '0xaa36a7'] }), + '0xa4b1': 'https://arb1.arbitrum.io/rpc', + }, + }, +}); +``` + +--- + +### 5. `Cannot find variable: Buffer` / `Buffer is not defined` (React Native) + +**Cause:** A dependency loaded before `@metamask/connect-multichain` uses `Buffer`. The connect package self-polyfills Buffer via its React Native entry point, but peer dependencies like `eciesjs` may execute first. + +**Fix:** Add this to `polyfills.ts` and import it early (after `react-native-get-random-values`, before other imports): + +```typescript +import { Buffer } from 'buffer'; +global.Buffer = Buffer; +``` + +--- + +### 6. `Cannot find variable: Event` / `CustomEvent is not defined` (React Native) + +**Cause:** wagmi dispatches DOM events internally, and React Native does not provide `Event`/`CustomEvent` globals. The `@metamask/connect-*` packages themselves never construct DOM events (they use `eventemitter3`) — this error only occurs when wagmi (or another DOM-dependent library) is in the stack. + +**Fix:** If you use wagmi in React Native, add standalone class polyfills in `polyfills.ts`. Do **not** `extends Event` — that references the very global that is missing: + +```typescript +class EventPolyfill { + type: string; + constructor(type: string) { + this.type = type; + } +} + +class CustomEventPolyfill extends EventPolyfill { + detail: any; + constructor(type: string, options?: { detail?: any }) { + super(type); + this.detail = options?.detail; + } +} + +global.Event = EventPolyfill as any; +global.CustomEvent = CustomEventPolyfill as any; +``` + +If you are not using wagmi and still see this error, the source is another dependency — not the MetaMask Connect SDK. + +--- + +### 7. Deeplinks not opening MetaMask app (React Native) + +**Cause:** The `mobile.preferredOpenLink` callback is not configured. + +**Fix:** Pass a function that calls `Linking.openURL`: + +```typescript +import { Linking } from 'react-native'; + +const client = await createEVMClient({ + dapp: { name: 'My DApp', url: 'https://mydapp.com' }, + mobile: { + preferredOpenLink: (deeplink: string) => Linking.openURL(deeplink), + }, +}); +``` + +--- + +### 8. App crashes on import of SDK (React Native) + +**Cause:** Metro bundler cannot resolve Node.js built-in modules (`stream`, `crypto`, `http`, `https`, `os`, `url`, `assert`, `events`, etc.) that SDK dependencies reference. + +**Fix:** Add `extraNodeModules` shims in `metro.config.js`: + +```javascript +const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); +const path = require('path'); + +// src/empty-module.js: `module.exports = {};` +// Only `stream` needs a real shim — the other Node built-ins are referenced +// by transitive deps but never called at runtime in React Native. +const emptyModule = path.resolve(__dirname, 'src', 'empty-module.js'); + +const config = { + resolver: { + extraNodeModules: { + stream: require.resolve('readable-stream'), + crypto: emptyModule, + http: emptyModule, + https: emptyModule, + net: emptyModule, + tls: emptyModule, + zlib: emptyModule, + os: emptyModule, + dns: emptyModule, + assert: emptyModule, + url: emptyModule, + path: emptyModule, + fs: emptyModule, + }, + }, +}; + +module.exports = mergeConfig(getDefaultConfig(__dirname), config); +``` + +Install the corresponding shim packages via `npm install`. + +--- + +### 9. `crypto.getRandomValues is not a function` (React Native) + +**Cause:** `react-native-get-random-values` is either not installed or not imported as the very first import. + +**Fix:** Import it as the **first line** of your entry file — before any other import: + +```typescript +import 'react-native-get-random-values'; +// all other imports follow +``` + +--- + +### 10. MetaMask wallet not appearing in Solana wallet adapter + +**Cause A:** `createSolanaClient` was never called (or was called only inside a component that hasn't mounted). Note that registration happens ~1 second *after* the factory resolves, and the wallet adapter discovers late registrations automatically — so a briefly empty wallet list right after startup is normal. + +**Fix:** Call client creation once in your bootstrap. Rendering does not need to block on it: + +```typescript +import { createSolanaClient } from '@metamask/connect-solana'; + +// Kick off client creation — no need to await before rendering; +// the wallet registers via the wallet-standard register event (~1s later) +void createSolanaClient({ + dapp: { name: 'My DApp', url: window.location.href }, +}); + +const root = createRoot(document.getElementById('root')!); +root.render(); +``` + +**Cause B:** The `wallets` prop on `WalletProvider` is not an empty array. MetaMask uses the wallet-standard auto-discovery protocol and must **not** be listed manually. + +**Fix:** Always pass `wallets={[]}`: + +```tsx + + + + + +``` + +--- + +### 11. Solana devnet / testnet not working + +**Cause:** The SDK models mainnet, devnet, and testnet Solana scopes, but a given cluster's availability depends on the connected MetaMask build/version — and public cluster RPC endpoints are frequently rate-limited or flaky. + +**Fix:** Confirm the connected wallet actually granted the devnet/testnet scope (inspect `session.sessionScopes`), and don't assume a non-mainnet cluster is present — handle the connection error. If the scope is granted but reads fail, the issue is likely an unreliable RPC endpoint; use a dedicated provider instead of the public default: + +```typescript +// Public endpoints can be rate-limited or unavailable — use a dedicated RPC: +const endpoint = 'https://api.devnet.solana.com'; // or your own Infura /Helius / QuickNode / Alchemy URL +``` + +--- + +### 12. Session lost after page reload + +**Cause:** The app is not re-deriving UI state after the automatic session restore. The EVM client syncs any persisted session **before** `createEVMClient` resolves, then re-emits `connect`/`accountsChanged` on the provider. (The EIP-1193 provider never emits `wallet_sessionChanged` — that event exists only on the multichain client.) + +**Fix:** Check the cached state right after client creation, and subscribe to the provider events: + +```typescript +const client = await createEVMClient({ /* ... */ }); + +// Synchronous check — a restored session is already reflected here +const account = client.getAccount(); +if (account) { + updateUI([account], client.getChainId()); +} + +const provider = client.getProvider(); +provider.on('connect', ({ accounts, chainId }) => updateUI(accounts, chainId)); +provider.on('accountsChanged', (accounts) => updateUI(accounts, client.getChainId())); +``` + +If you use the multichain client directly, listen there instead: `client.on('wallet_sessionChanged', (session) => session?.sessionScopes ...)`. + +Do not call `connect()` again immediately on page load if a session already exists. + +--- + +### 13. `disconnect()` doesn't fully disconnect + +**Cause:** Disconnect behavior differs by client. On the **multichain** client (`createMultichainClient`), `disconnect(scopes)` with specific CAIP scopes only revokes those scopes; `disconnect()` with no arguments revokes all. On the **EVM** client (`createEVMClient`), `disconnect()` takes **no arguments** and revokes only `eip155:*` scopes. On the **Solana** client (`createSolanaClient`), `disconnect()` takes no arguments and revokes only the Solana scopes. + +**Fix:** To fully terminate a multichain session, call the multichain client's `disconnect()` with no arguments: + +```typescript +// Multichain client — partial revoke (only the specified scope) +await multichainClient.disconnect(['eip155:1']); + +// Multichain client — full disconnect (all scopes) +await multichainClient.disconnect(); + +// EVM client — revokes eip155 scopes only (no scope argument) +await evmClient.disconnect(); +``` + +--- + +### 14. QR code not appearing + +**Cause A:** Headless mode is enabled but no `display_uri` listener is registered. The SDK generates the URI but has nowhere to render it. + +**Fix:** Register a `displayUri` handler (or a provider `display_uri` listener) **before** calling `connect()`. The EVM client itself has no `.on()` method: + +```typescript +const client = await createEVMClient({ + dapp: { name: 'My DApp', url: window.location.href }, + api: { supportedNetworks: getInfuraRpcUrls({ infuraApiKey: 'YOUR_INFURA_KEY' }) }, + ui: { headless: true }, + eventHandlers: { + displayUri: (uri) => renderQrCode(uri), // your QR rendering logic + }, +}); + +// Equivalent: client.getProvider().on('display_uri', renderQrCode); + +await client.connect({ chainIds: ['0x1'] }); +``` + +**Cause B:** The extension is detected and the SDK uses the extension transport instead of MWP. No QR is generated because none is needed. + +**Fix:** Force the MWP/QR flow by disabling extension preference: + +```typescript +const client = await createEVMClient({ + dapp: { name: 'My DApp', url: window.location.href }, + api: { supportedNetworks: getInfuraRpcUrls({ infuraApiKey: 'YOUR_INFURA_KEY' }) }, + ui: { preferExtension: false }, +}); +``` + +--- + +### 15. Extension transport used but want mobile QR + +**Cause:** `preferExtension` defaults to `true`. When the MetaMask browser extension is installed, the SDK always prefers it. + +**Fix:** Set `ui.preferExtension = false`: + +```typescript +const client = await createEVMClient({ + dapp: { name: 'My DApp', url: window.location.href }, + api: { supportedNetworks: getInfuraRpcUrls({ infuraApiKey: 'YOUR_INFURA_KEY' }) }, + ui: { preferExtension: false }, +}); +``` + +--- + +### 16. QR code modal blocked by dapp `Content-Security-Policy` + +**Cause:** Older versions of the QR modal created a `blob:` URL for the embedded MetaMask icon. If the host page's CSP `connect-src` directive did not include `blob:`, the `XMLHttpRequest` used to build the blob was rejected and the QR image failed to render. + +**Fix:** Upgrade to `@metamask/connect-multichain ^0.12.1` and `@metamask/multichain-ui ^0.4.1` (shipped in connect-monorepo `v30.0.0`). The icon is now embedded as a `data:` URI and `saveAsBlob: false` is set in the QR image options, so no `connect-src blob:` entry is needed: + +```bash +npm install @metamask/connect-multichain@^0.12.1 @metamask/multichain-ui@^0.4.1 +# or update @metamask/connect-evm to ^0.11.2 / @metamask/connect-solana to ^0.8.1 +# which pin the fixed multichain version transitively +``` + +--- + +### 17. `eth_coinbase` returns an array / inconsistent account responses + +**Cause:** Before `@metamask/connect-evm` 1.3.1, the SDK's intercepted EIP-1193 account requests returned the same accounts array for both `eth_requestAccounts` and `eth_coinbase`. Per spec, `eth_coinbase` should return a single address (`Address`), not an array. + +**Fix:** Upgrade to `@metamask/connect-evm` ^1.3.1 (connect-monorepo `v35.0.0`). After upgrade, `eth_requestAccounts` resolves to `Address[]` and `eth_coinbase` resolves to the currently selected account (`Address`). Update any code that destructured `eth_coinbase` as an array: + +```typescript +const accounts = await provider.request({ method: 'eth_requestAccounts' }); +const coinbase = await provider.request({ method: 'eth_coinbase' }); + +console.log(accounts[0]); // selected account +console.log(coinbase); // same address as accounts[0] — a string, NOT an array +``` + +```bash +npm install @metamask/connect-evm@^1.3.1 +``` + +--- + +### 18. `wallet_switchEthereumChain` masks `Unrecognized chain ID` with `No chain configuration found.` + +**Cause:** Before `@metamask/connect-evm` 1.2.0, calling `client.switchChain({ chainId })` without a `chainConfiguration` fallback (or invoking `wallet_switchEthereumChain` directly) replaced the wallet's original `Unrecognized chain ID` error with the wrapper message `No chain configuration found.`, hiding the underlying `4902` code from the dapp. + +**Fix:** Upgrade to `@metamask/connect-evm` ^1.2.0 (connect-monorepo `v33.0.0`). The original wallet error (EIP-1193 code `4902`) is now forwarded to the dapp. Handle it explicitly — either retry with a `chainConfiguration` fallback or call `wallet_addEthereumChain`: + +```typescript +try { + await client.switchChain({ chainId: '0xa4b1' }); +} catch (err) { + if ((err as { code?: number }).code === 4902) { + await client.switchChain({ + chainId: '0xa4b1', + chainConfiguration: { + chainId: '0xa4b1', + chainName: 'Arbitrum One', + rpcUrls: ['https://arb1.arbitrum.io/rpc'], + nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, + }, + }); + return; + } + throw err; +} +``` + +Do not pattern-match on the legacy `"No chain configuration found"` string — that branch will never fire after the upgrade. + +--- + +### 19. Analytics `_rejected` count looks artificially high / `wallet_unauthorized` mis-classified + +**Cause:** Before `@metamask/connect-multichain` 0.14.0, the `isRejectionError` helper that drives the `mmconnect_wallet_action_rejected` analytics event treated EIP-1193 `4100 Unauthorized` (a CAIP-25 permission denial) as a user rejection, matched any error message containing the bare substring `"user"` (catching unrelated phrases like Account Abstraction's `"user operation reverted"`), and masked wallet-side codes behind the router's transport-boundary wrapper (`code: 53`). + +**Fix:** Upgrade to `@metamask/connect-multichain` ^0.14.0 (connect-monorepo `v34.0.0`). The classifier now: + +- Unwraps `RPCInvokeMethodErr` so wallet-side codes survive the router boundary +- No longer counts `4100 wallet_unauthorized` as a rejection — it's a permission denial, surfaced under `mmconnect_wallet_action_failed` instead +- Narrows the substring match to four explicit phrases: `"user rejected"`, `"user denied"`, `"user cancelled"`, `"user canceled"` + +Net effect: `_rejected` becomes more precise, and `_failed` picks up everything `4100` was previously hiding. Update analytics dashboards / alerts that compared `_rejected` counts across the 0.13.x → 0.14.0 boundary — expect `_rejected` to drop and `_failed` to rise without an underlying behavior change. + +The same release adds three optional companion fields on `mmconnect_wallet_action_failed` and `mmconnect_connection_failed`: + +- `failure_reason` — coarse classifier (transport timeout, transport disconnect, EIP-1193 wallet errors `4100` / `4200` / `4902`, JSON-RPC wallet errors `-32601` / `-32602` / `-32603` and `-32000…-32099`, or `unknown`) +- `error_code` — raw wallet-side JSON-RPC / EIP-1193 code (e.g. `4001`, `-32603`) +- `error_message_sample` — sanitised, 200-char-max preview of the original error message (wallet addresses, hex blobs, URLs, and large decimal numbers scrubbed) + +Use these for finer triage in analytics consumers: + +```bash +npm install @metamask/connect-multichain@^0.14.0 +# or update @metamask/connect-evm to ^1.3.0 / @metamask/connect-solana to ^1.1.0 +# which pin the fixed multichain version transitively +``` + +--- + +### 20. Module-not-found / peer-version warning for `@metamask/connect-multichain` after upgrading to `connect-evm` 2.0.0 or `connect-solana` 2.0.0 + +**Cause:** You are on the 2.0.0 releases of `@metamask/connect-evm`/`@metamask/connect-solana`, which (only in that version) made `@metamask/connect-multichain` a **peer dependency** that was not installed transitively. 2.1.0 reverted this — `@metamask/connect-multichain` is a regular dependency again. If a wrong or duplicate version is resolved, the SDK logs a runtime warning about a version mismatch or duplicate `@metamask/connect-multichain` resolutions. + +**Fix:** Add it explicitly to your own `dependencies`: + +```bash +npm install @metamask/connect-multichain@^1.0.0 +``` + +- Ensure a **single** `@metamask/connect-multichain` resolves in your tree — `npm ls @metamask/connect-multichain` (or `yarn why` / `pnpm why`) should show one `1.x` version. Deduplicate (e.g. `npm dedupe`) if two copies appear, since duplicate resolutions trigger the runtime warning and can break singleton/session sharing. +- `@metamask/connect-multichain` is now a stable 1.0 package following strict semver, so `^1.0.0` is safe for all current ecosystem packages. + +--- + +### 21. MMConnect provider shows up twice in wallet discovery, or the wrong provider is selected + +**Cause:** Since `@metamask/connect-evm` 2.0.0, the MMConnect-managed EIP-1193 provider is announced through EIP-6963 **by default** (when native MetaMask hasn't already announced). If your app also announces a provider manually, or a discovery library (RainbowKit / ConnectKit / Web3Modal / wagmi) re-announces, you can end up with a duplicate MetaMask-style entry. + +**Fix:** + +- Pass `skipAutoAnnounce: true` to `createEVMClient()` to suppress the automatic announcement when you want to control discovery yourself, then call `client.announceProvider()` exactly when you need to surface it. +- Do **not** manually re-emit `eip6963:announceProvider` for the MMConnect provider in addition to the SDK — let the SDK own it, or use `skipAutoAnnounce` + `announceProvider()`, not both. +- Note the SDK restricts EIP-6963 extension detection to native MetaMask RDNS values, so the MMConnect-managed provider will not be mistaken for — or select — the browser-extension transport. + +--- + +## Diagnostic Checklist + +Run through this checklist when any MetaMask Connect integration is misbehaving: + +- [ ] **`supportedNetworks` has valid RPC URLs** — every chain the dApp uses must have an entry with a reachable URL +- [ ] **Chain IDs are hex strings for EVM** — use `'0x1'` not `1` or `'1'` +- [ ] **Polyfills loaded (React Native)** — `react-native-get-random-values` is first entry-file import (required for RN < 0.72); `window` shim present (required for all); `Event`/`CustomEvent` shims present **only if using wagmi**; `Buffer` set as safety net for peer deps +- [ ] **`preferredOpenLink` set (React Native)** — required for deeplinks to open MetaMask Mobile +- [ ] **Import order correct** — polyfills before SDK imports; `react-native-get-random-values` is the very first import +- [ ] **Error codes handled in catch blocks** — at minimum handle `4001` (user rejected) and `-32002` (pending) +- [ ] **Client not recreated per render** — call `createEVMClient` / `createMultichainClient` / `createSolanaClient` once; the shared multichain core is the singleton (its options merge), but each `create*Client` call still returns a fresh wrapper +- [ ] **`display_uri` listener registered before `connect()`** — required in headless mode for QR codes +- [ ] **Solana `wallets` prop is `[]`** — MetaMask uses wallet-standard discovery, not manual registration +- [ ] **Solana network availability checked** — mainnet/devnet/testnet scopes are all modeled by the SDK; don't assume a non-mainnet cluster is available on the connected wallet — handle connection errors +- [ ] **Analytics consumers use `failure_reason` / `error_code` / `error_message_sample`** — for `mmconnect_wallet_action_failed` / `mmconnect_connection_failed` triage (added in `@metamask/connect-multichain` 0.14.0); expect `_rejected` counts to drop and `_failed` counts to rise after upgrading past 0.13.x + +## Important Notes + +- Always check the **error code** first — it tells you the category of failure before you need to inspect the message. +- Use typed error classes from `@metamask/connect-multichain` for granular `instanceof` checks: `RPCInvokeMethodErr` (wallet errors from `invokeMethod` — original wallet code on `rpcCode`, revert data on `rpcData`), `RPCHttpErr` / `RPCReadonlyResponseErr` / `RPCReadonlyRequestErr` (RPC-node-routed read calls). +- The underlying multichain core is a **singleton**: `createMultichainClient` merges options into the shared core, and `createEVMClient` / `createSolanaClient` build chain-specific wrappers on top of it. Each `create*Client` call returns a fresh wrapper, so call it once at startup and reuse — do not wrap it in a React component render cycle. +- **Extension detection is synchronous** but **MWP connection is asynchronous** — if the extension is not installed, expect the flow to involve QR scanning or deeplinks with noticeable latency. +- In React Native, **import order matters critically**. `react-native-get-random-values` must be the very first import in the entry file (not inside `polyfills.ts`). The connect-* packages do not use DOM `Event`/`CustomEvent` — those polyfills are only needed when also using wagmi. `@metamask/connect-multichain` self-polyfills `Buffer` but set `global.Buffer` early as a safety net for peer deps. +- When debugging, enable `debug: true` in the client options to get verbose console output from the SDK internals. From 1c7d7a8858cf48da7147db7dc539a1a4ebca191b Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Mon, 15 Jun 2026 12:07:48 -0500 Subject: [PATCH 2/6] chore: add metamask-connect domain codeowners Adds @MetaMask/wallet-integrations and @adonesky1 as owners of the metamask-connect domain. --- .github/CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e2f3c74..56afb9a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,3 +1,5 @@ # GitHub code ownership for MetaMask skills. # ADR-58/recipe skills are experimental but owned by Perps during rollout. /domains/agentic/ @MetaMask/perps +# MetaMask Connect SDK skills (dApp integration) owned by Wallet Integrations. +/domains/metamask-connect/ @MetaMask/wallet-integrations @adonesky1 From cbde766bdd6281ffbc1d5d6414546982c3f50bac Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Mon, 15 Jun 2026 12:11:32 -0500 Subject: [PATCH 3/6] chore: add mobile-platform team as metamask-connect codeowner --- .github/CODEOWNERS | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 56afb9a..0355d82 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,5 +1,5 @@ # GitHub code ownership for MetaMask skills. # ADR-58/recipe skills are experimental but owned by Perps during rollout. /domains/agentic/ @MetaMask/perps -# MetaMask Connect SDK skills (dApp integration) owned by Wallet Integrations. -/domains/metamask-connect/ @MetaMask/wallet-integrations @adonesky1 +# MetaMask Connect SDK skills (dApp integration) owned by Wallet Integrations + Mobile Platform. +/domains/metamask-connect/ @MetaMask/wallet-integrations @MetaMask/mobile-platform @adonesky1 From 3ad5d341323b624d52d1cf6c855cabad54fee242 Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Mon, 15 Jun 2026 12:10:34 -0500 Subject: [PATCH 4/6] refactor(metamask-connect): consolidate into progressive-disclosure skills MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Collapses the 18 granular per-chain/platform/operation skills into 5 task-oriented skills, each with a thin routing skill.md that points into a references/ library (mirrors the domains/performance pattern): - setup-app (9 references: evm/solana x browser/react/react-native, multichain, wagmi, wagmi-connector) - sign-message (evm, solana) - send-transaction (evm, solana) - multichain-operations (evm, solana — invokeMethod sign/send) - migrate (from-sdk, wagmi-connector) troubleshoot-connection and metamask-connect-conventions are unchanged. Original (source-verified) bodies move verbatim into references; only stale cross-skill mentions were rewritten. Addresses review feedback to favor progressive disclosure and composability. --- CHANGELOG.md | 2 +- .../references/from-sdk.md} | 7 +--- .../references/wagmi-connector.md} | 5 --- .../metamask-connect/skills/migrate/skill.md | 17 +++++++++ .../references/evm.md} | 5 --- .../references/solana.md} | 5 --- .../skills/multichain-operations/skill.md | 17 +++++++++ .../references/evm.md} | 5 --- .../references/solana.md} | 9 ++--- .../skills/send-transaction/skill.md | 17 +++++++++ .../references/evm-browser.md} | 5 --- .../references/evm-react-native.md} | 5 --- .../references/evm-react.md} | 5 --- .../references/multichain.md} | 5 --- .../references/solana-browser.md} | 5 --- .../references/solana-react-native.md} | 5 --- .../references/solana-react.md} | 5 --- .../references/wagmi-connector.md} | 5 --- .../references/wagmi.md} | 5 --- .../skills/setup-app/skill.md | 35 +++++++++++++++++++ .../references/evm.md} | 5 --- .../references/solana.md} | 9 ++--- .../skills/sign-message/skill.md | 17 +++++++++ 23 files changed, 109 insertions(+), 91 deletions(-) rename domains/metamask-connect/skills/{migrate-from-sdk/skill.md => migrate/references/from-sdk.md} (97%) rename domains/metamask-connect/skills/{migrate-wagmi-metamask-connector/skill.md => migrate/references/wagmi-connector.md} (98%) create mode 100644 domains/metamask-connect/skills/migrate/skill.md rename domains/metamask-connect/skills/{sign-multichain-evm-transaction/skill.md => multichain-operations/references/evm.md} (95%) rename domains/metamask-connect/skills/{sign-multichain-solana-transaction/skill.md => multichain-operations/references/solana.md} (96%) create mode 100644 domains/metamask-connect/skills/multichain-operations/skill.md rename domains/metamask-connect/skills/{send-evm-transaction/skill.md => send-transaction/references/evm.md} (96%) rename domains/metamask-connect/skills/{send-solana-transaction/skill.md => send-transaction/references/solana.md} (96%) create mode 100644 domains/metamask-connect/skills/send-transaction/skill.md rename domains/metamask-connect/skills/{setup-evm-browser-app/skill.md => setup-app/references/evm-browser.md} (97%) rename domains/metamask-connect/skills/{setup-evm-react-native-app/skill.md => setup-app/references/evm-react-native.md} (97%) rename domains/metamask-connect/skills/{setup-evm-react-app/skill.md => setup-app/references/evm-react.md} (97%) rename domains/metamask-connect/skills/{setup-multichain-app/skill.md => setup-app/references/multichain.md} (97%) rename domains/metamask-connect/skills/{setup-solana-browser-app/skill.md => setup-app/references/solana-browser.md} (97%) rename domains/metamask-connect/skills/{setup-solana-react-native-app/skill.md => setup-app/references/solana-react-native.md} (97%) rename domains/metamask-connect/skills/{setup-solana-react-app/skill.md => setup-app/references/solana-react.md} (96%) rename domains/metamask-connect/skills/{setup-wagmi-metamask-connector/skill.md => setup-app/references/wagmi-connector.md} (98%) rename domains/metamask-connect/skills/{setup-wagmi-app/skill.md => setup-app/references/wagmi.md} (96%) create mode 100644 domains/metamask-connect/skills/setup-app/skill.md rename domains/metamask-connect/skills/{sign-evm-message/skill.md => sign-message/references/evm.md} (97%) rename domains/metamask-connect/skills/{sign-solana-message/skill.md => sign-message/references/solana.md} (92%) create mode 100644 domains/metamask-connect/skills/sign-message/skill.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 011c124..fa8c989 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `metamask-connect` domain: 18 skills for building dApps with the MetaMask Connect SDK (`@metamask/connect-evm`, `@metamask/connect-multichain`, `@metamask/connect-solana`) across EVM, Solana, multichain, wagmi, and React Native, plus signing, transaction, migration, and troubleshooting skills, and a `metamask-connect-conventions` guardrails skill +- Add `metamask-connect` domain for building dApps with the MetaMask Connect SDK (`@metamask/connect-evm`, `@metamask/connect-multichain`, `@metamask/connect-solana`) and the wagmi `metaMask()` connector. Organized with progressive disclosure as `setup-app`, `sign-message`, `send-transaction`, `multichain-operations`, and `migrate` (each routing into per-stack `references/`), plus `troubleshoot-connection` and a `metamask-connect-conventions` guardrails skill ## [0.1.0] diff --git a/domains/metamask-connect/skills/migrate-from-sdk/skill.md b/domains/metamask-connect/skills/migrate/references/from-sdk.md similarity index 97% rename from domains/metamask-connect/skills/migrate-from-sdk/skill.md rename to domains/metamask-connect/skills/migrate/references/from-sdk.md index 08b695c..ca967ae 100644 --- a/domains/metamask-connect/skills/migrate-from-sdk/skill.md +++ b/domains/metamask-connect/skills/migrate/references/from-sdk.md @@ -1,8 +1,3 @@ ---- -name: migrate-from-sdk -description: Migrate from @metamask/sdk to @metamask/connect-evm, @metamask/connect-multichain, and @metamask/connect-solana with step-by-step package, API, and configuration changes -maturity: stable ---- # Migrate from @metamask/sdk to @metamask/connect ## When to use @@ -315,7 +310,7 @@ Key differences: - The connect-evm-backed `metaMask()` connector ships in `wagmi/connectors` from wagmi 3.6 / `@wagmi/connectors` 8 — there is no `@metamask/connect-evm/wagmi` subpath; install `@metamask/connect-evm` at wagmi's declared peer range - Use `dapp` not `dappMetadata` - Connector ID is `'metaMaskSDK'` — find it with `connectors.find(c => c.id === 'metaMaskSDK')` -- Most wagmi hooks work unchanged, but note the wagmi v3 renames: `useConnect().connectors` → `useConnectors()`, `connectAsync` → `mutateAsync`, `useAccount` → `useConnection` (see the migrate-wagmi-metamask-connector skill) +- Most wagmi hooks work unchanged, but note the wagmi v3 renames: `useConnect().connectors` → `useConnectors()`, `connectAsync` → `mutateAsync`, `useAccount` → `useConnection` (see `references/wagmi-connector.md`) --- diff --git a/domains/metamask-connect/skills/migrate-wagmi-metamask-connector/skill.md b/domains/metamask-connect/skills/migrate/references/wagmi-connector.md similarity index 98% rename from domains/metamask-connect/skills/migrate-wagmi-metamask-connector/skill.md rename to domains/metamask-connect/skills/migrate/references/wagmi-connector.md index 04d9201..3c2e201 100644 --- a/domains/metamask-connect/skills/migrate-wagmi-metamask-connector/skill.md +++ b/domains/metamask-connect/skills/migrate/references/wagmi-connector.md @@ -1,8 +1,3 @@ ---- -name: migrate-wagmi-metamask-connector -description: Migrate a wagmi app from @metamask/sdk to the new @metamask/connect-evm connector (wagmi PR #4960) -maturity: stable ---- # Migrate Wagmi MetaMask Connector to @metamask/connect-evm ## When to use diff --git a/domains/metamask-connect/skills/migrate/skill.md b/domains/metamask-connect/skills/migrate/skill.md new file mode 100644 index 0000000..aced81b --- /dev/null +++ b/domains/metamask-connect/skills/migrate/skill.md @@ -0,0 +1,17 @@ +--- +name: migrate +description: Migrate an existing MetaMask integration to the MetaMask Connect SDK — from @metamask/sdk to @metamask/connect-evm / -multichain / -solana with step-by-step package, API, and config changes, or a wagmi app to the new connect-evm metaMask() connector. Routes to per-path references. +maturity: stable +--- +# Migrate to the MetaMask Connect SDK + +## When to use + +Use this when **moving an existing app** onto the MetaMask Connect SDK. + +| Migrating from | Reference | +|----------------|-----------| +| `@metamask/sdk` → `@metamask/connect-*` | [`references/from-sdk.md`](references/from-sdk.md) | +| wagmi app → the new `@metamask/connect-evm` connector | [`references/wagmi-connector.md`](references/wagmi-connector.md) | + +After migrating, follow the `metamask-connect-conventions` skill to catch behavior differences (singleton behavior, event payloads, hex chain IDs). diff --git a/domains/metamask-connect/skills/sign-multichain-evm-transaction/skill.md b/domains/metamask-connect/skills/multichain-operations/references/evm.md similarity index 95% rename from domains/metamask-connect/skills/sign-multichain-evm-transaction/skill.md rename to domains/metamask-connect/skills/multichain-operations/references/evm.md index 2d81c1b..ead1fb2 100644 --- a/domains/metamask-connect/skills/sign-multichain-evm-transaction/skill.md +++ b/domains/metamask-connect/skills/multichain-operations/references/evm.md @@ -1,8 +1,3 @@ ---- -name: sign-multichain-evm-transaction -description: Sign and send EVM transactions using the multichain client's invokeMethod. Covers eth_sendTransaction, personal_sign, eth_signTypedData_v4, scope selection, and RPC routing for read vs sign operations. -maturity: stable ---- # Sign EVM Transactions via Multichain Client ## When to use diff --git a/domains/metamask-connect/skills/sign-multichain-solana-transaction/skill.md b/domains/metamask-connect/skills/multichain-operations/references/solana.md similarity index 96% rename from domains/metamask-connect/skills/sign-multichain-solana-transaction/skill.md rename to domains/metamask-connect/skills/multichain-operations/references/solana.md index 3564e32..fd6297e 100644 --- a/domains/metamask-connect/skills/sign-multichain-solana-transaction/skill.md +++ b/domains/metamask-connect/skills/multichain-operations/references/solana.md @@ -1,8 +1,3 @@ ---- -name: sign-multichain-solana-transaction -description: Sign and send Solana transactions using the multichain client's invokeMethod. Covers signTransaction, signAndSendTransaction, signMessage, building transactions with @solana/web3.js, base64 encoding, mainnet/devnet scopes, and selective disconnect. -maturity: stable ---- # Sign Solana Transactions via Multichain Client ## When to use diff --git a/domains/metamask-connect/skills/multichain-operations/skill.md b/domains/metamask-connect/skills/multichain-operations/skill.md new file mode 100644 index 0000000..b11a7a9 --- /dev/null +++ b/domains/metamask-connect/skills/multichain-operations/skill.md @@ -0,0 +1,17 @@ +--- +name: multichain-operations +description: Sign and send transactions and messages through the MetaMask Connect multichain client's invokeMethod — EVM (eth_sendTransaction, personal_sign, eth_signTypedData_v4) and Solana (signTransaction, signAndSendTransaction, signMessage). Use after createMultichainClient when performing operations across CAIP-2 scopes, including building Solana transactions with @solana/web3.js, base64 encoding, mainnet/devnet scope selection, RPC routing for read vs sign, and selective disconnect. Routes to per-ecosystem references. +maturity: stable +--- +# Operations via the MetaMask Connect Multichain Client + +## When to use + +Use this when you created a **multichain** client (`createMultichainClient`, see the `setup-app` skill → `references/multichain.md`) and need to **sign or send** across CAIP-2 scopes with `invokeMethod`. For the single-chain `createEVMClient` / `createSolanaClient` paths, use the `sign-message` and `send-transaction` skills instead. + +| Ecosystem | Reference | +|-----------|-----------| +| EVM scopes (`eth_sendTransaction`, `personal_sign`, `eth_signTypedData_v4`) | [`references/evm.md`](references/evm.md) | +| Solana scopes (`signTransaction`, `signAndSendTransaction`, `signMessage`) | [`references/solana.md`](references/solana.md) | + +Follow the `metamask-connect-conventions` skill, especially the multichain session-lifecycle guardrails. diff --git a/domains/metamask-connect/skills/send-evm-transaction/skill.md b/domains/metamask-connect/skills/send-transaction/references/evm.md similarity index 96% rename from domains/metamask-connect/skills/send-evm-transaction/skill.md rename to domains/metamask-connect/skills/send-transaction/references/evm.md index 9e44e7c..ba98d34 100644 --- a/domains/metamask-connect/skills/send-evm-transaction/skill.md +++ b/domains/metamask-connect/skills/send-transaction/references/evm.md @@ -1,8 +1,3 @@ ---- -name: send-evm-transaction -description: Send ETH and contract transactions with MetaMask using eth_sendTransaction via the EIP-1193 provider, gas estimation, receipt polling, and the connectWith shortcut -maturity: stable ---- # Send EVM Transactions with MetaMask Connect ## When to use diff --git a/domains/metamask-connect/skills/send-solana-transaction/skill.md b/domains/metamask-connect/skills/send-transaction/references/solana.md similarity index 96% rename from domains/metamask-connect/skills/send-solana-transaction/skill.md rename to domains/metamask-connect/skills/send-transaction/references/solana.md index 8a51b72..1a03023 100644 --- a/domains/metamask-connect/skills/send-solana-transaction/skill.md +++ b/domains/metamask-connect/skills/send-transaction/references/solana.md @@ -1,8 +1,3 @@ ---- -name: send-solana-transaction -description: Build and send a Solana transaction using MetaMask Connect. Covers both the React wallet-adapter approach (sendTransaction) and the vanilla browser approach (signAndSendTransaction wallet-standard feature). -maturity: stable ---- # Send Solana Transaction with MetaMask ## When to use @@ -47,7 +42,7 @@ transaction.feePayer = senderPubkey; ### Step 2a: Send with React wallet-adapter (useWallet) -**Prerequisites:** `createSolanaClient` has been awaited before rendering, `WalletProvider` is configured with `wallets={[]}`, and the user is connected. See the `setup-solana-react-app` skill. +**Prerequisites:** `createSolanaClient` has been awaited before rendering, `WalletProvider` is configured with `wallets={[]}`, and the user is connected. See the `setup-app` skill (`references/solana-react.md`). ```tsx import { useWallet, useConnection } from '@solana/wallet-adapter-react'; @@ -102,7 +97,7 @@ function SendTransactionButton() { ### Step 2b: Send with vanilla browser (wallet-standard feature) -**Prerequisites:** `createSolanaClient` has been called and the wallet is connected via `standard:connect`. See the `setup-solana-browser-app` skill. +**Prerequisites:** `createSolanaClient` has been called and the wallet is connected via `standard:connect`. See the `setup-app` skill (`references/solana-browser.md`). ```typescript import { createSolanaClient } from '@metamask/connect-solana'; diff --git a/domains/metamask-connect/skills/send-transaction/skill.md b/domains/metamask-connect/skills/send-transaction/skill.md new file mode 100644 index 0000000..fdea081 --- /dev/null +++ b/domains/metamask-connect/skills/send-transaction/skill.md @@ -0,0 +1,17 @@ +--- +name: send-transaction +description: Send transactions with MetaMask in a dApp — EVM (eth_sendTransaction with gas estimation and receipt polling, plus the connectWith shortcut) and Solana (React wallet-adapter sendTransaction or vanilla signAndSendTransaction). Use when submitting on-chain transactions. Routes to per-chain references. For sending through the multichain client's invokeMethod, see the multichain-operations skill. +maturity: stable +--- +# Send Transactions with MetaMask Connect + +## When to use + +Use this to **submit an on-chain transaction** with a directly-created EVM or Solana client. If you set up the **multichain** client (`createMultichainClient`), send via `invokeMethod` instead — see the `multichain-operations` skill. + +| Chain | Reference | +|-------|-----------| +| EVM (`eth_sendTransaction`, gas, receipts, `connectWith`) | [`references/evm.md`](references/evm.md) | +| Solana (`sendTransaction` / `signAndSendTransaction`) | [`references/solana.md`](references/solana.md) | + +Follow the `metamask-connect-conventions` skill for provider/error-handling guardrails. diff --git a/domains/metamask-connect/skills/setup-evm-browser-app/skill.md b/domains/metamask-connect/skills/setup-app/references/evm-browser.md similarity index 97% rename from domains/metamask-connect/skills/setup-evm-browser-app/skill.md rename to domains/metamask-connect/skills/setup-app/references/evm-browser.md index f2a7256..ba274aa 100644 --- a/domains/metamask-connect/skills/setup-evm-browser-app/skill.md +++ b/domains/metamask-connect/skills/setup-app/references/evm-browser.md @@ -1,8 +1,3 @@ ---- -name: setup-evm-browser-app -description: Scaffold a vanilla JS/TS browser app with MetaMask EVM integration using createEVMClient, EIP-1193 provider event listeners, RPC methods, and chain switching with chainConfiguration fallback -maturity: stable ---- # Setup EVM Browser App with MetaMask Connect ## When to use diff --git a/domains/metamask-connect/skills/setup-evm-react-native-app/skill.md b/domains/metamask-connect/skills/setup-app/references/evm-react-native.md similarity index 97% rename from domains/metamask-connect/skills/setup-evm-react-native-app/skill.md rename to domains/metamask-connect/skills/setup-app/references/evm-react-native.md index 805825f..319183b 100644 --- a/domains/metamask-connect/skills/setup-evm-react-native-app/skill.md +++ b/domains/metamask-connect/skills/setup-app/references/evm-react-native.md @@ -1,8 +1,3 @@ ---- -name: setup-evm-react-native-app -description: Scaffold a React Native app with MetaMask EVM integration including required polyfills, metro.config.js shims, import order constraints, mobile deeplinks, and a full component example -maturity: stable ---- # Setup EVM React Native App with MetaMask Connect ## When to use diff --git a/domains/metamask-connect/skills/setup-evm-react-app/skill.md b/domains/metamask-connect/skills/setup-app/references/evm-react.md similarity index 97% rename from domains/metamask-connect/skills/setup-evm-react-app/skill.md rename to domains/metamask-connect/skills/setup-app/references/evm-react.md index 8472802..3e5239f 100644 --- a/domains/metamask-connect/skills/setup-evm-react-app/skill.md +++ b/domains/metamask-connect/skills/setup-app/references/evm-react.md @@ -1,8 +1,3 @@ ---- -name: setup-evm-react-app -description: Scaffold a React app with MetaMask EVM integration using createEVMClient, useState/useEffect/useRef patterns, provider.request calls, chain switching, and error handling -maturity: stable ---- # Setup EVM React App with MetaMask Connect ## When to use diff --git a/domains/metamask-connect/skills/setup-multichain-app/skill.md b/domains/metamask-connect/skills/setup-app/references/multichain.md similarity index 97% rename from domains/metamask-connect/skills/setup-multichain-app/skill.md rename to domains/metamask-connect/skills/setup-app/references/multichain.md index 6f4df71..13bfe81 100644 --- a/domains/metamask-connect/skills/setup-multichain-app/skill.md +++ b/domains/metamask-connect/skills/setup-app/references/multichain.md @@ -1,8 +1,3 @@ ---- -name: setup-multichain-app -description: Set up a multichain app using createMultichainClient from @metamask/connect-multichain. Covers EVM + Solana scopes, invokeMethod for both ecosystems, session events, headless mode, getInfuraRpcUrls, selective disconnect, and singleton behavior. -maturity: stable ---- # Setup Multichain App with MetaMask ## When to use diff --git a/domains/metamask-connect/skills/setup-solana-browser-app/skill.md b/domains/metamask-connect/skills/setup-app/references/solana-browser.md similarity index 97% rename from domains/metamask-connect/skills/setup-solana-browser-app/skill.md rename to domains/metamask-connect/skills/setup-app/references/solana-browser.md index 0fc1a6c..0b8a2e9 100644 --- a/domains/metamask-connect/skills/setup-solana-browser-app/skill.md +++ b/domains/metamask-connect/skills/setup-app/references/solana-browser.md @@ -1,8 +1,3 @@ ---- -name: setup-solana-browser-app -description: Set up a vanilla browser (non-React) app with @metamask/connect-solana using wallet-standard features directly. Use when integrating MetaMask Solana without a framework or wallet adapter library. -maturity: stable ---- # Setup Solana Browser App with MetaMask ## When to use diff --git a/domains/metamask-connect/skills/setup-solana-react-native-app/skill.md b/domains/metamask-connect/skills/setup-app/references/solana-react-native.md similarity index 97% rename from domains/metamask-connect/skills/setup-solana-react-native-app/skill.md rename to domains/metamask-connect/skills/setup-app/references/solana-react-native.md index a7bde3c..98a3832 100644 --- a/domains/metamask-connect/skills/setup-solana-react-native-app/skill.md +++ b/domains/metamask-connect/skills/setup-app/references/solana-react-native.md @@ -1,8 +1,3 @@ ---- -name: setup-solana-react-native-app -description: Set up a React Native app with @metamask/connect-solana using polyfills and multichain invokeMethod for Solana operations. Use when building Solana support in React Native where wallet-adapter is not available. -maturity: stable ---- # Setup Solana React Native App with MetaMask ## When to use diff --git a/domains/metamask-connect/skills/setup-solana-react-app/skill.md b/domains/metamask-connect/skills/setup-app/references/solana-react.md similarity index 96% rename from domains/metamask-connect/skills/setup-solana-react-app/skill.md rename to domains/metamask-connect/skills/setup-app/references/solana-react.md index 530729e..5eedcda 100644 --- a/domains/metamask-connect/skills/setup-solana-react-app/skill.md +++ b/domains/metamask-connect/skills/setup-app/references/solana-react.md @@ -1,8 +1,3 @@ ---- -name: setup-solana-react-app -description: Set up a React app with @metamask/connect-solana and the Solana wallet adapter. Use when integrating MetaMask with Solana in React, configuring WalletProvider, or building connect/sign/send flows with useWallet. -maturity: stable ---- # Setup Solana React App with MetaMask ## When to use diff --git a/domains/metamask-connect/skills/setup-wagmi-metamask-connector/skill.md b/domains/metamask-connect/skills/setup-app/references/wagmi-connector.md similarity index 98% rename from domains/metamask-connect/skills/setup-wagmi-metamask-connector/skill.md rename to domains/metamask-connect/skills/setup-app/references/wagmi-connector.md index eaf9936..549fe37 100644 --- a/domains/metamask-connect/skills/setup-wagmi-metamask-connector/skill.md +++ b/domains/metamask-connect/skills/setup-app/references/wagmi-connector.md @@ -1,8 +1,3 @@ ---- -name: setup-wagmi-metamask-connector -description: Set up a wagmi app with the MetaMask Connect EVM connector using @metamask/connect-evm -maturity: stable ---- # Set Up Wagmi with MetaMask Connect EVM Connector ## When to use diff --git a/domains/metamask-connect/skills/setup-wagmi-app/skill.md b/domains/metamask-connect/skills/setup-app/references/wagmi.md similarity index 96% rename from domains/metamask-connect/skills/setup-wagmi-app/skill.md rename to domains/metamask-connect/skills/setup-app/references/wagmi.md index 80bb4c5..1fb12e4 100644 --- a/domains/metamask-connect/skills/setup-wagmi-app/skill.md +++ b/domains/metamask-connect/skills/setup-app/references/wagmi.md @@ -1,8 +1,3 @@ ---- -name: setup-wagmi-app -description: Set up a React or React Native app with wagmi and a MetaMask connector implementation that matches your installed @metamask/connect-evm version. Use when integrating MetaMask with wagmi, configuring the metaMask() connector, or building connect/sign/send flows. -maturity: stable ---- # Setup wagmi App with MetaMask ## When to use diff --git a/domains/metamask-connect/skills/setup-app/skill.md b/domains/metamask-connect/skills/setup-app/skill.md new file mode 100644 index 0000000..981a974 --- /dev/null +++ b/domains/metamask-connect/skills/setup-app/skill.md @@ -0,0 +1,35 @@ +--- +name: setup-app +description: Scaffold a dApp that integrates MetaMask via the MetaMask Connect SDK — EVM, Solana, or both (multichain) — across vanilla browser JS/TS, React, and React Native, or through wagmi. Use when starting a new MetaMask integration or wiring connect/disconnect and provider setup. Routes to per-stack references covering createEVMClient, createSolanaClient, createMultichainClient, and the wagmi metaMask() connector, including EIP-1193 provider events, chain switching with chainConfiguration, Solana wallet-standard, React Native polyfills, and metro config. +maturity: stable +--- +# Set Up a MetaMask Connect dApp + +## When to use + +Use this when you are **starting or wiring up** a dApp's MetaMask integration: choosing a Connect SDK package, creating the client, and getting connect/disconnect + the provider working. For signing, sending transactions, or migrating, see the `sign-message`, `send-transaction`, `multichain-operations`, and `migrate` skills. + +## 1. Choose your client + +| You need | Package | Then read | +|----------|---------|-----------| +| EVM only | `@metamask/connect-evm` (`createEVMClient`) | an `evm-*` reference below | +| Solana only | `@metamask/connect-solana` (`createSolanaClient`) | a `solana-*` reference below | +| EVM **and** Solana in one session | `@metamask/connect-multichain` (`createMultichainClient`) | `references/multichain.md` | +| You already use wagmi | wagmi `metaMask()` connector | `references/wagmi.md` | + +## 2. Read the reference for your stack + +| Building | Reference | +|----------|-----------| +| EVM dApp — vanilla browser JS/TS | [`references/evm-browser.md`](references/evm-browser.md) | +| EVM dApp — React | [`references/evm-react.md`](references/evm-react.md) | +| EVM dApp — React Native | [`references/evm-react-native.md`](references/evm-react-native.md) | +| Solana dApp — vanilla browser | [`references/solana-browser.md`](references/solana-browser.md) | +| Solana dApp — React | [`references/solana-react.md`](references/solana-react.md) | +| Solana dApp — React Native | [`references/solana-react-native.md`](references/solana-react-native.md) | +| EVM + Solana (multichain) | [`references/multichain.md`](references/multichain.md) | +| wagmi app | [`references/wagmi.md`](references/wagmi.md) | +| wagmi + the connect-evm connector | [`references/wagmi-connector.md`](references/wagmi-connector.md) | + +Always apply the `metamask-connect-conventions` skill (hex chain IDs, singleton behavior, EIP-1193 events, Solana constraints, React Native polyfills) alongside whichever reference you follow. diff --git a/domains/metamask-connect/skills/sign-evm-message/skill.md b/domains/metamask-connect/skills/sign-message/references/evm.md similarity index 97% rename from domains/metamask-connect/skills/sign-evm-message/skill.md rename to domains/metamask-connect/skills/sign-message/references/evm.md index 751e996..c1f0734 100644 --- a/domains/metamask-connect/skills/sign-evm-message/skill.md +++ b/domains/metamask-connect/skills/sign-message/references/evm.md @@ -1,8 +1,3 @@ ---- -name: sign-evm-message -description: Sign messages with MetaMask using personal_sign and eth_signTypedData_v4 via the EIP-1193 provider, plus the connectAndSign shortcut -maturity: stable ---- # Sign EVM Messages with MetaMask Connect ## When to use diff --git a/domains/metamask-connect/skills/sign-solana-message/skill.md b/domains/metamask-connect/skills/sign-message/references/solana.md similarity index 92% rename from domains/metamask-connect/skills/sign-solana-message/skill.md rename to domains/metamask-connect/skills/sign-message/references/solana.md index f326b93..414085b 100644 --- a/domains/metamask-connect/skills/sign-solana-message/skill.md +++ b/domains/metamask-connect/skills/sign-message/references/solana.md @@ -1,8 +1,3 @@ ---- -name: sign-solana-message -description: Sign an arbitrary message on Solana using MetaMask Connect. Covers both the React wallet-adapter approach (useWallet) and the vanilla browser approach (wallet-standard features). -maturity: stable ---- # Sign Solana Message with MetaMask ## When to use @@ -25,7 +20,7 @@ const message = new TextEncoder().encode('Sign this message to verify your ident ### Step 2a: Sign with React wallet-adapter (useWallet) -**Prerequisites:** `createSolanaClient` has been awaited before rendering, `WalletProvider` is configured with `wallets={[]}`, and the user is connected. See the `setup-solana-react-app` skill. +**Prerequisites:** `createSolanaClient` has been awaited before rendering, `WalletProvider` is configured with `wallets={[]}`, and the user is connected. See the `setup-app` skill (`references/solana-react.md`). ```tsx import { useWallet } from '@solana/wallet-adapter-react'; @@ -64,7 +59,7 @@ function SignMessageButton() { ### Step 2b: Sign with vanilla browser (wallet-standard feature) -**Prerequisites:** `createSolanaClient` has been called and the wallet is connected via `standard:connect`. See the `setup-solana-browser-app` skill. +**Prerequisites:** `createSolanaClient` has been called and the wallet is connected via `standard:connect`. See the `setup-app` skill (`references/solana-browser.md`). ```typescript import { createSolanaClient } from '@metamask/connect-solana'; diff --git a/domains/metamask-connect/skills/sign-message/skill.md b/domains/metamask-connect/skills/sign-message/skill.md new file mode 100644 index 0000000..a1cd8a5 --- /dev/null +++ b/domains/metamask-connect/skills/sign-message/skill.md @@ -0,0 +1,17 @@ +--- +name: sign-message +description: Sign arbitrary messages with MetaMask in a dApp — EVM (personal_sign, eth_signTypedData_v4, plus the connectAndSign shortcut) and Solana (wallet-standard signMessage, via React wallet-adapter or vanilla browser). Use when adding message signing or wallet authentication such as Sign-In With Ethereum or nonce signing. Routes to per-chain references. For signing through the multichain client's invokeMethod, see the multichain-operations skill. +maturity: stable +--- +# Sign Messages with MetaMask Connect + +## When to use + +Use this to **sign an arbitrary message** (authentication, Sign-In With Ethereum, nonce signing) with a directly-created EVM or Solana client. If you set up the **multichain** client (`createMultichainClient`), sign via `invokeMethod` instead — see the `multichain-operations` skill. + +| Chain | Reference | +|-------|-----------| +| EVM (`personal_sign`, `eth_signTypedData_v4`, `connectAndSign`) | [`references/evm.md`](references/evm.md) | +| Solana (wallet-standard `signMessage`) | [`references/solana.md`](references/solana.md) | + +Follow the `metamask-connect-conventions` skill for provider/error-handling guardrails. From bf00219a6ee1b4801c9537d98ff621b204024a98 Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Mon, 15 Jun 2026 13:22:20 -0500 Subject: [PATCH 5/6] chore: drop individual codeowner from metamask-connect domain --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0355d82..ec52f1e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,4 +2,4 @@ # ADR-58/recipe skills are experimental but owned by Perps during rollout. /domains/agentic/ @MetaMask/perps # MetaMask Connect SDK skills (dApp integration) owned by Wallet Integrations + Mobile Platform. -/domains/metamask-connect/ @MetaMask/wallet-integrations @MetaMask/mobile-platform @adonesky1 +/domains/metamask-connect/ @MetaMask/wallet-integrations @MetaMask/mobile-platform From efd98c490efbbb629b45821bc061923b16352eef Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Mon, 15 Jun 2026 13:30:42 -0500 Subject: [PATCH 6/6] refactor(metamask-connect): fold into a single web3-tools skill Mirror smart-accounts-kit: collapse the dedicated metamask-connect domain into one web3-tools/skills/metamask-connect skill with a routing skill.md, a references/ knowledge base (conventions, troubleshooting), and a workflows/ recipe library (per-stack setup, sign/send for EVM + Solana, multichain invokeMethod, migration). Updates CODEOWNERS, README, CHANGELOG. --- .github/CODEOWNERS | 4 +- CHANGELOG.md | 2 +- README.md | 3 +- .../metamask-connect/skills/migrate/skill.md | 17 ---- .../skills/multichain-operations/skill.md | 17 ---- .../skills/send-transaction/skill.md | 17 ---- .../skills/setup-app/skill.md | 35 ------- .../skills/sign-message/skill.md | 17 ---- .../references/conventions.md} | 5 - .../references/troubleshooting.md} | 5 - .../skills/metamask-connect/skill.md | 93 +++++++++++++++++++ .../workflows/migrate-from-sdk.md} | 2 +- .../workflows/migrate-wagmi-connector.md} | 0 .../workflows/multichain-evm-operations.md} | 0 .../multichain-solana-operations.md} | 0 .../workflows/send-evm-transaction.md} | 0 .../workflows/send-solana-transaction.md} | 4 +- .../workflows/setup-evm-browser.md} | 0 .../workflows/setup-evm-react-native.md} | 0 .../workflows/setup-evm-react.md} | 0 .../workflows/setup-multichain.md} | 0 .../workflows/setup-solana-browser.md} | 0 .../workflows/setup-solana-react-native.md} | 0 .../workflows/setup-solana-react.md} | 0 .../workflows/setup-wagmi-connector.md} | 0 .../workflows/setup-wagmi.md} | 0 .../workflows/sign-evm-message.md} | 0 .../workflows/sign-solana-message.md} | 4 +- 28 files changed, 102 insertions(+), 123 deletions(-) delete mode 100644 domains/metamask-connect/skills/migrate/skill.md delete mode 100644 domains/metamask-connect/skills/multichain-operations/skill.md delete mode 100644 domains/metamask-connect/skills/send-transaction/skill.md delete mode 100644 domains/metamask-connect/skills/setup-app/skill.md delete mode 100644 domains/metamask-connect/skills/sign-message/skill.md rename domains/{metamask-connect/skills/metamask-connect-conventions/skill.md => web3-tools/skills/metamask-connect/references/conventions.md} (98%) rename domains/{metamask-connect/skills/troubleshoot-connection/skill.md => web3-tools/skills/metamask-connect/references/troubleshooting.md} (99%) create mode 100644 domains/web3-tools/skills/metamask-connect/skill.md rename domains/{metamask-connect/skills/migrate/references/from-sdk.md => web3-tools/skills/metamask-connect/workflows/migrate-from-sdk.md} (99%) rename domains/{metamask-connect/skills/migrate/references/wagmi-connector.md => web3-tools/skills/metamask-connect/workflows/migrate-wagmi-connector.md} (100%) rename domains/{metamask-connect/skills/multichain-operations/references/evm.md => web3-tools/skills/metamask-connect/workflows/multichain-evm-operations.md} (100%) rename domains/{metamask-connect/skills/multichain-operations/references/solana.md => web3-tools/skills/metamask-connect/workflows/multichain-solana-operations.md} (100%) rename domains/{metamask-connect/skills/send-transaction/references/evm.md => web3-tools/skills/metamask-connect/workflows/send-evm-transaction.md} (100%) rename domains/{metamask-connect/skills/send-transaction/references/solana.md => web3-tools/skills/metamask-connect/workflows/send-solana-transaction.md} (98%) rename domains/{metamask-connect/skills/setup-app/references/evm-browser.md => web3-tools/skills/metamask-connect/workflows/setup-evm-browser.md} (100%) rename domains/{metamask-connect/skills/setup-app/references/evm-react-native.md => web3-tools/skills/metamask-connect/workflows/setup-evm-react-native.md} (100%) rename domains/{metamask-connect/skills/setup-app/references/evm-react.md => web3-tools/skills/metamask-connect/workflows/setup-evm-react.md} (100%) rename domains/{metamask-connect/skills/setup-app/references/multichain.md => web3-tools/skills/metamask-connect/workflows/setup-multichain.md} (100%) rename domains/{metamask-connect/skills/setup-app/references/solana-browser.md => web3-tools/skills/metamask-connect/workflows/setup-solana-browser.md} (100%) rename domains/{metamask-connect/skills/setup-app/references/solana-react-native.md => web3-tools/skills/metamask-connect/workflows/setup-solana-react-native.md} (100%) rename domains/{metamask-connect/skills/setup-app/references/solana-react.md => web3-tools/skills/metamask-connect/workflows/setup-solana-react.md} (100%) rename domains/{metamask-connect/skills/setup-app/references/wagmi-connector.md => web3-tools/skills/metamask-connect/workflows/setup-wagmi-connector.md} (100%) rename domains/{metamask-connect/skills/setup-app/references/wagmi.md => web3-tools/skills/metamask-connect/workflows/setup-wagmi.md} (100%) rename domains/{metamask-connect/skills/sign-message/references/evm.md => web3-tools/skills/metamask-connect/workflows/sign-evm-message.md} (100%) rename domains/{metamask-connect/skills/sign-message/references/solana.md => web3-tools/skills/metamask-connect/workflows/sign-solana-message.md} (96%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ec52f1e..ec2fabd 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,5 +1,5 @@ # GitHub code ownership for MetaMask skills. # ADR-58/recipe skills are experimental but owned by Perps during rollout. /domains/agentic/ @MetaMask/perps -# MetaMask Connect SDK skills (dApp integration) owned by Wallet Integrations + Mobile Platform. -/domains/metamask-connect/ @MetaMask/wallet-integrations @MetaMask/mobile-platform +# MetaMask Connect SDK skill (dApp integration) owned by Wallet Integrations + Mobile Platform. +/domains/web3-tools/skills/metamask-connect/ @MetaMask/wallet-integrations @MetaMask/mobile-platform diff --git a/CHANGELOG.md b/CHANGELOG.md index fa8c989..718306b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `metamask-connect` domain for building dApps with the MetaMask Connect SDK (`@metamask/connect-evm`, `@metamask/connect-multichain`, `@metamask/connect-solana`) and the wagmi `metaMask()` connector. Organized with progressive disclosure as `setup-app`, `sign-message`, `send-transaction`, `multichain-operations`, and `migrate` (each routing into per-stack `references/`), plus `troubleshoot-connection` and a `metamask-connect-conventions` guardrails skill +- Add `metamask-connect` skill to the `web3-tools` domain for building dApps with the MetaMask Connect SDK (`@metamask/connect-evm`, `@metamask/connect-multichain`, `@metamask/connect-solana`) and the wagmi `metaMask()` connector. A single progressive-disclosure skill (mirroring `smart-accounts-kit`): the routing `skill.md` points into `references/` (always-on `conventions`, `troubleshooting`) and `workflows/` (per-stack setup, sign/send for EVM + Solana, multichain `invokeMethod`, and migration from `@metamask/sdk`) ## [0.1.0] diff --git a/README.md b/README.md index 64d4ac8..843eb3f 100644 --- a/README.md +++ b/README.md @@ -138,8 +138,7 @@ tools/ | Domain | Audience | Examples | | -------------- | ----------------- | ------------------------------------------- | -| `web3-tools` | dApp builders | `gator-cli`, `smart-accounts-kit`, `oh-my-opencode` | -| `metamask-connect` | dApp builders | EVM/Solana/multichain setup, signing, wagmi, React Native | +| `web3-tools` | dApp builders | `gator-cli`, `smart-accounts-kit`, `metamask-connect` | | `coding` | MM product eng | Coding guidelines, controller patterns | | `agentic` | MM product eng | Experimental recipe workflows and runtime proof tools | | `general` | All agents | `codex`, `gemini` CLI usage guides | diff --git a/domains/metamask-connect/skills/migrate/skill.md b/domains/metamask-connect/skills/migrate/skill.md deleted file mode 100644 index aced81b..0000000 --- a/domains/metamask-connect/skills/migrate/skill.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -name: migrate -description: Migrate an existing MetaMask integration to the MetaMask Connect SDK — from @metamask/sdk to @metamask/connect-evm / -multichain / -solana with step-by-step package, API, and config changes, or a wagmi app to the new connect-evm metaMask() connector. Routes to per-path references. -maturity: stable ---- -# Migrate to the MetaMask Connect SDK - -## When to use - -Use this when **moving an existing app** onto the MetaMask Connect SDK. - -| Migrating from | Reference | -|----------------|-----------| -| `@metamask/sdk` → `@metamask/connect-*` | [`references/from-sdk.md`](references/from-sdk.md) | -| wagmi app → the new `@metamask/connect-evm` connector | [`references/wagmi-connector.md`](references/wagmi-connector.md) | - -After migrating, follow the `metamask-connect-conventions` skill to catch behavior differences (singleton behavior, event payloads, hex chain IDs). diff --git a/domains/metamask-connect/skills/multichain-operations/skill.md b/domains/metamask-connect/skills/multichain-operations/skill.md deleted file mode 100644 index b11a7a9..0000000 --- a/domains/metamask-connect/skills/multichain-operations/skill.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -name: multichain-operations -description: Sign and send transactions and messages through the MetaMask Connect multichain client's invokeMethod — EVM (eth_sendTransaction, personal_sign, eth_signTypedData_v4) and Solana (signTransaction, signAndSendTransaction, signMessage). Use after createMultichainClient when performing operations across CAIP-2 scopes, including building Solana transactions with @solana/web3.js, base64 encoding, mainnet/devnet scope selection, RPC routing for read vs sign, and selective disconnect. Routes to per-ecosystem references. -maturity: stable ---- -# Operations via the MetaMask Connect Multichain Client - -## When to use - -Use this when you created a **multichain** client (`createMultichainClient`, see the `setup-app` skill → `references/multichain.md`) and need to **sign or send** across CAIP-2 scopes with `invokeMethod`. For the single-chain `createEVMClient` / `createSolanaClient` paths, use the `sign-message` and `send-transaction` skills instead. - -| Ecosystem | Reference | -|-----------|-----------| -| EVM scopes (`eth_sendTransaction`, `personal_sign`, `eth_signTypedData_v4`) | [`references/evm.md`](references/evm.md) | -| Solana scopes (`signTransaction`, `signAndSendTransaction`, `signMessage`) | [`references/solana.md`](references/solana.md) | - -Follow the `metamask-connect-conventions` skill, especially the multichain session-lifecycle guardrails. diff --git a/domains/metamask-connect/skills/send-transaction/skill.md b/domains/metamask-connect/skills/send-transaction/skill.md deleted file mode 100644 index fdea081..0000000 --- a/domains/metamask-connect/skills/send-transaction/skill.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -name: send-transaction -description: Send transactions with MetaMask in a dApp — EVM (eth_sendTransaction with gas estimation and receipt polling, plus the connectWith shortcut) and Solana (React wallet-adapter sendTransaction or vanilla signAndSendTransaction). Use when submitting on-chain transactions. Routes to per-chain references. For sending through the multichain client's invokeMethod, see the multichain-operations skill. -maturity: stable ---- -# Send Transactions with MetaMask Connect - -## When to use - -Use this to **submit an on-chain transaction** with a directly-created EVM or Solana client. If you set up the **multichain** client (`createMultichainClient`), send via `invokeMethod` instead — see the `multichain-operations` skill. - -| Chain | Reference | -|-------|-----------| -| EVM (`eth_sendTransaction`, gas, receipts, `connectWith`) | [`references/evm.md`](references/evm.md) | -| Solana (`sendTransaction` / `signAndSendTransaction`) | [`references/solana.md`](references/solana.md) | - -Follow the `metamask-connect-conventions` skill for provider/error-handling guardrails. diff --git a/domains/metamask-connect/skills/setup-app/skill.md b/domains/metamask-connect/skills/setup-app/skill.md deleted file mode 100644 index 981a974..0000000 --- a/domains/metamask-connect/skills/setup-app/skill.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -name: setup-app -description: Scaffold a dApp that integrates MetaMask via the MetaMask Connect SDK — EVM, Solana, or both (multichain) — across vanilla browser JS/TS, React, and React Native, or through wagmi. Use when starting a new MetaMask integration or wiring connect/disconnect and provider setup. Routes to per-stack references covering createEVMClient, createSolanaClient, createMultichainClient, and the wagmi metaMask() connector, including EIP-1193 provider events, chain switching with chainConfiguration, Solana wallet-standard, React Native polyfills, and metro config. -maturity: stable ---- -# Set Up a MetaMask Connect dApp - -## When to use - -Use this when you are **starting or wiring up** a dApp's MetaMask integration: choosing a Connect SDK package, creating the client, and getting connect/disconnect + the provider working. For signing, sending transactions, or migrating, see the `sign-message`, `send-transaction`, `multichain-operations`, and `migrate` skills. - -## 1. Choose your client - -| You need | Package | Then read | -|----------|---------|-----------| -| EVM only | `@metamask/connect-evm` (`createEVMClient`) | an `evm-*` reference below | -| Solana only | `@metamask/connect-solana` (`createSolanaClient`) | a `solana-*` reference below | -| EVM **and** Solana in one session | `@metamask/connect-multichain` (`createMultichainClient`) | `references/multichain.md` | -| You already use wagmi | wagmi `metaMask()` connector | `references/wagmi.md` | - -## 2. Read the reference for your stack - -| Building | Reference | -|----------|-----------| -| EVM dApp — vanilla browser JS/TS | [`references/evm-browser.md`](references/evm-browser.md) | -| EVM dApp — React | [`references/evm-react.md`](references/evm-react.md) | -| EVM dApp — React Native | [`references/evm-react-native.md`](references/evm-react-native.md) | -| Solana dApp — vanilla browser | [`references/solana-browser.md`](references/solana-browser.md) | -| Solana dApp — React | [`references/solana-react.md`](references/solana-react.md) | -| Solana dApp — React Native | [`references/solana-react-native.md`](references/solana-react-native.md) | -| EVM + Solana (multichain) | [`references/multichain.md`](references/multichain.md) | -| wagmi app | [`references/wagmi.md`](references/wagmi.md) | -| wagmi + the connect-evm connector | [`references/wagmi-connector.md`](references/wagmi-connector.md) | - -Always apply the `metamask-connect-conventions` skill (hex chain IDs, singleton behavior, EIP-1193 events, Solana constraints, React Native polyfills) alongside whichever reference you follow. diff --git a/domains/metamask-connect/skills/sign-message/skill.md b/domains/metamask-connect/skills/sign-message/skill.md deleted file mode 100644 index a1cd8a5..0000000 --- a/domains/metamask-connect/skills/sign-message/skill.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -name: sign-message -description: Sign arbitrary messages with MetaMask in a dApp — EVM (personal_sign, eth_signTypedData_v4, plus the connectAndSign shortcut) and Solana (wallet-standard signMessage, via React wallet-adapter or vanilla browser). Use when adding message signing or wallet authentication such as Sign-In With Ethereum or nonce signing. Routes to per-chain references. For signing through the multichain client's invokeMethod, see the multichain-operations skill. -maturity: stable ---- -# Sign Messages with MetaMask Connect - -## When to use - -Use this to **sign an arbitrary message** (authentication, Sign-In With Ethereum, nonce signing) with a directly-created EVM or Solana client. If you set up the **multichain** client (`createMultichainClient`), sign via `invokeMethod` instead — see the `multichain-operations` skill. - -| Chain | Reference | -|-------|-----------| -| EVM (`personal_sign`, `eth_signTypedData_v4`, `connectAndSign`) | [`references/evm.md`](references/evm.md) | -| Solana (wallet-standard `signMessage`) | [`references/solana.md`](references/solana.md) | - -Follow the `metamask-connect-conventions` skill for provider/error-handling guardrails. diff --git a/domains/metamask-connect/skills/metamask-connect-conventions/skill.md b/domains/web3-tools/skills/metamask-connect/references/conventions.md similarity index 98% rename from domains/metamask-connect/skills/metamask-connect-conventions/skill.md rename to domains/web3-tools/skills/metamask-connect/references/conventions.md index 188fb77..bee9f25 100644 --- a/domains/metamask-connect/skills/metamask-connect-conventions/skill.md +++ b/domains/web3-tools/skills/metamask-connect/references/conventions.md @@ -1,8 +1,3 @@ ---- -name: metamask-connect-conventions -description: Core conventions, constraints, and common mistakes for the MetaMask Connect SDK across EVM, Solana, multichain, wagmi, and React Native. Consult before writing or reviewing any MetaMask Connect integration code — covers hex chain IDs, supportedNetworks validation, EIP-1193 provider events, multichain session lifecycle, Solana constraints, React Native polyfills, and testing patterns. -maturity: stable ---- # MetaMask Connect — Conventions & Guardrails Always-on guardrails for the MetaMask Connect SDK, distilled from the [MetaMask Connect Cursor plugin](https://github.com/MetaMask/metamask-connect-cursor-plugin) rules. Apply these whenever you generate or review MetaMask Connect (`@metamask/connect-evm` / `-multichain` / `-solana`) or wagmi `metaMask()` connector code. diff --git a/domains/metamask-connect/skills/troubleshoot-connection/skill.md b/domains/web3-tools/skills/metamask-connect/references/troubleshooting.md similarity index 99% rename from domains/metamask-connect/skills/troubleshoot-connection/skill.md rename to domains/web3-tools/skills/metamask-connect/references/troubleshooting.md index f217cee..ce879da 100644 --- a/domains/metamask-connect/skills/troubleshoot-connection/skill.md +++ b/domains/web3-tools/skills/metamask-connect/references/troubleshooting.md @@ -1,8 +1,3 @@ ---- -name: troubleshoot-connection -description: Diagnose and fix common MetaMask Connect SDK connection failures, transport issues, and runtime errors -maturity: stable ---- # Troubleshoot MetaMask Connect Issues ## When to use diff --git a/domains/web3-tools/skills/metamask-connect/skill.md b/domains/web3-tools/skills/metamask-connect/skill.md new file mode 100644 index 0000000..6a99282 --- /dev/null +++ b/domains/web3-tools/skills/metamask-connect/skill.md @@ -0,0 +1,93 @@ +--- +name: metamask-connect +description: Build dApps that integrate MetaMask via the MetaMask Connect SDK — EVM (@metamask/connect-evm), Solana (@metamask/connect-solana), and multichain (@metamask/connect-multichain), plus the wagmi metaMask() connector. Covers client setup across browser/React/React Native, connecting, signing messages, sending transactions, multichain invokeMethod across CAIP-2 scopes, migrating from @metamask/sdk, and troubleshooting connection/polyfill issues. +--- + +# MetaMask Connect SDK + +## When to use + +- You want to set up a dApp's MetaMask integration — EVM, Solana, or both (multichain) — in vanilla browser JS/TS, React, or React Native +- You want to connect/disconnect, manage the provider and session state, or switch chains +- You want to sign messages (`personal_sign`, `eth_signTypedData_v4`, Solana `signMessage`) — e.g. Sign-In With Ethereum or nonce auth +- You want to send transactions (`eth_sendTransaction`, Solana `sendTransaction` / `signAndSendTransaction`) +- You want to operate across chains through the multichain client's `invokeMethod` +- You want to use or migrate to the wagmi `metaMask()` connector +- You want to migrate an existing `@metamask/sdk` integration to the Connect SDK +- You need to diagnose connection failures, React Native polyfill errors, or QR/deeplink issues + +## Installation + +Pick the client for your integration: + +| You need | Package | Factory | +|----------|---------|---------| +| EVM only | `@metamask/connect-evm` | `createEVMClient` | +| Solana only | `@metamask/connect-solana` | `createSolanaClient` | +| EVM **and** Solana in one session | `@metamask/connect-multichain` | `createMultichainClient` | +| You already use wagmi | wagmi `metaMask()` connector (needs `@metamask/connect-evm` as a peer) | — | + +## Always-on conventions + +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. + +## Set up (choose your stack) + +| Building | Workflow | +|----------|----------| +| EVM dApp — vanilla browser JS/TS | [workflows/setup-evm-browser.md](workflows/setup-evm-browser.md) | +| EVM dApp — React | [workflows/setup-evm-react.md](workflows/setup-evm-react.md) | +| EVM dApp — React Native | [workflows/setup-evm-react-native.md](workflows/setup-evm-react-native.md) | +| Solana dApp — vanilla browser | [workflows/setup-solana-browser.md](workflows/setup-solana-browser.md) | +| Solana dApp — React | [workflows/setup-solana-react.md](workflows/setup-solana-react.md) | +| Solana dApp — React Native | [workflows/setup-solana-react-native.md](workflows/setup-solana-react-native.md) | +| EVM + Solana (multichain) | [workflows/setup-multichain.md](workflows/setup-multichain.md) | +| wagmi app | [workflows/setup-wagmi.md](workflows/setup-wagmi.md) | +| wagmi + the connect-evm connector | [workflows/setup-wagmi-connector.md](workflows/setup-wagmi-connector.md) | + +## Sign & send (single-chain clients) + +Use these with a directly-created EVM or Solana client. If you set up the **multichain** client, sign/send via `invokeMethod` instead — see the multichain workflows below. + +| Task | Workflow | +|------|----------| +| Sign — EVM (`personal_sign`, `eth_signTypedData_v4`, `connectAndSign`) | [workflows/sign-evm-message.md](workflows/sign-evm-message.md) | +| Sign — Solana (wallet-standard `signMessage`) | [workflows/sign-solana-message.md](workflows/sign-solana-message.md) | +| Send — EVM (`eth_sendTransaction`, gas, receipts, `connectWith`) | [workflows/send-evm-transaction.md](workflows/send-evm-transaction.md) | +| Send — Solana (`sendTransaction` / `signAndSendTransaction`) | [workflows/send-solana-transaction.md](workflows/send-solana-transaction.md) | + +## Multichain operations (`invokeMethod` across CAIP-2 scopes) + +Use these after `createMultichainClient` to sign or send across CAIP-2 scopes. + +| Ecosystem | Workflow | +|-----------|----------| +| EVM scopes (`eth_sendTransaction`, `personal_sign`, `eth_signTypedData_v4`) | [workflows/multichain-evm-operations.md](workflows/multichain-evm-operations.md) | +| Solana scopes (`signTransaction`, `signAndSendTransaction`, `signMessage`) | [workflows/multichain-solana-operations.md](workflows/multichain-solana-operations.md) | + +## Migrate + +| Migrating from | Workflow | +|----------------|----------| +| `@metamask/sdk` → `@metamask/connect-*` | [workflows/migrate-from-sdk.md](workflows/migrate-from-sdk.md) | +| wagmi app → the new `@metamask/connect-evm` connector | [workflows/migrate-wagmi-connector.md](workflows/migrate-wagmi-connector.md) | + +## Troubleshooting + +When a connection hangs/fails, a React Native app crashes on a missing polyfill, QR codes or deeplinks don't work, the Solana wallet adapter doesn't detect MetaMask, or a session is lost after reload — see [references/troubleshooting.md](references/troubleshooting.md) for a symptom → cause → fix index and a diagnostic checklist. + +## Important notes + +These are the highest-value guardrails; [references/conventions.md](references/conventions.md) has the full, source-verified set. + +- EVM chain IDs are **hex strings** (`'0x1'`, not `1` or `'1'`); CAIP-2 scopes use **decimal** (`eip155:1`). +- Every chain the dApp touches must be in `api.supportedNetworks` with a reachable RPC URL — the check runs in the provider's `request()` path, not in `connect()`. +- The multichain core is a **singleton** — create clients once at startup, never inside a React render. +- Handle EIP-1193 code `4001` (user rejected) and `-32002` (extension request pending) in `catch` blocks; multichain `invokeMethod` errors arrive wrapped in `RPCInvokeMethodErr` (original code on `rpcCode`). +- React Native needs polyfills (a `window` shim always; `Event`/`CustomEvent` only when also using wagmi; `react-native-get-random-values` as the first import) plus metro `extraNodeModules` shims (`stream` → `readable-stream`, the rest → empty stubs). + +## Resources + +- NPM: `@metamask/connect-evm`, `@metamask/connect-solana`, `@metamask/connect-multichain` +- Source plugin: https://github.com/MetaMask/metamask-connect-cursor-plugin +- Provenance: generated from that plugin's `skills/` and always-on `rules/`, source-verified against the published `@metamask/connect-*` packages. diff --git a/domains/metamask-connect/skills/migrate/references/from-sdk.md b/domains/web3-tools/skills/metamask-connect/workflows/migrate-from-sdk.md similarity index 99% rename from domains/metamask-connect/skills/migrate/references/from-sdk.md rename to domains/web3-tools/skills/metamask-connect/workflows/migrate-from-sdk.md index ca967ae..3868b66 100644 --- a/domains/metamask-connect/skills/migrate/references/from-sdk.md +++ b/domains/web3-tools/skills/metamask-connect/workflows/migrate-from-sdk.md @@ -310,7 +310,7 @@ Key differences: - The connect-evm-backed `metaMask()` connector ships in `wagmi/connectors` from wagmi 3.6 / `@wagmi/connectors` 8 — there is no `@metamask/connect-evm/wagmi` subpath; install `@metamask/connect-evm` at wagmi's declared peer range - Use `dapp` not `dappMetadata` - Connector ID is `'metaMaskSDK'` — find it with `connectors.find(c => c.id === 'metaMaskSDK')` -- Most wagmi hooks work unchanged, but note the wagmi v3 renames: `useConnect().connectors` → `useConnectors()`, `connectAsync` → `mutateAsync`, `useAccount` → `useConnection` (see `references/wagmi-connector.md`) +- Most wagmi hooks work unchanged, but note the wagmi v3 renames: `useConnect().connectors` → `useConnectors()`, `connectAsync` → `mutateAsync`, `useAccount` → `useConnection` (see [`migrate-wagmi-connector.md`](migrate-wagmi-connector.md)) --- diff --git a/domains/metamask-connect/skills/migrate/references/wagmi-connector.md b/domains/web3-tools/skills/metamask-connect/workflows/migrate-wagmi-connector.md similarity index 100% rename from domains/metamask-connect/skills/migrate/references/wagmi-connector.md rename to domains/web3-tools/skills/metamask-connect/workflows/migrate-wagmi-connector.md diff --git a/domains/metamask-connect/skills/multichain-operations/references/evm.md b/domains/web3-tools/skills/metamask-connect/workflows/multichain-evm-operations.md similarity index 100% rename from domains/metamask-connect/skills/multichain-operations/references/evm.md rename to domains/web3-tools/skills/metamask-connect/workflows/multichain-evm-operations.md diff --git a/domains/metamask-connect/skills/multichain-operations/references/solana.md b/domains/web3-tools/skills/metamask-connect/workflows/multichain-solana-operations.md similarity index 100% rename from domains/metamask-connect/skills/multichain-operations/references/solana.md rename to domains/web3-tools/skills/metamask-connect/workflows/multichain-solana-operations.md diff --git a/domains/metamask-connect/skills/send-transaction/references/evm.md b/domains/web3-tools/skills/metamask-connect/workflows/send-evm-transaction.md similarity index 100% rename from domains/metamask-connect/skills/send-transaction/references/evm.md rename to domains/web3-tools/skills/metamask-connect/workflows/send-evm-transaction.md diff --git a/domains/metamask-connect/skills/send-transaction/references/solana.md b/domains/web3-tools/skills/metamask-connect/workflows/send-solana-transaction.md similarity index 98% rename from domains/metamask-connect/skills/send-transaction/references/solana.md rename to domains/web3-tools/skills/metamask-connect/workflows/send-solana-transaction.md index 1a03023..ba25a6c 100644 --- a/domains/metamask-connect/skills/send-transaction/references/solana.md +++ b/domains/web3-tools/skills/metamask-connect/workflows/send-solana-transaction.md @@ -42,7 +42,7 @@ transaction.feePayer = senderPubkey; ### Step 2a: Send with React wallet-adapter (useWallet) -**Prerequisites:** `createSolanaClient` has been awaited before rendering, `WalletProvider` is configured with `wallets={[]}`, and the user is connected. See the `setup-app` skill (`references/solana-react.md`). +**Prerequisites:** `createSolanaClient` has been awaited before rendering, `WalletProvider` is configured with `wallets={[]}`, and the user is connected. See [`setup-solana-react.md`](setup-solana-react.md). ```tsx import { useWallet, useConnection } from '@solana/wallet-adapter-react'; @@ -97,7 +97,7 @@ function SendTransactionButton() { ### Step 2b: Send with vanilla browser (wallet-standard feature) -**Prerequisites:** `createSolanaClient` has been called and the wallet is connected via `standard:connect`. See the `setup-app` skill (`references/solana-browser.md`). +**Prerequisites:** `createSolanaClient` has been called and the wallet is connected via `standard:connect`. See [`setup-solana-browser.md`](setup-solana-browser.md). ```typescript import { createSolanaClient } from '@metamask/connect-solana'; diff --git a/domains/metamask-connect/skills/setup-app/references/evm-browser.md b/domains/web3-tools/skills/metamask-connect/workflows/setup-evm-browser.md similarity index 100% rename from domains/metamask-connect/skills/setup-app/references/evm-browser.md rename to domains/web3-tools/skills/metamask-connect/workflows/setup-evm-browser.md diff --git a/domains/metamask-connect/skills/setup-app/references/evm-react-native.md b/domains/web3-tools/skills/metamask-connect/workflows/setup-evm-react-native.md similarity index 100% rename from domains/metamask-connect/skills/setup-app/references/evm-react-native.md rename to domains/web3-tools/skills/metamask-connect/workflows/setup-evm-react-native.md diff --git a/domains/metamask-connect/skills/setup-app/references/evm-react.md b/domains/web3-tools/skills/metamask-connect/workflows/setup-evm-react.md similarity index 100% rename from domains/metamask-connect/skills/setup-app/references/evm-react.md rename to domains/web3-tools/skills/metamask-connect/workflows/setup-evm-react.md diff --git a/domains/metamask-connect/skills/setup-app/references/multichain.md b/domains/web3-tools/skills/metamask-connect/workflows/setup-multichain.md similarity index 100% rename from domains/metamask-connect/skills/setup-app/references/multichain.md rename to domains/web3-tools/skills/metamask-connect/workflows/setup-multichain.md diff --git a/domains/metamask-connect/skills/setup-app/references/solana-browser.md b/domains/web3-tools/skills/metamask-connect/workflows/setup-solana-browser.md similarity index 100% rename from domains/metamask-connect/skills/setup-app/references/solana-browser.md rename to domains/web3-tools/skills/metamask-connect/workflows/setup-solana-browser.md diff --git a/domains/metamask-connect/skills/setup-app/references/solana-react-native.md b/domains/web3-tools/skills/metamask-connect/workflows/setup-solana-react-native.md similarity index 100% rename from domains/metamask-connect/skills/setup-app/references/solana-react-native.md rename to domains/web3-tools/skills/metamask-connect/workflows/setup-solana-react-native.md diff --git a/domains/metamask-connect/skills/setup-app/references/solana-react.md b/domains/web3-tools/skills/metamask-connect/workflows/setup-solana-react.md similarity index 100% rename from domains/metamask-connect/skills/setup-app/references/solana-react.md rename to domains/web3-tools/skills/metamask-connect/workflows/setup-solana-react.md diff --git a/domains/metamask-connect/skills/setup-app/references/wagmi-connector.md b/domains/web3-tools/skills/metamask-connect/workflows/setup-wagmi-connector.md similarity index 100% rename from domains/metamask-connect/skills/setup-app/references/wagmi-connector.md rename to domains/web3-tools/skills/metamask-connect/workflows/setup-wagmi-connector.md diff --git a/domains/metamask-connect/skills/setup-app/references/wagmi.md b/domains/web3-tools/skills/metamask-connect/workflows/setup-wagmi.md similarity index 100% rename from domains/metamask-connect/skills/setup-app/references/wagmi.md rename to domains/web3-tools/skills/metamask-connect/workflows/setup-wagmi.md diff --git a/domains/metamask-connect/skills/sign-message/references/evm.md b/domains/web3-tools/skills/metamask-connect/workflows/sign-evm-message.md similarity index 100% rename from domains/metamask-connect/skills/sign-message/references/evm.md rename to domains/web3-tools/skills/metamask-connect/workflows/sign-evm-message.md diff --git a/domains/metamask-connect/skills/sign-message/references/solana.md b/domains/web3-tools/skills/metamask-connect/workflows/sign-solana-message.md similarity index 96% rename from domains/metamask-connect/skills/sign-message/references/solana.md rename to domains/web3-tools/skills/metamask-connect/workflows/sign-solana-message.md index 414085b..6bf0aec 100644 --- a/domains/metamask-connect/skills/sign-message/references/solana.md +++ b/domains/web3-tools/skills/metamask-connect/workflows/sign-solana-message.md @@ -20,7 +20,7 @@ const message = new TextEncoder().encode('Sign this message to verify your ident ### Step 2a: Sign with React wallet-adapter (useWallet) -**Prerequisites:** `createSolanaClient` has been awaited before rendering, `WalletProvider` is configured with `wallets={[]}`, and the user is connected. See the `setup-app` skill (`references/solana-react.md`). +**Prerequisites:** `createSolanaClient` has been awaited before rendering, `WalletProvider` is configured with `wallets={[]}`, and the user is connected. See [`setup-solana-react.md`](setup-solana-react.md). ```tsx import { useWallet } from '@solana/wallet-adapter-react'; @@ -59,7 +59,7 @@ function SignMessageButton() { ### Step 2b: Sign with vanilla browser (wallet-standard feature) -**Prerequisites:** `createSolanaClient` has been called and the wallet is connected via `standard:connect`. See the `setup-app` skill (`references/solana-browser.md`). +**Prerequisites:** `createSolanaClient` has been called and the wallet is connected via `standard:connect`. See [`setup-solana-browser.md`](setup-solana-browser.md). ```typescript import { createSolanaClient } from '@metamask/connect-solana';