@deeeed/metamask-harness 0.2.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 (158) hide show
  1. package/CHANGELOG.md +161 -0
  2. package/README.md +140 -0
  3. package/bin/mm-harness +99 -0
  4. package/docs/CHEATSHEET.md +61 -0
  5. package/docs/CLI-SPEC.md +915 -0
  6. package/docs/MENTAL-MODEL.md +295 -0
  7. package/docs/architecture.md +367 -0
  8. package/docs/extension-runtime-commands.md +60 -0
  9. package/docs/harness-cli.md +43 -0
  10. package/docs/live-adapter-contract.md +188 -0
  11. package/docs/package-boundaries.md +47 -0
  12. package/docs/perps-flow-catalog.md +235 -0
  13. package/docs/recipe-libraries.md +95 -0
  14. package/docs/runtime-file-conventions.md +36 -0
  15. package/library/actions/core/perps/_controller.mjs +727 -0
  16. package/library/actions/core/perps/assert_orders.mjs +53 -0
  17. package/library/actions/core/perps/assert_positions.mjs +52 -0
  18. package/library/actions/core/perps/close_orders.mjs +97 -0
  19. package/library/actions/core/perps/close_positions.mjs +118 -0
  20. package/library/actions/core/perps/ensure_orders.mjs +40 -0
  21. package/library/actions/core/perps/ensure_positions.mjs +37 -0
  22. package/library/actions/core/perps/place_order.mjs +201 -0
  23. package/library/actions/core/perps/read_account.mjs +30 -0
  24. package/library/actions/core/perps/read_orders.mjs +27 -0
  25. package/library/actions/core/perps/read_positions.mjs +27 -0
  26. package/library/actions/core/perps/start_state.mjs +92 -0
  27. package/library/actions/core/perps/teardown_state.mjs +86 -0
  28. package/library/actions/extension/perps/assert_orders.mjs +11 -0
  29. package/library/actions/extension/perps/assert_positions.mjs +11 -0
  30. package/library/actions/extension/perps/close_orders.mjs +8 -0
  31. package/library/actions/extension/perps/close_positions.mjs +8 -0
  32. package/library/actions/extension/perps/ensure_orders.mjs +4 -0
  33. package/library/actions/extension/perps/ensure_positions.mjs +4 -0
  34. package/library/actions/extension/perps/perps.mjs +730 -0
  35. package/library/actions/extension/perps/place_order.mjs +7 -0
  36. package/library/actions/extension/perps/read_orders.mjs +4 -0
  37. package/library/actions/extension/perps/read_positions.mjs +3 -0
  38. package/library/actions/extension/platform/cdp.mjs +541 -0
  39. package/library/actions/extension/ui/navigate.mjs +44 -0
  40. package/library/actions/extension/wallet/ensure_unlocked.mjs +36 -0
  41. package/library/actions/extension/wallet/read_state.mjs +27 -0
  42. package/library/actions/extension/wallet/select_account.mjs +48 -0
  43. package/library/actions/extension/wallet/setup.mjs +35 -0
  44. package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +185 -0
  45. package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +1662 -0
  46. package/library/actions/mobile/bridge-runtime/cdp-bridge.cjs +686 -0
  47. package/library/actions/mobile/bridge-runtime/lib/cdp-eval.cjs +110 -0
  48. package/library/actions/mobile/bridge-runtime/lib/config.cjs +39 -0
  49. package/library/actions/mobile/bridge-runtime/lib/issue-capture.cjs +446 -0
  50. package/library/actions/mobile/bridge-runtime/lib/target-discovery.cjs +204 -0
  51. package/library/actions/mobile/bridge-runtime/lib/ws-client.cjs +108 -0
  52. package/library/actions/mobile/bridge-runtime/setup-wallet.sh +442 -0
  53. package/library/actions/mobile/perps/assert_orders.mjs +11 -0
  54. package/library/actions/mobile/perps/assert_positions.mjs +11 -0
  55. package/library/actions/mobile/perps/close_orders.mjs +8 -0
  56. package/library/actions/mobile/perps/close_positions.mjs +8 -0
  57. package/library/actions/mobile/perps/ensure_orders.mjs +4 -0
  58. package/library/actions/mobile/perps/ensure_positions.mjs +4 -0
  59. package/library/actions/mobile/perps/perps.mjs +709 -0
  60. package/library/actions/mobile/perps/place_order.mjs +7 -0
  61. package/library/actions/mobile/perps/read_orders.mjs +4 -0
  62. package/library/actions/mobile/perps/read_positions.mjs +3 -0
  63. package/library/actions/mobile/platform/bridge.mjs +283 -0
  64. package/library/actions/mobile/ui/navigate.mjs +38 -0
  65. package/library/actions/mobile/wallet/ensure_unlocked.mjs +107 -0
  66. package/library/actions/mobile/wallet/home.mjs +35 -0
  67. package/library/actions/mobile/wallet/read_state.mjs +40 -0
  68. package/library/actions/mobile/wallet/select_account.mjs +48 -0
  69. package/library/actions/mobile/wallet/setup.mjs +220 -0
  70. package/library/flows/perps.flows.json +64 -0
  71. package/library/library.json +7 -0
  72. package/library/manifests/core.action-manifest.json +1282 -0
  73. package/library/manifests/extension.action-manifest.json +1749 -0
  74. package/library/manifests/mobile.action-manifest.json +1753 -0
  75. package/library/recipes/action-validation.extension.recipe.json +417 -0
  76. package/library/recipes/action-validation.mobile.recipe.json +422 -0
  77. package/library/recipes/order-lifecycle.core.recipe.json +78 -0
  78. package/library/recipes/perps-lifecycle.recipe.json +194 -0
  79. package/library/recipes/read-markets.core.recipe.json +38 -0
  80. package/library/recipes/smoke.extension.recipe.json +31 -0
  81. package/library/recipes/smoke.mobile.recipe.json +31 -0
  82. package/library/recipes/trading-lifecycle.core.recipe.json +76 -0
  83. package/orchestration/compat-overlays/README.md +19 -0
  84. package/orchestration/compat-overlays/mobile/README.md +13 -0
  85. package/orchestration/compat-overlays/mobile/rn81-message-event-source.patch +42 -0
  86. package/orchestration/core/cleanup.sh +37 -0
  87. package/orchestration/core/inject.sh +154 -0
  88. package/orchestration/doctor.mjs +72 -0
  89. package/orchestration/extension/cleanup.mjs +60 -0
  90. package/orchestration/extension/console-tail.mjs +228 -0
  91. package/orchestration/extension/ensure-browser.sh +416 -0
  92. package/orchestration/extension/ensure-ready.ts +185 -0
  93. package/orchestration/extension/extension-id.ts +107 -0
  94. package/orchestration/extension/inject.mjs +266 -0
  95. package/orchestration/extension/launch-browser.cjs +216 -0
  96. package/orchestration/extension/launch.sh +175 -0
  97. package/orchestration/extension/live.sh +320 -0
  98. package/orchestration/extension/pin-remote-flags.cjs +45 -0
  99. package/orchestration/extension/readiness.mjs +414 -0
  100. package/orchestration/extension/refresh-build.sh +190 -0
  101. package/orchestration/extension/runtime-decision.ts +445 -0
  102. package/orchestration/extension/runtime.ts +407 -0
  103. package/orchestration/extension/seed-fixture.sh +177 -0
  104. package/orchestration/extension/sidepanel-toggle.sh +291 -0
  105. package/orchestration/extension/snapshot-dist.sh +84 -0
  106. package/orchestration/extension/start-watch.sh +339 -0
  107. package/orchestration/extension/wallet-fixture-state.cjs +1086 -0
  108. package/orchestration/lib/activate-repo-node.sh +144 -0
  109. package/orchestration/lib/cli-color.mjs +84 -0
  110. package/orchestration/lib/cli-commands.mjs +243 -0
  111. package/orchestration/lib/cli-home.mjs +354 -0
  112. package/orchestration/lib/cli-ux.sh +252 -0
  113. package/orchestration/lib/cli-version.mjs +123 -0
  114. package/orchestration/lib/ensure-runner-deps.sh +56 -0
  115. package/orchestration/lib/harness-path.sh +55 -0
  116. package/orchestration/lib/hash-helpers.sh +44 -0
  117. package/orchestration/lib/json-field.sh +23 -0
  118. package/orchestration/lib/log-tui.mjs +304 -0
  119. package/orchestration/lib/open-debug.mjs +317 -0
  120. package/orchestration/lib/path-defaults.json +4 -0
  121. package/orchestration/lib/progress.mjs +107 -0
  122. package/orchestration/lib/recipe-paths.mjs +26 -0
  123. package/orchestration/lib/resolve-farmslot-ports.sh +144 -0
  124. package/orchestration/manifest.json +358 -0
  125. package/orchestration/mobile/cleanup.sh +192 -0
  126. package/orchestration/mobile/deps-markers.ts +21 -0
  127. package/orchestration/mobile/inject.sh +681 -0
  128. package/orchestration/mobile/launch.sh +137 -0
  129. package/orchestration/mobile/live.sh +125 -0
  130. package/orchestration/mobile/runtime-decision.ts +292 -0
  131. package/orchestration/porcelain/metamask-recipe +99 -0
  132. package/orchestration/porcelain/mm-recipe +1591 -0
  133. package/orchestration/porcelain/mme-recipe +1181 -0
  134. package/package.json +59 -0
  135. package/runner/extension/verify.sh +511 -0
  136. package/runner/mobile/verify.sh +501 -0
  137. package/runner/src/adapters.ts +601 -0
  138. package/runner/src/cli.ts +1820 -0
  139. package/runner/src/commands/debug.ts +44 -0
  140. package/runner/src/commands/fixtures.ts +99 -0
  141. package/runner/src/commands/launch.ts +397 -0
  142. package/runner/src/commands/logs.ts +60 -0
  143. package/runner/src/commands/shared.ts +138 -0
  144. package/runner/src/completions-cache.ts +86 -0
  145. package/runner/src/doctor.ts +203 -0
  146. package/runner/src/harness.ts +516 -0
  147. package/runner/src/heal-bounds.ts +179 -0
  148. package/runner/src/index.ts +6 -0
  149. package/runner/src/live-adapter-contract.ts +274 -0
  150. package/runner/src/manifest.ts +47 -0
  151. package/runner/src/mm-harness-cli.ts +488 -0
  152. package/runner/src/paths.ts +198 -0
  153. package/runner/src/recording-target.ts +147 -0
  154. package/runner/src/run-recording.ts +329 -0
  155. package/runner/src/runner.ts +108 -0
  156. package/runner/src/types.ts +57 -0
  157. package/scripts/completions.sh +125 -0
  158. package/scripts/install-completions.sh +62 -0
