@deeeed/metamask-harness 0.36.0 → 0.38.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 (140) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/adapters/extension/artifact-runtime-state.cjs +138 -0
  3. package/adapters/extension/inject.mjs +2 -0
  4. package/adapters/extension/launch-browser.cjs +24 -0
  5. package/adapters/extension/live.sh +82 -12
  6. package/adapters/extension/readiness.mjs +77 -36
  7. package/adapters/extension/snapshot-dist.sh +88 -3
  8. package/adapters/extension/verify.sh +6 -2
  9. package/adapters/manifest.json +17 -1
  10. package/adapters/shared/log-tui.mjs +1 -1
  11. package/dist/adapters/extension/artifact-integrity.js +38 -0
  12. package/dist/adapters/extension/extension-id.js +23 -4
  13. package/dist/adapters/extension/release-artifact.js +386 -0
  14. package/dist/adapters/extension/runtime-decision.js +164 -20
  15. package/dist/adapters/extension/runtime.js +169 -3
  16. package/dist/adapters/mobile/provision.js +38 -3
  17. package/dist/adapters/mobile/release-artifact-state.js +124 -0
  18. package/dist/adapters/mobile/release-artifact.js +295 -0
  19. package/dist/adapters.js +78 -6
  20. package/dist/app-lifecycle.js +4 -3
  21. package/dist/command-contract.js +16 -0
  22. package/dist/commands/call.js +6 -2
  23. package/dist/commands/launch/extension.js +74 -9
  24. package/dist/commands/launch/index.js +3 -0
  25. package/dist/commands/launch/mobile.js +2 -0
  26. package/dist/commands/provision.js +2 -0
  27. package/dist/commands/run-engine.js +109 -14
  28. package/dist/commands/run.js +4 -2
  29. package/dist/commands/runtime-launch.js +178 -10
  30. package/dist/heal-bounds.js +1 -1
  31. package/dist/live-adapter-contract.js +3 -1
  32. package/dist/metamask-action-validation.js +47 -1
  33. package/dist/mm-harness-cli.js +31 -1
  34. package/dist/recipe-security.js +40 -0
  35. package/dist/run-diagnostics.js +1 -1
  36. package/dist/runner.js +3 -0
  37. package/docs/RELEASE-QA-CAPABILITY-MAP.md +125 -0
  38. package/library/actions/extension/assets/finish_send.mjs +128 -0
  39. package/library/actions/extension/assets/import_custom_token.mjs +227 -0
  40. package/library/actions/extension/assets/prepare_send.mjs +53 -0
  41. package/library/actions/extension/assets/read_details.mjs +57 -0
  42. package/library/actions/extension/assets/read_visible_state.mjs +50 -0
  43. package/library/actions/extension/assets/review_send.mjs +23 -0
  44. package/library/actions/extension/assets/select_network_scope.mjs +30 -0
  45. package/library/actions/extension/assets/send.mjs +104 -0
  46. package/library/actions/extension/assets/set_custom_gas.mjs +56 -0
  47. package/library/actions/extension/assets/set_token_visibility.mjs +129 -0
  48. package/library/actions/extension/assets/verify_sorting.mjs +89 -0
  49. package/library/actions/extension/networks/add_chainlist.mjs +94 -0
  50. package/library/actions/extension/networks/add_custom.mjs +123 -0
  51. package/library/actions/extension/networks/custom_network.mjs +103 -0
  52. package/library/actions/extension/networks/read_visible_state.mjs +50 -0
  53. package/library/actions/extension/networks/remove_custom.mjs +68 -0
  54. package/library/actions/extension/perps/close_orders.mjs +1 -0
  55. package/library/actions/extension/perps/close_visible_position.mjs +133 -0
  56. package/library/actions/extension/perps/compare_provider_market.mjs +109 -0
  57. package/library/actions/extension/perps/edit_margin.mjs +82 -0
  58. package/library/actions/extension/perps/open_balance_action.mjs +58 -0
  59. package/library/actions/extension/perps/open_position_action.mjs +80 -0
  60. package/library/actions/extension/perps/perps.mjs +196 -2
  61. package/library/actions/extension/perps/place_order.mjs +5 -2
  62. package/library/actions/extension/perps/read_eligibility.mjs +34 -0
  63. package/library/actions/extension/perps/read_funds_confirmation.mjs +69 -0
  64. package/library/actions/extension/perps/read_snapshot.mjs +470 -0
  65. package/library/actions/extension/perps/read_visible_state.mjs +98 -0
  66. package/library/actions/extension/perps/select_activity_filter.mjs +44 -0
  67. package/library/actions/extension/perps/select_market_filter.mjs +71 -0
  68. package/library/actions/extension/perps/set_market_favorite.mjs +47 -0
  69. package/library/actions/extension/perps/update_position_tpsl.mjs +106 -0
  70. package/library/actions/extension/platform/cdp.mjs +109 -3
  71. package/library/actions/extension/settings/set_basic_functionality.mjs +58 -0
  72. package/library/actions/extension/swap_bridge/read_visible_state.mjs +71 -0
  73. package/library/actions/extension/swap_bridge/select_assets.mjs +103 -0
  74. package/library/actions/extension/swap_bridge/set_amount.mjs +36 -0
  75. package/library/actions/extension/swap_bridge/set_max_amount.mjs +58 -0
  76. package/library/actions/extension/swap_bridge/set_slippage.mjs +121 -0
  77. package/library/actions/extension/swap_bridge/submit_transaction.mjs +363 -0
  78. package/library/actions/extension/ui/activate_and_observe.mjs +96 -0
  79. package/library/actions/extension/ui/navigate.mjs +264 -8
  80. package/library/actions/extension/wallet/ensure_unlocked.mjs +2 -6
  81. package/library/actions/extension/wallet/import.mjs +13 -46
  82. package/library/actions/extension/wallet/lock.mjs +35 -0
  83. package/library/actions/extension/wallet/secret-input.mjs +98 -0
  84. package/library/actions/extension/wallet/select_account.mjs +71 -7
  85. package/library/actions/mobile/assets/read_visible_state.mjs +37 -0
  86. package/library/actions/mobile/perps/read_visible_state.mjs +31 -0
  87. package/library/actions/mobile/platform/native-session-name.mjs +5 -0
  88. package/library/actions/mobile/platform/observe-ui.mjs +86 -2
  89. package/library/actions/mobile/swap_bridge/native-session.mjs +45 -0
  90. package/library/actions/mobile/swap_bridge/read_visible_state.mjs +31 -0
  91. package/library/actions/mobile/swap_bridge/select_assets.mjs +94 -0
  92. package/library/actions/mobile/swap_bridge/set_amount.mjs +73 -0
  93. package/library/actions/mobile/ui/native-navigation.mjs +265 -0
  94. package/library/actions/mobile/ui/navigate.mjs +10 -1
  95. package/library/actions/mobile/wallet/ensure_unlocked.mjs +17 -0
  96. package/library/actions/mobile/wallet/import.mjs +71 -2
  97. package/library/actions/mobile/wallet/native-ui.mjs +512 -0
  98. package/library/actions/mobile/wallet/read_state.mjs +16 -0
  99. package/library/actions/mobile/wallet/reset.mjs +17 -4
  100. package/library/actions/shared/assets/visible-state.mjs +176 -0
  101. package/library/actions/shared/networks/visible-state.mjs +70 -0
  102. package/library/actions/shared/perps/visible-state.mjs +354 -0
  103. package/library/actions/shared/swap-bridge/transaction.mjs +54 -0
  104. package/library/actions/shared/swap-bridge/visible-state.mjs +97 -0
  105. package/library/actions/shared/ui/locators.mjs +7 -0
  106. package/library/manifests/extension.action-manifest.json +1366 -184
  107. package/library/manifests/mobile.action-manifest.json +235 -1
  108. package/library/recipes/assets/release-custom-gas-send.recipe.json +88 -0
  109. package/library/recipes/assets/release-custom-network-token-persistence.recipe.json +106 -0
  110. package/library/recipes/assets/release-native-max-cancel.recipe.json +70 -0
  111. package/library/recipes/assets/release-native-max-send.recipe.json +78 -0
  112. package/library/recipes/assets/release-own-account-send.recipe.json +80 -0
  113. package/library/recipes/assets/release-token-address-import.recipe.json +47 -0
  114. package/library/recipes/assets/release-token-details.recipe.json +48 -0
  115. package/library/recipes/assets/release-token-hide-readd.recipe.json +55 -0
  116. package/library/recipes/assets/release-token-list.recipe.json +75 -0
  117. package/library/recipes/assets/release-token-persistence.recipe.json +73 -0
  118. package/library/recipes/assets/release-token-search-import.recipe.json +40 -0
  119. package/library/recipes/assets/release-token-sorting.recipe.json +29 -0
  120. package/library/recipes/extension/networks/release-balance-stability.recipe.json +51 -0
  121. package/library/recipes/extension/perps/release-live-limit-order.recipe.json +104 -0
  122. package/library/recipes/extension/perps/release-order-entry.recipe.json +155 -0
  123. package/library/recipes/extension/perps/release-order-validation.recipe.json +220 -0
  124. package/library/recipes/extension/runner/action-validation.recipe.json +12 -1
  125. package/library/recipes/perps/release-activity-filters.recipe.json +51 -0
  126. package/library/recipes/perps/release-funds-flows.recipe.json +76 -0
  127. package/library/recipes/perps/release-live-market-order.recipe.json +122 -0
  128. package/library/recipes/perps/release-market-details.recipe.json +83 -0
  129. package/library/recipes/perps/release-market-search.recipe.json +79 -0
  130. package/library/recipes/perps/release-watchlist.recipe.json +99 -0
  131. package/library/recipes/swap-bridge/release-custom-slippage.recipe.json +90 -0
  132. package/library/recipes/swap-bridge/release-live-max-swap.recipe.json +99 -0
  133. package/library/recipes/swap-bridge/release-live-swap.recipe.json +101 -0
  134. package/library/recipes/swap-bridge/release-max-quote.recipe.json +72 -0
  135. package/library/recipes/swap-bridge/release-quote.recipe.json +72 -0
  136. package/library/recipes/swap-bridge/release-surface.recipe.json +43 -0
  137. package/library/recipes/wallet/import.recipe.json +20 -1
  138. package/library/recipes/wallet/reset-import.recipe.json +20 -1
  139. package/package.json +6 -5
  140. package/scripts/validate-release-report.mjs +319 -0
