@deeeed/metamask-harness 0.36.0 → 0.37.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 +24 -0
- package/adapters/extension/artifact-runtime-state.cjs +128 -0
- package/adapters/extension/inject.mjs +1 -0
- package/adapters/extension/launch-browser.cjs +22 -0
- package/adapters/extension/live.sh +82 -12
- package/adapters/extension/readiness.mjs +77 -36
- package/adapters/extension/snapshot-dist.sh +88 -3
- package/adapters/extension/verify.sh +6 -2
- package/adapters/manifest.json +9 -1
- package/adapters/shared/log-tui.mjs +1 -1
- package/dist/adapters/extension/artifact-integrity.js +38 -0
- package/dist/adapters/extension/extension-id.js +23 -4
- package/dist/adapters/extension/release-artifact.js +386 -0
- package/dist/adapters/extension/runtime-decision.js +161 -20
- package/dist/adapters/extension/runtime.js +127 -0
- package/dist/adapters/mobile/release-artifact-state.js +124 -0
- package/dist/adapters/mobile/release-artifact.js +295 -0
- package/dist/adapters.js +11 -4
- package/dist/command-contract.js +16 -0
- package/dist/commands/call.js +2 -1
- package/dist/commands/launch/extension.js +55 -6
- package/dist/commands/launch/mobile.js +2 -0
- package/dist/commands/provision.js +2 -0
- package/dist/commands/run-engine.js +89 -5
- package/dist/commands/run.js +3 -1
- package/dist/commands/runtime-launch.js +178 -10
- package/dist/heal-bounds.js +1 -1
- package/dist/live-adapter-contract.js +3 -1
- package/dist/metamask-action-validation.js +47 -1
- package/dist/mm-harness-cli.js +31 -1
- package/dist/run-diagnostics.js +1 -1
- package/docs/RELEASE-QA-CAPABILITY-MAP.md +150 -0
- package/library/actions/extension/perps/perps.mjs +2 -0
- package/library/actions/extension/perps/read_snapshot.mjs +470 -0
- package/library/actions/extension/platform/cdp.mjs +6 -3
- package/library/actions/extension/wallet/import.mjs +13 -46
- package/library/actions/extension/wallet/secret-input.mjs +98 -0
- package/library/actions/mobile/platform/observe-ui.mjs +84 -2
- package/library/actions/mobile/ui/native-navigation.mjs +225 -0
- package/library/actions/mobile/ui/navigate.mjs +7 -0
- package/library/actions/mobile/wallet/import.mjs +71 -2
- package/library/actions/mobile/wallet/native-ui.mjs +493 -0
- package/library/actions/mobile/wallet/read_state.mjs +16 -0
- package/library/actions/mobile/wallet/reset.mjs +17 -4
- package/library/actions/shared/ui/locators.mjs +7 -0
- package/library/manifests/extension.action-manifest.json +116 -0
- package/library/manifests/mobile.action-manifest.json +16 -0
- package/library/recipes/extension/runner/action-validation.recipe.json +12 -1
- package/library/recipes/wallet/import.recipe.json +20 -1
- package/library/recipes/wallet/reset-import.recipe.json +20 -1
- package/package.json +1 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Release QA capability map
|
|
2
|
+
|
|
3
|
+
This document maps the Extension release checklists to durable recipe
|
|
4
|
+
capabilities. The baseline is `mm-harness` 0.36.0. It is a capability map, not
|
|
5
|
+
a claim that the listed release cases currently pass.
|
|
6
|
+
|
|
7
|
+
Source baseline: QA repository commit
|
|
8
|
+
[`77591c5`](https://github.com/MetaMask/experimental-mm-qa-ai-tasks/tree/77591c5998fcf6368417fd5be01871e6c9c3dc5b/tasks/rc-extension).
|
|
9
|
+
The relevant sources are the Perps
|
|
10
|
+
[`checklist`](https://github.com/MetaMask/experimental-mm-qa-ai-tasks/blob/77591c5998fcf6368417fd5be01871e6c9c3dc5b/tasks/rc-extension/perps-rc/perps-rc-regression.md)
|
|
11
|
+
and
|
|
12
|
+
[`runbook`](https://github.com/MetaMask/experimental-mm-qa-ai-tasks/blob/77591c5998fcf6368417fd5be01871e6c9c3dc5b/tasks/rc-extension/perps-rc/runbook.md),
|
|
13
|
+
Assets
|
|
14
|
+
[`checklist`](https://github.com/MetaMask/experimental-mm-qa-ai-tasks/blob/77591c5998fcf6368417fd5be01871e6c9c3dc5b/tasks/rc-extension/assets-rc/assets-rc-regression.md)
|
|
15
|
+
and
|
|
16
|
+
[`runbook`](https://github.com/MetaMask/experimental-mm-qa-ai-tasks/blob/77591c5998fcf6368417fd5be01871e6c9c3dc5b/tasks/rc-extension/assets-rc/runbook.md),
|
|
17
|
+
Swap and Bridge
|
|
18
|
+
[`checklist`](https://github.com/MetaMask/experimental-mm-qa-ai-tasks/blob/77591c5998fcf6368417fd5be01871e6c9c3dc5b/tasks/rc-extension/swap-bridge-rc/swap-bridge-rc-regression.md)
|
|
19
|
+
and
|
|
20
|
+
[`runbook`](https://github.com/MetaMask/experimental-mm-qa-ai-tasks/blob/77591c5998fcf6368417fd5be01871e6c9c3dc5b/tasks/rc-extension/swap-bridge-rc/runbook.md),
|
|
21
|
+
and Networks
|
|
22
|
+
[`checklist`](https://github.com/MetaMask/experimental-mm-qa-ai-tasks/blob/77591c5998fcf6368417fd5be01871e6c9c3dc5b/tasks/rc-extension/networks-regression-rc/networks-regression-rc.md)
|
|
23
|
+
and
|
|
24
|
+
[`runbook`](https://github.com/MetaMask/experimental-mm-qa-ai-tasks/blob/77591c5998fcf6368417fd5be01871e6c9c3dc5b/tasks/rc-extension/networks-regression-rc/runbook.md).
|
|
25
|
+
|
|
26
|
+
Ratings:
|
|
27
|
+
|
|
28
|
+
- **AVAILABLE**: a typed capability supplies the required setup, operation, or
|
|
29
|
+
machine-checkable observation.
|
|
30
|
+
- **PARTIAL**: generic UI operations or a narrower domain action can perform
|
|
31
|
+
part of the case, but deterministic setup or a domain assertion is absent.
|
|
32
|
+
- **MISSING**: no durable recipe capability covers the case family.
|
|
33
|
+
- **TARGET**: being implemented in the current change; not present in 0.36.0.
|
|
34
|
+
|
|
35
|
+
## Cross-cutting capabilities
|
|
36
|
+
|
|
37
|
+
| Requirement | 0.36.0 capability | Status |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| Discover the typed surface | `mm-harness actions --adapter extension --json` exposes the schemas in the [Extension action manifest](../library/manifests/extension.action-manifest.json), so a plan can reject unsupported domain operations before launch. | **AVAILABLE** |
|
|
40
|
+
| Validate and launch the downloaded official RC artifact | Network discovery and download remain in the release-validation workflow, following the QA [`download-build.sh`](https://github.com/MetaMask/experimental-mm-qa-ai-tasks/blob/77591c5998fcf6368417fd5be01871e6c9c3dc5b/tasks/rc-extension/assets-rc/scripts/download-build.sh) contract. | **TARGET**: verify a local Extension ZIP or Android APK against its trusted digest and declared product identity, then run it without using a checkout build. Extension adds safe extraction, cached tree integrity, and isolated launch. Android adds explicit-device install and launch; release recipes use native accessibility without a development bridge, Metro, or HUD. Downloaded iOS artifacts remain unsupported. |
|
|
41
|
+
| Start from an imported wallet or clean onboarding | [`metamask.wallet.import`](../library/actions/extension/wallet/import.mjs), [`metamask.wallet.reset`](../library/actions/extension/wallet/reset.mjs), and the shared [`import`](../library/recipes/wallet/import.recipe.json) and [`reset-import`](../library/recipes/wallet/reset-import.recipe.json) recipes can use visible UI and parameterized credential sources. | **AVAILABLE** |
|
|
42
|
+
| Drive and observe UI | The [Extension action manifest](../library/manifests/extension.action-manifest.json) exposes typed navigation, press, input, key, scroll, wait, screenshot, surface capture, locators, and CDP target selection. | **AVAILABLE** for mechanics; domain meaning remains **PARTIAL**. |
|
|
43
|
+
| Record evidence and a terminal verdict | Recipe traces, action results, screenshots, and artifact indexing exist. There is no release-plan completion gate equivalent to the QA [`validate-run-report.sh`](https://github.com/MetaMask/experimental-mm-qa-ai-tasks/blob/77591c5998fcf6368417fd5be01871e6c9c3dc5b/tasks/rc-extension/assets-rc/scripts/validate-run-report.sh), including canonical case-ID coverage and explicit remaining cases. | **PARTIAL** |
|
|
44
|
+
| Parameterized environment control | Wallet import choices and Extension analytics consent are typed. Domain fixtures, service doubles, geo eligibility, network catalogs, and remote stream scenarios are not general release-plan inputs. | **MISSING** |
|
|
45
|
+
|
|
46
|
+
## Perps: 64 Critical cases
|
|
47
|
+
|
|
48
|
+
The existing Perps surface can prepare, read, assert, and close selected
|
|
49
|
+
positions and open orders, and place a basic order. See the
|
|
50
|
+
[`Extension manifest`](../library/manifests/extension.action-manifest.json) and
|
|
51
|
+
[`smoke recipe`](../library/recipes/extension/perps/smoke.recipe.json). It does
|
|
52
|
+
not reproduce the checklist's seeded account snapshot: balance, long and short
|
|
53
|
+
positions, limit orders, take-profit/stop-loss state, activity, watchlist, and
|
|
54
|
+
controlled provider responses and streams.
|
|
55
|
+
|
|
56
|
+
| Case family | Required start state | Required operations and assertions | 0.36.0 coverage |
|
|
57
|
+
| --- | --- | --- | --- |
|
|
58
|
+
| Feature, navigation, session (4) | Feature/eligibility combinations and accounts with different Perps state | Toggle gates, lock/unlock, switch accounts, navigate back; assert gating, account isolation, and route recovery | **PARTIAL**: wallet/account and generic navigation exist; deterministic gates and Perps session assertions are missing. |
|
|
59
|
+
| Perps home (8) | Seeded balance, positions, orders, watchlist, and history | Open cards and balance actions, mutate watchlist; assert all seeded summaries and recent activity | **PARTIAL**: position/order reads and assertions exist; account, market, watchlist, and activity observations are missing. |
|
|
60
|
+
| Market list (4) | Controlled catalog, categories, and price updates | Search, filter, sort, open a market; assert catalog and live values | **PARTIAL**: UI mechanics only; no market catalog or price assertion. |
|
|
61
|
+
| Market details (6) | Selected market data, candles, position, and market-specific order | Navigate and change chart range; assert header, stats, chart states, action mode, and selected-market orders | **PARTIAL**: route and selected order/position state only; market and chart observations are missing. |
|
|
62
|
+
| Order entry (12) | Known balance, constraints, price, leverage limits, and order outcomes | Edit side, amount, percent, leverage, limit price, TP/SL; submit market/limit orders; assert validation, preview, fill, resting order, and duplicate protection | **PARTIAL**: basic placement and open-order convergence exist; rich entry controls, limit/TP-SL inputs, validation, and outcome fields are missing. |
|
|
63
|
+
| Position management (11) | Seeded isolated position, margin, TP/SL, and resting order | Add/reduce exposure, add margin, edit TP/SL, cancel order; assert size, margin, liquidation price, TP/SL, and removal | **PARTIAL**: close positions/orders exists; modify, margin, TP/SL, and rich field assertions are missing. |
|
|
64
|
+
| Deposit (3) | Funded wallet and known Perps balance | Enter amount/Max and confirm; assert validation, pending/success, and resulting balances/activity | **MISSING** |
|
|
65
|
+
| Withdraw (4) | Known withdrawable balance | Enter amount/percent/Max and confirm; assert validation, destination, balance, and activity | **MISSING** |
|
|
66
|
+
| Activity and history (6) | Seeded trades, orders, funding, deposits, and withdrawals | Filter and open details; assert type-specific rows and agreement with home | **MISSING** |
|
|
67
|
+
| Streaming and resilience (4) | Controllable initial responses, updates, reconnects, and order events | Refresh, disconnect/reconnect, close/reopen; assert price, position, order, activity, and loading-state reconciliation | **MISSING** |
|
|
68
|
+
| Geo/compliance (1) | Ineligible trade state | Attempt a trade; assert blocking UI and no submitted order | **MISSING** |
|
|
69
|
+
| Responsive presentation (1) | Popup-width Perps views with populated content | Traverse core views and capture surfaces; assert no clipping or overlap | **PARTIAL**: screenshots and generic UI assertions exist; no reusable Perps presentation assertion. |
|
|
70
|
+
|
|
71
|
+
The aggregate Extension Perps snapshot is a **TARGET**. It exposes account,
|
|
72
|
+
market, position, open-order, activity, and watchlist observations in one typed,
|
|
73
|
+
read-only result when the Extension background test hook is present. An
|
|
74
|
+
untouched production artifact does not expose that hook, so official-artifact
|
|
75
|
+
Perps assertions still require a product-supported observation boundary. The
|
|
76
|
+
snapshot also does not create the required start state.
|
|
77
|
+
|
|
78
|
+
## Assets: 22 Critical cases
|
|
79
|
+
|
|
80
|
+
| Case family | Required start state | Required operations and assertions | 0.36.0 coverage |
|
|
81
|
+
| --- | --- | --- | --- |
|
|
82
|
+
| Token list, prices, details, sorting (4) | Imported account with known live holdings across networks | Open and sort token list/details; assert holdings, fiat, change, chart, and market data | **PARTIAL**: wallet import and UI mechanics only; no asset reads or assertions. |
|
|
83
|
+
| Token import, hide, persistence, custom network (5) | Known ERC-20 contracts and custom network configuration | Search/import/hide/re-add/restart/remove network; assert visibility and persistence | **MISSING**: no token-list/import or custom-network domain actions. |
|
|
84
|
+
| Real-time and aggregate balances (2) | Known multi-chain balances and an externally triggered update | Observe stream and aggregate; assert prompt update and total | **MISSING** |
|
|
85
|
+
| EVM send and activity (3) | Two owned accounts with native/token funds and gas | Send token, native Max, and custom-gas transaction; assert confirmation, balances, gas behavior, and activity | **MISSING** |
|
|
86
|
+
| NFTs (3) | Owned indexed NFT plus owned and unowned contracts | Detect/import/filter/open media; assert ownership rejection, metadata, image, and network filter | **MISSING** |
|
|
87
|
+
| DeFi positions (2) | Account with known live protocol positions | Open list/details; assert protocol, supplied/borrowed assets, and USD totals | **MISSING** |
|
|
88
|
+
| Solana assets and send (2) | Imported Solana account with SOL and SPL balances | Inspect assets and send SPL; assert fiat, confirmation, balances, and activity | **MISSING** |
|
|
89
|
+
| Bitcoin balance (1) | Imported Bitcoin account with known balance | Open asset details; assert balance, fiat, and chart | **MISSING** |
|
|
90
|
+
|
|
91
|
+
## Swap and Bridge: 39 cases (25 Critical, 14 Non-Critical)
|
|
92
|
+
|
|
93
|
+
These cases intentionally form a sequential, live-funds journey. A successful
|
|
94
|
+
step changes the next step's network and balances, so generic navigation alone
|
|
95
|
+
cannot establish or prove the required state transitions.
|
|
96
|
+
|
|
97
|
+
| Case family | Required start state | Required operations and assertions | 0.36.0 coverage |
|
|
98
|
+
| --- | --- | --- | --- |
|
|
99
|
+
| Random ERC-20 round trip on Ethereum (2 Critical) | Funded imported account and live token/quote services | Select eligible random token, swap amount then Max; assert quote, confirmation, balance deltas, and activity | **MISSING** |
|
|
100
|
+
| WETH round trip (2 Critical) | Funded Ethereum account | Swap ETH to WETH and Max back; assert quotes, wrapping result, balance deltas, and activity | **MISSING** |
|
|
101
|
+
| Gasless round trip (2 Critical) | Smart Transactions enabled and funded ETH/USDC balances | Execute full-balance gasless swaps; assert route, no stranded input, balances, and activity | **MISSING** |
|
|
102
|
+
| Ethereum to native Solana (2 Critical) | Funded Ethereum source and derived Solana destination | Bridge Max and await completion; assert destination asset is native SOL, balances, and activity | **MISSING** |
|
|
103
|
+
| Half/Max swaps across destination networks (12 Critical, 8 Non-Critical) | Balance delivered by the preceding bridge on each network | Pick supported random token, swap about half and Max back; assert quote/route, confirmation, balance deltas, and activity | **MISSING** |
|
|
104
|
+
| Sequential onward bridges (5 Critical, 4 Non-Critical) | Source balance produced by the preceding swap | Bridge Max to the specified next network and await completion; assert source/destination assets, balances, and activity | **MISSING** |
|
|
105
|
+
| Quote-only networks (2 Non-Critical) | Selected network and funded source asset | Request but do not submit a quote; assert supported routes, fees, and destination | **MISSING** |
|
|
106
|
+
|
|
107
|
+
## Networks: 10 cases (7 Critical, 3 not marked Critical)
|
|
108
|
+
|
|
109
|
+
| Case family | Required start state | Required operations and assertions | 0.36.0 coverage |
|
|
110
|
+
| --- | --- | --- | --- |
|
|
111
|
+
| Custom-network asset rendering (4) | HyperEVM, INK, and XDC configurations plus known token contracts/balances | Add/select networks and tokens; assert decimals, render stability, logos, and token rows | **MISSING** |
|
|
112
|
+
| Fiat and price APIs (2) | Custom networks with supported native assets and conversion responses | Refresh/select network; assert conversion rates and fiat values | **MISSING** |
|
|
113
|
+
| Send Max and activity (2) | Funded HyperEVM/INK accounts and recipient | Send native Max; assert review amount, gas handling, completion, balance, and activity | **MISSING** |
|
|
114
|
+
| Bridge label, tracking, details (2) | Funded Monad account and a completed bridge route | Submit/open bridge activity; assert label, status tracking, route, fees, and details | **MISSING** |
|
|
115
|
+
|
|
116
|
+
## Prioritized delivery order
|
|
117
|
+
|
|
118
|
+
1. **Downloaded official artifact validation and launch — current target.**
|
|
119
|
+
Accept a local ZIP, trusted digest, and requested version from the
|
|
120
|
+
release-validation workflow; validate the manifest and extracted tree,
|
|
121
|
+
reuse a validated cache, and launch without modifying the source checkout
|
|
122
|
+
or original artifact. Prove onboarding, visible wallet import, normal popup
|
|
123
|
+
operation, and side-panel operation.
|
|
124
|
+
2. **Aggregate Extension Perps snapshot — current target.** Add a typed,
|
|
125
|
+
read-only result for account, markets, positions, open orders, activity, and
|
|
126
|
+
watchlist, with selectors suitable for machine assertions. It is usable in
|
|
127
|
+
source-built runtimes that expose the background test hook; an untouched
|
|
128
|
+
production artifact must fail explicitly until it has a supported read-only
|
|
129
|
+
observation boundary.
|
|
130
|
+
3. **Deterministic Perps start state.** Add setup-time, trace-visible provider
|
|
131
|
+
doubles and seeded balance, positions, limit orders, TP/SL, activity, and
|
|
132
|
+
watchlist. Keep setup outside the proof window and never mutate product
|
|
133
|
+
state to manufacture an assertion.
|
|
134
|
+
4. **Complete Perps operations and assertions.** Add parameterized feature and
|
|
135
|
+
eligibility control, watchlist, rich order entry, position changes, margin,
|
|
136
|
+
TP/SL, deposits, withdrawals, activity filters, and stream recovery. Extend
|
|
137
|
+
shared Mobile/Extension vocabulary where both clients support the behavior.
|
|
138
|
+
5. **Release-plan completion gate.** Validate canonical case IDs, verdict
|
|
139
|
+
counts, required evidence, and explicit remaining cases before reporting a
|
|
140
|
+
terminal result.
|
|
141
|
+
6. **Assets and Networks domain surfaces.** Add shared network selection and
|
|
142
|
+
custom-network state first, then typed assets, balances, token import,
|
|
143
|
+
transaction/activity, NFT, DeFi, and non-EVM observations.
|
|
144
|
+
7. **Swap and Bridge domain surface.** Add quote, route, submit, status,
|
|
145
|
+
balance-delta, and activity actions with explicit live-funds safeguards and
|
|
146
|
+
resumable sequential state.
|
|
147
|
+
|
|
148
|
+
Completing the two current targets does **not** provide full Perps parity. The
|
|
149
|
+
64-case plan still depends on deterministic domain state, controlled provider
|
|
150
|
+
behavior, richer mutations and assertions, and a canonical completion gate.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { pathToFileURL } from 'node:url';
|
|
2
2
|
import { dataTestId, marketSymbol, normalizeMarketSymbol, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
3
|
+
import { readSnapshot } from './read_snapshot.mjs';
|
|
3
4
|
import { readPerpsRuntimeStateExpression } from './state.mjs';
|
|
4
5
|
|
|
5
6
|
function sleep(ms) {
|
|
@@ -832,6 +833,7 @@ export async function teardownState(input) {
|
|
|
832
833
|
}
|
|
833
834
|
|
|
834
835
|
const DIRECT_ACTIONS = new Map([
|
|
836
|
+
['metamask.perps.read_snapshot', readSnapshot],
|
|
835
837
|
['metamask.perps.read_positions', readPositions],
|
|
836
838
|
['metamask.perps.read_orders', readOrders],
|
|
837
839
|
['metamask.perps.close_positions', closePositions],
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import { pathToFileURL } from 'node:url';
|
|
2
|
+
|
|
3
|
+
import { normalizeMarketSymbol, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
4
|
+
|
|
5
|
+
const ALL_SECTIONS = ['account', 'markets', 'positions', 'orders', 'history', 'watchlist'];
|
|
6
|
+
const DEFAULT_HISTORY_WINDOW_MS = 30 * 24 * 60 * 60 * 1000;
|
|
7
|
+
const MAX_HISTORY_WINDOW_MS = 365 * 24 * 60 * 60 * 1000;
|
|
8
|
+
|
|
9
|
+
function boundedInteger(value, fallback, minimum, maximum, name) {
|
|
10
|
+
const resolved = value == null ? fallback : Number(value);
|
|
11
|
+
if (!Number.isInteger(resolved) || resolved < minimum || resolved > maximum) {
|
|
12
|
+
throw new Error(`metamask.perps.read_snapshot ${name} must be an integer from ${minimum} to ${maximum}.`);
|
|
13
|
+
}
|
|
14
|
+
return resolved;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function requestedMarkets(node) {
|
|
18
|
+
if (node.market != null && node.markets != null) {
|
|
19
|
+
throw new Error('metamask.perps.read_snapshot accepts market or markets, not both.');
|
|
20
|
+
}
|
|
21
|
+
const values = node.markets ?? (node.market == null ? [] : [node.market]);
|
|
22
|
+
if (!Array.isArray(values)) {
|
|
23
|
+
throw new Error('metamask.perps.read_snapshot markets must be an array of market symbols.');
|
|
24
|
+
}
|
|
25
|
+
const markets = Array.from(new Set(
|
|
26
|
+
values.map((value) => typeof value === 'string' ? normalizeMarketSymbol(value) : '').filter(Boolean),
|
|
27
|
+
));
|
|
28
|
+
if (markets.length > 100) {
|
|
29
|
+
throw new Error('metamask.perps.read_snapshot accepts at most 100 market symbols.');
|
|
30
|
+
}
|
|
31
|
+
return markets;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function resolveSnapshotOptions(input, now = Date.now()) {
|
|
35
|
+
const node = input.node ?? {};
|
|
36
|
+
const include = node.include ?? ALL_SECTIONS;
|
|
37
|
+
if (!Array.isArray(include) || include.length === 0) {
|
|
38
|
+
throw new Error('metamask.perps.read_snapshot include must be a non-empty array.');
|
|
39
|
+
}
|
|
40
|
+
const sections = Array.from(new Set(include.map((section) => String(section).toLowerCase())));
|
|
41
|
+
const unsupported = sections.filter((section) => !ALL_SECTIONS.includes(section));
|
|
42
|
+
if (unsupported.length > 0) {
|
|
43
|
+
throw new Error(`metamask.perps.read_snapshot received unsupported sections: ${unsupported.join(', ')}.`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const history = node.history && typeof node.history === 'object' && !Array.isArray(node.history)
|
|
47
|
+
? node.history
|
|
48
|
+
: {};
|
|
49
|
+
const endTime = boundedInteger(
|
|
50
|
+
history.end_time_ms,
|
|
51
|
+
now,
|
|
52
|
+
0,
|
|
53
|
+
Number.MAX_SAFE_INTEGER,
|
|
54
|
+
'history.end_time_ms',
|
|
55
|
+
);
|
|
56
|
+
const startTime = boundedInteger(
|
|
57
|
+
history.start_time_ms,
|
|
58
|
+
Math.max(0, endTime - DEFAULT_HISTORY_WINDOW_MS),
|
|
59
|
+
0,
|
|
60
|
+
Number.MAX_SAFE_INTEGER,
|
|
61
|
+
'history.start_time_ms',
|
|
62
|
+
);
|
|
63
|
+
if (startTime > endTime) {
|
|
64
|
+
throw new Error('metamask.perps.read_snapshot history.start_time_ms must not exceed history.end_time_ms.');
|
|
65
|
+
}
|
|
66
|
+
if (endTime - startTime > MAX_HISTORY_WINDOW_MS) {
|
|
67
|
+
throw new Error('metamask.perps.read_snapshot history range must not exceed 365 days.');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
include: sections,
|
|
72
|
+
markets: requestedMarkets(node),
|
|
73
|
+
timeoutMs: boundedInteger(node.timeout_ms, 20000, 1000, 60000, 'timeout_ms'),
|
|
74
|
+
maxItems: boundedInteger(node.max_items_per_section, 100, 1, 500, 'max_items_per_section'),
|
|
75
|
+
history: {
|
|
76
|
+
startTime,
|
|
77
|
+
endTime,
|
|
78
|
+
accountId: history.account_id == null || history.account_id === ''
|
|
79
|
+
? undefined
|
|
80
|
+
: String(history.account_id),
|
|
81
|
+
limit: boundedInteger(history.limit, 100, 1, 500, 'history.limit'),
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function snapshotRequestExpression(options) {
|
|
87
|
+
const methods = {
|
|
88
|
+
account: ['perpsGetAccountState', []],
|
|
89
|
+
markets: ['perpsGetMarketDataWithPrices', []],
|
|
90
|
+
positions: ['perpsGetPositions', []],
|
|
91
|
+
orders: ['perpsGetOpenOrders', []],
|
|
92
|
+
history: ['perpsGetUserHistory', null],
|
|
93
|
+
watchlist: ['perpsGetWatchlistMarkets', []],
|
|
94
|
+
};
|
|
95
|
+
return `(async () => {
|
|
96
|
+
const request = globalThis.stateHooks?.submitRequestToBackground;
|
|
97
|
+
if (typeof request !== 'function') {
|
|
98
|
+
throw new Error('stateHooks.submitRequestToBackground is unavailable; cannot read live Perps snapshot.');
|
|
99
|
+
}
|
|
100
|
+
const timeoutMs = ${JSON.stringify(options.timeoutMs)};
|
|
101
|
+
const historyParams = {
|
|
102
|
+
startTime: ${JSON.stringify(options.history.startTime)},
|
|
103
|
+
endTime: ${JSON.stringify(options.history.endTime)},
|
|
104
|
+
accountId: ${options.history.accountId === undefined ? 'undefined' : JSON.stringify(options.history.accountId)},
|
|
105
|
+
};
|
|
106
|
+
const methods = ${JSON.stringify(methods)};
|
|
107
|
+
const markets = ${JSON.stringify(options.markets)};
|
|
108
|
+
const maxItems = ${JSON.stringify(options.maxItems)};
|
|
109
|
+
const historyLimit = ${JSON.stringify(Math.min(options.history.limit, options.maxItems))};
|
|
110
|
+
function field(record, names) {
|
|
111
|
+
for (const name of names) {
|
|
112
|
+
const value = record?.[name];
|
|
113
|
+
if (value === null || ['string', 'number', 'boolean'].includes(typeof value)) return value;
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
function marketFor(item) {
|
|
118
|
+
const value = field(item, ['symbol', 'coin', 'market', 'asset']);
|
|
119
|
+
return typeof value === 'string' ? value.replace(/-PERP$/iu, '').toUpperCase() : '';
|
|
120
|
+
}
|
|
121
|
+
function sideFor(item) {
|
|
122
|
+
const side = String(field(item, ['side', 'direction']) ?? '').toLowerCase();
|
|
123
|
+
if (['buy', 'b', 'long'].includes(side)) return 'long';
|
|
124
|
+
if (['sell', 'a', 's', 'short'].includes(side)) return 'short';
|
|
125
|
+
return side || null;
|
|
126
|
+
}
|
|
127
|
+
function leverageFor(item) {
|
|
128
|
+
const leverage = item?.leverage;
|
|
129
|
+
if (leverage === null || ['string', 'number'].includes(typeof leverage)) return leverage;
|
|
130
|
+
if (!leverage || typeof leverage !== 'object' || Array.isArray(leverage)) return null;
|
|
131
|
+
return { type: field(leverage, ['type']), value: field(leverage, ['value']) };
|
|
132
|
+
}
|
|
133
|
+
function normalize(section, item) {
|
|
134
|
+
if (section === 'markets') return {
|
|
135
|
+
market: marketFor(item) || null,
|
|
136
|
+
name: field(item, ['name']),
|
|
137
|
+
price: field(item, ['price', 'currentPrice']),
|
|
138
|
+
markPrice: field(item, ['markPrice', 'markPx']),
|
|
139
|
+
oraclePrice: field(item, ['oraclePrice', 'oraclePx']),
|
|
140
|
+
change24hPercent: field(item, ['change24hPercent', 'priceChange24h']),
|
|
141
|
+
volume24h: field(item, ['volume24h', 'dayNtlVlm']),
|
|
142
|
+
openInterest: field(item, ['openInterest']),
|
|
143
|
+
fundingRate: field(item, ['fundingRate', 'funding']),
|
|
144
|
+
maxLeverage: field(item, ['maxLeverage']),
|
|
145
|
+
};
|
|
146
|
+
if (section === 'positions') return {
|
|
147
|
+
market: marketFor(item) || null,
|
|
148
|
+
side: sideFor(item),
|
|
149
|
+
size: field(item, ['size', 'szi']),
|
|
150
|
+
entryPrice: field(item, ['entryPrice', 'entryPx']),
|
|
151
|
+
positionValue: field(item, ['positionValue']),
|
|
152
|
+
leverage: leverageFor(item),
|
|
153
|
+
liquidationPrice: field(item, ['liquidationPrice', 'liquidationPx']),
|
|
154
|
+
marginUsed: field(item, ['marginUsed']),
|
|
155
|
+
unrealizedPnl: field(item, ['unrealizedPnl']),
|
|
156
|
+
returnOnEquity: field(item, ['returnOnEquity']),
|
|
157
|
+
takeProfitPrice: field(item, ['takeProfitPrice']),
|
|
158
|
+
stopLossPrice: field(item, ['stopLossPrice']),
|
|
159
|
+
};
|
|
160
|
+
if (section === 'orders') return {
|
|
161
|
+
orderId: field(item, ['orderId', 'id', 'oid']),
|
|
162
|
+
market: marketFor(item) || null,
|
|
163
|
+
side: sideFor(item),
|
|
164
|
+
size: field(item, ['size', 'sz', 'szi']),
|
|
165
|
+
price: field(item, ['price', 'limitPx', 'px']),
|
|
166
|
+
type: field(item, ['orderType', 'type']),
|
|
167
|
+
status: field(item, ['status']),
|
|
168
|
+
timestamp: field(item, ['timestamp', 'time']),
|
|
169
|
+
reduceOnly: field(item, ['reduceOnly']),
|
|
170
|
+
isTrigger: field(item, ['isTrigger']),
|
|
171
|
+
triggerPrice: field(item, ['triggerPrice', 'triggerPx']),
|
|
172
|
+
triggerOrderType: field(item, ['triggerOrderType']),
|
|
173
|
+
takeProfitPrice: field(item, ['takeProfitPrice']),
|
|
174
|
+
stopLossPrice: field(item, ['stopLossPrice']),
|
|
175
|
+
};
|
|
176
|
+
return {
|
|
177
|
+
type: field(item, ['type', 'activityType']),
|
|
178
|
+
status: field(item, ['status']),
|
|
179
|
+
asset: field(item, ['asset', 'token', 'currency']),
|
|
180
|
+
market: marketFor(item) || null,
|
|
181
|
+
amount: field(item, ['amount', 'value']),
|
|
182
|
+
fee: field(item, ['fee']),
|
|
183
|
+
timestamp: field(item, ['timestamp', 'time']),
|
|
184
|
+
transactionHash: field(item, ['transactionHash', 'txHash', 'hash']),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function transport(section, value) {
|
|
188
|
+
if (section === 'account') {
|
|
189
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return { value };
|
|
190
|
+
const allowed = ${JSON.stringify([
|
|
191
|
+
'accountValue', 'totalBalance', 'balance', 'spendableBalance',
|
|
192
|
+
'withdrawableBalance', 'availableBalance', 'availableToTradeBalance',
|
|
193
|
+
'marginUsed', 'maintenanceMargin', 'unrealizedPnl', 'returnOnEquity',
|
|
194
|
+
])};
|
|
195
|
+
return { value: Object.fromEntries(allowed.map((name) => [name, field(value, [name])]).filter(([, item]) => item !== null)) };
|
|
196
|
+
}
|
|
197
|
+
if (!Array.isArray(value)) return { value };
|
|
198
|
+
if (section === 'watchlist') {
|
|
199
|
+
const items = value.map((item) => typeof item === 'string' ? item : marketFor(item)).filter(Boolean);
|
|
200
|
+
const selected = markets.length === 0 ? items : items.filter((item) => markets.includes(String(item).replace(/-PERP$/iu, '').toUpperCase()));
|
|
201
|
+
const bounded = selected.slice(0, maxItems).map((item) => String(item).replace(/-PERP$/iu, '').toUpperCase());
|
|
202
|
+
return { value: bounded, totalCount: value.length, matchingCount: selected.length, truncated: bounded.length < selected.length, transportBound: true };
|
|
203
|
+
}
|
|
204
|
+
const selected = markets.length === 0 ? value : value.filter((item) => markets.includes(marketFor(item)));
|
|
205
|
+
const limit = section === 'history' ? historyLimit : maxItems;
|
|
206
|
+
const bounded = selected.slice(0, limit).map((item) => normalize(section, item));
|
|
207
|
+
return { value: bounded, totalCount: value.length, matchingCount: selected.length, truncated: bounded.length < selected.length, transportBound: true };
|
|
208
|
+
}
|
|
209
|
+
async function read(section) {
|
|
210
|
+
const [method, configuredArgs] = methods[section];
|
|
211
|
+
const args = section === 'history' ? [historyParams] : configuredArgs;
|
|
212
|
+
let timeoutId;
|
|
213
|
+
try {
|
|
214
|
+
const value = await Promise.race([
|
|
215
|
+
request(method, args),
|
|
216
|
+
new Promise((_, reject) => {
|
|
217
|
+
timeoutId = setTimeout(
|
|
218
|
+
() => reject(new Error(method + ' timed out after ' + timeoutMs + 'ms')),
|
|
219
|
+
timeoutMs,
|
|
220
|
+
);
|
|
221
|
+
}),
|
|
222
|
+
]);
|
|
223
|
+
return [section, { ok: true, method, ...transport(section, value) }];
|
|
224
|
+
} catch (error) {
|
|
225
|
+
return [section, {
|
|
226
|
+
ok: false,
|
|
227
|
+
method,
|
|
228
|
+
error: error instanceof Error ? error.message : String(error),
|
|
229
|
+
}];
|
|
230
|
+
} finally {
|
|
231
|
+
clearTimeout(timeoutId);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return Object.fromEntries(await Promise.all(${JSON.stringify(options.include)}.map(read)));
|
|
235
|
+
})()`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function field(record, names) {
|
|
239
|
+
for (const name of names) {
|
|
240
|
+
const value = record?.[name];
|
|
241
|
+
if (value === null || ['string', 'number', 'boolean'].includes(typeof value)) return value;
|
|
242
|
+
}
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function normalizedSide(item) {
|
|
247
|
+
const side = String(field(item, ['side', 'direction']) ?? '').toLowerCase();
|
|
248
|
+
if (side === 'buy' || side === 'b' || side === 'long') return 'long';
|
|
249
|
+
if (side === 'sell' || side === 'a' || side === 's' || side === 'short') return 'short';
|
|
250
|
+
return side || null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function normalizedLeverage(item) {
|
|
254
|
+
const leverage = item?.leverage;
|
|
255
|
+
if (leverage === null || ['string', 'number'].includes(typeof leverage)) return leverage;
|
|
256
|
+
if (!leverage || typeof leverage !== 'object' || Array.isArray(leverage)) return null;
|
|
257
|
+
return {
|
|
258
|
+
type: field(leverage, ['type']),
|
|
259
|
+
value: field(leverage, ['value']),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function marketForItem(item) {
|
|
264
|
+
const value = field(item, ['symbol', 'coin', 'market', 'asset']);
|
|
265
|
+
return typeof value === 'string' && value.length > 0 ? normalizeMarketSymbol(value) : '';
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function normalizeAccount(account) {
|
|
269
|
+
const allowed = [
|
|
270
|
+
'accountValue',
|
|
271
|
+
'totalBalance',
|
|
272
|
+
'balance',
|
|
273
|
+
'spendableBalance',
|
|
274
|
+
'withdrawableBalance',
|
|
275
|
+
'availableBalance',
|
|
276
|
+
'availableToTradeBalance',
|
|
277
|
+
'marginUsed',
|
|
278
|
+
'maintenanceMargin',
|
|
279
|
+
'unrealizedPnl',
|
|
280
|
+
'returnOnEquity',
|
|
281
|
+
];
|
|
282
|
+
return Object.fromEntries(
|
|
283
|
+
allowed
|
|
284
|
+
.map((name) => [name, field(account, [name])])
|
|
285
|
+
.filter(([, value]) => value !== null),
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function normalizeMarket(item) {
|
|
290
|
+
return {
|
|
291
|
+
market: marketForItem(item) || null,
|
|
292
|
+
name: field(item, ['name']),
|
|
293
|
+
price: field(item, ['price', 'currentPrice']),
|
|
294
|
+
markPrice: field(item, ['markPrice', 'markPx']),
|
|
295
|
+
oraclePrice: field(item, ['oraclePrice', 'oraclePx']),
|
|
296
|
+
change24hPercent: field(item, ['change24hPercent', 'priceChange24h']),
|
|
297
|
+
volume24h: field(item, ['volume24h', 'dayNtlVlm']),
|
|
298
|
+
openInterest: field(item, ['openInterest']),
|
|
299
|
+
fundingRate: field(item, ['fundingRate', 'funding']),
|
|
300
|
+
maxLeverage: field(item, ['maxLeverage']),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function normalizePosition(item) {
|
|
305
|
+
return {
|
|
306
|
+
market: marketForItem(item) || null,
|
|
307
|
+
side: normalizedSide(item),
|
|
308
|
+
size: field(item, ['size', 'szi']),
|
|
309
|
+
entryPrice: field(item, ['entryPrice', 'entryPx']),
|
|
310
|
+
positionValue: field(item, ['positionValue']),
|
|
311
|
+
leverage: normalizedLeverage(item),
|
|
312
|
+
liquidationPrice: field(item, ['liquidationPrice', 'liquidationPx']),
|
|
313
|
+
marginUsed: field(item, ['marginUsed']),
|
|
314
|
+
unrealizedPnl: field(item, ['unrealizedPnl']),
|
|
315
|
+
returnOnEquity: field(item, ['returnOnEquity']),
|
|
316
|
+
takeProfitPrice: field(item, ['takeProfitPrice']),
|
|
317
|
+
stopLossPrice: field(item, ['stopLossPrice']),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function normalizeOrder(item) {
|
|
322
|
+
return {
|
|
323
|
+
orderId: field(item, ['orderId', 'id', 'oid']),
|
|
324
|
+
market: marketForItem(item) || null,
|
|
325
|
+
side: normalizedSide(item),
|
|
326
|
+
size: field(item, ['size', 'sz', 'szi']),
|
|
327
|
+
price: field(item, ['price', 'limitPx', 'px']),
|
|
328
|
+
type: field(item, ['orderType', 'type']),
|
|
329
|
+
status: field(item, ['status']),
|
|
330
|
+
timestamp: field(item, ['timestamp', 'time']),
|
|
331
|
+
reduceOnly: field(item, ['reduceOnly']),
|
|
332
|
+
isTrigger: field(item, ['isTrigger']),
|
|
333
|
+
triggerPrice: field(item, ['triggerPrice', 'triggerPx']),
|
|
334
|
+
triggerOrderType: field(item, ['triggerOrderType']),
|
|
335
|
+
takeProfitPrice: field(item, ['takeProfitPrice']),
|
|
336
|
+
stopLossPrice: field(item, ['stopLossPrice']),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function normalizeHistoryItem(item) {
|
|
341
|
+
return {
|
|
342
|
+
type: field(item, ['type', 'activityType']),
|
|
343
|
+
status: field(item, ['status']),
|
|
344
|
+
asset: field(item, ['asset', 'token', 'currency']),
|
|
345
|
+
market: marketForItem(item) || null,
|
|
346
|
+
amount: field(item, ['amount', 'value']),
|
|
347
|
+
fee: field(item, ['fee']),
|
|
348
|
+
timestamp: field(item, ['timestamp', 'time']),
|
|
349
|
+
transactionHash: field(item, ['transactionHash', 'txHash', 'hash']),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function selectAndBound(items, options, normalize, limit = options.maxItems, result = {}) {
|
|
354
|
+
const selected = result.transportBound === true || options.markets.length === 0
|
|
355
|
+
? items
|
|
356
|
+
: items.filter((item) => options.markets.includes(marketForItem(item)));
|
|
357
|
+
const bounded = selected.slice(0, Math.min(limit, options.maxItems));
|
|
358
|
+
return {
|
|
359
|
+
totalCount: result.transportBound === true ? result.totalCount : items.length,
|
|
360
|
+
matchingCount: result.transportBound === true ? result.matchingCount : selected.length,
|
|
361
|
+
returnedCount: bounded.length,
|
|
362
|
+
truncated: result.transportBound === true ? result.truncated === true : bounded.length < selected.length,
|
|
363
|
+
items: bounded.map(normalize),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function requireObjectSection(section, result) {
|
|
368
|
+
if (!result?.ok) {
|
|
369
|
+
throw new Error(`Requested Perps snapshot section ${section} is unavailable: ${result?.error ?? 'unknown error'}.`);
|
|
370
|
+
}
|
|
371
|
+
if (!result.value || typeof result.value !== 'object' || Array.isArray(result.value)) {
|
|
372
|
+
throw new Error(`Requested Perps snapshot section ${section} is unavailable: ${result.method} returned a non-object result.`);
|
|
373
|
+
}
|
|
374
|
+
return result.value;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function requireArraySection(section, result) {
|
|
378
|
+
if (!result?.ok) {
|
|
379
|
+
throw new Error(`Requested Perps snapshot section ${section} is unavailable: ${result?.error ?? 'unknown error'}.`);
|
|
380
|
+
}
|
|
381
|
+
if (!Array.isArray(result.value)) {
|
|
382
|
+
throw new Error(`Requested Perps snapshot section ${section} is unavailable: ${result.method} returned a non-array result.`);
|
|
383
|
+
}
|
|
384
|
+
return result.value;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export function normalizeSnapshotResult(raw, options) {
|
|
388
|
+
const sections = {};
|
|
389
|
+
for (const section of options.include) {
|
|
390
|
+
const result = raw?.[section];
|
|
391
|
+
if (section === 'account') {
|
|
392
|
+
sections.account = {
|
|
393
|
+
source: result?.method ?? 'perpsGetAccountState',
|
|
394
|
+
state: normalizeAccount(requireObjectSection(section, result)),
|
|
395
|
+
};
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (section === 'watchlist') {
|
|
399
|
+
const values = requireArraySection(section, result);
|
|
400
|
+
const items = values
|
|
401
|
+
.map((item) => typeof item === 'string' ? item : marketForItem(item))
|
|
402
|
+
.filter((item) => typeof item === 'string' && item.length > 0);
|
|
403
|
+
const selected = result.transportBound === true || options.markets.length === 0
|
|
404
|
+
? items
|
|
405
|
+
: items.filter((item) => options.markets.includes(normalizeMarketSymbol(item)));
|
|
406
|
+
const bounded = selected
|
|
407
|
+
.slice(0, options.maxItems)
|
|
408
|
+
.map((item) => typeof item === 'string' && item.length > 0 ? normalizeMarketSymbol(item) : '')
|
|
409
|
+
.filter(Boolean);
|
|
410
|
+
sections.watchlist = {
|
|
411
|
+
source: result.method,
|
|
412
|
+
totalCount: result.transportBound === true ? result.totalCount : values.length,
|
|
413
|
+
matchingCount: result.transportBound === true ? result.matchingCount : selected.length,
|
|
414
|
+
returnedCount: bounded.length,
|
|
415
|
+
truncated: result.transportBound === true ? result.truncated === true : bounded.length < selected.length,
|
|
416
|
+
markets: bounded,
|
|
417
|
+
};
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
const values = requireArraySection(section, result);
|
|
421
|
+
const normalizer = {
|
|
422
|
+
markets: normalizeMarket,
|
|
423
|
+
positions: normalizePosition,
|
|
424
|
+
orders: normalizeOrder,
|
|
425
|
+
history: normalizeHistoryItem,
|
|
426
|
+
}[section];
|
|
427
|
+
sections[section] = {
|
|
428
|
+
source: result.method,
|
|
429
|
+
...selectAndBound(
|
|
430
|
+
values,
|
|
431
|
+
options,
|
|
432
|
+
normalizer,
|
|
433
|
+
section === 'history' ? options.history.limit : options.maxItems,
|
|
434
|
+
result,
|
|
435
|
+
),
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
return sections;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export async function readSnapshot(input) {
|
|
442
|
+
const options = resolveSnapshotOptions(input);
|
|
443
|
+
return withExtensionPage(input, async (page) => {
|
|
444
|
+
const raw = await page.evaluate(snapshotRequestExpression(options));
|
|
445
|
+
return {
|
|
446
|
+
action: input.action,
|
|
447
|
+
source: 'extension-stateHooks-background',
|
|
448
|
+
include: options.include,
|
|
449
|
+
markets: options.markets,
|
|
450
|
+
historyQuery: options.include.includes('history')
|
|
451
|
+
? {
|
|
452
|
+
startTime: options.history.startTime,
|
|
453
|
+
endTime: options.history.endTime,
|
|
454
|
+
accountScoped: options.history.accountId !== undefined,
|
|
455
|
+
}
|
|
456
|
+
: null,
|
|
457
|
+
sections: normalizeSnapshotResult(raw, options),
|
|
458
|
+
redacted: true,
|
|
459
|
+
proofPath: 'background-perps-snapshot',
|
|
460
|
+
};
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function isDirectRun() {
|
|
465
|
+
return process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (isDirectRun()) {
|
|
469
|
+
runAdapter(readSnapshot);
|
|
470
|
+
}
|
|
@@ -67,9 +67,12 @@ export function extensionTarget(targets, extensionId = null) {
|
|
|
67
67
|
if (extensionId && extensionIdFromTarget(target) !== extensionId) return false;
|
|
68
68
|
return Boolean(target.webSocketDebuggerUrl);
|
|
69
69
|
});
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
const home = candidates.find((target) => String(target.url).includes('/home.html'));
|
|
71
|
+
const sidepanel = candidates.find((target) => String(target.url).includes('/sidepanel.html'));
|
|
72
|
+
if (home && String(home.url).includes('/onboarding/completion') && sidepanel) {
|
|
73
|
+
return sidepanel;
|
|
74
|
+
}
|
|
75
|
+
return home ?? sidepanel ?? null;
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
|