Skip to content

Commit 7495c37

Browse files
committed
feat: add metamask-connect domain
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.
1 parent d23becf commit 7495c37

21 files changed

Lines changed: 5660 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- 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
13+
1014
## [0.1.0]
1115

1216
### Added

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ tools/
139139
| Domain | Audience | Examples |
140140
| -------------- | ----------------- | ------------------------------------------- |
141141
| `web3-tools` | dApp builders | `gator-cli`, `smart-accounts-kit`, `oh-my-opencode` |
142+
| `metamask-connect` | dApp builders | EVM/Solana/multichain setup, signing, wagmi, React Native |
142143
| `coding` | MM product eng | Coding guidelines, controller patterns |
143144
| `agentic` | MM product eng | Experimental recipe workflows and runtime proof tools |
144145
| `general` | All agents | `codex`, `gemini` CLI usage guides |

domains/metamask-connect/skills/metamask-connect-conventions/skill.md

Lines changed: 580 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 357 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,357 @@
1+
---
2+
name: migrate-from-sdk
3+
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
4+
maturity: stable
5+
---
6+
# Migrate from @metamask/sdk to @metamask/connect
7+
8+
## When to use
9+
10+
Use this skill when:
11+
- Migrating an existing dApp from `@metamask/sdk` or `@metamask/sdk-react` to the new `@metamask/connect-*` packages
12+
- Updating initialization code, provider access, or event handling for the new API
13+
- Converting a wagmi integration to use the new `metaMask()` connector
14+
- Adding multichain or Solana support during the migration
15+
16+
## Workflow
17+
18+
### Step 1: Replace packages
19+
20+
Remove the old packages and install the new ones:
21+
22+
```bash
23+
# Remove old
24+
npm uninstall @metamask/sdk @metamask/sdk-react
25+
26+
# Install new — pick the packages you need
27+
npm install @metamask/connect-evm
28+
npm install @metamask/connect-multichain
29+
npm install @metamask/connect-solana
30+
```
31+
32+
---
33+
34+
### Step 1b: React Native polyfills (if applicable)
35+
36+
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.
37+
38+
**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.
39+
40+
---
41+
42+
### Step 2: Update imports
43+
44+
**Old:**
45+
46+
```typescript
47+
import { MetaMaskSDK } from '@metamask/sdk';
48+
import { MetaMaskProvider, useSDK } from '@metamask/sdk-react';
49+
```
50+
51+
**New (EVM):**
52+
53+
```typescript
54+
import { createEVMClient, getInfuraRpcUrls } from '@metamask/connect-evm';
55+
```
56+
57+
**New (Multichain):**
58+
59+
```typescript
60+
import { createMultichainClient } from '@metamask/connect-multichain';
61+
```
62+
63+
**New (Solana):**
64+
65+
```typescript
66+
import { createSolanaClient } from '@metamask/connect-solana';
67+
```
68+
69+
**New (wagmi connector):**
70+
71+
```typescript
72+
// Requires wagmi >= 3.6 / @wagmi/connectors >= 8 (the connect-evm-backed
73+
// connector), with @metamask/connect-evm installed at wagmi's declared peer
74+
// range (currently ^1.3.0). On older wagmi, copy the reference connector
75+
// from connect-monorepo/integrations/wagmi/metamask-connector.ts.
76+
import { metaMask } from 'wagmi/connectors';
77+
```
78+
79+
---
80+
81+
### Step 3: Update initialization
82+
83+
**Old:**
84+
85+
```typescript
86+
const sdk = new MetaMaskSDK({
87+
dappMetadata: {
88+
name: 'My DApp',
89+
url: window.location.href,
90+
},
91+
infuraAPIKey: 'YOUR_INFURA_KEY',
92+
readonlyRPCMap: {
93+
'0x89': 'https://polygon-rpc.com',
94+
},
95+
headless: true,
96+
extensionOnly: false,
97+
openDeeplink: (link) => window.open(link, '_blank'),
98+
});
99+
await sdk.init();
100+
```
101+
102+
**New:**
103+
104+
```typescript
105+
const client = await createEVMClient({
106+
dapp: {
107+
name: 'My DApp',
108+
url: window.location.href,
109+
},
110+
api: {
111+
supportedNetworks: {
112+
...getInfuraRpcUrls({ infuraApiKey: 'YOUR_INFURA_KEY', chainIds: ['0x1', '0x89'] }),
113+
'0xa4b1': 'https://arb1.arbitrum.io/rpc',
114+
},
115+
},
116+
ui: {
117+
headless: true,
118+
preferExtension: false,
119+
},
120+
// `mobile` block only needed for React Native
121+
// mobile: {
122+
// preferredOpenLink: (link: string) => Linking.openURL(link),
123+
// },
124+
});
125+
```
126+
127+
**Key option mappings:**
128+
129+
| Old (`MetaMaskSDK`) | New (`createEVMClient`) | Notes |
130+
|---|---|---|
131+
| `dappMetadata` | `dapp` | Same shape: `{ name, url, iconUrl }` |
132+
| `dappMetadata.name` | `dapp.name` | Required |
133+
| `dappMetadata.url` | `dapp.url` | Optional |
134+
| `infuraAPIKey` | `api.supportedNetworks` via `getInfuraRpcUrls({ infuraApiKey: key })` | Helper generates URLs for all Infura-supported chains; optional `chainIds` to limit to specific chains |
135+
| `readonlyRPCMap` | `api.supportedNetworks` | Merge into the same object |
136+
| `headless` | `ui.headless` | Same behavior |
137+
| `extensionOnly` | `ui.preferExtension` | `true` prefers extension (default); not the same as "only" |
138+
| `openDeeplink` | `mobile.preferredOpenLink` | Same signature: `(deeplink: string) => void` |
139+
| `useDeeplink` | `mobile.useDeeplink` | Same behavior |
140+
| `timer` | Removed | No longer configurable |
141+
| `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.) |
142+
| `communicationServerUrl` | Removed | Managed internally |
143+
| `storage` | Removed | Managed internally |
144+
145+
---
146+
147+
### Step 4: Update connection flow
148+
149+
**Old:**
150+
151+
```typescript
152+
const accounts = await sdk.connect();
153+
const chainId = await sdk.getProvider().request({ method: 'eth_chainId' });
154+
```
155+
156+
**New:**
157+
158+
```typescript
159+
const { accounts, chainId } = await client.connect({
160+
chainIds: ['0x1'],
161+
});
162+
```
163+
164+
Key differences:
165+
- `connect()` now returns an **object** with both `accounts` and `chainId` — no separate call needed
166+
- `chainIds` parameter specifies which chains to request (hex strings)
167+
- Use `connectAndSign` for connect + personal_sign in one step:
168+
169+
```typescript
170+
const { accounts, chainId, signature } = await client.connectAndSign({
171+
chainIds: ['0x1'],
172+
message: 'Sign in to My DApp',
173+
});
174+
```
175+
176+
- Use `connectWith` for connect + arbitrary RPC method:
177+
178+
```typescript
179+
const { accounts, chainId, result } = await client.connectWith({
180+
chainIds: ['0x1'],
181+
method: 'eth_sendTransaction',
182+
params: [{ from: '0x...', to: '0x...', value: '0x0' }],
183+
});
184+
```
185+
186+
---
187+
188+
### Step 5: Update provider access
189+
190+
**Old:**
191+
192+
```typescript
193+
const provider = sdk.getProvider(); // SDKProvider
194+
await provider.request({ method: 'eth_chainId' });
195+
```
196+
197+
**New:**
198+
199+
```typescript
200+
const provider = client.getProvider(); // EIP1193Provider
201+
await provider.request({ method: 'eth_chainId' });
202+
```
203+
204+
Key differences:
205+
- The provider is now a standard **EIP-1193 provider**, not the custom `SDKProvider`
206+
- The provider is available **immediately** after `createEVMClient` resolves — even before `connect()`
207+
- Before connection, RPC calls that require an account will fail; read-only calls (like `eth_blockNumber`) work against `supportedNetworks` RPCs
208+
- No more `sdk.getProvider()` returning `undefined` — the provider always exists
209+
210+
---
211+
212+
### Step 6: Update event handling
213+
214+
**Old:**
215+
216+
```typescript
217+
const provider = sdk.getProvider();
218+
provider.on('chainChanged', (chainId) => { /* ... */ });
219+
provider.on('accountsChanged', (accounts) => { /* ... */ });
220+
provider.on('disconnect', () => { /* ... */ });
221+
```
222+
223+
**New (same EIP-1193 events still work):**
224+
225+
```typescript
226+
const provider = client.getProvider();
227+
provider.on('chainChanged', (chainId) => { /* ... */ });
228+
provider.on('accountsChanged', (accounts) => { /* ... */ });
229+
provider.on('disconnect', () => { /* ... */ });
230+
```
231+
232+
**New (additional SDK-level events via constructor):**
233+
234+
```typescript
235+
const client = await createEVMClient({
236+
dapp: { name: 'My DApp' },
237+
eventHandlers: {
238+
displayUri: (uri) => { /* render QR code */ },
239+
},
240+
});
241+
```
242+
243+
Or subscribe on the EIP-1193 provider after creation:
244+
245+
```typescript
246+
const provider = client.getProvider();
247+
provider.on('display_uri', (uri) => { /* ... */ });
248+
```
249+
250+
For `wallet_sessionChanged`, use the multichain client directly:
251+
252+
```typescript
253+
const client = await createMultichainClient({ /* ... */ });
254+
client.on('wallet_sessionChanged', (session) => { /* ... */ });
255+
```
256+
257+
---
258+
259+
### Step 7: New capabilities to adopt
260+
261+
These features are **new** in the MetaMask Connect packages and have no old-SDK equivalent:
262+
263+
| Capability | Description |
264+
|---|---|
265+
| **Multichain client** | `createMultichainClient` supports CAIP-25 scopes across EVM and non-EVM chains |
266+
| **`invokeMethod`** | Call RPC methods on specific CAIP scopes: `client.invokeMethod({ scope: 'eip155:1', request: { method, params } })` |
267+
| **Solana support** | `createSolanaClient` from `@metamask/connect-solana` with wallet-standard adapter |
268+
| **`connectAndSign`** | Connect and sign a message in a single user approval |
269+
| **`connectWith`** | Connect and execute any RPC method in a single user approval |
270+
| **Partial disconnect** | `disconnect(scopes)` is available on the multichain client to revoke specific CAIP scopes while keeping others active |
271+
| **Singleton client** | Subsequent `createMultichainClient` calls merge into the existing instance |
272+
| **`wallet_sessionChanged`** | Multichain client event fired when session state changes or is restored |
273+
274+
---
275+
276+
### Step 8: Wagmi migration
277+
278+
**Old:**
279+
280+
```typescript
281+
// Old @metamask/sdk constructor takes flat options (no `options` wrapper):
282+
import { MetaMaskSDK } from '@metamask/sdk';
283+
284+
const sdk = new MetaMaskSDK({
285+
dappMetadata: { name: 'My DApp', url: window.location.href },
286+
});
287+
// (or the legacy wagmi `metaMask()` connector that wrapped @metamask/sdk)
288+
```
289+
290+
**New:**
291+
292+
```typescript
293+
import { createConfig, http } from 'wagmi';
294+
import { mainnet, sepolia } from 'wagmi/chains';
295+
import { metaMask } from 'wagmi/connectors';
296+
297+
export const wagmiConfig = createConfig({
298+
chains: [mainnet, sepolia],
299+
connectors: [
300+
metaMask({
301+
dapp: {
302+
name: 'My DApp',
303+
url: typeof window !== 'undefined' ? window.location.href : undefined,
304+
},
305+
}),
306+
],
307+
transports: {
308+
[mainnet.id]: http(),
309+
[sepolia.id]: http(),
310+
},
311+
});
312+
```
313+
314+
Key differences:
315+
- 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
316+
- Use `dapp` not `dappMetadata`
317+
- Connector ID is `'metaMaskSDK'` — find it with `connectors.find(c => c.id === 'metaMaskSDK')`
318+
- 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)
319+
320+
---
321+
322+
## Quick Reference: Full Option Mapping
323+
324+
| Old (`@metamask/sdk`) | New (`@metamask/connect-*`) | Status |
325+
|---|---|---|
326+
| `new MetaMaskSDK(opts)` | `await createEVMClient(opts)` | Renamed, async |
327+
| `sdk.init()` | Not needed | Init happens in `createEVMClient` |
328+
| `sdk.connect()` | `client.connect({ chainIds })` | Returns `{ accounts, chainId }` |
329+
| `sdk.getProvider()` | `client.getProvider()` | Returns EIP-1193 provider |
330+
| `sdk.disconnect()` | `client.disconnect()` | Same for EVM; partial disconnect is multichain-only |
331+
| `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 |
332+
| `dappMetadata` | `dapp` | Renamed |
333+
| `infuraAPIKey` | `getInfuraRpcUrls({ infuraApiKey: key })` in `api.supportedNetworks` | Helper function; optional `chainIds` filters to specific chains |
334+
| `readonlyRPCMap` | `api.supportedNetworks` | Merged with Infura URLs |
335+
| `headless` | `ui.headless` | Moved to `ui` namespace |
336+
| `extensionOnly` | `ui.preferExtension` | Renamed, slightly different semantics |
337+
| `openDeeplink` | `mobile.preferredOpenLink` | Moved to `mobile` namespace |
338+
| `useDeeplink` | `mobile.useDeeplink` | Moved to `mobile` namespace |
339+
| `MetaMaskProvider` (React) | No direct equivalent | Use wagmi `WagmiProvider` or call `createEVMClient` directly |
340+
| `useSDK()` hook | No direct equivalent | Use wagmi hooks or manage client state manually |
341+
| `SDKProvider` | `EIP1193Provider` | Standard provider interface |
342+
| `timer` | Removed ||
343+
| `enableAnalytics` | `analytics: { enabled: boolean }` ||
344+
| `communicationServerUrl` | Removed ||
345+
| `storage` | Removed ||
346+
347+
## Important Notes
348+
349+
- **`createEVMClient` is async** — unlike `new MetaMaskSDK()`, it returns a promise. Ensure you `await` it or handle the promise before accessing the client.
350+
- **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.
351+
- **`connect()` returns an object now** — destructure `{ accounts, chainId }` instead of treating the return value as an accounts array.
352+
- **Chain IDs must be hex strings** — use `'0x1'` not `1` or `'1'` in `chainIds` and `supportedNetworks` keys.
353+
- **No more `sdk.init()`** — initialization is part of `createEVMClient`. There is no separate init step.
354+
- **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.
355+
- **`@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.
356+
- **`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.
357+
- **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.

0 commit comments

Comments
 (0)