@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,4 +1,5 @@
|
|
|
1
1
|
import { pathToFileURL } from 'node:url';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import { dataTestId, marketSymbol, normalizeMarketSymbol, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
3
4
|
import {
|
|
4
5
|
assertTrustedExternalMutationTarget,
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
} from './mutation-receipt.mjs';
|
|
15
16
|
import { readSnapshot } from './read_snapshot.mjs';
|
|
16
17
|
import { readPerpsRuntimeStateExpression } from './state.mjs';
|
|
18
|
+
import { readWalletStateExpression } from '../wallet/state.mjs';
|
|
17
19
|
import {
|
|
18
20
|
orderIdentityMatches,
|
|
19
21
|
parseOrderCard,
|
|
@@ -25,6 +27,8 @@ import {
|
|
|
25
27
|
waitForStableGlobalPerpsHome,
|
|
26
28
|
} from './visible-mutation-identity.mjs';
|
|
27
29
|
|
|
30
|
+
const ORDER_ENTRY = '[data-testid="parent-selector-perps-order-entry"], [data-testid="perps-order-entry-page"]';
|
|
31
|
+
|
|
28
32
|
function sleep(ms) {
|
|
29
33
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
30
34
|
}
|
|
@@ -117,6 +121,17 @@ export function positionSelectionState(input, positions) {
|
|
|
117
121
|
return { hasMatching: selectedItems(input, positions).length > 0 };
|
|
118
122
|
}
|
|
119
123
|
|
|
124
|
+
export function placementReceiptSelection(input) {
|
|
125
|
+
const reference = input.node?.placement_node;
|
|
126
|
+
if (reference == null) return {};
|
|
127
|
+
if (typeof reference !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9_./:-]{0,255}$/u.test(reference)
|
|
128
|
+
|| reference.split('/').some((part) => !part || part === '.' || part === '..')) {
|
|
129
|
+
throw new Error('placement_node must reference a placement node in the current recipe call.');
|
|
130
|
+
}
|
|
131
|
+
const current = String(input.context?.nodeId ?? '');
|
|
132
|
+
return { nodeId: current.slice(0, current.lastIndexOf('/') + 1) + reference };
|
|
133
|
+
}
|
|
134
|
+
|
|
120
135
|
async function navigatePerps(input) {
|
|
121
136
|
const selected = String(input.node?.target ?? input.node?.destination ?? (input.node?.market || input.node?.symbol ? 'market' : 'home')).toLowerCase();
|
|
122
137
|
return withExtensionPage(input, async (page) => {
|
|
@@ -135,7 +150,7 @@ async function navigatePerps(input) {
|
|
|
135
150
|
if (selected === 'trade' || selected === 'order') {
|
|
136
151
|
const side = String(input.node?.side ?? 'long').toLowerCase();
|
|
137
152
|
const navigation = await page.navigateHash(`#/perps/trade/${encodeURIComponent(symbol)}?direction=${encodeURIComponent(side)}&mode=new`);
|
|
138
|
-
await page.waitForSelector(
|
|
153
|
+
await page.waitForSelector(ORDER_ENTRY, { timeoutMs: 20000 });
|
|
139
154
|
return { action: input.action, target: selected, market: symbol, side, navigation, proofPath: 'ui-navigation' };
|
|
140
155
|
}
|
|
141
156
|
throw new Error(`Unsupported extension Perps navigation target: ${selected}`);
|
|
@@ -144,18 +159,20 @@ async function navigatePerps(input) {
|
|
|
144
159
|
|
|
145
160
|
export async function readPositions(input) {
|
|
146
161
|
return withExtensionPage(input, async (page) => {
|
|
162
|
+
const network = await assertNetwork(page, input.node?.network);
|
|
147
163
|
const state = await page.readPositions();
|
|
148
164
|
if (!state.available) throw new Error('stateHooks.submitRequestToBackground is unavailable; cannot read live Perps positions.');
|
|
149
165
|
const matching = selectedItems(input, state.positions);
|
|
150
|
-
return { action: input.action, source: state.source ?? 'perps-stream-manager-cache', count: state.positions.length, matchingCount: matching.length, positions: matching.map(redactPosition) };
|
|
166
|
+
return { action: input.action, source: state.source ?? 'perps-stream-manager-cache', network, count: state.positions.length, matchingCount: matching.length, positions: matching.map(redactPosition) };
|
|
151
167
|
});
|
|
152
168
|
}
|
|
153
169
|
|
|
154
170
|
export async function readOrders(input) {
|
|
155
171
|
return withExtensionPage(input, async (page) => {
|
|
172
|
+
const network = await assertNetwork(page, input.node?.network);
|
|
156
173
|
const state = await readOpenOrdersState(page);
|
|
157
174
|
const matching = selectedItems(input, state.orders);
|
|
158
|
-
return { action: input.action, source: state.source, count: state.orders.length, matchingCount: matching.length, orders: matching.map(redactOrder) };
|
|
175
|
+
return { action: input.action, source: state.source, network, count: state.orders.length, matchingCount: matching.length, orders: matching.map(redactOrder) };
|
|
159
176
|
});
|
|
160
177
|
}
|
|
161
178
|
|
|
@@ -313,8 +330,10 @@ export async function closeOrders(input) {
|
|
|
313
330
|
const hasBackgroundRequest = await page.evaluate(
|
|
314
331
|
`typeof globalThis.stateHooks?.submitRequestToBackground === 'function'`,
|
|
315
332
|
);
|
|
316
|
-
if (hasBackgroundRequest
|
|
317
|
-
|
|
333
|
+
if (hasBackgroundRequest && input.node?.placement_node == null) {
|
|
334
|
+
return closeOrderThroughBackground(input, page, timeoutMs);
|
|
335
|
+
}
|
|
336
|
+
if (!hasBackgroundRequest || input.node?.placement_node != null) {
|
|
318
337
|
if (input.node?.confirm_live !== true) {
|
|
319
338
|
throw new Error('metamask.perps.close_orders requires confirm_live=true for visible live cancellation.');
|
|
320
339
|
}
|
|
@@ -352,6 +371,7 @@ export async function closeOrders(input) {
|
|
|
352
371
|
input,
|
|
353
372
|
priorLookupIdentity,
|
|
354
373
|
'metamask.perps.place_order',
|
|
374
|
+
placementReceiptSelection(input),
|
|
355
375
|
);
|
|
356
376
|
const expectedOrder = {
|
|
357
377
|
market: symbol,
|
|
@@ -414,7 +434,6 @@ export async function closeOrders(input) {
|
|
|
414
434
|
let dispatchTarget;
|
|
415
435
|
try {
|
|
416
436
|
await requireCurrentVisibleFundingBalance(input, page, timeoutMs, fundingBalance.observedBaseUnits);
|
|
417
|
-
await requirePerpsTradingContext(input, page);
|
|
418
437
|
assertTrustedExternalMutationTarget(input, page, identity);
|
|
419
438
|
await page.navigateHash(`#/perps/market/${encodeURIComponent(symbol)}`);
|
|
420
439
|
await page.waitForSelector(dataTestId('perps-market-detail-pair'), { timeoutMs });
|
|
@@ -435,6 +454,7 @@ export async function closeOrders(input) {
|
|
|
435
454
|
const rect = element.getBoundingClientRect();
|
|
436
455
|
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
|
|
437
456
|
})()`);
|
|
457
|
+
await requirePerpsTradingContext(input, page);
|
|
438
458
|
assertTrustedExternalMutationTarget(input, page, identity);
|
|
439
459
|
} catch (error) {
|
|
440
460
|
throw externalMutationNotDispatched(error instanceof Error ? error.message : String(error));
|
|
@@ -888,7 +908,9 @@ async function observePolicyViolationCleanupCandidate(page, expected, receipt, t
|
|
|
888
908
|
}
|
|
889
909
|
|
|
890
910
|
async function observeExactVisibleOrderResult(page, expected, receipt, timeoutMs) {
|
|
891
|
-
|
|
911
|
+
if (!expected.fromCurrentForm) {
|
|
912
|
+
await page.navigateHash(`#/perps/market/${encodeURIComponent(expected.market)}`);
|
|
913
|
+
}
|
|
892
914
|
await page.waitForSelector(dataTestId('perps-market-detail-pair'), { timeoutMs: 20_000 });
|
|
893
915
|
const deadline = Date.now() + timeoutMs;
|
|
894
916
|
let stableIdentity = null;
|
|
@@ -1085,12 +1107,28 @@ export async function requirePerpsTradingContext(input, page) {
|
|
|
1085
1107
|
}
|
|
1086
1108
|
const actualProvider = String(state.activeProvider || '').toLowerCase();
|
|
1087
1109
|
const actualNetwork = state.isTestnet === true ? 'testnet' : 'mainnet';
|
|
1110
|
+
const trusted = input.context?.trustedMutation;
|
|
1111
|
+
if (trusted?.proofLane === 'source-dev') {
|
|
1112
|
+
if (state.providerKnown !== true || provider !== 'hyperliquid' || network !== 'testnet'
|
|
1113
|
+
|| actualProvider !== 'hyperliquid' || actualNetwork !== 'testnet') {
|
|
1114
|
+
throw new Error('Source-dev funded mutations require independently verified Hyperliquid testnet.');
|
|
1115
|
+
}
|
|
1116
|
+
const wallet = await page.evaluate(readWalletStateExpression());
|
|
1117
|
+
const address = String(wallet?.selectedAccount?.address ?? '').toLowerCase();
|
|
1118
|
+
if (!/^0x[0-9a-f]{40}$/u.test(address)
|
|
1119
|
+
|| createHash('sha256').update(address).digest('hex') !== trusted.selectedIdentitySha256) {
|
|
1120
|
+
throw new Error('Source-dev funded mutation selected account changed after its visible identity proof.');
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1088
1123
|
if (actualProvider !== provider || actualNetwork !== network) {
|
|
1089
1124
|
throw new Error(
|
|
1090
1125
|
`Perps mutation expected ${provider}/${network}, got ${actualProvider || 'unknown'}/${actualNetwork}.`,
|
|
1091
1126
|
);
|
|
1092
1127
|
}
|
|
1093
|
-
return {
|
|
1128
|
+
return {
|
|
1129
|
+
provider, network,
|
|
1130
|
+
...(trusted?.proofLane === 'source-dev' ? { selectedIdentitySha256: trusted.selectedIdentitySha256 } : {}),
|
|
1131
|
+
};
|
|
1094
1132
|
}
|
|
1095
1133
|
|
|
1096
1134
|
export async function requireCurrentVisibleFundingBalance(input, page, timeoutMs, expectedBaseUnits) {
|
|
@@ -1137,7 +1175,7 @@ async function requireCurrentOrderEntryFundingBalance(input, page, expectedBaseU
|
|
|
1137
1175
|
if (BigInt(observedBaseUnits) < BigInt(requirement.minBaseUnits)) {
|
|
1138
1176
|
throw new Error('Current visible Perps balance is below the runner-trusted funding minimum.');
|
|
1139
1177
|
}
|
|
1140
|
-
if (observedBaseUnits !== expectedBaseUnits) {
|
|
1178
|
+
if (expectedBaseUnits != null && observedBaseUnits !== expectedBaseUnits) {
|
|
1141
1179
|
throw new Error('Current visible Perps balance changed after receipt publication.');
|
|
1142
1180
|
}
|
|
1143
1181
|
return {
|
|
@@ -1149,6 +1187,23 @@ async function requireCurrentOrderEntryFundingBalance(input, page, expectedBaseU
|
|
|
1149
1187
|
};
|
|
1150
1188
|
}
|
|
1151
1189
|
|
|
1190
|
+
async function readPreparedOrderConfiguration(page) {
|
|
1191
|
+
return page.evaluate(`(() => {
|
|
1192
|
+
const value = (testId) => document.querySelector('[data-testid="' + testId + '"] input')?.value ?? null;
|
|
1193
|
+
const submit = document.querySelector('[data-testid="submit-order-button"]');
|
|
1194
|
+
const direction = String(submit?.innerText || '').match(/\\b(long|short)\\b/iu)?.[1]?.toLowerCase() ?? null;
|
|
1195
|
+
return {
|
|
1196
|
+
route: String(location.hash),
|
|
1197
|
+
direction,
|
|
1198
|
+
autoCloseEnabled: Boolean(document.querySelector('input[data-testid="auto-close-toggle"]')?.checked),
|
|
1199
|
+
takeProfitPrice: value('tp-price-input'),
|
|
1200
|
+
stopLossPrice: value('sl-price-input'),
|
|
1201
|
+
takeProfitPercent: value('tp-percent-input'),
|
|
1202
|
+
stopLossPercent: value('sl-percent-input'),
|
|
1203
|
+
};
|
|
1204
|
+
})()`);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1152
1207
|
export async function placeOrder(input) {
|
|
1153
1208
|
requirePlaceOrderInputs(input);
|
|
1154
1209
|
const symbol = marketSymbol(input);
|
|
@@ -1159,6 +1214,10 @@ export async function placeOrder(input) {
|
|
|
1159
1214
|
const orderType = resolveOrderType(input);
|
|
1160
1215
|
const positionMode = String(input.node?.position_mode ?? 'new').toLowerCase();
|
|
1161
1216
|
const submitAttempts = Number(input.node?.submit_attempts ?? 1);
|
|
1217
|
+
const fromCurrentForm = input.node?.from_current_form === true;
|
|
1218
|
+
if (fromCurrentForm && (positionMode !== 'new' || submitAttempts !== 1)) {
|
|
1219
|
+
throw new Error('Prepared-form placement requires position_mode=new and submit_attempts=1.');
|
|
1220
|
+
}
|
|
1162
1221
|
const roundingToleranceBaseUnits = String(input.node?.identity_rounding_tolerance_base_units ?? '10000');
|
|
1163
1222
|
if (!/^(?:0|[1-9][0-9]{0,6})$/u.test(roundingToleranceBaseUnits)
|
|
1164
1223
|
|| BigInt(roundingToleranceBaseUnits) > 1_000_000n) {
|
|
@@ -1197,6 +1256,7 @@ export async function placeOrder(input) {
|
|
|
1197
1256
|
throw new Error('Requested Perps order exposure exceeds the runner-trusted operation cap.');
|
|
1198
1257
|
}
|
|
1199
1258
|
const expectedVisibleIdentity = {
|
|
1259
|
+
fromCurrentForm,
|
|
1200
1260
|
market: symbol,
|
|
1201
1261
|
side,
|
|
1202
1262
|
leverage,
|
|
@@ -1213,7 +1273,7 @@ export async function placeOrder(input) {
|
|
|
1213
1273
|
const hasBackgroundRequest = await page.evaluate(
|
|
1214
1274
|
`typeof globalThis.stateHooks?.submitRequestToBackground === 'function'`,
|
|
1215
1275
|
);
|
|
1216
|
-
if (hasBackgroundRequest) {
|
|
1276
|
+
if (hasBackgroundRequest && !fromCurrentForm) {
|
|
1217
1277
|
await requirePerpsTradingContext(input, page);
|
|
1218
1278
|
const resolvedPrice = await readCurrentMarketPrice(
|
|
1219
1279
|
page,
|
|
@@ -1243,7 +1303,9 @@ export async function placeOrder(input) {
|
|
|
1243
1303
|
const existingReceipt = await readExternalMutationReceipt(input, identity);
|
|
1244
1304
|
if (existingReceipt) {
|
|
1245
1305
|
return withExternalMutationRecovery(existingReceipt, async () => {
|
|
1246
|
-
|
|
1306
|
+
if (!fromCurrentForm) {
|
|
1307
|
+
await page.navigateHash(`#/perps/market/${encodeURIComponent(symbol)}`);
|
|
1308
|
+
}
|
|
1247
1309
|
await page.waitForSelector(dataTestId('perps-market-detail-pair'), { timeoutMs: 20_000 });
|
|
1248
1310
|
if (positionMode !== 'new') {
|
|
1249
1311
|
throw new Error('resumed funded position modification requires manual visible inspection');
|
|
@@ -1320,21 +1382,35 @@ export async function placeOrder(input) {
|
|
|
1320
1382
|
};
|
|
1321
1383
|
});
|
|
1322
1384
|
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1385
|
+
let initialClean;
|
|
1386
|
+
let fundingBalance;
|
|
1387
|
+
if (fromCurrentForm) {
|
|
1388
|
+
await page.waitForSelector(ORDER_ENTRY, { timeoutMs: 20_000 });
|
|
1389
|
+
const [positions, orders] = await Promise.all([page.readPositions(), readOpenOrders(page)]);
|
|
1390
|
+
if (!positions.available || positions.positions.length !== 0 || orders.length !== 0) {
|
|
1391
|
+
throw new Error('Prepared-form placement requires an independently verified empty position/order baseline.');
|
|
1392
|
+
}
|
|
1393
|
+
initialClean = { positionCount: 0, orderCount: 0 };
|
|
1394
|
+
fundingBalance = await requireCurrentOrderEntryFundingBalance(input, page);
|
|
1395
|
+
} else {
|
|
1396
|
+
await page.navigateHash('#/perps-home');
|
|
1397
|
+
initialClean = await waitForStableGlobalPerpsHome(page, {
|
|
1398
|
+
timeoutMs: Number(input.node?.timeout_ms ?? 45_000),
|
|
1399
|
+
requireZero: true,
|
|
1400
|
+
});
|
|
1401
|
+
fundingBalance = await requireCurrentVisibleFundingBalance(
|
|
1402
|
+
input,
|
|
1403
|
+
page,
|
|
1404
|
+
Number(input.node?.timeout_ms ?? 45_000),
|
|
1405
|
+
initialClean.balanceBaseUnits,
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1334
1408
|
const tradingContext = await requirePerpsTradingContext(input, page);
|
|
1335
1409
|
const balanceBeforeBaseUnits = fundingBalance.observedBaseUnits;
|
|
1336
|
-
|
|
1337
|
-
|
|
1410
|
+
if (!fromCurrentForm) {
|
|
1411
|
+
await page.navigateHash(`#/perps/market/${encodeURIComponent(symbol)}`);
|
|
1412
|
+
await page.waitForSelector(dataTestId('perps-market-detail-pair'), { timeoutMs: 20_000 });
|
|
1413
|
+
}
|
|
1338
1414
|
const resolvedPrice = await readCurrentMarketPrice(
|
|
1339
1415
|
page,
|
|
1340
1416
|
symbol,
|
|
@@ -1343,19 +1419,21 @@ export async function placeOrder(input) {
|
|
|
1343
1419
|
const limitPrice = orderType === 'limit'
|
|
1344
1420
|
? resolveLimitPrice(input, isBuy, resolvedPrice)
|
|
1345
1421
|
: null;
|
|
1346
|
-
const baselineOrderIds = orderType === 'limit'
|
|
1422
|
+
const baselineOrderIds = orderType === 'limit' && !fromCurrentForm
|
|
1347
1423
|
? (await readVisibleOrderCards(page, symbol)).map((order) => order.id)
|
|
1348
1424
|
: [];
|
|
1349
1425
|
const initialOpenOrderCount = baselineOrderIds.length;
|
|
1350
|
-
|
|
1351
|
-
|
|
1426
|
+
if (!fromCurrentForm) {
|
|
1427
|
+
await page.navigateHash(`#/perps/trade/${encodeURIComponent(symbol)}?direction=${encodeURIComponent(side)}&mode=${positionMode}`);
|
|
1428
|
+
await page.waitForSelector(ORDER_ENTRY, { timeoutMs: 20000 });
|
|
1429
|
+
}
|
|
1352
1430
|
const lateBackgroundRequest = await page.evaluate(
|
|
1353
1431
|
`typeof globalThis.stateHooks?.submitRequestToBackground === 'function'`,
|
|
1354
1432
|
);
|
|
1355
|
-
if (lateBackgroundRequest) {
|
|
1433
|
+
if (lateBackgroundRequest && !fromCurrentForm) {
|
|
1356
1434
|
throw new Error('Background Perps placement appeared after visible setup; refusing an ambiguous dispatch.');
|
|
1357
1435
|
}
|
|
1358
|
-
if (!lateBackgroundRequest) {
|
|
1436
|
+
if (!lateBackgroundRequest || fromCurrentForm) {
|
|
1359
1437
|
if (input.node?.confirm_live !== true) {
|
|
1360
1438
|
throw new Error('metamask.perps.place_order requires confirm_live=true for visible live submission.');
|
|
1361
1439
|
}
|
|
@@ -1363,9 +1441,11 @@ export async function placeOrder(input) {
|
|
|
1363
1441
|
if (submitAttempts !== 1) {
|
|
1364
1442
|
throw new Error('Visible funded Perps submission requires submit_attempts=1.');
|
|
1365
1443
|
}
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1444
|
+
if (!fromCurrentForm) {
|
|
1445
|
+
await page.setInput(`${dataTestId('amount-input-field')} input`, amount);
|
|
1446
|
+
await page.setInput(`${dataTestId('leverage-input')} input`, String(leverage));
|
|
1447
|
+
}
|
|
1448
|
+
if (orderType === 'limit' && !fromCurrentForm) {
|
|
1369
1449
|
await page.click(dataTestId('order-type-limit'));
|
|
1370
1450
|
const limitInput = `${dataTestId('limit-price-input')} input`;
|
|
1371
1451
|
await page.waitForSelector(limitInput, { timeoutMs: 10000 });
|
|
@@ -1379,6 +1459,7 @@ export async function placeOrder(input) {
|
|
|
1379
1459
|
const submit = document.querySelector('[data-testid="submit-order-button"]');
|
|
1380
1460
|
return {
|
|
1381
1461
|
amount: value('amount-input-field'),
|
|
1462
|
+
denomination: document.querySelector('[data-testid="amount-input-denomination-unit"]')?.textContent?.trim() ?? null,
|
|
1382
1463
|
leverage: value('leverage-input'),
|
|
1383
1464
|
limitPrice: limit,
|
|
1384
1465
|
limitSelected: document.querySelector('[data-testid="order-type-limit"]')?.getAttribute('aria-pressed') === 'true',
|
|
@@ -1391,19 +1472,29 @@ export async function placeOrder(input) {
|
|
|
1391
1472
|
if (Number(configured.amount) !== Number(amount) || Number(configured.leverage) !== leverage) {
|
|
1392
1473
|
throw new Error('Visible Perps order inputs did not retain the requested values.');
|
|
1393
1474
|
}
|
|
1475
|
+
if (configured.denomination !== null && configured.denomination !== 'USD') {
|
|
1476
|
+
throw new Error('Visible Perps amount must be denominated in USD before funded submission.');
|
|
1477
|
+
}
|
|
1394
1478
|
if (orderType === 'limit' && (!configured.limitSelected || Number(configured.limitPrice) !== limitPrice)) {
|
|
1395
1479
|
throw new Error('Visible Perps limit inputs did not retain the requested values.');
|
|
1396
1480
|
}
|
|
1397
1481
|
if (!configured.submitEnabled) throw new Error('Visible Perps order is not submittable.');
|
|
1482
|
+
const preparedConfiguration = fromCurrentForm ? await readPreparedOrderConfiguration(page) : null;
|
|
1483
|
+
if (preparedConfiguration && (preparedConfiguration.route.split('?')[0] !== `#/perps/trade/${encodeURIComponent(symbol)}`
|
|
1484
|
+
|| preparedConfiguration.direction !== side)) {
|
|
1485
|
+
throw new Error('Prepared Perps form does not match the requested market and direction.');
|
|
1486
|
+
}
|
|
1398
1487
|
const mutation = await runGuardedExternalMutation({
|
|
1399
1488
|
input,
|
|
1400
1489
|
identity,
|
|
1401
|
-
publicationFacts: {
|
|
1490
|
+
publicationFacts: {
|
|
1491
|
+
balanceBeforeBaseUnits, baselineOrderIds, fundingBalance, tradingContext,
|
|
1492
|
+
...(preparedConfiguration ? { preparedConfiguration } : {}),
|
|
1493
|
+
},
|
|
1402
1494
|
dispatch: async () => {
|
|
1403
1495
|
let dispatchTarget;
|
|
1404
1496
|
try {
|
|
1405
1497
|
await requireCurrentOrderEntryFundingBalance(input, page, fundingBalance.observedBaseUnits);
|
|
1406
|
-
await requirePerpsTradingContext(input, page);
|
|
1407
1498
|
const currentConfiguration = await page.evaluate(`(() => {
|
|
1408
1499
|
const value = (testId) => document.querySelector('[data-testid="' + testId + '"] input')?.value ?? null;
|
|
1409
1500
|
const limitSelected = document.querySelector('[data-testid="order-type-limit"]')?.getAttribute('aria-pressed') === 'true';
|
|
@@ -1415,20 +1506,26 @@ export async function placeOrder(input) {
|
|
|
1415
1506
|
const rect = element.getBoundingClientRect();
|
|
1416
1507
|
return {
|
|
1417
1508
|
amount: value('amount-input-field'),
|
|
1509
|
+
denomination: document.querySelector('[data-testid="amount-input-denomination-unit"]')?.textContent?.trim() ?? null,
|
|
1418
1510
|
leverage: value('leverage-input'),
|
|
1419
1511
|
limitPrice: value('limit-price-input'),
|
|
1420
1512
|
orderType: limitSelected ? 'limit' : 'market',
|
|
1421
1513
|
target: { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 },
|
|
1422
1514
|
};
|
|
1423
1515
|
})()`);
|
|
1424
|
-
if (currentConfiguration.amount !==
|
|
1425
|
-
|| currentConfiguration.
|
|
1516
|
+
if (parseUsdBaseUnits(currentConfiguration.amount) !== identity.amountBaseUnits
|
|
1517
|
+
|| (currentConfiguration.denomination !== null && currentConfiguration.denomination !== 'USD')
|
|
1518
|
+
|| Number(currentConfiguration.leverage) !== leverage
|
|
1426
1519
|
|| currentConfiguration.orderType !== orderType
|
|
1427
|
-
|| (orderType === 'limit' && currentConfiguration.limitPrice !==
|
|
1520
|
+
|| (orderType === 'limit' && parseUsdBaseUnits(currentConfiguration.limitPrice) !== expectedVisibleIdentity.limitPriceBaseUnits)
|
|
1428
1521
|
|| (orderType === 'market' && currentConfiguration.limitPrice != null)) {
|
|
1429
1522
|
throw new Error('Visible Perps order inputs changed before dispatch.');
|
|
1430
1523
|
}
|
|
1524
|
+
if (preparedConfiguration && JSON.stringify(await readPreparedOrderConfiguration(page)) !== JSON.stringify(preparedConfiguration)) {
|
|
1525
|
+
throw new Error('Prepared Perps direction or protection changed before dispatch.');
|
|
1526
|
+
}
|
|
1431
1527
|
dispatchTarget = currentConfiguration.target;
|
|
1528
|
+
await requirePerpsTradingContext(input, page);
|
|
1432
1529
|
assertTrustedExternalMutationTarget(input, page, identity);
|
|
1433
1530
|
} catch (error) {
|
|
1434
1531
|
throw externalMutationNotDispatched(error instanceof Error ? error.message : String(error));
|
|
@@ -1603,16 +1700,22 @@ async function ensureProvider(input, config) {
|
|
|
1603
1700
|
});
|
|
1604
1701
|
}
|
|
1605
1702
|
|
|
1703
|
+
async function assertNetwork(page, network) {
|
|
1704
|
+
if (!network) return { skipped: true };
|
|
1705
|
+
const before = await readPerpsRuntimeState(page);
|
|
1706
|
+
if (!before.available || !before.networkKnown) {
|
|
1707
|
+
throw new Error('Cannot verify the current Perps network.\nNext: mm-harness verify');
|
|
1708
|
+
}
|
|
1709
|
+
const actual = before.isTestnet ? 'testnet' : 'mainnet';
|
|
1710
|
+
if (actual !== network) {
|
|
1711
|
+
throw new Error(`Expected Perps network ${network}, got ${actual}.\nNext: select ${network} through the supported product UI, then rerun the recipe.`);
|
|
1712
|
+
}
|
|
1713
|
+
return { requested: network, isTestnet: before.isTestnet, changed: false };
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1606
1716
|
async function ensureNetwork(input, config) {
|
|
1607
1717
|
if (!config.network) return { skipped: true };
|
|
1608
|
-
|
|
1609
|
-
return withExtensionPage(input, async (page) => {
|
|
1610
|
-
const before = await readPerpsRuntimeState(page);
|
|
1611
|
-
if (!before.available) throw new Error('stateHooks.submitRequestToBackground is unavailable; cannot enforce Perps network.');
|
|
1612
|
-
if (!before.networkKnown) throw new Error('Unable to read the current Perps network from persisted controller state.');
|
|
1613
|
-
if (Boolean(before.isTestnet) === expectedTestnet) return { requested: config.network, isTestnet: before.isTestnet, changed: false };
|
|
1614
|
-
throw new Error(`Expected Perps network ${config.network}, got isTestnet=${before.isTestnet}. Automatic background network toggles are forbidden without a durable receipt; select the network through a supported visible action before running.`);
|
|
1615
|
-
});
|
|
1718
|
+
return withExtensionPage(input, (page) => assertNetwork(page, config.network));
|
|
1616
1719
|
}
|
|
1617
1720
|
|
|
1618
1721
|
async function assertReadyToTrade(input, config) {
|
|
@@ -1678,12 +1781,14 @@ export async function teardownState(input) {
|
|
|
1678
1781
|
orders: params.market || params.symbol || params.markets || params.symbols ? { state: 'none', mode: 'matching' } : false,
|
|
1679
1782
|
};
|
|
1680
1783
|
const config = mergeStateConfig(defaults, params);
|
|
1784
|
+
const network = await ensureNetwork(input, config);
|
|
1681
1785
|
const orders = await applyOrdersState(input, config.orders);
|
|
1682
1786
|
const positions = await applyPositionsState(input, config.positions);
|
|
1683
1787
|
const navigation = config.page === false ? { skipped: true } : await applyStateNavigation(input, config);
|
|
1684
1788
|
return {
|
|
1685
1789
|
action: input.action,
|
|
1686
1790
|
phase: 'teardown',
|
|
1791
|
+
network,
|
|
1687
1792
|
market: config.market ?? config.symbol ?? null,
|
|
1688
1793
|
orders,
|
|
1689
1794
|
positions,
|
|
@@ -117,6 +117,14 @@ export async function readFundsConfirmation(page, node, mode, timeoutMs = 30_000
|
|
|
117
117
|
if (observed?.title !== expectedTitle || observed?.amount === null) {
|
|
118
118
|
throw new Error(`Perps ${mode} confirmation did not finish loading.`);
|
|
119
119
|
}
|
|
120
|
+
if (node.require_preview_fields === true) {
|
|
121
|
+
const missing = ['transactionFee', 'estimatedTime', 'receive'].filter((field) => !observed[field]);
|
|
122
|
+
if (!observed.payToken && !observed.receiveToken) missing.push('payToken/receiveToken');
|
|
123
|
+
if (observed.loadingVisible) missing.push('loading not settled');
|
|
124
|
+
if (missing.length > 0) {
|
|
125
|
+
throw new Error(`Perps ${mode} preview is incomplete: ${missing.join(', ')}.\nNext: mm-harness logs --full`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
120
128
|
if (node.require_no_horizontal_overflow === true && (
|
|
121
129
|
observed.layout.horizontalOverflow || observed.layout.clippedTestIds.length > 0
|
|
122
130
|
)) {
|
|
@@ -499,7 +499,8 @@ async function visibleActivityCards(page) {
|
|
|
499
499
|
return {
|
|
500
500
|
testId: String(element.getAttribute('data-testid') || '').slice(0, 500),
|
|
501
501
|
label: String(element.innerText || '').replace(/\\s+/gu, ' ').trim().slice(0, 500),
|
|
502
|
-
visible: style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0
|
|
502
|
+
visible: style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0 &&
|
|
503
|
+
rect.bottom > 0 && rect.right > 0 && rect.top < innerHeight && rect.left < innerWidth,
|
|
503
504
|
};
|
|
504
505
|
})`);
|
|
505
506
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { pathToFileURL } from 'node:url';
|
|
2
|
+
|
|
1
3
|
import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
4
|
import {
|
|
3
5
|
normalizeVisiblePerpsState,
|
|
@@ -38,7 +40,7 @@ export async function readVisiblePerpsState(page, node = {}) {
|
|
|
38
40
|
const raw = await page.evaluate(`(() => {
|
|
39
41
|
const includeOffscreen = ${JSON.stringify(options.includeOffscreen)};
|
|
40
42
|
const maximum = ${JSON.stringify(options.maxItems)};
|
|
41
|
-
const relevant = /^(?:perps-|explore-markets-|market-list-view|market-row-|filter-select-button|sort-dropdown-button|search-input|position-card-|order-card-|transaction-card-|bottom-nav-|back-button|direction-|order-type-|amount-|balance-percent-|leverage-|auto-close|submit-order-button|limit-price-|tp-|sl-|custom-amount-|pay-with-|confirm-footer-button|wallet-initiated-header-back-button|transaction-fee-|bridge-(?:fee|time)-|receive-(?:row|value))/u;
|
|
43
|
+
const relevant = /^(?:perps-|parent-selector-perps-(?:market-list|order-entry)|explore-markets-|market-list-view|market-row-|filter-select-button|sort-dropdown-button|search-input|position-card-|order-card-|transaction-card-|bottom-nav-|back-button|direction-|order-type-|amount-|balance-percent-|leverage-|auto-close|submit-order-button|limit-price-|tp-|sl-|custom-amount-|pay-with-|confirm-footer-button|wallet-initiated-header-back-button|transaction-fee-|bridge-(?:fee|time)-|receive-(?:row|value))/u;
|
|
42
44
|
const items = [];
|
|
43
45
|
const offscreenItems = [];
|
|
44
46
|
const clippedTestIds = [];
|
|
@@ -115,7 +117,7 @@ export async function readVisiblePerpsState(page, node = {}) {
|
|
|
115
117
|
items,
|
|
116
118
|
offscreenItems,
|
|
117
119
|
truncated: matched > items.length + offscreenItems.length,
|
|
118
|
-
orderEntry: document.querySelector('[data-testid="perps-order-entry-page"]') ? {
|
|
120
|
+
orderEntry: document.querySelector('[data-testid="parent-selector-perps-order-entry"], [data-testid="perps-order-entry-page"]') ? {
|
|
119
121
|
symbol: field('perps-order-entry-asset-symbol')?.label || null,
|
|
120
122
|
direction,
|
|
121
123
|
orderType: field('order-type-limit')?.pressed === 'true' ? 'limit' : 'market',
|
|
@@ -150,7 +152,9 @@ export async function readVisiblePerpsState(page, node = {}) {
|
|
|
150
152
|
};
|
|
151
153
|
}
|
|
152
154
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
156
|
+
runAdapter(async (input) => withExtensionPage(input, async (page) => ({
|
|
157
|
+
action: input.action,
|
|
158
|
+
...await readVisiblePerpsState(page, input.node),
|
|
159
|
+
})));
|
|
160
|
+
}
|
|
@@ -4,7 +4,12 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
4
4
|
const query = String(input.node?.query ?? '').trim();
|
|
5
5
|
const timeoutMs = Number(input.node?.timeout_ms ?? 30_000);
|
|
6
6
|
if (!query) throw new Error('metamask.perps.search_markets requires a non-empty query.');
|
|
7
|
-
|
|
7
|
+
const searchInput = dataTestId('search-input');
|
|
8
|
+
if (!await page.evaluate(`Boolean(document.querySelector(${JSON.stringify(searchInput)}))`)) {
|
|
9
|
+
await page.click(dataTestId('market-list-search-toggle'));
|
|
10
|
+
await page.waitForSelector(searchInput, { timeoutMs });
|
|
11
|
+
}
|
|
12
|
+
await page.setInput(searchInput, query);
|
|
8
13
|
const deadline = Date.now() + timeoutMs;
|
|
9
14
|
let marketIds = [];
|
|
10
15
|
while (Date.now() <= deadline) {
|
|
@@ -18,7 +23,7 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
18
23
|
.filter(Boolean)
|
|
19
24
|
.slice(0, 100))()`);
|
|
20
25
|
if (marketIds.length > 0) break;
|
|
21
|
-
await
|
|
26
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
22
27
|
}
|
|
23
28
|
if (marketIds.length === 0) {
|
|
24
29
|
throw new Error(`Perps market search returned no visible rows for ${JSON.stringify(query)}.`);
|
|
@@ -62,7 +62,7 @@ export async function selectActivityFilter(page, input, options = {}) {
|
|
|
62
62
|
entries: cards.map((card) => card.label),
|
|
63
63
|
loadedBoundary: visible(document.querySelector('[data-testid="perps-activity-page-skeleton"]'))
|
|
64
64
|
? null
|
|
65
|
-
: visible(document.querySelector('[data-testid="perps-activity-page"]'))
|
|
65
|
+
: visible(document.querySelector('[data-testid="perps-activity-page"],[data-testid="parent-selector-perps-activity"]'))
|
|
66
66
|
? cards.length > 0 ? 'visible-cards' : 'loaded-empty'
|
|
67
67
|
: null,
|
|
68
68
|
empty: String(document.body?.innerText || '').replace(/\\s+/gu, ' ').trim().slice(0, 2_000),
|