@deeeed/metamask-harness 0.37.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.
- package/CHANGELOG.md +27 -0
- package/adapters/extension/artifact-runtime-state.cjs +13 -3
- package/adapters/extension/inject.mjs +1 -0
- package/adapters/extension/launch-browser.cjs +2 -0
- package/adapters/extension/live.sh +1 -1
- package/adapters/extension/readiness.mjs +1 -1
- package/adapters/manifest.json +8 -0
- package/dist/adapters/extension/runtime-decision.js +4 -1
- package/dist/adapters/extension/runtime.js +42 -3
- package/dist/adapters/mobile/provision.js +38 -3
- package/dist/adapters.js +68 -3
- package/dist/app-lifecycle.js +4 -3
- package/dist/commands/call.js +4 -1
- package/dist/commands/launch/extension.js +21 -5
- package/dist/commands/launch/index.js +3 -0
- package/dist/commands/run-engine.js +40 -29
- package/dist/commands/run.js +1 -1
- package/dist/recipe-security.js +40 -0
- package/dist/runner.js +3 -0
- package/docs/RELEASE-QA-CAPABILITY-MAP.md +116 -141
- package/library/actions/extension/assets/finish_send.mjs +128 -0
- package/library/actions/extension/assets/import_custom_token.mjs +227 -0
- package/library/actions/extension/assets/prepare_send.mjs +53 -0
- package/library/actions/extension/assets/read_details.mjs +57 -0
- package/library/actions/extension/assets/read_visible_state.mjs +50 -0
- package/library/actions/extension/assets/review_send.mjs +23 -0
- package/library/actions/extension/assets/select_network_scope.mjs +30 -0
- package/library/actions/extension/assets/send.mjs +104 -0
- package/library/actions/extension/assets/set_custom_gas.mjs +56 -0
- package/library/actions/extension/assets/set_token_visibility.mjs +129 -0
- package/library/actions/extension/assets/verify_sorting.mjs +89 -0
- package/library/actions/extension/networks/add_chainlist.mjs +94 -0
- package/library/actions/extension/networks/add_custom.mjs +123 -0
- package/library/actions/extension/networks/custom_network.mjs +103 -0
- package/library/actions/extension/networks/read_visible_state.mjs +50 -0
- package/library/actions/extension/networks/remove_custom.mjs +68 -0
- package/library/actions/extension/perps/close_orders.mjs +1 -0
- package/library/actions/extension/perps/close_visible_position.mjs +133 -0
- package/library/actions/extension/perps/compare_provider_market.mjs +109 -0
- package/library/actions/extension/perps/edit_margin.mjs +82 -0
- package/library/actions/extension/perps/open_balance_action.mjs +58 -0
- package/library/actions/extension/perps/open_position_action.mjs +80 -0
- package/library/actions/extension/perps/perps.mjs +194 -2
- package/library/actions/extension/perps/place_order.mjs +5 -2
- package/library/actions/extension/perps/read_eligibility.mjs +34 -0
- package/library/actions/extension/perps/read_funds_confirmation.mjs +69 -0
- package/library/actions/extension/perps/read_visible_state.mjs +98 -0
- package/library/actions/extension/perps/select_activity_filter.mjs +44 -0
- package/library/actions/extension/perps/select_market_filter.mjs +71 -0
- package/library/actions/extension/perps/set_market_favorite.mjs +47 -0
- package/library/actions/extension/perps/update_position_tpsl.mjs +106 -0
- package/library/actions/extension/platform/cdp.mjs +104 -1
- package/library/actions/extension/settings/set_basic_functionality.mjs +58 -0
- package/library/actions/extension/swap_bridge/read_visible_state.mjs +71 -0
- package/library/actions/extension/swap_bridge/select_assets.mjs +103 -0
- package/library/actions/extension/swap_bridge/set_amount.mjs +36 -0
- package/library/actions/extension/swap_bridge/set_max_amount.mjs +58 -0
- package/library/actions/extension/swap_bridge/set_slippage.mjs +121 -0
- package/library/actions/extension/swap_bridge/submit_transaction.mjs +363 -0
- package/library/actions/extension/ui/activate_and_observe.mjs +96 -0
- package/library/actions/extension/ui/navigate.mjs +264 -8
- package/library/actions/extension/wallet/ensure_unlocked.mjs +2 -6
- package/library/actions/extension/wallet/lock.mjs +35 -0
- package/library/actions/extension/wallet/select_account.mjs +71 -7
- package/library/actions/mobile/assets/read_visible_state.mjs +37 -0
- package/library/actions/mobile/perps/read_visible_state.mjs +31 -0
- package/library/actions/mobile/platform/native-session-name.mjs +5 -0
- package/library/actions/mobile/platform/observe-ui.mjs +5 -3
- package/library/actions/mobile/swap_bridge/native-session.mjs +45 -0
- package/library/actions/mobile/swap_bridge/read_visible_state.mjs +31 -0
- package/library/actions/mobile/swap_bridge/select_assets.mjs +94 -0
- package/library/actions/mobile/swap_bridge/set_amount.mjs +73 -0
- package/library/actions/mobile/ui/native-navigation.mjs +45 -5
- package/library/actions/mobile/ui/navigate.mjs +3 -1
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +17 -0
- package/library/actions/mobile/wallet/native-ui.mjs +21 -2
- package/library/actions/shared/assets/visible-state.mjs +176 -0
- package/library/actions/shared/networks/visible-state.mjs +70 -0
- package/library/actions/shared/perps/visible-state.mjs +354 -0
- package/library/actions/shared/swap-bridge/transaction.mjs +54 -0
- package/library/actions/shared/swap-bridge/visible-state.mjs +97 -0
- package/library/manifests/extension.action-manifest.json +1255 -189
- package/library/manifests/mobile.action-manifest.json +219 -1
- package/library/recipes/assets/release-custom-gas-send.recipe.json +88 -0
- package/library/recipes/assets/release-custom-network-token-persistence.recipe.json +106 -0
- package/library/recipes/assets/release-native-max-cancel.recipe.json +70 -0
- package/library/recipes/assets/release-native-max-send.recipe.json +78 -0
- package/library/recipes/assets/release-own-account-send.recipe.json +80 -0
- package/library/recipes/assets/release-token-address-import.recipe.json +47 -0
- package/library/recipes/assets/release-token-details.recipe.json +48 -0
- package/library/recipes/assets/release-token-hide-readd.recipe.json +55 -0
- package/library/recipes/assets/release-token-list.recipe.json +75 -0
- package/library/recipes/assets/release-token-persistence.recipe.json +73 -0
- package/library/recipes/assets/release-token-search-import.recipe.json +40 -0
- package/library/recipes/assets/release-token-sorting.recipe.json +29 -0
- package/library/recipes/extension/networks/release-balance-stability.recipe.json +51 -0
- package/library/recipes/extension/perps/release-live-limit-order.recipe.json +104 -0
- package/library/recipes/extension/perps/release-order-entry.recipe.json +155 -0
- package/library/recipes/extension/perps/release-order-validation.recipe.json +220 -0
- package/library/recipes/perps/release-activity-filters.recipe.json +51 -0
- package/library/recipes/perps/release-funds-flows.recipe.json +76 -0
- package/library/recipes/perps/release-live-market-order.recipe.json +122 -0
- package/library/recipes/perps/release-market-details.recipe.json +83 -0
- package/library/recipes/perps/release-market-search.recipe.json +79 -0
- package/library/recipes/perps/release-watchlist.recipe.json +99 -0
- package/library/recipes/swap-bridge/release-custom-slippage.recipe.json +90 -0
- package/library/recipes/swap-bridge/release-live-max-swap.recipe.json +99 -0
- package/library/recipes/swap-bridge/release-live-swap.recipe.json +101 -0
- package/library/recipes/swap-bridge/release-max-quote.recipe.json +72 -0
- package/library/recipes/swap-bridge/release-quote.recipe.json +72 -0
- package/library/recipes/swap-bridge/release-surface.recipe.json +43 -0
- package/package.json +6 -5
- package/scripts/validate-release-report.mjs +319 -0
|
@@ -204,6 +204,51 @@ export async function closeOrders(input) {
|
|
|
204
204
|
requireExplicitSelection(input);
|
|
205
205
|
return withExtensionPage(input, async (page) => {
|
|
206
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
|
+
}
|
|
207
252
|
const before = await readOpenOrders(page);
|
|
208
253
|
const matching = selectedItems(input, before);
|
|
209
254
|
const symbols = uniqueSymbols(matching.map(symbolForItem));
|
|
@@ -463,8 +508,17 @@ export async function placeOrder(input) {
|
|
|
463
508
|
const leverage = Number(input.node?.leverage ?? 3);
|
|
464
509
|
const isBuy = side !== 'short';
|
|
465
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
|
+
}
|
|
466
519
|
return withExtensionPage(input, async (page) => {
|
|
467
520
|
await page.navigateHash(`#/perps/market/${encodeURIComponent(symbol)}`);
|
|
521
|
+
await page.waitForSelector(dataTestId('perps-market-detail-pair'), { timeoutMs: 20_000 });
|
|
468
522
|
await page.evaluate(`(async () => {
|
|
469
523
|
const request = globalThis.stateHooks?.submitRequestToBackground;
|
|
470
524
|
if (typeof request !== 'function') return;
|
|
@@ -482,8 +536,146 @@ export async function placeOrder(input) {
|
|
|
482
536
|
const limitPrice = orderType === 'limit'
|
|
483
537
|
? resolveLimitPrice(input, isBuy, resolvedPrice)
|
|
484
538
|
: null;
|
|
485
|
-
|
|
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}`);
|
|
486
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
|
+
}
|
|
487
679
|
let order;
|
|
488
680
|
try {
|
|
489
681
|
order = await page.evaluate(`(async () => {
|
|
@@ -532,7 +724,7 @@ export async function placeOrder(input) {
|
|
|
532
724
|
} else {
|
|
533
725
|
await waitForPositionPresent(page, symbol, Number(input.node?.timeout_ms ?? 30000));
|
|
534
726
|
}
|
|
535
|
-
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' };
|
|
536
728
|
});
|
|
537
729
|
}
|
|
538
730
|
|
|
@@ -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
|
-
|
|
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
|
+
}));
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
|
+
import {
|
|
3
|
+
normalizeVisiblePerpsState,
|
|
4
|
+
resolveVisibleStateOptions,
|
|
5
|
+
} from '../../shared/perps/visible-state.mjs';
|
|
6
|
+
|
|
7
|
+
runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
8
|
+
const options = resolveVisibleStateOptions(input.node);
|
|
9
|
+
const raw = await page.evaluate(`(() => {
|
|
10
|
+
const includeOffscreen = ${JSON.stringify(options.includeOffscreen)};
|
|
11
|
+
const maximum = ${JSON.stringify(options.maxItems)};
|
|
12
|
+
const relevant = /^(?:perps-|explore-markets-|market-list-view|market-row-|filter-select-button|sort-dropdown-button|search-input|position-card-|order-card-|transaction-card-|bottom-nav-perps|direction-|order-type-|amount-|balance-percent-|leverage-|auto-close|submit-order-button|limit-price-|tp-|sl-)/u;
|
|
13
|
+
const items = [];
|
|
14
|
+
const offscreenItems = [];
|
|
15
|
+
const clippedTestIds = [];
|
|
16
|
+
let matched = 0;
|
|
17
|
+
for (const element of document.querySelectorAll('[data-testid]')) {
|
|
18
|
+
const testId = element.getAttribute('data-testid');
|
|
19
|
+
if (!testId || !relevant.test(testId)) continue;
|
|
20
|
+
const style = getComputedStyle(element);
|
|
21
|
+
const rect = element.getBoundingClientRect();
|
|
22
|
+
const rendered = style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
|
|
23
|
+
if (!rendered) continue;
|
|
24
|
+
matched += 1;
|
|
25
|
+
const visible = rect.bottom > 0 && rect.right > 0 && rect.top < innerHeight && rect.left < innerWidth;
|
|
26
|
+
if (rect.bottom > 0 && rect.top < innerHeight && (rect.left < -1 || rect.right > innerWidth + 1)) {
|
|
27
|
+
clippedTestIds.push(testId);
|
|
28
|
+
}
|
|
29
|
+
if (!visible && !includeOffscreen) continue;
|
|
30
|
+
if (items.length + offscreenItems.length >= maximum) break;
|
|
31
|
+
const label = String(element.getAttribute('aria-label') || element.innerText || element.getAttribute('value') || '')
|
|
32
|
+
.replace(/\\s+/gu, ' ')
|
|
33
|
+
.trim()
|
|
34
|
+
.slice(0, 500);
|
|
35
|
+
const item = {
|
|
36
|
+
testId,
|
|
37
|
+
label,
|
|
38
|
+
role: element.getAttribute('role') || element.tagName.toLowerCase(),
|
|
39
|
+
enabled: !element.disabled && element.getAttribute('aria-disabled') !== 'true',
|
|
40
|
+
visible,
|
|
41
|
+
};
|
|
42
|
+
(visible ? items : offscreenItems).push(item);
|
|
43
|
+
}
|
|
44
|
+
const field = (testId) => {
|
|
45
|
+
const element = document.querySelector('[data-testid="' + testId + '"]');
|
|
46
|
+
const input = element?.matches('input') ? element : element?.querySelector('input');
|
|
47
|
+
if (!element) return null;
|
|
48
|
+
return {
|
|
49
|
+
testId,
|
|
50
|
+
label: String(element.innerText || element.getAttribute('aria-label') || '').replace(/\\s+/gu, ' ').trim().slice(0, 500),
|
|
51
|
+
value: input && 'value' in input ? String(input.value) : null,
|
|
52
|
+
checked: input?.checked === true,
|
|
53
|
+
enabled: !element.disabled && element.getAttribute('aria-disabled') !== 'true',
|
|
54
|
+
pressed: element.getAttribute('aria-pressed'),
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
const submit = field('submit-order-button');
|
|
58
|
+
const direction = submit?.label.match(/\\b(long|short)\\b/iu)?.[1]?.toLowerCase() ||
|
|
59
|
+
String(location.hash).match(/[?&]direction=(long|short)(?:&|$)/u)?.[1] ||
|
|
60
|
+
null;
|
|
61
|
+
return {
|
|
62
|
+
route: location.hash,
|
|
63
|
+
title: document.title,
|
|
64
|
+
items,
|
|
65
|
+
offscreenItems,
|
|
66
|
+
truncated: matched > items.length + offscreenItems.length,
|
|
67
|
+
orderEntry: document.querySelector('[data-testid="perps-order-entry-page"]') ? {
|
|
68
|
+
symbol: field('perps-order-entry-asset-symbol')?.label || null,
|
|
69
|
+
direction,
|
|
70
|
+
orderType: field('order-type-limit')?.pressed === 'true' ? 'limit' : 'market',
|
|
71
|
+
amount: field('amount-input-field')?.value,
|
|
72
|
+
percent: field('balance-percent-input')?.value,
|
|
73
|
+
leverage: field('leverage-input')?.value,
|
|
74
|
+
autoClose: field('auto-close-toggle')?.checked === true,
|
|
75
|
+
takeProfitPrice: field('tp-price-input')?.value,
|
|
76
|
+
stopLossPrice: field('sl-price-input')?.value,
|
|
77
|
+
submit,
|
|
78
|
+
limitPrice: field('limit-price-input')?.value,
|
|
79
|
+
validation: [...document.querySelectorAll('[data-testid$="validation-error"], [role="alert"]')]
|
|
80
|
+
.map((element) => String(element.innerText || '').replace(/\\s+/gu, ' ').trim())
|
|
81
|
+
.filter(Boolean)
|
|
82
|
+
.slice(0, 20),
|
|
83
|
+
} : null,
|
|
84
|
+
layout: {
|
|
85
|
+
viewportWidth: innerWidth,
|
|
86
|
+
viewportHeight: innerHeight,
|
|
87
|
+
contentWidth: Math.max(document.documentElement?.scrollWidth || 0, document.body?.scrollWidth || 0),
|
|
88
|
+
horizontalOverflow: Math.max(document.documentElement?.scrollWidth || 0, document.body?.scrollWidth || 0) > innerWidth + 1,
|
|
89
|
+
clippedTestIds: [...new Set(clippedTestIds)].slice(0, 100),
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
})()`);
|
|
93
|
+
return {
|
|
94
|
+
action: input.action,
|
|
95
|
+
...normalizeVisiblePerpsState(raw, options, 'extension'),
|
|
96
|
+
proofPath: 'visible-dom-accessibility',
|
|
97
|
+
};
|
|
98
|
+
}));
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
|
+
|
|
3
|
+
const FILTERS = new Map([
|
|
4
|
+
['trades', 'trade'],
|
|
5
|
+
['orders', 'order'],
|
|
6
|
+
['funding', 'funding'],
|
|
7
|
+
['deposits', 'deposit'],
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
11
|
+
const filter = String(input.node?.filter ?? '').trim().toLowerCase();
|
|
12
|
+
const option = FILTERS.get(filter);
|
|
13
|
+
if (!option) {
|
|
14
|
+
throw new Error(`metamask.perps.select_activity_filter received unsupported filter ${JSON.stringify(filter)}.`);
|
|
15
|
+
}
|
|
16
|
+
await page.click(dataTestId('perps-activity-filter-button'));
|
|
17
|
+
await page.click(dataTestId(`perps-activity-filter-option-${option}`));
|
|
18
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
19
|
+
const observed = await page.evaluate(`(() => {
|
|
20
|
+
const button = document.querySelector('[data-testid="perps-activity-filter-button"]');
|
|
21
|
+
const entries = [...document.querySelectorAll('[data-testid^="transaction-card-"]')]
|
|
22
|
+
.slice(0, 200)
|
|
23
|
+
.map((element) => String(element.innerText || '').replace(/\\s+/gu, ' ').trim().slice(0, 500));
|
|
24
|
+
return {
|
|
25
|
+
route: String(location.hash),
|
|
26
|
+
selected: String(button?.innerText || '').trim(),
|
|
27
|
+
entries,
|
|
28
|
+
empty: String(document.body?.innerText || '').replace(/\\s+/gu, ' ').trim().slice(0, 2_000),
|
|
29
|
+
};
|
|
30
|
+
})()`);
|
|
31
|
+
if (observed.route !== '#/perps/activity') throw new Error('Perps activity filter left the activity route.');
|
|
32
|
+
if (observed.selected.toLowerCase() !== filter) {
|
|
33
|
+
throw new Error(`Expected Perps activity filter ${filter}, but observed ${observed.selected || 'none'}.`);
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
action: input.action,
|
|
37
|
+
filter,
|
|
38
|
+
selected: observed.selected,
|
|
39
|
+
count: observed.entries.length,
|
|
40
|
+
entries: observed.entries,
|
|
41
|
+
empty: observed.entries.length === 0 ? observed.empty : undefined,
|
|
42
|
+
proofPath: 'trusted-pointer-visible-ui',
|
|
43
|
+
};
|
|
44
|
+
}));
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
|
+
import {
|
|
3
|
+
normalizeVisiblePerpsState,
|
|
4
|
+
resolveVisibleStateOptions,
|
|
5
|
+
} from '../../shared/perps/visible-state.mjs';
|
|
6
|
+
|
|
7
|
+
const FILTERS = new Map([
|
|
8
|
+
['all', 'all'],
|
|
9
|
+
['crypto', 'crypto'],
|
|
10
|
+
['stocks', 'stock'],
|
|
11
|
+
['pre-ipo', 'pre-ipo'],
|
|
12
|
+
['index', 'index'],
|
|
13
|
+
['etf', 'etf'],
|
|
14
|
+
['commodities', 'commodity'],
|
|
15
|
+
['forex', 'forex'],
|
|
16
|
+
['new', 'new'],
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
20
|
+
const filter = String(input.node?.filter ?? '').trim().toLowerCase();
|
|
21
|
+
const option = FILTERS.get(filter);
|
|
22
|
+
if (!option) {
|
|
23
|
+
throw new Error(`metamask.perps.select_market_filter received unsupported filter ${JSON.stringify(filter)}.`);
|
|
24
|
+
}
|
|
25
|
+
const options = resolveVisibleStateOptions({
|
|
26
|
+
surface: 'market-list',
|
|
27
|
+
include_offscreen: true,
|
|
28
|
+
max_items: input.node?.max_items ?? 100,
|
|
29
|
+
minimum_market_count: input.node?.minimum_market_count ?? 1,
|
|
30
|
+
maximum_market_count: input.node?.maximum_market_count ?? 200,
|
|
31
|
+
});
|
|
32
|
+
await page.click(dataTestId('filter-select-button'));
|
|
33
|
+
const optionSelector = dataTestId(`filter-select-option-${option}`);
|
|
34
|
+
const found = await page.evaluate(`(() => Boolean(document.querySelector(${JSON.stringify(optionSelector)})))()`);
|
|
35
|
+
if (!found) {
|
|
36
|
+
throw new Error(`Perps market filter ${JSON.stringify(filter)} is not available in this release.`);
|
|
37
|
+
}
|
|
38
|
+
await page.click(optionSelector);
|
|
39
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
40
|
+
const raw = await page.evaluate(`(() => {
|
|
41
|
+
const maximum = ${JSON.stringify(options.maxItems)};
|
|
42
|
+
const items = [];
|
|
43
|
+
let matched = 0;
|
|
44
|
+
for (const element of document.querySelectorAll('[data-testid]')) {
|
|
45
|
+
const testId = element.getAttribute('data-testid');
|
|
46
|
+
if (!testId || !/^(?:market-list-view|market-row-|perps-token-logo-|filter-select-button)/u.test(testId)) continue;
|
|
47
|
+
const style = getComputedStyle(element);
|
|
48
|
+
const rect = element.getBoundingClientRect();
|
|
49
|
+
if (style.display === 'none' || style.visibility === 'hidden' || rect.width <= 0 || rect.height <= 0) continue;
|
|
50
|
+
matched += 1;
|
|
51
|
+
if (items.length >= maximum) break;
|
|
52
|
+
items.push({
|
|
53
|
+
testId,
|
|
54
|
+
label: String(element.getAttribute('aria-label') || element.innerText || element.getAttribute('value') || '')
|
|
55
|
+
.replace(/\\s+/gu, ' ')
|
|
56
|
+
.trim()
|
|
57
|
+
.slice(0, 500),
|
|
58
|
+
role: element.getAttribute('role') || element.tagName.toLowerCase(),
|
|
59
|
+
enabled: !element.disabled && element.getAttribute('aria-disabled') !== 'true',
|
|
60
|
+
visible: rect.bottom > 0 && rect.right > 0 && rect.top < innerHeight && rect.left < innerWidth,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return { route: location.hash, title: document.title, items, offscreenItems: [], truncated: matched > items.length };
|
|
64
|
+
})()`);
|
|
65
|
+
return {
|
|
66
|
+
action: input.action,
|
|
67
|
+
filter,
|
|
68
|
+
...normalizeVisiblePerpsState(raw, options, 'extension'),
|
|
69
|
+
proofPath: 'trusted-pointer-visible-ui',
|
|
70
|
+
};
|
|
71
|
+
}));
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
|
+
|
|
3
|
+
runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
4
|
+
const market = String(input.node?.market ?? '').trim().toUpperCase();
|
|
5
|
+
if (!/^[A-Z0-9:_-]{2,20}$/u.test(market)) {
|
|
6
|
+
throw new Error('metamask.perps.set_market_favorite requires a valid market symbol.');
|
|
7
|
+
}
|
|
8
|
+
if (typeof input.node?.favorite !== 'boolean') {
|
|
9
|
+
throw new Error('metamask.perps.set_market_favorite requires favorite=true or false.');
|
|
10
|
+
}
|
|
11
|
+
const desired = input.node.favorite;
|
|
12
|
+
const selector = dataTestId('perps-market-detail-favorite-button');
|
|
13
|
+
const readState = () => page.evaluate(`(() => {
|
|
14
|
+
const market = ${JSON.stringify(market)};
|
|
15
|
+
const pair = String(document.querySelector('[data-testid="perps-market-detail-pair"]')?.innerText || '')
|
|
16
|
+
.trim()
|
|
17
|
+
.toUpperCase();
|
|
18
|
+
if (!pair.startsWith(market + '-')) throw new Error('Visible Perps market does not match the requested favorite market.');
|
|
19
|
+
const button = document.querySelector(${JSON.stringify(selector)});
|
|
20
|
+
if (!button) throw new Error('Perps favorite control is not visible.');
|
|
21
|
+
const label = String(button.getAttribute('aria-label') || button.innerText || '').trim();
|
|
22
|
+
return { label, favorite: /^remove from favorites$/iu.test(label) };
|
|
23
|
+
})()`);
|
|
24
|
+
const before = await readState();
|
|
25
|
+
if (before.favorite !== desired) {
|
|
26
|
+
await page.click(selector);
|
|
27
|
+
}
|
|
28
|
+
const deadline = Date.now() + 10_000;
|
|
29
|
+
let after = before;
|
|
30
|
+
while (Date.now() <= deadline) {
|
|
31
|
+
after = await readState();
|
|
32
|
+
if (after.favorite === desired) break;
|
|
33
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
34
|
+
}
|
|
35
|
+
if (after.favorite !== desired) {
|
|
36
|
+
throw new Error(`Perps favorite state did not become ${desired}.`);
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
action: input.action,
|
|
40
|
+
market,
|
|
41
|
+
requestedFavorite: desired,
|
|
42
|
+
changed: before.favorite !== after.favorite,
|
|
43
|
+
before,
|
|
44
|
+
after,
|
|
45
|
+
proofPath: 'trusted-pointer-visible-ui',
|
|
46
|
+
};
|
|
47
|
+
}));
|