@deeeed/metamask-harness 0.48.0 → 0.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/commands/help.js +17 -5
  3. package/dist/funding-execution-context.js +19 -4
  4. package/dist/metamask-action-validation.js +3 -2
  5. package/dist/mm-harness-cli.js +2 -1
  6. package/docs/RECIPES.md +2 -2
  7. package/library/actions/core/perps/_controller.mjs +14 -24
  8. package/library/actions/core/perps/assert_orders.mjs +3 -5
  9. package/library/actions/core/perps/assert_positions.mjs +3 -5
  10. package/library/actions/core/perps/close_orders.mjs +28 -4
  11. package/library/actions/core/perps/close_positions.mjs +53 -12
  12. package/library/actions/core/perps/edit_order.mjs +2 -0
  13. package/library/actions/core/perps/ensure_orders.mjs +3 -4
  14. package/library/actions/core/perps/ensure_positions.mjs +4 -5
  15. package/library/actions/core/perps/place_order.mjs +7 -14
  16. package/library/actions/core/perps/read_account.mjs +2 -0
  17. package/library/actions/core/perps/read_orders.mjs +2 -0
  18. package/library/actions/core/perps/read_positions.mjs +3 -1
  19. package/library/actions/core/perps/read_snapshot.mjs +3 -0
  20. package/library/actions/core/perps/start_state.mjs +3 -14
  21. package/library/actions/core/perps/teardown_state.mjs +3 -14
  22. package/library/actions/core/perps/update_position_tpsl.mjs +13 -19
  23. package/library/actions/core/wallet/list_accounts.mjs +3 -0
  24. package/library/actions/extension/analytics/consent.mjs +2 -0
  25. package/library/actions/extension/analytics/set_consent.mjs +2 -0
  26. package/library/actions/extension/assets/home-token-identity.mjs +1 -0
  27. package/library/actions/extension/assets/import_custom_token.mjs +2 -0
  28. package/library/actions/extension/assets/open_details.mjs +1 -0
  29. package/library/actions/extension/assets/prepare_send.mjs +1 -0
  30. package/library/actions/extension/assets/read_details.mjs +1 -0
  31. package/library/actions/extension/assets/read_visible_state.mjs +1 -0
  32. package/library/actions/extension/assets/review_send.mjs +1 -0
  33. package/library/actions/extension/assets/select_network_scope.mjs +1 -0
  34. package/library/actions/extension/assets/send.mjs +2 -0
  35. package/library/actions/extension/assets/set_custom_gas.mjs +1 -0
  36. package/library/actions/extension/assets/set_token_visibility.mjs +2 -0
  37. package/library/actions/extension/assets/verify_sorting.mjs +1 -0
  38. package/library/actions/extension/deeplink/open.mjs +1 -0
  39. package/library/actions/extension/networks/add_chainlist.mjs +2 -0
  40. package/library/actions/extension/networks/add_custom.mjs +2 -0
  41. package/library/actions/extension/networks/custom_network.mjs +2 -0
  42. package/library/actions/extension/networks/read_visible_state.mjs +1 -0
  43. package/library/actions/extension/networks/remove_custom.mjs +1 -0
  44. package/library/actions/extension/performance/_navigation-memory.mjs +2 -0
  45. package/library/actions/extension/performance/compare_idle_navigation_memory.mjs +2 -0
  46. package/library/actions/extension/performance/measure_detached_dom.mjs +2 -0
  47. package/library/actions/extension/performance/measure_navigation_memory.mjs +2 -0
  48. package/library/actions/extension/perps/assert_orders.mjs +1 -0
  49. package/library/actions/extension/perps/assert_positions.mjs +1 -0
  50. package/library/actions/extension/perps/assert_visible_consistency.mjs +2 -0
  51. package/library/actions/extension/perps/close_orders.mjs +4 -1
  52. package/library/actions/extension/perps/close_positions.mjs +1 -0
  53. package/library/actions/extension/perps/close_visible_position.mjs +159 -10
  54. package/library/actions/extension/perps/compare_provider_market.mjs +2 -0
  55. package/library/actions/extension/perps/edit_margin.mjs +2 -0
  56. package/library/actions/extension/perps/ensure_orders.mjs +1 -0
  57. package/library/actions/extension/perps/ensure_positions.mjs +1 -0
  58. package/library/actions/extension/perps/mutation-receipt.mjs +54 -3
  59. package/library/actions/extension/perps/open_balance_action.mjs +1 -0
  60. package/library/actions/extension/perps/open_position_action.mjs +1 -0
  61. package/library/actions/extension/perps/perps.mjs +401 -25
  62. package/library/actions/extension/perps/place_order.mjs +1 -0
  63. package/library/actions/extension/perps/read_eligibility.mjs +1 -0
  64. package/library/actions/extension/perps/read_funds_confirmation.mjs +2 -0
  65. package/library/actions/extension/perps/read_orders.mjs +1 -0
  66. package/library/actions/extension/perps/read_positions.mjs +1 -0
  67. package/library/actions/extension/perps/read_snapshot.mjs +2 -0
  68. package/library/actions/extension/perps/read_visible_state.mjs +2 -0
  69. package/library/actions/extension/perps/search_markets.mjs +1 -0
  70. package/library/actions/extension/perps/select_activity_filter.mjs +1 -0
  71. package/library/actions/extension/perps/select_market_filter.mjs +1 -0
  72. package/library/actions/extension/perps/set_market_favorite.mjs +1 -0
  73. package/library/actions/extension/perps/state.mjs +1 -0
  74. package/library/actions/extension/perps/update_position_tpsl.mjs +2 -0
  75. package/library/actions/extension/perps/visible-mutation-identity.mjs +2 -0
  76. package/library/actions/extension/platform/cdp.mjs +29 -2
  77. package/library/actions/extension/settings/set_basic_functionality.mjs +1 -0
  78. package/library/actions/extension/swap_bridge/quote-state.mjs +2 -0
  79. package/library/actions/extension/swap_bridge/read_visible_state.mjs +1 -0
  80. package/library/actions/extension/swap_bridge/select_assets.mjs +1 -0
  81. package/library/actions/extension/swap_bridge/set_amount.mjs +1 -0
  82. package/library/actions/extension/swap_bridge/set_max_amount.mjs +1 -0
  83. package/library/actions/extension/swap_bridge/set_slippage.mjs +1 -0
  84. package/library/actions/extension/ui/locators.mjs +1 -0
  85. package/library/actions/extension/ui/navigate.mjs +2 -0
  86. package/library/actions/extension/wallet/ensure_unlocked.mjs +2 -0
  87. package/library/actions/extension/wallet/import.mjs +2 -0
  88. package/library/actions/extension/wallet/list_accounts.mjs +1 -0
  89. package/library/actions/extension/wallet/lock.mjs +2 -0
  90. package/library/actions/extension/wallet/read_state.mjs +1 -0
  91. package/library/actions/extension/wallet/reset.mjs +2 -0
  92. package/library/actions/extension/wallet/secret-input.mjs +2 -0
  93. package/library/actions/extension/wallet/select_account.mjs +2 -0
  94. package/library/actions/extension/wallet/setup.mjs +1 -0
  95. package/library/actions/extension/wallet/state.mjs +2 -0
  96. package/library/actions/extension/wallet/visible-session.mjs +2 -0
  97. package/library/actions/mobile/analytics/consent-settings.mjs +1 -0
  98. package/library/actions/mobile/analytics/set_consent.mjs +1 -0
  99. package/library/actions/mobile/app/network-control.mjs +2 -0
  100. package/library/actions/mobile/app/network.mjs +1 -0
  101. package/library/actions/mobile/app/network_assert.mjs +1 -0
  102. package/library/actions/mobile/app/network_capture.mjs +1 -0
  103. package/library/actions/mobile/assets/import_custom_token.mjs +2 -0
  104. package/library/actions/mobile/assets/open_details.mjs +1 -0
  105. package/library/actions/mobile/assets/read_details.mjs +1 -0
  106. package/library/actions/mobile/assets/read_visible_state.mjs +1 -0
  107. package/library/actions/mobile/assets/set_token_visibility.mjs +2 -0
  108. package/library/actions/mobile/assets/verify_sorting.mjs +90 -7
  109. package/library/actions/mobile/deeplink/open.mjs +1 -0
  110. package/library/actions/mobile/networks/add_custom.mjs +1 -0
  111. package/library/actions/mobile/networks/network-management.mjs +1 -0
  112. package/library/actions/mobile/networks/read_visible_state.mjs +1 -0
  113. package/library/actions/mobile/networks/remove_custom.mjs +1 -0
  114. package/library/actions/mobile/perps/assert_orders.mjs +1 -0
  115. package/library/actions/mobile/perps/assert_positions.mjs +1 -0
  116. package/library/actions/mobile/perps/capture_performance.mjs +1 -0
  117. package/library/actions/mobile/perps/close_orders.mjs +1 -0
  118. package/library/actions/mobile/perps/close_positions.mjs +1 -0
  119. package/library/actions/mobile/perps/ensure_orders.mjs +1 -0
  120. package/library/actions/mobile/perps/ensure_positions.mjs +1 -0
  121. package/library/actions/mobile/perps/measure_homepage_visible.mjs +1 -0
  122. package/library/actions/mobile/perps/performance-capture.mjs +2 -0
  123. package/library/actions/mobile/perps/perps.mjs +6 -2
  124. package/library/actions/mobile/perps/place_order.mjs +1 -0
  125. package/library/actions/mobile/perps/prepare_local_snapshot_endpoint.mjs +2 -0
  126. package/library/actions/mobile/perps/read-visible-state-loop.mjs +2 -0
  127. package/library/actions/mobile/perps/read_orders.mjs +1 -0
  128. package/library/actions/mobile/perps/read_positions.mjs +1 -0
  129. package/library/actions/mobile/perps/read_visible_state.mjs +1 -0
  130. package/library/actions/mobile/perps/search_markets.mjs +1 -0
  131. package/library/actions/mobile/perps/set_market_favorite.mjs +2 -0
  132. package/library/actions/mobile/platform/bridge.mjs +16 -4
  133. package/library/actions/mobile/platform/native-session-name.mjs +1 -0
  134. package/library/actions/mobile/platform/native-session.mjs +2 -0
  135. package/library/actions/mobile/platform/observe-ui.mjs +2 -0
  136. package/library/actions/mobile/platform/tool-paths.mjs +1 -0
  137. package/library/actions/mobile/swap_bridge/native-session.mjs +1 -0
  138. package/library/actions/mobile/swap_bridge/read_visible_state.mjs +1 -0
  139. package/library/actions/mobile/swap_bridge/select_assets.mjs +2 -0
  140. package/library/actions/mobile/swap_bridge/set_amount.mjs +1 -0
  141. package/library/actions/mobile/swap_bridge/set_max_amount.mjs +1 -0
  142. package/library/actions/mobile/swap_bridge/set_slippage.mjs +1 -0
  143. package/library/actions/mobile/swap_bridge/submit_transaction.mjs +2 -0
  144. package/library/actions/mobile/ui/locators.mjs +1 -0
  145. package/library/actions/mobile/ui/native-navigation.mjs +2 -0
  146. package/library/actions/mobile/ui/navigate.mjs +2 -0
  147. package/library/actions/mobile/wallet/ensure_unlocked.mjs +2 -0
  148. package/library/actions/mobile/wallet/home.mjs +1 -0
  149. package/library/actions/mobile/wallet/import.mjs +2 -0
  150. package/library/actions/mobile/wallet/list_accounts.mjs +1 -0
  151. package/library/actions/mobile/wallet/lock.mjs +1 -0
  152. package/library/actions/mobile/wallet/native-ui.mjs +2 -0
  153. package/library/actions/mobile/wallet/read_state.mjs +2 -0
  154. package/library/actions/mobile/wallet/reset-helper.mjs +1 -0
  155. package/library/actions/mobile/wallet/reset.mjs +1 -0
  156. package/library/actions/mobile/wallet/select_account.mjs +2 -0
  157. package/library/actions/mobile/wallet/setup.mjs +2 -0
  158. package/library/actions/shared/analytics/assert_events.mjs +3 -0
  159. package/library/actions/shared/analytics/consent.mjs +3 -0
  160. package/library/actions/shared/analytics/read_events.mjs +2 -0
  161. package/library/actions/shared/analytics/start_capture.mjs +3 -0
  162. package/library/actions/shared/app/network-artifact.mjs +2 -0
  163. package/library/actions/shared/app/network-assert.mjs +2 -0
  164. package/library/actions/shared/assets/visible-state.mjs +2 -0
  165. package/library/actions/shared/deeplink/url.mjs +3 -0
  166. package/library/actions/shared/networks/visible-state.mjs +2 -0
  167. package/library/actions/shared/perps/visible-state.mjs +2 -0
  168. package/library/actions/shared/swap-bridge/transaction.mjs +3 -0
  169. package/library/actions/shared/swap-bridge/visible-state.mjs +2 -0
  170. package/library/actions/shared/ui/locators.mjs +2 -0
  171. package/library/actions/shared/wallet/import-source.mjs +3 -0
  172. package/library/manifests/core.action-manifest.json +20 -0
  173. package/library/manifests/extension.action-manifest.json +2 -2
  174. package/library/manifests/mobile.action-manifest.json +3 -3
  175. package/library/recipes/mobile/app/{lifecycle.android-smoke.recipe.json → lifecycle-smoke.recipe.json} +12 -11
  176. package/package.json +1 -1
  177. package/site/assets/help-recipes.json +188 -35
