@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,3 +1,6 @@
1
+ // Provide checkout-bound Extension CDP sessions, page actions and screenshot helpers.
2
+ // Bind browser and service-worker observation to the selected runtime.
3
+
1
4
  import {
2
5
  constants,
3
6
  access,
@@ -240,32 +243,31 @@ async function captureHelperBrowserPid(context, port) {
240
243
  );
241
244
  }
242
245
 
243
- async function captureCdpViewportSnapshot(page, context, relPath, metadata, captureHelperError = null) {
246
+ async function captureCdpSnapshot(page, context, relPath, metadata, captureHelperError = null) {
244
247
  const { relative, absolute, artifactsRoot } = resolveRelativeArtifactPath(context.artifactsDir, relPath);
245
248
  const destination = await preparePrivateEvidenceDestination(absolute, artifactsRoot);
246
249
  let result;
247
250
  try {
248
- const timeoutMs = Number(metadata?.cdpTimeoutMs ?? 5000);
249
- result = await Promise.race([
250
- page.session.call('Page.captureScreenshot', {
251
- format: 'png',
252
- fromSurface: true,
253
- captureBeyondViewport: false,
254
- }),
255
- new Promise((_, reject) => setTimeout(() => reject(new Error(`Chrome Page.captureScreenshot timed out after ${timeoutMs}ms.`)), timeoutMs)),
256
- ]);
257
- if (typeof result?.data !== 'string' || result.data.length === 0) {
251
+ const timeoutMs = Number(metadata?.cdpTimeoutMs ?? metadata?.timeoutMs ?? 5000);
252
+ result = await CdpWebPage.prototype.screenshot.call(page, undefined, undefined, {
253
+ ...metadata,
254
+ timeoutMs,
255
+ });
256
+ if (typeof result !== 'string' || result.length === 0) {
258
257
  throw new Error('Chrome Page.captureScreenshot returned no image data.');
259
258
  }
260
259
  } catch (error) {
261
260
  await cleanupEvidenceStaging(destination.stagingDir);
262
261
  const cdpError = error instanceof Error ? error.message : String(error);
263
- return captureDomRasterSnapshot(page, context, relPath, metadata, captureHelperError, cdpError);
262
+ throw new Error(
263
+ `Extension screenshot capture failed: ${captureHelperError ? 'native=' + captureHelperError + '; ' : ''}CDP=${cdpError}.\n` +
264
+ 'Next: mm-harness doctor --adapter extension --json',
265
+ );
264
266
  }
265
267
  try {
266
268
  await writeFile(
267
269
  destination.stagedPath,
268
- Buffer.from(result.data, 'base64'),
270
+ Buffer.from(result, 'base64'),
269
271
  { flag: 'wx', mode: 0o600 },
270
272
  );
271
273
  } catch (error) {
@@ -283,89 +285,15 @@ async function captureCdpViewportSnapshot(page, context, relPath, metadata, capt
283
285
  metadata: {
284
286
  provider: 'cdp',
285
287
  mode: 'Page.captureScreenshot',
288
+ fullPage: metadata?.fullPage === true,
286
289
  ...(captureHelperError ? { fallbackFrom: 'capture-helper', captureHelperError } : {}),
287
290
  },
288
291
  };
289
292
  }
290
293
 
291
- async function captureDomRasterSnapshot(page, context, relPath, metadata, captureHelperError, cdpError) {
292
- const { relative, absolute, artifactsRoot } = resolveRelativeArtifactPath(context.artifactsDir, relPath);
293
- const destination = await preparePrivateEvidenceDestination(absolute, artifactsRoot);
294
- const dataUrlPromise = page.evaluate(`(async () => {
295
- const width = Math.max(1, window.innerWidth);
296
- const height = Math.max(1, window.innerHeight);
297
- const source = document.documentElement;
298
- const clone = source.cloneNode(true);
299
- const sourceNodes = [source, ...source.querySelectorAll('*')];
300
- const cloneNodes = [clone, ...clone.querySelectorAll('*')];
301
- for (let index = 0; index < sourceNodes.length; index += 1) {
302
- const sourceNode = sourceNodes[index];
303
- const cloneNode = cloneNodes[index];
304
- if (!(sourceNode instanceof Element) || !(cloneNode instanceof Element)) continue;
305
- const computed = getComputedStyle(sourceNode);
306
- cloneNode.setAttribute('style', Array.from(computed).map((name) => name + ':' + computed.getPropertyValue(name) + ';').join(''));
307
- if ('value' in sourceNode && typeof sourceNode.value === 'string') cloneNode.setAttribute('value', sourceNode.value);
308
- if (sourceNode instanceof HTMLCanvasElement) {
309
- const image = document.createElement('img');
310
- try { image.src = sourceNode.toDataURL('image/png'); } catch { image.alt = 'canvas'; }
311
- image.setAttribute('style', cloneNode.getAttribute('style') || '');
312
- cloneNode.replaceWith(image);
313
- }
314
- }
315
- clone.querySelectorAll('script').forEach((node) => node.remove());
316
- clone.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml');
317
- clone.style.width = width + 'px';
318
- clone.style.height = height + 'px';
319
- clone.style.overflow = 'hidden';
320
- const serialized = new XMLSerializer().serializeToString(clone);
321
- const svg = '<svg xmlns="http://www.w3.org/2000/svg" width="' + width + '" height="' + height + '"><foreignObject width="100%" height="100%">' + serialized + '</foreignObject></svg>';
322
- const image = new Image();
323
- const loaded = new Promise((resolve, reject) => { image.onload = resolve; image.onerror = () => reject(new Error('DOM raster image load failed.')); });
324
- image.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svg)));
325
- await loaded;
326
- const canvas = document.createElement('canvas');
327
- canvas.width = width;
328
- canvas.height = height;
329
- const context = canvas.getContext('2d');
330
- context.drawImage(image, 0, 0, width, height);
331
- return canvas.toDataURL('image/png');
332
- })()`);
333
- let dataUrl;
334
- try {
335
- dataUrl = await dataUrlPromise;
336
- if (typeof dataUrl !== 'string' || !dataUrl.startsWith('data:image/png;base64,')) {
337
- throw new Error(`Extension screenshot fallbacks failed: capture-helper=${captureHelperError ?? 'not attempted'}; cdp=${cdpError}; DOM raster returned no PNG.`);
338
- }
339
- await writeFile(
340
- destination.stagedPath,
341
- Buffer.from(dataUrl.slice('data:image/png;base64,'.length), 'base64'),
342
- { flag: 'wx', mode: 0o600 },
343
- );
344
- } catch (error) {
345
- await cleanupEvidenceStaging(destination.stagingDir);
346
- throw error;
347
- }
348
- await publishPrivateEvidence(destination);
349
- return {
350
- path: relative,
351
- type: 'screenshot',
352
- nodeId: context.nodeId,
353
- label: metadata?.label ?? `${context.nodeId} screenshot`,
354
- category: metadata?.category ?? 'evidence',
355
- mimeType: 'image/png',
356
- metadata: {
357
- provider: 'dom-raster',
358
- mode: 'computed-style-viewport',
359
- fallbackFrom: captureHelperError ? 'capture-helper+cdp' : 'cdp',
360
- ...(captureHelperError ? { captureHelperError } : {}),
361
- cdpError,
362
- },
363
- };
364
- }
365
-
366
294
  export async function captureHelperSnapshot(page, context, relPath, metadata) {
367
- if (process.platform !== 'darwin') {
368
- return captureCdpViewportSnapshot(page, context, relPath, metadata);
295
+ if (metadata?.fullPage === true || process.platform !== 'darwin') {
296
+ return captureCdpSnapshot(page, context, relPath, metadata);
369
297
  }
370
298
  const { relative, absolute, artifactsRoot } = resolveRelativeArtifactPath(context.artifactsDir, relPath);
371
299
  const destination = await preparePrivateEvidenceDestination(absolute, artifactsRoot);
@@ -388,7 +316,7 @@ export async function captureHelperSnapshot(page, context, relPath, metadata) {
388
316
  parsePositivePid(process.env.METAMASK_RECIPE_EXTENSION_ACTIVE_RECORDING_PID) === pid
389
317
  ) {
390
318
  await cleanupEvidenceStaging(destination.stagingDir);
391
- return captureCdpViewportSnapshot(
319
+ return captureCdpSnapshot(
392
320
  page,
393
321
  context,
394
322
  relPath,
@@ -414,7 +342,7 @@ export async function captureHelperSnapshot(page, context, relPath, metadata) {
414
342
  } catch (error) {
415
343
  await cleanupEvidenceStaging(destination.stagingDir);
416
344
  const message = error instanceof Error ? error.message : String(error);
417
- return captureCdpViewportSnapshot(page, context, relPath, metadata, message);
345
+ return captureCdpSnapshot(page, context, relPath, metadata, message);
418
346
  }
419
347
  await publishPrivateEvidence(destination);
420
348
  return {
@@ -811,45 +739,22 @@ export class ExtensionPage extends CdpWebPage {
811
739
  });
812
740
  }
813
741
 
814
- async click(selector) {
815
- const target = await this.evaluate(`(() => {
816
- const selector = ${JSON.stringify(selector)};
817
- const element = document.querySelector(selector);
818
- if (!element) throw new Error('Clickable element not found: ' + selector);
819
- element.scrollIntoView({ block: 'center', inline: 'center' });
820
- const rect = element.getBoundingClientRect();
821
- const style = getComputedStyle(element);
822
- if (style.display === 'none' || style.visibility === 'hidden' || rect.width <= 0 || rect.height <= 0) {
823
- throw new Error('Clickable element is not visible: ' + selector);
824
- }
825
- return {
826
- x: rect.left + (rect.width / 2),
827
- y: rect.top + (rect.height / 2),
828
- tagName: element.tagName,
829
- };
830
- })()`);
831
- await this.session.call('Input.dispatchMouseEvent', {
832
- type: 'mouseMoved',
833
- x: target.x,
834
- y: target.y,
835
- });
836
- await this.session.call('Input.dispatchMouseEvent', {
837
- type: 'mousePressed',
838
- x: target.x,
839
- y: target.y,
840
- button: 'left',
841
- buttons: 1,
842
- clickCount: 1,
843
- });
844
- await this.session.call('Input.dispatchMouseEvent', {
845
- type: 'mouseReleased',
846
- x: target.x,
847
- y: target.y,
848
- button: 'left',
849
- buttons: 0,
850
- clickCount: 1,
851
- });
852
- return { clicked: true, selector, tagName: target.tagName };
742
+ async connectServiceWorker() {
743
+ const browserVersion = await jsonGet(`http://127.0.0.1:${this.port}/json/version`);
744
+ const current = extensionRuntimeIdentity({ port: this.port, target: this.target, browserVersion });
745
+ if (current.runtimeSessionId !== this.runtimeIdentity.runtimeSessionId) {
746
+ throw new Error('Extension browser changed before service-worker observation.');
747
+ }
748
+ const targets = await jsonGet(`http://127.0.0.1:${this.port}/json/list`);
749
+ const workers = targets.filter((target) => target.type === 'service_worker'
750
+ && String(target.url).startsWith(`${this.origin}/`));
751
+ if (workers.length !== 1) throw new Error('Extension requires one matching service worker for order observation.');
752
+ const worker = workers[0];
753
+ const socket = new URL(worker.webSocketDebuggerUrl);
754
+ if (socket.protocol !== 'ws:' || socket.hostname !== '127.0.0.1' || Number(socket.port) !== this.port) {
755
+ throw new Error('Extension service-worker observation requires this browser localhost endpoint.');
756
+ }
757
+ return { session: await CdpSession.connect(socket.href), targetId: worker.id };
853
758
  }
854
759
 
855
760
  async setInput(selector, value) {
@@ -863,8 +768,14 @@ export class ExtensionPage extends CdpWebPage {
863
768
  if (typeof element.select === 'function') element.select();
864
769
  return { tagName: element.tagName, previousLength: String(element.value ?? '').length };
865
770
  })()`);
866
- await this.session.call('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Backspace' });
867
- await this.session.call('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Backspace' });
771
+ await this.session.call('Input.dispatchKeyEvent', {
772
+ type: 'keyDown', key: 'Backspace', code: 'Backspace',
773
+ windowsVirtualKeyCode: 8, nativeVirtualKeyCode: 8,
774
+ });
775
+ await this.session.call('Input.dispatchKeyEvent', {
776
+ type: 'keyUp', key: 'Backspace', code: 'Backspace',
777
+ windowsVirtualKeyCode: 8, nativeVirtualKeyCode: 8,
778
+ });
868
779
  for (const character of input) {
869
780
  const upper = character.toUpperCase();
870
781
  const isLetter = /^[A-Z]$/u.test(upper);
@@ -947,22 +858,19 @@ export class ExtensionPage extends CdpWebPage {
947
858
  }
948
859
 
949
860
  async screenshot(contextOrInput, relPath, metadata = {}) {
950
- if (contextOrInput?.context) {
951
- const options = {
861
+ const context = contextOrInput?.context ?? contextOrInput;
862
+ const options = contextOrInput?.context
863
+ ? {
952
864
  label: contextOrInput.node?.description || `${contextOrInput.action} screenshot`,
953
865
  category: 'evidence',
954
866
  timeoutMs: contextOrInput.node?.timeout_ms,
955
867
  ...metadata,
956
- };
957
- if (contextOrInput.node?.screenshot_mode === 'dom_raster' || process.env.METAMASK_RECIPE_EXTENSION_SCREENSHOT_MODE === 'dom-raster') {
958
- throw new Error('Extension ui.screenshot no longer supports DOM raster screenshots. Use capture-helper snapshot evidence.');
959
868
  }
960
- return captureHelperSnapshot(this, contextOrInput.context, relPath, options);
961
- }
962
- if (process.env.METAMASK_RECIPE_EXTENSION_SCREENSHOT_MODE === 'dom-raster') {
869
+ : metadata;
870
+ if (contextOrInput?.node?.screenshot_mode === 'dom_raster' || process.env.METAMASK_RECIPE_EXTENSION_SCREENSHOT_MODE === 'dom-raster') {
963
871
  throw new Error('Extension screenshots no longer support DOM raster mode. Use capture-helper snapshot evidence.');
964
872
  }
965
- return captureHelperSnapshot(this, contextOrInput, relPath, metadata);
873
+ return captureHelperSnapshot(this, context, relPath, options);
966
874
  }
967
875
  }
968
876
 
@@ -1,3 +1,4 @@
1
+ // Sets Basic Functionality in Privacy settings, accepting the disable confirmation when needed.
1
2
  import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
3
 
3
4
  const TOGGLE_SELECTOR = dataTestId('basic-functionality-toggle');
@@ -0,0 +1,56 @@
1
+ // Compares exposed bridge quote state with the selected assets and displayed amounts.
2
+ // Requires debug store access to distinguish matching, stale, loading, and unavailable quotes.
3
+ export async function readQuoteInputIdentity(page) {
4
+ return page.evaluate(`(() => {
5
+ const state = globalThis.stateHooks?.store?.getState?.();
6
+ if (!state?.bridge || !state?.metamask) {
7
+ throw new Error('Swap quote identity requires exposed bridge quote state on this build; UI amounts alone cannot bind a quote to its request.');
8
+ }
9
+ const value = (id) => {
10
+ const root = document.querySelector('[data-testid="' + id + '"]');
11
+ return String((root?.matches('input,textarea') ? root : root?.querySelector('input,textarea'))?.value ?? '').trim();
12
+ };
13
+ const amount = value('from-amount');
14
+ const destinationAmount = value('to-amount');
15
+ const from = state.bridge.fromToken, to = state.bridge.toToken;
16
+ const decimals = from?.decimals;
17
+ if (!from?.assetId || !to?.assetId || !Number.isInteger(decimals) || decimals < 0 || decimals > 36 || !/^\\d+(?:\\.\\d*)?$/u.test(amount)) {
18
+ return { status: 'unavailable', reason: 'Exact token-unit input and asset identities are required.' };
19
+ }
20
+ const [integer, fraction = ''] = amount.split('.');
21
+ if (fraction.length > decimals) return { status: 'unavailable', reason: 'Input exceeds token precision.' };
22
+ const sourceBaseUnits = BigInt(integer + fraction.padEnd(decimals, '0')).toString();
23
+ const request = state.metamask.quoteRequest?.[0];
24
+ const quotes = state.metamask.quotes ?? [];
25
+ const sameAsset = (left, right) => left === right || (
26
+ /^eip155:/u.test(String(left)) && String(left).toLowerCase() === String(right).toLowerCase()
27
+ );
28
+ const matches = quotes.length > 0 && quotes.every(({ quote }) =>
29
+ quote?.requestId && quote.src?.amount === sourceBaseUnits &&
30
+ sameAsset(quote.src?.asset?.assetId, from.assetId) &&
31
+ sameAsset(quote.dest?.asset?.assetId, to.assetId)
32
+ );
33
+ const destinationMatches = /^\\d+(?:\\.\\d*)?$/u.test(destinationAmount) && quotes.some(({ quote }) => {
34
+ const precision = quote?.dest?.asset?.decimals;
35
+ if (!Number.isInteger(precision) || precision < 0 || precision > 36 || !/^\\d+$/u.test(String(quote?.dest?.amount))) return false;
36
+ const [whole, part = ''] = destinationAmount.split('.');
37
+ if (part.length > precision) return false;
38
+ const displayed = BigInt(whole + part.padEnd(precision, '0'));
39
+ const quoted = BigInt(quote.dest.amount);
40
+ const difference = displayed > quoted ? displayed - quoted : quoted - displayed;
41
+ const displayUnit = 10n ** BigInt(precision - part.length);
42
+ return displayed > 0n && difference * 2n <= displayUnit;
43
+ });
44
+ return {
45
+ status: state.metamask.quotesLoadingStatus === 0 ? 'loading'
46
+ : request?.srcTokenAmount === sourceBaseUnits && matches && destinationMatches ? 'matching' : 'stale',
47
+ sourceAmount: amount,
48
+ destinationAmount,
49
+ destinationMatches,
50
+ sourceBaseUnits,
51
+ sourceAssetId: from.assetId,
52
+ destinationAssetId: to.assetId,
53
+ quoteCount: quotes.length,
54
+ };
55
+ })()`);
56
+ }
@@ -1,4 +1,6 @@
1
+ // Reads displayed swap/bridge inputs and quote details, optionally checking quote identity in the store.
1
2
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
3
+ import { readQuoteInputIdentity } from './quote-state.mjs';
2
4
  import {
3
5
  normalizeVisibleSwapBridgeState,
4
6
  resolveVisibleSwapBridgeOptions,
@@ -54,7 +56,8 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
54
56
  })()`);
55
57
  const state = normalizeVisibleSwapBridgeState(raw, options, 'extension');
56
58
  const requireQuote = input.node?.require_quote === true;
57
- if (requireQuote && (!raw.quote.ctaEnabled || !(Number(state.destinationAmount) > 0)
59
+ const quoteIdentity = requireQuote ? await readQuoteInputIdentity(page) : undefined;
60
+ if (requireQuote && (quoteIdentity.status !== 'matching' || quoteIdentity.sourceAmount !== state.sourceAmount || quoteIdentity.destinationAmount !== state.destinationAmount || !raw.quote.ctaEnabled || !(Number(state.destinationAmount) > 0)
58
61
  || !/^(?:Swap|Bridge)$/iu.test(String(raw.quote.cta)))) {
59
62
  throw new Error('Swap quote is not ready for review.');
60
63
  }
@@ -62,6 +65,7 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
62
65
  action: input.action,
63
66
  ...state,
64
67
  quote: raw.quote,
68
+ ...(quoteIdentity ? { quoteIdentity } : {}),
65
69
  proofPath: 'visible-dom-accessibility',
66
70
  };
67
71
  }));
@@ -1,3 +1,4 @@
1
+ // Opens Swap and selects source and destination assets by exact ID or unambiguous chain and symbol.
1
2
  import { runAdapter, withExtensionPage, dataTestId } from '../platform/cdp.mjs';
2
3
  import { openSwap } from '../ui/navigate.mjs';
3
4
  import { resolveAssetSelectionOptions } from '../../shared/swap-bridge/transaction.mjs';
@@ -57,7 +58,17 @@ async function selectAsset(page, role, asset, deadline) {
57
58
  if (!(await isVisible(page, search))) {
58
59
  throw new Error(`Swap ${role} asset picker did not open.`);
59
60
  }
60
- await page.setInput(search, asset.query);
61
+ const existing = await page.evaluate(`(() => {
62
+ const prefix = ${JSON.stringify(`bridge-asset--${asset.chain}/`)};
63
+ const exactId = ${JSON.stringify(asset.assetId ? `bridge-asset--${asset.assetId}` : null)};
64
+ return [...document.querySelectorAll('[data-testid^="bridge-asset--"]')]
65
+ .filter((row) => exactId
66
+ ? row.getAttribute('data-testid') === exactId
67
+ : String(row.getAttribute('data-testid') || '').startsWith(prefix)
68
+ && String(row.querySelector('[data-testid="bridge-asset-symbol"]')?.textContent || '').trim().toUpperCase() === ${JSON.stringify(asset.symbol)})
69
+ .map((row) => row.getAttribute('data-testid'));
70
+ })()`);
71
+ if (existing.length === 0) await page.setInput(search, asset.query);
61
72
  const testId = await findAsset(page, asset, deadline);
62
73
  await page.click(dataTestId(testId));
63
74
  while (Date.now() < deadline) {
@@ -1,5 +1,7 @@
1
+ // Enters a swap/bridge amount and optionally waits for a matching usable quote or an unavailable route.
1
2
  import { runAdapter, withExtensionPage, dataTestId } from '../platform/cdp.mjs';
2
3
  import { resolveAmountOptions } from '../../shared/swap-bridge/transaction.mjs';
4
+ import { readQuoteInputIdentity } from './quote-state.mjs';
3
5
 
4
6
  runAdapter(async (input) => withExtensionPage(input, async (page) => {
5
7
  const options = resolveAmountOptions(input.node);
@@ -7,7 +9,6 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
7
9
  await page.setInput(source, options.amount);
8
10
  const deadline = Date.now() + options.timeoutMs;
9
11
  let observed;
10
- let lastRefreshAt = 0;
11
12
  do {
12
13
  observed = await page.evaluate(`(() => {
13
14
  const value = (testId) => {
@@ -24,22 +25,24 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
24
25
  noQuote: document.body.innerText.includes("This trade route isn't available right now"),
25
26
  };
26
27
  })()`);
28
+ if (options.waitForQuote) {
29
+ observed.quoteIdentity = await readQuoteInputIdentity(page);
30
+ if (observed.quoteIdentity.sourceAmount !== observed.sourceAmount || observed.quoteIdentity.destinationAmount !== observed.destinationAmount) observed.quoteIdentity.status = 'stale';
31
+ }
27
32
  if (!options.waitForQuote || observed.noQuote) break;
28
- if (/^(?:Get new quote|Refresh quote)$/iu.test(String(observed.cta))
29
- && observed.ctaEnabled && Date.now() - lastRefreshAt >= 1000) {
30
- await page.click(dataTestId('bridge-cta-button'));
31
- lastRefreshAt = Date.now();
33
+ if (observed.quoteIdentity.status === 'matching' && !observed.ctaEnabled && /^Insufficient funds$/iu.test(String(observed.cta))) {
34
+ throw new Error(`Swap quote matches the input but cannot be submitted: ${JSON.stringify(observed)}.`);
32
35
  }
33
- if (Number(observed.destinationAmount) > 0 && observed.ctaEnabled
36
+ if (observed.quoteIdentity?.status === 'matching' && Number(observed.destinationAmount) > 0 && observed.ctaEnabled
34
37
  && /^(?:Swap|Bridge)$/iu.test(String(observed.cta))) break;
35
38
  await page.evaluate('new Promise((resolve) => setTimeout(resolve, 250))');
36
39
  } while (Date.now() < deadline);
37
40
  if (observed.sourceAmount !== options.amount) {
38
41
  throw new Error(`Swap source amount mismatch: expected ${options.amount}, observed ${observed.sourceAmount}.`);
39
42
  }
40
- if (options.waitForQuote && !observed.noQuote && !(Number(observed.destinationAmount) > 0
43
+ if (options.waitForQuote && !observed.noQuote && !(observed.quoteIdentity?.status === 'matching' && Number(observed.destinationAmount) > 0
41
44
  && observed.ctaEnabled && /^(?:Swap|Bridge)$/iu.test(String(observed.cta)))) {
42
- throw new Error(`Swap quote did not resolve within ${options.timeoutMs}ms.`);
45
+ throw new Error(`Swap quote did not resolve within ${options.timeoutMs}ms: ${JSON.stringify(observed)}.\nNext: mm-harness call metamask.swap_bridge.read_visible_state`);
43
46
  }
44
47
  return { action: input.action, ...observed, proofPath: 'visible-ui-amount' };
45
48
  }));
@@ -1,5 +1,8 @@
1
+ // Uses the visible Max control and optionally waits for a matching usable quote or an unavailable route.
1
2
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
3
  import { resolveAmountOptions } from '../../shared/swap-bridge/transaction.mjs';
4
+ import { requireVisibleObservation } from '../../shared/ui/locators.mjs';
5
+ import { readQuoteInputIdentity } from './quote-state.mjs';
3
6
 
4
7
  async function pause(page) {
5
8
  await page.evaluate('new Promise((resolve) => setTimeout(resolve, 250))');
@@ -11,21 +14,11 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
11
14
  wait_for_quote: input.node?.wait_for_quote,
12
15
  timeout_ms: input.node?.timeout_ms,
13
16
  });
14
- const marker = `mm-harness-${Date.now()}-${Math.random().toString(16).slice(2)}`;
15
- const selector = await page.evaluate(`(() => {
16
- const marker = ${JSON.stringify(marker)};
17
- for (const element of document.querySelectorAll('button, [role="button"]')) {
18
- const label = String(element.innerText || element.textContent || '').replace(/\\s+/gu, ' ').trim();
19
- const style = getComputedStyle(element);
20
- const rect = element.getBoundingClientRect();
21
- if (label !== 'Max' || style.display === 'none' || style.visibility === 'hidden' || rect.width <= 0 || rect.height <= 0) continue;
22
- element.setAttribute('data-mm-harness-target', marker);
23
- return '[data-mm-harness-target="' + marker + '"]';
24
- }
25
- return null;
26
- })()`);
27
- if (!selector) throw new Error('Swap Max control was not visible.');
28
- await page.click(selector);
17
+ const visible = requireVisibleObservation(await page.observe(['ui.visible']));
18
+ const controls = visible.items.filter((item) =>
19
+ item.label === 'Max' && item.selector && item.enabled !== false);
20
+ if (controls.length !== 1) throw new Error('Swap requires exactly one visible, enabled Max control.');
21
+ await page.click(controls[0].selector);
29
22
  const deadline = Date.now() + options.timeoutMs;
30
23
  let observed = null;
31
24
  while (Date.now() < deadline) {
@@ -45,14 +38,21 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
45
38
  noQuote: document.body.innerText.includes("This trade route isn't available right now"),
46
39
  };
47
40
  })()`);
41
+ if (options.waitForQuote) {
42
+ observed.quoteIdentity = await readQuoteInputIdentity(page);
43
+ if (observed.quoteIdentity.sourceAmount !== observed.sourceAmount || observed.quoteIdentity.destinationAmount !== observed.destinationAmount) observed.quoteIdentity.status = 'stale';
44
+ }
45
+ if (observed.quoteIdentity?.status === 'matching' && !observed.loading && !observed.ctaEnabled && /^Insufficient funds$/iu.test(String(observed.cta))) {
46
+ throw new Error(`Swap Max quote matches the input but cannot be submitted: ${JSON.stringify(observed)}.`);
47
+ }
48
48
  if (Number(observed.sourceAmount) > 0
49
- && (!options.waitForQuote || observed.noQuote || (!observed.loading && Number(observed.destinationAmount) > 0 && observed.ctaEnabled))) break;
49
+ && (!options.waitForQuote || observed.noQuote || (observed.quoteIdentity?.status === 'matching' && !observed.loading && Number(observed.destinationAmount) > 0 && observed.ctaEnabled && /^(?:Swap|Bridge)$/iu.test(String(observed.cta))))) break;
50
50
  await pause(page);
51
51
  }
52
52
  if (!(Number(observed?.sourceAmount) > 0)) throw new Error('Swap Max did not populate a positive source amount.');
53
53
  if (options.waitForQuote && !observed.noQuote
54
- && (observed.loading || !(Number(observed.destinationAmount) > 0) || !observed.ctaEnabled)) {
55
- throw new Error(`Swap Max quote did not resolve within ${options.timeoutMs}ms.`);
54
+ && (observed.quoteIdentity?.status !== 'matching' || observed.loading || !(Number(observed.destinationAmount) > 0) || !observed.ctaEnabled || !/^(?:Swap|Bridge)$/iu.test(String(observed.cta)))) {
55
+ throw new Error(`Swap Max quote did not resolve within ${options.timeoutMs}ms: ${JSON.stringify(observed)}.`);
56
56
  }
57
57
  return { action: input.action, ...observed, proofPath: 'trusted-pointer-visible-ui' };
58
58
  }));
@@ -1,3 +1,4 @@
1
+ // Saves custom swap/bridge slippage and waits for the displayed quote and submit control to refresh.
1
2
  import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
3
 
3
4
  async function pause(page) {
@@ -74,10 +75,8 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
74
75
  if (!observed?.includes(`${percent}%`)) {
75
76
  throw new Error(`Swap quote did not display Slippage: ${percent}%.`);
76
77
  }
77
- await page.evaluate('new Promise((resolve) => setTimeout(resolve, 500))');
78
78
  let stableSince = null;
79
79
  let quote = null;
80
- let lastRefreshAt = 0;
81
80
  while (Date.now() < deadline) {
82
81
  quote = await page.evaluate(`(() => {
83
82
  const destination = document.querySelector('[data-testid="to-amount"]');
@@ -90,13 +89,6 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
90
89
  ctaEnabled: Boolean(cta && !cta.disabled),
91
90
  };
92
91
  })()`);
93
- if (/^(?:Get new quote|Refresh quote)$/iu.test(quote.cta) && quote.ctaEnabled && Date.now() - lastRefreshAt >= 1000) {
94
- await page.click(dataTestId('bridge-cta-button'));
95
- lastRefreshAt = Date.now();
96
- stableSince = null;
97
- await pause(page);
98
- continue;
99
- }
100
92
  const ready = !quote.loading && Number(quote.destinationAmount) > 0 && quote.ctaEnabled
101
93
  && /^(?:Swap|Bridge)$/iu.test(quote.cta);
102
94
  if (ready) {
@@ -1,3 +1,4 @@
1
+ // Derives locator candidates from the Extension page's current visible-UI observation.
1
2
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
3
  import {
3
4
  locatorsFromObservation,