@deeeed/metamask-harness 0.47.4 → 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 (261) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +3 -2
  3. package/adapters/extension/lib/macos-focus.cjs +2 -2
  4. package/adapters/extension/live.sh +6 -8
  5. package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +1 -0
  6. package/adapters/shared/ensure-runner-deps.sh +6 -0
  7. package/dist/adapters/extension/runtime-decision.js +18 -1
  8. package/dist/adapters/extension/runtime.js +5 -5
  9. package/dist/adapters/extension/surface.js +1 -0
  10. package/dist/app-lifecycle.js +3 -1
  11. package/dist/command-contract.js +2 -0
  12. package/dist/commands/call.js +1 -8
  13. package/dist/commands/device-target.js +12 -3
  14. package/dist/commands/doctor.js +7 -5
  15. package/dist/commands/fixtures.js +1 -1
  16. package/dist/commands/help.js +17 -5
  17. package/dist/commands/launch/index.js +10 -0
  18. package/dist/commands/manifest.js +3 -1
  19. package/dist/commands/mobile-device-view.js +13 -3
  20. package/dist/commands/parse-args.js +2 -0
  21. package/dist/commands/run-engine.js +29 -4
  22. package/dist/commands/run.js +1 -1
  23. package/dist/commands/runtime-launch.js +10 -1
  24. package/dist/commands/status.js +8 -4
  25. package/dist/devices.js +31 -9
  26. package/dist/execution-provenance.js +8 -7
  27. package/dist/funding-execution-context.js +62 -17
  28. package/dist/heal-bounds.js +3 -3
  29. package/dist/live-adapter-contract.js +4 -0
  30. package/dist/manifest.js +2 -13
  31. package/dist/metamask-action-validation.js +3 -2
  32. package/dist/mm-harness-cli.js +4 -2
  33. package/dist/runner.js +14 -3
  34. package/docs/RECIPES.md +135 -24
  35. package/docs/RELEASE-QA-CAPABILITY-MAP.md +3 -2
  36. package/library/actions/core/perps/_controller.mjs +24 -24
  37. package/library/actions/core/perps/assert_orders.mjs +3 -5
  38. package/library/actions/core/perps/assert_positions.mjs +3 -5
  39. package/library/actions/core/perps/close_orders.mjs +39 -19
  40. package/library/actions/core/perps/close_positions.mjs +53 -12
  41. package/library/actions/core/perps/edit_order.mjs +2 -0
  42. package/library/actions/core/perps/ensure_orders.mjs +3 -4
  43. package/library/actions/core/perps/ensure_positions.mjs +4 -5
  44. package/library/actions/core/perps/place_order.mjs +7 -14
  45. package/library/actions/core/perps/read_account.mjs +2 -0
  46. package/library/actions/core/perps/read_orders.mjs +2 -0
  47. package/library/actions/core/perps/read_positions.mjs +3 -1
  48. package/library/actions/core/perps/read_snapshot.mjs +3 -0
  49. package/library/actions/core/perps/start_state.mjs +3 -14
  50. package/library/actions/core/perps/teardown_state.mjs +3 -14
  51. package/library/actions/core/perps/update_position_tpsl.mjs +13 -19
  52. package/library/actions/core/wallet/list_accounts.mjs +3 -0
  53. package/library/actions/extension/analytics/consent.mjs +2 -0
  54. package/library/actions/extension/analytics/set_consent.mjs +2 -0
  55. package/library/actions/extension/assets/home-token-identity.mjs +31 -0
  56. package/library/actions/extension/assets/import_custom_token.mjs +63 -79
  57. package/library/actions/extension/assets/open_details.mjs +19 -7
  58. package/library/actions/extension/assets/prepare_send.mjs +1 -0
  59. package/library/actions/extension/assets/read_details.mjs +62 -29
  60. package/library/actions/extension/assets/read_visible_state.mjs +5 -1
  61. package/library/actions/extension/assets/review_send.mjs +1 -0
  62. package/library/actions/extension/assets/select_network_scope.mjs +17 -9
  63. package/library/actions/extension/assets/send.mjs +2 -0
  64. package/library/actions/extension/assets/set_custom_gas.mjs +1 -0
  65. package/library/actions/extension/assets/set_token_visibility.mjs +86 -17
  66. package/library/actions/extension/assets/verify_sorting.mjs +63 -14
  67. package/library/actions/extension/deeplink/open.mjs +1 -0
  68. package/library/actions/extension/networks/add_chainlist.mjs +2 -0
  69. package/library/actions/extension/networks/add_custom.mjs +21 -17
  70. package/library/actions/extension/networks/custom_network.mjs +10 -3
  71. package/library/actions/extension/networks/read_visible_state.mjs +2 -0
  72. package/library/actions/extension/networks/remove_custom.mjs +1 -0
  73. package/library/actions/extension/performance/_navigation-memory.mjs +37 -6
  74. package/library/actions/extension/performance/compare_idle_navigation_memory.mjs +5 -2
  75. package/library/actions/extension/performance/measure_detached_dom.mjs +5 -2
  76. package/library/actions/extension/performance/measure_navigation_memory.mjs +5 -2
  77. package/library/actions/extension/perps/assert_orders.mjs +1 -0
  78. package/library/actions/extension/perps/assert_positions.mjs +1 -0
  79. package/library/actions/extension/perps/assert_visible_consistency.mjs +11 -4
  80. package/library/actions/extension/perps/close_orders.mjs +4 -1
  81. package/library/actions/extension/perps/close_positions.mjs +1 -0
  82. package/library/actions/extension/perps/close_visible_position.mjs +160 -11
  83. package/library/actions/extension/perps/compare_provider_market.mjs +2 -0
  84. package/library/actions/extension/perps/edit_margin.mjs +3 -1
  85. package/library/actions/extension/perps/ensure_orders.mjs +1 -0
  86. package/library/actions/extension/perps/ensure_positions.mjs +1 -0
  87. package/library/actions/extension/perps/mutation-receipt.mjs +54 -3
  88. package/library/actions/extension/perps/open_balance_action.mjs +17 -7
  89. package/library/actions/extension/perps/open_position_action.mjs +1 -0
  90. package/library/actions/extension/perps/perps.mjs +550 -69
  91. package/library/actions/extension/perps/place_order.mjs +1 -0
  92. package/library/actions/extension/perps/read_eligibility.mjs +1 -0
  93. package/library/actions/extension/perps/read_funds_confirmation.mjs +10 -0
  94. package/library/actions/extension/perps/read_orders.mjs +1 -0
  95. package/library/actions/extension/perps/read_positions.mjs +1 -0
  96. package/library/actions/extension/perps/read_snapshot.mjs +4 -1
  97. package/library/actions/extension/perps/read_visible_state.mjs +12 -6
  98. package/library/actions/extension/perps/search_markets.mjs +8 -2
  99. package/library/actions/extension/perps/select_activity_filter.mjs +2 -1
  100. package/library/actions/extension/perps/select_market_filter.mjs +75 -45
  101. package/library/actions/extension/perps/set_market_favorite.mjs +7 -3
  102. package/library/actions/extension/perps/state.mjs +2 -0
  103. package/library/actions/extension/perps/update_position_tpsl.mjs +3 -1
  104. package/library/actions/extension/perps/visible-mutation-identity.mjs +6 -2
  105. package/library/actions/extension/platform/cdp.mjs +50 -142
  106. package/library/actions/extension/settings/set_basic_functionality.mjs +1 -0
  107. package/library/actions/extension/swap_bridge/quote-state.mjs +56 -0
  108. package/library/actions/extension/swap_bridge/read_visible_state.mjs +5 -1
  109. package/library/actions/extension/swap_bridge/select_assets.mjs +12 -1
  110. package/library/actions/extension/swap_bridge/set_amount.mjs +11 -8
  111. package/library/actions/extension/swap_bridge/set_max_amount.mjs +18 -18
  112. package/library/actions/extension/swap_bridge/set_slippage.mjs +1 -9
  113. package/library/actions/extension/ui/locators.mjs +1 -0
  114. package/library/actions/extension/ui/navigate.mjs +60 -14
  115. package/library/actions/extension/wallet/ensure_unlocked.mjs +2 -0
  116. package/library/actions/extension/wallet/import.mjs +2 -0
  117. package/library/actions/extension/wallet/list_accounts.mjs +1 -0
  118. package/library/actions/extension/wallet/lock.mjs +4 -1
  119. package/library/actions/extension/wallet/read_state.mjs +1 -0
  120. package/library/actions/extension/wallet/reset.mjs +2 -0
  121. package/library/actions/extension/wallet/secret-input.mjs +2 -0
  122. package/library/actions/extension/wallet/select_account.mjs +2 -0
  123. package/library/actions/extension/wallet/setup.mjs +1 -0
  124. package/library/actions/extension/wallet/state.mjs +2 -0
  125. package/library/actions/extension/wallet/visible-session.mjs +2 -0
  126. package/library/actions/mobile/analytics/consent-settings.mjs +1 -0
  127. package/library/actions/mobile/analytics/set_consent.mjs +1 -0
  128. package/library/actions/mobile/app/network-control.mjs +2 -0
  129. package/library/actions/mobile/app/network.mjs +1 -0
  130. package/library/actions/mobile/app/network_assert.mjs +1 -0
  131. package/library/actions/mobile/app/network_capture.mjs +1 -0
  132. package/library/actions/mobile/assets/import_custom_token.mjs +3 -0
  133. package/library/actions/mobile/assets/open_details.mjs +1 -0
  134. package/library/actions/mobile/assets/read_details.mjs +1 -0
  135. package/library/actions/mobile/assets/read_visible_state.mjs +4 -1
  136. package/library/actions/mobile/assets/set_token_visibility.mjs +15 -9
  137. package/library/actions/mobile/assets/verify_sorting.mjs +90 -7
  138. package/library/actions/mobile/deeplink/open.mjs +1 -0
  139. package/library/actions/mobile/networks/add_custom.mjs +1 -0
  140. package/library/actions/mobile/networks/network-management.mjs +23 -15
  141. package/library/actions/mobile/networks/read_visible_state.mjs +1 -0
  142. package/library/actions/mobile/networks/remove_custom.mjs +7 -2
  143. package/library/actions/mobile/perps/assert_orders.mjs +1 -0
  144. package/library/actions/mobile/perps/assert_positions.mjs +1 -0
  145. package/library/actions/mobile/perps/capture_performance.mjs +1 -0
  146. package/library/actions/mobile/perps/close_orders.mjs +1 -0
  147. package/library/actions/mobile/perps/close_positions.mjs +1 -0
  148. package/library/actions/mobile/perps/ensure_orders.mjs +1 -0
  149. package/library/actions/mobile/perps/ensure_positions.mjs +1 -0
  150. package/library/actions/mobile/perps/measure_homepage_visible.mjs +1 -0
  151. package/library/actions/mobile/perps/performance-capture.mjs +2 -0
  152. package/library/actions/mobile/perps/perps.mjs +8 -1
  153. package/library/actions/mobile/perps/place_order.mjs +1 -0
  154. package/library/actions/mobile/perps/prepare_local_snapshot_endpoint.mjs +2 -0
  155. package/library/actions/mobile/perps/read-visible-state-loop.mjs +3 -0
  156. package/library/actions/mobile/perps/read_orders.mjs +1 -0
  157. package/library/actions/mobile/perps/read_positions.mjs +1 -0
  158. package/library/actions/mobile/perps/read_visible_state.mjs +1 -0
  159. package/library/actions/mobile/perps/search_markets.mjs +1 -0
  160. package/library/actions/mobile/perps/set_market_favorite.mjs +17 -19
  161. package/library/actions/mobile/platform/bridge.mjs +16 -4
  162. package/library/actions/mobile/platform/native-session-name.mjs +1 -0
  163. package/library/actions/mobile/platform/native-session.mjs +28 -17
  164. package/library/actions/mobile/platform/observe-ui.mjs +4 -2
  165. package/library/actions/mobile/platform/tool-paths.mjs +1 -0
  166. package/library/actions/mobile/swap_bridge/native-session.mjs +1 -0
  167. package/library/actions/mobile/swap_bridge/read_visible_state.mjs +1 -0
  168. package/library/actions/mobile/swap_bridge/select_assets.mjs +2 -0
  169. package/library/actions/mobile/swap_bridge/set_amount.mjs +1 -0
  170. package/library/actions/mobile/swap_bridge/set_max_amount.mjs +1 -0
  171. package/library/actions/mobile/swap_bridge/set_slippage.mjs +1 -0
  172. package/library/actions/mobile/swap_bridge/submit_transaction.mjs +2 -0
  173. package/library/actions/mobile/ui/locators.mjs +1 -0
  174. package/library/actions/mobile/ui/native-navigation.mjs +2 -0
  175. package/library/actions/mobile/ui/navigate.mjs +3 -1
  176. package/library/actions/mobile/wallet/ensure_unlocked.mjs +2 -0
  177. package/library/actions/mobile/wallet/home.mjs +1 -0
  178. package/library/actions/mobile/wallet/import.mjs +2 -0
  179. package/library/actions/mobile/wallet/list_accounts.mjs +1 -0
  180. package/library/actions/mobile/wallet/lock.mjs +1 -0
  181. package/library/actions/mobile/wallet/native-ui.mjs +2 -0
  182. package/library/actions/mobile/wallet/read_state.mjs +2 -0
  183. package/library/actions/mobile/wallet/reset-helper.mjs +1 -0
  184. package/library/actions/mobile/wallet/reset.mjs +1 -0
  185. package/library/actions/mobile/wallet/select_account.mjs +2 -0
  186. package/library/actions/mobile/wallet/setup.mjs +2 -0
  187. package/library/actions/shared/analytics/assert_events.mjs +3 -0
  188. package/library/actions/shared/analytics/consent.mjs +3 -0
  189. package/library/actions/shared/analytics/read_events.mjs +2 -0
  190. package/library/actions/shared/analytics/start_capture.mjs +3 -0
  191. package/library/actions/shared/app/network-artifact.mjs +2 -0
  192. package/library/actions/shared/app/network-assert.mjs +2 -0
  193. package/library/actions/shared/assets/visible-state.mjs +2 -0
  194. package/library/actions/shared/deeplink/url.mjs +3 -0
  195. package/library/actions/shared/networks/visible-state.mjs +2 -0
  196. package/library/actions/shared/perps/visible-state.mjs +16 -3
  197. package/library/actions/shared/swap-bridge/transaction.mjs +3 -0
  198. package/library/actions/shared/swap-bridge/visible-state.mjs +2 -0
  199. package/library/actions/shared/ui/locators.mjs +2 -0
  200. package/library/actions/shared/wallet/import-source.mjs +3 -0
  201. package/library/manifests/core.action-manifest.json +34 -2
  202. package/library/manifests/extension.action-manifest.json +50 -17
  203. package/library/manifests/mobile.action-manifest.json +11 -10
  204. package/library/recipes/core/perps/smoke.recipe.json +23 -0
  205. package/library/recipes/core/perps/snapshot.recipe.json +26 -1
  206. package/library/recipes/extension/assets/release-custom-network-token-persistence.recipe.json +25 -8
  207. package/library/recipes/extension/performance/navigation-memory-suite.recipe.json +219 -0
  208. package/library/recipes/extension/performance/navigation-memory.recipe.json +61 -156
  209. package/library/recipes/extension/wallet/smoke.recipe.json +17 -8
  210. package/library/recipes/mobile/app/lifecycle-smoke.recipe.json +89 -0
  211. package/library/recipes/mobile/networks/release-custom-network-persistence.recipe.json +31 -6
  212. package/library/recipes/mobile/networks/release-multichain-visibility.recipe.json +5 -4
  213. package/library/recipes/mobile/wallet/smoke.recipe.json +17 -8
  214. package/library/recipes/{assets → shared/assets}/release-token-address-import.recipe.json +7 -0
  215. package/library/recipes/{assets → shared/assets}/release-token-details.recipe.json +2 -2
  216. package/library/recipes/shared/assets/release-token-hide-readd.recipe.json +91 -0
  217. package/library/recipes/{assets → shared/assets}/release-token-persistence.recipe.json +3 -0
  218. package/library/recipes/{assets → shared/assets}/release-token-search-import.recipe.json +4 -1
  219. package/library/recipes/{assets → shared/assets}/release-token-sorting.recipe.json +13 -2
  220. package/library/recipes/{swap-bridge → shared/swap-bridge}/release-quote.recipe.json +29 -3
  221. package/package.json +3 -3
  222. package/site/assets/help-recipes.json +220 -27
  223. package/library/recipes/assets/release-token-hide-readd.recipe.json +0 -57
  224. package/library/recipes/core/perps/clean-market-testnet.recipe.json +0 -44
  225. package/library/recipes/core/perps/order-lifecycle.recipe.json +0 -82
  226. package/library/recipes/core/perps/read-markets.recipe.json +0 -36
  227. package/library/recipes/core/perps/trading-lifecycle.recipe.json +0 -80
  228. package/library/recipes/extension/perps/release-activity-filters.recipe.json +0 -51
  229. package/library/recipes/extension/perps/release-funds-flows.recipe.json +0 -76
  230. package/library/recipes/extension/perps/release-live-limit-order.recipe.json +0 -109
  231. package/library/recipes/extension/perps/release-market-filters.recipe.json +0 -97
  232. package/library/recipes/extension/perps/release-order-entry.recipe.json +0 -158
  233. package/library/recipes/extension/perps/release-order-validation.recipe.json +0 -259
  234. package/library/recipes/extension/perps/release-visible-consistency.recipe.json +0 -47
  235. package/library/recipes/extension/perps/source-dev-snapshot-consistency.recipe.json +0 -76
  236. package/library/recipes/mobile/app/lifecycle.android-smoke.recipe.json +0 -69
  237. package/library/recipes/mobile/perps/chase-assert-running.recipe.json +0 -110
  238. package/library/recipes/mobile/perps/chase-place.recipe.json +0 -145
  239. package/library/recipes/mobile/perps/chase-terminate.recipe.json +0 -98
  240. package/library/recipes/mobile/perps/performance.recipe.json +0 -722
  241. package/library/recipes/mobile/perps/pro-order-setup.recipe.json +0 -111
  242. package/library/recipes/mobile/perps/pro-order-start-state.recipe.json +0 -53
  243. package/library/recipes/mobile/perps/scale-assert-orders.recipe.json +0 -126
  244. package/library/recipes/mobile/perps/scale-place.recipe.json +0 -186
  245. package/library/recipes/mobile/perps/twap-assert-active.recipe.json +0 -97
  246. package/library/recipes/mobile/perps/twap-place.recipe.json +0 -146
  247. package/library/recipes/mobile/runner/action-validation.recipe.json +0 -336
  248. package/library/recipes/perps/clean-market-testnet.recipe.json +0 -49
  249. package/library/recipes/perps/lifecycle.recipe.json +0 -144
  250. package/library/recipes/perps/release-live-market-order.recipe.json +0 -139
  251. package/library/recipes/perps/release-watchlist.recipe.json +0 -111
  252. package/library/recipes/swap-bridge/release-max-quote.recipe.json +0 -72
  253. /package/library/recipes/{assets → shared/assets}/release-token-list.recipe.json +0 -0
  254. /package/library/recipes/{perps → shared/perps}/release-market-details.recipe.json +0 -0
  255. /package/library/recipes/{perps → shared/perps}/release-market-search.recipe.json +0 -0
  256. /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-custom-slippage.recipe.json +0 -0
  257. /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-live-max-swap.recipe.json +0 -0
  258. /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-live-swap.recipe.json +0 -0
  259. /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-surface.recipe.json +0 -0
  260. /package/library/recipes/{wallet → shared/wallet}/import.recipe.json +0 -0
  261. /package/library/recipes/{wallet → shared/wallet}/reset-import.recipe.json +0 -0
