@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
@@ -1,9 +1,40 @@
1
+ // Runs navigation and idle memory measurements with forced GC, CDP metrics, and heap snapshots.
2
+ // Checks configured growth bounds and writes diagnostic artifacts for failed measurements.
3
+ import { mkdtemp, writeFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+
1
6
  const DEFAULT_SCREEN_TIMEOUT_MS = 15_000;
2
7
  const DEFAULT_SETTLE_MS = 1_500;
3
8
  const DEFAULT_GC_SETTLE_MS = 300;
4
9
  const DEFAULT_ACTION_TIMEOUT_MS = 400_000;
5
10
  const DEFAULT_ROUND_DURATION_MS = 10_000;
6
11
 
12
+ export async function withMemoryFailureReport(input, measure) {
13
+ try {
14
+ return await measure();
15
+ } catch (error) {
16
+ if (!error?.memoryMeasurement) throw error;
17
+ const directory = await mkdtemp(path.join(input.context.artifactsDir, 'navigation-memory-'));
18
+ const report = path.join(directory, 'measurement.json');
19
+ await writeFile(report, `${JSON.stringify({
20
+ nodeId: input.context.nodeId,
21
+ action: input.action,
22
+ ...error.memoryMeasurement,
23
+ }, null, 2)}\n`, { mode: 0o600 });
24
+ await writeFile(input.outputPath, `${JSON.stringify({ artifacts: [{
25
+ path: path.relative(input.context.artifactsDir, report),
26
+ type: 'report',
27
+ nodeId: input.context.nodeId,
28
+ label: 'Failed memory measurement',
29
+ category: 'diagnostic',
30
+ mimeType: 'application/json',
31
+ }] })}\n`, { mode: 0o600 });
32
+ delete error.memoryMeasurement;
33
+ error.message += `\nNext: jq . ${JSON.stringify(report)}`;
34
+ throw error;
35
+ }
36
+ }
37
+
7
38
  function finiteNumber(value, fallback, label) {
8
39
  const parsed = value ?? fallback;
9
40
  if (!Number.isFinite(parsed)) throw new Error(`${label} must be a finite number.`);
@@ -217,9 +248,9 @@ export async function measureNavigationSlope(page, options, bounds) {
217
248
  passed,
218
249
  };
219
250
  if (!passed.nodes || !passed.listeners) {
220
- throw new Error(
251
+ throw Object.assign(new Error(
221
252
  `Navigation memory bounds exceeded: nodes ${slopes.nodes.toFixed(2)}/${bounds.maxNodesPerCycle} per cycle; listeners ${slopes.listeners.toFixed(2)}/${bounds.maxListenersPerCycle} per cycle.`,
222
- );
253
+ ), { memoryMeasurement: result });
223
254
  }
224
255
  return result;
225
256
  }
@@ -282,9 +313,9 @@ export async function measureDetachedDom(page, options, maxDetachedGrowth) {
282
313
  passed: after - before <= maxDetachedGrowth,
283
314
  };
284
315
  if (!result.passed) {
285
- throw new Error(
316
+ throw Object.assign(new Error(
286
317
  `Detached DOM bound exceeded: growth ${result.growth}/${maxDetachedGrowth} nodes.`,
287
- );
318
+ ), { memoryMeasurement: result });
288
319
  }
289
320
  return result;
290
321
  }
@@ -333,9 +364,9 @@ export async function compareIdleNavigation(page, options, maxAttributableListen
333
364
  passed: attributableListenersPerCycle <= maxAttributableListenersPerCycle,
334
365
  };
335
366
  if (!result.passed) {
336
- throw new Error(
367
+ throw Object.assign(new Error(
337
368
  `Idle-control bound exceeded: attributable listeners ${attributableListenersPerCycle.toFixed(2)}/${maxAttributableListenersPerCycle} per cycle (navigation ${navigation.slopes.listeners.toFixed(2)}, idle ${idleListenersPerCycle.toFixed(2)}).`,
338
- );
369
+ ), { memoryMeasurement: result });
339
370
  }
340
371
  return result;
341
372
  }
