@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
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
|
+
|
|
3
|
+
function marketSymbol(value) {
|
|
4
|
+
const result = String(value ?? '').trim().toUpperCase();
|
|
5
|
+
if (!/^[A-Z0-9:_-]{2,20}$/u.test(result)) {
|
|
6
|
+
throw new Error('metamask.perps.close_visible_position requires a valid market symbol.');
|
|
7
|
+
}
|
|
8
|
+
return result;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
12
|
+
const market = marketSymbol(input.node?.market);
|
|
13
|
+
const partialAmount = input.node?.amount == null ? null : Number(input.node.amount);
|
|
14
|
+
if (partialAmount != null && (!Number.isFinite(partialAmount) || partialAmount <= 0)) {
|
|
15
|
+
throw new Error('metamask.perps.close_visible_position amount must be positive.');
|
|
16
|
+
}
|
|
17
|
+
if (input.node?.confirm_live !== true) {
|
|
18
|
+
throw new Error('metamask.perps.close_visible_position requires confirm_live=true.');
|
|
19
|
+
}
|
|
20
|
+
await page.navigateHash(`#/perps/market/${encodeURIComponent(market)}`);
|
|
21
|
+
await page.waitForSelector(dataTestId('perps-position-size-value'), { timeoutMs: 20_000 });
|
|
22
|
+
const before = await page.evaluate(`(() => {
|
|
23
|
+
const market = ${JSON.stringify(market)};
|
|
24
|
+
const route = String(location.hash);
|
|
25
|
+
const symbol = String(document.querySelector('[data-testid="perps-market-detail-pair"]')?.innerText || '')
|
|
26
|
+
.trim()
|
|
27
|
+
.toUpperCase();
|
|
28
|
+
if (!route.includes('/perps/market/') || !symbol.startsWith(market + '-')) {
|
|
29
|
+
throw new Error('Visible Perps market does not match the requested close market.');
|
|
30
|
+
}
|
|
31
|
+
const read = (testId) => String(document.querySelector('[data-testid="' + testId + '"]')?.innerText || '')
|
|
32
|
+
.replace(/\\s+/gu, ' ')
|
|
33
|
+
.trim()
|
|
34
|
+
.slice(0, 500);
|
|
35
|
+
const size = read('perps-position-size-value');
|
|
36
|
+
if (!size) throw new Error('No visible Perps position is available to close.');
|
|
37
|
+
return {
|
|
38
|
+
route,
|
|
39
|
+
market,
|
|
40
|
+
size,
|
|
41
|
+
direction: read('perps-position-leverage'),
|
|
42
|
+
margin: read('perps-position-margin-value'),
|
|
43
|
+
entryPrice: read('perps-position-entry-value'),
|
|
44
|
+
liquidationPrice: read('perps-position-liquidation-value'),
|
|
45
|
+
};
|
|
46
|
+
})()`);
|
|
47
|
+
let confirmation;
|
|
48
|
+
for (let attempt = 0; attempt < 3 && !confirmation; attempt += 1) {
|
|
49
|
+
await page.click(dataTestId('perps-close-cta-button'));
|
|
50
|
+
const deadline = Date.now() + 3_000;
|
|
51
|
+
while (Date.now() <= deadline) {
|
|
52
|
+
confirmation = await page.evaluate(`(() => {
|
|
53
|
+
const modal = document.querySelector('[data-testid="perps-close-position-modal"]');
|
|
54
|
+
const submit = document.querySelector('[data-testid="perps-close-position-modal-submit"]');
|
|
55
|
+
if (!modal || !submit) return null;
|
|
56
|
+
const read = (testId) => String(document.querySelector('[data-testid="' + testId + '"]')?.innerText || '')
|
|
57
|
+
.replace(/\\s+/gu, ' ')
|
|
58
|
+
.trim()
|
|
59
|
+
.slice(0, 500);
|
|
60
|
+
return {
|
|
61
|
+
amount: String(document.querySelector('[data-testid="close-amount-value"] input')?.value || read('close-amount-value')),
|
|
62
|
+
fullSizeSelected: Boolean(document.querySelector('[data-testid="close-amount-slider-pct-100"]')),
|
|
63
|
+
margin: read('perps-close-summary-margin-value'),
|
|
64
|
+
fees: read('perps-close-summary-fees-value'),
|
|
65
|
+
receive: read('perps-close-summary-receive-value'),
|
|
66
|
+
submitEnabled: !submit.disabled && submit.getAttribute('aria-disabled') !== 'true',
|
|
67
|
+
};
|
|
68
|
+
})()`);
|
|
69
|
+
if (confirmation) break;
|
|
70
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (!confirmation) throw new Error('Perps close confirmation did not become visible.');
|
|
74
|
+
if (partialAmount != null) {
|
|
75
|
+
await page.setInput('input[data-testid="close-amount-value"], [data-testid="close-amount-value"] input', String(partialAmount));
|
|
76
|
+
const readyDeadline = Date.now() + 10_000;
|
|
77
|
+
while (Date.now() <= readyDeadline) {
|
|
78
|
+
confirmation = await page.evaluate(`(() => {
|
|
79
|
+
const submit = document.querySelector('[data-testid="perps-close-position-modal-submit"]');
|
|
80
|
+
return {
|
|
81
|
+
amount: document.querySelector('input[data-testid="close-amount-value"], [data-testid="close-amount-value"] input')?.value ?? null,
|
|
82
|
+
margin: String(document.querySelector('[data-testid="perps-close-summary-margin-value"]')?.innerText || '').trim(),
|
|
83
|
+
fees: String(document.querySelector('[data-testid="perps-close-summary-fees-value"]')?.innerText || '').trim(),
|
|
84
|
+
receive: String(document.querySelector('[data-testid="perps-close-summary-receive-value"]')?.innerText || '').trim(),
|
|
85
|
+
submitEnabled: Boolean(submit && !submit.disabled && submit.getAttribute('aria-disabled') !== 'true'),
|
|
86
|
+
warnings: [...document.querySelectorAll('[role="alert"]')]
|
|
87
|
+
.map((element) => String(element.innerText || '').replace(/\\s+/gu, ' ').trim())
|
|
88
|
+
.filter(Boolean)
|
|
89
|
+
.slice(0, 20),
|
|
90
|
+
};
|
|
91
|
+
})()`);
|
|
92
|
+
if (Number(confirmation.amount) === partialAmount && confirmation.submitEnabled) break;
|
|
93
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (!confirmation.submitEnabled) throw new Error('Perps close confirmation is not submit-ready.');
|
|
97
|
+
await page.click(dataTestId('perps-close-position-modal-submit'));
|
|
98
|
+
const closeDeadline = Date.now() + 45_000;
|
|
99
|
+
let finalState;
|
|
100
|
+
while (Date.now() <= closeDeadline) {
|
|
101
|
+
finalState = await page.evaluate(`(() => ({
|
|
102
|
+
route: String(location.hash),
|
|
103
|
+
positionVisible: Boolean(document.querySelector('[data-testid="perps-position-size-value"]')),
|
|
104
|
+
positionSize: String(document.querySelector('[data-testid="perps-position-size-value"]')?.innerText || '').trim(),
|
|
105
|
+
modalVisible: Boolean(document.querySelector('[data-testid="perps-close-position-modal"]')),
|
|
106
|
+
error: String(document.querySelector('[data-testid="perps-order-submit-error"]')?.innerText || '').trim(),
|
|
107
|
+
activity: [...document.querySelectorAll('[data-testid^="transaction-card-"]')]
|
|
108
|
+
.slice(0, 5)
|
|
109
|
+
.map((element) => String(element.innerText || '').replace(/\\s+/gu, ' ').trim().slice(0, 500)),
|
|
110
|
+
}))()`);
|
|
111
|
+
if (finalState.error) throw new Error(`Perps close failed: ${finalState.error}`);
|
|
112
|
+
if (partialAmount != null) {
|
|
113
|
+
if (!finalState.modalVisible && finalState.positionVisible && finalState.positionSize !== before.size) break;
|
|
114
|
+
} else if (!finalState.positionVisible && !finalState.modalVisible) break;
|
|
115
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
116
|
+
}
|
|
117
|
+
const incomplete = partialAmount != null
|
|
118
|
+
? finalState?.modalVisible || !finalState?.positionVisible || finalState?.positionSize === before.size
|
|
119
|
+
: finalState?.positionVisible || finalState?.modalVisible;
|
|
120
|
+
if (incomplete) {
|
|
121
|
+
throw new Error('Perps position did not reach the requested close state after the timeout.');
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
action: input.action,
|
|
125
|
+
market,
|
|
126
|
+
closed: partialAmount == null,
|
|
127
|
+
partial: partialAmount != null,
|
|
128
|
+
positionBefore: before,
|
|
129
|
+
confirmation,
|
|
130
|
+
finalState,
|
|
131
|
+
proofPath: 'trusted-pointer-visible-ui',
|
|
132
|
+
};
|
|
133
|
+
}));
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { dataTestId, marketSymbol, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
|
+
|
|
3
|
+
function numeric(value) {
|
|
4
|
+
const match = String(value ?? '').replace(/[$,]/gu, '').match(/-?[0-9]+(?:\.[0-9]+)?/u);
|
|
5
|
+
if (!match) return null;
|
|
6
|
+
const parsed = Number(match[0]);
|
|
7
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function compactUsd(value) {
|
|
11
|
+
const parsed = numeric(value);
|
|
12
|
+
if (parsed == null) return null;
|
|
13
|
+
const suffix = String(value).trim().match(/[KMBT](?=$|\s)/u)?.[0];
|
|
14
|
+
return parsed * ({ K: 1e3, M: 1e6, B: 1e9, T: 1e12 }[suffix] ?? 1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function relativeDifference(actual, expected) {
|
|
18
|
+
return Math.abs(actual - expected) / Math.max(Math.abs(expected), 1e-12);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
runAdapter(async (input) => {
|
|
22
|
+
const market = marketSymbol(input);
|
|
23
|
+
if (!market) throw new Error('metamask.perps.compare_provider_market requires market=<symbol>.');
|
|
24
|
+
const endpoint = String(input.node?.endpoint ?? 'https://api.hyperliquid.xyz/info');
|
|
25
|
+
if (endpoint !== 'https://api.hyperliquid.xyz/info') {
|
|
26
|
+
throw new Error('metamask.perps.compare_provider_market endpoint must be the Hyperliquid production info API.');
|
|
27
|
+
}
|
|
28
|
+
const response = await fetch(endpoint, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: { 'content-type': 'application/json' },
|
|
31
|
+
body: JSON.stringify({ type: 'metaAndAssetCtxs' }),
|
|
32
|
+
signal: AbortSignal.timeout(Number(input.node?.timeout_ms ?? 15_000)),
|
|
33
|
+
});
|
|
34
|
+
if (!response.ok) throw new Error(`Hyperliquid market request failed with HTTP ${response.status}.`);
|
|
35
|
+
const payload = await response.json();
|
|
36
|
+
const universe = payload?.[0]?.universe;
|
|
37
|
+
const contexts = payload?.[1];
|
|
38
|
+
const index = Array.isArray(universe) ? universe.findIndex((entry) => entry?.name === market) : -1;
|
|
39
|
+
if (index < 0 || !Array.isArray(contexts) || !contexts[index]) {
|
|
40
|
+
throw new Error(`Hyperliquid production response did not contain ${market}.`);
|
|
41
|
+
}
|
|
42
|
+
const metadata = universe[index];
|
|
43
|
+
const context = contexts[index];
|
|
44
|
+
|
|
45
|
+
return withExtensionPage(input, async (page) => {
|
|
46
|
+
await page.navigateHash(`#/perps/market/${encodeURIComponent(market)}`);
|
|
47
|
+
await page.waitForSelector(dataTestId('perps-market-detail-pair'), { timeoutMs: 20_000 });
|
|
48
|
+
const visible = await page.evaluate(`(() => {
|
|
49
|
+
const read = (testId) => String(document.querySelector('[data-testid="' + testId + '"]')?.innerText || '').replace(/\\s+/gu, ' ').trim();
|
|
50
|
+
const text = read('perps-market-detail-page');
|
|
51
|
+
const after = (label) => text.match(new RegExp(label + '\\\\s+([^\\\\s]+)', 'u'))?.[1] ?? null;
|
|
52
|
+
return {
|
|
53
|
+
route: String(location.hash),
|
|
54
|
+
name: read('perps-market-detail-name'),
|
|
55
|
+
pair: read('perps-market-detail-pair'),
|
|
56
|
+
maxLeverage: read('perps-market-max-leverage'),
|
|
57
|
+
price: read('perps-market-detail-price'),
|
|
58
|
+
volume: after('24h Volume'),
|
|
59
|
+
openInterest: after('Open interest'),
|
|
60
|
+
fundingRate: after('Funding rate'),
|
|
61
|
+
oraclePrice: read('perps-market-detail-oracle-price'),
|
|
62
|
+
favoriteLabel: String(document.querySelector('[data-testid="perps-market-detail-favorite-button"]')?.getAttribute('aria-label') || '').trim(),
|
|
63
|
+
};
|
|
64
|
+
})()`);
|
|
65
|
+
const provider = {
|
|
66
|
+
symbol: metadata.name,
|
|
67
|
+
maxLeverage: Number(metadata.maxLeverage),
|
|
68
|
+
markPrice: Number(context.markPx),
|
|
69
|
+
oraclePrice: Number(context.oraclePx),
|
|
70
|
+
volume24hUsd: Number(context.dayNtlVlm),
|
|
71
|
+
openInterestUsd: Number(context.openInterest) * Number(context.markPx),
|
|
72
|
+
fundingRate: Number(context.funding),
|
|
73
|
+
};
|
|
74
|
+
const observed = {
|
|
75
|
+
price: numeric(visible.price),
|
|
76
|
+
oraclePrice: numeric(visible.oraclePrice),
|
|
77
|
+
volume24hUsd: compactUsd(visible.volume),
|
|
78
|
+
openInterestUsd: compactUsd(visible.openInterest),
|
|
79
|
+
fundingRate: numeric(visible.fundingRate) == null ? null : numeric(visible.fundingRate) / 100,
|
|
80
|
+
maxLeverage: numeric(visible.maxLeverage),
|
|
81
|
+
};
|
|
82
|
+
const checks = {
|
|
83
|
+
route: visible.route === `#/perps/market/${market}`,
|
|
84
|
+
symbol: visible.pair.startsWith(`${market}-`),
|
|
85
|
+
name: visible.name.length > 0,
|
|
86
|
+
maxLeverage: observed.maxLeverage === provider.maxLeverage,
|
|
87
|
+
price: observed.price != null && relativeDifference(observed.price, provider.markPrice) <= 0.02,
|
|
88
|
+
oraclePrice: observed.oraclePrice != null && relativeDifference(observed.oraclePrice, provider.oraclePrice) <= 0.02,
|
|
89
|
+
volume24hUsd: observed.volume24hUsd != null && relativeDifference(observed.volume24hUsd, provider.volume24hUsd) <= 0.05,
|
|
90
|
+
openInterestUsd: observed.openInterestUsd != null && relativeDifference(observed.openInterestUsd, provider.openInterestUsd) <= 0.05,
|
|
91
|
+
fundingRate: observed.fundingRate != null && Math.abs(observed.fundingRate - provider.fundingRate) <= 0.00001,
|
|
92
|
+
favoriteState: /favorites?/iu.test(visible.favoriteLabel),
|
|
93
|
+
};
|
|
94
|
+
const failed = Object.entries(checks).filter(([, passed]) => !passed).map(([name]) => name);
|
|
95
|
+
if (failed.length > 0) {
|
|
96
|
+
throw new Error(`Visible ${market} market differs from the production provider for: ${failed.join(', ')}.`);
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
action: input.action,
|
|
100
|
+
market,
|
|
101
|
+
endpoint,
|
|
102
|
+
visible,
|
|
103
|
+
observed,
|
|
104
|
+
provider,
|
|
105
|
+
checks,
|
|
106
|
+
proofPath: 'visible-ui-plus-live-provider',
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { dataTestId, marketSymbol, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
|
+
|
|
3
|
+
runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
4
|
+
const market = marketSymbol(input);
|
|
5
|
+
const mode = String(input.node?.margin_action ?? 'add').trim().toLowerCase();
|
|
6
|
+
const amount = Number(input.node?.amount);
|
|
7
|
+
if (!market) throw new Error('metamask.perps.edit_margin requires market=<symbol>.');
|
|
8
|
+
if (mode !== 'add' && mode !== 'remove') {
|
|
9
|
+
throw new Error(`metamask.perps.edit_margin received unsupported margin_action ${JSON.stringify(mode)}.`);
|
|
10
|
+
}
|
|
11
|
+
if (!Number.isFinite(amount) || amount <= 0) {
|
|
12
|
+
throw new Error('metamask.perps.edit_margin requires a positive amount.');
|
|
13
|
+
}
|
|
14
|
+
if (input.node?.confirm_live !== true) {
|
|
15
|
+
throw new Error('metamask.perps.edit_margin requires confirm_live=true for live submission.');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
await page.navigateHash(`#/perps/market/${encodeURIComponent(market)}`);
|
|
19
|
+
await page.waitForSelector(dataTestId('perps-position-margin-value'), { timeoutMs: 20_000 });
|
|
20
|
+
const before = await page.evaluate(`(() => ({
|
|
21
|
+
margin: String(document.querySelector('[data-testid="perps-position-margin-value"]')?.textContent || '').trim(),
|
|
22
|
+
liquidation: String(document.querySelector('[data-testid="perps-position-liquidation-value"]')?.textContent || '').trim(),
|
|
23
|
+
}))()`);
|
|
24
|
+
await page.click(dataTestId('perps-margin-card'));
|
|
25
|
+
const menuItem = mode === 'add' ? 'perps-margin-menu-add' : 'perps-margin-menu-remove';
|
|
26
|
+
await page.waitForSelector(dataTestId(menuItem), { timeoutMs: 10_000 });
|
|
27
|
+
await page.click(dataTestId(menuItem));
|
|
28
|
+
const modal = mode === 'add' ? 'perps-add-margin-modal' : 'perps-decrease-margin-modal';
|
|
29
|
+
await page.waitForSelector(dataTestId(modal), { timeoutMs: 10_000 });
|
|
30
|
+
await page.setInput(`${dataTestId('perps-edit-margin-amount-input')} input`, String(amount));
|
|
31
|
+
|
|
32
|
+
const saveDeadline = Date.now() + 10_000;
|
|
33
|
+
let ready;
|
|
34
|
+
while (Date.now() <= saveDeadline) {
|
|
35
|
+
ready = await page.evaluate(`(() => {
|
|
36
|
+
const input = document.querySelector('[data-testid="perps-edit-margin-amount-input"] input');
|
|
37
|
+
const save = document.querySelector('[data-testid="perps-edit-margin-modal-save"]');
|
|
38
|
+
return {
|
|
39
|
+
amount: input?.value ?? null,
|
|
40
|
+
saveEnabled: Boolean(save && !save.disabled && save.getAttribute('aria-disabled') !== 'true'),
|
|
41
|
+
available: String(document.querySelector('[data-testid="perps-edit-margin-available-value"]')?.textContent || '').trim(),
|
|
42
|
+
liquidation: String(document.querySelector('[data-testid="perps-edit-margin-liquidation-price-value"]')?.textContent || '').trim(),
|
|
43
|
+
};
|
|
44
|
+
})()`);
|
|
45
|
+
if (ready.saveEnabled) break;
|
|
46
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
47
|
+
}
|
|
48
|
+
if (Number(ready?.amount) !== amount || !ready?.saveEnabled) {
|
|
49
|
+
throw new Error(`Perps margin update is not ready: ${JSON.stringify(ready)}.`);
|
|
50
|
+
}
|
|
51
|
+
await page.click(dataTestId('perps-edit-margin-modal-save'));
|
|
52
|
+
|
|
53
|
+
const resultDeadline = Date.now() + Number(input.node?.timeout_ms ?? 30_000);
|
|
54
|
+
let after;
|
|
55
|
+
while (Date.now() <= resultDeadline) {
|
|
56
|
+
after = await page.evaluate(`(() => ({
|
|
57
|
+
modalVisible: Boolean(document.querySelector(${JSON.stringify(dataTestId(modal))})),
|
|
58
|
+
margin: String(document.querySelector('[data-testid="perps-position-margin-value"]')?.textContent || '').trim(),
|
|
59
|
+
liquidation: String(document.querySelector('[data-testid="perps-position-liquidation-value"]')?.textContent || '').trim(),
|
|
60
|
+
errors: [...document.querySelectorAll('[role="alert"]')]
|
|
61
|
+
.map((element) => String(element.innerText || '').replace(/\\s+/gu, ' ').trim())
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.slice(0, 20),
|
|
64
|
+
}))()`);
|
|
65
|
+
if (!after.modalVisible && after.margin && after.margin !== before.margin) break;
|
|
66
|
+
if (after.errors.length > 0) throw new Error(`Perps margin update failed: ${after.errors.join(' | ')}`);
|
|
67
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
68
|
+
}
|
|
69
|
+
if (!after || after.modalVisible || after.margin === before.margin) {
|
|
70
|
+
throw new Error(`Perps margin did not reconcile after submission: ${JSON.stringify({ before, after })}.`);
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
action: input.action,
|
|
74
|
+
market,
|
|
75
|
+
marginAction: mode,
|
|
76
|
+
amount,
|
|
77
|
+
before,
|
|
78
|
+
ready,
|
|
79
|
+
after,
|
|
80
|
+
proofPath: 'visible-product-controls',
|
|
81
|
+
};
|
|
82
|
+
}));
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
|
+
|
|
3
|
+
const ACTIONS = new Map([
|
|
4
|
+
['add-funds', 'perps-balance-dropdown-add-funds'],
|
|
5
|
+
['withdraw', 'perps-balance-dropdown-withdraw'],
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
9
|
+
const balanceAction = String(input.node?.balance_action ?? '').trim().toLowerCase();
|
|
10
|
+
const actionTestId = ACTIONS.get(balanceAction);
|
|
11
|
+
if (!actionTestId) {
|
|
12
|
+
throw new Error(`metamask.perps.open_balance_action received unsupported balance_action ${JSON.stringify(balanceAction)}.`);
|
|
13
|
+
}
|
|
14
|
+
await page.click(dataTestId('perps-balance-dropdown-balance'));
|
|
15
|
+
const actionSelector = dataTestId(actionTestId);
|
|
16
|
+
const deadline = Date.now() + 10_000;
|
|
17
|
+
while (Date.now() <= deadline) {
|
|
18
|
+
const present = await page.evaluate(`(() => Boolean(document.querySelector(${JSON.stringify(actionSelector)})))()`);
|
|
19
|
+
if (present) break;
|
|
20
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
21
|
+
}
|
|
22
|
+
await page.click(actionSelector);
|
|
23
|
+
const expected = balanceAction === 'withdraw'
|
|
24
|
+
? ['perps-withdraw-page', 'perps-withdraw-container', 'perps-withdraw-view']
|
|
25
|
+
: ['confirm-page-container-content', 'confirm-page-container', 'transaction-confirmation'];
|
|
26
|
+
const resultDeadline = Date.now() + 15_000;
|
|
27
|
+
let observed;
|
|
28
|
+
while (Date.now() <= resultDeadline) {
|
|
29
|
+
observed = await page.evaluate(`(() => {
|
|
30
|
+
const expected = ${JSON.stringify(expected)};
|
|
31
|
+
const visibleTestIds = [...document.querySelectorAll('[data-testid]')]
|
|
32
|
+
.filter((element) => {
|
|
33
|
+
const style = getComputedStyle(element);
|
|
34
|
+
const rect = element.getBoundingClientRect();
|
|
35
|
+
return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
|
|
36
|
+
})
|
|
37
|
+
.map((element) => element.getAttribute('data-testid'))
|
|
38
|
+
.filter(Boolean);
|
|
39
|
+
return {
|
|
40
|
+
route: String(location.hash),
|
|
41
|
+
matched: expected.filter((testId) => visibleTestIds.includes(testId)),
|
|
42
|
+
visibleTestIds: visibleTestIds.filter((testId) => /(?:perps|confirm|transaction|amount|token|network)/u.test(testId)).slice(0, 150),
|
|
43
|
+
text: String(document.body?.innerText || '').replace(/\\s+/gu, ' ').trim().slice(0, 10_000),
|
|
44
|
+
};
|
|
45
|
+
})()`);
|
|
46
|
+
if (observed.matched.length > 0 || observed.route !== '#/perps-home') break;
|
|
47
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
48
|
+
}
|
|
49
|
+
if (!observed || (observed.matched.length === 0 && observed.route === '#/perps-home')) {
|
|
50
|
+
throw new Error(`Perps ${balanceAction} did not open: ${JSON.stringify(observed)}.`);
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
action: input.action,
|
|
54
|
+
balanceAction,
|
|
55
|
+
...observed,
|
|
56
|
+
proofPath: 'trusted-pointer-visible-ui',
|
|
57
|
+
};
|
|
58
|
+
}));
|
|
@@ -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
|
+
}));
|