@deeeed/metamask-harness 0.48.0 → 0.49.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 (177) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/commands/help.js +17 -5
  3. package/dist/funding-execution-context.js +19 -4
  4. package/dist/metamask-action-validation.js +3 -2
  5. package/dist/mm-harness-cli.js +2 -1
  6. package/docs/RECIPES.md +2 -2
  7. package/library/actions/core/perps/_controller.mjs +14 -24
  8. package/library/actions/core/perps/assert_orders.mjs +3 -5
  9. package/library/actions/core/perps/assert_positions.mjs +3 -5
  10. package/library/actions/core/perps/close_orders.mjs +28 -4
  11. package/library/actions/core/perps/close_positions.mjs +53 -12
  12. package/library/actions/core/perps/edit_order.mjs +2 -0
  13. package/library/actions/core/perps/ensure_orders.mjs +3 -4
  14. package/library/actions/core/perps/ensure_positions.mjs +4 -5
  15. package/library/actions/core/perps/place_order.mjs +7 -14
  16. package/library/actions/core/perps/read_account.mjs +2 -0
  17. package/library/actions/core/perps/read_orders.mjs +2 -0
  18. package/library/actions/core/perps/read_positions.mjs +3 -1
  19. package/library/actions/core/perps/read_snapshot.mjs +3 -0
  20. package/library/actions/core/perps/start_state.mjs +3 -14
  21. package/library/actions/core/perps/teardown_state.mjs +3 -14
  22. package/library/actions/core/perps/update_position_tpsl.mjs +13 -19
  23. package/library/actions/core/wallet/list_accounts.mjs +3 -0
  24. package/library/actions/extension/analytics/consent.mjs +2 -0
  25. package/library/actions/extension/analytics/set_consent.mjs +2 -0
  26. package/library/actions/extension/assets/home-token-identity.mjs +1 -0
  27. package/library/actions/extension/assets/import_custom_token.mjs +2 -0
  28. package/library/actions/extension/assets/open_details.mjs +1 -0
  29. package/library/actions/extension/assets/prepare_send.mjs +1 -0
  30. package/library/actions/extension/assets/read_details.mjs +1 -0
  31. package/library/actions/extension/assets/read_visible_state.mjs +1 -0
  32. package/library/actions/extension/assets/review_send.mjs +1 -0
  33. package/library/actions/extension/assets/select_network_scope.mjs +1 -0
  34. package/library/actions/extension/assets/send.mjs +2 -0
  35. package/library/actions/extension/assets/set_custom_gas.mjs +1 -0
  36. package/library/actions/extension/assets/set_token_visibility.mjs +2 -0
  37. package/library/actions/extension/assets/verify_sorting.mjs +1 -0
  38. package/library/actions/extension/deeplink/open.mjs +1 -0
  39. package/library/actions/extension/networks/add_chainlist.mjs +2 -0
  40. package/library/actions/extension/networks/add_custom.mjs +2 -0
  41. package/library/actions/extension/networks/custom_network.mjs +2 -0
  42. package/library/actions/extension/networks/read_visible_state.mjs +1 -0
  43. package/library/actions/extension/networks/remove_custom.mjs +1 -0
  44. package/library/actions/extension/performance/_navigation-memory.mjs +2 -0
  45. package/library/actions/extension/performance/compare_idle_navigation_memory.mjs +2 -0
  46. package/library/actions/extension/performance/measure_detached_dom.mjs +2 -0
  47. package/library/actions/extension/performance/measure_navigation_memory.mjs +2 -0
  48. package/library/actions/extension/perps/assert_orders.mjs +1 -0
  49. package/library/actions/extension/perps/assert_positions.mjs +1 -0
  50. package/library/actions/extension/perps/assert_visible_consistency.mjs +2 -0
  51. package/library/actions/extension/perps/close_orders.mjs +4 -1
  52. package/library/actions/extension/perps/close_positions.mjs +1 -0
  53. package/library/actions/extension/perps/close_visible_position.mjs +159 -10
  54. package/library/actions/extension/perps/compare_provider_market.mjs +2 -0
  55. package/library/actions/extension/perps/edit_margin.mjs +2 -0
  56. package/library/actions/extension/perps/ensure_orders.mjs +1 -0
  57. package/library/actions/extension/perps/ensure_positions.mjs +1 -0
  58. package/library/actions/extension/perps/mutation-receipt.mjs +54 -3
  59. package/library/actions/extension/perps/open_balance_action.mjs +1 -0
  60. package/library/actions/extension/perps/open_position_action.mjs +1 -0
  61. package/library/actions/extension/perps/perps.mjs +401 -25
  62. package/library/actions/extension/perps/place_order.mjs +1 -0
  63. package/library/actions/extension/perps/read_eligibility.mjs +1 -0
  64. package/library/actions/extension/perps/read_funds_confirmation.mjs +2 -0
  65. package/library/actions/extension/perps/read_orders.mjs +1 -0
  66. package/library/actions/extension/perps/read_positions.mjs +1 -0
  67. package/library/actions/extension/perps/read_snapshot.mjs +2 -0
  68. package/library/actions/extension/perps/read_visible_state.mjs +2 -0
  69. package/library/actions/extension/perps/search_markets.mjs +1 -0
  70. package/library/actions/extension/perps/select_activity_filter.mjs +1 -0
  71. package/library/actions/extension/perps/select_market_filter.mjs +1 -0
  72. package/library/actions/extension/perps/set_market_favorite.mjs +1 -0
  73. package/library/actions/extension/perps/state.mjs +1 -0
  74. package/library/actions/extension/perps/update_position_tpsl.mjs +2 -0
  75. package/library/actions/extension/perps/visible-mutation-identity.mjs +2 -0
  76. package/library/actions/extension/platform/cdp.mjs +29 -2
  77. package/library/actions/extension/settings/set_basic_functionality.mjs +1 -0
  78. package/library/actions/extension/swap_bridge/quote-state.mjs +2 -0
  79. package/library/actions/extension/swap_bridge/read_visible_state.mjs +1 -0
  80. package/library/actions/extension/swap_bridge/select_assets.mjs +1 -0
  81. package/library/actions/extension/swap_bridge/set_amount.mjs +1 -0
  82. package/library/actions/extension/swap_bridge/set_max_amount.mjs +1 -0
  83. package/library/actions/extension/swap_bridge/set_slippage.mjs +1 -0
  84. package/library/actions/extension/ui/locators.mjs +1 -0
  85. package/library/actions/extension/ui/navigate.mjs +2 -0
  86. package/library/actions/extension/wallet/ensure_unlocked.mjs +2 -0
  87. package/library/actions/extension/wallet/import.mjs +2 -0
  88. package/library/actions/extension/wallet/list_accounts.mjs +1 -0
  89. package/library/actions/extension/wallet/lock.mjs +2 -0
  90. package/library/actions/extension/wallet/read_state.mjs +1 -0
  91. package/library/actions/extension/wallet/reset.mjs +2 -0
  92. package/library/actions/extension/wallet/secret-input.mjs +2 -0
  93. package/library/actions/extension/wallet/select_account.mjs +2 -0
  94. package/library/actions/extension/wallet/setup.mjs +1 -0
  95. package/library/actions/extension/wallet/state.mjs +2 -0
  96. package/library/actions/extension/wallet/visible-session.mjs +2 -0
  97. package/library/actions/mobile/analytics/consent-settings.mjs +1 -0
  98. package/library/actions/mobile/analytics/set_consent.mjs +1 -0
  99. package/library/actions/mobile/app/network-control.mjs +2 -0
  100. package/library/actions/mobile/app/network.mjs +1 -0
  101. package/library/actions/mobile/app/network_assert.mjs +1 -0
  102. package/library/actions/mobile/app/network_capture.mjs +1 -0
  103. package/library/actions/mobile/assets/import_custom_token.mjs +2 -0
  104. package/library/actions/mobile/assets/open_details.mjs +1 -0
  105. package/library/actions/mobile/assets/read_details.mjs +1 -0
  106. package/library/actions/mobile/assets/read_visible_state.mjs +1 -0
  107. package/library/actions/mobile/assets/set_token_visibility.mjs +2 -0
  108. package/library/actions/mobile/assets/verify_sorting.mjs +90 -7
  109. package/library/actions/mobile/deeplink/open.mjs +1 -0
  110. package/library/actions/mobile/networks/add_custom.mjs +1 -0
  111. package/library/actions/mobile/networks/network-management.mjs +1 -0
  112. package/library/actions/mobile/networks/read_visible_state.mjs +1 -0
  113. package/library/actions/mobile/networks/remove_custom.mjs +1 -0
  114. package/library/actions/mobile/perps/assert_orders.mjs +1 -0
  115. package/library/actions/mobile/perps/assert_positions.mjs +1 -0
  116. package/library/actions/mobile/perps/capture_performance.mjs +1 -0
  117. package/library/actions/mobile/perps/close_orders.mjs +1 -0
  118. package/library/actions/mobile/perps/close_positions.mjs +1 -0
  119. package/library/actions/mobile/perps/ensure_orders.mjs +1 -0
  120. package/library/actions/mobile/perps/ensure_positions.mjs +1 -0
  121. package/library/actions/mobile/perps/measure_homepage_visible.mjs +1 -0
  122. package/library/actions/mobile/perps/performance-capture.mjs +2 -0
  123. package/library/actions/mobile/perps/perps.mjs +6 -2
  124. package/library/actions/mobile/perps/place_order.mjs +1 -0
  125. package/library/actions/mobile/perps/prepare_local_snapshot_endpoint.mjs +2 -0
  126. package/library/actions/mobile/perps/read-visible-state-loop.mjs +2 -0
  127. package/library/actions/mobile/perps/read_orders.mjs +1 -0
  128. package/library/actions/mobile/perps/read_positions.mjs +1 -0
  129. package/library/actions/mobile/perps/read_visible_state.mjs +1 -0
  130. package/library/actions/mobile/perps/search_markets.mjs +1 -0
  131. package/library/actions/mobile/perps/set_market_favorite.mjs +2 -0
  132. package/library/actions/mobile/platform/bridge.mjs +16 -4
  133. package/library/actions/mobile/platform/native-session-name.mjs +1 -0
  134. package/library/actions/mobile/platform/native-session.mjs +2 -0
  135. package/library/actions/mobile/platform/observe-ui.mjs +2 -0
  136. package/library/actions/mobile/platform/tool-paths.mjs +1 -0
  137. package/library/actions/mobile/swap_bridge/native-session.mjs +1 -0
  138. package/library/actions/mobile/swap_bridge/read_visible_state.mjs +1 -0
  139. package/library/actions/mobile/swap_bridge/select_assets.mjs +2 -0
  140. package/library/actions/mobile/swap_bridge/set_amount.mjs +1 -0
  141. package/library/actions/mobile/swap_bridge/set_max_amount.mjs +1 -0
  142. package/library/actions/mobile/swap_bridge/set_slippage.mjs +1 -0
  143. package/library/actions/mobile/swap_bridge/submit_transaction.mjs +2 -0
  144. package/library/actions/mobile/ui/locators.mjs +1 -0
  145. package/library/actions/mobile/ui/native-navigation.mjs +2 -0
  146. package/library/actions/mobile/ui/navigate.mjs +2 -0
  147. package/library/actions/mobile/wallet/ensure_unlocked.mjs +2 -0
  148. package/library/actions/mobile/wallet/home.mjs +1 -0
  149. package/library/actions/mobile/wallet/import.mjs +2 -0
  150. package/library/actions/mobile/wallet/list_accounts.mjs +1 -0
  151. package/library/actions/mobile/wallet/lock.mjs +1 -0
  152. package/library/actions/mobile/wallet/native-ui.mjs +2 -0
  153. package/library/actions/mobile/wallet/read_state.mjs +2 -0
  154. package/library/actions/mobile/wallet/reset-helper.mjs +1 -0
  155. package/library/actions/mobile/wallet/reset.mjs +1 -0
  156. package/library/actions/mobile/wallet/select_account.mjs +2 -0
  157. package/library/actions/mobile/wallet/setup.mjs +2 -0
  158. package/library/actions/shared/analytics/assert_events.mjs +3 -0
  159. package/library/actions/shared/analytics/consent.mjs +3 -0
  160. package/library/actions/shared/analytics/read_events.mjs +2 -0
  161. package/library/actions/shared/analytics/start_capture.mjs +3 -0
  162. package/library/actions/shared/app/network-artifact.mjs +2 -0
  163. package/library/actions/shared/app/network-assert.mjs +2 -0
  164. package/library/actions/shared/assets/visible-state.mjs +2 -0
  165. package/library/actions/shared/deeplink/url.mjs +3 -0
  166. package/library/actions/shared/networks/visible-state.mjs +2 -0
  167. package/library/actions/shared/perps/visible-state.mjs +2 -0
  168. package/library/actions/shared/swap-bridge/transaction.mjs +3 -0
  169. package/library/actions/shared/swap-bridge/visible-state.mjs +2 -0
  170. package/library/actions/shared/ui/locators.mjs +2 -0
  171. package/library/actions/shared/wallet/import-source.mjs +3 -0
  172. package/library/manifests/core.action-manifest.json +20 -0
  173. package/library/manifests/extension.action-manifest.json +2 -2
  174. package/library/manifests/mobile.action-manifest.json +3 -3
  175. package/library/recipes/mobile/app/{lifecycle.android-smoke.recipe.json → lifecycle-smoke.recipe.json} +12 -11
  176. package/package.json +1 -1
  177. package/site/assets/help-recipes.json +188 -35
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.49.0 - 2026-09-07
6
+
7
+ - Simplify Mobile setup and sizing; correct nested-route verification, lifecycle portability, wallet selection, input clearing and token-sort restoration.
8
+ - Keep piped help complete; add platform-scoped recipe guidance, fixture troubleshooting and action descriptions.
9
+ - Consolidate market/limit validation into `perps.release-live-order`; bind balances to their wallet runtime and cleanup to accepted orders, correct sizing/direction checks, and support approved source-dev testnet QA up to $20.
10
+
5
11
  ## 0.48.0 - 2026-09-07
