@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
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env bash
2
+ # Reset one installed MetaMask Mobile app without rebuilding it.
3
+ set -euo pipefail
4
+
5
+ PLATFORM="${PLATFORM:-ios}"
6
+ TARGET="$PWD"
7
+ SIMULATOR="${SIM_UDID:-${IOS_SIMULATOR:-booted}}"
8
+ ADB_SERIAL_ARG="${ADB_SERIAL:-${ANDROID_SERIAL:-}}"
9
+ ADB_BIN="${MM_HARNESS_ADB_PATH:-}"
10
+ MM_RESET_TEMP_ROOT=""
11
+ MM_RESET_STAGING_DIR=""
12
+
13
+ while [ "$#" -gt 0 ]; do
14
+ case "$1" in
15
+ --platform) PLATFORM="$2"; shift 2 ;;
16
+ --target) TARGET="$2"; shift 2 ;;
17
+ --simulator) SIMULATOR="$2"; shift 2 ;;
18
+ --adb-serial) ADB_SERIAL_ARG="$2"; shift 2 ;;
19
+ -h|--help)
20
+ printf 'Usage: reset-app-data.sh --platform ios|android [--target <dir>] [--simulator <udid|name>] [--adb-serial <serial>]\n'
21
+ exit 0
22
+ ;;
23
+ *) printf 'reset-app-data: unknown arg: %s\n' "$1" >&2; exit 2 ;;
24
+ esac
25
+ done
26
+
27
+ TARGET="$(cd "$TARGET" && pwd -P)"
28
+
29
+ reset_ios() {
30
+ local bundle_id="" app_source="" candidate="" staged_app=""
31
+ local temp_root="${TMPDIR:-/tmp}" staging_dir=""
32
+ local -a candidates installed_ids installed_paths
33
+
34
+ temp_root="${temp_root%/}"
35
+ [ -n "$temp_root" ] && [ "$temp_root" != "/" ] && [ -d "$temp_root" ] && [ ! -L "$temp_root" ] || {
36
+ printf 'reset-app-data: unsafe temporary directory: %s\n' "$temp_root" >&2
37
+ return 1
38
+ }
39
+ if [ -n "${IOS_BUNDLE_ID:-}" ]; then
40
+ candidates=("$IOS_BUNDLE_ID")
41
+ else
42
+ candidates=("io.metamask.MetaMask" "io.metamask" "io.metamask.MetaMask-Flask" "io.metamask.qa")
43
+ fi
44
+ for candidate in "${candidates[@]}"; do
45
+ if app_source="$(xcrun simctl get_app_container "$SIMULATOR" "$candidate" app 2>/dev/null)"; then
46
+ installed_ids+=("$candidate")
47
+ installed_paths+=("$app_source")
48
+ fi
49
+ done
50
+ if [ "${#installed_ids[@]}" -gt 1 ]; then
51
+ printf 'reset-app-data: multiple MetaMask iOS variants are installed on simulator %s: %s.\n' "$SIMULATOR" "${installed_ids[*]}" >&2
52
+ printf 'Next: set IOS_BUNDLE_ID to the exact variant and retry.\n' >&2
53
+ return 1
54
+ fi
55
+ bundle_id="${installed_ids[0]:-}"
56
+ app_source="${installed_paths[0]:-}"
57
+ [ -n "$bundle_id" ] && [ -n "$app_source" ] || {
58
+ printf 'reset-app-data: no installed MetaMask app was found on iOS simulator %s.\n' "$SIMULATOR" >&2
59
+ printf "Next: mm-harness launch --adapter mobile --platform ios --target '%s'\n" "$TARGET" >&2
60
+ return 1
61
+ }
62
+ case "$app_source" in
63
+ */Library/Developer/CoreSimulator/Devices/*/data/Containers/Bundle/Application/*/*.app) ;;
64
+ *) printf 'reset-app-data: refusing unexpected iOS app path: %s\n' "$app_source" >&2; return 1 ;;
65
+ esac
66
+ [ -d "$app_source" ] && [ ! -L "$app_source" ] || {
67
+ printf 'reset-app-data: iOS app is not a real directory: %s\n' "$app_source" >&2
68
+ return 1
69
+ }
70
+
71
+ staging_dir="$(mktemp -d "$temp_root/mm-harness-ios-reset.XXXXXX")"
72
+ case "$staging_dir" in
73
+ "$temp_root"/mm-harness-ios-reset.*) ;;
74
+ *) printf 'reset-app-data: refusing unexpected staging path: %s\n' "$staging_dir" >&2; return 1 ;;
75
+ esac
76
+ [ -d "$staging_dir" ] && [ ! -L "$staging_dir" ] || return 1
77
+ staged_app="$staging_dir/$(basename "$app_source")"
78
+ MM_RESET_TEMP_ROOT="$temp_root"
79
+ MM_RESET_STAGING_DIR="$staging_dir"
80
+ cleanup_staging() {
81
+ case "$MM_RESET_STAGING_DIR" in
82
+ "$MM_RESET_TEMP_ROOT"/mm-harness-ios-reset.*)
83
+ [ -d "$MM_RESET_STAGING_DIR" ] && [ ! -L "$MM_RESET_STAGING_DIR" ] && rm -r -- "$MM_RESET_STAGING_DIR"
84
+ ;;
85
+ *) printf 'reset-app-data: staging cleanup refused: %s\n' "$MM_RESET_STAGING_DIR" >&2 ;;
86
+ esac
87
+ }
88
+ trap cleanup_staging EXIT HUP INT TERM
89
+
90
+ ditto "$app_source" "$staged_app"
91
+ [ -d "$staged_app" ] && [ ! -L "$staged_app" ] || {
92
+ printf 'reset-app-data: failed to preserve the installed iOS app.\n' >&2
93
+ return 1
94
+ }
95
+ printf 'Resetting iOS app data: simulator=%s bundle=%s\n' "$SIMULATOR" "$bundle_id" >&2
96
+ xcrun simctl terminate "$SIMULATOR" "$bundle_id" >/dev/null 2>&1 || true
97
+ xcrun simctl uninstall "$SIMULATOR" "$bundle_id"
98
+ xcrun simctl install "$SIMULATOR" "$staged_app"
99
+ xcrun simctl get_app_container "$SIMULATOR" "$bundle_id" app >/dev/null
100
+ printf 'Mobile app data reset; preserved installed iOS build %s.\n' "$bundle_id" >&2
101
+ }
102
+
103
+ reset_android() {
104
+ local package_id="" candidate="" clear_output=""
105
+ local -a candidates adb_target installed_ids
106
+ if [ -z "$ADB_BIN" ]; then
107
+ ADB_BIN="$(command -v adb 2>/dev/null || true)"
108
+ fi
109
+ [ -n "$ADB_BIN" ] && [ "${ADB_BIN#/}" != "$ADB_BIN" ] && [ -x "$ADB_BIN" ] || {
110
+ printf 'reset-app-data: Android SDK Platform-Tools (adb) is required.\n' >&2
111
+ printf 'Next: brew install android-platform-tools\n' >&2
112
+ return 1
113
+ }
114
+ [ -n "$ADB_SERIAL_ARG" ] || {
115
+ printf 'reset-app-data: an Android serial is required.\n' >&2
116
+ printf 'Next: mm-harness devices --adapter mobile --json\n' >&2
117
+ return 1
118
+ }
119
+ adb_target=(-s "$ADB_SERIAL_ARG")
120
+ if [ -n "${ANDROID_PACKAGE_ID:-}" ]; then
121
+ candidates=("$ANDROID_PACKAGE_ID")
122
+ else
123
+ candidates=("io.metamask" "io.metamask.flask" "io.metamask.qa")
124
+ fi
125
+ for candidate in "${candidates[@]}"; do
126
+ if "$ADB_BIN" "${adb_target[@]}" shell pm path "$candidate" 2>/dev/null | grep -q '^package:'; then
127
+ installed_ids+=("$candidate")
128
+ fi
129
+ done
130
+ if [ "${#installed_ids[@]}" -gt 1 ]; then
131
+ printf 'reset-app-data: multiple MetaMask Android variants are installed on device %s: %s.\n' "$ADB_SERIAL_ARG" "${installed_ids[*]}" >&2
132
+ printf 'Next: set ANDROID_PACKAGE_ID to the exact variant and retry.\n' >&2
133
+ return 1
134
+ fi
135
+ package_id="${installed_ids[0]:-}"
136
+ [ -n "$package_id" ] || {
137
+ printf 'reset-app-data: no installed MetaMask app was found on Android device %s.\n' "$ADB_SERIAL_ARG" >&2
138
+ printf "Next: mm-harness launch --adapter mobile --platform android --target '%s' --device '%s'\n" "$TARGET" "$ADB_SERIAL_ARG" >&2
139
+ return 1
140
+ }
141
+ printf 'Resetting Android app data: device=%s package=%s\n' "$ADB_SERIAL_ARG" "$package_id" >&2
142
+ clear_output="$("$ADB_BIN" "${adb_target[@]}" shell pm clear "$package_id")"
143
+ [ "$clear_output" = "Success" ] || {
144
+ printf 'reset-app-data: Android package clear failed: %s\n' "$clear_output" >&2
145
+ return 1
146
+ }
147
+ printf 'Mobile app data reset; preserved installed Android build %s.\n' "$package_id" >&2
148
+ }
149
+
150
+ case "$PLATFORM" in
151
+ ios) reset_ios ;;
152
+ android) reset_android ;;
153
+ *) printf 'reset-app-data: --platform must be ios or android (got: %s)\n' "$PLATFORM" >&2; exit 2 ;;
154
+ esac
@@ -325,6 +325,35 @@ JSON
325
325
  ) > "$log_path" 2>&1
