@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,12 +1,11 @@
1
+ // Converge selected testnet orders to open or absent state, then assert the result.
2
+ // Reuse an existing matching order instead of placing another.
3
+
1
4
  import { getCoreController, isDirectRun, requireExplicitSelection, runAdapter, selectedItems } from './_controller.mjs';
2
5
  import { assertOrders } from './assert_orders.mjs';
3
6
  import { closeOrders } from './close_orders.mjs';
4
7
  import { placeOrder } from './place_order.mjs';
5
8
 
6
- // core Higher-level wrapper that reads selected open orders, converges them to the
7
- // requested `state` (open/none) by canceling or placing, then asserts the final
8
- // order state. Mirrors ensure_positions.mjs.
9
-
10
9
  export async function ensureOrders(input) {
11
10
  if (input.node?.state == null) throw new Error('metamask.perps.ensure_orders requires state=open or state=none.');
12
11
  requireExplicitSelection(input);
@@ -1,12 +1,11 @@
1
+ // Converge selected testnet positions to open or absent state, then assert the result.
2
+ // Open a position only when the selected state requires one.
3
+
1
4
  import { getCoreController, isDirectRun, requireExplicitSelection, runAdapter, selectedItems } from './_controller.mjs';
2
5
  import { assertPositions } from './assert_positions.mjs';
3
6
  import { closePositions } from './close_positions.mjs';
4
7
  import { placeOrder } from './place_order.mjs';
5
8
 
6
- // core Higher-level wrapper that reads selected positions, converges them to the
7
- // requested `state` (open/none) by placing or closing, then asserts the final
8
- // state. Mirrors the extension ensure_positions semantics.
9
-
10
9
  export async function ensurePositions(input) {
11
10
  if (input.node?.state == null) throw new Error('metamask.perps.ensure_positions requires state=open or state=none.');
12
11
  requireExplicitSelection(input);
@@ -26,7 +25,7 @@ export async function ensurePositions(input) {
26
25
  userAddress: accountAddress,
27
26
  });
28
27
  let order = null;
29
- if (selectedItems(input, positions).length === 0) {
28
+ if (selectedItems(input, positions, 'position').length === 0) {
30
29
  order = await placeOrder(input);
31
30
  }
32
31
  const assertion = await assertPositions(input, true);
@@ -1,3 +1,6 @@
1
+ // Submit and verify testnet market, limit or trigger orders through the headless controller.
2
+ // Supports declared rejection cases and attached protection parameters.
3
+
1
4
  import {
2
5
  configuredSymbols,
3
6
  controllerRejection,
@@ -12,24 +15,14 @@ import {
12
15
  selectedItems,
13
16
  } from './_controller.mjs';
14
17
 
15
- // core Place a real Perps order on HyperLiquid testnet by driving the headless
16
- // PerpsController.placeOrder() through the full signing/provider path (Slice 2).
17
- // Mirrors the extension adapter's input vocabulary and notional→size math:
18
- // size = (usdAmount * leverage) / currentPrice
19
- // The HyperLiquidProvider recalculates size from usdAmount internally; we still
20
- // pass a derived size for the controller's own validation, plus currentPrice/
21
- // priceAtCalculation so the provider's slippage guard has a fresh anchor.
22
- //
23
18
  // LIMIT orders (orderType=limit) place a RESTING order on the book instead of
24
19
  // filling: the provider sends tif=Gtc and uses the provided `price` verbatim
25
20
  // (calculateOrderPriceAndSize / buildOrdersArray in core's orderCalculations.ts).
26
21
  // The limit price is derived from the live mid by `offset_pct` (e.g. -30 places a
27
- // BUY 30% below mid that will NOT fill), or supplied absolutely via `limit_price`.
22
+ // BUY 30% below mid), or supplied absolutely via `limit_price`.
28
23
  // For a limit order we size from the LIMIT price (not mid) and omit usdAmount so
29
- // the controller uses our explicit size verbatim that guarantees the
30
- // on-exchange notional (size * limitPrice) clears HyperLiquid's ~$10 minimum even
31
- // though the limit sits far from mid. After placing we verify the resting open
32
- // order exists (not a filled position).
24
+ // the controller receives an explicit size; venue quantity normalization still
25
+ // applies. After placing we verify the resting open order exists, not a filled position.
33
26
  //
34
27
  // TRIGGER placements (order_type=stop_market | stop_limit | take_profit_market |
35
28
  // take_profit_limit) rest off-book until `trigger_price` is reached, then execute
@@ -567,7 +560,7 @@ export async function placeOrder(input) {
567
560
  userAddress: accountAddress,
568
561
  });
569
562
  }
570
- const matchingPositions = selectedItems(input, positions);
563
+ const matchingPositions = selectedItems(input, positions, 'position');
571
564
 
572
565
  if (restsOnBook && matchingOrders.length === 0) {
573
566
  throw new Error(
@@ -1,3 +1,5 @@
1
+ // Read the selected account's Perps balance and margin data through standalone controller APIs.
2
+
1
3
  import {
2
4
  getCoreController,
3
5
  isDirectRun,
@@ -1,3 +1,5 @@
1
+ // Read standalone Perps orders and return redacted selected data with total/matching counts.
2
+
1
3
  import {
2
4
  getCoreController,
3
5
  isDirectRun,
@@ -1,3 +1,5 @@
1
+ // Read standalone Perps positions and return redacted selected data with total/matching counts.
2
+
1
3
  import {
2
4
  getCoreController,
3
5
  isDirectRun,
@@ -12,7 +14,7 @@ export async function readPositions(input) {
12
14
  standalone: true,
13
15
  userAddress: accountAddress,
14
16
  });
15
- const matching = selectedItems(input, positions);
17
+ const matching = selectedItems(input, positions, 'position');
16
18
  return {
17
19
  action: input.action,
18
20
  source: 'perps-controller-standalone',
@@ -1,3 +1,6 @@
1
+ // Summarize public Perps market data, account state or both for a headless proof.
2
+ // Retain data-source and account identity metadata alongside redacted results.
3
+
1
4
  import {
2
5
  getCoreController,
3
6
  isDirectRun,
@@ -1,21 +1,10 @@
1
+ // Prepare selected testnet baselines before proof, handling positions before orders.
2
+ // Omitted domains are skipped; this headless action performs no UI navigation.
3
+
1
4
  import { isDirectRun, resolveNetwork, runAdapter } from './_controller.mjs';
2
5
  import { ensureOrders } from './ensure_orders.mjs';
3
6
  import { ensurePositions } from './ensure_positions.mjs';
4
7
 
5
- // core Headless composite that converges the POSITION and ORDER baselines for the
6
- // selected market(s) on HyperLiquid testnet before the proof window. Mirrors the
7
- // extension perps-domain-dispatcher start_state semantics but SKIPS every
8
- // navigation/page/HUD step — headless core has no UI surface.
9
- //
10
- // Params (extension schema, testnet-default): profile, market(s)/symbol(s),
11
- // positions{state,...}, orders{state,...}, network. The UI-only `page`/`hud`
12
- // fields are accepted by the schema but intentionally ignored here.
13
- //
14
- // A baseline block converges to its `state` by composing the ensure_* helpers; a
15
- // block set to `false` (or omitted) skips that domain. Position convergence runs
16
- // first (closing a position also clears its TP/SL orders), then order
17
- // convergence, so the final order baseline is authoritative.
18
-
19
8
  const BASELINE_KEYS = [
20
9
  'market',
21
10
  'symbol',
@@ -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 {
@@ -0,0 +1,31 @@
1
+ // Opens a Home token to verify its ERC-20 identity from the details route, then returns Home.
2
+ import { openHome } from '../ui/navigate.mjs';
3
+
4
+ export async function verifyHomeIdentity(page, token, assetId, timeoutMs) {
5
+ try {
6
+ await page.clickPoint(token.point.x, token.point.y);
7
+ const deadline = Date.now() + timeoutMs;
8
+ let route;
9
+ while (Date.now() < deadline) {
10
+ const value = String(await page.evaluate('location.hash'));
11
+ if (value.startsWith('#/asset/')) {
12
+ route = value;
13
+ break;
14
+ }
15
+ await page.evaluate('new Promise((resolve) => setTimeout(resolve, 150))');
16
+ }
17
+ if (!route) throw new Error('The selected Home token did not expose an asset-details route.');
18
+ const [, , chainId, address] = route.split('/');
19
+ if (!/^0x[0-9a-f]+$/iu.test(chainId ?? '') || !/^0x[0-9a-f]{40}$/iu.test(address ?? '')) {
20
+ throw new Error('The selected Home route does not expose an ERC-20 identity.');
21
+ }
22
+ const observed = `eip155:${BigInt(chainId).toString()}/erc20:${address.toLowerCase()}`;
23
+ if (observed !== assetId) {
24
+ throw new Error(`Home asset identity ${observed} does not match ${assetId}.\nNext: select a network filter with an unambiguous matching token before continuing.`);
25
+ }
26
+ return { assetId, route, verified: true };
27
+ } finally {
28
+ await openHome(page, timeoutMs);
29
+ delete token.point;
30
+ }
31
+ }
@@ -1,10 +1,15 @@
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';
4
+ import { readWalletStateExpression } from '../wallet/state.mjs';
5
+ import { verifyHomeIdentity } from './home-token-identity.mjs';
2
6
 
3
7
  const OPTIONS = dataTestId('asset-list-control-bar-action-button');
4
8
  const MANAGE = dataTestId('manageTokens__button');
5
9
  const ADD = dataTestId('token-management-add-custom-token-button');
6
10
  const PAGE = dataTestId('custom-token-import-page');
7
11
  const NETWORK = dataTestId('network-selector');
12
+ const NETWORK_PICKER = dataTestId('custom-token-import-network-selector');
8
13
  const ADDRESS = dataTestId('custom-token-import-address-input');
9
14
  const SYMBOL = dataTestId('custom-token-import-symbol-input');
10
15
  const DECIMALS = dataTestId('custom-token-import-decimal-input');
@@ -33,96 +38,69 @@ async function waitFor(page, predicate, message, timeoutMs) {
33
38
  throw new Error(message);
34
39
  }
35
40
 
36
- async function visibleButton(page, label) {
37
- const marker = `mm-harness-${Date.now()}-${Math.random().toString(16).slice(2)}`;
38
- const found = await page.evaluate(`(() => {
39
- const label = ${JSON.stringify(label)};
40
- const marker = ${JSON.stringify(marker)};
41
- for (const element of document.querySelectorAll('button')) {
42
- const text = String(element.innerText || element.textContent || '').replace(/\\s+/gu, ' ').trim();
43
- const style = getComputedStyle(element);
44
- const rect = element.getBoundingClientRect();
45
- if (text === label && style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0) {
46
- element.setAttribute('data-mm-harness-target', marker);
47
- return true;
48
- }
49
- }
50
- return false;
51
- })()`);
52
- if (!found) throw new Error(`Visible button ${JSON.stringify(label)} was not found.`);
53
- return `[data-mm-harness-target="${marker}"]`;
54
- }
55
-
56
- async function networkOption(page, network, chainId) {
41
+ async function networkOption(page, chainId) {
57
42
  return page.evaluate(`(() => {
58
- const network = ${JSON.stringify(network.toLowerCase())};
43
+ const picker = document.querySelector(${JSON.stringify(NETWORK_PICKER)});
44
+ if (!picker) return null;
59
45
  const chainIds = new Set(${JSON.stringify([
60
46
  String(chainId),
61
47
  `0x${chainId.toString(16)}`,
62
48
  `eip155:${chainId}`,
63
49
  ].map((value) => value.toLowerCase()))});
64
- for (const element of document.querySelectorAll('[data-testid^="network-list-item-"]')) {
50
+ for (const element of picker.querySelectorAll('[data-testid^="network-list-item-"]')) {
65
51
  const testId = String(element.getAttribute('data-testid') || '');
66
52
  const suffix = testId.slice('network-list-item-'.length).toLowerCase();
53
+ if (!chainIds.has(suffix)) continue;
54
+ element.scrollIntoView({ block: 'nearest', inline: 'nearest' });
55
+ const rect = element.getBoundingClientRect();
56
+ const hit = document.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2);
57
+ if (!element.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true })
58
+ || rect.width <= 0 || rect.height <= 0 || !hit || !element.contains(hit)) continue;
67
59
  const label = String(element.innerText || '').replace(/\\s+/gu, ' ').trim().slice(0, 500);
68
- if (chainIds.has(suffix) || label.toLowerCase().includes(network)) return { testId, label };
60
+ return { testId, label };
69
61
  }
70
62
  return null;
71
63
  })()`);
72
64
  }
73
65
 
74
- async function homeToken(page, symbol) {
66
+ async function homeToken(page, symbol, verifyIdentity = false) {
75
67
  return page.evaluate(`(() => {
76
68
  const symbol = ${JSON.stringify(symbol.toLowerCase())};
69
+ const verifyIdentity = ${JSON.stringify(verifyIdentity)};
70
+ const matches = [];
77
71
  for (const row of document.querySelectorAll('[data-testid="multichain-token-list-button"]')) {
78
72
  const name = String(row.querySelector('[data-testid="multichain-token-list-item-token-name"]')?.innerText || '').replace(/\\s+/gu, ' ').trim();
79
73
  const amount = String(row.querySelector('[data-testid="multichain-token-list-item-value"]')?.innerText || '').replace(/\\s+/gu, ' ').trim();
80
74
  const observed = amount.match(/\\b([A-Za-z][A-Za-z0-9]{1,11})$/u)?.[1] || '';
81
- if (observed.toLowerCase() === symbol) return { name, symbol: observed.toUpperCase(), amount };
75
+ const style = getComputedStyle(row);
76
+ const rect = row.getBoundingClientRect();
77
+ if (observed.toLowerCase() !== symbol || style.display === 'none' || style.visibility === 'hidden'
78
+ || rect.width <= 0 || rect.height <= 0) continue;
79
+ matches.push({ row, name, symbol: observed.toUpperCase(), amount });
82
80
  }
83
- return null;
81
+ if (matches.length > 1) throw new Error('Visible token symbol matches multiple rows; identity is ambiguous.\\nNext: narrow the product network filter before importing.');
82
+ if (!matches.length) return null;
83
+ const { row, ...token } = matches[0];
84
+ if (verifyIdentity) {
85
+ row.scrollIntoView({ block: 'center', inline: 'nearest' });
86
+ const rect = row.getBoundingClientRect();
87
+ const x = rect.left + rect.width / 2;
88
+ const y = rect.top + rect.height / 2;
89
+ const hit = document.elementFromPoint(x, y);
90
+ if (!hit || !row.contains(hit)) throw new Error('The matching Home token is covered; identity cannot be verified.\\nNext: uncover the token row before importing.');
91
+ token.point = { x, y };
92
+ }
93
+ return token;
84
94
  })()`);
85
95
  }
86
96
 
87
- async function importedTokenIdentity(page, chainId, address, expectedSymbol) {
88
- return page.evaluate(`(async () => {
89
- const hooks = globalThis.stateHooks || {};
90
- const root = (await hooks.getCleanAppState?.()) || hooks.store?.getState?.() || {};
91
- const state = root.metamask || root;
92
- const allTokens = state.allTokens;
93
- const selectedAddress = String(state.selectedAddress || '').toLowerCase();
94
- if (!allTokens || typeof allTokens !== 'object' || !selectedAddress) {
95
- return { available: false, token: null };
96
- }
97
- const expectedChainId = ${JSON.stringify(chainId)};
98
- const chainEntry = Object.entries(allTokens).find(([key]) => {
99
- const normalized = String(key).toLowerCase();
100
- const value = normalized.startsWith('eip155:')
101
- ? Number(normalized.slice('eip155:'.length))
102
- : normalized.startsWith('0x')
103
- ? Number.parseInt(normalized.slice(2), 16)
104
- : Number(normalized);
105
- return value === expectedChainId;
106
- });
107
- const accountEntry = chainEntry && Object.entries(chainEntry[1] || {})
108
- .find(([account]) => String(account).toLowerCase() === selectedAddress);
109
- const tokens = Array.isArray(accountEntry?.[1]) ? accountEntry[1] : [];
110
- const expectedAddress = ${JSON.stringify(address.toLowerCase())};
111
- const expectedSymbol = ${JSON.stringify(expectedSymbol)};
112
- const token = tokens.find((entry) => {
113
- const tokenAddress = String(entry?.address || entry?.contractAddress || '').toLowerCase();
114
- return tokenAddress === expectedAddress && String(entry?.symbol || '').toUpperCase() === expectedSymbol;
115
- });
116
- return {
117
- available: true,
118
- token: token ? {
119
- address: String(token.address || token.contractAddress),
120
- symbol: String(token.symbol),
121
- decimals: token.decimals == null ? null : String(token.decimals),
122
- chainId: chainEntry?.[0] || null,
123
- } : null,
124
- };
125
- })()`, { awaitPromise: true });
97
+ async function selectedEvmAccount(page) {
98
+ const { selectedAccount } = await page.evaluate(readWalletStateExpression(), { awaitPromise: true });
99
+ if (!selectedAccount?.id || !['eip155:eoa', 'eip155:erc4337'].includes(selectedAccount.type)
100
+ || !/^0x[0-9a-f]{40}$/iu.test(selectedAccount.address ?? '')) {
101
+ throw new Error('The selected EVM account is unavailable; existing token identity cannot be verified.\nNext: select the intended EVM account before importing.');
102
+ }
103
+ return { id: selectedAccount.id, address: selectedAccount.address.toLowerCase(), type: selectedAccount.type };
126
104
  }
127
105
 
128
106
  runAdapter(async (input) => withExtensionPage(input, async (page) => {
@@ -136,39 +114,44 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
136
114
  throw new Error('metamask.assets.import_custom_token requires network, positive chain_id, ERC-20 address, and expected_symbol.');
137
115
  }
138
116
 
139
- const existingToken = await homeToken(page, expectedSymbol);
140
- const existingIdentity = existingToken
141
- ? await importedTokenIdentity(page, chainId, address, expectedSymbol)
142
- : null;
143
- if (existingToken && existingIdentity?.token) {
117
+ const account = await selectedEvmAccount(page);
118
+ const existingToken = await homeToken(page, expectedSymbol, true);
119
+ if (existingToken) {
120
+ const existingIdentity = await verifyHomeIdentity(
121
+ page, existingToken, `eip155:${chainId}/erc20:${address.toLowerCase()}`, timeoutMs,
122
+ );
123
+ const afterAccount = await selectedEvmAccount(page);
124
+ if (JSON.stringify(afterAccount) !== JSON.stringify(account)) {
125
+ throw new Error('The selected EVM account changed while checking the existing token.\nNext: keep the intended account selected for the full import check.');
126
+ }
144
127
  return {
145
128
  action: input.action,
146
129
  network,
147
130
  chainId,
148
131
  address,
149
132
  alreadyImported: true,
133
+ changed: false,
150
134
  token: existingToken,
151
- identity: existingIdentity.token,
135
+ identity: { ...existingIdentity, account, address, symbol: expectedSymbol, decimals: null, chainId: `0x${chainId.toString(16)}` },
152
136
  proofPath: 'trusted-pointer-visible-ui',
153
137
  };
154
138
  }
155
139
 
156
140
  if (!(await visible(page, MANAGE))) await page.click(OPTIONS);
157
141
  await waitFor(page, () => visible(page, MANAGE), 'Manage tokens did not become visible.', timeoutMs);
158
- await page.click(await visibleButton(page, 'Manage tokens'));
142
+ await page.click(MANAGE);
159
143
  await waitFor(page, () => visible(page, ADD), 'Add a custom token did not become visible.', timeoutMs);
160
- await page.click(await visibleButton(page, 'Add a custom token'));
144
+ await page.click(ADD);
161
145
  await waitFor(page, () => visible(page, PAGE), 'Custom token import did not open.', timeoutMs);
162
146
  await page.click(NETWORK);
163
147
  const selectedNetwork = await waitFor(
164
148
  page,
165
- () => networkOption(page, network, chainId),
149
+ () => networkOption(page, chainId),
166
150
  `${network} (${chainId}) was not available in the custom token network selector.`,
167
151
  timeoutMs,
168
152
  );
169
- await page.click(dataTestId(selectedNetwork.testId));
170
- await waitFor(page, () => visible(page, ADDRESS), 'Custom token address input did not become visible.', timeoutMs);
171
- await page.evaluate('new Promise((resolve) => setTimeout(resolve, 2000))');
153
+ await page.click(`${NETWORK_PICKER} ${dataTestId(selectedNetwork.testId)}`);
154
+ await waitFor(page, async () => !(await visible(page, NETWORK_PICKER)) && await visible(page, ADDRESS), 'Custom token network selection did not return to the address form.', timeoutMs);
172
155
  await page.setInput(ADDRESS, address);
173
156
 
174
157
  let metadataSource = 'network';
@@ -183,7 +166,7 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
183
166
  return state.symbol && state.enabled ? state : null;
184
167
  }, 'Custom token metadata did not resolve to an enabled import.', Math.min(timeoutMs, 15_000));
185
168
  } catch (error) {
186
- if (expectedDecimals === null) throw error;
169
+ if (expectedDecimals === null || !(await visible(page, SYMBOL)) || !(await visible(page, DECIMALS))) throw error;
187
170
  metadataSource = 'manual';
188
171
  await page.setInput(SYMBOL, expectedSymbol);
189
172
  await page.setInput(DECIMALS, expectedDecimals);
@@ -216,6 +199,7 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
216
199
 
217
200
  return {
218
201
  action: input.action,
202
+ changed: true,
219
203
  network: selectedNetwork.label || network,
220
204
  chainId,
221
205
  address,
@@ -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) => {
@@ -6,19 +7,30 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
6
7
  if (!/^[A-Z0-9._-]{2,20}$/u.test(symbol)) {
7
8
  throw new Error('metamask.assets.open_details requires a valid token symbol.');
8
9
  }
9
- const selector = await page.evaluate(`(() => {
10
+ const match = await page.evaluate(`(() => {
10
11
  const symbol = ${JSON.stringify(symbol)};
11
- for (const [index, row] of [...document.querySelectorAll('[data-testid="multichain-token-list-button"]')].entries()) {
12
+ const selectors = [];
13
+ for (const row of document.querySelectorAll('[data-testid="multichain-token-list-button"]')) {
14
+ const style = getComputedStyle(row);
15
+ const rect = row.getBoundingClientRect();
16
+ if (style.display === 'none' || style.visibility === 'hidden' || rect.width <= 0 || rect.height <= 0
17
+ || rect.bottom <= 0 || rect.right <= 0 || rect.top >= innerHeight || rect.left >= innerWidth) continue;
12
18
  const amount = String(row.querySelector('[data-testid="multichain-token-list-item-value"]')?.innerText || '').trim();
13
19
  const observed = amount.match(/\\b([A-Za-z][A-Za-z0-9._-]{1,19})$/u)?.[1]?.toUpperCase();
14
20
  if (observed !== symbol) continue;
15
- row.setAttribute('data-mm-harness-asset-index', String(index));
16
- return '[data-mm-harness-asset-index="' + index + '"]';
21
+ const parts = [];
22
+ for (let element = row; element; element = element.parentElement) {
23
+ const index = element.parentElement
24
+ ? [...element.parentElement.children].indexOf(element) + 1 : 1;
25
+ parts.unshift(element.localName + ':nth-child(' + index + ')');
26
+ }
27
+ selectors.push(parts.join(' > '));
17
28
  }
18
- return null;
29
+ return { count: selectors.length, selector: selectors.length === 1 ? selectors[0] : null };
19
30
  })()`);
20
- if (!selector) throw new Error(`Visible asset ${symbol} was not found.`);
21
- await page.click(selector);
31
+ if (!match.count) throw new Error(`Visible asset ${symbol} was not found.`);
32
+ if (match.count !== 1) throw new Error(`Visible asset ${symbol} matches ${match.count} rows; selection is ambiguous.\nNext: use a uniquely visible token symbol or narrow the product network filter before retrying.`);
33
+ await page.click(match.selector);
22
34
  const deadline = Date.now() + timeoutMs;
23
35
  while (Date.now() <= deadline) {
24
36
  if (String(await page.evaluate('location.hash')).startsWith('#/asset/')) {
@@ -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