@@ -0,0 +1,442 @@
1
+ #!/bin/bash
2
+ # Setup wallet from a JSON fixture via CDP + AgenticService.setupWallet().
3
+ #
4
+ # Expects a FRESH app (no existing vault). For a clean environment, run:
5
+ # yarn a:setup:ios # full clean setup + wallet
6
+ #
7
+ # Usage:
8
+ # setup-wallet.sh [--fixture path/to/fixture.json]
9
+ #
10
+ # Fixture JSON format:
11
+ # {
12
+ # "password": "yourpassword",
13
+ # "accounts": [
14
+ # { "type": "mnemonic", "value": "word1 word2 ...", "name": "Primary" },
15
+ # { "type": "privateKey", "value": "0xabc...", "name": "dev1" },
16
+ # { "type": "privateKey", "value": "0xdef...", "name": "dev2" }
17
+ # ],
18
+ # "settings": { "metametrics": true, "skipGtmModals": true, "skipPerpsTutorial": true, "autoLockNever": true, "deviceAuthEnabled": true }
19
+ # }
20
+
21
+ set -euo pipefail
22
+
23
+ # Resolve the script directory to an absolute path BEFORE cd, so sourcing and
24
+ # helper paths work regardless of the caller's CWD (e.g. ./setup-wallet.sh).
25
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
26
+ # Run from the target product checkout; the runner invokes this with cwd=projectRoot.
27
+ APP_ROOT="${APP_ROOT:-$PWD}"
28
+ cd "$APP_ROOT"
29
+
30
+ PORT="${WATCHER_PORT:-8081}"
31
+ [[ "$PORT" =~ ^[0-9]+$ ]] || { echo "ERROR: WATCHER_PORT must be numeric (got: $PORT)" >&2; exit 1; }
32
+ SCRIPTS="$SCRIPT_DIR"
33
+ # Account derivation (applyWalletFixture / setupWallet) is far slower than a
34
+ # normal eval — a 10+ account fixture easily exceeds 30s. The value is a cap, not
35
+ # a delay, so a high default is harmless for the fast evals. Override via env.
36
+ export CDP_TIMEOUT="${CDP_TIMEOUT:-120000}"
37
+ export CDP_DISCOVERY_RETRIES="${CDP_DISCOVERY_RETRIES:-3}"
38
+ WALLET_SETUP_SETTLE_TIMEOUT="${WALLET_SETUP_SETTLE_TIMEOUT:-180}"
39
+ CDP="node $SCRIPTS/cdp-bridge.cjs"
40
+ FIXTURE_PATH=""
41
+
42
+ cdp_json_command() {
43
+ local mode="$1"
44
+ local expression="$2"
45
+ local output=""
46
+ if ! output=$($CDP "$mode" "$expression" 2>&1); then
47
+ echo "ERROR: CDP ${mode} failed: $output" >&2
48
+ return 1
49
+ fi
50
+ if ! printf '%s\n' "$output" | jq -r '.'; then
51
+ echo "ERROR: CDP ${mode} returned non-JSON output: $output" >&2
52
+ return 1
53
+ fi
54
+ }
55
+ cdp_eval() { cdp_json_command eval "$1"; }
56
+ cdp_eval_async() { cdp_json_command eval-async "$1"; }
57
+
58
+ # -- Parse args --
59
+ while [[ $# -gt 0 ]]; do
60
+ case "$1" in
61
+ --fixture)
62
+ [ $# -ge 2 ] || { echo "ERROR: --fixture requires a path argument"; exit 2; }
63
+ FIXTURE_PATH="$2"; shift 2 ;;
64
+ -h|--help) echo "Usage: setup-wallet.sh [--fixture path.json]"; exit 0 ;;
65
+ *) echo "Unknown arg: $1"; exit 2 ;;
66
+ esac
67
+ done
68
+
69
+ # -- Resolve + validate fixture --
70
+ [ -z "$FIXTURE_PATH" ] && FIXTURE_PATH="${WALLET_FIXTURE:-${RECIPE_RUNTIME_DIR:-temp/recipe/runtime}/wallet-fixture.json}"
71
+
72
+ if [ ! -f "$FIXTURE_PATH" ]; then
73
+ echo "ERROR: Fixture not found: $FIXTURE_PATH"
74
+ echo " create ${RECIPE_RUNTIME_DIR:-temp/recipe/runtime}/wallet-fixture.json"
75
+ exit 1
76
+ fi
77
+ echo "Reading fixture: $FIXTURE_PATH"
78
+
79
+ jq empty "$FIXTURE_PATH" 2>/dev/null || { echo "ERROR: Invalid JSON"; exit 1; }
80
+
81
+ PASSWORD=$(jq -r '.password // empty' "$FIXTURE_PATH")
82
+ [ -z "$PASSWORD" ] && { echo "ERROR: fixture missing 'password'"; exit 1; }
83
+
84
+ if jq -e '.import' "$FIXTURE_PATH" >/dev/null 2>&1 && ! jq -e '.accounts' "$FIXTURE_PATH" >/dev/null 2>&1; then
85
+ echo "ERROR: Old format. Use an accounts array in the wallet fixture."
86
+ exit 1
87
+ fi
88
+
89
+ ACCOUNT_COUNT=$(jq -r '.accounts | length // 0' "$FIXTURE_PATH" 2>/dev/null || echo 0)
90
+ for i in $(seq 0 $((ACCOUNT_COUNT - 1))); do
91
+ ACC_TYPE=$(jq -r ".accounts[$i].type // empty" "$FIXTURE_PATH")
92
+ ACC_VALUE=$(jq -r ".accounts[$i].value // empty" "$FIXTURE_PATH")
93
+ if [ -z "$ACC_TYPE" ] || { [ "$ACC_TYPE" != "mnemonic" ] && [ "$ACC_TYPE" != "privateKey" ]; }; then
94
+ echo "ERROR: accounts[$i].type must be 'mnemonic' or 'privateKey' (got '${ACC_TYPE:-empty}')"
95
+ echo " See the Recipe runner Mobile fixture format."
96
+ exit 1
97
+ fi
98
+ [ -z "$ACC_VALUE" ] && { echo "ERROR: accounts[$i].value is empty"; exit 1; }
99
+ done
100
+ # Expected EVM account total = sum of mnemonic counts + one per private key.
101
+ # A mnemonic entry with count=N materializes N HD accounts, not one, so the
102
+ # entry count alone (ACCOUNT_COUNT) under-counts and would let setup pass while
103
+ # silently missing HD accounts.
104
+ EXPECTED_ETH_TOTAL=$(jq -r '[.accounts[] | if .type == "mnemonic" then (.count // .numberOfAccounts // 1) else 1 end] | add // 0' "$FIXTURE_PATH")
105
+ echo "Fixture OK: password + ${ACCOUNT_COUNT} entry(ies), ${EXPECTED_ETH_TOTAL} expected EVM account(s)"
106
+
107
+ wait_for_cdp_bridge() {
108
+ local waited=0
109
+ local max="${CDP_READY_TIMEOUT:-90}"
110
+ local output=""
111
+ while [ "$waited" -lt "$max" ]; do
112
+ if output=$($CDP eval "JSON.stringify({ok:true})" 2>&1); then
113
+ echo "CDP bridge connected."
114
+ return 0
115
+ fi
116
+ sleep 1
117
+ waited=$((waited + 1))
118
+ [ "$waited" -eq 5 ] && echo "Waiting for CDP bridge..."
119
+ done
120
+ echo "ERROR: CDP not reachable after ${max}s"
121
+ [ -n "$output" ] && echo "$output" >&2
122
+ return 1
123
+ }
124
+
125
+ # -- Check CDP --
126
+ wait_for_cdp_bridge
127
+
128
+ wait_for_engine() {
129
+ local waited=0
130
+ local max="${ENGINE_READY_TIMEOUT:-60}"
131
+ local engine_ok=""
132
+ while [ "$waited" -lt "$max" ]; do
133
+ engine_ok=$(cdp_eval "(function(){ var engine = globalThis.Engine; return engine && engine.context && engine.context.KeyringController ? 'ready' : 'waiting'; })()" 2>/dev/null || echo "")
134
+ [ "$engine_ok" = "ready" ] && return 0
135
+ sleep 1
136
+ waited=$((waited + 1))
137
+ [ "$waited" -eq 5 ] && echo "Waiting for Engine to initialize..."
138
+ done
139
+ echo "ERROR: Engine.context.KeyringController not available after ${max}s"
140
+ return 1
141
+ }
142
+
143
+ # -- Wait for Engine to be ready (KeyringController must exist) --
144
+ wait_for_engine
145
+
146
+ # The backup subscriber reads the Engine *class* static
147
+ # `disableAutomaticVaultBackup`. AgenticService.setupWallet/applyWalletFixture
148
+ # set that static authoritatively before any account import/rename. The
149
+ # CDP-exposed `Engine` is the facade object, not the class, so we cannot set the
150
+ # static from here — only record harness intent for observability.
151
+ if ! DISABLE_BACKUP=$(cdp_eval "(function(){ globalThis.__AGENTIC_DISABLE_VAULT_BACKUP = true; return JSON.stringify({agenticDisableVaultBackup: globalThis.__AGENTIC_DISABLE_VAULT_BACKUP === true}); })()"); then
152
+ echo "WARN: could not record vault backup guard intent before wallet setup"
153
+ DISABLE_BACKUP='{}'
154
+ fi
155
+ echo "Vault backup guard intent: $DISABLE_BACKUP"
156
+ wait_for_engine
157
+
158
+ # Read fixture JSON and escape it for safe embedding in a JS string literal.
159
+ FIXTURE_JSON=$(jq -c '.' "$FIXTURE_PATH")
160
+ ESCAPED_FIXTURE=$(node -p "JSON.stringify(JSON.stringify(JSON.parse(process.argv[1])))" "$FIXTURE_JSON")
161
+ EXPECTED_ADDRESSES=$(node - "$FIXTURE_PATH" <<'NODE'
162
+ const fs = require('fs');
163
+ const { Wallet } = require('ethers');
164
+ const fixture = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
165
+ const addresses = [];
166
+ for (const account of fixture.accounts || []) {
167
+ if (account.type === 'mnemonic') {
168
+ const rawCount =
169
+ account.count != null
170
+ ? account.count
171
+ : account.numberOfAccounts != null
172
+ ? account.numberOfAccounts
173
+ : 1;
174
+ const count = Number(rawCount);
175
+ for (let i = 0; i < count; i += 1) {
176
+ addresses.push(
177
+ Wallet.fromMnemonic(account.value, `m/44'/60'/0'/0/${i}`).address.toLowerCase(),
178
+ );
179
+ }
180
+ } else if (account.type === 'privateKey') {
181
+ const key = account.value.startsWith('0x') ? account.value : `0x${account.value}`;
182
+ addresses.push(new Wallet(key).address.toLowerCase());
183
+ }
184
+ }
185
+ console.log(JSON.stringify(addresses));
186
+ NODE
187
+ )
188
+ FIRST_EXPECTED_ADDRESS=$(EXPECTED_JSON="$EXPECTED_ADDRESSES" node <<'NODE'
189
+ const addresses = JSON.parse(process.env.EXPECTED_JSON || '[]');
190
+ process.stdout.write(addresses[0] || '');
191
+ NODE
192
+ )
193
+
194
+ read_wallet_state() {
195
+ cdp_eval "(function(){ try { var engine = globalThis.Engine; var ctx = engine && engine.context ? engine.context : {}; var accountsController = ctx.AccountsController || {}; var keyringController = ctx.KeyringController || {}; var state = accountsController.state || {}; var internalAccounts = state.internalAccounts || {}; var accountsById = internalAccounts.accounts || {}; var accs = Object.values(accountsById); var eth = accs.filter(function(a){ return String(a && a.address || '').indexOf('0x') === 0; }); var route = globalThis.__AGENTIC__ && globalThis.__AGENTIC__.getRoute ? globalThis.__AGENTIC__.getRoute() : null; var selected = null; if (typeof accountsController.getSelectedAccount === 'function') { selected = accountsController.getSelectedAccount(); } else if (internalAccounts.selectedAccount && accountsById[internalAccounts.selectedAccount]) { selected = accountsById[internalAccounts.selectedAccount]; } return JSON.stringify({ok:true, unlocked: typeof keyringController.isUnlocked === 'function' ? keyringController.isUnlocked() : null, routeName: route && route.name, selected: selected ? {name: selected.metadata && selected.metadata.name, address: selected.address} : null, total: accs.length, ethAccounts: eth.length, accounts: eth.map(function(a){ return {name: a && a.metadata && a.metadata.name, address: a && a.address}; }), first3: eth.slice(0,3).map(function(a){ return {name: a && a.metadata && a.metadata.name, address: a && a.address}; })}); } catch (e) { return JSON.stringify({ok:false, error: e && (e.stack || e.message) || String(e)}); } })()"
196
+ }
197
+
198
+ missing_fixture_addresses() {
199
+ ACCOUNTS_JSON="$1" EXPECTED_JSON="$EXPECTED_ADDRESSES" node <<'NODE'
200
+ const accounts = JSON.parse(process.env.ACCOUNTS_JSON);
201
+ const expected = JSON.parse(process.env.EXPECTED_JSON);
202
+ const actual = new Set((accounts.accounts || accounts.first3 || []).map((a) => String(a.address || '').toLowerCase()));
203
+ const missing = expected.filter((address) => !actual.has(address));
204
+ console.log(JSON.stringify(missing));
205
+ NODE
206
+ }
207
+
208
+ wallet_state_issue() {
209
+ local accounts_json="$1"
210
+ if [ -z "$accounts_json" ]; then
211
+ echo "wallet state unavailable"
212
+ return
213
+ fi
214
+
215
+ local ok unlocked eth_count missing_count
216
+ ok=$(echo "$accounts_json" | jq -r '.ok // false' 2>/dev/null || echo false)
217
+ if [ "$ok" != "true" ]; then
218
+ echo "$(echo "$accounts_json" | jq -r '.error // "wallet state read failed"' 2>/dev/null || echo "wallet state read failed")"
219
+ return
220
+ fi
221
+
222
+ unlocked=$(echo "$accounts_json" | jq -r '.unlocked')
223
+ eth_count=$(echo "$accounts_json" | jq -r '.ethAccounts')
224
+ if [ "$unlocked" != "true" ]; then
225
+ echo "vault still locked"
226
+ return
227
+ fi
228
+ if [ "$eth_count" -lt "$EXPECTED_ETH_TOTAL" ]; then
229
+ echo "$eth_count ETH account(s), expected at least $EXPECTED_ETH_TOTAL"
230
+ return
231
+ fi
232
+
233
+ missing_count=$(missing_fixture_addresses "$accounts_json" | jq -r 'length')
234
+ if [ "$missing_count" != "0" ]; then
235
+ echo "$missing_count expected fixture address(es) missing"
236
+ return
237
+ fi
238
+
239
+ echo "ready"
240
+ }
241
+
242
+ wallet_state_ready() {
243
+ [ "$(wallet_state_issue "$1")" = "ready" ]
244
+ }
245
+
246
+ wallet_fixture_present() {
247
+ local accounts_json="$1"
248
+ local ok eth_count missing_count
249
+ [ -n "$accounts_json" ] || return 1
250
+ ok=$(echo "$accounts_json" | jq -r '.ok // false' 2>/dev/null || echo false)
251
+ [ "$ok" = "true" ] || return 1
252
+ eth_count=$(echo "$accounts_json" | jq -r '.ethAccounts // 0' 2>/dev/null || echo 0)
253
+ [ "$eth_count" -ge "$EXPECTED_ETH_TOTAL" ] || return 1
254
+ missing_count=$(missing_fixture_addresses "$accounts_json" | jq -r 'length')
255
+ [ "$missing_count" = "0" ]
256
+ }
257
+
258
+ PRECHECK_ACCOUNTS=$(read_wallet_state || true)
259
+ if wallet_state_ready "$PRECHECK_ACCOUNTS"; then
260
+ echo "Wallet fixture already valid; skipping wallet mutation."
261
+ else
262
+
263
+ wait_for_wallet_state_after_eval_timeout() {
264
+ local label="$1"
265
+ local rc="$2"
266
+ local elapsed=0
267
+ local interval=5
268
+ local last_accounts=""
269
+ local issue=""
270
+
271
+ echo "WARN: ${label} eval did not return (rc=$rc) before ${CDP_TIMEOUT}ms; validating app wallet state for up to ${WALLET_SETUP_SETTLE_TIMEOUT}s."
272
+ while [ "$elapsed" -le "$WALLET_SETUP_SETTLE_TIMEOUT" ]; do
273
+ last_accounts=$(read_wallet_state 2>/dev/null || true)
274
+ if wallet_state_ready "$last_accounts"; then
275
+ echo "Wallet state validated after ${label} eval timeout."
276
+ return 0
277
+ fi
278
+
279
+ issue=$(wallet_state_issue "$last_accounts")
280
+ [ "$elapsed" -eq 0 ] && echo " Still waiting for fixture completion: $issue"
281
+ sleep "$interval"
282
+ elapsed=$((elapsed + interval))
283
+ done
284
+
285
+ echo "ERROR: ${label} eval failed or timed out (rc=$rc), and wallet state did not validate after ${WALLET_SETUP_SETTLE_TIMEOUT}s."
286
+ echo "Last observed state:"
287
+ if [ -n "$last_accounts" ]; then
288
+ echo "$last_accounts" | jq .
289
+ else
290
+ echo " unavailable"
291
+ fi
292
+ return 1
293
+ }
294
+
295
+ # -- Check vault state --
296
+ VAULT_STATE=$(cdp_eval "(function(){ var engine = globalThis.Engine; var ctx = engine && engine.context; var keyringController = ctx && ctx.KeyringController; if (!keyringController) return JSON.stringify({ready:false, hasVault:false, isUnlocked:false}); var v = keyringController.state || {}; return JSON.stringify({ready:true, hasVault: v.vault !== undefined && v.vault !== null, isUnlocked: keyringController.isUnlocked()}); })()")
297
+ HAS_VAULT=$(echo "$VAULT_STATE" | jq -r '.hasVault')
298
+ IS_UNLOCKED=$(echo "$VAULT_STATE" | jq -r '.isUnlocked')
299
+ echo "Vault state: hasVault=$HAS_VAULT, isUnlocked=$IS_UNLOCKED"
300
+
301
+ if [ "$HAS_VAULT" = "true" ]; then
302
+ if wallet_fixture_present "$PRECHECK_ACCOUNTS"; then
303
+ if [ "$IS_UNLOCKED" = "true" ]; then
304
+ echo "Wallet fixture already present; vault is unlocked. Skipping wallet mutation."
305
+ else
306
+ echo "Wallet fixture already present; unlocking existing vault without wallet mutation."
307
+ $CDP unlock "$PASSWORD" >/dev/null
308
+ for _ in $(seq 1 30); do
309
+ POST_UNLOCK_ACCOUNTS=$(read_wallet_state 2>/dev/null || true)
310
+ if wallet_state_ready "$POST_UNLOCK_ACCOUNTS"; then
311
+ echo "Existing fixture vault unlocked."
312
+ break
313
+ fi
314
+ sleep 1
315
+ done
316
+ if ! wallet_state_ready "${POST_UNLOCK_ACCOUNTS:-}"; then
317
+ echo "WARN: existing fixture vault did not validate after unlock; falling back to fixture apply."
318
+ fi
319
+ fi
320
+ fi
321
+
322
+ if wallet_state_ready "${POST_UNLOCK_ACCOUNTS:-$PRECHECK_ACCOUNTS}"; then
323
+ :
324
+ else
325
+ # Do NOT unlock here with a bare KeyringController.submitPassword(): that
326
+ # bypasses the real auth flow (multichain init + dispatchLogin/password state)
327
+ # and can leave Redux/auth state stale. applyWalletFixture tries the real
328
+ # Authentication.unlockWallet() path first, then recovers interrupted fixture
329
+ # state with a keyring fallback inside AgenticService.
330
+ if [ "$IS_UNLOCKED" = "true" ]; then
331
+ echo "Vault already unlocked."
332
+ else
333
+ echo "Vault locked — applyWalletFixture will unlock via auth flow with fixture recovery fallback."
334
+ fi
335
+
336
+ echo "Applying fixture accounts/names to existing vault..."
337
+ set +e
338
+ APPLY_RESULT=$(cdp_eval_async "(function(){ var fixture = JSON.parse($ESCAPED_FIXTURE); if (!globalThis.__AGENTIC__ || typeof globalThis.__AGENTIC__.applyWalletFixture !== 'function') { return JSON.stringify({ok:false, error:'__AGENTIC__.applyWalletFixture is not installed; reload the app from Metro'}); } return globalThis.__AGENTIC__.applyWalletFixture(fixture).then(function(r){ return JSON.stringify(r); }).catch(function(e){ return JSON.stringify({ok:false, error: e.message || String(e)}); }); })()")
339
+ APPLY_RC=$?
340
+ set -e
341
+ if [ "$APPLY_RC" -ne 0 ] || [ -z "$APPLY_RESULT" ]; then
342
+ wait_for_wallet_state_after_eval_timeout "applyWalletFixture" "$APPLY_RC" || exit 1
343
+ echo "Wallet fixture apply result recovered from validated wallet state."
344
+ else
345
+ APPLY_OK=$(echo "$APPLY_RESULT" | jq -r '.ok')
346
+ if [ "$APPLY_OK" != "true" ]; then
347
+ APPLY_ERR=$(echo "$APPLY_RESULT" | jq -r '.error // "unknown error"')
348
+ echo "ERROR: applyWalletFixture failed — $APPLY_ERR"
349
+ exit 1
350
+ fi
351
+ echo "Wallet fixture apply result:"
352
+ echo "$APPLY_RESULT" | jq -r '.accounts[]? | " \(.name): \(.address)"'
353
+ fi
354
+ fi
355
+ else
356
+ # -- Call AgenticService.setupWallet() on fresh app only --
357
+ echo "Calling __AGENTIC__.setupWallet()..."
358
+
359
+ set +e
360
+ SETUP_RESULT=$(cdp_eval_async "(function(){ var fixture = JSON.parse($ESCAPED_FIXTURE); if (!globalThis.__AGENTIC__ || typeof globalThis.__AGENTIC__.setupWallet !== 'function') { return JSON.stringify({ok:false, error:'__AGENTIC__.setupWallet is not installed'}); } return globalThis.__AGENTIC__.setupWallet(fixture).then(function(r){ return JSON.stringify(r); }).catch(function(e){ return JSON.stringify({ok:false, error: e.message || String(e)}); }); })()")
361
+ SETUP_RC=$?
362
+ set -e
363
+ if [ "$SETUP_RC" -ne 0 ] || [ -z "$SETUP_RESULT" ]; then
364
+ wait_for_wallet_state_after_eval_timeout "setupWallet" "$SETUP_RC" || exit 1
365
+ echo "Wallet setup result recovered from validated wallet state."
366
+ else
367
+ SETUP_OK=$(echo "$SETUP_RESULT" | jq -r '.ok')
368
+ if [ "$SETUP_OK" != "true" ]; then
369
+ SETUP_ERR=$(echo "$SETUP_RESULT" | jq -r '.error // "unknown error"')
370
+ SETUP_STEP=$(echo "$SETUP_RESULT" | jq -r '.step // "unknown-step"')
371
+ echo "ERROR: setupWallet failed at ${SETUP_STEP} — $SETUP_ERR"
372
+ exit 1
373
+ fi
374
+
375
+ echo "Wallet setup result:"
376
+ echo "$SETUP_RESULT" | jq -r '.accounts[]? | " \(.name): \(.address)"'
377
+ fi
378
+ sleep 2
379
+ fi
380
+ fi
381
+
382
+ # -- Ask the app to leave auth/onboarding after unlock.
383
+ # HomeNav matches the product auth reset path, but some warm/onboarding states
384
+ # land on intermediate post-onboarding screens. Follow with WalletView so the
385
+ # harness proves the user-visible unlocked wallet, not just a populated vault.
386
+ $CDP navigate HomeNav >/dev/null 2>&1 || true
387
+ sleep 1
388
+ $CDP navigate WalletView >/dev/null 2>&1 || true
389
+ sleep 1
390
+ if [ -n "$FIRST_EXPECTED_ADDRESS" ]; then
391
+ cdp_eval "(function(){ try { if (globalThis.__AGENTIC__ && typeof globalThis.__AGENTIC__.switchAccount === 'function') { return JSON.stringify(globalThis.__AGENTIC__.switchAccount('$FIRST_EXPECTED_ADDRESS')); } return JSON.stringify({switched:false, error:'__AGENTIC__.switchAccount unavailable'}); } catch (e) { return JSON.stringify({switched:false, error:e && (e.message || String(e))}); } })()" >/dev/null 2>&1 || true
392
+ fi
393
+
394
+ # -- Summary + hard validation --
395
+ ACCOUNTS=$(read_wallet_state)
396
+ ACCOUNTS_OK=$(echo "$ACCOUNTS" | jq -r '.ok // false')
397
+ if [ "$ACCOUNTS_OK" != "true" ]; then
398
+ echo "ERROR: Unable to read wallet state after setupWallet"
399
+ echo "$ACCOUNTS" | jq .
400
+ exit 1
401
+ fi
402
+ TOTAL=$(echo "$ACCOUNTS" | jq -r '.total')
403
+ ETH_COUNT=$(echo "$ACCOUNTS" | jq -r '.ethAccounts')
404
+ UNLOCKED=$(echo "$ACCOUNTS" | jq -r '.unlocked')
405
+ ROUTE_NAME=$(echo "$ACCOUNTS" | jq -r '.routeName // empty')
406
+ MISSING_ADDRESSES=$(missing_fixture_addresses "$ACCOUNTS")
407
+ if [ "$UNLOCKED" != "true" ]; then
408
+ echo "ERROR: Wallet setup did not unlock the vault"
409
+ echo "$ACCOUNTS" | jq .
410
+ exit 1
411
+ fi
412
+ if [ "$ETH_COUNT" -lt "$EXPECTED_ETH_TOTAL" ]; then
413
+ echo "ERROR: Wallet setup produced $ETH_COUNT ETH account(s), expected at least $EXPECTED_ETH_TOTAL from fixture"
414
+ echo "$ACCOUNTS" | jq .
415
+ exit 1
416
+ fi
417
+ if [ "$(echo "$MISSING_ADDRESSES" | jq 'length')" != "0" ]; then
418
+ echo "ERROR: Wallet setup did not import/unlock the expected fixture account(s)"
419
+ echo "Missing addresses:"
420
+ echo "$MISSING_ADDRESSES" | jq -r '.[] | " " + .'
421
+ echo "Actual wallet state:"
422
+ echo "$ACCOUNTS" | jq .
423
+ exit 1
424
+ fi
425
+ case "$ROUTE_NAME" in
426
+ Login|Onboarding|ExperienceEnhancer|FoxLoader|"")
427
+ echo "ERROR: Wallet setup did not reach the unlocked wallet UI (route=${ROUTE_NAME:-empty})"
428
+ echo "$ACCOUNTS" | jq .
429
+ exit 1
430
+ ;;
431
+ esac
432
+ echo ""
433
+ echo "=== Wallet Ready ==="
434
+ echo "Route: ${ROUTE_NAME:-unknown}"
435
+ echo "Unlocked: $UNLOCKED"
436
+ echo "Accounts: $ETH_COUNT ETH (${TOTAL} total)"
437
+ echo "$ACCOUNTS" | jq -r '.first3[] | " \(.name): \(.address)"'
438
+ echo "Selected:"
439
+ echo "$ACCOUNTS" | jq -r '.selected | " \(.name): \(.address)"'
440
+ echo ""
441
+ echo "Done."
442
+ exit 0
@@ -0,0 +1,11 @@
1
+ import { runAdapter } from '../platform/bridge.mjs';
2
+ import { assertOrders } from './perps.mjs';
3
+
4
+ function expectedOpen(input) {
5
+ const state = String(input.node?.state ?? input.node?.orders ?? 'open').toLowerCase();
6
+ if (state === 'open' || state === 'present') return true;
7
+ if (state === 'none' || state === 'closed' || state === 'absent') return false;
8
+ throw new Error(`metamask.perps.assert_orders received unsupported state: ${state}`);
9
+ }
10
+
11
+ runAdapter((input) => assertOrders(input, expectedOpen(input)));
@@ -0,0 +1,11 @@
1
+ import { runAdapter } from '../platform/bridge.mjs';
2
+ import { assertPositions } from './perps.mjs';
3
+
4
+ function expectedOpen(input) {
5
+ const state = String(input.node?.state ?? input.node?.position ?? 'open').toLowerCase();
6
+ if (state === 'open' || state === 'present') return true;
7
+ if (state === 'none' || state === 'closed' || state === 'absent') return false;
8
+ throw new Error(`metamask.perps.assert_positions received unsupported state: ${state}`);
9
+ }
10
+
11
+ runAdapter((input) => assertPositions(input, expectedOpen(input)));
@@ -0,0 +1,8 @@
1
+ import { runAdapter } from '../platform/bridge.mjs';
2
+ import { assertOrders, closeOrders } from './perps.mjs';
3
+
4
+ runAdapter(async (input) => {
5
+ const close = await closeOrders(input);
6
+ const assertion = await assertOrders(input, false);
7
+ return { ...assertion, close };
8
+ });
@@ -0,0 +1,8 @@
1
+ import { runAdapter } from '../platform/bridge.mjs';
2
+ import { assertPositions, closePositions } from './perps.mjs';
3
+
4
+ runAdapter(async (input) => {
5
+ const close = await closePositions(input);
6
+ const assertion = await assertPositions(input, false);
7
+ return { ...assertion, close };
8
+ });
@@ -0,0 +1,4 @@
1
+ import { runAdapter } from '../platform/bridge.mjs';
2
+ import { ensureOrders } from './perps.mjs';
3
+
4
+ runAdapter(ensureOrders);
@@ -0,0 +1,4 @@
1
+ import { runAdapter } from '../platform/bridge.mjs';
2
+ import { ensurePositions } from './perps.mjs';
3
+
4
+ runAdapter(ensurePositions);