@deeeed/metamask-harness 0.47.4 → 0.48.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 +6 -0
- package/README.md +3 -2
- package/adapters/extension/lib/macos-focus.cjs +2 -2
- package/adapters/extension/live.sh +6 -8
- package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +1 -0
- package/adapters/shared/ensure-runner-deps.sh +6 -0
- package/dist/adapters/extension/runtime-decision.js +18 -1
- package/dist/adapters/extension/runtime.js +5 -5
- package/dist/adapters/extension/surface.js +1 -0
- package/dist/app-lifecycle.js +3 -1
- package/dist/command-contract.js +2 -0
- package/dist/commands/call.js +1 -8
- package/dist/commands/device-target.js +12 -3
- package/dist/commands/doctor.js +7 -5
- package/dist/commands/fixtures.js +1 -1
- package/dist/commands/launch/index.js +10 -0
- package/dist/commands/manifest.js +3 -1
- package/dist/commands/mobile-device-view.js +13 -3
- package/dist/commands/parse-args.js +2 -0
- package/dist/commands/run-engine.js +29 -4
- package/dist/commands/run.js +1 -1
- package/dist/commands/runtime-launch.js +10 -1
- package/dist/commands/status.js +8 -4
- package/dist/devices.js +31 -9
- package/dist/execution-provenance.js +8 -7
- package/dist/funding-execution-context.js +43 -13
- package/dist/heal-bounds.js +3 -3
- package/dist/live-adapter-contract.js +4 -0
- package/dist/manifest.js +2 -13
- package/dist/mm-harness-cli.js +2 -1
- package/dist/runner.js +14 -3
- package/docs/RECIPES.md +133 -22
- package/docs/RELEASE-QA-CAPABILITY-MAP.md +3 -2
- package/library/actions/core/perps/_controller.mjs +10 -0
- package/library/actions/core/perps/close_orders.mjs +13 -17
- package/library/actions/extension/assets/home-token-identity.mjs +30 -0
- package/library/actions/extension/assets/import_custom_token.mjs +61 -79
- package/library/actions/extension/assets/open_details.mjs +18 -7
- package/library/actions/extension/assets/read_details.mjs +61 -29
- package/library/actions/extension/assets/read_visible_state.mjs +4 -1
- package/library/actions/extension/assets/select_network_scope.mjs +16 -9
- package/library/actions/extension/assets/set_token_visibility.mjs +84 -17
- package/library/actions/extension/assets/verify_sorting.mjs +62 -14
- package/library/actions/extension/networks/add_custom.mjs +19 -17
- package/library/actions/extension/networks/custom_network.mjs +8 -3
- package/library/actions/extension/networks/read_visible_state.mjs +1 -0
- package/library/actions/extension/performance/_navigation-memory.mjs +35 -6
- package/library/actions/extension/performance/compare_idle_navigation_memory.mjs +3 -2
- package/library/actions/extension/performance/measure_detached_dom.mjs +3 -2
- package/library/actions/extension/performance/measure_navigation_memory.mjs +3 -2
- package/library/actions/extension/perps/assert_visible_consistency.mjs +9 -4
- package/library/actions/extension/perps/close_visible_position.mjs +3 -3
- package/library/actions/extension/perps/edit_margin.mjs +1 -1
- package/library/actions/extension/perps/open_balance_action.mjs +16 -7
- package/library/actions/extension/perps/perps.mjs +150 -45
- package/library/actions/extension/perps/read_funds_confirmation.mjs +8 -0
- package/library/actions/extension/perps/read_snapshot.mjs +2 -1
- package/library/actions/extension/perps/read_visible_state.mjs +10 -6
- package/library/actions/extension/perps/search_markets.mjs +7 -2
- package/library/actions/extension/perps/select_activity_filter.mjs +1 -1
- package/library/actions/extension/perps/select_market_filter.mjs +74 -45
- package/library/actions/extension/perps/set_market_favorite.mjs +6 -3
- package/library/actions/extension/perps/state.mjs +1 -0
- package/library/actions/extension/perps/update_position_tpsl.mjs +1 -1
- package/library/actions/extension/perps/visible-mutation-identity.mjs +4 -2
- package/library/actions/extension/platform/cdp.mjs +23 -142
- package/library/actions/extension/swap_bridge/quote-state.mjs +54 -0
- package/library/actions/extension/swap_bridge/read_visible_state.mjs +4 -1
- package/library/actions/extension/swap_bridge/select_assets.mjs +11 -1
- package/library/actions/extension/swap_bridge/set_amount.mjs +10 -8
- package/library/actions/extension/swap_bridge/set_max_amount.mjs +17 -18
- package/library/actions/extension/swap_bridge/set_slippage.mjs +0 -9
- package/library/actions/extension/ui/navigate.mjs +58 -14
- package/library/actions/extension/wallet/lock.mjs +2 -1
- package/library/actions/mobile/assets/import_custom_token.mjs +1 -0
- package/library/actions/mobile/assets/read_visible_state.mjs +3 -1
- package/library/actions/mobile/assets/set_token_visibility.mjs +13 -9
- package/library/actions/mobile/networks/network-management.mjs +22 -15
- package/library/actions/mobile/networks/remove_custom.mjs +6 -2
- package/library/actions/mobile/perps/perps.mjs +3 -0
- package/library/actions/mobile/perps/read-visible-state-loop.mjs +1 -0
- package/library/actions/mobile/perps/set_market_favorite.mjs +15 -19
- package/library/actions/mobile/platform/native-session.mjs +26 -17
- package/library/actions/mobile/platform/observe-ui.mjs +2 -2
- package/library/actions/mobile/ui/navigate.mjs +1 -1
- package/library/actions/shared/perps/visible-state.mjs +14 -3
- package/library/manifests/core.action-manifest.json +14 -2
- package/library/manifests/extension.action-manifest.json +49 -16
- package/library/manifests/mobile.action-manifest.json +8 -7
- package/library/recipes/core/perps/smoke.recipe.json +23 -0
- package/library/recipes/core/perps/snapshot.recipe.json +26 -1
- package/library/recipes/extension/assets/release-custom-network-token-persistence.recipe.json +25 -8
- package/library/recipes/extension/performance/navigation-memory-suite.recipe.json +219 -0
- package/library/recipes/extension/performance/navigation-memory.recipe.json +61 -156
- package/library/recipes/extension/wallet/smoke.recipe.json +17 -8
- package/library/recipes/mobile/app/lifecycle.android-smoke.recipe.json +23 -4
- package/library/recipes/mobile/networks/release-custom-network-persistence.recipe.json +31 -6
- package/library/recipes/mobile/networks/release-multichain-visibility.recipe.json +5 -4
- package/library/recipes/mobile/wallet/smoke.recipe.json +17 -8
- package/library/recipes/{assets → shared/assets}/release-token-address-import.recipe.json +7 -0
- package/library/recipes/{assets → shared/assets}/release-token-details.recipe.json +2 -2
- package/library/recipes/shared/assets/release-token-hide-readd.recipe.json +91 -0
- package/library/recipes/{assets → shared/assets}/release-token-persistence.recipe.json +3 -0
- package/library/recipes/{assets → shared/assets}/release-token-search-import.recipe.json +4 -1
- package/library/recipes/{assets → shared/assets}/release-token-sorting.recipe.json +13 -2
- package/library/recipes/{swap-bridge → shared/swap-bridge}/release-quote.recipe.json +29 -3
- package/package.json +3 -3
- package/site/assets/help-recipes.json +63 -23
- package/library/recipes/assets/release-token-hide-readd.recipe.json +0 -57
- package/library/recipes/core/perps/clean-market-testnet.recipe.json +0 -44
- package/library/recipes/core/perps/order-lifecycle.recipe.json +0 -82
- package/library/recipes/core/perps/read-markets.recipe.json +0 -36
- package/library/recipes/core/perps/trading-lifecycle.recipe.json +0 -80
- package/library/recipes/extension/perps/release-activity-filters.recipe.json +0 -51
- package/library/recipes/extension/perps/release-funds-flows.recipe.json +0 -76
- package/library/recipes/extension/perps/release-live-limit-order.recipe.json +0 -109
- package/library/recipes/extension/perps/release-market-filters.recipe.json +0 -97
- package/library/recipes/extension/perps/release-order-entry.recipe.json +0 -158
- package/library/recipes/extension/perps/release-order-validation.recipe.json +0 -259
- package/library/recipes/extension/perps/release-visible-consistency.recipe.json +0 -47
- package/library/recipes/extension/perps/source-dev-snapshot-consistency.recipe.json +0 -76
- package/library/recipes/mobile/perps/chase-assert-running.recipe.json +0 -110
- package/library/recipes/mobile/perps/chase-place.recipe.json +0 -145
- package/library/recipes/mobile/perps/chase-terminate.recipe.json +0 -98
- package/library/recipes/mobile/perps/performance.recipe.json +0 -722
- package/library/recipes/mobile/perps/pro-order-setup.recipe.json +0 -111
- package/library/recipes/mobile/perps/pro-order-start-state.recipe.json +0 -53
- package/library/recipes/mobile/perps/scale-assert-orders.recipe.json +0 -126
- package/library/recipes/mobile/perps/scale-place.recipe.json +0 -186
- package/library/recipes/mobile/perps/twap-assert-active.recipe.json +0 -97
- package/library/recipes/mobile/perps/twap-place.recipe.json +0 -146
- package/library/recipes/mobile/runner/action-validation.recipe.json +0 -336
- package/library/recipes/perps/clean-market-testnet.recipe.json +0 -49
- package/library/recipes/perps/lifecycle.recipe.json +0 -144
- package/library/recipes/perps/release-live-market-order.recipe.json +0 -139
- package/library/recipes/perps/release-watchlist.recipe.json +0 -111
- package/library/recipes/swap-bridge/release-max-quote.recipe.json +0 -72
- /package/library/recipes/{assets → shared/assets}/release-token-list.recipe.json +0 -0
- /package/library/recipes/{perps → shared/perps}/release-market-details.recipe.json +0 -0
- /package/library/recipes/{perps → shared/perps}/release-market-search.recipe.json +0 -0
- /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-custom-slippage.recipe.json +0 -0
- /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-live-max-swap.recipe.json +0 -0
- /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-live-swap.recipe.json +0 -0
- /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-surface.recipe.json +0 -0
- /package/library/recipes/{wallet → shared/wallet}/import.recipe.json +0 -0
- /package/library/recipes/{wallet → shared/wallet}/reset-import.recipe.json +0 -0
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { pathToFileURL } from 'node:url';
|
|
2
2
|
|
|
3
3
|
import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
4
|
-
import {
|
|
5
|
-
normalizeVisiblePerpsState,
|
|
6
|
-
resolveVisibleStateOptions,
|
|
7
|
-
} from '../../shared/perps/visible-state.mjs';
|
|
4
|
+
import { readVisiblePerpsState } from './read_visible_state.mjs';
|
|
8
5
|
|
|
9
6
|
const FILTERS = new Map([
|
|
10
7
|
['all', 'all'],
|
|
@@ -34,21 +31,7 @@ async function visibleLabel(page, selector) {
|
|
|
34
31
|
})()`);
|
|
35
32
|
}
|
|
36
33
|
|
|
37
|
-
|
|
38
|
-
const filter = String(input.node?.filter ?? '').trim().toLowerCase();
|
|
39
|
-
const option = FILTERS.get(filter);
|
|
40
|
-
if (!option) {
|
|
41
|
-
throw new Error(`metamask.perps.select_market_filter received unsupported filter ${JSON.stringify(filter)}.`);
|
|
42
|
-
}
|
|
43
|
-
const options = resolveVisibleStateOptions({
|
|
44
|
-
surface: 'market-list',
|
|
45
|
-
include_offscreen: true,
|
|
46
|
-
max_items: input.node?.max_items ?? 100,
|
|
47
|
-
minimum_market_count: input.node?.minimum_market_count ?? 1,
|
|
48
|
-
maximum_market_count: input.node?.maximum_market_count ?? 200,
|
|
49
|
-
});
|
|
50
|
-
const timeoutMs = pollOptions.timeoutMs ?? input.node?.timeout_ms ?? 10_000;
|
|
51
|
-
const pollIntervalMs = pollOptions.pollIntervalMs ?? 100;
|
|
34
|
+
async function selectDropdownFilter(page, filter, option, timeoutMs, pollIntervalMs) {
|
|
52
35
|
await page.click(dataTestId('filter-select-button'));
|
|
53
36
|
const optionSelector = dataTestId(`filter-select-option-${option}`);
|
|
54
37
|
try {
|
|
@@ -76,37 +59,83 @@ export async function selectMarketFilter(page, input, pollOptions = {}) {
|
|
|
76
59
|
}
|
|
77
60
|
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
78
61
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
62
|
+
return { selectedLabel: selectedState.label, controlKind: 'dropdown', alreadySelected: false };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function categoryRailState(page) {
|
|
66
|
+
return page.evaluate(`(() => ({
|
|
67
|
+
active: [...document.querySelectorAll('[data-testid^="market-list-categories-pill-"][aria-pressed="true"]')].map((element) => ({
|
|
68
|
+
id: element.getAttribute('data-testid'),
|
|
69
|
+
option: element.getAttribute('data-testid').slice('market-list-categories-pill-'.length),
|
|
70
|
+
label: String(element.innerText || '').trim(),
|
|
71
|
+
})),
|
|
72
|
+
watchlist: document.querySelector('[data-testid="market-list-watchlist-toggle"]')?.getAttribute('aria-pressed') === 'true',
|
|
73
|
+
}))()`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function selectCategoryRail(page, option, timeoutMs, pollIntervalMs) {
|
|
77
|
+
const deadline = Date.now() + timeoutMs;
|
|
78
|
+
const matches = (state) => !state.watchlist && (option === 'all'
|
|
79
|
+
? state.active.length === 0
|
|
80
|
+
: state.active.length === 1 && state.active[0].option === option);
|
|
81
|
+
const initial = await categoryRailState(page);
|
|
82
|
+
const alreadySelected = matches(initial);
|
|
83
|
+
if (!alreadySelected) {
|
|
84
|
+
if (option === 'all') {
|
|
85
|
+
if (initial.watchlist) await page.click(dataTestId('market-list-watchlist-toggle'));
|
|
86
|
+
for (const selected of initial.active) await page.click(dataTestId(selected.id));
|
|
87
|
+
} else {
|
|
88
|
+
let selector = dataTestId(`market-list-categories-pill-${option}`);
|
|
89
|
+
if (!(await visibleLabel(page, selector)).visible) {
|
|
90
|
+
const more = dataTestId('market-list-categories-more-button');
|
|
91
|
+
selector = dataTestId(`market-list-categories-more-option-${option}`);
|
|
92
|
+
if (!(await visibleLabel(page, selector)).visible) {
|
|
93
|
+
if (!(await visibleLabel(page, more)).visible) {
|
|
94
|
+
throw new Error(`Perps market filter ${JSON.stringify(option)} is not available in this release.`);
|
|
95
|
+
}
|
|
96
|
+
await page.click(more);
|
|
97
|
+
}
|
|
98
|
+
await page.waitForSelector(selector, { timeoutMs: Math.max(1, deadline - Date.now()) });
|
|
99
|
+
}
|
|
100
|
+
await page.click(selector);
|
|
101
101
|
}
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
}
|
|
103
|
+
let selected = await categoryRailState(page);
|
|
104
|
+
while (!matches(selected)) {
|
|
105
|
+
if (Date.now() >= deadline) {
|
|
106
|
+
throw new Error(`Perps category ${JSON.stringify(option)} did not become selected: ${JSON.stringify(selected)}.`);
|
|
107
|
+
}
|
|
108
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
109
|
+
selected = await categoryRailState(page);
|
|
110
|
+
}
|
|
111
|
+
return { selectedLabel: selected.active[0]?.label ?? null, controlKind: 'category-rail', alreadySelected };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function selectMarketFilter(page, input, pollOptions = {}) {
|
|
115
|
+
const filter = String(input.node?.filter ?? '').trim().toLowerCase();
|
|
116
|
+
const option = FILTERS.get(filter);
|
|
117
|
+
if (!option) {
|
|
118
|
+
throw new Error(`metamask.perps.select_market_filter received unsupported filter ${JSON.stringify(filter)}.`);
|
|
119
|
+
}
|
|
120
|
+
const timeoutMs = pollOptions.timeoutMs ?? input.node?.timeout_ms ?? 10_000;
|
|
121
|
+
const pollIntervalMs = pollOptions.pollIntervalMs ?? 100;
|
|
122
|
+
const rail = await visibleLabel(page, dataTestId('market-list-categories'));
|
|
123
|
+
const selection = rail.visible
|
|
124
|
+
? await selectCategoryRail(page, option, timeoutMs, pollIntervalMs)
|
|
125
|
+
: await selectDropdownFilter(page, filter, option, timeoutMs, pollIntervalMs);
|
|
126
|
+
const state = await readVisiblePerpsState(page, {
|
|
127
|
+
surface: 'market-list',
|
|
128
|
+
include_offscreen: true,
|
|
129
|
+
max_items: input.node?.max_items ?? 100,
|
|
130
|
+
minimum_market_count: input.node?.minimum_market_count ?? 1,
|
|
131
|
+
maximum_market_count: input.node?.maximum_market_count ?? 200,
|
|
132
|
+
});
|
|
104
133
|
return {
|
|
105
134
|
action: input.action,
|
|
106
135
|
filter,
|
|
107
136
|
option,
|
|
108
|
-
|
|
109
|
-
...
|
|
137
|
+
...selection,
|
|
138
|
+
...state,
|
|
110
139
|
proofPath: 'trusted-pointer-visible-ui',
|
|
111
140
|
};
|
|
112
141
|
}
|
|
@@ -20,9 +20,15 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
20
20
|
const button = document.querySelector(${JSON.stringify(selector)});
|
|
21
21
|
if (!button) throw new Error('Perps favorite control is not visible.');
|
|
22
22
|
const label = String(button.getAttribute('aria-label') || button.innerText || '').trim();
|
|
23
|
+
if (!/^(?:add to|remove from) favorites$/iu.test(label)) {
|
|
24
|
+
throw new Error('Perps favorite control has an unsupported label: ' + JSON.stringify(label));
|
|
25
|
+
}
|
|
23
26
|
return { label, favorite: /^remove from favorites$/iu.test(label) };
|
|
24
27
|
})()`);
|
|
25
28
|
const before = await readState();
|
|
29
|
+
if (requireUnchanged && before.favorite !== desired) {
|
|
30
|
+
throw new Error(`Perps favorite state for ${market} did not persist across navigation; no change was made.`);
|
|
31
|
+
}
|
|
26
32
|
if (before.favorite !== desired) {
|
|
27
33
|
await page.click(selector);
|
|
28
34
|
}
|
|
@@ -37,9 +43,6 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
37
43
|
throw new Error(`Perps favorite state did not become ${desired}.`);
|
|
38
44
|
}
|
|
39
45
|
const changed = before.favorite !== after.favorite;
|
|
40
|
-
if (requireUnchanged && changed) {
|
|
41
|
-
throw new Error(`Perps favorite state for ${market} did not persist across navigation.`);
|
|
42
|
-
}
|
|
43
46
|
return {
|
|
44
47
|
action: input.action,
|
|
45
48
|
market,
|
|
@@ -11,6 +11,7 @@ export function readPerpsRuntimeStateExpression() {
|
|
|
11
11
|
return {
|
|
12
12
|
available: typeof request === 'function',
|
|
13
13
|
activeProvider: metamask.activeProvider || nestedPerps.activeProvider || 'hyperliquid',
|
|
14
|
+
providerKnown: typeof (metamask.activeProvider ?? nestedPerps.activeProvider) === 'string',
|
|
14
15
|
networkKnown: typeof metamask.isTestnet === 'boolean' || typeof nestedPerps.isTestnet === 'boolean',
|
|
15
16
|
isTestnet: Boolean(metamask.isTestnet ?? nestedPerps.isTestnet),
|
|
16
17
|
initializationState: metamask.initializationState || nestedPerps.initializationState || null,
|
|
@@ -229,7 +229,6 @@ export async function updatePositionTpsl(input) {
|
|
|
229
229
|
let dispatchTarget;
|
|
230
230
|
try {
|
|
231
231
|
await requireCurrentVisibleFundingBalance(input, page, timeoutMs, fundingBalance.observedBaseUnits);
|
|
232
|
-
await requirePerpsTradingContext(input, page);
|
|
233
232
|
assertTrustedExternalMutationTarget(input, page, identity);
|
|
234
233
|
const current = await readPosition(page, market);
|
|
235
234
|
if (current.size !== before.size || current.direction !== before.direction
|
|
@@ -246,6 +245,7 @@ export async function updatePositionTpsl(input) {
|
|
|
246
245
|
const rect = element.getBoundingClientRect();
|
|
247
246
|
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
|
|
248
247
|
})()`);
|
|
248
|
+
await requirePerpsTradingContext(input, page);
|
|
249
249
|
assertTrustedExternalMutationTarget(input, page, identity);
|
|
250
250
|
} catch (error) {
|
|
251
251
|
throw externalMutationNotDispatched(error instanceof Error ? error.message : String(error));
|
|
@@ -129,10 +129,12 @@ export async function waitForStableGlobalPerpsHome(page, options = {}) {
|
|
|
129
129
|
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
|
|
130
130
|
};
|
|
131
131
|
const visibleCount = (selector) => [...document.querySelectorAll(selector)].filter(visible).length;
|
|
132
|
-
const balance = document.querySelector('[data-testid="perps-balance-
|
|
132
|
+
const balance = document.querySelector('[data-testid="perps-balance-actions-available-value"]')
|
|
133
|
+
?? document.querySelector('[data-testid="perps-balance-dropdown-balance"]');
|
|
133
134
|
return {
|
|
134
135
|
route: String(location.hash),
|
|
135
|
-
viewVisible: visible(document.querySelector('[data-testid="perps-
|
|
136
|
+
viewVisible: visible(document.querySelector('[data-testid="perps-home-page"]')
|
|
137
|
+
?? document.querySelector('[data-testid="perps-view"]')),
|
|
136
138
|
loading: visibleCount('[data-testid$="-skeleton"], [data-testid*="loading"]') > 0,
|
|
137
139
|
marketCount: visibleCount('[data-testid="perps-explore-markets-row"]'),
|
|
138
140
|
positionCount: visibleCount('[data-testid^="position-card-"]:not([data-testid^="position-card-roe-"])'),
|
|
@@ -240,32 +240,31 @@ async function captureHelperBrowserPid(context, port) {
|
|
|
240
240
|
);
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
async function
|
|
243
|
+
async function captureCdpSnapshot(page, context, relPath, metadata, captureHelperError = null) {
|
|
244
244
|
const { relative, absolute, artifactsRoot } = resolveRelativeArtifactPath(context.artifactsDir, relPath);
|
|
245
245
|
const destination = await preparePrivateEvidenceDestination(absolute, artifactsRoot);
|
|
246
246
|
let result;
|
|
247
247
|
try {
|
|
248
|
-
const timeoutMs = Number(metadata?.cdpTimeoutMs ?? 5000);
|
|
249
|
-
result = await
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}),
|
|
255
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error(`Chrome Page.captureScreenshot timed out after ${timeoutMs}ms.`)), timeoutMs)),
|
|
256
|
-
]);
|
|
257
|
-
if (typeof result?.data !== 'string' || result.data.length === 0) {
|
|
248
|
+
const timeoutMs = Number(metadata?.cdpTimeoutMs ?? metadata?.timeoutMs ?? 5000);
|
|
249
|
+
result = await CdpWebPage.prototype.screenshot.call(page, undefined, undefined, {
|
|
250
|
+
...metadata,
|
|
251
|
+
timeoutMs,
|
|
252
|
+
});
|
|
253
|
+
if (typeof result !== 'string' || result.length === 0) {
|
|
258
254
|
throw new Error('Chrome Page.captureScreenshot returned no image data.');
|
|
259
255
|
}
|
|
260
256
|
} catch (error) {
|
|
261
257
|
await cleanupEvidenceStaging(destination.stagingDir);
|
|
262
258
|
const cdpError = error instanceof Error ? error.message : String(error);
|
|
263
|
-
|
|
259
|
+
throw new Error(
|
|
260
|
+
`Extension screenshot capture failed: ${captureHelperError ? 'native=' + captureHelperError + '; ' : ''}CDP=${cdpError}.\n` +
|
|
261
|
+
'Next: mm-harness doctor --adapter extension --json',
|
|
262
|
+
);
|
|
264
263
|
}
|
|
265
264
|
try {
|
|
266
265
|
await writeFile(
|
|
267
266
|
destination.stagedPath,
|
|
268
|
-
Buffer.from(result
|
|
267
|
+
Buffer.from(result, 'base64'),
|
|
269
268
|
{ flag: 'wx', mode: 0o600 },
|
|
270
269
|
);
|
|
271
270
|
} catch (error) {
|
|
@@ -283,89 +282,15 @@ async function captureCdpViewportSnapshot(page, context, relPath, metadata, capt
|
|
|
283
282
|
metadata: {
|
|
284
283
|
provider: 'cdp',
|
|
285
284
|
mode: 'Page.captureScreenshot',
|
|
285
|
+
fullPage: metadata?.fullPage === true,
|
|
286
286
|
...(captureHelperError ? { fallbackFrom: 'capture-helper', captureHelperError } : {}),
|
|
287
287
|
},
|
|
288
288
|
};
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
-
async function captureDomRasterSnapshot(page, context, relPath, metadata, captureHelperError, cdpError) {
|
|
292
|
-
const { relative, absolute, artifactsRoot } = resolveRelativeArtifactPath(context.artifactsDir, relPath);
|
|
293
|
-
const destination = await preparePrivateEvidenceDestination(absolute, artifactsRoot);
|
|
294
|
-
const dataUrlPromise = page.evaluate(`(async () => {
|
|
295
|
-
const width = Math.max(1, window.innerWidth);
|
|
296
|
-
const height = Math.max(1, window.innerHeight);
|
|
297
|
-
const source = document.documentElement;
|
|
298
|
-
const clone = source.cloneNode(true);
|
|
299
|
-
const sourceNodes = [source, ...source.querySelectorAll('*')];
|
|
300
|
-
const cloneNodes = [clone, ...clone.querySelectorAll('*')];
|
|
301
|
-
for (let index = 0; index < sourceNodes.length; index += 1) {
|
|
302
|
-
const sourceNode = sourceNodes[index];
|
|
303
|
-
const cloneNode = cloneNodes[index];
|
|
304
|
-
if (!(sourceNode instanceof Element) || !(cloneNode instanceof Element)) continue;
|
|
305
|
-
const computed = getComputedStyle(sourceNode);
|
|
306
|
-
cloneNode.setAttribute('style', Array.from(computed).map((name) => name + ':' + computed.getPropertyValue(name) + ';').join(''));
|
|
307
|
-
if ('value' in sourceNode && typeof sourceNode.value === 'string') cloneNode.setAttribute('value', sourceNode.value);
|
|
308
|
-
if (sourceNode instanceof HTMLCanvasElement) {
|
|
309
|
-
const image = document.createElement('img');
|
|
310
|
-
try { image.src = sourceNode.toDataURL('image/png'); } catch { image.alt = 'canvas'; }
|
|
311
|
-
image.setAttribute('style', cloneNode.getAttribute('style') || '');
|
|
312
|
-
cloneNode.replaceWith(image);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
clone.querySelectorAll('script').forEach((node) => node.remove());
|
|
316
|
-
clone.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml');
|
|
317
|
-
clone.style.width = width + 'px';
|
|
318
|
-
clone.style.height = height + 'px';
|
|
319
|
-
clone.style.overflow = 'hidden';
|
|
320
|
-
const serialized = new XMLSerializer().serializeToString(clone);
|
|
321
|
-
const svg = '<svg xmlns="http://www.w3.org/2000/svg" width="' + width + '" height="' + height + '"><foreignObject width="100%" height="100%">' + serialized + '</foreignObject></svg>';
|
|
322
|
-
const image = new Image();
|
|
323
|
-
const loaded = new Promise((resolve, reject) => { image.onload = resolve; image.onerror = () => reject(new Error('DOM raster image load failed.')); });
|
|
324
|
-
image.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svg)));
|
|
325
|
-
await loaded;
|
|
326
|
-
const canvas = document.createElement('canvas');
|
|
327
|
-
canvas.width = width;
|
|
328
|
-
canvas.height = height;
|
|
329
|
-
const context = canvas.getContext('2d');
|
|
330
|
-
context.drawImage(image, 0, 0, width, height);
|
|
331
|
-
return canvas.toDataURL('image/png');
|
|
332
|
-
})()`);
|
|
333
|
-
let dataUrl;
|
|
334
|
-
try {
|
|
335
|
-
dataUrl = await dataUrlPromise;
|
|
336
|
-
if (typeof dataUrl !== 'string' || !dataUrl.startsWith('data:image/png;base64,')) {
|
|
337
|
-
throw new Error(`Extension screenshot fallbacks failed: capture-helper=${captureHelperError ?? 'not attempted'}; cdp=${cdpError}; DOM raster returned no PNG.`);
|
|
338
|
-
}
|
|
339
|
-
await writeFile(
|
|
340
|
-
destination.stagedPath,
|
|
341
|
-
Buffer.from(dataUrl.slice('data:image/png;base64,'.length), 'base64'),
|
|
342
|
-
{ flag: 'wx', mode: 0o600 },
|
|
343
|
-
);
|
|
344
|
-
} catch (error) {
|
|
345
|
-
await cleanupEvidenceStaging(destination.stagingDir);
|
|
346
|
-
throw error;
|
|
347
|
-
}
|
|
348
|
-
await publishPrivateEvidence(destination);
|
|
349
|
-
return {
|
|
350
|
-
path: relative,
|
|
351
|
-
type: 'screenshot',
|
|
352
|
-
nodeId: context.nodeId,
|
|
353
|
-
label: metadata?.label ?? `${context.nodeId} screenshot`,
|
|
354
|
-
category: metadata?.category ?? 'evidence',
|
|
355
|
-
mimeType: 'image/png',
|
|
356
|
-
metadata: {
|
|
357
|
-
provider: 'dom-raster',
|
|
358
|
-
mode: 'computed-style-viewport',
|
|
359
|
-
fallbackFrom: captureHelperError ? 'capture-helper+cdp' : 'cdp',
|
|
360
|
-
...(captureHelperError ? { captureHelperError } : {}),
|
|
361
|
-
cdpError,
|
|
362
|
-
},
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
|
|
366
291
|
export async function captureHelperSnapshot(page, context, relPath, metadata) {
|
|
367
|
-
if (process.platform !== 'darwin') {
|
|
368
|
-
return
|
|
292
|
+
if (metadata?.fullPage === true || process.platform !== 'darwin') {
|
|
293
|
+
return captureCdpSnapshot(page, context, relPath, metadata);
|
|
369
294
|
}
|
|
370
295
|
const { relative, absolute, artifactsRoot } = resolveRelativeArtifactPath(context.artifactsDir, relPath);
|
|
371
296
|
const destination = await preparePrivateEvidenceDestination(absolute, artifactsRoot);
|
|
@@ -388,7 +313,7 @@ export async function captureHelperSnapshot(page, context, relPath, metadata) {
|
|
|
388
313
|
parsePositivePid(process.env.METAMASK_RECIPE_EXTENSION_ACTIVE_RECORDING_PID) === pid
|
|
389
314
|
) {
|
|
390
315
|
await cleanupEvidenceStaging(destination.stagingDir);
|
|
391
|
-
return
|
|
316
|
+
return captureCdpSnapshot(
|
|
392
317
|
page,
|
|
393
318
|
context,
|
|
394
319
|
relPath,
|
|
@@ -414,7 +339,7 @@ export async function captureHelperSnapshot(page, context, relPath, metadata) {
|
|
|
414
339
|
} catch (error) {
|
|
415
340
|
await cleanupEvidenceStaging(destination.stagingDir);
|
|
416
341
|
const message = error instanceof Error ? error.message : String(error);
|
|
417
|
-
return
|
|
342
|
+
return captureCdpSnapshot(page, context, relPath, metadata, message);
|
|
418
343
|
}
|
|
419
344
|
await publishPrivateEvidence(destination);
|
|
420
345
|
return {
|
|
@@ -811,47 +736,6 @@ export class ExtensionPage extends CdpWebPage {
|
|
|
811
736
|
});
|
|
812
737
|
}
|
|
813
738
|
|
|
814
|
-
async click(selector) {
|
|
815
|
-
const target = await this.evaluate(`(() => {
|
|
816
|
-
const selector = ${JSON.stringify(selector)};
|
|
817
|
-
const element = document.querySelector(selector);
|
|
818
|
-
if (!element) throw new Error('Clickable element not found: ' + selector);
|
|
819
|
-
element.scrollIntoView({ block: 'center', inline: 'center' });
|
|
820
|
-
const rect = element.getBoundingClientRect();
|
|
821
|
-
const style = getComputedStyle(element);
|
|
822
|
-
if (style.display === 'none' || style.visibility === 'hidden' || rect.width <= 0 || rect.height <= 0) {
|
|
823
|
-
throw new Error('Clickable element is not visible: ' + selector);
|
|
824
|
-
}
|
|
825
|
-
return {
|
|
826
|
-
x: rect.left + (rect.width / 2),
|
|
827
|
-
y: rect.top + (rect.height / 2),
|
|
828
|
-
tagName: element.tagName,
|
|
829
|
-
};
|
|
830
|
-
})()`);
|
|
831
|
-
await this.session.call('Input.dispatchMouseEvent', {
|
|
832
|
-
type: 'mouseMoved',
|
|
833
|
-
x: target.x,
|
|
834
|
-
y: target.y,
|
|
835
|
-
});
|
|
836
|
-
await this.session.call('Input.dispatchMouseEvent', {
|
|
837
|
-
type: 'mousePressed',
|
|
838
|
-
x: target.x,
|
|
839
|
-
y: target.y,
|
|
840
|
-
button: 'left',
|
|
841
|
-
buttons: 1,
|
|
842
|
-
clickCount: 1,
|
|
843
|
-
});
|
|
844
|
-
await this.session.call('Input.dispatchMouseEvent', {
|
|
845
|
-
type: 'mouseReleased',
|
|
846
|
-
x: target.x,
|
|
847
|
-
y: target.y,
|
|
848
|
-
button: 'left',
|
|
849
|
-
buttons: 0,
|
|
850
|
-
clickCount: 1,
|
|
851
|
-
});
|
|
852
|
-
return { clicked: true, selector, tagName: target.tagName };
|
|
853
|
-
}
|
|
854
|
-
|
|
855
739
|
async setInput(selector, value) {
|
|
856
740
|
const input = String(value ?? '');
|
|
857
741
|
const target = await this.evaluate(`(() => {
|
|
@@ -947,22 +831,19 @@ export class ExtensionPage extends CdpWebPage {
|
|
|
947
831
|
}
|
|
948
832
|
|
|
949
833
|
async screenshot(contextOrInput, relPath, metadata = {}) {
|
|
950
|
-
|
|
951
|
-
|
|
834
|
+
const context = contextOrInput?.context ?? contextOrInput;
|
|
835
|
+
const options = contextOrInput?.context
|
|
836
|
+
? {
|
|
952
837
|
label: contextOrInput.node?.description || `${contextOrInput.action} screenshot`,
|
|
953
838
|
category: 'evidence',
|
|
954
839
|
timeoutMs: contextOrInput.node?.timeout_ms,
|
|
955
840
|
...metadata,
|
|
956
|
-
};
|
|
957
|
-
if (contextOrInput.node?.screenshot_mode === 'dom_raster' || process.env.METAMASK_RECIPE_EXTENSION_SCREENSHOT_MODE === 'dom-raster') {
|
|
958
|
-
throw new Error('Extension ui.screenshot no longer supports DOM raster screenshots. Use capture-helper snapshot evidence.');
|
|
959
841
|
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
if (process.env.METAMASK_RECIPE_EXTENSION_SCREENSHOT_MODE === 'dom-raster') {
|
|
842
|
+
: metadata;
|
|
843
|
+
if (contextOrInput?.node?.screenshot_mode === 'dom_raster' || process.env.METAMASK_RECIPE_EXTENSION_SCREENSHOT_MODE === 'dom-raster') {
|
|
963
844
|
throw new Error('Extension screenshots no longer support DOM raster mode. Use capture-helper snapshot evidence.');
|
|
964
845
|
}
|
|
965
|
-
return captureHelperSnapshot(this,
|
|
846
|
+
return captureHelperSnapshot(this, context, relPath, options);
|
|
966
847
|
}
|
|
967
848
|
}
|
|
968
849
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export async function readQuoteInputIdentity(page) {
|
|
2
|
+
return page.evaluate(`(() => {
|
|
3
|
+
const state = globalThis.stateHooks?.store?.getState?.();
|
|
4
|
+
if (!state?.bridge || !state?.metamask) {
|
|
5
|
+
throw new Error('Swap quote identity requires exposed bridge quote state on this build; UI amounts alone cannot bind a quote to its request.');
|
|
6
|
+
}
|
|
7
|
+
const value = (id) => {
|
|
8
|
+
const root = document.querySelector('[data-testid="' + id + '"]');
|
|
9
|
+
return String((root?.matches('input,textarea') ? root : root?.querySelector('input,textarea'))?.value ?? '').trim();
|
|
10
|
+
};
|
|
11
|
+
const amount = value('from-amount');
|
|
12
|
+
const destinationAmount = value('to-amount');
|
|
13
|
+
const from = state.bridge.fromToken, to = state.bridge.toToken;
|
|
14
|
+
const decimals = from?.decimals;
|
|
15
|
+
if (!from?.assetId || !to?.assetId || !Number.isInteger(decimals) || decimals < 0 || decimals > 36 || !/^\\d+(?:\\.\\d*)?$/u.test(amount)) {
|
|
16
|
+
return { status: 'unavailable', reason: 'Exact token-unit input and asset identities are required.' };
|
|
17
|
+
}
|
|
18
|
+
const [integer, fraction = ''] = amount.split('.');
|
|
19
|
+
if (fraction.length > decimals) return { status: 'unavailable', reason: 'Input exceeds token precision.' };
|
|
20
|
+
const sourceBaseUnits = BigInt(integer + fraction.padEnd(decimals, '0')).toString();
|
|
21
|
+
const request = state.metamask.quoteRequest?.[0];
|
|
22
|
+
const quotes = state.metamask.quotes ?? [];
|
|
23
|
+
const sameAsset = (left, right) => left === right || (
|
|
24
|
+
/^eip155:/u.test(String(left)) && String(left).toLowerCase() === String(right).toLowerCase()
|
|
25
|
+
);
|
|
26
|
+
const matches = quotes.length > 0 && quotes.every(({ quote }) =>
|
|
27
|
+
quote?.requestId && quote.src?.amount === sourceBaseUnits &&
|
|
28
|
+
sameAsset(quote.src?.asset?.assetId, from.assetId) &&
|
|
29
|
+
sameAsset(quote.dest?.asset?.assetId, to.assetId)
|
|
30
|
+
);
|
|
31
|
+
const destinationMatches = /^\\d+(?:\\.\\d*)?$/u.test(destinationAmount) && quotes.some(({ quote }) => {
|
|
32
|
+
const precision = quote?.dest?.asset?.decimals;
|
|
33
|
+
if (!Number.isInteger(precision) || precision < 0 || precision > 36 || !/^\\d+$/u.test(String(quote?.dest?.amount))) return false;
|
|
34
|
+
const [whole, part = ''] = destinationAmount.split('.');
|
|
35
|
+
if (part.length > precision) return false;
|
|
36
|
+
const displayed = BigInt(whole + part.padEnd(precision, '0'));
|
|
37
|
+
const quoted = BigInt(quote.dest.amount);
|
|
38
|
+
const difference = displayed > quoted ? displayed - quoted : quoted - displayed;
|
|
39
|
+
const displayUnit = 10n ** BigInt(precision - part.length);
|
|
40
|
+
return displayed > 0n && difference * 2n <= displayUnit;
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
status: state.metamask.quotesLoadingStatus === 0 ? 'loading'
|
|
44
|
+
: request?.srcTokenAmount === sourceBaseUnits && matches && destinationMatches ? 'matching' : 'stale',
|
|
45
|
+
sourceAmount: amount,
|
|
46
|
+
destinationAmount,
|
|
47
|
+
destinationMatches,
|
|
48
|
+
sourceBaseUnits,
|
|
49
|
+
sourceAssetId: from.assetId,
|
|
50
|
+
destinationAssetId: to.assetId,
|
|
51
|
+
quoteCount: quotes.length,
|
|
52
|
+
};
|
|
53
|
+
})()`);
|
|
54
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
|
+
import { readQuoteInputIdentity } from './quote-state.mjs';
|
|
2
3
|
import {
|
|
3
4
|
normalizeVisibleSwapBridgeState,
|
|
4
5
|
resolveVisibleSwapBridgeOptions,
|
|
@@ -54,7 +55,8 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
54
55
|
})()`);
|
|
55
56
|
const state = normalizeVisibleSwapBridgeState(raw, options, 'extension');
|
|
56
57
|
const requireQuote = input.node?.require_quote === true;
|
|
57
|
-
|
|
58
|
+
const quoteIdentity = requireQuote ? await readQuoteInputIdentity(page) : undefined;
|
|
59
|
+
if (requireQuote && (quoteIdentity.status !== 'matching' || quoteIdentity.sourceAmount !== state.sourceAmount || quoteIdentity.destinationAmount !== state.destinationAmount || !raw.quote.ctaEnabled || !(Number(state.destinationAmount) > 0)
|
|
58
60
|
|| !/^(?:Swap|Bridge)$/iu.test(String(raw.quote.cta)))) {
|
|
59
61
|
throw new Error('Swap quote is not ready for review.');
|
|
60
62
|
}
|
|
@@ -62,6 +64,7 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
62
64
|
action: input.action,
|
|
63
65
|
...state,
|
|
64
66
|
quote: raw.quote,
|
|
67
|
+
...(quoteIdentity ? { quoteIdentity } : {}),
|
|
65
68
|
proofPath: 'visible-dom-accessibility',
|
|
66
69
|
};
|
|
67
70
|
}));
|
|
@@ -57,7 +57,17 @@ async function selectAsset(page, role, asset, deadline) {
|
|
|
57
57
|
if (!(await isVisible(page, search))) {
|
|
58
58
|
throw new Error(`Swap ${role} asset picker did not open.`);
|
|
59
59
|
}
|
|
60
|
-
await page.
|
|
60
|
+
const existing = await page.evaluate(`(() => {
|
|
61
|
+
const prefix = ${JSON.stringify(`bridge-asset--${asset.chain}/`)};
|
|
62
|
+
const exactId = ${JSON.stringify(asset.assetId ? `bridge-asset--${asset.assetId}` : null)};
|
|
63
|
+
return [...document.querySelectorAll('[data-testid^="bridge-asset--"]')]
|
|
64
|
+
.filter((row) => exactId
|
|
65
|
+
? row.getAttribute('data-testid') === exactId
|
|
66
|
+
: String(row.getAttribute('data-testid') || '').startsWith(prefix)
|
|
67
|
+
&& String(row.querySelector('[data-testid="bridge-asset-symbol"]')?.textContent || '').trim().toUpperCase() === ${JSON.stringify(asset.symbol)})
|
|
68
|
+
.map((row) => row.getAttribute('data-testid'));
|
|
69
|
+
})()`);
|
|
70
|
+
if (existing.length === 0) await page.setInput(search, asset.query);
|
|
61
71
|
const testId = await findAsset(page, asset, deadline);
|
|
62
72
|
await page.click(dataTestId(testId));
|
|
63
73
|
while (Date.now() < deadline) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { runAdapter, withExtensionPage, dataTestId } from '../platform/cdp.mjs';
|
|
2
2
|
import { resolveAmountOptions } from '../../shared/swap-bridge/transaction.mjs';
|
|
3
|
+
import { readQuoteInputIdentity } from './quote-state.mjs';
|
|
3
4
|
|
|
4
5
|
runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
5
6
|
const options = resolveAmountOptions(input.node);
|
|
@@ -7,7 +8,6 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
7
8
|
await page.setInput(source, options.amount);
|
|
8
9
|
const deadline = Date.now() + options.timeoutMs;
|
|
9
10
|
let observed;
|
|
10
|
-
let lastRefreshAt = 0;
|
|
11
11
|
do {
|
|
12
12
|
observed = await page.evaluate(`(() => {
|
|
13
13
|
const value = (testId) => {
|
|
@@ -24,22 +24,24 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
24
24
|
noQuote: document.body.innerText.includes("This trade route isn't available right now"),
|
|
25
25
|
};
|
|
26
26
|
})()`);
|
|
27
|
+
if (options.waitForQuote) {
|
|
28
|
+
observed.quoteIdentity = await readQuoteInputIdentity(page);
|
|
29
|
+
if (observed.quoteIdentity.sourceAmount !== observed.sourceAmount || observed.quoteIdentity.destinationAmount !== observed.destinationAmount) observed.quoteIdentity.status = 'stale';
|
|
30
|
+
}
|
|
27
31
|
if (!options.waitForQuote || observed.noQuote) break;
|
|
28
|
-
if (
|
|
29
|
-
|
|
30
|
-
await page.click(dataTestId('bridge-cta-button'));
|
|
31
|
-
lastRefreshAt = Date.now();
|
|
32
|
+
if (observed.quoteIdentity.status === 'matching' && !observed.ctaEnabled && /^Insufficient funds$/iu.test(String(observed.cta))) {
|
|
33
|
+
throw new Error(`Swap quote matches the input but cannot be submitted: ${JSON.stringify(observed)}.`);
|
|
32
34
|
}
|
|
33
|
-
if (Number(observed.destinationAmount) > 0 && observed.ctaEnabled
|
|
35
|
+
if (observed.quoteIdentity?.status === 'matching' && Number(observed.destinationAmount) > 0 && observed.ctaEnabled
|
|
34
36
|
&& /^(?:Swap|Bridge)$/iu.test(String(observed.cta))) break;
|
|
35
37
|
await page.evaluate('new Promise((resolve) => setTimeout(resolve, 250))');
|
|
36
38
|
} while (Date.now() < deadline);
|
|
37
39
|
if (observed.sourceAmount !== options.amount) {
|
|
38
40
|
throw new Error(`Swap source amount mismatch: expected ${options.amount}, observed ${observed.sourceAmount}.`);
|
|
39
41
|
}
|
|
40
|
-
if (options.waitForQuote && !observed.noQuote && !(Number(observed.destinationAmount) > 0
|
|
42
|
+
if (options.waitForQuote && !observed.noQuote && !(observed.quoteIdentity?.status === 'matching' && Number(observed.destinationAmount) > 0
|
|
41
43
|
&& observed.ctaEnabled && /^(?:Swap|Bridge)$/iu.test(String(observed.cta)))) {
|
|
42
|
-
throw new Error(`Swap quote did not resolve within ${options.timeoutMs}ms
|
|
44
|
+
throw new Error(`Swap quote did not resolve within ${options.timeoutMs}ms: ${JSON.stringify(observed)}.\nNext: mm-harness call metamask.swap_bridge.read_visible_state`);
|
|
43
45
|
}
|
|
44
46
|
return { action: input.action, ...observed, proofPath: 'visible-ui-amount' };
|
|
45
47
|
}));
|