6
12
 
7
13
  - Consolidate parameterized recipes, move specialized Perps journeys into the team library, and separate short performance checks from full coverage suites.
@@ -3,19 +3,31 @@ import path from "node:path";
3
3
  import { detectAdapter } from "../harness.js";
4
4
  import { optionString, parseArgs, targetPath } from "./parse-args.js";
5
5
  const UNSCOPED_NEXT = "cd into a MetaMask checkout or pass --adapter <mobile|extension|core>";
6
+ function validHelpLine(value) {
7
+ return typeof value === "string" || Boolean(value && typeof value.text === "string" && Array.isArray(value.adapters) && value.adapters.length > 0 && value.adapters.every((adapter) => ["mobile", "extension", "core"].includes(adapter)));
8
+ }
9
+ function scopedSections(topic, adapter) {
10
+ const select = (lines) => lines.flatMap((line) => typeof line === "string" ? [line] : line.adapters.includes(adapter) ? [line.text] : []);
11
+ return topic.sections.map((section) => ({
12
+ ...section,
13
+ details: select(section.details),
14
+ commands: select(section.commands)
15
+ }));
16
+ }
6
17
  function loadRecipeHelp(packageRoot) {
7
18
  const file = path.join(packageRoot, "site", "assets", "help-recipes.json");
8
19
  const value = JSON.parse(fs.readFileSync(file, "utf8"));
9
20
  if (value.schemaVersion !== 1 || value.topic !== "recipes" || typeof value.title !== "string" || typeof value.summary !== "string" || typeof value.capabilities !== "string" || typeof value.scopeNotice !== "string" || typeof value.recipeProtocolVersion !== "string" || typeof value.safety !== "string" || !Array.isArray(value.sections) || value.sections.length !== 8 || value.sections.some(
10
- (section) => !section || typeof section.id !== "string" || typeof section.title !== "string" || typeof section.instruction !== "string" || !Array.isArray(section.details) || section.details.some((detail) => typeof detail !== "string") || !Array.isArray(section.commands) || section.commands.some((command) => typeof command !== "string")
21
+ (section) => !section || typeof section.id !== "string" || typeof section.title !== "string" || typeof section.instruction !== "string" || !Array.isArray(section.details) || section.details.some((detail) => !validHelpLine(detail)) || !Array.isArray(section.commands) || section.commands.some((command) => !validHelpLine(command))
11
22
  )) {
12
23
  throw new Error(`Invalid recipe help data: ${file}`);
13
24
  }
14
25
  return value;
15
26
  }
