@deeeed/metamask-harness 0.34.4 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +9 -1
  3. package/adapters/core/inject.sh +3 -3
  4. package/adapters/extension/check-infura-readiness.cjs +102 -0
  5. package/adapters/extension/inject.mjs +4 -4
  6. package/adapters/extension/live.sh +25 -10
  7. package/adapters/extension/start-watch.sh +15 -0
  8. package/adapters/extension/wallet-fixture-state.cjs +3 -1
  9. package/adapters/manifest.json +16 -0
  10. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +73 -42
  11. package/adapters/mobile/inject.sh +4 -3
  12. package/adapters/mobile/reset-app-data.sh +154 -0
  13. package/adapters/mobile/verify.sh +37 -0
  14. package/dist/adapters/extension/product-config.js +29 -1
  15. package/dist/adapters/extension/runtime.js +60 -15
  16. package/dist/adapters/extension/surface.js +6 -1
  17. package/dist/adapters/mobile/surface.js +6 -1
  18. package/dist/adapters.js +18 -0
  19. package/dist/cli-commands.js +1 -1
  20. package/dist/cli.js +2 -2
  21. package/dist/command-contract.js +1 -1
  22. package/dist/commands/device-target.js +5 -0
  23. package/dist/commands/fixtures.js +106 -31
  24. package/dist/commands/launch/extension.js +39 -3
  25. package/dist/commands/launch/index.js +13 -0
  26. package/dist/doctor.js +30 -1
  27. package/dist/mm-harness-cli.js +6 -3
  28. package/dist/recipe-security.js +3 -0
  29. package/docs/RECIPES.md +29 -0
  30. package/library/actions/extension/wallet/import.mjs +234 -0
  31. package/library/actions/extension/wallet/reset.mjs +98 -0
  32. package/library/actions/extension/wallet/state.mjs +1 -0
  33. package/library/actions/mobile/analytics/consent-settings.mjs +112 -0
  34. package/library/actions/mobile/analytics/set_consent.mjs +4 -112
  35. package/library/actions/mobile/platform/bridge.mjs +8 -0
  36. package/library/actions/mobile/wallet/import.mjs +259 -0
  37. package/library/actions/mobile/wallet/reset-helper.mjs +99 -0
  38. package/library/actions/mobile/wallet/reset.mjs +7 -0
  39. package/library/actions/shared/wallet/import-source.mjs +101 -0
  40. package/library/manifests/extension.action-manifest.json +112 -0
  41. package/library/manifests/mobile.action-manifest.json +108 -0
  42. package/library/recipes/wallet/import.recipe.json +83 -0
  43. package/library/recipes/wallet/reset-import.recipe.json +88 -0
  44. package/package.json +1 -1
  45. package/scripts/completions.sh +2 -2
  46. package/scripts/site-contrast.mjs +7 -0
  47. package/site/architecture.html +14 -7
  48. package/site/assets/metamask-fox.svg +24 -0
  49. package/site/assets/progress.mjs +6 -0
  50. package/site/assets/style.css +88 -29
  51. package/site/cheatsheet.html +5 -6
  52. package/site/ecosystem.html +162 -0
  53. package/site/how-it-works.html +8 -9
  54. package/site/index.html +17 -12
  55. package/site/perps.html +103 -45
  56. package/site/recipes.html +5 -6
  57. package/site/reviewers.html +5 -6
  58. package/site/tutorials/index.html +5 -6
  59. package/site/tutorials/v1.html +5 -6
  60. package/site/tutorials/v2.html +6 -8
  61. package/site/tutorials/v3.html +5 -6
  62. package/site/tutorials/v4.html +5 -6
  63. package/site/tutorials/v5.html +5 -6
  64. package/site/tutorials/v6.html +5 -6
  65. package/site/tutorials/v7.html +5 -6
