@deeeed/metamask-harness 0.79.0 → 0.81.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 (252) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +1 -1
  3. package/adapters/extension/ensure-browser.sh +2 -2
  4. package/adapters/extension/inject.mjs +0 -1
  5. package/adapters/extension/launch-browser.cjs +39 -613
  6. package/adapters/extension/readiness.mjs +2 -5
  7. package/adapters/extension/reattach.sh +2 -2
  8. package/adapters/extension/stamp-runtime-title.cjs +1 -1
  9. package/adapters/manifest.json +16 -64
  10. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +57 -1358
  11. package/adapters/mobile/bridge-runtime/teaching.cjs +20 -0
  12. package/adapters/mobile/inject.sh +16 -0
  13. package/adapters/mobile/launch-console-forwarder.cjs +1 -2
  14. package/adapters/mobile/open-device.sh +2 -2
  15. package/adapters/mobile/reload-app.mjs +4 -4
  16. package/adapters/mobile/reset-app-data.sh +1 -1
  17. package/adapters/mobile/start-console-forwarder.sh +3 -2
  18. package/adapters/mobile/start-metro.sh +17 -12
  19. package/adapters/mobile/wait-for-bridge.cjs +2 -2
  20. package/adapters/shared/harness-source-fingerprint.mjs +13 -8
  21. package/adapters/shared/open-debug.mjs +2 -2
  22. package/adapters/shared/sync-wallet-fixture.sh +8 -5
  23. package/adapters/terminal/cleanup.mjs +2 -1
  24. package/adapters/terminal/inject.mjs +2 -1
  25. package/adapters/terminal/launch.mjs +17 -13
  26. package/adapters/terminal/lib/mainnet-guard.mjs +9 -0
  27. package/adapters/terminal/lib/readiness.mjs +20 -12
  28. package/adapters/terminal/policy.mjs +35 -0
  29. package/adapters/terminal/verify.mjs +10 -3
  30. package/adapters/terminal/wallet-host.mjs +43 -225
  31. package/bin/mm-harness +9 -28
  32. package/dist/adapters/checkout-files.js +16 -0
  33. package/dist/adapters/console-capture.js +14 -1
  34. package/dist/adapters/core/actions.js +8 -4
  35. package/dist/adapters/core/overlay.js +1 -1
  36. package/dist/adapters/core/readiness.js +28 -1
  37. package/dist/adapters/core/run.js +2 -2
  38. package/dist/adapters/core/surface.js +16 -1
  39. package/dist/adapters/extension/actions.js +6 -2
  40. package/dist/adapters/extension/browser-pid.js +56 -0
  41. package/dist/adapters/extension/console-capture.js +1 -1
  42. package/dist/adapters/extension/doctor-checks.js +1 -1
  43. package/dist/adapters/extension/ensure-ready.js +5 -4
  44. package/dist/adapters/extension/extension-id.js +1 -1
  45. package/dist/adapters/extension/feature-flags.js +1 -1
  46. package/dist/adapters/extension/fixtures.js +1 -2
  47. package/dist/adapters/extension/launch.js +4 -5
  48. package/dist/adapters/extension/launched-browser.js +1 -2
  49. package/dist/adapters/extension/network-observer.js +14 -1
  50. package/dist/adapters/extension/overlay.js +1 -1
  51. package/dist/adapters/extension/product-config.js +1 -1
  52. package/dist/adapters/extension/readiness.js +1 -1
  53. package/dist/adapters/extension/reload.js +1 -1
  54. package/dist/adapters/extension/run.js +135 -14
  55. package/dist/adapters/extension/runtime-decision.js +2 -1
  56. package/dist/adapters/extension/runtime.js +9 -5
  57. package/dist/adapters/extension/surface.js +15 -2
  58. package/dist/adapters/extension/verify.js +2 -2
  59. package/dist/adapters/harness-freshness.js +2 -1
  60. package/dist/adapters/mobile/actions.js +4 -1
  61. package/dist/adapters/mobile/android-gfxinfo-performance.js +1 -3
  62. package/dist/adapters/mobile/app-identity.js +10 -0
  63. package/dist/adapters/mobile/doctor-checks.js +1 -1
  64. package/dist/adapters/mobile/fixtures.js +1 -1
  65. package/dist/adapters/mobile/launch.js +27 -28
  66. package/dist/adapters/mobile/lifecycle.js +1 -1
  67. package/dist/adapters/mobile/metro-env.js +9 -59
  68. package/dist/adapters/mobile/network-observer.js +68 -0
  69. package/dist/adapters/mobile/performance-observer.js +2 -4
  70. package/dist/adapters/mobile/prepare-target.js +1 -1
  71. package/dist/adapters/mobile/prepare.js +3 -4
  72. package/dist/adapters/mobile/provision.js +1 -1
  73. package/dist/adapters/mobile/readiness.js +1 -1
  74. package/dist/adapters/mobile/recording-target.js +15 -0
  75. package/dist/adapters/mobile/release-artifact-state.js +1 -1
  76. package/dist/adapters/mobile/reload.js +1 -1
  77. package/dist/adapters/mobile/run.js +39 -15
  78. package/dist/adapters/mobile/runtime-decision.js +2 -4
  79. package/dist/adapters/mobile/source-freshness.js +22 -126
  80. package/dist/adapters/mobile/surface.js +26 -3
  81. package/dist/adapters/mobile/ui.js +16 -5
  82. package/dist/adapters/mobile/verify.js +2 -3
  83. package/dist/adapters/performance/cdp-trace.js +1 -1
  84. package/dist/adapters/performance/device-platform.js +1 -1
  85. package/dist/adapters/perps-order-inputs.js +36 -0
  86. package/dist/adapters/proof-failure.js +11 -0
  87. package/dist/adapters/slot-ports.js +1 -1
  88. package/dist/adapters/surface.js +1 -0
  89. package/dist/adapters/terminal/actions.js +4 -1
  90. package/dist/adapters/terminal/console-capture.js +1 -1
  91. package/dist/adapters/terminal/doctor-checks.js +2 -0
  92. package/dist/adapters/terminal/launch.js +4 -1
  93. package/dist/adapters/terminal/overlay.js +8 -1
  94. package/dist/adapters/terminal/run.js +33 -5
  95. package/dist/adapters/terminal/surface.js +9 -1
  96. package/dist/adapters.js +27 -7
  97. package/dist/advisor-policy.js +1 -1
  98. package/dist/checkout-view.js +1 -2
  99. package/dist/cli.js +59 -21
  100. package/dist/command-contract.js +1 -26
  101. package/dist/commands/check.js +33 -10
  102. package/dist/commands/checklist.js +1 -1
  103. package/dist/commands/completion-candidates.js +6 -14
  104. package/dist/commands/config.js +2 -2
  105. package/dist/commands/core-readiness.js +12 -7
  106. package/dist/commands/debug.js +3 -3
  107. package/dist/commands/device-target.js +5 -5
  108. package/dist/commands/doctor.js +2 -14
  109. package/dist/commands/domain.js +2 -2
  110. package/dist/commands/ensure-ready.js +1 -7
  111. package/dist/commands/fixtures.js +5 -6
  112. package/dist/commands/help.js +3 -4
  113. package/dist/commands/launch/device.js +10 -17
  114. package/dist/commands/launch/extension.js +2 -8
  115. package/dist/commands/launch/mobile.js +1 -1
  116. package/dist/commands/logs.js +3 -3
  117. package/dist/commands/mobile-device-view.js +4 -2
  118. package/dist/commands/overlay.js +110 -0
  119. package/dist/commands/pin.js +1 -1
  120. package/dist/commands/pr-body.js +2 -2
  121. package/dist/commands/prepare.js +4 -9
  122. package/dist/commands/provision.js +3 -12
  123. package/dist/commands/recipe-advice.js +1 -2
  124. package/dist/commands/recipe-quality.js +2 -3
  125. package/dist/commands/resolve-extension.js +1 -7
  126. package/dist/commands/review.js +4 -4
  127. package/dist/commands/runtime-decision.js +1 -6
  128. package/dist/commands/runtime-health.js +1 -7
  129. package/dist/commands/runtime-launch.js +1 -13
  130. package/dist/commands/self-test.js +5 -8
  131. package/dist/commands/status-watch.js +1 -2
  132. package/dist/commands/status.js +2 -5
  133. package/dist/commands/task-init.js +2 -3
  134. package/dist/commands/tutorial.js +2 -2
  135. package/dist/commands/update.js +1 -1
  136. package/dist/completions-cache.js +1 -1
  137. package/dist/console-classifier.js +19 -0
  138. package/dist/doctor.js +2 -3
  139. package/dist/engine.js +57 -0
  140. package/dist/funding-execution-context.js +96 -1
  141. package/dist/host.js +32 -0
  142. package/dist/index.js +1 -6
  143. package/dist/live-tsx.js +49 -0
  144. package/dist/mm-harness-cli.js +4 -6
  145. package/dist/paths.js +34 -64
  146. package/dist/performance-observation.js +3 -5
  147. package/dist/progress.js +1 -1
  148. package/dist/recipe-assistance-context.js +6 -9
  149. package/dist/recipe-security.js +2 -59
  150. package/dist/review/knowledge.js +2 -2
  151. package/dist/runner.js +7 -4
  152. package/dist/runtime-context.js +1 -1
  153. package/dist/task-view.js +2 -3
  154. package/docs/CONTRIBUTING.md +5 -3
  155. package/docs/RECIPES.md +31 -3
  156. package/docs/cli-contract.md +2 -2
  157. package/docs/feature-guard.md +90 -0
  158. package/library/README.md +3 -1
  159. package/library/actions/core/perps/_controller.mjs +153 -134
  160. package/library/actions/core/perps/_rewards.mjs +186 -0
  161. package/library/actions/core/perps/close_positions.mjs +1 -1
  162. package/library/actions/core/perps/place_order.mjs +167 -45
  163. package/library/actions/core/perps/quote_fees.mjs +61 -0
  164. package/library/actions/core/wallet/list_accounts.mjs +14 -57
  165. package/library/actions/extension/feature_flags/state.mjs +1 -1
  166. package/library/actions/extension/perps/close_visible_position.mjs +5 -0
  167. package/library/actions/extension/perps/edit_margin.mjs +4 -0
  168. package/library/actions/extension/perps/mutation-receipt.mjs +95 -3
  169. package/library/actions/extension/perps/perps.mjs +320 -3
  170. package/library/actions/extension/perps/place_order.mjs +3 -1
  171. package/library/actions/extension/perps/update_position_tpsl.mjs +4 -0
  172. package/library/actions/extension/platform/cdp.mjs +73 -11
  173. package/library/actions/harness-exports.mjs +1 -6
  174. package/library/actions/mobile/app/network-control.mjs +6 -8
  175. package/library/actions/mobile/perps/perps.mjs +216 -11
  176. package/library/actions/mobile/perps/place_order.mjs +3 -1
  177. package/library/actions/mobile/perps/prepare_local_snapshot_endpoint.mjs +1 -1
  178. package/library/actions/mobile/platform/bridge.mjs +8 -8
  179. package/library/actions/mobile/platform/native-session-name.mjs +21 -1
  180. package/library/actions/mobile/platform/native-session.mjs +3 -4
  181. package/library/actions/mobile/platform/observe-ui.mjs +12 -6
  182. package/library/actions/mobile/ui/native-navigation.mjs +4 -3
  183. package/library/actions/mobile/wallet/native-ui.mjs +3 -2
  184. package/library/actions/shared/analytics/_adapter.mjs +1 -1
  185. package/library/actions/shared/console-findings.mjs +1 -1
  186. package/library/actions/shared/perps/order-policy.mjs +177 -0
  187. package/library/actions/shared/wallet/fixture-accounts.mjs +84 -0
  188. package/library/actions/terminal/app/launch.mjs +3 -2
  189. package/library/actions/terminal/platform/hud.mjs +2 -1
  190. package/library/actions/terminal/platform/page.mjs +2 -1
  191. package/library/actions/terminal/platform/runtime.mjs +2 -1
  192. package/library/actions/terminal/wallet/_signature-log.mjs +40 -161
  193. package/library/actions/terminal/wallet/assert_signatures.mjs +6 -12
  194. package/library/actions/terminal/wallet/read_signatures.mjs +10 -9
  195. package/library/manifests/core.action-manifest.json +87 -13
  196. package/library/manifests/extension.action-manifest.json +4 -0
  197. package/library/manifests/mobile.action-manifest.json +12 -0
  198. package/package.json +8 -9
  199. package/site/assets/help-recipes.json +1 -0
  200. package/adapters/extension/lib/slot-title.cjs +0 -292
  201. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +0 -472
  202. package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +0 -125
  203. package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +0 -1055
  204. package/adapters/mobile/bridge-runtime/lib/cdp-eval.cjs +0 -110
  205. package/adapters/mobile/bridge-runtime/lib/config.cjs +0 -61
  206. package/adapters/mobile/bridge-runtime/lib/console-format.cjs +0 -53
  207. package/adapters/mobile/bridge-runtime/lib/devtools-proxy.cjs +0 -177
  208. package/adapters/mobile/bridge-runtime/lib/issue-capture.cjs +0 -446
  209. package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +0 -84
  210. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +0 -403
  211. package/adapters/mobile/bridge-runtime/lib/ws-client.cjs +0 -116
  212. package/adapters/mobile/coalesce-metro-log.cjs +0 -24
  213. package/adapters/mobile/launch-metro.cjs +0 -77
  214. package/adapters/mobile/metro-config.cjs +0 -98
  215. package/adapters/mobile/metro-log-generation.cjs +0 -106
  216. package/adapters/terminal/lib/origin.mjs +0 -30
  217. package/adapters/terminal/lib/page-script.mjs +0 -246
  218. package/adapters/terminal/lib/strict-wallet.mjs +0 -138
  219. package/dist/adapters/mobile/frame-metrics.js +0 -118
  220. package/dist/adapters/mobile/video-recorder.js +0 -289
  221. package/dist/artifact-files.js +0 -92
  222. package/dist/checkout-lock.js +0 -149
  223. package/dist/cli-color.js +0 -98
  224. package/dist/command-journal.js +0 -308
  225. package/dist/commands/call.js +0 -685
  226. package/dist/commands/last.js +0 -60
  227. package/dist/commands/launch/index.js +0 -109
  228. package/dist/commands/list-executables.js +0 -247
  229. package/dist/commands/manifest.js +0 -488
  230. package/dist/commands/parse-args.js +0 -279
  231. package/dist/commands/reload.js +0 -41
  232. package/dist/commands/run-engine.js +0 -1340
  233. package/dist/commands/run-report.js +0 -241
  234. package/dist/commands/run.js +0 -751
  235. package/dist/commands/shared.js +0 -323
  236. package/dist/commands/stop.js +0 -60
  237. package/dist/devices.js +0 -100
  238. package/dist/discovery.js +0 -15
  239. package/dist/execution-provenance.js +0 -332
  240. package/dist/harness.js +0 -451
  241. package/dist/heal-bounds.js +0 -137
  242. package/dist/json-stream.js +0 -60
  243. package/dist/leaf-invoke.js +0 -19
  244. package/dist/library-provenance.js +0 -33
  245. package/dist/live-adapter-contract.js +0 -380
  246. package/dist/metamask-action-validation.js +0 -68
  247. package/dist/network-observation.js +0 -290
  248. package/dist/recording-target.js +0 -120
  249. package/dist/run-diagnostics.js +0 -364
  250. package/dist/run-recording.js +0 -481
  251. package/dist/runtime-proof.js +0 -90
  252. package/library/actions/mobile/platform/tool-paths.mjs +0 -123