@@ -1,12 +1,15 @@
1
+ // Compares post-GC listener growth across equal-duration idle and navigation rounds against a configured bound.
2
+ // Writes a diagnostic report when the measurement exceeds that bound.
1
3
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
4
  import {
3
5
  compareIdleNavigation,
4
6
  idleControlBound,
5
7
  navigationMemoryOptions,
8
+ withMemoryFailureReport,
6
9
  } from './_navigation-memory.mjs';
7
10
 
8
11
  runAdapter((input) =>
9
- withExtensionPage(input, async (page) => ({
12
+ withMemoryFailureReport(input, () => withExtensionPage(input, async (page) => ({
10
13
  action: input.action,
11
14
  status: 'pass',
12
15
  ...(await compareIdleNavigation(
@@ -14,5 +17,5 @@ runAdapter((input) =>
14
17
  navigationMemoryOptions(input.node, 'rounds', 8),
15
18
  idleControlBound(input.node),
16
19
  )),
17
- })),
20
+ }))),
18
21
  );
@@ -1,12 +1,15 @@
1
+ // Checks detached-node growth between post-GC heap snapshots before and after repeated screen navigation.
2
+ // Writes a diagnostic report when growth exceeds the configured bound.
1
3
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
4
  import {
3
5
  detachedDomBound,
4
6
  measureDetachedDom,
5
7
  navigationMemoryOptions,
8
+ withMemoryFailureReport,
6
9
  } from './_navigation-memory.mjs';
7
10
 
8
11
  runAdapter((input) =>
9
- withExtensionPage(input, async (page) => ({
12
+ withMemoryFailureReport(input, () => withExtensionPage(input, async (page) => ({
10
13
  action: input.action,
11
14
  status: 'pass',
12
15
  ...(await measureDetachedDom(
@@ -14,5 +17,5 @@ runAdapter((input) =>
14
17
  navigationMemoryOptions(input.node, 'cycles', 5),
15
18
  detachedDomBound(input.node),
16
19
  )),
17
- })),
20
+ }))),
18
21
  );
@@ -1,12 +1,15 @@
1
+ // Repeatedly navigates a screen and measures post-GC DOM-node, listener, and heap growth slopes.
2
+ // Enforces node and listener slope bounds and writes a diagnostic report when either fails.
1
3
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
4
  import {
3
5
  measureNavigationSlope,
4
6
  navigationMemoryOptions,
5
7
  navigationSlopeBounds,
8
+ withMemoryFailureReport,
6
9
  } from './_navigation-memory.mjs';
7
10
 
8
11
  runAdapter((input) =>
9
- withExtensionPage(input, async (page) => ({
12
+ withMemoryFailureReport(input, () => withExtensionPage(input, async (page) => ({
10
13
  action: input.action,
11
14
  status: 'pass',
12
15
  ...(await measureNavigationSlope(
@@ -14,5 +17,5 @@ runAdapter((input) =>
14
17
  navigationMemoryOptions(input.node),
15
18
  navigationSlopeBounds(input.node),
16
19
  )),
17
- })),
20
+ }))),
18
21
  );
@@ -1,3 +1,4 @@
1
+ // Assert that explicitly selected Perps orders are present or absent in live order state.
1
2
  import { runAdapter } from '../platform/cdp.mjs';
2
3
  import { assertOrders } from './perps.mjs';
3
4
 
@@ -1,3 +1,4 @@
1
+ // Assert that explicitly selected Perps positions are present or absent in runtime position state.
1
2
  import { runAdapter } from '../platform/cdp.mjs';
2
3
  import { assertPositions } from './perps.mjs';
3
4
 
@@ -1,3 +1,5 @@
1
+ // Sample changing Perps UI values or check recent activity containment across filters.
2
+ // Some modes navigate lists, select filters, or open watchlist market details.
1
3
  import { pathToFileURL } from 'node:url';
2
4
 
3
5
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
@@ -86,7 +88,8 @@ async function visiblePrice(page, surface, market) {
86
88
  if (!element) return false;
87
89
  const style = getComputedStyle(element);
88
90
  const rect = element.getBoundingClientRect();
89
- return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
91
+ return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0
92
+ && rect.bottom > 0 && rect.right > 0 && rect.top < innerHeight && rect.left < innerWidth;
90
93
  };
91
94
  const root = document.querySelector('[data-testid=${JSON.stringify(rootTestId)}]');
92
95
  const price = ${priceTestId
@@ -111,7 +114,8 @@ async function marketRows(page, maxItems) {
111
114
  if (testId.startsWith('market-row-ticker-') || /-(?:token-logo|asset-label)$/u.test(testId)) return false;
112
115
  const style = getComputedStyle(element);
113
116
  const rect = element.getBoundingClientRect();
114
- return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
117
+ return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0
118
+ && rect.bottom > 0 && rect.right > 0 && rect.top < innerHeight && rect.left < innerWidth;
115
119
  }).slice(0, ${JSON.stringify(maxItems)}).map((element) => ({
116
120
  testId: String(element.getAttribute('data-testid') || '').slice(0, 500),
117
121
  label: String(element.innerText || '').replace(/\\s+/gu, ' ').trim().slice(0, 500),
@@ -124,7 +128,8 @@ async function watchlistRows(page, maxItems) {
124
128
  ].filter((element) => {
125
129
  const style = getComputedStyle(element);
126
130
  const rect = element.getBoundingClientRect();
127
- return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
131
+ return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0
132
+ && rect.bottom > 0 && rect.right > 0 && rect.top < innerHeight && rect.left < innerWidth;
128
133
  }).slice(0, ${JSON.stringify(maxItems)}).map((element) => ({
129
134
  testId: String(element.getAttribute('data-testid') || '').slice(0, 500),
130
135
  label: String(element.innerText || '').replace(/\\s+/gu, ' ').trim().slice(0, 500),
@@ -138,6 +143,7 @@ function changedRows(first, last) {
138
143
  .map((entry) => entry.testId);
139
144
  }
140
145
 
146
+ // Containment checks loaded full-list rows, including rows below the viewport.
141
147
  async function activityIds(page, maxItems) {
142
148
  const selector = '[data-testid^="transaction-card-"]';
143
149
  return page.evaluate(`[
@@ -155,7 +161,8 @@ async function recentActivityState(page, maxItems) {
155
161
  if (!element) return false;
156
162
  const style = getComputedStyle(element);
157
163
  const rect = element.getBoundingClientRect();
158
- return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
164
+ return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0
165
+ && rect.bottom > 0 && rect.right > 0 && rect.top < innerHeight && rect.left < innerWidth;
159
166
  };
160
167
  return {
161
168
  ids: [...document.querySelectorAll('[data-testid^="transaction-card-"]')]
@@ -1,3 +1,4 @@
1
+ // Cancel selected Perps orders through guarded mutation helpers and verify their absence.
1
2
  import { runAdapter } from '../platform/cdp.mjs';
2
3
  import { assertOrders, closeOrders } from './perps.mjs';
3
4
 
@@ -5,5 +6,7 @@ runAdapter(async (input) => {
5
6
  const close = await closeOrders(input);
6
7
  if (close.proofPath === 'visible-product-controls') return close;
7
8
  const assertion = await assertOrders(input, false);
8
- return { ...assertion, close };
9
+ return close.proofPath === 'accepted-order-status'
10
+ ? { ...assertion, ...close }
11
+ : { ...assertion, close };
9
12
  });
@@ -1,3 +1,4 @@
1
+ // Verify selected Perps positions are already absent; reject positions that need live closure.
1
2
  import { runAdapter } from '../platform/cdp.mjs';
2
3
  import { assertPositions, closePositions } from './perps.mjs';
3
4
 
@@ -1,3 +1,6 @@
1
+ // Close visible Perps positions with funding checks and receipt-bound identity.
2
+ // Accepted fills require lineage checks; full closes retain their own venue identity.
3
+
1
4
  import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
5
  import {
3
6
  assertTrustedExternalMutationTarget,
@@ -10,7 +13,7 @@ import {
10
13
  trustedExternalMutationPlanIsExact,
11
14
  withExternalMutationRecovery,
12
15
  } from './mutation-receipt.mjs';
13
- import { requireCurrentVisibleFundingBalance, requirePerpsTradingContext } from './perps.mjs';
16
+ import { observeHyperliquidSubmission, placementReceiptSelection, requireCurrentVisibleFundingBalance, requirePerpsTradingContext } from './perps.mjs';
14
17
  import {
15
18
  absoluteDelta,
16
19
  parsePositionDetail,
@@ -58,6 +61,97 @@ async function readVisibleClosePosition(page, market) {
58
61
  })()`);
59
62
  }
60
63
 
64
+ async function readAcceptedFill(page, placement, market, side, visibleSize) {
65
+ const accepted = placement.record?.acceptance;
66
+ if (!accepted?.venueOrderId) throw new Error('Position cleanup requires the exact accepted venue order ID.');
67
+ const deadline = Date.now() + 10_000;
68
+ let startTime = null;
69
+ // Explicit end windows read current fills instead of the controller's recent-history cache.
70
+ while (Date.now() < deadline) {
71
+ const result = await page.session.call('Runtime.evaluate', {
72
+ expression: `(async () => {
73
+ const request = globalThis.stateHooks?.submitRequestToBackground;
74
+ if (typeof request !== 'function') throw new Error('Read-only Perps fill API is unavailable.');
75
+ const recent = await request('perpsGetOrderFills', [{ aggregateByTime: false, endTime: ${JSON.stringify(Date.now())} }]);
76
+ if (!Array.isArray(recent) || recent.some(fill => !fill || typeof fill !== 'object'
77
+ || Array.isArray(fill) || !['string', 'number'].includes(typeof fill.orderId))) {
78
+ throw new Error('Read-only Perps fill history is malformed.');
79
+ }
80
+ const owned = recent.filter(fill => String(fill?.orderId) === ${JSON.stringify(accepted.venueOrderId)});
81
+ if (owned.some(fill => !Number.isSafeInteger(fill.timestamp) || fill.timestamp <= 0)) {
82
+ throw new Error('Accepted order has malformed fill timestamps.');
83
+ }
84
+ return owned.length ? Math.min(...owned.map(fill => fill.timestamp)) : null;
85
+ })()`,
86
+ awaitPromise: true, returnByValue: true,
87
+ }, { timeoutMs: Math.max(1, deadline - Date.now()) });
88
+ if (result.exceptionDetails) {
89
+ throw new Error(result.exceptionDetails.exception?.description ?? result.exceptionDetails.text ?? 'Perps fill observation failed.');
90
+ }
91
+ startTime = result.result?.value;
92
+ if (startTime !== null) {
93
+ if (!Number.isSafeInteger(startTime) || startTime <= 0) throw new Error('Accepted fill observation returned an invalid timestamp.');
94
+ break;
95
+ }
96
+ await new Promise((resolve) => setTimeout(resolve, Math.min(250, Math.max(1, deadline - Date.now()))));
97
+ }
98
+ if (startTime === null) throw new Error('Accepted order did not reach fill history before the cleanup observation deadline.');
99
+ const data = await page.evaluate(`(async () => {
100
+ const request = globalThis.stateHooks?.submitRequestToBackground;
101
+ if (typeof request !== 'function') throw new Error('Read-only Perps fill API is unavailable.');
102
+ const startTime = ${JSON.stringify(startTime)};
103
+ const [fills, orders] = await Promise.all([
104
+ request('perpsGetOrderFills', [{ aggregateByTime: false, startTime, endTime: ${JSON.stringify(Date.now())} }]),
105
+ request('perpsGetOpenOrders', []),
106
+ ]);
107
+ if (!Array.isArray(fills) || !Array.isArray(orders) || fills.length >= 2000) {
108
+ throw new Error('Read-only Perps fill coverage is incomplete.');
109
+ }
110
+ const market = ${JSON.stringify(market)};
111
+ return {
112
+ openOrderCount: orders.filter(order => String(order.symbol ?? order.coin).toUpperCase() === market).length,
113
+ fills: fills.filter(fill => String(fill.symbol).toUpperCase() === market).map(fill => ({
114
+ orderId: String(fill.orderId), side: fill.side, size: fill.size,
115
+ startPosition: fill.startPosition, timestamp: fill.timestamp,
116
+ })),
117
+ };
118
+ })()`);
119
+ if (data.openOrderCount !== 0) throw new Error('Cancel the exact accepted order remainder before closing its filled position.');
120
+ const decimal = (value) => {
121
+ const match = /^(-?)(0|[1-9][0-9]{0,19})(?:\.([0-9]{1,8}))?$/u.exec(String(value));
122
+ if (!match) throw new Error('Perps fill size identity is invalid.');
123
+ return BigInt(`${match[1]}${match[2]}${String(match[3] ?? '').padEnd(8, '0')}`);
124
+ };
125
+ const owned = data.fills.filter((fill) => fill.orderId === accepted.venueOrderId);
126
+ if (!owned.length || data.fills.some((fill) => !Number.isSafeInteger(fill.timestamp) || fill.timestamp <= 0)) {
127
+ throw new Error('Accepted venue order has no independently observed complete fill identity.');
128
+ }
129
+ const firstTimestamp = Math.min(...owned.map((fill) => fill.timestamp));
130
+ if (data.fills.some((fill) => fill.timestamp >= firstTimestamp && fill.orderId !== accepted.venueOrderId)) {
131
+ throw new Error('Another order changed this market after the accepted fill; refusing shared-position cleanup.');
132
+ }
133
+ const sign = side === 'long' ? 1n : -1n;
134
+ owned.sort((left, right) => {
135
+ const difference = sign * (decimal(left.startPosition) - decimal(right.startPosition));
136
+ return difference < 0n ? -1 : difference > 0n ? 1 : 0;
137
+ });
138
+ let position = 0n;
139
+ for (const fill of owned) {
140
+ const size = decimal(fill.size);
141
+ if (fill.side !== (side === 'long' ? 'buy' : 'sell') || size <= 0n || decimal(fill.startPosition) !== position) {
142
+ throw new Error('Accepted order fills do not prove an exclusively owned position starting from zero.');
143
+ }
144
+ position += sign * size;
145
+ }
146
+ const sizeMatch = /^([0-9.,]+) ([A-Z0-9:_-]{2,20})$/u.exec(visibleSize);
147
+ if (!sizeMatch || sizeMatch[2] !== market || decimal(sizeMatch[1].replace(/,/gu, '')) !== sign * position
148
+ || sign * position > decimal(accepted.size)) {
149
+ throw new Error('Visible position size does not equal the exact accepted-order fills.');
150
+ }
151
+ return { venueOrderId: accepted.venueOrderId, fillCount: owned.length, size: visibleSize,
152
+ firstFillTimestamp: firstTimestamp, lastFillTimestamp: Math.max(...owned.map((fill) => fill.timestamp)) };
153
+ }
154
+
61
155
  async function prepareCloseModal(page, partialAmount) {
62
156
  let confirmation;
63
157
  for (let attempt = 0; attempt < 3 && !confirmation; attempt += 1) {
@@ -75,6 +169,7 @@ async function prepareCloseModal(page, partialAmount) {
75
169
  return {
76
170
  amount: String(document.querySelector('[data-testid="close-amount-value"] input')?.value || read('close-amount-value')),
77
171
  fullSizeSelected: Boolean(document.querySelector('[data-testid="close-amount-slider-pct-100"]')),
172
+ marketOrder: !modal.querySelector('[data-testid="limit-price-input"]'),
78
173
  margin: read('perps-close-summary-margin-value'),
79
174
  fees: read('perps-close-summary-fees-value'),
80
175
  receive: read('perps-close-summary-receive-value'),
@@ -129,6 +224,20 @@ async function proveGlobalCleanup(page, balanceBeforeBaseUnits, toleranceBaseUni
129
224
  };
130
225
  }
131
226
 
227
+ function requireCloseAcceptance(receipt, maximumBaseUnits) {
228
+ const accepted = receipt?.acceptance;
229
+ const size = receipt?.facts?.closeSizing?.size;
230
+ if (!accepted?.venueOrderId || accepted.status !== 'filled' || Number(accepted.filledSize) !== Number(size)
231
+ || accepted.venueOrderId === receipt?.facts?.acceptedFill?.venueOrderId) {
232
+ throw new Error('Full close has no distinct accepted full-fill identity.');
233
+ }
234
+ const notional = Math.ceil(Number(accepted.filledSize) * Number(accepted.averagePrice) * 1_000_000);
235
+ if (!Number.isSafeInteger(notional) || notional <= 0 || BigInt(notional) > BigInt(maximumBaseUnits)) {
236
+ throw new Error('Accepted close fill exceeds the trusted operation cap.');
237
+ }
238
+ return { venueOrderId: accepted.venueOrderId, filledSize: accepted.filledSize, averagePrice: accepted.averagePrice };
239
+ }
240
+
132
241
  export async function closeVisiblePosition(input) {
133
242
  return withExtensionPage(input, async (page) => {
134
243
  const market = marketSymbol(input.node?.market);
@@ -176,6 +285,7 @@ export async function closeVisiblePosition(input) {
176
285
  roundingToleranceBaseUnits,
177
286
  ...(cleanupOnly ? { positionSize: fundingPositionSize, cleanupOnly: true } : {}),
178
287
  });
288
+ const closeBudget = trustedExternalMutationBudget(input, 'perps-close-position');
179
289
  const existingReceipt = await readExternalMutationReceipt(input, identity);
180
290
  if (existingReceipt) {
181
291
  return withExternalMutationRecovery(existingReceipt, async () => {
@@ -187,7 +297,8 @@ export async function closeVisiblePosition(input) {
187
297
  dispatch: async () => {
188
298
  throw new Error('resumed close must not dispatch');
189
299
  },
190
- observe: async () => {
300
+ observe: async ({ receipt }) => {
301
+ const acceptedClose = partialAmount == null ? requireCloseAcceptance(receipt, closeBudget.maxBaseUnits) : null;
191
302
  const deadline = Date.now() + 45_000;
192
303
  let finalState;
193
304
  let absentSince = null;
@@ -218,6 +329,7 @@ export async function closeVisiblePosition(input) {
218
329
  );
219
330
  return {
220
331
  success: true,
332
+ ...acceptedClose,
221
333
  finalState,
222
334
  cleanup,
223
335
  };
@@ -238,6 +350,8 @@ export async function closeVisiblePosition(input) {
238
350
  receipt: mutation.receipt,
239
351
  resumed: true,
240
352
  recovery: mutation.recovery,
353
+ ...(partialAmount == null ? { venueOrderId: mutation.observation.venueOrderId,
354
+ filledSize: mutation.observation.filledSize, averagePrice: mutation.observation.averagePrice } : {}),
241
355
  finalState: mutation.observation.finalState,
242
356
  cleanup: mutation.observation.cleanup ?? null,
243
357
  proofPath: 'trusted-pointer-visible-ui',
@@ -275,20 +389,30 @@ export async function closeVisiblePosition(input) {
275
389
  const observedNotionalBaseUnits = sizeMatch && sizeMatch[2] === market && priceBaseUnits != null
276
390
  ? `${Math.round(Number(sizeMatch[1]) * Number(priceBaseUnits))}`
277
391
  : null;
278
- const closeBudget = trustedExternalMutationBudget(input, 'perps-close-position');
279
392
  if (!/^[1-9][0-9]{0,29}$/u.test(String(observedNotionalBaseUnits ?? ''))
280
393
  || BigInt(observedNotionalBaseUnits) > BigInt(closeBudget.maxBaseUnits)) {
281
394
  throw new Error('Visible Perps position notional is unavailable or exceeds the authorized close cap.');
282
395
  }
283
- if (!cleanupOnly) {
284
- await readPriorMutationReceipt(input, identity, 'metamask.perps.place_order');
396
+ const placement = cleanupOnly ? null
397
+ : await readPriorMutationReceipt(input, identity, 'metamask.perps.place_order', placementReceiptSelection(input));
398
+ let acceptedFill = null;
399
+ if (['limit', 'market'].includes(placement?.record?.identity?.orderType)) {
400
+ if (partialAmount != null) throw new Error('Accepted-fill cleanup requires a full owned-size close.');
401
+ acceptedFill = await readAcceptedFill(page, placement, market, fundingSide, exactBefore.size);
285
402
  }
286
- const balanceBeforeBaseUnits = fundingBalance.observedBaseUnits;
403
+ const balanceBeforeBaseUnits = String(placement?.record?.facts?.balanceBeforeBaseUnits ?? fundingBalance.observedBaseUnits);
287
404
  if (!/^(?:0|[1-9][0-9]{0,29})$/u.test(balanceBeforeBaseUnits)) {
288
405
  throw new Error('Prior order receipt has no valid visible pre-order balance.');
289
406
  }
290
407
  const confirmation = await prepareCloseModal(page, partialAmount);
291
- const mutation = await runGuardedExternalMutation({
408
+ if (partialAmount == null && (!confirmation.fullSizeSelected || !confirmation.marketOrder)) {
409
+ throw new Error('Receipt-bound full close requires the visible 100% market-close form.');
410
+ }
411
+ const closeSizing = partialAmount == null ? { size: sizeMatch[1], side: fundingSide === 'long' ? 'short' : 'long' } : null;
412
+ let submissionObserver = null;
413
+ let mutation;
414
+ try {
415
+ mutation = await runGuardedExternalMutation({
292
416
  input,
293
417
  identity,
294
418
  publicationFacts: {
@@ -298,12 +422,13 @@ export async function closeVisiblePosition(input) {
298
422
  cleanupOnly,
299
423
  fundingBalance,
300
424
  tradingContext,
425
+ ...(acceptedFill ? { acceptedFill } : {}),
426
+ ...(closeSizing ? { closeSizing } : {}),
301
427
  },
302
428
  dispatch: async () => {
303
429
  let dispatchTarget;
304
430
  try {
305
431
  await requireCurrentVisibleFundingBalance(input, page, timeoutMs, fundingBalance.observedBaseUnits);
306
- await requirePerpsTradingContext(input, page);
307
432
  assertTrustedExternalMutationTarget(input, page, identity);
308
433
  const current = await readVisibleClosePosition(page, market);
309
434
  const currentPosition = parsePositionDetail({ pair: `${market}-USD`, ...current });
@@ -313,7 +438,20 @@ export async function closeVisiblePosition(input) {
313
438
  }) || currentPosition.size !== exactBefore.size) {
314
439
  throw new Error('Visible Perps position changed before close dispatch.');
315
440
  }
316
- await prepareCloseModal(page, partialAmount);
441
+ if (acceptedFill && JSON.stringify(await readAcceptedFill(page, placement, market, fundingSide, currentPosition.size))
442
+ !== JSON.stringify(acceptedFill)) {
443
+ throw new Error('Accepted-order fill identity changed before close dispatch.');
444
+ }
445
+ const currentConfirmation = await prepareCloseModal(page, partialAmount);
446
+ if (closeSizing) {
447
+ if (!currentConfirmation.fullSizeSelected || !currentConfirmation.marketOrder) {
448
+ throw new Error('Full market-close selection changed before dispatch.');
449
+ }
450
+ submissionObserver = await observeHyperliquidSubmission(page, { ...tradingContext, market,
451
+ side: closeSizing.side, size: closeSizing.size, orderType: 'market', reduceOnly: true,
452
+ leverage, maximumNotionalBaseUnits: closeBudget.maxBaseUnits });
453
+ await submissionObserver.checkSizing();
454
+ }
317
455
  dispatchTarget = await page.evaluate(`(() => {
318
456
  const element = document.querySelector('[data-testid="perps-close-position-modal-submit"]');
319
457
  if (!element || element.disabled || element.getAttribute('aria-disabled') === 'true') {
@@ -322,10 +460,12 @@ export async function closeVisiblePosition(input) {
322
460
  const rect = element.getBoundingClientRect();
323
461
  return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
324
462
  })()`);
463
+ await requirePerpsTradingContext(input, page);
325
464
  assertTrustedExternalMutationTarget(input, page, identity);
326
465
  } catch (error) {
327
466
  throw externalMutationNotDispatched(error instanceof Error ? error.message : String(error));
328
467
  }
468
+ submissionObserver?.arm();
329
469
  await page.session.call('Input.dispatchMouseEvent', {
330
470
  type: 'mousePressed', x: dispatchTarget.x, y: dispatchTarget.y,
331
471
  button: 'left', buttons: 1, clickCount: 1,
@@ -334,8 +474,10 @@ export async function closeVisiblePosition(input) {
334
474
  type: 'mouseReleased', x: dispatchTarget.x, y: dispatchTarget.y,
335
475
  button: 'left', buttons: 0, clickCount: 1,
336
476
  });
477
+ return submissionObserver ? submissionObserver.accepted(timeoutMs) : undefined;
337
478
  },
338
- observe: async () => {
479
+ observe: async ({ receipt }) => {
480
+ const acceptedClose = closeSizing ? requireCloseAcceptance(receipt, closeBudget.maxBaseUnits) : null;
339
481
  const closeDeadline = Date.now() + 45_000;
340
482
  let finalState;
341
483
  while (Date.now() <= closeDeadline) {
@@ -364,6 +506,7 @@ export async function closeVisiblePosition(input) {
364
506
  );
365
507
  return {
366
508
  success: true,
509
+ ...acceptedClose,
367
510
  finalState,
368
511
  cleanup,
369
512
  };
@@ -373,7 +516,10 @@ export async function closeVisiblePosition(input) {
373
516
  }
374
517
  throw new Error('matching closed-position postcondition was not visible before timeout');
375
518
  },
376
- });
519
+ });
520
+ } finally {
521
+ submissionObserver?.close();
522
+ }
377
523
  return {
378
524
  action: input.action,
379
525
  market,
@@ -387,6 +533,9 @@ export async function closeVisiblePosition(input) {
387
533
  receipt: mutation.receipt,
388
534
  resumed: mutation.resumed,
389
535
  recovery: mutation.recovery,
536
+ ...(closeSizing ? { venueOrderId: mutation.observation.venueOrderId,
537
+ filledSize: mutation.observation.filledSize, averagePrice: mutation.observation.averagePrice,
538
+ closeSizing: submissionObserver?.sizing } : {}),
390
539
  proofPath: 'trusted-pointer-visible-ui',
391
540
  };
392
541
  });
@@ -1,3 +1,5 @@
1
+ // Fetch production Hyperliquid data and compare it with the opened market's displayed values.
2
+ // Also check the funding countdown and nonblank chart canvases.
1
3
  import { dataTestId, marketSymbol, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
4
 
3
5
  function numeric(value) {
@@ -1,3 +1,5 @@
1
+ // Submit live margin changes through visible controls with mutation receipts and position identity checks.
2
+ // Observe the resulting margin and balance; removal requires a prior successful add receipt.
1
3
  import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
4
  import {
3
5
  assertTrustedExternalMutationTarget,
@@ -266,7 +268,6 @@ export async function editMargin(input) {
266
268
  let dispatchTarget;
267
269
  try {
268
270
  await requireCurrentVisibleFundingBalance(input, page, timeoutMs, fundingBalance.observedBaseUnits);
269
- await requirePerpsTradingContext(input, page);
270
271
  assertTrustedExternalMutationTarget(input, page, identity);
271
272
  const current = await readPosition(page, market);
272
273
  if (current.marginBaseUnits !== before.marginBaseUnits || current.size !== before.size
@@ -284,6 +285,7 @@ export async function editMargin(input) {
284
285
  const rect = element.getBoundingClientRect();
285
286
  return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
286
287
  })()`);
288
+ await requirePerpsTradingContext(input, page);
287
289
  assertTrustedExternalMutationTarget(input, page, identity);
288
290
  } catch (error) {
289
291
  throw externalMutationNotDispatched(error instanceof Error ? error.message : String(error));
@@ -1,3 +1,4 @@
1
+ // Require selected Perps orders to match the requested state without placing or cancelling orders.
1
2
  import { runAdapter } from '../platform/cdp.mjs';
2
3
  import { ensureOrders } from './perps.mjs';
3
4
 
@@ -1,3 +1,4 @@
1
+ // Require selected Perps positions to match the requested state without opening or closing positions.
1
2
  import { runAdapter } from '../platform/cdp.mjs';
2
3
  import { ensurePositions } from './perps.mjs';
3
4