16
27
  function recipeHelpPayload(topic, harnessVersion, adapter, target) {
17
- const commands = Array.from(new Set(topic.sections.flatMap((section) => section.commands)));
18
- const discoveryCommands = topic.sections.find((section) => section.id === "discover")?.commands ?? [];
28
+ const sections = scopedSections(topic, adapter);
29
+ const commands = Array.from(new Set(sections.flatMap((section) => section.commands)));
30
+ const discoveryCommands = sections.find((section) => section.id === "discover")?.commands ?? [];
19
31
  return {
20
32
  schemaVersion: topic.schemaVersion,
21
33
  command: "help",
@@ -29,7 +41,7 @@ function recipeHelpPayload(topic, harnessVersion, adapter, target) {
29
41
  summary: topic.summary,
30
42
  capabilities: topic.capabilities,
31
43
  scopeNotice: topic.scopeNotice,
32
- sections: topic.sections,
44
+ sections,
33
45
  commands,
34
46
  discoveryCommands,
35
47
  safety: topic.safety
@@ -45,7 +57,7 @@ function renderRecipeHelp(topic, harnessVersion, adapter) {
45
57
  "Visual tutorial: mm-harness tutorial"
46
58
  ];
47
59
  if (adapter === "unscoped") lines.push(`No MetaMask checkout detected. Next: ${UNSCOPED_NEXT}.`);
48
- for (const [index, section] of topic.sections.entries()) {
60
+ for (const [index, section] of scopedSections(topic, adapter).entries()) {
49
61
  lines.push("", `${index + 1}. ${section.title}`, ` ${section.instruction}`);
50
62
  for (const detail of section.details) lines.push(` - ${detail}`);
51
63
  for (const command of section.commands) lines.push(` $ ${command}`);
@@ -36,7 +36,8 @@ const CLAIM_DIFF_FIELDS = [
36
36
  ];
37
37
  const trustedActionContexts = /* @__PURE__ */ new WeakSet();
38
38
  const fundingSources = /* @__PURE__ */ new WeakMap();
39
- const CURRENT_FUNDING_REGISTRY_SHA256 = "7e79ee2a5f7bdde0a5548208cb2eab55881575cc7d826ee5f7acdbf15d49609c";
39
+ const CURRENT_FUNDING_REGISTRY_SHA256 = "d4416e0e8a812a0e2f1798ae6f901a5820f7ee0b9783cfa5d515d0c7863ea80e";
40
+ const PREVIOUS_FUNDING_REGISTRY_SHA256 = "7e79ee2a5f7bdde0a5548208cb2eab55881575cc7d826ee5f7acdbf15d49609c";
40
41
  const FROZEN_G005_FUNDING_REGISTRY_SHA256 = "c2230bc02fa00d6902915d1cb81bcc08389ea7e399ee9618f26fb0d9fc00b291";
41
42
  const LIVE_PERPS_TINY_REGISTRY = {
42
43
  "live-perps-tiny": {
@@ -58,8 +59,15 @@ const LIVE_PERPS_TINY_REGISTRY = {
58
59
  };
59
60
  const TRUSTED_FUNDING_REGISTRIES = {
60
61
  [CURRENT_FUNDING_REGISTRY_SHA256]: LIVE_PERPS_TINY_REGISTRY,
62
+ [PREVIOUS_FUNDING_REGISTRY_SHA256]: LIVE_PERPS_TINY_REGISTRY,
61
63
  [FROZEN_G005_FUNDING_REGISTRY_SHA256]: LIVE_PERPS_TINY_REGISTRY
62
64
  };
65
+ const SOURCE_DEV_TESTNET_BUDGETS = /* @__PURE__ */ new Set([
66
+ "perps-market-order:usdc-base-units",
67
+ "perps-limit-order:usdc-base-units",
68
+ "perps-cancel-order:usdc-base-units",
69
+ "perps-close-position:usdc-base-units"
70
+ ]);
63
71
  const OPERATION_ACTION = /* @__PURE__ */ new Map([
64
72
  ["perps-market-order", "metamask.perps.place_order"],
65
73
  ["perps-limit-order", "metamask.perps.place_order"],
@@ -1002,7 +1010,7 @@ async function validateFundingEvidenceChain(documents, identity) {
1002
1010
  }
1003
1011
  balanceRuntime = currentRuntime;
1004
1012
  const visibleState = requireRecord(evidenceDocument.visibleState, "funding balance evidence visibleState");
1005
- if (visibleState.perpsAvailableMarginBaseUnits !== actualBaseUnits || visibleBalanceBaseUnits(trace, emittedNodes) !== actualBaseUnits) {
1013
+ if (visibleState.perpsAvailableMarginBaseUnits !== actualBaseUnits || visibleBalanceBaseUnits(trace, emittedNodes, currentRuntime) !== actualBaseUnits) {
1006
1014
  throw new Error("Funding balance actual value does not match its visible trace.");
1007
1015
  }
1008
1016
  if (!exactTerminalPass(recipe, trace, summary, artifactManifest)) {
@@ -1086,11 +1094,17 @@ function extensionOrigin(value, label) {
1086
1094
  throw new Error(`${label} Extension origin is invalid.`);
1087
1095
  }
1088
1096
  }
1089
- function visibleBalanceBaseUnits(trace, emittedNodes) {
1097
+ function visibleBalanceBaseUnits(trace, emittedNodes, runtime) {
1090
1098
  const entries = Array.isArray(trace.entries) ? trace.entries : [];
1091
1099
  const matches = entries.filter((entry) => isRecord(entry) && entry.action === "metamask.perps.read_visible_state" && entry.ok === true && isRecord(entry.output) && entry.output.action === entry.action && entry.output.surface === "home" && entry.output.source === "visible-ui" && isRecord(entry.output.balance) && entry.output.balance.visible === true && isRecord(emittedNodes.get(String(entry.nodeId))) && emittedNodes.get(String(entry.nodeId))?.surface === "home");
1092
1100
  const matchEntry = matches.at(-1);
1093
1101
  if (!matchEntry) throw new Error("Funding balance evidence requires a visible Perps Home balance observation.");
1102
+ for (const entry of matches) {
1103
+ const output = requireRecord(entry.output, "funding balance observation");
1104
+ if (output.runtimeSessionId !== runtime.runtimeSessionId || output.cdpPort !== runtime.cdpPort || extensionOrigin(output.targetUrl, "funding balance observation") !== runtime.extensionOrigin) {
1105
+ throw new Error("Funding balance observation is not from its anchored wallet runtime.");
1106
+ }
1107
+ }
1094
1108
  const label = String(matchEntry.output.balance.label ?? "");
1095
1109
  const match = /(?:^|\s)\$([0-9]+(?:\.[0-9]{1,2})?)(?:\s|$)/u.exec(label);
1096
1110
  if (!match) throw new Error("Funding balance evidence visible amount is invalid.");
@@ -1325,7 +1339,8 @@ function validateTrustedFundingRegistry(identity, operations, budgets) {
1325
1339
  for (const [key, aggregate] of requested) {
1326
1340
  const trusted = domain.budgets.get(key);
1327
1341
  const supplied = suppliedBudgets.get(key);
1328
- if (!trusted || !supplied || BigInt(supplied.maxBaseUnits) > BigInt(trusted.maxBaseUnits) || supplied.maxCount > trusted.maxCount || aggregate.maxAmountBaseUnits > BigInt(trusted.maxBaseUnits) || aggregate.count > trusted.maxCount || aggregate.amountBaseUnits > BigInt(trusted.maxBaseUnits) * BigInt(trusted.maxCount)) {
1342
+ const maximum = BigInt(identity.registrySha256 === CURRENT_FUNDING_REGISTRY_SHA256 && identity.client === "extension" && identity.proofLane === "source-dev" && SOURCE_DEV_TESTNET_BUDGETS.has(key) ? "20000000" : trusted?.maxBaseUnits ?? "0");
1343
+ if (!trusted || !supplied || BigInt(supplied.maxBaseUnits) > maximum || supplied.maxCount > trusted.maxCount || aggregate.maxAmountBaseUnits > maximum || aggregate.count > trusted.maxCount || aggregate.amountBaseUnits > maximum * BigInt(trusted.maxCount)) {
1329
1344
  throw new Error(`Funding operation ${aggregate.operation} is not bounded by the runner-trusted funding registry.`);
1330
1345
  }
1331
1346
  }
@@ -191,7 +191,8 @@ function validateMetaMaskActionInputs(recipe, adapter, params) {
191
191
  }
192
192
  if (action === "metamask.perps.close_orders") {
193
193
  required("side", /^(?:long|short)$/u, `${action} requires the exact order side`);
194
- if (!/^[1-9][0-9]*(?:\.[0-9]{1,6})?$/u.test(String(node.limit_price ?? ""))) {
194
+ const outputPrice = /^\{\{outputs\.[A-Za-z0-9_.-]+\}\}$/u.test(String(node.limit_price ?? ""));
195
+ if (!outputPrice && (!/^(?:0|[1-9][0-9]*)(?:\.[0-9]{1,6})?$/u.test(String(node.limit_price ?? "")) || !(Number(node.limit_price) > 0))) {
195
196
  findings.push({
196
197
  severity: "error",
197
198
  code: "recipe.invalid_param",
@@ -200,7 +201,7 @@ function validateMetaMaskActionInputs(recipe, adapter, params) {
200
201
  });
201
202
  }
202
203
  }
203
- if (action === "metamask.perps.place_order" && String(node.order_type ?? "market") === "limit" && !/^[1-9][0-9]*(?:\.[0-9]{1,6})?$/u.test(String(node.limit_price ?? ""))) {
204
+ if (action === "metamask.perps.place_order" && String(node.order_type ?? "market") === "limit" && (!/^(?:0|[1-9][0-9]*)(?:\.[0-9]{1,6})?$/u.test(String(node.limit_price ?? "")) || !(Number(node.limit_price) > 0))) {
204
205
  findings.push({
205
206
  severity: "error",
206
207
  code: "recipe.invalid_param",
@@ -826,7 +826,8 @@ for (const command of REAL) {
826
826
  process.exit(await handleUpdate(rawArgv.slice(1)));
827
827
  }
828
828
  if (command.name === "help") {
829
- process.exit(handleHelp(rawArgv.slice(1), { packageRoot, harnessVersion: pkgVersion }));
829
+ process.exitCode = handleHelp(rawArgv.slice(1), { packageRoot, harnessVersion: pkgVersion });
830
+ return;
830
831
  }
831
832
  if (command.name === "tutorial") {
832
833
  process.exit(handleTutorial(rawArgv.slice(1), { harnessVersion: pkgVersion }));
package/docs/RECIPES.md CHANGED
@@ -115,8 +115,8 @@ mm-harness run perps.release-market-filters --describe --adapter extension --jso
115
115
  | --- | --- |
116
116
  | Activity filter routes | `perps.release-activity-filters` |
117
117
  | Deposit and withdraw previews without submission | `perps.release-funds-flows` |
118
- | Resting limit placement and exact cancellation | `perps.release-live-limit-order` |
119
- | Filled market placement and exact position close | `perps.release-live-market-order` |
118
+ | Resting limit placement and exact cancellation | `perps.release-live-order order_type=limit limit_price='"<price>"'` |
119
+ | Filled market placement and exact position close | `perps.release-live-order order_type=market` |
120
120
  | Market header and statistics | `perps.release-market-details` |
121
121
  | Market categories | `perps.release-market-filters` |
122
122
  | Market text search | `perps.release-market-search` |
@@ -1,26 +1,14 @@
1
+ // Load the target checkout's Perps controller and resolve fixture-backed accounts.
2
+ // Share headless selection, signing, formatting and adapter-output helpers.
3
+
1
4
  import { readFile, writeFile } from 'node:fs/promises';
2
5
  import path from 'node:path';
3
6
  import { pathToFileURL } from 'node:url';
4
7
 
5
8
  import { mnemonicToAccount, privateKeyToAccount } from 'viem/accounts';
6
9
 
7
- // Importing a runner .ts source signals the live-adapter contract to execute
8
- // this adapter under the bundled tsx (see commandFor/importsSourceTypescript in
9
- // src/live-adapter-contract.ts). The core adapter dynamic-imports the perps
10
- // controller TypeScript at runtime, so it MUST run under tsx, not plain node.
11
10
  import { walletFixturePath } from '../../harness-exports.mjs';
12
11
 
13
- // Shared headless instantiation for the MetaMask `core` adapter.
14
- //
15
- // Slice 1: read-only. We instantiate @metamask/perps-controller against a
16
- // resolved MetaMask/core checkout (context.projectRoot) and drive its standalone
17
- // read path, which talks to HyperLiquid testnet over HTTP — no CDP, no bridge,
18
- // no UI, no signer. The controller's standalone read methods
19
- // (getPositions/getOpenOrders/getAccountState with { standalone: true,
20
- // userAddress }) create their own InfoClient and never touch the messenger, so
21
- // reads need ZERO external action handlers. Signing/account-resolution through
22
- // the messenger is Slice 2.
23
-
24
12
  function controllerEntry(projectRoot) {
25
13
  return pathToFileURL(path.join(projectRoot, 'packages/perps-controller/src/index.ts')).href;
26
14
  }
@@ -590,12 +578,7 @@ export function symbolForItem(item) {
590
578
  return normalizeMarketSymbol(item?.symbol ?? item?.coin ?? '');
591
579
  }
592
580
 
593
- // Normalize an item's side to the long/short vocabulary the selector uses.
594
- // Positions report side as long/short already; OPEN ORDERS report it as buy/sell
595
- // (a resting BUY = long direction, a resting SELL = short). Mapping both onto
596
- // long/short lets the shared `side` selector filter positions and orders alike —
597
- // without this, requesting side:"long" silently drops every order (whose side is
598
- // "buy"), which is the bug this normalization fixes.
581
+ // Orders carry buy/sell sides; Position.size is signed and authoritative.
599
582
  function normalizeSide(rawSide) {
600
583
  const side = String(rawSide ?? '').toLowerCase();
601
584
  if (side === 'buy' || side === 'b') return 'long';
@@ -603,7 +586,14 @@ function normalizeSide(rawSide) {
603
586
  return side;
604
587
  }
605
588
 
606
- function sideForItem(item) {
589
+ function sideForItem(item, itemType) {
590
+ if (itemType === 'position') {
591
+ const rawSize = item?.size ?? item?.szi;
592
+ if (typeof rawSize !== 'string' && typeof rawSize !== 'number') return '';
593
+ const size = Number(rawSize);
594
+ if (!Number.isFinite(size) || size === 0) return '';
595
+ return size > 0 ? 'long' : 'short';
596
+ }
607
597
  return normalizeSide(item?.side ?? item?.direction ?? '');
608
598
  }
609
599
 
@@ -646,7 +636,7 @@ export function requireExplicitSelection(input) {
646
636
  * vocabulary as the extension perps adapter. With no selector and no
647
637
  * mode=all, returns every item (read defaults to showing all live state).
648
638
  */
649
- export function selectedItems(input, items) {
639
+ export function selectedItems(input, items, itemType = 'order') {
650
640
  const requested = configuredSymbols(input, items);
651
641
  const selector =
652
642
  input.node?.selector && typeof input.node.selector === 'object' ? input.node.selector : {};
@@ -655,7 +645,7 @@ export function selectedItems(input, items) {
655
645
  const symbols = requested.length > 0 ? new Set(requested) : null;
656
646
  return items.filter((item) => {
657
647
  if (symbols && !symbols.has(symbolForItem(item))) return false;
658
- if (side && sideForItem(item) && sideForItem(item) !== side) return false;
648
+ if (side && sideForItem(item, itemType) !== side) return false;
659
649
  return true;
660
650
  });
661
651
  }
@@ -1,3 +1,6 @@
1
+ // Assert selected standalone orders, trigger fields and parent/child protection links.
2
+ // Checks observed state without placing or canceling orders.
3
+
1
4
  import {
2
5
  getCoreController,
3
6
  isDirectRun,
@@ -8,11 +11,6 @@ import {
8
11
  selectedItems,
9
12
  } from './_controller.mjs';
10
13
 
11
- // core Assert selected live Perps open orders are present or absent. Pure read
12
- // over the controller's standalone getOpenOrders path (no signer / provider init
13
- // needed) — throws on mismatch so the recipe fails loudly. Mirrors
14
- // assert_positions.mjs.
15
- //
16
14
  // Optional expect_* fields additionally assert the TRIGGER DATA the exchange
17
15
  // round-tripped for every matching order: expect_trigger_order_type (the
18
16
  // normalized placement type, e.g. stop_market), expect_trigger_price,
@@ -1,3 +1,5 @@
1
+ // Assert required presence, absence or count of selected standalone Perps positions.
2
+
1
3
  import {
2
4
  getCoreController,
3
5
  isDirectRun,
@@ -8,10 +10,6 @@ import {
8
10
  selectedItems,
9
11
  } from './_controller.mjs';
10
12
 
11
- // core Assert selected live Perps positions are present or absent. Pure read
12
- // over the controller's standalone path (no signer / provider init needed) —
13
- // throws on mismatch so the recipe fails loudly.
14
-
15
13
  export function expectedOpen(input) {
16
14
  if (input.node?.state == null) throw new Error('metamask.perps.assert_positions requires state=open or state=none.');
17
15
  const state = String(input.node.state).toLowerCase();
@@ -39,7 +37,7 @@ export async function assertPositions(input, expectOpen = expectedOpen(input)) {
39
37
  standalone: true,
40
38
  userAddress: accountAddress,
41
39
  });
42
- matching = selectedItems(input, positions);
40
+ matching = selectedItems(input, positions, 'position');
43
41
  const reached = expectedCount === undefined
44
42
  ? matching.length > 0
45
43
  : matching.length === Number(expectedCount);
@@ -1,5 +1,9 @@
1
+ // Cancel selected testnet orders by a fixed ID set and verify their absence.
2
+ // Optional expected IDs reject changed selection before signing and dispatch.
3
+
1
4
  import {
2
5
  controllerRejection,
6
+ getCoreController,
3
7
  getCoreControllerWithSigner,
4
8
  isDirectRun,
5
9
  redactOrder,
@@ -10,9 +14,6 @@ import {
10
14
  optionalParam,
11
15
  } from './_controller.mjs';
12
16
 
13
- // Snapshot selection into exact IDs so dispatch and postcondition checks cannot
14
- // expand to opposite-side or newly created orders sharing a market symbol.
15
-
16
17
  function sleep(ms) {
17
18
  return new Promise((resolve) => setTimeout(resolve, ms));
18
19
  }
@@ -46,7 +47,15 @@ export function cancelOrdersFailure(result) {
46
47
 
47
48
  export async function closeOrders(input) {
48
49
  requireExplicitSelection(input);
49
- const { controller, accountAddress, network } = await getCoreControllerWithSigner(input);
50
+ const expectedIds = input.node?.expected_order_ids;
51
+ if (expectedIds !== undefined && (!Array.isArray(expectedIds) ||
52
+ expectedIds.some((id) => typeof id !== 'string' || !id.trim()) ||
53
+ new Set(expectedIds).size !== expectedIds.length)) {
54
+ throw new Error('expected_order_ids must contain unique non-empty receipt IDs.');
55
+ }
56
+ const { controller, accountAddress, network } = await (expectedIds === undefined
57
+ ? getCoreControllerWithSigner(input)
58
+ : getCoreController(input));
50
59
  const timeoutMs = Number(
51
60
  optionalParam(input.node ?? {}, 'timeout_ms', 'timeoutMs') ?? 30000,
52
61
  );
@@ -56,6 +65,14 @@ export async function closeOrders(input) {
56
65
  userAddress: accountAddress,
57
66
  });
58
67
  const matching = selectedItems(input, orders);
68
+ const checkExpectedIds = (selected) => {
69
+ if (expectedIds === undefined) return;
70
+ const actual = selected.map((order) => order.orderId);
71
+ if (actual.length !== expectedIds.length || actual.some((id) => !expectedIds.includes(id))) {
72
+ throw new Error('CORE_ORDER_RECEIPT_MISMATCH: selected live orders do not match expected_order_ids; cancellation was not dispatched.');
73
+ }
74
+ };
75
+ checkExpectedIds(matching);
59
76
  const symbols = Array.from(new Set(matching.map(symbolForItem)));
60
77
 
61
78
  if (symbols.length === 0) {
@@ -76,6 +93,13 @@ export async function closeOrders(input) {
76
93
  if (orderIds.some((id) => typeof id !== 'string' || !id.trim()) || new Set(orderIds).size !== orderIds.length) {
77
94
  throw new Error('Selected Core orders require unique non-empty orderId values before cancellation.\nNext: mm-harness actions metamask.perps.read_orders --adapter core --json');
78
95
  }
96
+ if (expectedIds !== undefined) {
97
+ await getCoreControllerWithSigner(input);
98
+ checkExpectedIds(selectedItems(input, await controller.getOpenOrders({
99
+ standalone: true,
100
+ userAddress: accountAddress,
101
+ })));
102
+ }
79
103
  const result = await controller.cancelOrders({ orderIds });
80
104
  const failure = cancelOrdersFailure(result);
81
105
  if (failure !== undefined) {
@@ -1,6 +1,10 @@
1
+ // Close selected testnet positions and verify their absence.
2
+ // Optional receipt guards bind the observed position, reducing side and maximum quantity.
3
+
1
4
  import {
2
5
  controllerRejection,
3
6
  currentMarketPrice,
7
+ getCoreController,
4
8
  getCoreControllerWithSigner,
5
9
  isDirectRun,
6
10
  redactPosition,
@@ -11,12 +15,6 @@ import {
11
15
  optionalParam,
12
16
  } from './_controller.mjs';
13
17
 
14
- // core Close selected live Perps positions on HyperLiquid testnet by driving the
15
- // headless PerpsController.closePosition() per matching position (full close)
16
- // through the full signing/provider path (Slice 2). Mirrors the extension
17
- // adapter: read → close each matching position with a fresh price → verify the
18
- // selected positions are gone.
19
-
20
18
  function sleep(ms) {
21
19
  return new Promise((resolve) => setTimeout(resolve, ms));
22
20
  }
@@ -45,7 +43,17 @@ export function closePositionsFailure(results) {
45
43
 
46
44
  export async function closePositions(input) {
47
45
  requireExplicitSelection(input);
48
- const { controller, accountAddress, network } = await getCoreControllerWithSigner(input);
46
+ const expectedPositions = input.node?.expected_positions;
47
+ if (expectedPositions !== undefined && (!Array.isArray(expectedPositions) || expectedPositions.length > 1 ||
48
+ expectedPositions.some((position) => !position || typeof position.coin !== 'string' ||
49
+ !position.coin.trim() || !Number.isFinite(Number(position.size)) || Number(position.size) === 0 ||
50
+ !Number.isFinite(Number(position.entryPrice)) || Number(position.entryPrice) <= 0) ||
51
+ new Set(expectedPositions.map((position) => position.coin)).size !== expectedPositions.length)) {
52
+ throw new Error('expected_positions must contain at most one market with nonzero signed size and positive entryPrice.');
53
+ }
54
+ const { controller, accountAddress, network } = await (expectedPositions === undefined
55
+ ? getCoreControllerWithSigner(input)
56
+ : getCoreController(input));
49
57
  const timeoutMs = Number(
50
58
  optionalParam(input.node ?? {}, 'timeout_ms', 'timeoutMs') ?? 30000,
51
59
  );
@@ -61,7 +69,20 @@ export async function closePositions(input) {
61
69
  standalone: true,
62
70
  userAddress: accountAddress,
63
71
  });
64
- const matching = selectedItems(input, positions);
72
+ const matching = selectedItems(input, positions, 'position');
73
+ const checkExpectedPositions = (selected) => {
74
+ if (expectedPositions === undefined) return;
75
+ if (selected.length !== expectedPositions.length || selected.some((position) => {
76
+ const actual = redactPosition(position);
77
+ const expected = expectedPositions.find((item) => item.coin === actual.coin);
78
+ return !expected || Number(expected.size) !== Number(actual.size) ||
79
+ Number(expected.entryPrice) !== Number(actual.entryPrice) ||
80
+ (expected.side != null && expected.side !== actual.side);
81
+ })) {
82
+ throw new Error('CORE_POSITION_RECEIPT_MISMATCH: selected live position size, side or entry price changed; close was not dispatched.');
83
+ }
84
+ };
85
+ checkExpectedPositions(matching);
65
86
  const symbols = Array.from(new Set(matching.map(symbolForItem)));
66
87
 
67
88
  if (symbols.length === 0) {
@@ -80,13 +101,29 @@ export async function closePositions(input) {
80
101
 
81
102
  const results = [];
82
103
  let successCount = 0;
104
+ if (expectedPositions !== undefined) await getCoreControllerWithSigner(input);
83
105
  for (const position of matching) {
84
106
  const symbol = symbolForItem(position);
85
107
  const currentPrice = await currentMarketPrice(controller, symbol);
86
- // Full close: omit size so the provider closes 100% of the position. Pass
87
- // live position + fresh price so the provider skips a REST refetch and the
88
- // slippage guard has a current anchor.
89
- const result = await controller.closePosition({
108
+ if (expectedPositions !== undefined) {
109
+ checkExpectedPositions(selectedItems(input, await controller.getPositions({
110
+ standalone: true,
111
+ userAddress: accountAddress,
112
+ }), 'position'));
113
+ }
114
+ // Guarded closes keep receipt direction and quantity fixed. A venue position
115
+ // is netted, so matching fields cannot distinguish an identical replacement.
116
+ const expected = expectedPositions?.find((item) => item.coin === symbol);
117
+ const result = expected ? await controller.placeOrder({
118
+ symbol,
119
+ isBuy: Number(expected.size) < 0,
120
+ size: String(Math.abs(Number(expected.size))),
121
+ orderType: 'market',
122
+ reduceOnly: true,
123
+ currentPrice,
124
+ priceAtCalculation: currentPrice,
125
+ maxSlippageBps,
126
+ }) : await controller.closePosition({
90
127
  symbol,
91
128
  orderType: 'market',
92
129
  currentPrice,
@@ -128,6 +165,10 @@ export async function closePositions(input) {
128
165
  results,
129
166
  positions: stillOpen.map(redactPosition),
130
167
  proofPath: 'perps-controller-closePosition',
168
+ ...(expectedPositions === undefined ? {} : {
169
+ expectedPositions,
170
+ proofPath: 'perps-controller-placeOrder-reduce-only-receipt-size',
171
+ }),
131
172
  };
132
173
  }
133
174
 
@@ -1,3 +1,5 @@
1
+ // Edit a selected resting testnet order and verify its replacement or declared rejection.
2
+
1
3
  import {
2
4
  configuredSymbols,
3
5
  controllerRejection,
@@ -1,12 +1,11 @@
1
+ // Converge selected testnet orders to open or absent state, then assert the result.
2
+ // Reuse an existing matching order instead of placing another.
3
+
1
4
  import { getCoreController, isDirectRun, requireExplicitSelection, runAdapter, selectedItems } from './_controller.mjs';
2
5
  import { assertOrders } from './assert_orders.mjs';
3
6
  import { closeOrders } from './close_orders.mjs';
4
7
  import { placeOrder } from './place_order.mjs';
5
8
 
6
- // core Higher-level wrapper that reads selected open orders, converges them to the
7
- // requested `state` (open/none) by canceling or placing, then asserts the final
8
- // order state. Mirrors ensure_positions.mjs.
9
-
10
9
  export async function ensureOrders(input) {
11
10
  if (input.node?.state == null) throw new Error('metamask.perps.ensure_orders requires state=open or state=none.');
12
11
  requireExplicitSelection(input);
@@ -1,12 +1,11 @@
1
+ // Converge selected testnet positions to open or absent state, then assert the result.
2
+ // Open a position only when the selected state requires one.
3
+
1
4
  import { getCoreController, isDirectRun, requireExplicitSelection, runAdapter, selectedItems } from './_controller.mjs';
2
5
  import { assertPositions } from './assert_positions.mjs';
3
6
  import { closePositions } from './close_positions.mjs';
4
7
  import { placeOrder } from './place_order.mjs';
5
8
 
6
- // core Higher-level wrapper that reads selected positions, converges them to the
7
- // requested `state` (open/none) by placing or closing, then asserts the final
8
- // state. Mirrors the extension ensure_positions semantics.
9
-
10
9
  export async function ensurePositions(input) {
11
10
  if (input.node?.state == null) throw new Error('metamask.perps.ensure_positions requires state=open or state=none.');
12
11
  requireExplicitSelection(input);
@@ -26,7 +25,7 @@ export async function ensurePositions(input) {
26
25
  userAddress: accountAddress,
27
26
  });
28
27
  let order = null;
29
- if (selectedItems(input, positions).length === 0) {
28
+ if (selectedItems(input, positions, 'position').length === 0) {
30
29
  order = await placeOrder(input);
31
30
  }
32
31
  const assertion = await assertPositions(input, true);
@@ -1,3 +1,6 @@
1
+ // Submit and verify testnet market, limit or trigger orders through the headless controller.
2
+ // Supports declared rejection cases and attached protection parameters.
3
+
1
4
  import {
2
5
  configuredSymbols,
3
6
  controllerRejection,
@@ -12,24 +15,14 @@ import {
12
15
  selectedItems,
13
16
  } from './_controller.mjs';
14
17
 
15
- // core Place a real Perps order on HyperLiquid testnet by driving the headless
16
- // PerpsController.placeOrder() through the full signing/provider path (Slice 2).
17
- // Mirrors the extension adapter's input vocabulary and notional→size math:
18
- // size = (usdAmount * leverage) / currentPrice
19
- // The HyperLiquidProvider recalculates size from usdAmount internally; we still
20
- // pass a derived size for the controller's own validation, plus currentPrice/
21
- // priceAtCalculation so the provider's slippage guard has a fresh anchor.
22
- //
23
18
  // LIMIT orders (orderType=limit) place a RESTING order on the book instead of
24
19
  // filling: the provider sends tif=Gtc and uses the provided `price` verbatim
25
20
  // (calculateOrderPriceAndSize / buildOrdersArray in core's orderCalculations.ts).
26
21
  // The limit price is derived from the live mid by `offset_pct` (e.g. -30 places a
27
- // BUY 30% below mid that will NOT fill), or supplied absolutely via `limit_price`.
22
+ // BUY 30% below mid), or supplied absolutely via `limit_price`.
28
23
  // For a limit order we size from the LIMIT price (not mid) and omit usdAmount so
29
- // the controller uses our explicit size verbatim that guarantees the
30
- // on-exchange notional (size * limitPrice) clears HyperLiquid's ~$10 minimum even
31
- // though the limit sits far from mid. After placing we verify the resting open
32
- // order exists (not a filled position).
24
+ // the controller receives an explicit size; venue quantity normalization still
25
+ // applies. After placing we verify the resting open order exists, not a filled position.
33
26
  //
34
27
  // TRIGGER placements (order_type=stop_market | stop_limit | take_profit_market |
35
28
  // take_profit_limit) rest off-book until `trigger_price` is reached, then execute
@@ -567,7 +560,7 @@ export async function placeOrder(input) {
567
560
  userAddress: accountAddress,
568
561
  });
569
562
  }
570
- const matchingPositions = selectedItems(input, positions);
563
+ const matchingPositions = selectedItems(input, positions, 'position');
571
564
 
572
565
  if (restsOnBook && matchingOrders.length === 0) {
573
566
  throw new Error(
@@ -1,3 +1,5 @@
1
+ // Read the selected account's Perps balance and margin data through standalone controller APIs.
2
+
1
3
  import {
2
4
  getCoreController,
3
5
  isDirectRun,
@@ -1,3 +1,5 @@
1
+ // Read standalone Perps orders and return redacted selected data with total/matching counts.
2
+
1
3
  import {
2
4
  getCoreController,
3
5
  isDirectRun,
@@ -1,3 +1,5 @@
1
+ // Read standalone Perps positions and return redacted selected data with total/matching counts.
2
+
1
3
  import {
2
4
  getCoreController,
3
5
  isDirectRun,
@@ -12,7 +14,7 @@ export async function readPositions(input) {
12
14
  standalone: true,
13
15
  userAddress: accountAddress,
14
16
  });
15
- const matching = selectedItems(input, positions);
17
+ const matching = selectedItems(input, positions, 'position');
16
18
  return {
17
19
  action: input.action,
18
20
  source: 'perps-controller-standalone',
@@ -1,3 +1,6 @@
1
+ // Summarize public Perps market data, account state or both for a headless proof.
2
+ // Retain data-source and account identity metadata alongside redacted results.
3
+
1
4
  import {
2
5
  getCoreController,
3
6
  isDirectRun,
@@ -1,21 +1,10 @@
1
+ // Prepare selected testnet baselines before proof, handling positions before orders.
2
+ // Omitted domains are skipped; this headless action performs no UI navigation.
3
+
1
4
  import { isDirectRun, resolveNetwork, runAdapter } from './_controller.mjs';
2
5
  import { ensureOrders } from './ensure_orders.mjs';
3
6
  import { ensurePositions } from './ensure_positions.mjs';
4
7
 
5
- // core Headless composite that converges the POSITION and ORDER baselines for the
6
- // selected market(s) on HyperLiquid testnet before the proof window. Mirrors the
7
- // extension perps-domain-dispatcher start_state semantics but SKIPS every
8
- // navigation/page/HUD step — headless core has no UI surface.
9
- //
10
- // Params (extension schema, testnet-default): profile, market(s)/symbol(s),
11
- // positions{state,...}, orders{state,...}, network. The UI-only `page`/`hud`
12
- // fields are accepted by the schema but intentionally ignored here.
13
- //
14
- // A baseline block converges to its `state` by composing the ensure_* helpers; a
15
- // block set to `false` (or omitted) skips that domain. Position convergence runs
16
- // first (closing a position also clears its TP/SL orders), then order
17
- // convergence, so the final order baseline is authoritative.
18
-
19
8
  const BASELINE_KEYS = [
20
9
  'market',
21
10
  'symbol',