package/docs/RECIPES.md CHANGED
@@ -32,7 +32,7 @@ task proof and may call other recipes.
32
32
  "paramsSchema": {
33
33
  "type": "object",
34
34
  "properties": {
35
- "account": { "type": "string", "default": "Account 1" }
35
+ "timeout_ms": { "type": "integer", "minimum": 1, "default": 15000 }
36
36
  },
37
37
  "additionalProperties": false
38
38
  },
@@ -41,6 +41,7 @@ task proof and may call other recipes.
41
41
  "nodes": {
42
42
  "unlock": {
43
43
  "action": "metamask.wallet.ensure_unlocked",
44
+ "timeout_ms": "{{params.timeout_ms}}",
44
45
  "intent": "Make the fixture-backed wallet ready for use",
45
46
  "next": "done"
46
47
  },
@@ -54,8 +55,8 @@ Defaults are applied before validation. Root values use `key=value`; a nested
54
55
  recipe call uses `action: "call"`, `ref`, and `params`.
55
56
 
56
57
  ```bash
57
- mm-harness run recipe.json account="Account 2" --plan
58
- mm-harness run recipe.json account="Account 2"
58
+ mm-harness run recipe.json timeout_ms=20000 --plan
59
+ mm-harness run recipe.json timeout_ms=20000
59
60
  ```
60
61
 
61
62
  Use the shared wallet setup recipe on either Mobile or Extension. `auto`
@@ -76,24 +77,53 @@ fixture. Secret values are redacted from commands and evidence. Visible imports
76
77
  default to MetaMetrics off and skip Mobile's optional interests; password terms
77
78
  are required and always accepted by the action.
78
79
 
80
+ ### Retained-memory measurements
81
+
82
+ Budget failures retain their samples and limits in a private JSON report linked
83
+ from the failed node's error. Inspect that report before attributing growth or
84
+ changing a budget; it is supporting evidence, not a passing measurement.
85
+
86
+ Use `performance.navigation-memory` for one screen in an unlocked Extension.
87
+ Choose its current route/marker, sample count, and growth bounds before running:
88
+
89
+ ```sh
90
+ mm-harness run performance.navigation-memory \
91
+ 'screen_hash=#/settings' screen_test_id=settings-tab-bar-grouped --hud hide
92
+ ```
93
+
94
+ `performance.navigation-memory-suite` retains the full eight-screen audit,
95
+ detached-DOM check, and idle-control comparison. A one-screen slope does not
96
+ prove those broader claims. Check the suite's reference markers against the
97
+ tested build before running it.
98
+
79
99
  ### Extension Perps release recipes
80
100
 
81
101
  Discover these identifiers with `run --list --adapter extension --json` and
82
102
  inspect the exact graph with `run <id> --describe --adapter extension --json`:
83
103
 
104
+ Market/activity filters, visible consistency, funds previews, order-form cases,
105
+ live order journeys, watchlist and source-dev snapshot consistency are owned by the
106
+ [Perps team library](https://github.com/MetaMask/experimental-metamask-recipe-perps).
107
+ Register the library checkout to discover and compose those unchanged names:
108
+
109
+ ```sh
110
+ export RECIPE_LIBRARY_PATH="perps=/path/to/experimental-metamask-recipe-perps"
111
+ mm-harness run perps.release-market-filters --describe --adapter extension --json
112
+ ```
113
+
84
114
  | Behavior | Canonical recipe |
85
115
  | --- | --- |
86
116
  | Activity filter routes | `perps.release-activity-filters` |
87
117
  | Deposit and withdraw previews without submission | `perps.release-funds-flows` |
88
- | Resting limit placement and exact cancellation | `perps.release-live-limit-order` |
89
- | 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` |
90
120
  | Market header and statistics | `perps.release-market-details` |
91
121
  | Market categories | `perps.release-market-filters` |
92
122
  | Market text search | `perps.release-market-search` |
93
- | Long/Short and Market/Limit entry controls | `perps.release-order-entry` |
94
- | Amount, leverage, keyboard, auto-close, and TP/SL validation | `perps.release-order-validation` |
123
+ | Long/Short and Market/Limit entry controls | `perps.release-order-entry case=configuration` |
124
+ | Amount, leverage, keyboard, auto-close, and TP/SL validation | `perps.release-order-entry case=validation` |
95
125
  | Official-build rendered market/activity consistency | `perps.release-visible-consistency` |
96
- | Favorite persistence and cleanup | `perps.release-watchlist` |
126
+ | Favorite add/remove persistence and observed-baseline restoration | `perps.release-watchlist` |
97
127
  | Hook-backed controller snapshot consistency | `perps.source-dev-snapshot-consistency` |
98
128
 
99
129
  The source-development snapshot recipe is compatibility evidence only and must
@@ -102,11 +132,22 @@ Extension-scoped because their semantic actions are not shared release
102
132
  capabilities. Keep ticket-specific orchestration task-local and compose these
103
133
  identifiers with `call` instead of copying their graphs.
104
134
 
105
- Order-entry and order-validation callers must supply a market with no existing
135
+ Watchlist proof requires a visible market-bound favorite baseline before any
136
+ toggle. Unknown is not false. Restoration is verified on the audited Extension
137
+ runtime; the audited Mobile reader cannot establish that baseline, so it must
138
+ stop before mutation rather than infer identity from unrelated market text.
139
+
140
+ The order-form recipe is in the registered Perps team library. Both cases require a market with no existing
106
141
  position; the product exposes Modify/Close instead of New Long/Short for an
107
- already-open market. Source-development snapshot callers may opt into a
142
+ already-open market. Validation prices and leverage must match the selected
143
+ market's current rules; the examples are not universal defaults. Give each
144
+ composed case a distinct `capture_path`.
145
+ Source-development snapshot callers may opt into a
108
146
  positive `minimum_changed_market_count`; the default does not require a price
109
147
  tick, while typed-history and visible-activity agreement remain strict.
148
+ Use the snapshot recipe's `history` object to bound existing record dates and
149
+ `capture_path` to separate evidence when composing calls. Market sampling does
150
+ not repeatedly fetch history.
110
151
 
111
152
  The two live recipes require explicit `confirm_live=true`. The trusted runner
112
153
  must also verify the exact consented funding request, preflight, release proof,
@@ -126,15 +167,56 @@ using a generic UI press invalidates the cached identity proof; a fresh visible
126
167
  `ensure_positions`, `ensure_orders`, and `teardown_state` only observe and assert
127
168
  state that already exists; their schemas reject creation and cleanup inputs.
128
169
  Extension does not expose `start_state`, readiness claims, multi-position
129
- background closure, or the mutating `runner.action-validation` recipe. Financial
170
+ background closure. The former `runner.action-validation` is now a repository-only
171
+ maintainer exercise at `scripts/qa/mobile-action-validation.recipe.json`, outside
172
+ the installed catalog. It requires explicit disposable setup and reserved testnet
173
+ account/market inputs; it is not a full action audit or a normal task recipe. Financial
130
174
  transitions must use an explicitly funded leaf action.
131
- The shared `perps.clean-market-testnet` convergence recipe is therefore Mobile-
132
- only: Extension discovery omits it and a direct Extension plan fails statically.
175
+ The Perps team library owns `perps.clean-market-testnet` for Mobile and Core,
176
+ not Extension. Register that library before discovery or execution.
177
+ They require explicit `account`, `market` and `exclusive_testnet_market=true`.
178
+ That confirmation does not reserve the market or authorize deleting someone
179
+ else's trades. Plan first and execute only with actual task ownership.
133
180
  Planning or calling a recipe never weakens those gates.
134
181
 
182
+ ### Mobile advanced-order setup
183
+
184
+ These recipes belong to the Perps team library. Register it once:
185
+
186
+ ```sh
187
+ export RECIPE_LIBRARY_PATH="perps=/path/to/experimental-metamask-recipe-perps"
188
+ ```
189
+
190
+ `perps.pro-order-setup` preserves existing orders, positions, and strategies by
191
+ default. Its Scale, Chase, and TWAP placement callers inherit that policy.
192
+ When a proof requires a clean testnet market, reserve the account/market and
193
+ compose explicit setup with `trading_state=clean_selected_testnet_market`.
194
+ This parameter never authorizes cleanup of another task's trades.
195
+
196
+ `perps.scale-assert-orders` checks the complete market order count before
197
+ capturing three visible rows. The generic `assert_output` action compares a
198
+ prior node's structured field, for example `$.matchingCount` with `eq` and
199
+ `{{params.orders}}`; it is not limited to command output. Count and visibility
200
+ still need task-owned submission IDs to prove a specific ladder.
201
+
202
+ ### Fixed and Max quotes
203
+
204
+ `swap-bridge.release-quote` uses one graph for both amount modes. It defaults to
205
+ a fixed 0.0001 ETH-to-WETH quote on Ethereum with a 60-second transition budget.
206
+ Max ignores `amount` and uses the selected asset's available balance. Neither
207
+ mode submits a transaction.
208
+
209
+ ```sh
210
+ mm-harness run swap-bridge.release-quote amount=0.0001
211
+ mm-harness run swap-bridge.release-quote amount_mode=max
212
+ ```
213
+
214
+ Set `capture_path` to a distinct filename when composing multiple quotes in
215
+ one run. Pair, chain, asset identity, and timeout inputs remain explicit options.
216
+
135
217
  ### Extension submission boundary
136
218
 
137
- Extension currently exposes read-only Assets and Swap/Bridge evidence plus send
219
+ Extension currently exposes non-submitting Assets and Swap/Bridge evidence plus send
138
220
  preparation/review, quote, Max, and slippage actions. It does not expose
139
221
  `metamask.assets.finish_send`, `metamask.swap_bridge.submit_transaction`, or
140
222
  generic `ui.activate_and_observe`. Those operations remain unavailable until a
@@ -178,12 +260,13 @@ as supporting stills.
178
260
 
179
261
  ### Perps loading lifecycle validation
180
262
 
181
- One platform-neutral recipe owns the reusable Perps lifecycle proof. The
182
- harness adapter supplies Android or iOS behavior; the graph and lifecycle
183
- parameters remain identical. Run it with an explicit device pin:
263
+ The Mobile loading recipe shares one graph across Android and iOS. It requires
264
+ the product's source-dev instrumentation; platform parity still needs live
265
+ validation. Register the Perps team library, then pin the device:
184
266
 
185
267
  ```bash
186
268
  mm-harness run perps.performance \
269
+ --library perps=/path/to/experimental-metamask-recipe-perps \
187
270
  --device <android-serial-or-ios-udid> \
188
271
  account=<fixture-account-name> \
189
272
  content_variant=trending \
@@ -196,10 +279,10 @@ The Mobile recipe HUD is shown by default, including during performance
196
279
  validation. Use `--hud hide` only when a specific opaque/release proof requires
197
280
  an unobstructed product surface; do not hide it merely to collect timings.
198
281
 
199
- Supported lifecycle values are `navigate_return`, `cold_disk_cache`,
200
- `cold_no_cache`, `background_short`, `background_reconnect`, `account_switch`,
201
- and `network_switch`. The shared graph uses an existing fixture position and
202
- never fabricates exchange state inside the measurement. It proves native
282
+ Declared lifecycle values are `navigate_return`, `cold_no_cache`,
283
+ `background_short`, `background_reconnect`, `account_switch`, and
284
+ `network_switch`. The graph requires an existing position or an account with
285
+ no positions or orders; it never trades to create either prerequisite. It checks native
203
286
  visible content with `ui.wait_for`, captures production `[PerpsLoadProof]` and
204
287
  WebSocket milestones, and leaves detailed TTC/DFD to the app's Sentry traces.
205
288
  It never relabels Metro, fixture, or process-start duration as TTC.
@@ -207,6 +290,12 @@ The recipe does not label a checkout as main or candidate; pin and record the
207
290
  exact checkout commit externally, then use captured source milestones to verify
208
291
  which data path actually ran.
209
292
 
293
+ Warm return and short resume require resident-content milestones. The other
294
+ lifecycles also require live positions, orders and account streams. Each group
295
+ shares one capture ending, with the lifecycle and content identity preserved.
296
+ Cold preparation clears Perps disk/controller caches and disconnects its
297
+ channels before capture; an explicit process restart begins the proof.
298
+
210
299
  Rules:
211
300
 
212
301
  - Validate with `--plan` before side effects.
@@ -237,8 +326,9 @@ Locators, labels, routes, and screen structure can legitimately change, so a
237
326
  failed recipe is not by itself evidence of either recipe drift or a product
238
327
  regression.
239
328
 
240
- 1. Reproduce the failing node and compare it with authoritative acceptance
241
- criteria, product contracts, or known-good evidence.
329
+ 1. Preserve the first failure and compare it with authoritative acceptance
330
+ criteria, product contracts, or known-good evidence. Reproduce only after
331
+ confirming the action is safe to repeat.
242
332
  2. Inspect the current product surface and resolved recipe source, then classify
243
333
  the cause as product regression, recipe drift, fixture/runtime drift, or
244
334
  harness defect.
@@ -268,6 +358,23 @@ If changing a value would violate the recipe's postcondition, keep it as a
268
358
  documented recipe invariant rather than a parameter. Add a shared action only
269
359
  when repeated direct access has a stable cross-task contract.
270
360
 
361
+ Core actions construct isolated controllers. Pass account and network into
362
+ each action; a separate initialization node cannot prepare the next process.
363
+ Skip setup that does no work, while retaining the independent read assertions.
364
+ Signing requires a non-empty fixture account name. Address-only inputs support
365
+ reads; they must not silently choose the default signing account.
366
+
367
+ The Perps team library's `perps.trading-lifecycle` combines the Core long market
368
+ and limit examples. The harness retains read-only `perps.smoke` and
369
+ `perps.snapshot` examples; trading policy belongs to the team library.
370
+ It requires an explicit fixture account, market, and confirmation of exclusive
371
+ testnet ownership. Initial assertions refuse non-flat state rather than clearing
372
+ it. Failure-path cleanup is guarded by completed initial assertions and remains
373
+ market-scoped, so it cannot safely share that account/market with another task.
374
+ The limit branch uses the original 30%-below-mid offset; it must prove the order
375
+ rests rather than assume it cannot fill. These trading paths still require live
376
+ validation with a reserved funded testnet account.
377
+
271
378
  The protocol is authoritative:
272
379
  <https://farmslot.io/docs/reference/recipe-protocol-v1>.
273
380
 
@@ -307,7 +414,11 @@ mm-harness run onboarding.smoke --plan
307
414
  Use `--library wallet=/path/to/library` for one command. The alias is the
308
415
  source name; without one, the directory name is used. Resolution follows the
309
416
  explicit library order, then bundled MetaMask. Adapter-specific variants are
310
- selected deterministically from `recipes/<adapter>/<domain>/`. Legacy
417
+ selected deterministically from `recipes/<adapter>/<domain>/`. Identical
418
+ cross-adapter graphs live once in `recipes/shared/<domain>/`; an adapter-specific
419
+ variant takes precedence. Both resolve as `<domain>.<recipe>`, without a platform
420
+ or `shared` prefix. Keep domains below `core`, `extension`, `mobile`, or `shared`.
421
+ Legacy
311
422
  `*.<adapter>.recipe.json` paths remain readable during migration, but new and
312
423
  migrated libraries use the adapter-first layout. A file cannot declare its
313
424
  adapter through both forms. Every run records the root recipe, exact resolved
@@ -22,8 +22,9 @@ and trusted SHA-256. mm-harness never discovers or downloads a release.
22
22
 
23
23
  For Extension, `runtime-launch` verifies the ZIP digest and manifest version,
24
24
  extracts it safely, records archive and tree provenance, copies the exact tree
25
- to an isolated runtime directory, resets only the selected Chrome profile, and
26
- launches without building or patching the checkout.
25
+ to an isolated runtime directory, preserves the selected Chrome profile, and
26
+ launches without building or patching the checkout. A profile reset requires
27
+ explicit `--reset-profile`; an ordinary launch does not apply a wallet fixture.
27
28
 
28
29
  For Android, `runtime-launch` requires one physical-device serial, verifies the
29
30
  local APK digest, package ID, version name, and version code, installs it, and
@@ -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
  }
@@ -498,6 +486,16 @@ function registerSignerHandlers(rootMessenger, childMessenger, account) {
498
486
  * @returns { controller, projectRoot, network, accountAddress, signerAddress }.
499
487
  */
500
488
  export async function getCoreControllerWithSigner(input) {
489
+ const requestedName = input.node?.account_name ?? input.node?.account;
490
+ if (requestedName !== undefined && (
491
+ typeof requestedName !== 'string' || !requestedName.trim() ||
492
+ /^0x[0-9a-fA-F]{40}$/u.test(requestedName.trim())
493
+ )) {
494
+ throw new Error(
495
+ 'Core signing requires a non-empty fixture account name, not an address.\n' +
496
+ 'Next: pass account=<reserved-fixture-name>; use addresses only for read-only actions.',
497
+ );
498
+ }
501
499
  const base = await getCoreController(input);
502
500
  const { controller, messenger: childMessenger, rootMessenger, accountAddress } = base;
503
501
  if (!rootMessenger || !childMessenger) {
@@ -580,12 +578,7 @@ export function symbolForItem(item) {
580
578
  return normalizeMarketSymbol(item?.symbol ?? item?.coin ?? '');
581
579
  }
582
580
 
583
- // Normalize an item's side to the long/short vocabulary the selector uses.
584
- // Positions report side as long/short already; OPEN ORDERS report it as buy/sell
585
- // (a resting BUY = long direction, a resting SELL = short). Mapping both onto
586
- // long/short lets the shared `side` selector filter positions and orders alike —
587
- // without this, requesting side:"long" silently drops every order (whose side is
588
- // "buy"), which is the bug this normalization fixes.
581
+ // Orders carry buy/sell sides; Position.size is signed and authoritative.
589
582
  function normalizeSide(rawSide) {
590
583
  const side = String(rawSide ?? '').toLowerCase();
591
584
  if (side === 'buy' || side === 'b') return 'long';
@@ -593,7 +586,14 @@ function normalizeSide(rawSide) {
593
586
  return side;
594
587
  }
595
588
 
596
- 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
+ }
597
597
  return normalizeSide(item?.side ?? item?.direction ?? '');
598
598
  }
599
599
 
@@ -636,7 +636,7 @@ export function requireExplicitSelection(input) {
636
636
  * vocabulary as the extension perps adapter. With no selector and no
637
637
  * mode=all, returns every item (read defaults to showing all live state).
638
638
  */
639
- export function selectedItems(input, items) {
639
+ export function selectedItems(input, items, itemType = 'order') {
640
640
  const requested = configuredSymbols(input, items);
641
641
  const selector =
642
642
  input.node?.selector && typeof input.node.selector === 'object' ? input.node.selector : {};
@@ -645,7 +645,7 @@ export function selectedItems(input, items) {
645
645
  const symbols = requested.length > 0 ? new Set(requested) : null;
646
646
  return items.filter((item) => {
647
647
  if (symbols && !symbols.has(symbolForItem(item))) return false;
648
- if (side && sideForItem(item) && sideForItem(item) !== side) return false;
648
+ if (side && sideForItem(item, itemType) !== side) return false;
649
649
  return true;
650
650
  });
651
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,21 +14,11 @@ import {
10
14
  optionalParam,
11
15
  } from './_controller.mjs';
12
16
 
13
- // core Cancel selected live Perps open orders on HyperLiquid testnet by driving
14
- // the headless PerpsController.cancelOrders() through the full signing/provider
15
- // path (Slice 2). Mirrors close_positions.mjs: read open orders → cancel the
16
- // selected subset via the controller → verify the selected orders are gone.
17
- //
18
- // cancelOrders() resolves the concrete orders to cancel via the ACTIVE provider's
19
- // getOpenOrders() (PerpsController wires getOpenOrders: () => this.getOpenOrders()
20
- // into the cancel context) and then batch-cancels by symbol. We pass the selected
21
- // symbols; with mode=all (no explicit market) we cancel every open order.
22
-
23
17
  function sleep(ms) {
24
18
  return new Promise((resolve) => setTimeout(resolve, ms));
25
19
  }
26
20
 
27
- async function waitForOrdersAbsent(controller, accountAddress, symbols, timeoutMs) {
21
+ async function waitForOrdersAbsent(controller, accountAddress, orderIds, timeoutMs) {
28
22
  const deadline = Date.now() + timeoutMs;
29
23
  let orders = [];
30
24
  for (;;) {
@@ -32,7 +26,7 @@ async function waitForOrdersAbsent(controller, accountAddress, symbols, timeoutM
32
26
  standalone: true,
33
27
  userAddress: accountAddress,
34
28
  });
35
- const remaining = orders.filter((order) => symbols.includes(symbolForItem(order)));
29
+ const remaining = orders.filter((order) => orderIds.includes(order.orderId));
36
30
  if (remaining.length === 0) return orders;
37
31
  if (Date.now() >= deadline) return orders;
38
32
  await sleep(1000);
@@ -53,7 +47,15 @@ export function cancelOrdersFailure(result) {
53
47
 
54
48
  export async function closeOrders(input) {
55
49
  requireExplicitSelection(input);
56
- 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));
57
59
  const timeoutMs = Number(
58
60
  optionalParam(input.node ?? {}, 'timeout_ms', 'timeoutMs') ?? 30000,
59
61
  );
@@ -63,6 +65,14 @@ export async function closeOrders(input) {
63
65
  userAddress: accountAddress,
64
66
  });
65
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);
66
76
  const symbols = Array.from(new Set(matching.map(symbolForItem)));
67
77
 
68
78
  if (symbols.length === 0) {
@@ -79,20 +89,29 @@ export async function closeOrders(input) {
79
89
  };
80
90
  }
81
91
 
82
- // Cancel by the selected symbols. cancelOrders() filters the active provider's
83
- // open orders to these symbols and batch-cancels them on the exchange.
84
- const result = await controller.cancelOrders({ symbols });
92
+ const orderIds = matching.map((order) => order.orderId);
93
+ if (orderIds.some((id) => typeof id !== 'string' || !id.trim()) || new Set(orderIds).size !== orderIds.length) {
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');
95
+ }
96
+ if (expectedIds !== undefined) {
97
+ await getCoreControllerWithSigner(input);
98
+ checkExpectedIds(selectedItems(input, await controller.getOpenOrders({
99
+ standalone: true,
100
+ userAddress: accountAddress,
101
+ })));
102
+ }
103
+ const result = await controller.cancelOrders({ orderIds });
85
104
  const failure = cancelOrdersFailure(result);
86
105
  if (failure !== undefined) {
87
106
  throw controllerRejection({
88
107
  action: 'core cancelOrders',
89
- detail: `${JSON.stringify(symbols)}: ${JSON.stringify(result)}`,
108
+ detail: `${JSON.stringify(orderIds)}: ${JSON.stringify(result)}`,
90
109
  code: failure,
91
110
  });
92
111
  }
93
112
 
94
- const after = await waitForOrdersAbsent(controller, accountAddress, symbols, timeoutMs);
95
- const stillOpen = after.filter((order) => symbols.includes(symbolForItem(order)));
113
+ const after = await waitForOrdersAbsent(controller, accountAddress, orderIds, timeoutMs);
114
+ const stillOpen = after.filter((order) => orderIds.includes(order.orderId));
96
115
  if (stillOpen.length > 0) {
97
116
  throw new Error(
98
117
  `Expected selected orders to cancel, but ${stillOpen.length} remain: ${JSON.stringify(result)}`,
@@ -109,6 +128,7 @@ export async function closeOrders(input) {
109
128
  successCount: result.successCount,
110
129
  failureCount: result.failureCount,
111
130
  symbols,
131
+ orderIds,
112
132
  results: result.results,
113
133
  orders: stillOpen.map(redactOrder),
114
134
  proofPath: 'perps-controller-cancelOrders',
@@ -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,