@@ -48,7 +48,7 @@ function fixturesStatus(options) {
48
48
  const fixture = { ...summary, path: canonical };
49
49
  const ready = summary.status === "ready";
50
50
  const nextCommand = ready ? "mm-harness fixtures set" : "mm-harness fixtures init --from <path> # or: mm-harness fixtures init --dev";
51
- const operations = adapter === "extension" ? ["init", "sync", "set", "generate", "finalize"] : ["init", "sync", "set"];
51
+ const operations = adapter === "extension" ? ["init", "sync", "set", "reset", "generate", "finalize"] : adapter === "mobile" ? ["init", "sync", "set", "reset"] : ["init", "sync", "set"];
52
52
  if (json) {
53
53
  console.log(JSON.stringify({
54
54
  schemaVersion: 1,
@@ -103,8 +103,8 @@ async function handleFixturesLocked(argv) {
103
103
  const { positional, options } = parseFlags(argv, FIXTURES_BOOLEANS);
104
104
  const json = flag(options, "json");
105
105
  const sub = positional[0];
106
- if (sub !== "init" && sub !== "sync" && sub !== "set" && sub !== "generate" && sub !== "finalize") {
107
- return usageOut(json, "fixtures", "fixtures requires a subcommand: mm-harness fixtures <init|sync|set|generate|finalize>", "mm-harness fixtures init --from <path> or mm-harness fixtures init --dev");
106
+ if (sub !== "init" && sub !== "sync" && sub !== "set" && sub !== "reset" && sub !== "generate" && sub !== "finalize") {
107
+ return usageOut(json, "fixtures", "fixtures requires a subcommand: mm-harness fixtures <init|sync|set|reset|generate|finalize>", "mm-harness fixtures init --from <path> or mm-harness fixtures init --dev");
108
108
  }
109
109
  if (sub !== "init" && (flag(options, "dev") || flag(options, "force") || str(options, "from"))) {
110
110
  return usageOut(
@@ -121,7 +121,7 @@ async function handleFixturesLocked(argv) {
121
121
  }
122
122
  const surface = getAdapterSurface(adapter);
123
123
  surface.resolveSlotPorts(target);
124
- const dtResult = applyDeviceTargeting("fixtures", adapter, options, { gate: false, rerun: "" });
124
+ const dtResult = applyDeviceTargeting("fixtures", adapter, options, { gate: sub === "reset", rerun: "" });
125
125
  if ("code" in dtResult) {
126
126
  return usageOut(json, "fixtures", dtResult.message, dtResult.userAction);
127
127
  }
@@ -129,6 +129,9 @@ async function handleFixturesLocked(argv) {
129
129
  if (sub === "finalize") return fixturesFinalize(adapter, target, options, json);
130
130
  const canonicalFixture = walletFixturePath(target);
131
131
  if (sub === "init") return fixturesInit(adapter, target, options, json);
132
+ if (sub === "reset" && surface.headless) {
133
+ return usageOut(json, "fixtures", "fixtures reset requires a Mobile or Extension client.", "use fixtures set for the core adapter");
134
+ }
132
135
  if (surface.headless && sub === "set") {
133
136
  const fixture = fixtureSummary(target, adapter);
134
137
  if (fixture.status !== "ready") {
@@ -139,7 +142,7 @@ async function handleFixturesLocked(argv) {
139
142
  else console.error(message);
140
143
  return EXIT.ok;
141
144
  }
142
- const retryHint = `${surface.hints.relaunch} # relaunch, then retry: mm-harness fixtures set`;
145
+ const retryHint = `${surface.hints.relaunch} # relaunch, then retry: mm-harness fixtures ${sub}`;
143
146
  if (sub === "sync") {
144
147
  const exitCode = fixturesSync(adapter, target, json);
145
148
  if (json) {
@@ -162,34 +165,105 @@ async function handleFixturesLocked(argv) {
162
165
  "mm-harness fixtures init --dev && mm-harness fixtures set"
163
166
  );
164
167
  }
168
+ if (sub === "reset") {
169
+ const selectedFixture = fixtureFileSummary(fixturePath, adapter, fixturePath);
170
+ if (selectedFixture.status !== "ready") {
171
+ return usageOut(
172
+ json,
173
+ "fixtures",
174
+ `wallet fixture is ${String(selectedFixture.status)} at ${fixturePath}; reset was not started.`,
175
+ `mm-harness fixtures init --from <path> --target ${target}`
176
+ );
177
+ }
178
+ }
165
179
  if (adapter === "extension") {
166
- return applyExtensionFixture(target, fixturePath, canonicalFixture, json);
180
+ return applyExtensionFixture(target, fixturePath, canonicalFixture, json, sub);
167
181
  }
168
- process.stderr.write(`\u2192 fixtures set ${adapter} \u2014 connecting bridge + applying wallet fixture (can take ~30s)\u2026
182
+ const reset = sub === "reset";
183
+ process.stderr.write(`\u2192 fixtures ${sub} ${adapter} \u2014 ${reset ? "resetting selected app data, relaunching, then applying" : "connecting bridge + applying"} wallet fixture (can take ~30s)\u2026
169
184
  `);
170
185
  let status;
171
186
  let mobileSetupResult;
187
+ let resetError;
172
188
  if (adapter === "mobile") {
173
189
  const setupWalletSh = path.join(runnerDir, "adapters/mobile/bridge-runtime/setup-wallet.sh");
174
190
  const previousAppRoot = process.env.APP_ROOT;
175
191
  process.env.APP_ROOT = target;
176
192
  try {
193
+ if (reset) {
194
+ const platform = process.env.PLATFORM === "android" || process.env.ADB_SERIAL || process.env.ANDROID_SERIAL ? "android" : "ios";
195
+ const port = process.env.WATCHER_PORT ?? process.env.METRO_PORT ?? "8081";
196
+ const resetAppData = path.join(runnerDir, "adapters/mobile/reset-app-data.sh");
197
+ const resetArgs = ["--platform", platform, "--target", target];
198
+ if (platform === "ios") {
199
+ resetArgs.push("--simulator", process.env.SIM_UDID || process.env.IOS_SIMULATOR || "booted");
200
+ } else if (process.env.ADB_SERIAL || process.env.ANDROID_SERIAL) {
201
+ resetArgs.push("--adb-serial", process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || "");
202
+ }
203
+ const resetResult = await spawnScriptStreaming(resetAppData, resetArgs, target, {
204
+ timeoutMs: FIXTURE_STREAM_TIMEOUT_MS
205
+ });
206
+ if (resetResult.status !== 0) {
207
+ resetError = new Error("Mobile app data reset failed.");
208
+ } else {
209
+ const prewarmBundle = path.join(runnerDir, "adapters/mobile/prewarm-bundle.sh");
210
+ const prewarmResult = await spawnScriptStreaming(
211
+ prewarmBundle,
212
+ ["--platform", platform, "--target", target, "--port", port],
213
+ target,
214
+ {
215
+ timeoutMs: FIXTURE_STREAM_TIMEOUT_MS
216
+ }
217
+ );
218
+ if (prewarmResult.status !== 0) {
219
+ resetError = new Error("Mobile bundle prewarm failed after app data reset.");
220
+ }
221
+ }
222
+ if (!resetError) {
223
+ const openDevice = path.join(runnerDir, "adapters/mobile/open-device.sh");
224
+ const openArgs = ["--platform", platform, "--target", target, "--port", port, "--preflight-mode", "fast", "--restart"];
225
+ if (platform === "ios") {
226
+ openArgs.push("--simulator", process.env.SIM_UDID || process.env.IOS_SIMULATOR || "booted");
227
+ } else if (process.env.ADB_SERIAL || process.env.ANDROID_SERIAL) {
228
+ openArgs.push("--adb-serial", process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || "");
229
+ }
230
+ const openResult = await spawnScriptStreaming(openDevice, openArgs, target, {
231
+ timeoutMs: FIXTURE_STREAM_TIMEOUT_MS
232
+ });
233
+ if (openResult.status !== 0) {
234
+ resetError = new Error("Mobile app relaunch after data reset failed.");
235
+ } else {
236
+ const waitForBridge = path.join(runnerDir, "adapters/mobile/wait-for-bridge.sh");
237
+ const waitResult = await spawnScriptStreaming(
238
+ waitForBridge,
239
+ ["--target", target, "--port", port, "--platform", platform],
240
+ target,
241
+ { timeoutMs: FIXTURE_STREAM_TIMEOUT_MS }
242
+ );
243
+ if (waitResult.status !== 0) {
244
+ resetError = new Error("Mobile bridge did not return after app data reset.");
245
+ }
246
+ }
247
+ }
248
+ }
177
249
  const configuredTimeout = Number(process.env.MM_HARNESS_FIXTURES_SET_TIMEOUT_MS ?? FIXTURE_STREAM_TIMEOUT_MS);
178
250
  const timeoutMs = Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? configuredTimeout : FIXTURE_STREAM_TIMEOUT_MS;
179
- mobileSetupResult = await spawnScriptStreaming(
180
- setupWalletSh,
181
- ["--fixture", fixturePath],
182
- target,
183
- {
184
- env: {
185
- APP_ROOT: target,
186
- WATCHER_PORT: process.env.WATCHER_PORT ?? "8081",
187
- METRO_PORT: process.env.METRO_PORT ?? process.env.WATCHER_PORT ?? "8081"
188
- },
189
- timeoutMs
190
- }
191
- );
192
- status = mobileSetupResult.status === 0 ? "pass" : "fail";
251
+ if (!resetError) {
252
+ mobileSetupResult = await spawnScriptStreaming(
253
+ setupWalletSh,
254
+ ["--fixture", fixturePath],
255
+ target,
256
+ {
257
+ env: {
258
+ APP_ROOT: target,
259
+ WATCHER_PORT: process.env.WATCHER_PORT ?? "8081",
260
+ METRO_PORT: process.env.METRO_PORT ?? process.env.WATCHER_PORT ?? "8081"
261
+ },
262
+ timeoutMs
263
+ }
264
+ );
265
+ }
266
+ status = !resetError && mobileSetupResult?.status === 0 ? "pass" : "fail";
193
267
  } finally {
194
268
  if (previousAppRoot === void 0) delete process.env.APP_ROOT;
195
269
  else process.env.APP_ROOT = previousAppRoot;
@@ -205,16 +279,17 @@ async function handleFixturesLocked(argv) {
205
279
  {
206
280
  schemaVersion: 1,
207
281
  command: "fixtures",
208
- action: "set",
282
+ action: sub,
209
283
  adapter,
210
284
  fixture: fixturePath,
211
285
  canonicalFixture,
212
286
  status,
213
287
  exitCode: status === "pass" ? EXIT.ok : EXIT.runtime,
288
+ walletReset: reset && status === "pass",
214
289
  message: teaching,
215
290
  error: status === "fail" ? {
216
- code: mobileSetupResult?.timedOut ? "SETUP_WALLET_TIMEOUT" : "SETUP_WALLET_FAILED",
217
- message: mobileSetupResult?.timedOut ? `wallet fixture setup exceeded its ${String(mobileSetupResult.timeoutMs)}ms bound on Metro port ${process.env.WATCHER_PORT ?? "8081"}` : `wallet fixture setup failed on ${process.env.WATCHER_PORT ?? "8081"}`,
291
+ code: resetError ? "MOBILE_WALLET_RESET_FAILED" : mobileSetupResult?.timedOut ? "SETUP_WALLET_TIMEOUT" : "SETUP_WALLET_FAILED",
292
+ message: resetError ? `wallet reset failed: ${resetError instanceof Error ? resetError.message : String(resetError)}` : mobileSetupResult?.timedOut ? `wallet fixture setup exceeded its ${String(mobileSetupResult.timeoutMs)}ms bound on Metro port ${process.env.WATCHER_PORT ?? "8081"}` : `wallet fixture setup failed on ${process.env.WATCHER_PORT ?? "8081"}`,
218
293
  userAction: retryHint
219
294
  } : null
220
295
  },
@@ -227,7 +302,7 @@ async function handleFixturesLocked(argv) {
227
302
  }
228
303
  return status === "pass" ? EXIT.ok : EXIT.runtime;
229
304
  }
230
- async function applyExtensionFixture(target, fixturePath, canonicalFixture, json) {
305
+ async function applyExtensionFixture(target, fixturePath, canonicalFixture, json, action) {
231
306
  const distManifest = path.join(target, "dist/chrome/manifest.json");
232
307
  const userAction = `mm-harness launch --build --verify --target ${JSON.stringify(target)}`;
233
308
  if (!fs.existsSync(distManifest)) {
@@ -236,7 +311,7 @@ async function applyExtensionFixture(target, fixturePath, canonicalFixture, json
236
311
  console.log(JSON.stringify({
237
312
  schemaVersion: 1,
238
313
  command: "fixtures",
239
- action: "set",
314
+ action,
240
315
  adapter: "extension",
241
316
  fixture: fixturePath,
242
317
  canonicalFixture,
@@ -245,15 +320,15 @@ async function applyExtensionFixture(target, fixturePath, canonicalFixture, json
245
320
  error: { code: "EXTENSION_FIXTURE_BUILD_REQUIRED", message: message2, userAction }
246
321
  }, null, 2));
247
322
  } else {
248
- console.error(`\u2717 mm-harness fixtures set: ${message2}
323
+ console.error(`\u2717 mm-harness fixtures ${action}: ${message2}
249
324
  Next: ${userAction}`);
250
325
  }
251
326
  return EXIT.runtime;
252
327
  }
253
- process.stderr.write(`${colorHumanMessage("\u2192 fixtures set extension \u2014 resetting and seeding the slot-owned profile from the existing build\u2026")}
328
+ process.stderr.write(`${colorHumanMessage(`\u2192 fixtures ${action} extension \u2014 resetting and seeding the slot-owned profile from the existing build\u2026`)}
254
329
  `);
255
330
  const liveScript = path.join(runnerDir, "adapters/extension/live.sh");
256
- const args = ["--target", target, "--launch-existing-dist"];
331
+ const args = ["--target", target, "--launch-existing-dist", "--launch-only"];
257
332
  if (process.env.CDP_PORT) args.push("--cdp-port", process.env.CDP_PORT);
258
333
  const result = await spawnScriptStreaming(liveScript, args, target, {
259
334
  env: { RECIPE_WALLET_FIXTURE: fixturePath },
@@ -266,7 +341,7 @@ async function applyExtensionFixture(target, fixturePath, canonicalFixture, json
266
341
  console.log(JSON.stringify({
267
342
  schemaVersion: 1,
268
343
  command: "fixtures",
269
- action: "set",
344
+ action,
270
345
  adapter: "extension",
271
346
  fixture: fixturePath,
272
347
  canonicalFixture,
@@ -279,7 +354,7 @@ async function applyExtensionFixture(target, fixturePath, canonicalFixture, json
279
354
  } else if (status === "pass") {
280
355
  console.error(message);
281
356
  } else {
282
- console.error(`\u2717 mm-harness fixtures set: ${message}
357
+ console.error(`\u2717 mm-harness fixtures ${action}: ${message}
283
358
  Next: ${userAction}`);
284
359
  }
285
360
  return exitCode;
@@ -15,11 +15,16 @@ import {
15
15
  runnerDir
16
16
  } from "../../paths.js";
17
17
  import { extensionIdFromKey } from "../../adapters/extension/extension-id.js";
18
- import { extensionProductConfigBlock } from "../../adapters/extension/product-config.js";
18
+ import {
19
+ EXTENSION_PRODUCT_CONFIG_FINGERPRINT_FILENAME,
20
+ extensionCompiledScriptsMatchProductConfig,
21
+ extensionProductConfigBlock,
22
+ extensionProductConfigFingerprint
23
+ } from "../../adapters/extension/product-config.js";
19
24
  import { isExtensionDistStale } from "../../adapters/extension/runtime-decision.js";
20
25
  import { checkExtensionRuntimeHealth } from "../../adapters/extension/runtime.js";
21
26
  import { ensureHarnessFresh } from "../../adapters/harness-freshness.js";
22
- import { stopExtensionWatcher } from "../../adapters/slot-ports.js";
27
+ import { isExtensionWatcherLive, stopExtensionWatcher } from "../../adapters/slot-ports.js";
23
28
  import { EXIT, spawnScriptStreaming } from "../shared.js";
24
29
  const { CdpSession } = await importRecipeHarnessRuntimeCdp();
25
30
  const { RUNTIME_IDENTITY_FILENAME, RUNTIME_NONCE_PREFIX } = createRequire(import.meta.url)(
@@ -75,12 +80,14 @@ Next: ${block.userAction}`
75
80
  if (reusable) {
76
81
  return extensionReattach(target, displayMode);
77
82
  }
78
- return extensionRebuild(target);
83
+ return extensionLaunchDevelopment(target);
79
84
  }
80
85
  async function extensionRuntimeReusable(target) {
81
86
  const cdpPort = process.env.CDP_PORT;
82
87
  if (!cdpPort) return false;
83
88
  if (isExtensionDistStale(target)) return false;
89
+ if (isExtensionWatcherLive(target) && !harnessWatcherMatchesProductConfig(target)) return false;
90
+ if (!extensionCompiledScriptsMatchProductConfig(target, expectedRuntimeDist(target))) return false;
84
91
  for (let attempt = 0; attempt < 3; attempt += 1) {
85
92
  const reachable = await cdpVersionReachable(cdpPort);
86
93
  const nonceOwned = reachable && await cdpOwnedByRuntimeNonce(cdpPort, target);
@@ -325,6 +332,34 @@ async function extensionReattach(target, displayMode = "fullscreen") {
325
332
  console.error(colorHumanMessage(`\u2192 extension quick reattach \u2014 reload in place \xB7 CDP :${process.env.CDP_PORT ?? "default"} (no rebuild, no relaunch)`));
326
333
  return spawnScriptStreaming(reattachScript, reattachArgs, target);
327
334
  }
335
+ async function extensionLaunchDevelopment(target) {
336
+ const liveScript = recipeHarnessPath(target, "extension", "scripts", "live.sh");
337
+ const liveArgs = ["--target", target];
338
+ const existingBuild = isExtensionWatcherLive(target) && harnessWatcherMatchesProductConfig(target) && extensionCompiledScriptsMatchProductConfig(target, path.join(target, "dist/chrome")) && fs.existsSync(path.join(target, "dist/chrome/manifest.json")) && !isExtensionDistStale(target);
339
+ liveArgs.push(existingBuild ? "--launch-existing-dist" : "--start-watch");
340
+ if (process.env.CDP_PORT) liveArgs.push("--cdp-port", process.env.CDP_PORT);
341
+ if (process.env.EXTENSION_START_URL) liveArgs.push("--start-url", process.env.EXTENSION_START_URL);
342
+ console.error(colorHumanMessage(
343
+ existingBuild ? `\u2192 extension quick launch \u2014 existing yarn start build \xB7 CDP :${process.env.CDP_PORT ?? "default"} (output streams below)` : `\u2192 extension quick launch \u2014 yarn start development build \xB7 CDP :${process.env.CDP_PORT ?? "default"} (output streams below)`
344
+ ));
345
+ return spawnScriptStreaming(liveScript, liveArgs, target);
346
+ }
347
+ function harnessWatcherMatchesProductConfig(target) {
348
+ const runtimeDir = path.join(target, recipeRuntimeDir());
349
+ const harnessPidFile = path.join(runtimeDir, "recipe-harness-webpack.pid");
350
+ if (!fs.existsSync(harnessPidFile)) return true;
351
+ const expected = extensionProductConfigFingerprint(target);
352
+ if (!expected) return true;
353
+ try {
354
+ const actual = fs.readFileSync(
355
+ path.join(runtimeDir, EXTENSION_PRODUCT_CONFIG_FINGERPRINT_FILENAME),
356
+ "utf8"
357
+ ).trim();
358
+ return actual === expected;
359
+ } catch {
360
+ return false;
361
+ }
362
+ }
328
363
  async function extensionRebuild(target) {
329
364
  const runtimeDirRel = recipeRuntimeDir();
330
365
  const runtimeAbs = path.join(target, runtimeDirRel);
@@ -348,6 +383,7 @@ async function extensionRebuild(target) {
348
383
  }
349
384
  export {
350
385
  extensionDepsBlock,
386
+ extensionLaunchDevelopment,
351
387
  extensionReattach,
352
388
  extensionRebuild,
353
389
  extensionRuntimeReusable,
@@ -259,6 +259,16 @@ async function handleLaunchLocked(argv, stream) {
259
259
  exitCode: EXIT.infra
260
260
  });
261
261
  }
262
+ if (adapter === "extension" && extensionEvmRpcUnreachable(attempt.output)) {
263
+ return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
264
+ code: "EVM_RPC_UNREACHABLE",
265
+ message: "EVM RPC readiness failed; Infura configured does not mean RPC reachable.",
266
+ recoverable: false,
267
+ userAction: `Check the effective INFURA_PROJECT_ID from the environment, .metamaskprodrc, or ${shellQuote(path.join(target, ".metamaskrc"))}, then rerun: mm-harness launch --verify --target ${shellQuote(target)}`,
268
+ exitCode: EXIT.runtime,
269
+ originalError: attempt.output.trim() || void 0
270
+ });
271
+ }
262
272
  if (heal === "off") {
263
273
  return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
264
274
  code: "LAUNCH_FAILED",
@@ -374,6 +384,9 @@ function extensionBrowserMissing(output) {
374
384
  function extensionProductConfigMissing(output) {
375
385
  return /EXTENSION_PRODUCT_CONFIG_REQUIRED:/u.test(output);
376
386
  }
387
+ function extensionEvmRpcUnreachable(output) {
388
+ return /EVM_RPC_UNREACHABLE|EVM RPC readiness probe failed|Infura configured ≠ RPC reachable/u.test(output);
389
+ }
377
390
  function mobileProvisioningBlocked(output) {
378
391
  return /open-device: configured iOS simulator '.+' does not exist|open-device: no MetaMask bundle found|fast mode requires an installed (?:iOS dev client|Android dev client)/u.test(output);
379
392
  }
package/dist/doctor.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import fs from "node:fs";
2
+ import { createRequire } from "node:module";
2
3
  import path from "node:path";
3
4
  import { color } from "./cli-color.js";
4
5
  import {
@@ -148,7 +149,8 @@ function createDoctorReport(adapter, target, manifestValidation, actionManifestP
148
149
  required: false,
149
150
  message: mode === "unsupported/no bridge" ? `No ${adapter} bridge is available for this checkout.` : `${adapter} compatibility mode: ${mode}.`
150
151
  },
151
- ...adapter === "mobile" ? mobileToolDoctorChecks(mobilePlatform) : []
152
+ ...adapter === "mobile" ? mobileToolDoctorChecks(mobilePlatform) : [],
153
+ ...adapter === "extension" ? [extensionBrowserDoctorCheck(target)] : []
152
154
  ];
153
155
  const requiredChecks = requiredDoctorCheckSummary(checks);
154
156
  return {
@@ -169,6 +171,32 @@ function createDoctorReport(adapter, target, manifestValidation, actionManifestP
169
171
  manifestValidation: manifestValidation.summary
170
172
  };
171
173
  }
174
+ function extensionBrowserDoctorCheck(target) {
175
+ const requireFromTarget = createRequire(path.join(path.resolve(target), "package.json"));
176
+ for (const packageName of ["@playwright/test", "playwright"]) {
177
+ try {
178
+ const playwright = requireFromTarget(packageName);
179
+ const executable = playwright.chromium?.executablePath?.();
180
+ if (executable && fs.existsSync(executable)) {
181
+ return {
182
+ id: "playwright-chromium",
183
+ status: "pass",
184
+ required: true,
185
+ message: "Checkout-pinned Playwright Chromium is installed.",
186
+ detail: executable
187
+ };
188
+ }
189
+ } catch {
190
+ }
191
+ }
192
+ return {
193
+ id: "playwright-chromium",
194
+ status: "fail",
195
+ required: true,
196
+ message: "Checkout-pinned Playwright Chromium is not installed.",
197
+ userAction: "yarn playwright install chromium"
198
+ };
199
+ }
172
200
  function mobileToolDoctorChecks(platform) {
173
201
  const specs = platform === "android" ? [{ tool: "adb", platform: "Android" }] : platform === "ios" ? [{ tool: "idb", platform: "iOS" }] : [
174
202
  { tool: "adb", platform: "Android" },
@@ -250,6 +278,7 @@ function readJsonObject(file) {
250
278
  export {
251
279
  compatibilityMode,
252
280
  createDoctorReport,
281
+ extensionBrowserDoctorCheck,
253
282
  fixtureFileSummary,
254
283
  fixtureSummary,
255
284
  mobileToolDoctorChecks,
@@ -452,7 +452,7 @@ Example:
452
452
  Extension quick launch REUSES a healthy live runtime (reload in place, no rebuild);
453
453
  --build is the escape hatch that always clean-builds + relaunches a fresh runtime.
454
454
 
455
- --build Full clean build + relaunch (escape hatch; skips extension reuse)
455
+ --build Full production-like build + relaunch (escape hatch; skips extension reuse)
456
456
  --clear-metro Mobile only: clear Metro's transform cache before launch
457
457
  --verify Launch then poll CDP/bridge until ready (absorbs the old \`live\`)
458
458
  --runway Post-launch runway check (mobile only; teaching error elsewhere)
@@ -539,9 +539,9 @@ Example:
539
539
  },
540
540
  {
541
541
  name: "fixtures",
542
- summary: "Manage the canonical wallet fixture (wallet DATA only) \u2014 init / sync / set / generate / finalize.",
542
+ summary: "Manage the canonical wallet fixture (wallet DATA only) \u2014 init / sync / set / reset / generate / finalize.",
543
543
  example: "mm-harness fixtures set",
544
- helpText: `mm-harness fixtures [<init|sync|set|generate|finalize>] [flags]
544
+ helpText: `mm-harness fixtures [<init|sync|set|reset|generate|finalize>] [flags]
545
545
 
546
546
  Manage the ONE canonical wallet fixture per checkout \u2014 wallet DATA only.
547
547
  With no action, show the current fixture status and safe next command.
@@ -550,6 +550,8 @@ Example:
550
550
  sync Refresh the wallet fixture files on the target.
551
551
  set Apply the canonical fixture (SRP/password/accounts); the password is
552
552
  read FROM the fixture, never typed.
553
+ reset Delete the current Mobile/Extension wallet, then apply and validate
554
+ the canonical fixture from a clean state.
553
555
  generate Render the extension fixture-state.json from a wallet fixture, for
554
556
  pre-launch profile prefill (extension-only). Requires --fixture and --out.
555
557
  finalize Seed account labels/selection into the LIVE extension over CDP and
@@ -578,6 +580,7 @@ Example:
578
580
  mm-harness fixtures init --dev
579
581
  mm-harness fixtures sync
580
582
  mm-harness fixtures set
583
+ mm-harness fixtures reset
581
584
  mm-harness fixtures generate --fixture wallet-fixture.json --out fixture-state.json
582
585
  mm-harness fixtures finalize --fixture wallet-fixture.json --state fixture-state.json --cdp-port 6661 --extension-dir dist/chrome`
583
586
  }
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  const READ_ONLY_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
5
5
  "ui.locators",
6
6
  "metamask.wallet.fixture_status",
7
+ "metamask.wallet.validate_import",
7
8
  "metamask.wallet.list_accounts",
8
9
  "metamask.wallet.read_state",
9
10
  "metamask.perps.read_positions",
@@ -19,6 +20,8 @@ const READ_ONLY_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
19
20
  ]);
20
21
  const APP_MUTATION_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
21
22
  "metamask.wallet.setup",
23
+ "metamask.wallet.import",
24
+ "metamask.wallet.reset",
22
25
  "metamask.wallet.ensure_unlocked",
23
26
  "metamask.wallet.select_account",
24
27
  "metamask.deeplink.open",
package/docs/RECIPES.md CHANGED
@@ -58,6 +58,35 @@ mm-harness run recipe.json account="Account 2" --plan
58
58
  mm-harness run recipe.json account="Account 2"
59
59
  ```
60
60
 
61
+ Use the shared wallet setup recipe on either Mobile or Extension. `auto`
62
+ reuses a ready fixture-backed profile and otherwise completes visible
63
+ onboarding; `ui` requires a fresh onboarding state.
64
+
65
+ ```bash
66
+ mm-harness run wallet.import method=auto
67
+ mm-harness run wallet.import method=ui credential_source=environment
68
+ mm-harness run wallet.import method=ui metametrics=true 'interests=["trade_perpetuals"]'
69
+ ```
70
+
71
+ The UI path reads the primary mnemonic from `MM_HARNESS_WALLET_SRP` and the
72
+ password from `MM_HARNESS_WALLET_PASSWORD`, or from the canonical wallet
73
+ fixture. Secret values are redacted from commands and evidence. Visible imports
74
+ default to MetaMetrics off and skip Mobile's optional interests; password terms
75
+ are required and always accepted by the action.
76
+
77
+ To prove the full real-user recovery journey, delete the current wallet through
78
+ visible client UI and import it again:
79
+
80
+ ```bash
81
+ mm-harness run wallet.reset-import credential_source=fixture
82
+ ```
83
+
84
+ For fixture-backed development clients, `mm-harness fixtures reset` performs a
85
+ clean reset and reapplies the canonical fixture. Extension resets its owned
86
+ Chrome profile; Mobile clears only the selected app's data, preserves the
87
+ installed build, relaunches it, and then applies the fixture. Use
88
+ `wallet.reset-import` when the reset itself must be exercised through visible UI.
89
+
61
90
  ### Mobile device targeting
62
91
 
63
92
  Always pass `--device <name|udid|serial>` when more than one simulator/device may