@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.
- package/CHANGELOG.md +51 -0
- package/adapters/extension/artifact-runtime-state.cjs +138 -0
- package/adapters/extension/inject.mjs +2 -0
- package/adapters/extension/launch-browser.cjs +24 -0
- package/adapters/extension/live.sh +82 -12
- package/adapters/extension/readiness.mjs +77 -36
- package/adapters/extension/snapshot-dist.sh +88 -3
- package/adapters/extension/verify.sh +6 -2
- package/adapters/manifest.json +17 -1
- package/adapters/shared/log-tui.mjs +1 -1
- package/dist/adapters/extension/artifact-integrity.js +38 -0
- package/dist/adapters/extension/extension-id.js +23 -4
- package/dist/adapters/extension/release-artifact.js +386 -0
- package/dist/adapters/extension/runtime-decision.js +164 -20
- package/dist/adapters/extension/runtime.js +169 -3
- package/dist/adapters/mobile/provision.js +38 -3
- package/dist/adapters/mobile/release-artifact-state.js +124 -0
- package/dist/adapters/mobile/release-artifact.js +295 -0
- package/dist/adapters.js +78 -6
- package/dist/app-lifecycle.js +4 -3
- package/dist/command-contract.js +16 -0
- package/dist/commands/call.js +6 -2
- package/dist/commands/launch/extension.js +74 -9
- package/dist/commands/launch/index.js +3 -0
- package/dist/commands/launch/mobile.js +2 -0
- package/dist/commands/provision.js +2 -0
- package/dist/commands/run-engine.js +109 -14
- package/dist/commands/run.js +4 -2
- package/dist/commands/runtime-launch.js +178 -10
- package/dist/heal-bounds.js +1 -1
- package/dist/live-adapter-contract.js +3 -1
- package/dist/metamask-action-validation.js +47 -1
- package/dist/mm-harness-cli.js +31 -1
- package/dist/recipe-security.js +40 -0
- package/dist/run-diagnostics.js +1 -1
- package/dist/runner.js +3 -0
- package/docs/RELEASE-QA-CAPABILITY-MAP.md +125 -0
- 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 +196 -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_snapshot.mjs +470 -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 +109 -3
- 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/import.mjs +13 -46
- package/library/actions/extension/wallet/lock.mjs +35 -0
- package/library/actions/extension/wallet/secret-input.mjs +98 -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 +86 -2
- 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 +265 -0
- package/library/actions/mobile/ui/navigate.mjs +10 -1
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +17 -0
- package/library/actions/mobile/wallet/import.mjs +71 -2
- package/library/actions/mobile/wallet/native-ui.mjs +512 -0
- package/library/actions/mobile/wallet/read_state.mjs +16 -0
- package/library/actions/mobile/wallet/reset.mjs +17 -4
- 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/actions/shared/ui/locators.mjs +7 -0
- package/library/manifests/extension.action-manifest.json +1366 -184
- package/library/manifests/mobile.action-manifest.json +235 -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/extension/runner/action-validation.recipe.json +12 -1
- 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/library/recipes/wallet/import.recipe.json +20 -1
- package/library/recipes/wallet/reset-import.recipe.json +20 -1
- package/package.json +6 -5
- 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
|
-
|
|
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
|
-
|
|
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
|
+
}));
|