@@ -0,0 +1,80 @@
1
+ import { dataTestId, marketSymbol, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
+
3
+ const ACTIONS = new Map([
4
+ ['add-exposure', {
5
+ first: 'perps-modify-cta-button',
6
+ second: 'perps-modify-menu-add-exposure',
7
+ expected: 'perps-order-entry-page',
8
+ }],
9
+ ['reduce-exposure', {
10
+ first: 'perps-modify-cta-button',
11
+ second: 'perps-modify-menu-reduce-exposure',
12
+ expected: 'perps-close-position-modal',
13
+ }],
14
+ ['reverse', {
15
+ first: 'perps-modify-cta-button',
16
+ second: 'perps-modify-menu-reverse-position',
17
+ expected: 'perps-reverse-position-modal',
18
+ }],
19
+ ['add-margin', {
20
+ first: 'perps-margin-card',
21
+ second: 'perps-margin-menu-add',
22
+ expected: 'perps-add-margin-modal',
23
+ }],
24
+ ['remove-margin', {
25
+ first: 'perps-margin-card',
26
+ second: 'perps-margin-menu-remove',
27
+ expected: 'perps-decrease-margin-modal',
28
+ }],
29
+ ['auto-close', {
30
+ first: 'perps-auto-close-row',
31
+ expected: 'perps-update-tpsl-modal',
32
+ }],
33
+ ]);
34
+
35
+ runAdapter(async (input) => withExtensionPage(input, async (page) => {
36
+ const market = marketSymbol(input);
37
+ if (!market) throw new Error('metamask.perps.open_position_action requires market=<symbol>.');
38
+ const positionAction = String(input.node?.position_action ?? '').trim().toLowerCase();
39
+ const configuration = ACTIONS.get(positionAction);
40
+ if (!configuration) {
41
+ throw new Error(`metamask.perps.open_position_action received unsupported position_action ${JSON.stringify(positionAction)}.`);
42
+ }
43
+
44
+ await page.navigateHash(`#/perps/market/${encodeURIComponent(market)}`);
45
+ await page.waitForSelector(dataTestId('perps-position-size-value'), { timeoutMs: 20_000 });
46
+ await page.click(dataTestId(configuration.first));
47
+ if (configuration.second) {
48
+ await page.waitForSelector(dataTestId(configuration.second), { timeoutMs: 10_000 });
49
+ await page.click(dataTestId(configuration.second));
50
+ }
51
+ await page.waitForSelector(dataTestId(configuration.expected), { timeoutMs: 15_000 });
52
+
53
+ const observed = await page.evaluate(`(() => {
54
+ const controls = [...document.querySelectorAll('[data-testid]')]
55
+ .filter((element) => {
56
+ const style = getComputedStyle(element);
57
+ const rect = element.getBoundingClientRect();
58
+ return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
59
+ })
60
+ .map((element) => ({
61
+ testId: element.getAttribute('data-testid'),
62
+ label: String(element.getAttribute('aria-label') || element.innerText || '')
63
+ .replace(/\\s+/gu, ' ')
64
+ .trim()
65
+ .slice(0, 500),
66
+ enabled: !element.disabled && element.getAttribute('aria-disabled') !== 'true',
67
+ }))
68
+ .filter((entry) => entry.testId && /perps/u.test(entry.testId))
69
+ .slice(0, 150);
70
+ return { route: String(location.hash), controls };
71
+ })()`);
72
+ return {
73
+ action: input.action,
74
+ market,
75
+ positionAction,
76
+ expectedTestId: configuration.expected,
77
+ ...observed,
78
+ proofPath: 'trusted-pointer-visible-ui',
79
+ };
80
+ }));
@@ -1,5 +1,6 @@
1
1
  import { pathToFileURL } from 'node:url';
2
2
  import { dataTestId, marketSymbol, normalizeMarketSymbol, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
3
+ import { readSnapshot } from './read_snapshot.mjs';
3
4
  import { readPerpsRuntimeStateExpression } from './state.mjs';
4
5
 
5
6
  function sleep(ms) {
@@ -203,6 +204,51 @@ export async function closeOrders(input) {
203
204
  requireExplicitSelection(input);
204
205
  return withExtensionPage(input, async (page) => {
205
206
  const timeoutMs = Number(input.node?.timeout_ms ?? 30000);
207
+ const hasBackgroundRequest = await page.evaluate(
208
+ `typeof globalThis.stateHooks?.submitRequestToBackground === 'function'`,
209
+ );
210
+ if (!hasBackgroundRequest) {
211
+ if (input.node?.confirm_live !== true) {
212
+ throw new Error('metamask.perps.close_orders requires confirm_live=true for visible live cancellation.');
213
+ }
214
+ const selector = input.node?.selector && typeof input.node.selector === 'object' ? input.node.selector : {};
215
+ if (input.node?.side != null || selector.side != null) {
216
+ throw new Error('Visible Perps order cancellation cannot prove an order side. Next: select one market without side or use a runtime with background state hooks.');
217
+ }
218
+ const symbols = configuredSymbols(input, []);
219
+ if (symbols.length !== 1) {
220
+ throw new Error('Visible Perps order cancellation requires exactly one market.');
221
+ }
222
+ const symbol = symbols[0];
223
+ await page.navigateHash(`#/perps/market/${encodeURIComponent(symbol)}`);
224
+ const orderSelector = '[data-testid^="order-card-"]';
225
+ await page.waitForSelector(orderSelector, { timeoutMs });
226
+ let canceled = 0;
227
+ while (await page.evaluate(`Boolean(document.querySelector(${JSON.stringify(orderSelector)}))`)) {
228
+ const beforeCount = await page.evaluate(`document.querySelectorAll(${JSON.stringify(orderSelector)}).length`);
229
+ await page.click(orderSelector);
230
+ await page.waitForSelector(dataTestId('perps-cancel-order-modal'), { timeoutMs });
231
+ await page.click(dataTestId('perps-cancel-order-button'));
232
+ canceled += 1;
233
+ const deadline = Date.now() + timeoutMs;
234
+ while (Date.now() < deadline) {
235
+ const state = await page.evaluate(`({
236
+ modal: Boolean(document.querySelector(${JSON.stringify(dataTestId('perps-cancel-order-modal'))})),
237
+ orders: document.querySelectorAll(${JSON.stringify(orderSelector)}).length,
238
+ })`);
239
+ if (!state.modal && state.orders < beforeCount) break;
240
+ await sleep(250);
241
+ }
242
+ if (canceled >= 100) throw new Error('Visible Perps order cancellation exceeded 100 orders.');
243
+ }
244
+ return {
245
+ action: input.action,
246
+ canceled: canceled > 0,
247
+ matchingCount: canceled,
248
+ symbols: [symbol],
249
+ proofPath: 'visible-product-controls',
250
+ };
251
+ }
206
252
  const before = await readOpenOrders(page);
207
253
  const matching = selectedItems(input, before);
208
254
  const symbols = uniqueSymbols(matching.map(symbolForItem));
@@ -462,8 +508,17 @@ export async function placeOrder(input) {
462
508
  const leverage = Number(input.node?.leverage ?? 3);
463
509
  const isBuy = side !== 'short';
464
510
  const orderType = resolveOrderType(input);
511
+ const positionMode = String(input.node?.position_mode ?? 'new').toLowerCase();
512
+ const submitAttempts = Number(input.node?.submit_attempts ?? 1);
513
+ if (positionMode !== 'new' && positionMode !== 'modify') {
514
+ throw new Error(`${input.action} received unsupported position_mode: ${positionMode}.`);
515
+ }
516
+ if (!Number.isInteger(submitAttempts) || submitAttempts < 1 || submitAttempts > 2) {
517
+ throw new Error(`${input.action} submit_attempts must be 1 or 2.`);
518
+ }
465
519
  return withExtensionPage(input, async (page) => {
466
520
  await page.navigateHash(`#/perps/market/${encodeURIComponent(symbol)}`);
521
+ await page.waitForSelector(dataTestId('perps-market-detail-pair'), { timeoutMs: 20_000 });
467
522
  await page.evaluate(`(async () => {
468
523
  const request = globalThis.stateHooks?.submitRequestToBackground;
469
524
  if (typeof request !== 'function') return;
@@ -481,8 +536,146 @@ export async function placeOrder(input) {
481
536
  const limitPrice = orderType === 'limit'
482
537
  ? resolveLimitPrice(input, isBuy, resolvedPrice)
483
538
  : null;
484
- await page.navigateHash(`#/perps/trade/${encodeURIComponent(symbol)}?direction=${encodeURIComponent(side)}&mode=new`);
539
+ const initialPositionSize = positionMode === 'modify'
540
+ ? await page.evaluate(`document.querySelector('[data-testid="perps-position-size-value"]')?.textContent?.trim() ?? null`)
541
+ : null;
542
+ const initialOpenOrderCount = orderType === 'limit'
543
+ ? await page.evaluate(`document.querySelectorAll('[data-testid^="order-card-"]').length`)
544
+ : null;
545
+ await page.navigateHash(`#/perps/trade/${encodeURIComponent(symbol)}?direction=${encodeURIComponent(side)}&mode=${positionMode}`);
485
546
  await page.waitForSelector(dataTestId('perps-order-entry-page'), { timeoutMs: 20000 });
547
+ const hasBackgroundRequest = await page.evaluate(
548
+ `typeof globalThis.stateHooks?.submitRequestToBackground === 'function'`,
549
+ );
550
+ if (!hasBackgroundRequest) {
551
+ if (input.node?.confirm_live !== true) {
552
+ throw new Error('metamask.perps.place_order requires confirm_live=true for visible live submission.');
553
+ }
554
+ await page.setInput(`${dataTestId('amount-input-field')} input`, amount);
555
+ await page.setInput(`${dataTestId('leverage-input')} input`, String(leverage));
556
+ if (orderType === 'limit') {
557
+ await page.click(dataTestId('order-type-limit'));
558
+ const limitInput = `${dataTestId('limit-price-input')} input`;
559
+ await page.waitForSelector(limitInput, { timeoutMs: 10000 });
560
+ await page.setInput(limitInput, String(limitPrice));
561
+ }
562
+ const configuredDeadline = Date.now() + 10_000;
563
+ let configured;
564
+ while (Date.now() < configuredDeadline) {
565
+ configured = await page.evaluate(`(() => {
566
+ const value = (testId) => document.querySelector('[data-testid="' + testId + '"] input')?.value ?? null;
567
+ const limit = document.querySelector('[data-testid="limit-price-input"] input')?.value ?? null;
568
+ const submit = document.querySelector('[data-testid="submit-order-button"]');
569
+ return {
570
+ amount: value('amount-input-field'),
571
+ leverage: value('leverage-input'),
572
+ limitPrice: limit,
573
+ limitSelected: document.querySelector('[data-testid="order-type-limit"]')?.getAttribute('aria-pressed') === 'true',
574
+ submitEnabled: Boolean(submit && !submit.disabled && submit.getAttribute('aria-disabled') !== 'true'),
575
+ validation: [...document.querySelectorAll('[role="alert"], [data-testid$="validation-error"], [data-testid$="warning"]')]
576
+ .map((element) => String(element.innerText || '').replace(/\\s+/gu, ' ').trim())
577
+ .filter(Boolean)
578
+ .slice(0, 20),
579
+ };
580
+ })()`);
581
+ if (configured.submitEnabled) break;
582
+ await sleep(250);
583
+ }
584
+ if (Number(configured.amount) !== Number(amount) || Number(configured.leverage) !== leverage) {
585
+ throw new Error(`Visible Perps order inputs did not retain the requested values: ${JSON.stringify(configured)}.`);
586
+ }
587
+ if (orderType === 'limit' && (!configured.limitSelected || Number(configured.limitPrice) !== limitPrice)) {
588
+ throw new Error(`Visible Perps limit inputs did not retain the requested values: ${JSON.stringify(configured)}.`);
589
+ }
590
+ if (!configured.submitEnabled) {
591
+ throw new Error(`Visible Perps order is not submittable: ${JSON.stringify(configured)}.`);
592
+ }
593
+ const submitTarget = await page.evaluate(`(() => {
594
+ const element = document.querySelector('[data-testid="submit-order-button"]');
595
+ if (!element) throw new Error('Visible Perps submit control disappeared.');
596
+ element.scrollIntoView({ block: 'center', inline: 'center' });
597
+ const rect = element.getBoundingClientRect();
598
+ return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
599
+ })()`);
600
+ for (let attempt = 0; attempt < submitAttempts; attempt += 1) {
601
+ await page.session.call('Input.dispatchMouseEvent', {
602
+ type: 'mousePressed',
603
+ x: submitTarget.x,
604
+ y: submitTarget.y,
605
+ button: 'left',
606
+ buttons: 1,
607
+ clickCount: 1,
608
+ });
609
+ await page.session.call('Input.dispatchMouseEvent', {
610
+ type: 'mouseReleased',
611
+ x: submitTarget.x,
612
+ y: submitTarget.y,
613
+ button: 'left',
614
+ buttons: 0,
615
+ clickCount: 1,
616
+ });
617
+ }
618
+ const resultSelector = orderType === 'limit'
619
+ ? '[data-testid^="order-card-"]'
620
+ : dataTestId('perps-position-size-value');
621
+ const resultDeadline = Date.now() + Number(input.node?.timeout_ms ?? 45000);
622
+ let outcome;
623
+ while (Date.now() < resultDeadline) {
624
+ outcome = await page.evaluate(`(() => ({
625
+ success: ${JSON.stringify(positionMode)} === 'modify'
626
+ ? String(location.hash).startsWith(${JSON.stringify(`#/perps/market/${symbol}`)}) &&
627
+ String(document.querySelector('[data-testid="perps-position-size-value"]')?.textContent || '').trim() !== ${JSON.stringify(initialPositionSize)}
628
+ : Boolean(document.querySelector(${JSON.stringify(resultSelector)})),
629
+ route: String(location.hash),
630
+ submit: (() => {
631
+ const element = document.querySelector('[data-testid="submit-order-button"]');
632
+ return element ? {
633
+ label: String(element.innerText || '').replace(/\\s+/gu, ' ').trim(),
634
+ enabled: !element.disabled && element.getAttribute('aria-disabled') !== 'true',
635
+ } : null;
636
+ })(),
637
+ errors: [...document.querySelectorAll('[data-testid="perps-order-submit-error"], [role="alert"]')]
638
+ .map((element) => String(element.innerText || '').replace(/\\s+/gu, ' ').trim())
639
+ .filter(Boolean)
640
+ .slice(0, 20),
641
+ }))()`);
642
+ if (outcome.success) break;
643
+ if (outcome.errors.length > 0) {
644
+ throw new Error(`Visible Perps order submission failed: ${outcome.errors.join(' | ')}`);
645
+ }
646
+ await sleep(250);
647
+ }
648
+ if (!outcome?.success) {
649
+ throw new Error(`Visible Perps order submission did not produce ${resultSelector}; observed ${JSON.stringify(outcome)}.`);
650
+ }
651
+ let openOrderCount = null;
652
+ if (orderType === 'limit') {
653
+ await sleep(2_000);
654
+ await page.navigateHash(`#/perps/market/${encodeURIComponent(symbol)}`);
655
+ await page.waitForSelector(dataTestId('perps-market-detail-pair'), { timeoutMs: 20_000 });
656
+ openOrderCount = await page.evaluate(`document.querySelectorAll('[data-testid^="order-card-"]').length`);
657
+ if (openOrderCount !== initialOpenOrderCount + 1) {
658
+ throw new Error(`Perps limit submission created an unexpected open-order count: before=${initialOpenOrderCount}, after=${openOrderCount}.`);
659
+ }
660
+ }
661
+ return {
662
+ action: input.action,
663
+ market: symbol,
664
+ side,
665
+ orderType,
666
+ positionMode,
667
+ amount,
668
+ leverage,
669
+ limitPrice,
670
+ submitted: true,
671
+ submitAttempts,
672
+ initialOpenOrderCount,
673
+ openOrderCount,
674
+ configured,
675
+ outcome,
676
+ proofPath: 'visible-product-controls',
677
+ };
678
+ }
486
679
  let order;
487
680
  try {
488
681
  order = await page.evaluate(`(async () => {
@@ -531,7 +724,7 @@ export async function placeOrder(input) {
531
724
  } else {
532
725
  await waitForPositionPresent(page, symbol, Number(input.node?.timeout_ms ?? 30000));
533
726
  }
534
- return { action: input.action, market: symbol, side, orderType, amount, leverage, limitPrice, submitted: true, order, proofPath: 'background-perpsPlaceOrder' };
727
+ return { action: input.action, market: symbol, side, orderType, positionMode, amount, leverage, limitPrice, submitted: true, order, proofPath: 'background-perpsPlaceOrder' };
535
728
  });
536
729
  }
537
730
 
@@ -832,6 +1025,7 @@ export async function teardownState(input) {
832
1025
  }
833
1026
 
834
1027
  const DIRECT_ACTIONS = new Map([
1028
+ ['metamask.perps.read_snapshot', readSnapshot],
835
1029
  ['metamask.perps.read_positions', readPositions],
836
1030
  ['metamask.perps.read_orders', readOrders],
837
1031
  ['metamask.perps.close_positions', closePositions],
@@ -1,7 +1,10 @@
1
1
  import { runAdapter } from '../platform/cdp.mjs';
2
- import { assertPositions, placeOrder } from './perps.mjs';
2
+ import { assertOrders, assertPositions, placeOrder } from './perps.mjs';
3
3
  runAdapter(async (input) => {
4
4
  const order = await placeOrder(input);
5
- const assertion = await assertPositions(input, true);
5
+ if (order.proofPath === 'visible-product-controls') return order;
6
+ const assertion = order.orderType === 'limit'
7
+ ? await assertOrders(input, true)
8
+ : await assertPositions(input, true);
6
9
  return { ...assertion, submitted: order.submitted === true, order };
7
10
  });
@@ -0,0 +1,34 @@
1
+ import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
+
3
+ runAdapter(async (input) => {
4
+ const endpoint = 'https://geolocation.api.cx.metamask.io/v2/geolocation';
5
+ const response = await fetch(endpoint, { signal: AbortSignal.timeout(Number(input.node?.timeout_ms ?? 15_000)) });
6
+ if (!response.ok) throw new Error(`MetaMask geolocation request failed with HTTP ${response.status}.`);
7
+ const geolocation = await response.json();
8
+ return withExtensionPage(input, async (page) => {
9
+ const product = await page.evaluate(`(async () => {
10
+ const hooks = globalThis.stateHooks || {};
11
+ const root = (await hooks.getCleanAppState?.()) || hooks.store?.getState?.() || {};
12
+ const metamask = root.metamask || root;
13
+ return {
14
+ isEligible: metamask.isEligible ?? null,
15
+ geoBlockModalVisible: Boolean(document.querySelector('[data-testid="perps-geo-block-modal"]')),
16
+ route: String(location.hash),
17
+ };
18
+ })()`, { awaitPromise: true });
19
+ if (typeof product.isEligible !== 'boolean') {
20
+ throw new Error('The release UI did not expose the current Perps eligibility state.');
21
+ }
22
+ return {
23
+ action: input.action,
24
+ endpoint,
25
+ geolocation: {
26
+ country: geolocation?.country ?? null,
27
+ region: geolocation?.region ?? null,
28
+ timezone: geolocation?.timezone ?? null,
29
+ },
30
+ product,
31
+ proofPath: 'release-state-plus-live-geolocation',
32
+ };
33
+ });
34
+ });
@@ -0,0 +1,69 @@
1
+ import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
+
3
+ runAdapter(async (input) => withExtensionPage(input, async (page) => {
4
+ const mode = String(input.node?.mode ?? '').trim().toLowerCase();
5
+ if (mode !== 'deposit' && mode !== 'withdraw') {
6
+ throw new Error('metamask.perps.read_funds_confirmation mode must be deposit or withdraw.');
7
+ }
8
+ const expectedTitle = mode === 'deposit' ? 'Deposit funds to Perps' : 'Withdraw';
9
+ const deadline = Date.now() + 30_000;
10
+ let observed;
11
+ while (Date.now() <= deadline) {
12
+ observed = await page.evaluate(`(() => {
13
+ const read = (testId) => String(document.querySelector('[data-testid="' + testId + '"]')?.innerText || '')
14
+ .replace(/\\s+/gu, ' ')
15
+ .trim()
16
+ .slice(0, 500);
17
+ const amount = document.querySelector('[data-testid="custom-amount-input"]');
18
+ const submit = document.querySelector('[data-testid="confirm-footer-button"]');
19
+ return {
20
+ route: String(location.hash),
21
+ title: String(document.body?.innerText || '').trim().split(/\\n+/u)[0] || '',
22
+ amount: amount && 'value' in amount ? String(amount.value) : null,
23
+ availableBalance: read('perps-withdraw-balance-value'),
24
+ transactionFee: read('transaction-fee-value'),
25
+ estimatedTime: read('bridge-time-value'),
26
+ receive: read('receive-value'),
27
+ payToken: String(document.body?.innerText || '').match(/Pay with\\s+([^\\n]+)/u)?.[1] || '',
28
+ submitLabel: String(submit?.innerText || '').trim(),
29
+ submitEnabled: Boolean(submit && !submit.disabled && submit.getAttribute('aria-disabled') !== 'true'),
30
+ layout: {
31
+ viewportWidth: innerWidth,
32
+ viewportHeight: innerHeight,
33
+ contentWidth: Math.max(document.documentElement?.scrollWidth || 0, document.body?.scrollWidth || 0),
34
+ horizontalOverflow: Math.max(document.documentElement?.scrollWidth || 0, document.body?.scrollWidth || 0) > innerWidth + 1,
35
+ clippedTestIds: [...document.querySelectorAll('[data-testid]')]
36
+ .filter((element) => {
37
+ const style = getComputedStyle(element);
38
+ const rect = element.getBoundingClientRect();
39
+ return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0 &&
40
+ rect.bottom > 0 && rect.top < innerHeight && (rect.left < -1 || rect.right > innerWidth + 1);
41
+ })
42
+ .map((element) => element.getAttribute('data-testid'))
43
+ .filter(Boolean)
44
+ .slice(0, 100),
45
+ },
46
+ };
47
+ })()`);
48
+ if (observed.title === expectedTitle && observed.amount !== null) break;
49
+ await new Promise((resolve) => setTimeout(resolve, 100));
50
+ }
51
+ if (observed?.title !== expectedTitle || observed?.amount === null) {
52
+ throw new Error(`Perps ${mode} confirmation did not finish loading.`);
53
+ }
54
+ if (input.node?.require_no_horizontal_overflow === true && (
55
+ observed.layout.horizontalOverflow || observed.layout.clippedTestIds.length > 0
56
+ )) {
57
+ throw new Error(`Visible Perps funds layout overflows horizontally: ${JSON.stringify(observed.layout)}.`);
58
+ }
59
+ if (typeof input.node?.expected_submit_enabled === 'boolean'
60
+ && observed.submitEnabled !== input.node.expected_submit_enabled) {
61
+ throw new Error(`Perps ${mode} submit state mismatch: expected ${input.node.expected_submit_enabled}, observed ${observed.submitEnabled}.`);
62
+ }
63
+ return {
64
+ action: input.action,
65
+ mode,
66
+ ...observed,
67
+ proofPath: 'visible-dom-accessibility',
68
+ };
69
+ }));