@@ -14,6 +14,14 @@ import {
14
14
  import { ensureUnlocked } from '../wallet/ensure_unlocked.mjs';
15
15
  import { selectAccount } from '../wallet/select_account.mjs';
16
16
  import { observeNativeUi } from '../platform/observe-ui.mjs';
17
+ import {
18
+ assertFlatBeforeRoundTrip,
19
+ assertHouseLimitPosition,
20
+ closeImmediatelyRequested,
21
+ orderNetwork,
22
+ requireHouseNotional,
23
+ requirePerpsOrderAllowed,
24
+ } from '../../shared/perps/order-policy.mjs';
17
25
 
18
26
  function sleep(ms) {
19
27
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -1281,7 +1289,7 @@ function closePositionParams(input, position) {
1281
1289
  return params;
1282
1290
  }
1283
1291
 
1284
- async function closePositionItem(input, position) {
1292
+ async function closePositionItem(input, position, bound = null) {
1285
1293
  const maxAttempts = Number(input.node?.close_attempts ?? 3);
1286
1294
  const retryDelayMs = Number(input.node?.close_retry_delay_ms ?? 1000);
1287
1295
  const deadline = deadlineFromTimeout(
@@ -1293,7 +1301,7 @@ async function closePositionItem(input, position) {
1293
1301
  let lastResult = null;
1294
1302
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
1295
1303
  if (Date.now() >= deadline) break;
1296
- const currentPositions = await readPositions(input);
1304
+ const currentPositions = bound ? await readBoundPositions(input, bound) : await readPositions(input);
1297
1305
  const currentPosition = currentPositions.find((candidate) => symbolForItem(candidate) === baseParams.symbol);
1298
1306
  if (!currentPosition) return { symbol: baseParams.symbol, result: { success: true, alreadyClosed: true }, attempts };
1299
1307
 
@@ -1301,8 +1309,12 @@ async function closePositionItem(input, position) {
1301
1309
  const params = { ...baseParams, position: currentPosition, priceAtCalculation };
1302
1310
  const result = await evalAsync(
1303
1311
  input,
1304
- `Engine.context.PerpsController.closePosition(${JSON.stringify(params)}).then(function(r){return JSON.stringify(r)})`,
1312
+ `(function(){ ${boundContextGuard(bound)}
1313
+ return Engine.context.PerpsController.closePosition(${sourceLiteral(params)}).then(function(r){return JSON.stringify(r)}); })()`,
1305
1314
  );
1315
+ if (result?.contextChanged) {
1316
+ throw new Error(`Close of ${baseParams.symbol} stopped: the selected account or network changed; nothing more was sent.`);
1317
+ }
1306
1318
  attempts.push({ attempt, priceAtCalculation, result });
1307
1319
  lastResult = result;
1308
1320
  if (result?.success === false) {
@@ -1394,7 +1406,37 @@ export async function placeOrder(input) {
1394
1406
  const maxSlippageBps = Number(input.node?.max_slippage_bps ?? input.node?.maxSlippageBps ?? 300);
1395
1407
  const isBuy = orderSideIsBuy(side);
1396
1408
  const orderType = resolveOrderType(input);
1409
+ const closeImmediately = closeImmediatelyRequested(input.node);
1410
+ const runtime = await readPerpsRuntimeState(input);
1411
+ const network = orderNetwork({ declared: input.node?.network, liveIsTestnet: runtime?.isTestnet });
1412
+ // The account and network the rule allows, rechecked at dispatch and before any close.
1413
+ const bound = { network, address: String((await readSelectedAddress(input)) ?? '').toLowerCase() };
1414
+ const policy = await requirePerpsOrderAllowed({
1415
+ network,
1416
+ amount,
1417
+ leverage,
1418
+ orderType,
1419
+ closeImmediately,
1420
+ projectRoot: input.context?.projectRoot,
1421
+ selectedAddress: bound.address,
1422
+ });
1423
+ if ((closeImmediately || network === 'mainnet') && input.node?.size != null) {
1424
+ throw new Error('close_immediately and mainnet orders are sized from amount; drop size.');
1425
+ }
1426
+ if (closeImmediately) {
1427
+ const book = await readBoundBook(input, bound);
1428
+ assertFlatBeforeRoundTrip(book.positions, symbol, symbolForItem, book.orders);
1429
+ }
1397
1430
  const currentPrice = await currentPriceForOrder(input, symbol);
1431
+ const houseNotional = requireHouseNotional({
1432
+ network,
1433
+ symbol,
1434
+ amount,
1435
+ leverage,
1436
+ price: currentPrice,
1437
+ szDecimals: network === 'mainnet' ? await readSizeDecimals(input, symbol) : null,
1438
+ maxSlippageBps,
1439
+ });
1398
1440
  const limitPrice = orderType === 'limit'
1399
1441
  ? resolveLimitPrice(input, isBuy, currentPrice)
1400
1442
  : null;
@@ -1426,16 +1468,179 @@ export async function placeOrder(input) {
1426
1468
  priceAtCalculation: currentPrice,
1427
1469
  maxSlippageBps,
1428
1470
  };
1429
- const result = await evalAsync(input, `Engine.context.PerpsController.placeOrder(${JSON.stringify(orderParams)}).then(function(r){return JSON.stringify(r)})`);
1430
- if (result?.success === false || result == null) {
1431
- throw new Error(`Failed to place ${symbol} ${side}: ${result?.error || JSON.stringify(result)}`);
1471
+ if (await tradingContextChanged(input, bound)) {
1472
+ throw new Error('Perps order not dispatched: the selected account or network changed after the order rule allowed it.');
1473
+ }
1474
+ let result;
1475
+ try {
1476
+ result = await evalAsync(input, `(function(){ ${boundContextGuard(bound)}
1477
+ return Engine.context.PerpsController.placeOrder(${sourceLiteral(orderParams)}).then(function(r){return JSON.stringify(r)}); })()`);
1478
+ if (result?.contextChanged) {
1479
+ throw Object.assign(
1480
+ new Error('Perps order not dispatched: the selected account or network changed after the order rule allowed it.'),
1481
+ { knownNotDispatched: true },
1482
+ );
1483
+ }
1484
+ if (result?.success === false || result == null) {
1485
+ throw new Error(`Failed to place ${symbol} ${side}: ${result?.error || JSON.stringify(result)}`);
1486
+ }
1487
+ if (orderType === 'limit') {
1488
+ await waitForSelectedState(input, readOpenOrders, true, Number(input.node?.timeout_ms ?? 30000));
1489
+ } else {
1490
+ await waitForPositionPresent(input, symbol, Number(input.node?.timeout_ms ?? 30000));
1491
+ }
1492
+ } catch (error) {
1493
+ if (!closeImmediately || error?.knownNotDispatched) throw error;
1494
+ throw await closeAfterFailedPlacement(input, symbol, error, bound);
1495
+ }
1496
+ const placed = { action: input.action, market: symbol, side, orderType, amount, size, leverage, limitPrice, submitted: true, result, network, policy: policy.lane, ...(houseNotional ? { houseNotional } : {}), proofPath: 'mobile-perps-controller-place-order' };
1497
+ if (!closeImmediately) return placed;
1498
+ return { ...placed, roundTrip: await closeOpenedPosition(input, symbol, bound) };
1499
+ }
1500
+
1501
+ // close_immediately started from a flat market (checked before dispatch), so a
1502
+ // position in it after a failed placement is this order's: close it rather
1503
+ // than leave it open, and say what happened.
1504
+ async function closeAfterFailedPlacement(input, symbol, error, bound) {
1505
+ const message = error instanceof Error ? error.message : String(error);
1506
+ let outcome;
1507
+ try {
1508
+ if (await tradingContextChanged(input, bound)) {
1509
+ return new Error(`${message} close_immediately: the selected account or network changed, so nothing was closed; check ${symbol} on the original account now.`);
1510
+ }
1511
+ const position = (await readBoundPositions(input, bound)).find((candidate) => symbolForItem(candidate) === symbol);
1512
+ if (!position) {
1513
+ outcome = `no ${symbol} position is open`;
1514
+ } else {
1515
+ await closePositionItem(fullCloseInput(input), position, bound);
1516
+ outcome = await waitForBoundPositionAbsent(input, bound, symbol, Number(input.node?.timeout_ms ?? 30000))
1517
+ ? `the ${symbol} position it opened was closed`
1518
+ : `the ${symbol} position is STILL OPEN after a close; close it now`;
1519
+ }
1520
+ } catch (closeError) {
1521
+ outcome = `the ${symbol} position could not be checked or closed (${closeError instanceof Error ? closeError.message : String(closeError)}); check it now`;
1522
+ }
1523
+ return new Error(`${message} close_immediately: ${outcome}.`);
1524
+ }
1525
+
1526
+ async function readSizeDecimals(input, symbol) {
1527
+ const markets = await evalAsync(
1528
+ input,
1529
+ `Engine.context.PerpsController.getMarkets({ symbols: [${sourceLiteral(symbol)}] }).then(function(r){return JSON.stringify((r||[]).map(function(m){return { name: m.name, szDecimals: m.szDecimals };}))})`,
1530
+ );
1531
+ return (Array.isArray(markets) ? markets : []).find((market) => normalizeMarketSymbol(market?.name ?? '') === symbol)?.szDecimals ?? null;
1532
+ }
1533
+
1534
+ // The close takes the whole position: the order's own size or amount never
1535
+ // becomes a partial close size.
1536
+ function fullCloseInput(input) {
1537
+ const { size: _size, amount: _amount, notional: _notional, ...node } = input.node ?? {};
1538
+ return { ...input, node };
1539
+ }
1540
+
1541
+ // Bridge sources embed values as JSON literals. JSON.stringify leaves `<`, `>`,
1542
+ // `/` and U+2028/U+2029 raw; escape them so no value can end a script context
1543
+ // or a statement in the evaluated source.
1544
+ const UNSAFE_SOURCE_CHARS = { '<': '\\u003C', '>': '\\u003E', '/': '\\u002F', '\u2028': '\\u2028', '\u2029': '\\u2029' };
1545
+ export function sourceLiteral(value) {
1546
+ return JSON.stringify(value).replace(/[<>/\u2028\u2029]/gu, (char) => UNSAFE_SOURCE_CHARS[char]);
1547
+ }
1548
+
1549
+ // Bridge source that resolves to { contextChanged: true } unless the selected
1550
+ // account and Perps network are still the bound ones. It runs in the same
1551
+ // evaluation as the controller call it guards, so a switch can't slip between.
1552
+ function boundContextGuard(bound) {
1553
+ if (!bound) return '';
1554
+ return `var __account = Engine.context.AccountsController.getSelectedAccount();
1555
+ var __address = String((__account && __account.address) || '').toLowerCase();
1556
+ var __network = Engine.context.PerpsController.state.isTestnet ? 'testnet' : 'mainnet';
1557
+ if (__address !== ${sourceLiteral(bound.address)} || __network !== ${sourceLiteral(bound.network)}) {
1558
+ return Promise.resolve(JSON.stringify({ success: false, contextChanged: true }));
1559
+ }`;
1560
+ }
1561
+
1562
+ // The positions of the bound account on the bound network, read in one guarded
1563
+ // evaluation; a changed context throws instead of reading another account.
1564
+ async function readBoundPositions(input, bound) {
1565
+ const positions = await evalAsync(input, `(function(){ ${boundContextGuard(bound)}
1566
+ return Engine.context.PerpsController.getPositions().then(function(r){return JSON.stringify(r)}); })()`);
1567
+ if (positions?.contextChanged) {
1568
+ throw new Error('Perps positions not read: the selected account or network changed after the order rule allowed it. Check the order\'s market on the original account now.');
1569
+ }
1570
+ if (!Array.isArray(positions)) throw new Error('PerpsController.getPositions returned a non-array result.');
1571
+ return positions;
1572
+ }
1573
+
1574
+ // The flat-market baseline: the bound account's positions and resting orders,
1575
+ // read in one evaluation with the context guard before, between and after the
1576
+ // reads, so a switch to another account's book and back can't pass as flat.
1577
+ async function readBoundBook(input, bound) {
1578
+ const guard = `(function(){ ${boundContextGuard(bound)} return null; })()`;
1579
+ const book = await evalAsync(input, `(function(){
1580
+ var controller = Engine.context.PerpsController;
1581
+ var positions;
1582
+ return ${guard} || controller.getPositions().then(function(r){
1583
+ positions = r;
1584
+ return ${guard} || controller.getOpenOrders().then(function(orders){
1585
+ return ${guard} || JSON.stringify({ positions: positions, orders: orders });
1586
+ });
1587
+ }); })()`);
1588
+ if (book?.contextChanged) {
1589
+ throw new Error('Perps order not dispatched: the selected account or network changed while the market was checked flat.');
1590
+ }
1591
+ if (!Array.isArray(book?.positions) || !Array.isArray(book?.orders)) {
1592
+ throw new Error('PerpsController.getPositions or getOpenOrders returned a non-array result.');
1593
+ }
1594
+ return book;
1595
+ }
1596
+
1597
+ async function waitForBoundPositionAbsent(input, bound, symbol, timeoutMs) {
1598
+ const deadline = Date.now() + timeoutMs;
1599
+ for (;;) {
1600
+ const positions = await readBoundPositions(input, bound);
1601
+ if (!positions.some((position) => symbolForItem(position) === symbol)) return true;
1602
+ if (Date.now() >= deadline) return false;
1603
+ await sleep(1000);
1604
+ }
1605
+ }
1606
+
1607
+ async function tradingContextChanged(input, bound) {
1608
+ const runtime = await readPerpsRuntimeState(input);
1609
+ const network = runtime?.isTestnet === true ? 'testnet' : runtime?.isTestnet === false ? 'mainnet' : null;
1610
+ const address = String((await readSelectedAddress(input)) ?? '').toLowerCase();
1611
+ return network !== bound.network || address !== bound.address;
1612
+ }
1613
+
1614
+ async function readSelectedAddress(input) {
1615
+ return evalAsync(input, 'JSON.stringify(Engine.context.AccountsController.getSelectedAccount()?.address ?? null)');
1616
+ }
1617
+
1618
+ // close_immediately: close the position this order opened and prove the market
1619
+ // is flat. On mainnet the opened position must be isolated 1x; it is closed
1620
+ // either way before that check fails the action.
1621
+ async function closeOpenedPosition(input, symbol, bound) {
1622
+ const { network } = bound;
1623
+ if (await tradingContextChanged(input, bound)) {
1624
+ throw new Error(`close_immediately did not close ${symbol}: the selected account or network changed after the order. The order's position is still open on the original account; close it now.`);
1625
+ }
1626
+ const position = (await readBoundPositions(input, bound)).find((candidate) => symbolForItem(candidate) === symbol);
1627
+ if (!position) {
1628
+ throw new Error(`close_immediately could not find the ${symbol} position the order opened; it may still fill. Check ${symbol} now and close it if it opened.`);
1629
+ }
1630
+ let limitError = null;
1631
+ if (network === 'mainnet') {
1632
+ try {
1633
+ assertHouseLimitPosition(position, symbol);
1634
+ } catch (error) {
1635
+ limitError = error;
1636
+ }
1432
1637
  }
1433
- if (orderType === 'limit') {
1434
- await waitForSelectedState(input, readOpenOrders, true, Number(input.node?.timeout_ms ?? 30000));
1435
- } else {
1436
- await waitForPositionPresent(input, symbol, Number(input.node?.timeout_ms ?? 30000));
1638
+ const closed = await closePositionItem(fullCloseInput(input), position, bound);
1639
+ if (!(await waitForBoundPositionAbsent(input, bound, symbol, Number(input.node?.timeout_ms ?? 30000)))) {
1640
+ throw new Error(`close_immediately left the ${symbol} position open: ${JSON.stringify(redactPosition(position))}. Close it now.`);
1437
1641
  }
1438
- return { action: input.action, market: symbol, side, orderType, amount, size, leverage, limitPrice, submitted: true, result, proofPath: 'mobile-perps-controller-place-order' };
1642
+ if (limitError) throw limitError;
1643
+ return { opened: redactPosition(position), closed: true, result: closed.result };
1439
1644
  }
1440
1645
 
1441
1646
  export async function ensurePositions(input) {
@@ -1,8 +1,10 @@
1
1
  // Submit a Perps order through the Mobile controller and assert a matching open position.
2
+ // With close_immediately the order's position is already closed again, so the market must be flat.
2
3
  import { runAdapter } from '../platform/bridge.mjs';
4
+ import { closeImmediatelyRequested } from '../../shared/perps/order-policy.mjs';
3
5
  import { assertPositions, placeOrder } from './perps.mjs';
4
6
  runAdapter(async (input) => {
5
7
  const order = await placeOrder(input);
6
- const assertion = await assertPositions(input, true);
8
+ const assertion = await assertPositions(input, !closeImmediatelyRequested(input.node));
7
9
  return { ...assertion, submitted: order.submitted === true, order };
8
10
  });
@@ -4,7 +4,7 @@ import { execFile } from 'node:child_process';
4
4
  import { promisify } from 'node:util';
5
5
 
6
6
  import { runAdapter } from '../platform/bridge.mjs';
7
- import { resolveMobileToolPath } from '../platform/tool-paths.mjs';
7
+ import { resolveMobileToolPath } from '@farmslot/adapter-rn';
8
8
 
9
9
  const execFileAsync = promisify(execFile);
10
10
 
@@ -8,10 +8,10 @@ import { promisify } from 'node:util';
8
8
  import os from 'node:os';
9
9
  import path from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
- import bridgeErrors from '../../../../adapters/mobile/bridge-runtime/lib/bridge-errors.cjs';
12
- import brokerModule from '../../../../adapters/mobile/bridge-runtime/lib/cdp-broker.cjs';
13
- import configModule from '../../../../adapters/mobile/bridge-runtime/lib/config.cjs';
14
- import { resolveMobileToolPath } from './tool-paths.mjs';
11
+ import bridgeErrors from '@farmslot/adapter-rn/bridge-runtime/lib/bridge-errors.cjs';
12
+ import brokerModule from '@farmslot/recipe-runner/cdp-broker';
13
+ import configModule from '@farmslot/adapter-rn/bridge-runtime/lib/config.cjs';
14
+ import { resolveMobileToolPath } from '@farmslot/adapter-rn';
15
15
 
16
16
  const {
17
17
  BRIDGE_ERROR_CODES,
@@ -155,8 +155,8 @@ export async function bridgeEnv(input) {
155
155
  ...(input.context?.env || {}),
156
156
  CDP_TIMEOUT: String(input.node?.cdp_timeout_ms ?? process.env.CDP_TIMEOUT ?? '10000'),
157
157
  };
158
- if (adbPath) env.MM_HARNESS_ADB_PATH = adbPath;
159
- if (idbPath) env.MM_HARNESS_IDB_PATH = idbPath;
158
+ if (adbPath) env.RECIPE_RN_ADB_PATH = adbPath;
159
+ if (idbPath) env.RECIPE_RN_IDB_PATH = idbPath;
160
160
  const target = resolveMobileTarget(input);
161
161
  const requestedPlatform = target.requestedPlatform;
162
162
  const watcherPort = target.watcherPort;
@@ -238,8 +238,8 @@ export function resolveMobileTarget(input) {
238
238
  input.node?.sim_udid ?? contextEnv.SIM_UDID ?? process.env.SIM_UDID;
239
239
  const configuredPlatform = String(
240
240
  input.node?.platform ??
241
- contextEnv.MM_HARNESS_EXPLICIT_PLATFORM ??
242
- process.env.MM_HARNESS_EXPLICIT_PLATFORM ??
241
+ contextEnv.RECIPE_RN_EXPLICIT_PLATFORM ??
242
+ process.env.RECIPE_RN_EXPLICIT_PLATFORM ??
243
243
  contextEnv.PLATFORM ??
244
244
  process.env.PLATFORM ??
245
245
  '',
@@ -1,7 +1,27 @@
1
1
  // Derive native session names and device-specific state directories, honoring configured overrides.
2
2
  import path from 'node:path';
3
3
 
4
- export function nativeSessionName(env, device, app = 'io.metamask') {
4
+ // The host adapter supplies the app under test in the action env
5
+ // (ANDROID_PACKAGE_ID / IOS_BUNDLE_ID, display name in
6
+ // METAMASK_RECIPE_MOBILE_APP_LABEL); the library has no default app.
7
+ export function nativeAppId(env, platform) {
8
+ const key = platform === 'android' ? 'ANDROID_PACKAGE_ID' : 'IOS_BUNDLE_ID';
9
+ const app = String(env[key] ?? '').trim();
10
+ if (!app) {
11
+ throw new Error(`Mobile native UI needs the app identity in ${key}. Next: run through mm-harness run or call, or set ${key}.`);
12
+ }
13
+ return app;
14
+ }
15
+
16
+ export function nativeAppLabel(env) {
17
+ const label = String(env.METAMASK_RECIPE_MOBILE_APP_LABEL ?? '').trim();
18
+ if (!label) {
19
+ throw new Error('Mobile native UI needs the app display name in METAMASK_RECIPE_MOBILE_APP_LABEL. Next: run through mm-harness run or call, or set METAMASK_RECIPE_MOBILE_APP_LABEL.');
20
+ }
21
+ return label;
22
+ }
23
+
24
+ export function nativeSessionName(env, device, app) {
5
25
  const configured = String(env.METAMASK_RECIPE_MOBILE_SESSION ?? '').trim();
6
26
  const source = configured || `mm-harness-${app}-${device}`;
7
27
  return source.replace(/[^A-Za-z0-9._-]/gu, '-').slice(0, 120);
@@ -1,6 +1,6 @@
1
1
  // Create device-targeted native UI sessions for actions and accessibility snapshots.
2
2
  // Opening an instrumented iOS session hides the accessibility-masking HUD.
3
- import { createAgentDeviceUiTransport } from '@farmslot/expo-recipe';
3
+ import { createAgentDeviceUiTransport } from '@farmslot/adapter-rn';
4
4
  import { createAgentDeviceClient } from 'agent-device';
5
5
 
6
6
  import { resolveMobileTarget } from './bridge.mjs';
@@ -8,6 +8,7 @@ import { hideAccessibilityMaskingHud } from './observe-ui.mjs';
8
8
 
9
9
  import {
10
10
  nativeAgentDeviceStateDir,
11
+ nativeAppId,
11
12
  nativeSessionName,
12
13
  } from './native-session-name.mjs';
13
14
 
@@ -19,9 +20,7 @@ export function createNativeSession(input, domain, sessionSuffix = '') {
19
20
  ? target.adbSerial ?? ''
20
21
  : input.node?.sim_udid ?? env.SIM_UDID ?? target.iosSimulator ?? '').trim();
21
22
  if (!device) throw new Error(`Mobile ${domain} native actions require one explicit ${platform} device. Next: pass --device <device>.`);
22
- const app = String(platform === 'android'
23
- ? env.ANDROID_PACKAGE_ID ?? 'io.metamask'
24
- : env.IOS_BUNDLE_ID ?? 'io.metamask.MetaMask');
23
+ const app = nativeAppId(env, platform);
25
24
  const baseSession = nativeSessionName(env, device, app);
26
25
  const session = sessionSuffix
27
26
  ? `${baseSession}-${String(sessionSuffix).replace(/[^A-Za-z0-9._-]/gu, '-')}`.slice(0, 120)
@@ -7,9 +7,11 @@ import { createAgentDeviceClient } from 'agent-device';
7
7
  import { bridgeCommand } from './bridge.mjs';
8
8
  import {
9
9
  nativeAgentDeviceStateDir,
10
+ nativeAppId,
11
+ nativeAppLabel,
10
12
  nativeSessionName,
11
13
  } from './native-session-name.mjs';
12
- import { mobileToolRecovery, resolveMobileToolPath } from './tool-paths.mjs';
14
+ import { mobileToolRecovery, resolveMobileToolPath } from '@farmslot/adapter-rn';
13
15
 
14
16
  const execFileAsync = promisify(execFile);
15
17
  const VISIBLE_LIMIT = 200;
@@ -93,8 +95,13 @@ async function readNativeHierarchy(payload, context) {
93
95
  env.METAMASK_RECIPE_PROJECT_ROOT = String(context.projectRoot);
94
96
  }
95
97
  const platform = resolvePlatform(node, env);
96
- if (platform === 'android') return readAndroidHierarchy(node, env);
97
- return readIosHierarchy(node, env);
98
+ const appLabel = nativeAppLabel(env);
99
+ const hierarchy = platform === 'android'
100
+ ? await readAndroidHierarchy(node, env)
101
+ : await readIosHierarchy(node, env);
102
+ // No screen name and no labelled item: name the screen after the app.
103
+ hierarchy.screen.name ??= appLabel;
104
+ return hierarchy;
98
105
  }
99
106
 
100
107
  async function readIosHierarchy(node, env) {
@@ -166,7 +173,7 @@ async function readAndroidHierarchy(node, env) {
166
173
  }
167
174
 
168
175
  async function readAndroidAgentDeviceHierarchy(serial, env) {
169
- const app = text(env.ANDROID_PACKAGE_ID) ?? 'io.metamask';
176
+ const app = nativeAppId(env, 'android');
170
177
  const session = nativeSessionName(env, serial, app);
171
178
  const stateDir = nativeAgentDeviceStateDir(env, env.METAMASK_RECIPE_PROJECT_ROOT ?? process.cwd(), serial);
172
179
  const client = createAgentDeviceClient({
@@ -257,8 +264,7 @@ function normalizedHierarchy(
257
264
  const items = nodes.slice(0, VISIBLE_LIMIT);
258
265
  const hiddenOrOffscreen = hiddenNodes.slice(0, HIDDEN_LIMIT);
259
266
  const name = metadata.screenName ??
260
- items.find((item) => item.label)?.label ??
261
- 'MetaMask';
267
+ items.find((item) => item.label)?.label;
262
268
  return {
263
269
  screen: {
264
270
  provider,
@@ -1,9 +1,10 @@
1
1
  // Navigate Android Home, Swap, and Perps through native controls and verify visible destinations.
2
2
  // Switch Perps Lite or Pro mode through visible controls on Android and iOS.
3
- import { createAgentDeviceUiTransport } from '@farmslot/expo-recipe';
3
+ import { createAgentDeviceUiTransport } from '@farmslot/adapter-rn';
4
4
  import { createAgentDeviceClient } from 'agent-device';
5
5
  import {
6
6
  nativeAgentDeviceStateDir,
7
+ nativeAppId,
7
8
  nativeSessionName,
8
9
  } from '../platform/native-session-name.mjs';
9
10
  import { bridgeCommand } from '../platform/bridge.mjs';
@@ -51,7 +52,7 @@ export async function navigateOpaqueMobile(input, destination) {
51
52
  if (!device) {
52
53
  throw new Error('Opaque Mobile navigation requires one explicit Android device serial.');
53
54
  }
54
- const app = String(env.ANDROID_PACKAGE_ID ?? 'io.metamask');
55
+ const app = nativeAppId(env, 'android');
55
56
  const session = nativeSessionName(env, device, app);
56
57
  const stateDir = nativeAgentDeviceStateDir(env, input.context.projectRoot, device);
57
58
  const client = createAgentDeviceClient({
@@ -184,7 +185,7 @@ export async function ensureVisibleAndroidPerpsMode(input, perpsMode) {
184
185
  'Android Perps mode requires one explicit device serial.',
185
186
  );
186
187
  }
187
- const app = String(env.ANDROID_PACKAGE_ID ?? 'io.metamask');
188
+ const app = nativeAppId(env, 'android');
188
189
  const session = nativeSessionName(env, device, app);
189
190
  const stateDir = nativeAgentDeviceStateDir(env, input.context.projectRoot, device);
190
191
  const client = createAgentDeviceClient({
@@ -1,9 +1,10 @@
1
1
  // Drive Android wallet onboarding, lock, unlock, and reset through native accessibility.
2
2
  // Wallet observations can navigate home and open the address list to verify the selected account.
3
- import { createAgentDeviceUiTransport } from '@farmslot/expo-recipe';
3
+ import { createAgentDeviceUiTransport } from '@farmslot/adapter-rn';
4
4
  import { createAgentDeviceClient } from 'agent-device';
5
5
  import {
6
6
  nativeAgentDeviceStateDir,
7
+ nativeAppId,
7
8
  nativeSessionName,
8
9
  } from '../platform/native-session-name.mjs';
9
10
 
@@ -249,7 +250,7 @@ function nativeSession(input) {
249
250
  const env = { ...process.env, ...(input.context?.env ?? {}) };
250
251
  const device = String(env.ADB_SERIAL ?? env.ANDROID_SERIAL ?? '').trim();
251
252
  if (!device) throw new Error('Opaque Mobile UI execution requires one explicit Android device serial.');
252
- const app = String(env.ANDROID_PACKAGE_ID ?? 'io.metamask');
253
+ const app = nativeAppId(env, 'android');
253
254
  const session = nativeSessionName(env, device, app);
254
255
  const stateDir = nativeAgentDeviceStateDir(env, input.context.projectRoot, device);
255
256
  const client = createAgentDeviceClient({
@@ -1,6 +1,6 @@
1
1
  // Minimal adapter IO for the shared analytics actions.
2
2
  //
3
- // Mirrors the live-adapter contract in src/live-adapter-contract.ts. Neither
3
+ // Mirrors the live-adapter contract in @farmslot/recipe-cli/harness. Neither
4
4
  // existing runAdapter fits: extension's lives under actions/extension/platform
5
5
  // and core's carries PerpsController teardown and expect_error handling. A
6
6
  // `shared/` action importing either would couple every platform to one of them,
@@ -1,6 +1,6 @@
1
1
  // The one console matcher: classify application console events and apply a
2
2
  // recipe-library allowlist of known noise. Used by run diagnostics
3
- // (src/run-diagnostics.ts), the assert_no_console_errors action, the
3
+ // (src/console-classifier.ts), the assert_no_console_errors action, the
4
4
  // allowlist generator, and the recipe library's checks/console-guard.mjs
5
5
  // (which imports this file), so every caller judges an event the same way.
6
6
  //