@@ -1,21 +1,10 @@
1
+ // Restore selected testnet baselines, canceling orders before closing positions.
2
+ // Omitted domains default to empty; explicit false skips a domain.
3
+
1
4
  import { isDirectRun, resolveNetwork, runAdapter } from './_controller.mjs';
2
5
  import { ensureOrders } from './ensure_orders.mjs';
3
6
  import { ensurePositions } from './ensure_positions.mjs';
4
7
 
5
- // core Headless composite that restores the POSITION and ORDER baselines for the
6
- // selected market(s) on HyperLiquid testnet after the proof window. Mirrors the
7
- // extension perps-domain-dispatcher teardown_state semantics but SKIPS every
8
- // navigation/page/HUD step — headless core has no UI surface.
9
- //
10
- // Params (extension schema, testnet-default): profile, market(s)/symbol(s),
11
- // positions{state,...}, orders{state,...}, network. The UI-only `page`/`hud`
12
- // fields are accepted by the schema but intentionally ignored here.
13
- //
14
- // Teardown defaults BOTH domains to state=none (leave the shared dev account
15
- // FLAT) when a baseline block is omitted; pass a block explicitly to override, or
16
- // `false` to skip a domain. Orders are canceled FIRST, then positions are closed,
17
- // so resting orders never re-arm against a position mid-teardown.
18
-
19
8
  const BASELINE_KEYS = [
20
9
  'market',
21
10
  'symbol',
@@ -1,3 +1,6 @@
1
+ // Manage full or partial TP/SL for a selected testnet position and verify the resulting triggers.
2
+ // Fractional sizes use live venue quantity precision.
3
+
1
4
  import {
2
5
  configuredSymbols,
3
6
  controllerRejection,
@@ -10,14 +13,6 @@ import {
10
13
  selectedItems,
11
14
  } from './_controller.mjs';
12
15
 
13
- // core Set or replace the TP/SL attached to an existing Perps POSITION by driving
14
- // the headless PerpsController.updatePositionTPSL() through the full
15
- // signing/provider path. This is the position-bound half of the advanced-order
16
- // contract (TAT-3511): it covers both whole-position TP/SL and PARTIAL
17
- // (quantity-scoped) TP/SL, which the provider must express as standalone
18
- // reduce-only trigger orders because a position-bound TP/SL always closes the
19
- // whole position.
20
- //
21
16
  // Params use the controller's own provider-agnostic names: take_profit_price /
22
17
  // take_profit_size / stop_loss_price / stop_loss_size (camelCase aliases
23
18
  // accepted). Omitting a price removes that side; omitting a size covers the
@@ -129,9 +124,6 @@ export async function updatePositionTpsl(input) {
129
124
  // Prices and sizes are resolved from live mid and the actual position, so a
130
125
  // recipe never has to name a number that only holds for one market at one
131
126
  // moment. An absolute value still wins when the caller gives one.
132
- // Both derivations below need the market, and each used to fetch it
133
- // separately. getMarketDataWithPrices reads every market, so doing it twice in
134
- // one adapter is what pushed this past the node timeout.
135
127
  let market;
136
128
  const readMarket = async () => {
137
129
  if (market === undefined) {
@@ -181,11 +173,16 @@ export async function updatePositionTpsl(input) {
181
173
 
182
174
  if (takeProfitFraction !== undefined || stopLossFraction !== undefined) {
183
175
  const absolutePositionSize = Math.abs(Number(position.size ?? position.szi ?? 0));
184
- // Round to the market's own size precision before submitting. The venue
185
- // rounds anyway, and the read-back below matches on size — so an unrounded
186
- // fraction asks for a size that can never come back, and the poll spins
187
- // until it times out.
188
- const szDecimals = Number((await readMarket())?.szDecimals);
176
+ // MarketInfo carries quantity precision; formatted market prices do not.
177
+ const markets = await controller.getMarkets({ symbols: [symbol] });
178
+ const szDecimals = markets.find(
179
+ (item) => item.name.toUpperCase() === symbol.toUpperCase(),
180
+ )?.szDecimals;
181
+ if (!Number.isInteger(szDecimals) || szDecimals < 0) {
182
+ throw new Error(
183
+ `core updatePositionTPSL found no valid size precision for ${symbol}.\nNext: run mm-harness verify --adapter core`,
184
+ );
185
+ }
189
186
  const fromFraction = (raw, name) => {
190
187
  const fraction = Number(raw);
191
188
  if (!Number.isFinite(fraction) || fraction <= 0 || fraction > 1) {
@@ -194,9 +191,6 @@ export async function updatePositionTpsl(input) {
194
191
  );
195
192
  }
196
193
  const exact = absolutePositionSize * fraction;
197
- if (!Number.isFinite(szDecimals)) {
198
- return String(exact);
199
- }
200
194
  const rounded = Number(exact.toFixed(szDecimals));
201
195
  if (rounded <= 0) {
202
196
  throw new Error(
@@ -1,3 +1,6 @@
1
+ // Derive public account identities from the checkout's wallet fixture and apply scope selection.
2
+ // Credential values are not included in the returned account list.
3
+
1
4
  import { readFile, writeFile } from 'node:fs/promises';
2
5
  import { pathToFileURL } from 'node:url';
3
6
  import { mnemonicToAccount, privateKeyToAccount } from 'viem/accounts';
@@ -1,3 +1,5 @@
1
+ // Builds controller requests to set and verify analytics consent.
2
+ // The fallback reloads the page with a temporary Chrome bridge and removes it afterward.
1
3
  export function consentControllerExpression({
2
4
  bridgeSymbol,
3
5
  participate,
@@ -1,3 +1,5 @@
1
+ // Sets MetaMetrics and marketing consent through background controllers and checks settled state.
2
+ // Reloads with a temporary Chrome bridge when the debug request bridge is unavailable.
1
3
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
4
  import { consentParams } from '../../shared/analytics/consent.mjs';
3
5
  import {
@@ -1,3 +1,4 @@
1
+ // Opens a Home token to verify its ERC-20 identity from the details route, then returns Home.
1
2
  import { openHome } from '../ui/navigate.mjs';
2
3
 
3
4
  export async function verifyHomeIdentity(page, token, assetId, timeoutMs) {
@@ -1,3 +1,5 @@
1
+ // Imports an ERC-20 through token management and checks its Home row.
2
+ // Reusing a visible token requires a matching details-route identity and an unchanged account.
1
3
  import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
4
  import { readWalletStateExpression } from '../wallet/state.mjs';
3
5
  import { verifyHomeIdentity } from './home-token-identity.mjs';
@@ -1,3 +1,4 @@
1
+ // Opens asset details from a uniquely visible token symbol and waits for the asset route.
1
2
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
3
 
3
4
  runAdapter(async (input) => withExtensionPage(input, async (page) => {
@@ -1,3 +1,4 @@
1
+ // Fills a token send with a recipient and explicit or Max amount, stopping at enabled Continue.
1
2
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
3
  import { selectors, tokenRow, visible, waitFor } from './send.mjs';
3
4
 
@@ -1,3 +1,4 @@
1
+ // Reads asset details from the DOM, rejecting malformed values and waiting for requested chart data.
1
2
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
3
 
3
4
  function clean(value) {
@@ -1,3 +1,4 @@
1
+ // Captures token rows within the viewport, including displayed balances and price-change tones.
1
2
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
3
  import {
3
4
  normalizeVisibleAssetsState,
@@ -1,3 +1,4 @@
1
+ // Advances a prepared send through alerts to confirmation and checks that a network fee is shown.
1
2
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
3
  import { bodyState, selectors, visible, waitFor } from './send.mjs';
3
4
 
@@ -1,3 +1,4 @@
1
+ // Opens Home and selects the all-networks token filter, then checks the label and closed menu.
1
2
  import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
3
  import { openHome } from '../ui/navigate.mjs';
3
4
  import { visible, waitFor } from './send.mjs';
@@ -1,3 +1,5 @@
1
+ // Shares send controls, UI polling, and transaction-state readers.
2
+ // Token and activity lookup helpers mark matching DOM rows for later clicks.
1
3
  import { dataTestId } from '../platform/cdp.mjs';
2
4
 
3
5
  export async function visible(page, selector) {
@@ -1,3 +1,4 @@
1
+ // Saves a custom gas limit in send confirmation, supplying a positive priority fee if needed.
1
2
  import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
3
  import { bodyState, visible, waitFor } from './send.mjs';
3
4
 
@@ -1,3 +1,5 @@
1
+ // Sets a token-management toggle and checks Home visibility, with optional exact ERC-20 identity.
2
+ // Can require a change or an already-matching preference.
1
3
  import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
4
  import { verifyHomeIdentity } from './home-token-identity.mjs';
3
5
 
@@ -1,3 +1,4 @@
1
+ // Checks Home token order under alphabetical and declining-balance sorts, then restores the preference.
1
2
  import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
3
 
3
4
  const TOGGLE = dataTestId('sort-by-popover-toggle');
@@ -1,3 +1,4 @@
1
+ // Navigates the Extension page to an external deeplink URL and returns the navigation result.
1
2
  import { pathToFileURL } from 'node:url';
2
3
 
3
4
  import {
@@ -1,3 +1,5 @@
1
+ // Adds a missing network through ChainList and selects it in the Home network filter.
2
+ // Can reject existing networks so later cleanup does not delete prior configuration.
1
3
  import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
4
  import {
3
5
  assertNetworkAdditionAllowed,
@@ -1,3 +1,5 @@
1
+ // Adds a missing custom RPC network through the UI and selects it on Home.
2
+ // Can reject existing networks so later cleanup does not delete prior configuration.
1
3
  import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
4
  import {
3
5
  assertNetworkAdditionAllowed,
@@ -1,3 +1,5 @@
1
+ // Shares network-manager navigation, chain/name matching, and Home network selection.
2
+ // Button lookup marks DOM targets; network additions can reject existing configuration.
1
3
  import { dataTestId } from '../platform/cdp.mjs';
2
4
 
3
5
  async function visible(page, selector) {
@@ -1,3 +1,4 @@
1
+ // Samples the displayed network, token rows, and errors, with an optional settling interval.
1
2
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
3
  import {
3
4
  normalizeVisibleNetworkState,
@@ -1,3 +1,4 @@
1
+ // Switches Home to Ethereum, deletes the named custom network, and checks its absence in the manager.
1
2
  import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
3
  import {
3
4
  findNetworkItem,
@@ -1,3 +1,5 @@
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.
1
3
  import { mkdtemp, writeFile } from 'node:fs/promises';
2
4
  import path from 'node:path';
3
5
 
@@ -1,3 +1,5 @@
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,
@@ -1,3 +1,5 @@
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,
@@ -1,3 +1,5 @@
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,
@@ -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';
@@ -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 { placementReceiptSelection, 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', placementReceiptSelection(input));
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,6 +422,8 @@ 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;
@@ -312,7 +438,20 @@ export async function closeVisiblePosition(input) {
312
438
  }) || currentPosition.size !== exactBefore.size) {
313
439
  throw new Error('Visible Perps position changed before close dispatch.');
314
440
  }
315
- 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
+ }
316
455
  dispatchTarget = await page.evaluate(`(() => {
317
456
  const element = document.querySelector('[data-testid="perps-close-position-modal-submit"]');
318
457
  if (!element || element.disabled || element.getAttribute('aria-disabled') === 'true') {
@@ -326,6 +465,7 @@ export async function closeVisiblePosition(input) {
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,
@@ -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