326
326
  }
327
327
 
328
+ evm_rpc_ready() {
329
+ local log_path="$1"
330
+ local bridge="$HARNESS_DIR/bridge-runtime/cdp-bridge.cjs"
331
+ local expression
332
+ expression="(async function(){try{var engine=globalThis.Engine;var controller=engine&&engine.context&&engine.context.NetworkController;if(!controller||typeof controller.getSelectedNetworkClient!=='function')throw new Error('NetworkController selected client is unavailable');var client=controller.getSelectedNetworkClient();if(!client||!client.provider||typeof client.provider.request!=='function')throw new Error('selected EVM provider is unavailable');var result=await client.provider.request({method:'eth_blockNumber'});if(typeof result!=='string'||!/^0x[0-9a-f]+$/i.test(result))throw new Error('eth_blockNumber returned an invalid result');return {ok:true,result:result};}catch(error){return {ok:false,error:error&&(error.message||String(error))};}})()"
333
+ [ -f "$bridge" ] || {
334
+ printf 'Mobile bridge is missing at %s\n' "$bridge" > "$log_path"
335
+ return 1
336
+ }
337
+ if ! run_with_timeout "$log_path" 25 env \
338
+ APP_ROOT="$TARGET" \
339
+ WATCHER_PORT="$port" \
340
+ IOS_SIMULATOR="${IOS_SIMULATOR:-$ios_simulator_resolved}" \
341
+ ADB_SERIAL="${ADB_SERIAL:-$adb_serial_resolved}" \
342
+ ANDROID_SERIAL="${ANDROID_SERIAL:-$adb_serial_resolved}" \
343
+ node "$bridge" eval-async "$expression"; then
344
+ return 1
345
+ fi
346
+ node - "$log_path" <<'NODE'
347
+ const fs = require('fs');
348
+ try {
349
+ const result = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
350
+ process.exit(result.ok === true ? 0 : 1);
351
+ } catch {
352
+ process.exit(1);
353
+ }
354
+ NODE
355
+ }
356
+
328
357
  ensure_live_runtime() {
329
358
  local device_target="$1"
330
359
  local attempt
@@ -419,6 +448,14 @@ JSON
419
448
  add_note "Runner v1 live bridge smoke failed; inspect logs/runner-live-smoke.log and runner-live-smoke/trace.json."
420
449
  status="fail"
421
450
  fi
451
+
452
+ if evm_rpc_ready "$ARTIFACTS/logs/evm-rpc-readiness.log"; then
453
+ checks+=("{\"name\":\"live EVM RPC readiness\",\"status\":\"pass\"}")
454
+ else
455
+ checks+=("{\"name\":\"live EVM RPC readiness\",\"status\":\"fail\",\"detail\":\"see logs/evm-rpc-readiness.log\"}")
456
+ add_note "EVM RPC readiness probe failed. Infura configured ≠ RPC reachable. Next: confirm $TARGET/.js.env has a real MM_INFURA_PROJECT_ID (not 00000000000), then rerun mm-harness launch $PLATFORM --verify --target '$TARGET'."
457
+ status="fail"
458
+ fi
422
459
  fi
423
460
 
424
461
  RECIPE_HARNESS_PREFLIGHT_MODE="$PREFLIGHT_MODE" RECIPE_HARNESS_ROOT_EXCLUDE="$HARNESS_ROOT" node - "$ARTIFACTS" "$TARGET" "$status" "${checks[@]}" <<'NODE'
@@ -1,6 +1,31 @@
1
+ import { createHash } from "node:crypto";
2
+ import fs from "node:fs";
1
3
  import path from "node:path";
2
4
  import productConfig from "../../../adapters/extension/lib/product-config.cjs";
3
5
  import { shellQuote } from "../../commands/parse-args.js";
6
+ const EXTENSION_PRODUCT_CONFIG_FINGERPRINT_FILENAME = "extension-product-config.sha256";
7
+ function extensionProductConfigFingerprint(target, environment = process.env) {
8
+ const resolution = productConfig.resolveExtensionInfuraProjectId(target, environment);
9
+ if (resolution.kind !== "configured") return null;
10
+ return createHash("sha256").update(resolution.value).digest("hex");
11
+ }
12
+ function extensionCompiledScriptsMatchProductConfig(target, dist, environment = process.env) {
13
+ const resolution = productConfig.resolveExtensionInfuraProjectId(target, environment);
14
+ if (resolution.kind !== "configured") return true;
15
+ const pending = [dist];
16
+ while (pending.length > 0) {
17
+ const current = pending.pop();
18
+ if (!current || !fs.existsSync(current)) continue;
19
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
20
+ const file = path.join(current, entry.name);
21
+ if (entry.isDirectory()) pending.push(file);
22
+ else if (entry.isFile() && entry.name.endsWith(".js") && fs.readFileSync(file, "utf8").includes(resolution.value)) {
23
+ return true;
24
+ }
25
+ }
26
+ }
27
+ return false;
28
+ }
4
29
  function extensionProductConfigBlock(target) {
5
30
  const resolution = productConfig.resolveExtensionInfuraProjectId(
6
31
  target,
@@ -55,5 +80,8 @@ function invalidConfigBlock(target, name, reason) {
55
80
  };
56
81
  }
57
82
  export {
58
- extensionProductConfigBlock
83
+ EXTENSION_PRODUCT_CONFIG_FINGERPRINT_FILENAME,
84
+ extensionCompiledScriptsMatchProductConfig,
85
+ extensionProductConfigBlock,
86
+ extensionProductConfigFingerprint
59
87
  };
@@ -153,12 +153,26 @@ async function checkExtensionRuntimeHealth(projectRoot, cdpPort, options = {}) {
153
153
  if (runtime.backgroundUnresponsive === true) {
154
154
  findings.push("Extension UI reports background connection unresponsive.");
155
155
  }
156
+ if (runtime.ethereumConnectionUnavailable === true) {
157
+ findings.push('Extension UI reports "Unable to connect to Ethereum".');
158
+ }
159
+ if (runtime.evmRpcProbeOk !== true) {
160
+ const probeError = String(runtime.evmRpcProbeError ?? "unknown error").replace(/[.\s]+$/u, "");
161
+ findings.push(
162
+ `EVM RPC readiness probe failed: ${probeError}. Infura configured \u2260 RPC reachable.`
163
+ );
164
+ }
156
165
  if (runtime.hasSubmitRequest === true && runtime.backgroundProbeOk !== true) {
157
166
  warnings.push(`Perps background read probe failed: ${runtime.backgroundProbeError ?? "unknown error"}.`);
158
167
  }
159
168
  const extensionId = safeExtensionId(target);
169
+ const evmRpcUnreachable = runtime.ethereumConnectionUnavailable === true || runtime.evmRpcProbeOk !== true;
160
170
  return {
161
171
  status: findings.length === 0 ? "PASS" : "FAIL",
172
+ ...evmRpcUnreachable ? {
173
+ errorCode: "EVM_RPC_UNREACHABLE",
174
+ userAction: evmRpcRecoveryAction(projectRoot)
175
+ } : {},
162
176
  warnings,
163
177
  cdpPort,
164
178
  targetUrl: target.url,
@@ -219,21 +233,42 @@ async function evaluateHealth(session, timeoutMs) {
219
233
  const storeState = hooks.store?.getState?.() || {};
220
234
  const manager = hooks.getPerpsStreamManager?.();
221
235
  const accountCache = manager?.account?.cache;
236
+ const networkConnectionBanner = storeState.metamask?.networkConnectionBanner;
237
+ const networkClientId = storeState.metamask?.selectedNetworkClientId || networkConnectionBanner?.networkClientId;
222
238
  return Promise.race([
223
239
  (async () => {
224
- let backgroundProbeOk = false;
225
- let backgroundProbeError = null;
226
- if (typeof hooks.submitRequestToBackground === 'function') {
227
- try {
228
- const accountState = await hooks.submitRequestToBackground('perpsGetAccountState', []);
229
- backgroundProbeOk = Boolean(accountState && typeof accountState === 'object');
230
- if (!backgroundProbeOk) backgroundProbeError = 'perpsGetAccountState returned an empty result';
231
- } catch (error) {
232
- backgroundProbeError = String(error?.message || error);
233
- }
234
- } else {
235
- backgroundProbeError = 'submitRequestToBackground is not a function';
236
- }
240
+ const [backgroundProbe, evmRpcProbe] = await Promise.all([
241
+ (async () => {
242
+ if (typeof hooks.submitRequestToBackground !== 'function') {
243
+ return { ok: false, error: 'submitRequestToBackground is not a function' };
244
+ }
245
+ try {
246
+ const accountState = await hooks.submitRequestToBackground('perpsGetAccountState', []);
247
+ const ok = Boolean(accountState && typeof accountState === 'object');
248
+ return { ok, error: ok ? null : 'perpsGetAccountState returned an empty result' };
249
+ } catch (error) {
250
+ return { ok: false, error: String(error?.message || error) };
251
+ }
252
+ })(),
253
+ (async () => {
254
+ if (typeof hooks.submitRequestToBackground !== 'function') {
255
+ return { ok: false, error: 'submitRequestToBackground is not a function' };
256
+ }
257
+ if (typeof networkClientId !== 'string' || networkClientId.length === 0) {
258
+ return { ok: false, error: 'selected EVM network client is unavailable' };
259
+ }
260
+ try {
261
+ const code = await hooks.submitRequestToBackground('getCode', [
262
+ '0x0000000000000000000000000000000000000000',
263
+ networkClientId,
264
+ ]);
265
+ const ok = typeof code === 'string' && code.startsWith('0x');
266
+ return { ok, error: ok ? null : 'background EVM RPC returned an invalid result' };
267
+ } catch (error) {
268
+ return { ok: false, error: String(error?.message || error) };
269
+ }
270
+ })(),
271
+ ]);
237
272
  return {
238
273
  href: location.href,
239
274
  title: document.title,
@@ -242,14 +277,17 @@ async function evaluateHealth(session, timeoutMs) {
242
277
  hasStore: Boolean(hooks.store),
243
278
  hasPerpsStreamManager: typeof hooks.getPerpsStreamManager === 'function',
244
279
  backgroundUnresponsive: bodyText.includes('Background connection unresponsive') || bodyText.includes('MetaMask had trouble starting'),
280
+ ethereumConnectionUnavailable: bodyText.includes('Unable to connect to Ethereum'),
245
281
  activeProvider: storeState.metamask?.activeProvider || null,
246
282
  isTestnet: Boolean(storeState.metamask?.isTestnet),
247
283
  perpsManagerInitialized: Boolean(manager?.isInitialized?.()),
248
284
  positionsCacheIsArray: Array.isArray(manager?.positions?.cache),
249
285
  ordersCacheIsArray: Array.isArray(manager?.orders?.cache),
250
286
  accountCachePresent: Boolean(accountCache && typeof accountCache === 'object'),
251
- backgroundProbeOk,
252
- backgroundProbeError,
287
+ backgroundProbeOk: backgroundProbe.ok,
288
+ backgroundProbeError: backgroundProbe.error,
289
+ evmRpcProbeOk: evmRpcProbe.ok,
290
+ evmRpcProbeError: evmRpcProbe.error,
253
291
  };
254
292
  })(),
255
293
  new Promise((resolve) => setTimeout(() => resolve({
@@ -260,8 +298,11 @@ async function evaluateHealth(session, timeoutMs) {
260
298
  hasStore: Boolean(hooks.store),
261
299
  hasPerpsStreamManager: typeof hooks.getPerpsStreamManager === 'function',
262
300
  backgroundUnresponsive: bodyText.includes('Background connection unresponsive') || bodyText.includes('MetaMask had trouble starting'),
301
+ ethereumConnectionUnavailable: bodyText.includes('Unable to connect to Ethereum'),
263
302
  backgroundProbeOk: false,
264
303
  backgroundProbeError: 'perpsGetAccountState timed out after ${backgroundProbeTimeoutMs}ms',
304
+ evmRpcProbeOk: false,
305
+ evmRpcProbeError: 'background EVM RPC timed out after ${backgroundProbeTimeoutMs}ms',
265
306
  }), ${backgroundProbeTimeoutMs})),
266
307
  ]);
267
308
  })()`,
@@ -293,6 +334,9 @@ function compositorRecoveryAction(projectRoot) {
293
334
  const recovery = `mm-harness launch --verify --target ${JSON.stringify(projectRoot)}`;
294
335
  return process.platform === "darwin" ? `Unlock the macOS session, then rerun: ${recovery}` : `Restore an active display compositor, then rerun: ${recovery}`;
295
336
  }
337
+ function evmRpcRecoveryAction(projectRoot) {
338
+ return `Confirm ${shellQuote(path.join(projectRoot, ".metamaskrc"))} has a real INFURA_PROJECT_ID (not 00000000000), then rerun: mm-harness launch --verify --target ${shellQuote(projectRoot)}`;
339
+ }
296
340
  function resolveCdpPort(rawPort, slot) {
297
341
  const raw = rawPort ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT ?? slot?.cdpPort;
298
342
  const port = Number(raw);
@@ -759,6 +803,7 @@ export {
759
803
  assertHealthyExtensionRuntime,
760
804
  checkExtensionRuntimeHealth,
761
805
  compositorRecoveryAction,
806
+ evmRpcRecoveryAction,
762
807
  extensionBackgroundProbeTimeoutMs,
763
808
  formatHealthFailure,
764
809
  prepareExtensionRuntime
@@ -14,6 +14,10 @@ function watcherStatus(buildLog) {
14
14
  if (buildLog === "no-watch") return "down";
15
15
  return buildLog;
16
16
  }
17
+ function extensionRuntimeNextAction(reasonCode, target) {
18
+ const resolved = shellQuote(path.resolve(target));
19
+ return reasonCode === "deps-missing" || reasonCode === "deps-stale" ? `cd ${resolved} && yarn install --immutable` : `mm-harness launch --adapter extension --target ${resolved}`;
20
+ }
17
21
  const extensionSurface = {
18
22
  adapter: "extension",
19
23
  headless: false,
@@ -42,7 +46,7 @@ const extensionSurface = {
42
46
  decision: report.decision,
43
47
  reasonCode: report.reasonCode,
44
48
  reasons: report.reasons,
45
- nextAction: report.decision === "ready" ? void 0 : `mm-harness launch --adapter extension --target ${shellQuote(path.resolve(target))}`,
49
+ nextAction: report.decision === "ready" ? void 0 : extensionRuntimeNextAction(report.reasonCode, target),
46
50
  deps: report.checks.deps.status,
47
51
  devServer: {
48
52
  label: "webpack",
@@ -96,5 +100,6 @@ const extensionSurface = {
96
100
  }
97
101
  };
98
102
  export {
103
+ extensionRuntimeNextAction,
99
104
  extensionSurface
100
105
  };
@@ -5,6 +5,10 @@ import { shellQuote } from "../../commands/parse-args.js";
5
5
  import { resolveMobileSlotPorts } from "../slot-ports.js";
6
6
  import { mobileRuntimeStatus } from "./prepare.js";
7
7
  import { hasRunwayProvisionBaseline, provisionRunwayMobile } from "./provision.js";
8
+ function mobileRuntimeNextAction(reasonCode, target, platform) {
9
+ const resolved = shellQuote(path.resolve(target));
10
+ return reasonCode === "deps-missing" || reasonCode === "deps-partial" || reasonCode === "deps-stale" ? `cd ${resolved} && yarn setup` : `mm-harness launch ${platform} --adapter mobile --target ${resolved}`;
11
+ }
8
12
  const mobileSurface = {
9
13
  adapter: "mobile",
10
14
  headless: false,
@@ -21,7 +25,7 @@ const mobileSurface = {
21
25
  decision: depsPending ? "launch" : report.decision,
22
26
  reasonCode: depsPending ? "app-installed-deps-pending" : report.reasonCode,
23
27
  reasons: depsPending ? ["Runway app is installed; JavaScript dependencies are pending until dispatch-time launch."] : report.reasons,
24
- nextAction: report.decision === "ready" && !depsPending ? void 0 : `mm-harness launch ${platform} --adapter mobile --target ${shellQuote(path.resolve(target))}`,
28
+ nextAction: report.decision === "ready" && !depsPending ? void 0 : mobileRuntimeNextAction(report.reasonCode, target, platform),
25
29
  deps: runwayProvisioned && report.checks?.deps?.status !== "current" ? "pending" : report.checks?.deps?.status,
26
30
  devServer: { label: "metro", status: report.checks?.metro?.status ?? "unprobed" }
27
31
  };
@@ -58,5 +62,6 @@ const mobileSurface = {
58
62
  }
59
63
  };
60
64
  export {
65
+ mobileRuntimeNextAction,
61
66
  mobileSurface
62
67
  };
package/dist/adapters.js CHANGED
@@ -9,6 +9,7 @@ import { withExtensionPage } from "../library/actions/extension/platform/cdp.mjs
9
9
  import { bridgeCommand, evalAsync, evalSync, MOBILE_BRIDGE_ERROR_CODES, selectBridgeStatusEntry, simulatorScreenshot } from "../library/actions/mobile/platform/bridge.mjs";
10
10
  import { observeNativeUi } from "../library/actions/mobile/platform/observe-ui.mjs";
11
11
  import { resolveMobileToolPath } from "../library/actions/mobile/platform/tool-paths.mjs";
12
+ import { resolveWalletImportCredentials, validateWalletImportOptions } from "../library/actions/shared/wallet/import-source.mjs";
12
13
  const execFileAsync = promisify(execFile);
13
14
  const NATIVE_PROVIDER_UI_ACTIONS = /* @__PURE__ */ new Set([
14
15
  "ui.swipe",
@@ -51,6 +52,8 @@ const CORE_ONLY_PERPS_ACTIONS = /* @__PURE__ */ new Set([
51
52
  ]);
52
53
  const LIVE_ONLY_WALLET_ACTIONS = /* @__PURE__ */ new Set([
53
54
  "metamask.wallet.setup",
55
+ "metamask.wallet.import",
56
+ "metamask.wallet.reset",
54
57
  "metamask.wallet.ensure_unlocked",
55
58
  "metamask.wallet.select_account",
56
59
  "metamask.wallet.list_accounts",
@@ -110,6 +113,18 @@ async function semanticResult(platform, action, node, context, forceLive = false
110
113
  }
111
114
  const output = { platform, action, redacted: true };
112
115
  if (action === "metamask.wallet.fixture_status") return { output: fixtureSummary(context.projectRoot) };
116
+ if (action === "metamask.wallet.validate_import") {
117
+ validateWalletImportOptions(platform, node);
118
+ const credentials = await resolveWalletImportCredentials({ node, context });
119
+ return {
120
+ output: {
121
+ ...output,
122
+ credentialSource: credentials.source,
123
+ credentialSourceName: credentials.sourceName,
124
+ expectedAddress: credentials.expectedAddress
125
+ }
126
+ };
127
+ }
113
128
  return {
114
129
  output: {
115
130
  ...output,
@@ -183,7 +198,10 @@ function probeHttpJson(url, timeoutMs = 1e3) {
183
198
  function createMetaMaskSemanticAdapters(platform, declaredCustomActions = [], preparedLiveAdapters) {
184
199
  const walletActions = [
185
200
  "metamask.wallet.fixture_status",
201
+ "metamask.wallet.validate_import",
186
202
  "metamask.wallet.setup",
203
+ "metamask.wallet.import",
204
+ "metamask.wallet.reset",
187
205
  "metamask.wallet.ensure_unlocked",
188
206
  "metamask.wallet.select_account",
189
207
  "metamask.wallet.list_accounts",
@@ -15,7 +15,7 @@ const SPEC = {
15
15
  { name: "sync", desc: "Refresh harness + canonicalize wallet fixture", flags: ["--json"] },
16
16
  { name: "logs", aliases: ["tail"], desc: "Compact build events or full log", flags: ["--full", "-f", "--window", "--events", "--source", "--json"] },
17
17
  { name: "debug", aliases: ["devtools", "inspect"], desc: "Open DevTools UI", flags: ["--json", "--no-open"] },
18
- { name: "fixtures", desc: "Manage the canonical wallet fixture (sync/set/generate)", args: ["sync", "set", "generate"], flags: ["--fixture", "--out", "--adapter", "--target", "--device", "--json"] },
18
+ { name: "fixtures", desc: "Manage the canonical wallet fixture (sync/set/reset/generate)", args: ["sync", "set", "reset", "generate"], flags: ["--fixture", "--out", "--adapter", "--target", "--device", "--json"] },
19
19
  { name: "actions", desc: "List runnable recipe actions", flags: ["--json", "--matrix", "--categories", "--category", "--action", "--library"] },
20
20
  { name: "doctor", desc: "Check harness/orchestration health", flags: ["--json", "--target", "--adapter", "--runtime-dir", "--expect-live", "--print-ready", "--cdp-port", "--device"] },
21
21
  { name: "run", desc: "Execute a proof recipe (path or library name, e.g. run perps.smoke)", args: ["recipe.json|name"], flags: ["--list", "--device"] },
package/dist/cli.js CHANGED
@@ -47,8 +47,8 @@ DAILY LOOP \u2014 what a teammate runs many times a day:
47
47
  mm-harness logs
48
48
  debug Open the debug console (extension DevTools / mobile RN).
49
49
  mm-harness debug
50
- fixtures Sync files + set the wallet + generate fixture-state + finalize labels over CDP.
51
- mm-harness fixtures sync # or: set | generate --fixture <f> --out <o> | finalize \u2026
50
+ fixtures Sync files, set/reset the wallet, generate fixture-state, or finalize labels over CDP.
51
+ mm-harness fixtures sync # or: set | reset | generate --fixture <f> --out <o> | finalize \u2026
52
52
 
53
53
  PROVE \u2014 run recipes and inspect capabilities:
54
54
  run Run a recipe and write evidence (summary/trace/artifacts).
@@ -270,7 +270,7 @@ const PUBLIC_COMMAND_CONTRACTS = {
270
270
  "--extension-id-file": value(),
271
271
  "--action-manifest": value()
272
272
  }),
273
- positionals: [{ label: "action", choices: ["init", "sync", "set", "generate", "finalize"] }],
273
+ positionals: [{ label: "action", choices: ["init", "sync", "set", "reset", "generate", "finalize"] }],
274
274
  minimumPositionals: 0
275
275
  }
276
276
  };
@@ -256,6 +256,11 @@ ${formatConnectedDevices(connected)}
256
256
  userAction: deviceRecovery(options, opts.rerun)
257
257
  };
258
258
  }
259
+ if (targetable.length === 1) {
260
+ const selected = targetable[0];
261
+ if (selected.platform === "android") setAndroidDeviceEnv(selected.id, selected.name);
262
+ else setIosDeviceEnv(selected.id, selected.name);
263
+ }
259
264
  return { ok: true };
260
265
  }
261
266
  function scopedDevices(devices, allDevices = false) {