@deeeed/metamask-harness 0.2.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 +161 -0
- package/README.md +140 -0
- package/bin/mm-harness +99 -0
- package/docs/CHEATSHEET.md +61 -0
- package/docs/CLI-SPEC.md +915 -0
- package/docs/MENTAL-MODEL.md +295 -0
- package/docs/architecture.md +367 -0
- package/docs/extension-runtime-commands.md +60 -0
- package/docs/harness-cli.md +43 -0
- package/docs/live-adapter-contract.md +188 -0
- package/docs/package-boundaries.md +47 -0
- package/docs/perps-flow-catalog.md +235 -0
- package/docs/recipe-libraries.md +95 -0
- package/docs/runtime-file-conventions.md +36 -0
- package/library/actions/core/perps/_controller.mjs +727 -0
- package/library/actions/core/perps/assert_orders.mjs +53 -0
- package/library/actions/core/perps/assert_positions.mjs +52 -0
- package/library/actions/core/perps/close_orders.mjs +97 -0
- package/library/actions/core/perps/close_positions.mjs +118 -0
- package/library/actions/core/perps/ensure_orders.mjs +40 -0
- package/library/actions/core/perps/ensure_positions.mjs +37 -0
- package/library/actions/core/perps/place_order.mjs +201 -0
- package/library/actions/core/perps/read_account.mjs +30 -0
- package/library/actions/core/perps/read_orders.mjs +27 -0
- package/library/actions/core/perps/read_positions.mjs +27 -0
- package/library/actions/core/perps/start_state.mjs +92 -0
- package/library/actions/core/perps/teardown_state.mjs +86 -0
- package/library/actions/extension/perps/assert_orders.mjs +11 -0
- package/library/actions/extension/perps/assert_positions.mjs +11 -0
- package/library/actions/extension/perps/close_orders.mjs +8 -0
- package/library/actions/extension/perps/close_positions.mjs +8 -0
- package/library/actions/extension/perps/ensure_orders.mjs +4 -0
- package/library/actions/extension/perps/ensure_positions.mjs +4 -0
- package/library/actions/extension/perps/perps.mjs +730 -0
- package/library/actions/extension/perps/place_order.mjs +7 -0
- package/library/actions/extension/perps/read_orders.mjs +4 -0
- package/library/actions/extension/perps/read_positions.mjs +3 -0
- package/library/actions/extension/platform/cdp.mjs +541 -0
- package/library/actions/extension/ui/navigate.mjs +44 -0
- package/library/actions/extension/wallet/ensure_unlocked.mjs +36 -0
- package/library/actions/extension/wallet/read_state.mjs +27 -0
- package/library/actions/extension/wallet/select_account.mjs +48 -0
- package/library/actions/extension/wallet/setup.mjs +35 -0
- package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +185 -0
- package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +1662 -0
- package/library/actions/mobile/bridge-runtime/cdp-bridge.cjs +686 -0
- package/library/actions/mobile/bridge-runtime/lib/cdp-eval.cjs +110 -0
- package/library/actions/mobile/bridge-runtime/lib/config.cjs +39 -0
- package/library/actions/mobile/bridge-runtime/lib/issue-capture.cjs +446 -0
- package/library/actions/mobile/bridge-runtime/lib/target-discovery.cjs +204 -0
- package/library/actions/mobile/bridge-runtime/lib/ws-client.cjs +108 -0
- package/library/actions/mobile/bridge-runtime/setup-wallet.sh +442 -0
- package/library/actions/mobile/perps/assert_orders.mjs +11 -0
- package/library/actions/mobile/perps/assert_positions.mjs +11 -0
- package/library/actions/mobile/perps/close_orders.mjs +8 -0
- package/library/actions/mobile/perps/close_positions.mjs +8 -0
- package/library/actions/mobile/perps/ensure_orders.mjs +4 -0
- package/library/actions/mobile/perps/ensure_positions.mjs +4 -0
- package/library/actions/mobile/perps/perps.mjs +709 -0
- package/library/actions/mobile/perps/place_order.mjs +7 -0
- package/library/actions/mobile/perps/read_orders.mjs +4 -0
- package/library/actions/mobile/perps/read_positions.mjs +3 -0
- package/library/actions/mobile/platform/bridge.mjs +283 -0
- package/library/actions/mobile/ui/navigate.mjs +38 -0
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +107 -0
- package/library/actions/mobile/wallet/home.mjs +35 -0
- package/library/actions/mobile/wallet/read_state.mjs +40 -0
- package/library/actions/mobile/wallet/select_account.mjs +48 -0
- package/library/actions/mobile/wallet/setup.mjs +220 -0
- package/library/flows/perps.flows.json +64 -0
- package/library/library.json +7 -0
- package/library/manifests/core.action-manifest.json +1282 -0
- package/library/manifests/extension.action-manifest.json +1749 -0
- package/library/manifests/mobile.action-manifest.json +1753 -0
- package/library/recipes/action-validation.extension.recipe.json +417 -0
- package/library/recipes/action-validation.mobile.recipe.json +422 -0
- package/library/recipes/order-lifecycle.core.recipe.json +78 -0
- package/library/recipes/perps-lifecycle.recipe.json +194 -0
- package/library/recipes/read-markets.core.recipe.json +38 -0
- package/library/recipes/smoke.extension.recipe.json +31 -0
- package/library/recipes/smoke.mobile.recipe.json +31 -0
- package/library/recipes/trading-lifecycle.core.recipe.json +76 -0
- package/orchestration/compat-overlays/README.md +19 -0
- package/orchestration/compat-overlays/mobile/README.md +13 -0
- package/orchestration/compat-overlays/mobile/rn81-message-event-source.patch +42 -0
- package/orchestration/core/cleanup.sh +37 -0
- package/orchestration/core/inject.sh +154 -0
- package/orchestration/doctor.mjs +72 -0
- package/orchestration/extension/cleanup.mjs +60 -0
- package/orchestration/extension/console-tail.mjs +228 -0
- package/orchestration/extension/ensure-browser.sh +416 -0
- package/orchestration/extension/ensure-ready.ts +185 -0
- package/orchestration/extension/extension-id.ts +107 -0
- package/orchestration/extension/inject.mjs +266 -0
- package/orchestration/extension/launch-browser.cjs +216 -0
- package/orchestration/extension/launch.sh +175 -0
- package/orchestration/extension/live.sh +320 -0
- package/orchestration/extension/pin-remote-flags.cjs +45 -0
- package/orchestration/extension/readiness.mjs +414 -0
- package/orchestration/extension/refresh-build.sh +190 -0
- package/orchestration/extension/runtime-decision.ts +445 -0
- package/orchestration/extension/runtime.ts +407 -0
- package/orchestration/extension/seed-fixture.sh +177 -0
- package/orchestration/extension/sidepanel-toggle.sh +291 -0
- package/orchestration/extension/snapshot-dist.sh +84 -0
- package/orchestration/extension/start-watch.sh +339 -0
- package/orchestration/extension/wallet-fixture-state.cjs +1086 -0
- package/orchestration/lib/activate-repo-node.sh +144 -0
- package/orchestration/lib/cli-color.mjs +84 -0
- package/orchestration/lib/cli-commands.mjs +243 -0
- package/orchestration/lib/cli-home.mjs +354 -0
- package/orchestration/lib/cli-ux.sh +252 -0
- package/orchestration/lib/cli-version.mjs +123 -0
- package/orchestration/lib/ensure-runner-deps.sh +56 -0
- package/orchestration/lib/harness-path.sh +55 -0
- package/orchestration/lib/hash-helpers.sh +44 -0
- package/orchestration/lib/json-field.sh +23 -0
- package/orchestration/lib/log-tui.mjs +304 -0
- package/orchestration/lib/open-debug.mjs +317 -0
- package/orchestration/lib/path-defaults.json +4 -0
- package/orchestration/lib/progress.mjs +107 -0
- package/orchestration/lib/recipe-paths.mjs +26 -0
- package/orchestration/lib/resolve-farmslot-ports.sh +144 -0
- package/orchestration/manifest.json +358 -0
- package/orchestration/mobile/cleanup.sh +192 -0
- package/orchestration/mobile/deps-markers.ts +21 -0
- package/orchestration/mobile/inject.sh +681 -0
- package/orchestration/mobile/launch.sh +137 -0
- package/orchestration/mobile/live.sh +125 -0
- package/orchestration/mobile/runtime-decision.ts +292 -0
- package/orchestration/porcelain/metamask-recipe +99 -0
- package/orchestration/porcelain/mm-recipe +1591 -0
- package/orchestration/porcelain/mme-recipe +1181 -0
- package/package.json +59 -0
- package/runner/extension/verify.sh +511 -0
- package/runner/mobile/verify.sh +501 -0
- package/runner/src/adapters.ts +601 -0
- package/runner/src/cli.ts +1820 -0
- package/runner/src/commands/debug.ts +44 -0
- package/runner/src/commands/fixtures.ts +99 -0
- package/runner/src/commands/launch.ts +397 -0
- package/runner/src/commands/logs.ts +60 -0
- package/runner/src/commands/shared.ts +138 -0
- package/runner/src/completions-cache.ts +86 -0
- package/runner/src/doctor.ts +203 -0
- package/runner/src/harness.ts +516 -0
- package/runner/src/heal-bounds.ts +179 -0
- package/runner/src/index.ts +6 -0
- package/runner/src/live-adapter-contract.ts +274 -0
- package/runner/src/manifest.ts +47 -0
- package/runner/src/mm-harness-cli.ts +488 -0
- package/runner/src/paths.ts +198 -0
- package/runner/src/recording-target.ts +147 -0
- package/runner/src/run-recording.ts +329 -0
- package/runner/src/runner.ts +108 -0
- package/runner/src/types.ts +57 -0
- package/scripts/completions.sh +125 -0
- package/scripts/install-completions.sh +62 -0
|
@@ -0,0 +1,730 @@
|
|
|
1
|
+
import { pathToFileURL } from 'node:url';
|
|
2
|
+
import { dataTestId, marketSymbol, normalizeMarketSymbol, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
3
|
+
|
|
4
|
+
function sleep(ms) {
|
|
5
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function symbolForItem(item) {
|
|
9
|
+
return normalizeMarketSymbol(item?.coin ?? item?.symbol ?? '');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function sideForItem(item) {
|
|
13
|
+
return String(item?.side ?? item?.direction ?? '').toLowerCase();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function uniqueSymbols(symbols) {
|
|
17
|
+
return Array.from(new Set(symbols.filter(Boolean)));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function configuredSymbols(input, items) {
|
|
21
|
+
const selector = input.node?.selector && typeof input.node.selector === 'object' ? input.node.selector : {};
|
|
22
|
+
const mode = String(input.node?.mode ?? selector.mode ?? 'matching').toLowerCase();
|
|
23
|
+
if (mode === 'all') return uniqueSymbols(items.map(symbolForItem).filter(Boolean));
|
|
24
|
+
const explicit = input.node?.markets ?? input.node?.symbols ?? selector.markets ?? selector.symbols;
|
|
25
|
+
if (Array.isArray(explicit) && explicit.length > 0) return uniqueSymbols(explicit.map(normalizeMarketSymbol));
|
|
26
|
+
if (typeof explicit === 'string' && explicit.length > 0) return uniqueSymbols(explicit.split(',').map((part) => normalizeMarketSymbol(part.trim())).filter(Boolean));
|
|
27
|
+
return [marketSymbol(input)];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function selectedItems(input, items) {
|
|
31
|
+
const symbols = new Set(configuredSymbols(input, items));
|
|
32
|
+
const selector = input.node?.selector && typeof input.node.selector === 'object' ? input.node.selector : {};
|
|
33
|
+
const side = input.node?.side ?? selector.side;
|
|
34
|
+
return items.filter((item) => {
|
|
35
|
+
if (!symbols.has(symbolForItem(item))) return false;
|
|
36
|
+
if (side && sideForItem(item) && sideForItem(item) !== String(side).toLowerCase()) return false;
|
|
37
|
+
return true;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function navigatePerps(input) {
|
|
42
|
+
const selected = String(input.node?.target ?? input.node?.destination ?? (input.node?.market || input.node?.symbol ? 'market' : 'home')).toLowerCase();
|
|
43
|
+
return withExtensionPage(input, async (page) => {
|
|
44
|
+
if (selected === 'home' || selected === 'perps' || selected === 'perps_home') {
|
|
45
|
+
const navigation = await page.navigateHash('#/?tab=perps');
|
|
46
|
+
await page.waitForExpression('document.body && document.body.innerText.includes("Perps")', { timeoutMs: 15000 });
|
|
47
|
+
return { action: input.action, target: selected, navigation, proofPath: 'ui-navigation' };
|
|
48
|
+
}
|
|
49
|
+
const symbol = marketSymbol(input);
|
|
50
|
+
if (selected === 'market' || selected === 'market_details') {
|
|
51
|
+
const encodedSymbol = encodeURIComponent(symbol);
|
|
52
|
+
const navigation = await page.navigateHash(`#/perps/market/${encodedSymbol}`);
|
|
53
|
+
await page.waitForExpression(`location.hash.includes(${JSON.stringify(`/perps/market/${encodedSymbol}`)})`, { timeoutMs: 15000 });
|
|
54
|
+
return { action: input.action, target: selected, market: symbol, navigation, proofPath: 'ui-navigation' };
|
|
55
|
+
}
|
|
56
|
+
if (selected === 'trade' || selected === 'order') {
|
|
57
|
+
const side = String(input.node?.side ?? 'long').toLowerCase();
|
|
58
|
+
const navigation = await page.navigateHash(`#/perps/trade/${encodeURIComponent(symbol)}?direction=${encodeURIComponent(side)}&mode=new`);
|
|
59
|
+
await page.waitForSelector(dataTestId('perps-order-entry-page'), { timeoutMs: 20000 });
|
|
60
|
+
return { action: input.action, target: selected, market: symbol, side, navigation, proofPath: 'ui-navigation' };
|
|
61
|
+
}
|
|
62
|
+
throw new Error(`Unsupported extension Perps navigation target: ${selected}`);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function readPositions(input) {
|
|
67
|
+
return withExtensionPage(input, async (page) => {
|
|
68
|
+
const state = await page.readPositions();
|
|
69
|
+
if (!state.available) throw new Error('stateHooks.submitRequestToBackground is unavailable; cannot read live Perps positions.');
|
|
70
|
+
const matching = selectedItems(input, state.positions);
|
|
71
|
+
return { action: input.action, source: state.source ?? 'perps-stream-manager-cache', count: state.positions.length, matchingCount: matching.length, positions: matching.map(redactPosition) };
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function readOrders(input) {
|
|
76
|
+
return withExtensionPage(input, async (page) => {
|
|
77
|
+
const orders = await readOpenOrders(page);
|
|
78
|
+
const matching = selectedItems(input, orders);
|
|
79
|
+
return { action: input.action, source: 'perps-stream-manager-cache', count: orders.length, matchingCount: matching.length, orders: matching.map(redactOrder) };
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function assertPositions(input, expectedOpen) {
|
|
84
|
+
return withExtensionPage(input, async (page) => {
|
|
85
|
+
const timeoutMs = Number(input.node?.timeout_ms ?? 0);
|
|
86
|
+
if (expectedOpen && timeoutMs > 0) {
|
|
87
|
+
await waitForPositionPresent(page, marketSymbol(input), timeoutMs);
|
|
88
|
+
}
|
|
89
|
+
const state = await page.readPositions();
|
|
90
|
+
if (!state.available) throw new Error('stateHooks.submitRequestToBackground is unavailable; cannot assert live Perps positions.');
|
|
91
|
+
const matching = selectedItems(input, state.positions);
|
|
92
|
+
const hasPosition = matching.length > 0;
|
|
93
|
+
if (expectedOpen && !hasPosition) throw new Error(`Expected selected Perps position(s), but none matched ${JSON.stringify(input.node)}.`);
|
|
94
|
+
if (!expectedOpen && hasPosition) throw new Error(`Expected no selected Perps positions, but ${matching.length} matched ${JSON.stringify(input.node)}.`);
|
|
95
|
+
return { action: input.action, expectedOpen, matchingCount: matching.length, positions: matching.map(redactPosition), source: state.source ?? 'perps-stream-manager-cache' };
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function assertOrders(input, expectedOpen) {
|
|
100
|
+
return withExtensionPage(input, async (page) => {
|
|
101
|
+
const orders = await readOpenOrders(page);
|
|
102
|
+
const matching = selectedItems(input, orders);
|
|
103
|
+
const hasOrders = matching.length > 0;
|
|
104
|
+
if (expectedOpen && !hasOrders) throw new Error(`Expected selected Perps order(s), but none matched ${JSON.stringify(input.node)}.`);
|
|
105
|
+
if (!expectedOpen && hasOrders) throw new Error(`Expected no selected Perps orders, but ${matching.length} matched ${JSON.stringify(input.node)}.`);
|
|
106
|
+
return { action: input.action, expectedOpen, matchingCount: matching.length, orders: matching.map(redactOrder), source: 'perps-stream-manager-cache' };
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function closePositions(input) {
|
|
111
|
+
return withExtensionPage(input, async (page) => {
|
|
112
|
+
const timeoutMs = Number(input.node?.timeout_ms ?? 30000);
|
|
113
|
+
const state = await page.readPositions();
|
|
114
|
+
if (!state.available) throw new Error('Background Perps API unavailable; cannot verify or close live positions.');
|
|
115
|
+
const matching = selectedItems(input, state.positions);
|
|
116
|
+
const symbols = uniqueSymbols(matching.map(symbolForItem));
|
|
117
|
+
if (symbols.length === 0) return { action: input.action, closed: false, matchingCount: 0, symbols, reason: 'no matching open positions' };
|
|
118
|
+
const closeResult = { success: true, successCount: 0, failureCount: 0, results: [] };
|
|
119
|
+
for (const position of matching) {
|
|
120
|
+
const symbol = symbolForItem(position);
|
|
121
|
+
const currentPrice = await readCurrentMarketPrice(page, symbol);
|
|
122
|
+
const positionSize = Number(position?.size ?? position?.szi);
|
|
123
|
+
if (!Number.isFinite(positionSize) || positionSize === 0) {
|
|
124
|
+
closeResult.success = false;
|
|
125
|
+
closeResult.failureCount += 1;
|
|
126
|
+
closeResult.results.push({ symbol, success: false, error: `Invalid position size for ${symbol}: ${String(position?.size ?? position?.szi)}` });
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const result = await closePositionWithFreshPrice(page, input, position, symbol);
|
|
130
|
+
const success = result?.success === true;
|
|
131
|
+
closeResult.results.push({ symbol, success, result });
|
|
132
|
+
if (success) closeResult.successCount += 1;
|
|
133
|
+
else {
|
|
134
|
+
closeResult.success = false;
|
|
135
|
+
closeResult.failureCount += 1;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const after = await waitForPositionsAbsent(page, symbols, timeoutMs);
|
|
139
|
+
const stillOpen = after.positions.filter((position) => symbols.includes(symbolForItem(position)));
|
|
140
|
+
if (stillOpen.length > 0) throw new Error(`Expected selected positions to close after perpsClosePosition. Result: ${JSON.stringify(closeResult)}`);
|
|
141
|
+
return { action: input.action, closed: true, matchingCount: matching.length, symbols, closeResult, proofPath: 'background-perpsClosePosition' };
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function closeOrders(input) {
|
|
146
|
+
return withExtensionPage(input, async (page) => {
|
|
147
|
+
const timeoutMs = Number(input.node?.timeout_ms ?? 30000);
|
|
148
|
+
const before = await readOpenOrders(page);
|
|
149
|
+
const matching = selectedItems(input, before);
|
|
150
|
+
const symbols = uniqueSymbols(matching.map(symbolForItem));
|
|
151
|
+
if (symbols.length === 0) return { action: input.action, canceled: false, matchingCount: 0, symbols, result: null };
|
|
152
|
+
const result = { success: true, successCount: 0, failureCount: 0, results: [] };
|
|
153
|
+
for (const order of matching) {
|
|
154
|
+
const orderId = order?.orderId ?? order?.id;
|
|
155
|
+
const symbol = symbolForItem(order);
|
|
156
|
+
if (!orderId || !symbol) {
|
|
157
|
+
result.success = false;
|
|
158
|
+
result.failureCount += 1;
|
|
159
|
+
result.results.push({ orderId: orderId ?? null, symbol: symbol || null, success: false, error: 'missing orderId or symbol' });
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const cancelResult = await requestBackground(page, 'perpsCancelOrder', [{ orderId: String(orderId), symbol }]);
|
|
163
|
+
const success = cancelResult?.success !== false;
|
|
164
|
+
result.results.push({ orderId: String(orderId), symbol, success, result: cancelResult });
|
|
165
|
+
if (success) result.successCount += 1;
|
|
166
|
+
else {
|
|
167
|
+
result.success = false;
|
|
168
|
+
result.failureCount += 1;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const after = await waitForOrdersAbsent(page, symbols, timeoutMs);
|
|
172
|
+
const stillOpen = after.filter((order) => symbols.includes(symbolForItem(order)));
|
|
173
|
+
if (stillOpen.length > 0) throw new Error(`Expected selected Perps orders to cancel, but ${stillOpen.length} are still open.`);
|
|
174
|
+
return { action: input.action, canceled: true, matchingCount: matching.length, symbols, result, proofPath: 'background-perpsCancelOrder' };
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function requestBackground(page, method, args, timeoutMs = 20000) {
|
|
179
|
+
const result = await page.evaluate(`(async () => {
|
|
180
|
+
const request = globalThis.stateHooks?.submitRequestToBackground;
|
|
181
|
+
if (typeof request !== 'function') throw new Error('stateHooks.submitRequestToBackground is unavailable; cannot call ${method}.');
|
|
182
|
+
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('${method} timed out after ${timeoutMs}ms')), ${JSON.stringify(timeoutMs)}));
|
|
183
|
+
return Promise.race([request(${JSON.stringify(method)}, ${JSON.stringify(args)}), timeout]);
|
|
184
|
+
})()`);
|
|
185
|
+
if (!result) throw new Error(`${method} returned no result.`);
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function isTransientClosePriceError(result) {
|
|
190
|
+
const message = String(result?.error ?? result?.message ?? '');
|
|
191
|
+
// Retry local pricing/slippage races only. Provider-level oracle rejections
|
|
192
|
+
// such as "Price too far from oracle" are surfaced as real market failures.
|
|
193
|
+
return /IOC_CANCEL|Slippage/i.test(message);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function closePositionWithFreshPrice(page, input, position, symbol) {
|
|
197
|
+
const maxAttempts = Number(input.node?.close_attempts ?? 3);
|
|
198
|
+
const attempts = [];
|
|
199
|
+
let lastResult = null;
|
|
200
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
201
|
+
const currentPrice = await readCurrentMarketPrice(page, symbol, input.node?.current_price);
|
|
202
|
+
const params = {
|
|
203
|
+
symbol,
|
|
204
|
+
size: String(input.node?.size ?? ''),
|
|
205
|
+
orderType: 'market',
|
|
206
|
+
priceAtCalculation: currentPrice,
|
|
207
|
+
maxSlippageBps: Number(input.node?.max_slippage_bps ?? input.node?.maxSlippageBps ?? 300),
|
|
208
|
+
position,
|
|
209
|
+
};
|
|
210
|
+
const result = await requestBackground(page, 'perpsClosePosition', [params]);
|
|
211
|
+
attempts.push({ attempt, currentPrice, result });
|
|
212
|
+
lastResult = result;
|
|
213
|
+
if (result?.success === true) return { ...result, attempts, params };
|
|
214
|
+
if (!isTransientClosePriceError(result) || attempt === maxAttempts) break;
|
|
215
|
+
await sleep(Number(input.node?.close_retry_delay_ms ?? 1000));
|
|
216
|
+
}
|
|
217
|
+
return { ...(lastResult && typeof lastResult === 'object' ? lastResult : { success: false, error: String(lastResult) }), attempts };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function readCurrentMarketPrice(page, symbol, explicitPrice) {
|
|
221
|
+
if (explicitPrice !== undefined && explicitPrice !== null && explicitPrice !== '') {
|
|
222
|
+
const parsed = Number(String(explicitPrice).replace(/[$,]/g, ''));
|
|
223
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
224
|
+
throw new Error(`Invalid explicit current price for ${symbol}: ${String(explicitPrice)}`);
|
|
225
|
+
}
|
|
226
|
+
const price = await page.evaluate(String.raw`(async () => {
|
|
227
|
+
const request = globalThis.stateHooks?.submitRequestToBackground;
|
|
228
|
+
const manager = globalThis.stateHooks?.getPerpsStreamManager?.();
|
|
229
|
+
const symbol = ${JSON.stringify(symbol)};
|
|
230
|
+
function parsePrice(value) {
|
|
231
|
+
const parsed = Number(String(value ?? '').replace(/[$,]/g, '').match(/[0-9]+(?:[.][0-9]+)?/)?.[0] || '0');
|
|
232
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
233
|
+
}
|
|
234
|
+
function bySymbol(item) {
|
|
235
|
+
return item?.symbol === symbol || item?.coin === symbol;
|
|
236
|
+
}
|
|
237
|
+
if (typeof request === 'function') {
|
|
238
|
+
const markets = await request('perpsGetMarketDataWithPrices', []);
|
|
239
|
+
const market = Array.isArray(markets) ? markets.find(bySymbol) : null;
|
|
240
|
+
const fromMarket = parsePrice(
|
|
241
|
+
market?.oraclePrice ?? market?.oraclePx ?? market?.markPrice ?? market?.currentPrice ?? market?.price,
|
|
242
|
+
);
|
|
243
|
+
if (fromMarket) return { price: fromMarket, source: 'perpsGetMarketDataWithPrices' };
|
|
244
|
+
}
|
|
245
|
+
const streamPrices = manager?.prices?.cache;
|
|
246
|
+
const streamPrice = Array.isArray(streamPrices) ? streamPrices.find(bySymbol) : null;
|
|
247
|
+
const fromStream = parsePrice(
|
|
248
|
+
streamPrice?.oraclePrice ?? streamPrice?.oraclePx ?? streamPrice?.markPrice ?? streamPrice?.price,
|
|
249
|
+
);
|
|
250
|
+
if (fromStream) return { price: fromStream, source: 'perps-stream-manager-prices' };
|
|
251
|
+
const visibleSelectors = [
|
|
252
|
+
'[data-testid="perps-market-detail-oracle-price"]',
|
|
253
|
+
'[data-testid="perps-order-entry-price"]',
|
|
254
|
+
'[data-testid="perps-market-detail-price"]',
|
|
255
|
+
];
|
|
256
|
+
for (const selector of visibleSelectors) {
|
|
257
|
+
const fromDom = parsePrice(document.querySelector(selector)?.innerText);
|
|
258
|
+
if (fromDom) return { price: fromDom, source: selector };
|
|
259
|
+
}
|
|
260
|
+
const body = document.body?.innerText || '';
|
|
261
|
+
const line = body.split('\n').find((text) => /\$[0-9][0-9,]*(?:\.[0-9]+)?/.test(text));
|
|
262
|
+
const fromBody = parsePrice(line);
|
|
263
|
+
return fromBody ? { price: fromBody, source: 'document-body-price-line' } : null;
|
|
264
|
+
})()`);
|
|
265
|
+
const numericPrice = Number(price?.price ?? price);
|
|
266
|
+
if (!Number.isFinite(numericPrice) || numericPrice <= 0) throw new Error(`Unable to determine current Perps price for ${symbol}.`);
|
|
267
|
+
return numericPrice;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function readOpenOrders(page) {
|
|
271
|
+
const state = await page.evaluate(`(async () => {
|
|
272
|
+
const request = globalThis.stateHooks?.submitRequestToBackground;
|
|
273
|
+
const manager = globalThis.stateHooks?.getPerpsStreamManager?.();
|
|
274
|
+
const cached = manager?.orders?.cache;
|
|
275
|
+
if (typeof request === 'function') {
|
|
276
|
+
const result = await Promise.race([
|
|
277
|
+
request('perpsGetOpenOrders', []).then((orders) => ({ ok: true, orders })),
|
|
278
|
+
new Promise((resolve) => setTimeout(() => resolve({ ok: false, timeout: true }), 5000)),
|
|
279
|
+
]);
|
|
280
|
+
if (result.ok && Array.isArray(result.orders)) return { available: true, source: 'background-perpsGetOpenOrders', orders: result.orders };
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
available: Array.isArray(cached),
|
|
284
|
+
source: 'perps-stream-manager-cache',
|
|
285
|
+
initialized: Boolean(manager?.isInitialized?.()),
|
|
286
|
+
connected: Boolean(manager?.orders?.isConnected),
|
|
287
|
+
orders: Array.isArray(cached) ? cached : [],
|
|
288
|
+
};
|
|
289
|
+
})()`);
|
|
290
|
+
if (!state.available) throw new Error('Perps orders are unavailable from both background and stream manager cache.');
|
|
291
|
+
return state.orders;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function readAccountState(page) {
|
|
295
|
+
const state = await page.evaluate(`(async () => {
|
|
296
|
+
const request = globalThis.stateHooks?.submitRequestToBackground;
|
|
297
|
+
const manager = globalThis.stateHooks?.getPerpsStreamManager?.();
|
|
298
|
+
const cached = manager?.account?.cache;
|
|
299
|
+
if (typeof request === 'function') {
|
|
300
|
+
const result = await Promise.race([
|
|
301
|
+
request('perpsGetAccountState', []).then((account) => ({ ok: true, account })),
|
|
302
|
+
new Promise((resolve) => setTimeout(() => resolve({ ok: false, timeout: true }), 5000)),
|
|
303
|
+
]);
|
|
304
|
+
if (result.ok && result.account && typeof result.account === 'object') return { available: true, source: 'background-perpsGetAccountState', account: result.account };
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
available: Boolean(cached && typeof cached === 'object'),
|
|
308
|
+
source: 'perps-stream-manager-cache',
|
|
309
|
+
initialized: Boolean(manager?.isInitialized?.()),
|
|
310
|
+
connected: Boolean(manager?.account?.isConnected),
|
|
311
|
+
account: cached && typeof cached === 'object' ? cached : null,
|
|
312
|
+
};
|
|
313
|
+
})()`);
|
|
314
|
+
if (!state.available) throw new Error('Perps account state is unavailable from both background and stream manager cache.');
|
|
315
|
+
return state.account;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function waitForPositionsAbsent(page, symbols, timeoutMs) {
|
|
319
|
+
const deadline = Date.now() + timeoutMs;
|
|
320
|
+
let lastState = { available: true, positions: [] };
|
|
321
|
+
while (Date.now() < deadline) {
|
|
322
|
+
const state = await page.readPositions();
|
|
323
|
+
if (!state.available) throw new Error('stateHooks.submitRequestToBackground is unavailable; cannot assert live Perps positions.');
|
|
324
|
+
lastState = state;
|
|
325
|
+
const remaining = state.positions.filter((position) => symbols.includes(symbolForItem(position)));
|
|
326
|
+
if (remaining.length === 0) return state;
|
|
327
|
+
await sleep(500);
|
|
328
|
+
}
|
|
329
|
+
return lastState;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function waitForOrdersAbsent(page, symbols, timeoutMs) {
|
|
333
|
+
const deadline = Date.now() + timeoutMs;
|
|
334
|
+
let last = [];
|
|
335
|
+
while (Date.now() < deadline) {
|
|
336
|
+
last = await readOpenOrders(page);
|
|
337
|
+
const remaining = last.filter((order) => symbols.includes(symbolForItem(order)));
|
|
338
|
+
if (remaining.length === 0) return last;
|
|
339
|
+
await sleep(500);
|
|
340
|
+
}
|
|
341
|
+
return last;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function waitForPositionPresent(page, symbol, timeoutMs) {
|
|
345
|
+
const deadline = Date.now() + timeoutMs;
|
|
346
|
+
let lastState = { available: true, positions: [] };
|
|
347
|
+
while (Date.now() < deadline) {
|
|
348
|
+
const state = await page.readPositions();
|
|
349
|
+
if (!state.available) throw new Error('stateHooks.submitRequestToBackground is unavailable; cannot assert live Perps positions.');
|
|
350
|
+
lastState = state;
|
|
351
|
+
const matching = state.positions.filter((position) => symbolForItem(position) === symbol);
|
|
352
|
+
if (matching.length > 0) return state;
|
|
353
|
+
await sleep(500);
|
|
354
|
+
}
|
|
355
|
+
return lastState;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export async function placeOrder(input) {
|
|
359
|
+
const symbol = marketSymbol(input);
|
|
360
|
+
const side = String(input.node?.side ?? 'long').toLowerCase();
|
|
361
|
+
const amount = String(input.node?.amount ?? input.node?.notional ?? '11');
|
|
362
|
+
const leverage = Number(input.node?.leverage ?? 3);
|
|
363
|
+
return withExtensionPage(input, async (page) => {
|
|
364
|
+
await page.navigateHash(`#/perps/market/${encodeURIComponent(symbol)}`);
|
|
365
|
+
await page.evaluate(`(async () => {
|
|
366
|
+
const request = globalThis.stateHooks?.submitRequestToBackground;
|
|
367
|
+
if (typeof request !== 'function') return;
|
|
368
|
+
const state = globalThis.stateHooks?.store?.getState?.()?.metamask;
|
|
369
|
+
if (state && state.isTestnet === false) {
|
|
370
|
+
await request('perpsToggleTestnet', []);
|
|
371
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
372
|
+
}
|
|
373
|
+
})()`, { awaitPromise: true });
|
|
374
|
+
const resolvedPrice = await readCurrentMarketPrice(
|
|
375
|
+
page,
|
|
376
|
+
symbol,
|
|
377
|
+
input.node?.current_price ?? input.node?.currentPrice,
|
|
378
|
+
);
|
|
379
|
+
await page.navigateHash(`#/perps/trade/${encodeURIComponent(symbol)}?direction=${encodeURIComponent(side)}&mode=new`);
|
|
380
|
+
await page.waitForSelector(dataTestId('perps-order-entry-page'), { timeoutMs: 20000 });
|
|
381
|
+
let order;
|
|
382
|
+
try {
|
|
383
|
+
order = await page.evaluate(`(async () => {
|
|
384
|
+
const request = globalThis.stateHooks?.submitRequestToBackground;
|
|
385
|
+
if (typeof request !== 'function') throw new Error('stateHooks.submitRequestToBackground is unavailable; cannot place live Perps order.');
|
|
386
|
+
const currentPrice = Number(${JSON.stringify(resolvedPrice)});
|
|
387
|
+
if (!Number.isFinite(currentPrice) || currentPrice <= 0) throw new Error('Unable to determine current Perps price for order placement.');
|
|
388
|
+
const usdAmount = ${JSON.stringify(amount)};
|
|
389
|
+
const leverage = ${JSON.stringify(leverage)};
|
|
390
|
+
const orderParams = {
|
|
391
|
+
symbol: ${JSON.stringify(symbol)},
|
|
392
|
+
isBuy: ${JSON.stringify(side !== 'short')},
|
|
393
|
+
size: ((Number(usdAmount) * Number(leverage)) / currentPrice).toString(),
|
|
394
|
+
orderType: 'market',
|
|
395
|
+
leverage,
|
|
396
|
+
currentPrice,
|
|
397
|
+
usdAmount,
|
|
398
|
+
maxSlippageBps: ${JSON.stringify(Number(input.node?.max_slippage_bps ?? input.node?.maxSlippageBps ?? 300))},
|
|
399
|
+
};
|
|
400
|
+
const result = await request('perpsPlaceOrder', [orderParams]);
|
|
401
|
+
if (!result || result.success !== true) {
|
|
402
|
+
throw new Error(result?.error || 'perpsPlaceOrder failed.');
|
|
403
|
+
}
|
|
404
|
+
return { result, orderParams };
|
|
405
|
+
})()`, { awaitPromise: true });
|
|
406
|
+
} catch (error) {
|
|
407
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
408
|
+
throw new Error(`${message} (resolvedPrice=${resolvedPrice})`);
|
|
409
|
+
}
|
|
410
|
+
await waitForPositionPresent(page, symbol, Number(input.node?.timeout_ms ?? 30000));
|
|
411
|
+
return { action: input.action, market: symbol, side, amount, leverage, submitted: true, order, proofPath: 'background-perpsPlaceOrder' };
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export async function ensurePositions(input) {
|
|
416
|
+
const state = String(input.node?.state ?? input.node?.position ?? 'none').toLowerCase();
|
|
417
|
+
if (state === 'none' || state === 'closed' || state === 'absent') {
|
|
418
|
+
const close = await closePositions(input);
|
|
419
|
+
return { ...(await assertPositions(input, false)), close };
|
|
420
|
+
}
|
|
421
|
+
if (state === 'open' || state === 'present') {
|
|
422
|
+
const current = await withExtensionPage(input, async (page) => {
|
|
423
|
+
const stateResult = await page.readPositions();
|
|
424
|
+
if (!stateResult.available) throw new Error('stateHooks.submitRequestToBackground is unavailable; cannot ensure live Perps positions.');
|
|
425
|
+
return stateResult.positions;
|
|
426
|
+
});
|
|
427
|
+
if (selectedItems(input, current).length === 0) await placeOrder(input);
|
|
428
|
+
return assertPositions(input, true);
|
|
429
|
+
}
|
|
430
|
+
throw new Error(`metamask.perps.ensure_positions received unsupported state: ${state}`);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export async function ensureOrders(input) {
|
|
434
|
+
const state = String(input.node?.state ?? input.node?.orders ?? 'none').toLowerCase();
|
|
435
|
+
if (state === 'none' || state === 'closed' || state === 'absent') {
|
|
436
|
+
const close = await closeOrders(input);
|
|
437
|
+
return { ...(await assertOrders(input, false)), close };
|
|
438
|
+
}
|
|
439
|
+
if (state === 'open' || state === 'present') return assertOrders(input, true);
|
|
440
|
+
throw new Error(`metamask.perps.ensure_orders received unsupported state: ${state}`);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function redactPosition(position) {
|
|
444
|
+
return {
|
|
445
|
+
coin: position.coin ?? position.symbol ?? null,
|
|
446
|
+
size: position.size ?? position.szi ?? null,
|
|
447
|
+
side: position.side ?? null,
|
|
448
|
+
entryPrice: position.entryPrice ?? position.entryPx ?? null,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function redactOrder(order) {
|
|
453
|
+
return {
|
|
454
|
+
coin: order.coin ?? order.symbol ?? null,
|
|
455
|
+
side: order.side ?? null,
|
|
456
|
+
size: order.size ?? order.sz ?? order.szi ?? null,
|
|
457
|
+
price: order.price ?? order.limitPx ?? order.px ?? null,
|
|
458
|
+
type: order.orderType ?? order.type ?? null,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
function paramsForState(input) {
|
|
464
|
+
const nested = input.node?.params && typeof input.node.params === 'object' ? input.node.params : {};
|
|
465
|
+
return { ...nested, ...input.node };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function profileDefaults(profile) {
|
|
469
|
+
const selected = String(profile ?? 'clean_market_testnet');
|
|
470
|
+
if (selected === 'clean_market_testnet') {
|
|
471
|
+
return {
|
|
472
|
+
provider: 'hyperliquid',
|
|
473
|
+
network: 'testnet',
|
|
474
|
+
page: 'market',
|
|
475
|
+
positions: { state: 'none', mode: 'matching' },
|
|
476
|
+
orders: { state: 'none', mode: 'matching' },
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
if (selected === 'open_position_testnet') {
|
|
480
|
+
return {
|
|
481
|
+
provider: 'hyperliquid',
|
|
482
|
+
network: 'testnet',
|
|
483
|
+
page: 'market',
|
|
484
|
+
positions: { state: 'open', mode: 'matching' },
|
|
485
|
+
orders: { state: 'none', mode: 'matching' },
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
if (selected === 'open_order_testnet') {
|
|
489
|
+
return {
|
|
490
|
+
provider: 'hyperliquid',
|
|
491
|
+
network: 'testnet',
|
|
492
|
+
page: 'market',
|
|
493
|
+
positions: false,
|
|
494
|
+
orders: { state: 'open', mode: 'matching' },
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
if (selected === 'provider_mainnet_readonly') {
|
|
498
|
+
return {
|
|
499
|
+
provider: 'hyperliquid',
|
|
500
|
+
network: 'mainnet',
|
|
501
|
+
page: 'home',
|
|
502
|
+
positions: false,
|
|
503
|
+
orders: false,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
if (selected === 'clean_market_mainnet') {
|
|
507
|
+
return {
|
|
508
|
+
provider: 'hyperliquid',
|
|
509
|
+
network: 'mainnet',
|
|
510
|
+
page: 'market',
|
|
511
|
+
positions: { state: 'none', mode: 'matching' },
|
|
512
|
+
orders: { state: 'none', mode: 'matching' },
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
return {};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function mergeStateConfig(defaults, params) {
|
|
519
|
+
return {
|
|
520
|
+
...defaults,
|
|
521
|
+
...params,
|
|
522
|
+
positions: mergeNested(defaults.positions, params.positions),
|
|
523
|
+
orders: mergeNested(defaults.orders, params.orders),
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function mergeNested(defaultValue, overrideValue) {
|
|
528
|
+
if (overrideValue === false) return false;
|
|
529
|
+
if (overrideValue === undefined) return defaultValue;
|
|
530
|
+
if (defaultValue && typeof defaultValue === 'object' && overrideValue && typeof overrideValue === 'object') {
|
|
531
|
+
return { ...defaultValue, ...overrideValue };
|
|
532
|
+
}
|
|
533
|
+
return overrideValue;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function childInput(input, node) {
|
|
537
|
+
const params = paramsForState(input);
|
|
538
|
+
return {
|
|
539
|
+
...input,
|
|
540
|
+
node: {
|
|
541
|
+
...params,
|
|
542
|
+
...node,
|
|
543
|
+
market: node.market ?? node.symbol ?? params.market ?? params.symbol,
|
|
544
|
+
markets: node.markets ?? params.markets,
|
|
545
|
+
symbols: node.symbols ?? params.symbols,
|
|
546
|
+
side: node.side ?? params.side,
|
|
547
|
+
timeout_ms: node.timeout_ms ?? params.timeout_ms,
|
|
548
|
+
},
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
async function applyOrdersState(input, config) {
|
|
553
|
+
if (config === false) return { skipped: true };
|
|
554
|
+
const node = config && typeof config === 'object' ? config : { state: String(config ?? 'none') };
|
|
555
|
+
return ensureOrders(childInput(input, node));
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async function applyPositionsState(input, config) {
|
|
559
|
+
if (config === false) return { skipped: true };
|
|
560
|
+
const node = config && typeof config === 'object' ? config : { state: String(config ?? 'none') };
|
|
561
|
+
return ensurePositions(childInput(input, node));
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async function applyStateNavigation(input, config) {
|
|
565
|
+
if (!config.page && !config.market && !config.symbol) return { skipped: true };
|
|
566
|
+
const target = config.page ?? (config.market || config.symbol ? 'market' : 'home');
|
|
567
|
+
return navigatePerps(childInput(input, { target, market: config.market, symbol: config.symbol }));
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
async function readPerpsRuntimeState(page) {
|
|
572
|
+
return page.evaluate(`(async () => {
|
|
573
|
+
const hooks = globalThis.stateHooks || {};
|
|
574
|
+
const request = hooks.submitRequestToBackground;
|
|
575
|
+
const reduxState = hooks.store?.getState?.() || {};
|
|
576
|
+
const cleanState = hooks.getCleanAppState?.() || hooks.getState?.() || {};
|
|
577
|
+
// Extension flattens PerpsController state into state.metamask via ComposableObservableStore.getFlatState().
|
|
578
|
+
const metamask = reduxState.metamask || cleanState.metamask || {};
|
|
579
|
+
const nestedPerps = metamask.PerpsController || cleanState.PerpsController || cleanState.perps || {};
|
|
580
|
+
return {
|
|
581
|
+
available: typeof request === 'function',
|
|
582
|
+
activeProvider: metamask.activeProvider || nestedPerps.activeProvider || 'hyperliquid',
|
|
583
|
+
isTestnet: Boolean(metamask.isTestnet ?? nestedPerps.isTestnet),
|
|
584
|
+
initializationState: metamask.initializationState || nestedPerps.initializationState || null,
|
|
585
|
+
isEligible: Boolean(metamask.isEligible ?? nestedPerps.isEligible),
|
|
586
|
+
};
|
|
587
|
+
})()`);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
async function ensureProvider(input, config) {
|
|
591
|
+
if (!config.provider) return { skipped: true };
|
|
592
|
+
const expected = String(config.provider).toLowerCase();
|
|
593
|
+
return withExtensionPage(input, async (page) => {
|
|
594
|
+
const state = await readPerpsRuntimeState(page);
|
|
595
|
+
if (!state.available) throw new Error('stateHooks.submitRequestToBackground is unavailable; cannot enforce Perps provider.');
|
|
596
|
+
if (!state.activeProvider && expected === 'hyperliquid') return { requested: expected, activeProvider: 'hyperliquid', changed: false, defaulted: true };
|
|
597
|
+
if (String(state.activeProvider || '').toLowerCase() !== expected) {
|
|
598
|
+
throw new Error(`Expected Perps provider ${expected}, got ${state.activeProvider || 'unknown'}. Extension does not expose a provider switch request yet; select the provider before running or add a supported product request.`);
|
|
599
|
+
}
|
|
600
|
+
return { requested: expected, activeProvider: state.activeProvider, changed: false };
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function ensureNetwork(input, config) {
|
|
605
|
+
if (!config.network) return { skipped: true };
|
|
606
|
+
const expectedTestnet = String(config.network).toLowerCase() === 'testnet';
|
|
607
|
+
return withExtensionPage(input, async (page) => {
|
|
608
|
+
const before = await readPerpsRuntimeState(page);
|
|
609
|
+
if (!before.available) throw new Error('stateHooks.submitRequestToBackground is unavailable; cannot enforce Perps network.');
|
|
610
|
+
if (Boolean(before.isTestnet) === expectedTestnet) return { requested: config.network, isTestnet: before.isTestnet, changed: false };
|
|
611
|
+
const result = await requestBackground(page, 'perpsToggleTestnet', []);
|
|
612
|
+
const after = await waitForNetworkState(page, expectedTestnet, Number(input.node?.timeout_ms ?? 30000));
|
|
613
|
+
if (Boolean(after.isTestnet) !== expectedTestnet) {
|
|
614
|
+
throw new Error(`Expected Perps network ${config.network}, got isTestnet=${after.isTestnet}.`);
|
|
615
|
+
}
|
|
616
|
+
return { requested: config.network, isTestnet: after.isTestnet, changed: true, result };
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async function waitForNetworkState(page, expectedTestnet, timeoutMs) {
|
|
621
|
+
const deadline = Date.now() + timeoutMs;
|
|
622
|
+
let last = await readPerpsRuntimeState(page);
|
|
623
|
+
while (Date.now() < deadline) {
|
|
624
|
+
last = await readPerpsRuntimeState(page);
|
|
625
|
+
if (Boolean(last.isTestnet) === expectedTestnet) return last;
|
|
626
|
+
await sleep(500);
|
|
627
|
+
}
|
|
628
|
+
return last;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
async function assertReadyToTrade(input, config) {
|
|
632
|
+
if (config.readyToTrade === undefined || config.readyToTrade === false) return { skipped: true };
|
|
633
|
+
return withExtensionPage(input, async (page) => {
|
|
634
|
+
const accountState = await readAccountState(page);
|
|
635
|
+
return { ready: true, source: 'perps-stream-manager-cache', accountStatePresent: Boolean(accountState) };
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
async function assertBalance(input, config) {
|
|
640
|
+
if (!config.balance) return { skipped: true };
|
|
641
|
+
const balanceConfig = config.balance && typeof config.balance === 'object' ? config.balance : {};
|
|
642
|
+
const minWithdrawable = Number(balanceConfig.minWithdrawableUsd ?? balanceConfig.minUsd ?? 0);
|
|
643
|
+
const minSpendable = Number(balanceConfig.minSpendableUsd ?? 0);
|
|
644
|
+
return withExtensionPage(input, async (page) => {
|
|
645
|
+
const accountState = await readAccountState(page);
|
|
646
|
+
const withdrawable = Number(accountState.withdrawableBalance ?? 0);
|
|
647
|
+
const spendable = Number(accountState.spendableBalance ?? accountState.withdrawableBalance ?? 0);
|
|
648
|
+
if (Number.isFinite(minWithdrawable) && withdrawable < minWithdrawable) {
|
|
649
|
+
throw new Error(`Perps withdrawable balance ${withdrawable} is below requested minimum ${minWithdrawable}.`);
|
|
650
|
+
}
|
|
651
|
+
if (Number.isFinite(minSpendable) && spendable < minSpendable) {
|
|
652
|
+
throw new Error(`Perps spendable balance ${spendable} is below requested minimum ${minSpendable}.`);
|
|
653
|
+
}
|
|
654
|
+
return { withdrawableBalance: withdrawable, spendableBalance: spendable, minWithdrawableUsd: minWithdrawable, minSpendableUsd: minSpendable };
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
export async function startState(input) {
|
|
661
|
+
const params = paramsForState(input);
|
|
662
|
+
const config = mergeStateConfig(profileDefaults(params.profile), params);
|
|
663
|
+
const provider = await ensureProvider(input, config);
|
|
664
|
+
const network = await ensureNetwork(input, config);
|
|
665
|
+
const readyToTrade = await assertReadyToTrade(input, config);
|
|
666
|
+
const balance = await assertBalance(input, config);
|
|
667
|
+
const orders = await applyOrdersState(input, config.orders);
|
|
668
|
+
const positions = await applyPositionsState(input, config.positions);
|
|
669
|
+
const navigation = await applyStateNavigation(input, config);
|
|
670
|
+
return {
|
|
671
|
+
action: input.action,
|
|
672
|
+
profile: config.profile ?? params.profile ?? 'clean_market_testnet',
|
|
673
|
+
phase: 'start_state',
|
|
674
|
+
provider,
|
|
675
|
+
network,
|
|
676
|
+
readyToTrade,
|
|
677
|
+
balance,
|
|
678
|
+
market: config.market ?? config.symbol ?? null,
|
|
679
|
+
orders,
|
|
680
|
+
positions,
|
|
681
|
+
navigation,
|
|
682
|
+
proofPath: 'metamask-perps-start-state',
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
export async function teardownState(input) {
|
|
687
|
+
const params = paramsForState(input);
|
|
688
|
+
const defaults = {
|
|
689
|
+
page: 'home',
|
|
690
|
+
positions: params.market || params.symbol || params.markets || params.symbols ? { state: 'none', mode: 'matching' } : false,
|
|
691
|
+
orders: params.market || params.symbol || params.markets || params.symbols ? { state: 'none', mode: 'matching' } : false,
|
|
692
|
+
};
|
|
693
|
+
const config = mergeStateConfig(defaults, params);
|
|
694
|
+
const orders = await applyOrdersState(input, config.orders);
|
|
695
|
+
const positions = await applyPositionsState(input, config.positions);
|
|
696
|
+
const navigation = config.page === false ? { skipped: true } : await applyStateNavigation(input, config);
|
|
697
|
+
return {
|
|
698
|
+
action: input.action,
|
|
699
|
+
phase: 'teardown',
|
|
700
|
+
market: config.market ?? config.symbol ?? null,
|
|
701
|
+
orders,
|
|
702
|
+
positions,
|
|
703
|
+
navigation,
|
|
704
|
+
proofPath: 'metamask-perps-teardown-state',
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const DIRECT_ACTIONS = new Map([
|
|
709
|
+
['metamask.perps.read_positions', readPositions],
|
|
710
|
+
['metamask.perps.read_orders', readOrders],
|
|
711
|
+
['metamask.perps.close_positions', closePositions],
|
|
712
|
+
['metamask.perps.close_orders', closeOrders],
|
|
713
|
+
['metamask.perps.place_order', placeOrder],
|
|
714
|
+
['metamask.perps.ensure_positions', ensurePositions],
|
|
715
|
+
['metamask.perps.ensure_orders', ensureOrders],
|
|
716
|
+
['metamask.perps.start_state', startState],
|
|
717
|
+
['metamask.perps.teardown_state', teardownState],
|
|
718
|
+
]);
|
|
719
|
+
|
|
720
|
+
function isDirectRun() {
|
|
721
|
+
return process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (isDirectRun()) {
|
|
725
|
+
runAdapter((input) => {
|
|
726
|
+
const handler = DIRECT_ACTIONS.get(input.action);
|
|
727
|
+
if (!handler) throw new Error(`No Perps domain dispatcher handler for ${input.action}.`);
|
|
728
|
+
return handler(input);
|
|
729
|
+
});
|
|
730
|
+
}
|