@deeeed/metamask-harness 0.18.0 → 0.19.1
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 +200 -0
- package/README.md +4 -7
- package/adapters/core/inject.sh +1 -6
- package/adapters/extension/inject.mjs +1 -4
- package/adapters/extension/verify.sh +1 -1
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +33 -0
- package/adapters/mobile/inject.sh +5 -4
- package/adapters/mobile/verify.sh +15 -19
- package/adapters/shared/harness-source-fingerprint.mjs +14 -12
- package/dist/adapters/harness-freshness.js +49 -0
- package/dist/adapters.js +6 -1
- package/dist/cli.js +0 -4
- package/dist/command-contract.js +51 -40
- package/dist/command-journal.js +50 -12
- package/dist/commands/call.js +95 -20
- package/dist/commands/check.js +1 -1
- package/dist/commands/completion-candidates.js +7 -19
- package/dist/commands/fixtures.js +50 -1
- package/dist/commands/last.js +9 -1
- package/dist/commands/launch/extension.js +2 -2
- package/dist/commands/launch/mobile.js +2 -0
- package/dist/commands/list-executables.js +56 -20
- package/dist/commands/manifest.js +28 -12
- package/dist/commands/parse-args.js +55 -3
- package/dist/commands/provision.js +0 -1
- package/dist/commands/run-engine.js +514 -336
- package/dist/commands/run.js +25 -37
- package/dist/commands/shared.js +1 -1
- package/dist/heal-bounds.js +5 -0
- package/dist/live-adapter-contract.js +15 -2
- package/dist/mm-harness-cli.js +22 -30
- package/dist/run-diagnostics.js +1 -1
- package/dist/run-recording.js +1 -1
- package/dist/runner.js +24 -2
- package/docs/CONTRIBUTING.md +2 -3
- package/docs/QA.md +0 -1
- package/docs/RECIPES.md +67 -91
- package/library/README.md +5 -5
- package/library/actions/core/perps/_controller.mjs +33 -1
- package/library/actions/core/perps/assert_orders.mjs +22 -12
- package/library/actions/core/perps/assert_positions.mjs +22 -12
- package/library/actions/core/perps/close_orders.mjs +2 -0
- package/library/actions/core/perps/close_positions.mjs +2 -0
- package/library/actions/core/perps/ensure_orders.mjs +4 -2
- package/library/actions/core/perps/ensure_positions.mjs +4 -2
- package/library/actions/core/perps/place_order.mjs +7 -3
- package/library/actions/extension/perps/assert_orders.mjs +2 -1
- package/library/actions/extension/perps/assert_positions.mjs +2 -1
- package/library/actions/extension/perps/perps.mjs +151 -30
- package/library/actions/mobile/perps/assert_orders.mjs +2 -1
- package/library/actions/mobile/perps/assert_positions.mjs +2 -1
- package/library/actions/mobile/perps/perps.mjs +148 -22
- package/library/library.json +1 -1
- package/library/manifests/core.action-manifest.json +1204 -411
- package/library/manifests/extension.action-manifest.json +1536 -656
- package/library/manifests/mobile.action-manifest.json +1728 -758
- package/library/recipes/app/lifecycle.android-smoke.mobile.recipe.json +63 -81
- package/library/recipes/perps/clean-market-testnet.core.recipe.json +44 -0
- package/library/recipes/perps/clean-market-testnet.recipe.json +49 -0
- package/library/recipes/perps/lifecycle.recipe.json +136 -180
- package/library/recipes/perps/order-lifecycle.core.recipe.json +71 -67
- package/library/recipes/perps/performance.background-resume.mobile.recipe.json +51 -67
- package/library/recipes/perps/performance.cold-start.mobile.recipe.json +51 -67
- package/library/recipes/perps/performance.mobile.recipe.json +37 -51
- package/library/recipes/perps/performance.warm-start.mobile.recipe.json +44 -59
- package/library/recipes/perps/read-markets.core.recipe.json +29 -31
- package/library/recipes/perps/smoke.core.recipe.json +29 -32
- package/library/recipes/perps/smoke.extension.recipe.json +41 -44
- package/library/recipes/perps/smoke.mobile.recipe.json +42 -44
- package/library/recipes/perps/trading-lifecycle.core.recipe.json +69 -65
- package/library/recipes/runner/action-validation.extension.recipe.json +312 -405
- package/library/recipes/runner/action-validation.mobile.recipe.json +316 -409
- package/library/recipes/runner/smoke.core.recipe.json +18 -20
- package/library/recipes/runner/smoke.extension.recipe.json +23 -24
- package/library/recipes/runner/smoke.mobile.recipe.json +23 -24
- package/library/recipes/wallet/smoke.extension.recipe.json +33 -35
- package/library/recipes/wallet/smoke.mobile.recipe.json +33 -35
- package/package.json +3 -3
- package/scripts/completions.sh +1 -4
- package/dist/adapters/extension/harness-freshness.js +0 -39
- package/dist/commands/flows.js +0 -91
- package/library/flows/perps.flows.json +0 -64
package/library/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Recipe library
|
|
2
2
|
|
|
3
3
|
- An **action** performs one typed operation.
|
|
4
|
-
- A **
|
|
5
|
-
|
|
4
|
+
- A **recipe** is a parameterized, composable graph that may call actions and
|
|
5
|
+
other recipes.
|
|
6
6
|
|
|
7
7
|
- `actions/<adapter>/<group>/<name>.mjs` — executable action modules.
|
|
8
8
|
**Importing an action module RUNS it**: each ends with `runAdapter(main)` at top
|
|
@@ -13,6 +13,6 @@
|
|
|
13
13
|
- Actions load harness helpers ONLY through `actions/harness-exports.mjs` (the
|
|
14
14
|
dist-preferring bridge) — never deep-import `src/` (published installs have no
|
|
15
15
|
src/, and the entrypoint would close an import cycle through adapters.ts).
|
|
16
|
-
- `recipes/` — runnable
|
|
17
|
-
-
|
|
18
|
-
|
|
16
|
+
- `recipes/` — runnable recipes (`mm-harness run <name-or-file>`).
|
|
17
|
+
- Recipe resolution is explicit library order, then bundled MetaMask; selected
|
|
18
|
+
variants and shadows are recorded in run evidence.
|
|
@@ -246,6 +246,14 @@ export function resolveNetwork(input) {
|
|
|
246
246
|
return raw;
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
+
export function requireRuntimeExport(moduleNamespace, exportName, sourceLabel) {
|
|
250
|
+
const value = moduleNamespace?.[exportName] ?? moduleNamespace?.default?.[exportName];
|
|
251
|
+
if (value === undefined) {
|
|
252
|
+
throw new Error(`${sourceLabel} does not export ${exportName}.`);
|
|
253
|
+
}
|
|
254
|
+
return value;
|
|
255
|
+
}
|
|
256
|
+
|
|
249
257
|
/**
|
|
250
258
|
* Instantiate the PerpsController headlessly against the resolved core checkout.
|
|
251
259
|
* Returns the controller plus the resolved read account address. Cached per
|
|
@@ -261,10 +269,25 @@ export async function getCoreController(input) {
|
|
|
261
269
|
return { ...cached, accountAddress };
|
|
262
270
|
}
|
|
263
271
|
|
|
264
|
-
const [
|
|
272
|
+
const [controllerModule, messengerModule] = await Promise.all([
|
|
265
273
|
import(controllerEntry(projectRoot)),
|
|
266
274
|
import(messengerEntry(projectRoot)),
|
|
267
275
|
]);
|
|
276
|
+
const PerpsController = requireRuntimeExport(
|
|
277
|
+
controllerModule,
|
|
278
|
+
'PerpsController',
|
|
279
|
+
'Core perps-controller entrypoint',
|
|
280
|
+
);
|
|
281
|
+
const Messenger = requireRuntimeExport(
|
|
282
|
+
messengerModule,
|
|
283
|
+
'Messenger',
|
|
284
|
+
'Core messenger entrypoint',
|
|
285
|
+
);
|
|
286
|
+
const MOCK_ANY_NAMESPACE = requireRuntimeExport(
|
|
287
|
+
messengerModule,
|
|
288
|
+
'MOCK_ANY_NAMESPACE',
|
|
289
|
+
'Core messenger entrypoint',
|
|
290
|
+
);
|
|
268
291
|
|
|
269
292
|
const stubbed = new Set();
|
|
270
293
|
const infrastructure = buildInfrastructure(stubbed);
|
|
@@ -558,6 +581,15 @@ export function configuredSymbols(input, items) {
|
|
|
558
581
|
return single ? [normalizeMarketSymbol(single)] : [];
|
|
559
582
|
}
|
|
560
583
|
|
|
584
|
+
export function requireExplicitSelection(input) {
|
|
585
|
+
const selector =
|
|
586
|
+
input.node?.selector && typeof input.node.selector === 'object' ? input.node.selector : {};
|
|
587
|
+
const mode = String(input.node?.mode ?? selector.mode ?? '').toLowerCase();
|
|
588
|
+
if (configuredSymbols(input, []).length === 0 && mode !== 'all') {
|
|
589
|
+
throw new Error(`${input.action} requires market=<symbol> or mode=all.`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
561
593
|
/**
|
|
562
594
|
* Filter live items to the recipe-selected subset using the same market/side
|
|
563
595
|
* vocabulary as the extension perps adapter. With no selector and no
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
getCoreController,
|
|
3
3
|
isDirectRun,
|
|
4
4
|
redactOrder,
|
|
5
|
+
requireExplicitSelection,
|
|
5
6
|
runAdapter,
|
|
6
7
|
selectedItems,
|
|
7
8
|
} from './_controller.mjs';
|
|
@@ -12,30 +13,39 @@ import {
|
|
|
12
13
|
// assert_positions.mjs.
|
|
13
14
|
|
|
14
15
|
export function expectedOpen(input) {
|
|
15
|
-
|
|
16
|
+
if (input.node?.state == null) throw new Error('metamask.perps.assert_orders requires state=open or state=none.');
|
|
17
|
+
const state = String(input.node.state).toLowerCase();
|
|
16
18
|
if (state === 'open' || state === 'present') return true;
|
|
17
19
|
if (state === 'none' || state === 'closed' || state === 'absent') return false;
|
|
18
20
|
throw new Error(`metamask.perps.assert_orders received unsupported state: ${state}`);
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export async function assertOrders(input, expectOpen = expectedOpen(input)) {
|
|
24
|
+
requireExplicitSelection(input);
|
|
22
25
|
const { controller, accountAddress, network } = await getCoreController(input);
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
const timeoutMs = Number(input.node?.timeout_ms ?? 0);
|
|
27
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
28
|
+
let orders;
|
|
29
|
+
let matching;
|
|
30
|
+
while (true) {
|
|
31
|
+
orders = await controller.getOpenOrders({
|
|
32
|
+
standalone: true,
|
|
33
|
+
userAddress: accountAddress,
|
|
34
|
+
});
|
|
35
|
+
matching = selectedItems(input, orders);
|
|
36
|
+
if (expectOpen ? matching.length > 0 : matching.length === 0) break;
|
|
37
|
+
if (Date.now() >= deadline) break;
|
|
38
|
+
await new Promise((resolve) =>
|
|
39
|
+
setTimeout(resolve, Math.min(500, Math.max(1, deadline - Date.now()))),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
28
42
|
const hasOrder = matching.length > 0;
|
|
29
43
|
|
|
30
44
|
if (expectOpen && !hasOrder) {
|
|
31
|
-
throw new Error(
|
|
32
|
-
`Expected selected Perps open order(s), but none matched ${JSON.stringify(input.node)}.`,
|
|
33
|
-
);
|
|
45
|
+
throw new Error('Expected at least one matching open Perps order, but found none.');
|
|
34
46
|
}
|
|
35
47
|
if (!expectOpen && hasOrder) {
|
|
36
|
-
throw new Error(
|
|
37
|
-
`Expected no selected Perps open orders, but ${matching.length} matched ${JSON.stringify(input.node)}.`,
|
|
38
|
-
);
|
|
48
|
+
throw new Error(`Expected no matching open Perps orders, but found ${matching.length}.`);
|
|
39
49
|
}
|
|
40
50
|
|
|
41
51
|
return {
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
getCoreController,
|
|
3
3
|
isDirectRun,
|
|
4
4
|
redactPosition,
|
|
5
|
+
requireExplicitSelection,
|
|
5
6
|
runAdapter,
|
|
6
7
|
selectedItems,
|
|
7
8
|
} from './_controller.mjs';
|
|
@@ -11,30 +12,39 @@ import {
|
|
|
11
12
|
// throws on mismatch so the recipe fails loudly.
|
|
12
13
|
|
|
13
14
|
export function expectedOpen(input) {
|
|
14
|
-
|
|
15
|
+
if (input.node?.state == null) throw new Error('metamask.perps.assert_positions requires state=open or state=none.');
|
|
16
|
+
const state = String(input.node.state).toLowerCase();
|
|
15
17
|
if (state === 'open' || state === 'present') return true;
|
|
16
18
|
if (state === 'none' || state === 'closed' || state === 'absent') return false;
|
|
17
19
|
throw new Error(`metamask.perps.assert_positions received unsupported state: ${state}`);
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
export async function assertPositions(input, expectOpen = expectedOpen(input)) {
|
|
23
|
+
requireExplicitSelection(input);
|
|
21
24
|
const { controller, accountAddress, network } = await getCoreController(input);
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
const timeoutMs = Number(input.node?.timeout_ms ?? 0);
|
|
26
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
27
|
+
let positions;
|
|
28
|
+
let matching;
|
|
29
|
+
while (true) {
|
|
30
|
+
positions = await controller.getPositions({
|
|
31
|
+
standalone: true,
|
|
32
|
+
userAddress: accountAddress,
|
|
33
|
+
});
|
|
34
|
+
matching = selectedItems(input, positions);
|
|
35
|
+
if (expectOpen ? matching.length > 0 : matching.length === 0) break;
|
|
36
|
+
if (Date.now() >= deadline) break;
|
|
37
|
+
await new Promise((resolve) =>
|
|
38
|
+
setTimeout(resolve, Math.min(500, Math.max(1, deadline - Date.now()))),
|
|
39
|
+
);
|
|
40
|
+
}
|
|
27
41
|
const hasPosition = matching.length > 0;
|
|
28
42
|
|
|
29
43
|
if (expectOpen && !hasPosition) {
|
|
30
|
-
throw new Error(
|
|
31
|
-
`Expected selected Perps position(s), but none matched ${JSON.stringify(input.node)}.`,
|
|
32
|
-
);
|
|
44
|
+
throw new Error('Expected at least one matching open Perps position, but found none.');
|
|
33
45
|
}
|
|
34
46
|
if (!expectOpen && hasPosition) {
|
|
35
|
-
throw new Error(
|
|
36
|
-
`Expected no selected Perps positions, but ${matching.length} matched ${JSON.stringify(input.node)}.`,
|
|
37
|
-
);
|
|
47
|
+
throw new Error(`Expected no matching open Perps positions, but found ${matching.length}.`);
|
|
38
48
|
}
|
|
39
49
|
|
|
40
50
|
return {
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
getCoreControllerWithSigner,
|
|
3
3
|
isDirectRun,
|
|
4
4
|
redactOrder,
|
|
5
|
+
requireExplicitSelection,
|
|
5
6
|
runAdapter,
|
|
6
7
|
selectedItems,
|
|
7
8
|
symbolForItem,
|
|
@@ -37,6 +38,7 @@ async function waitForOrdersAbsent(controller, accountAddress, symbols, timeoutM
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
export async function closeOrders(input) {
|
|
41
|
+
requireExplicitSelection(input);
|
|
40
42
|
const { controller, accountAddress, network } = await getCoreControllerWithSigner(input);
|
|
41
43
|
const timeoutMs = Number(input.node?.timeout_ms ?? 30000);
|
|
42
44
|
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
getCoreControllerWithSigner,
|
|
4
4
|
isDirectRun,
|
|
5
5
|
redactPosition,
|
|
6
|
+
requireExplicitSelection,
|
|
6
7
|
runAdapter,
|
|
7
8
|
selectedItems,
|
|
8
9
|
symbolForItem,
|
|
@@ -36,6 +37,7 @@ async function waitForPositionsAbsent(controller, accountAddress, symbols, timeo
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
export async function closePositions(input) {
|
|
40
|
+
requireExplicitSelection(input);
|
|
39
41
|
const { controller, accountAddress, network } = await getCoreControllerWithSigner(input);
|
|
40
42
|
const timeoutMs = Number(input.node?.timeout_ms ?? 30000);
|
|
41
43
|
const maxSlippageBps = Number(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getCoreController, isDirectRun, runAdapter, selectedItems } from './_controller.mjs';
|
|
1
|
+
import { getCoreController, isDirectRun, requireExplicitSelection, runAdapter, selectedItems } from './_controller.mjs';
|
|
2
2
|
import { assertOrders } from './assert_orders.mjs';
|
|
3
3
|
import { closeOrders } from './close_orders.mjs';
|
|
4
4
|
import { placeOrder } from './place_order.mjs';
|
|
@@ -8,7 +8,9 @@ import { placeOrder } from './place_order.mjs';
|
|
|
8
8
|
// order state. Mirrors ensure_positions.mjs.
|
|
9
9
|
|
|
10
10
|
export async function ensureOrders(input) {
|
|
11
|
-
|
|
11
|
+
if (input.node?.state == null) throw new Error('metamask.perps.ensure_orders requires state=open or state=none.');
|
|
12
|
+
requireExplicitSelection(input);
|
|
13
|
+
const state = String(input.node.state).toLowerCase();
|
|
12
14
|
|
|
13
15
|
if (state === 'none' || state === 'closed' || state === 'absent') {
|
|
14
16
|
const close = await closeOrders(input);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getCoreController, isDirectRun, runAdapter, selectedItems } from './_controller.mjs';
|
|
1
|
+
import { getCoreController, isDirectRun, requireExplicitSelection, runAdapter, selectedItems } from './_controller.mjs';
|
|
2
2
|
import { assertPositions } from './assert_positions.mjs';
|
|
3
3
|
import { closePositions } from './close_positions.mjs';
|
|
4
4
|
import { placeOrder } from './place_order.mjs';
|
|
@@ -8,7 +8,9 @@ import { placeOrder } from './place_order.mjs';
|
|
|
8
8
|
// state. Mirrors the extension ensure_positions semantics.
|
|
9
9
|
|
|
10
10
|
export async function ensurePositions(input) {
|
|
11
|
-
|
|
11
|
+
if (input.node?.state == null) throw new Error('metamask.perps.ensure_positions requires state=open or state=none.');
|
|
12
|
+
requireExplicitSelection(input);
|
|
13
|
+
const state = String(input.node.state).toLowerCase();
|
|
12
14
|
|
|
13
15
|
if (state === 'none' || state === 'closed' || state === 'absent') {
|
|
14
16
|
const close = await closePositions(input);
|
|
@@ -81,12 +81,16 @@ function resolveLimitPrice(input, isBuy, mid) {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
export async function placeOrder(input) {
|
|
84
|
-
const { controller, accountAddress, network } = await getCoreControllerWithSigner(input);
|
|
85
84
|
const symbol = resolveSymbol(input);
|
|
86
|
-
|
|
85
|
+
if (input.node?.side == null) throw new Error('metamask.perps.place_order requires side=long or side=short.');
|
|
86
|
+
if (input.node?.amount == null && input.node?.notional == null) {
|
|
87
|
+
throw new Error('metamask.perps.place_order requires amount=<usd> or notional=<usd>.');
|
|
88
|
+
}
|
|
89
|
+
const { controller, accountAddress, network } = await getCoreControllerWithSigner(input);
|
|
90
|
+
const side = String(input.node.side).toLowerCase();
|
|
87
91
|
const isBuy = side !== 'short';
|
|
88
92
|
const orderType = resolveOrderType(input);
|
|
89
|
-
const usdAmount = String(input.node
|
|
93
|
+
const usdAmount = String(input.node.amount ?? input.node.notional);
|
|
90
94
|
const leverage = Number(input.node?.leverage ?? 3);
|
|
91
95
|
const maxSlippageBps = Number(
|
|
92
96
|
input.node?.max_slippage_bps ?? input.node?.maxSlippageBps ?? 300,
|
|
@@ -2,7 +2,8 @@ import { runAdapter } from '../platform/cdp.mjs';
|
|
|
2
2
|
import { assertOrders } from './perps.mjs';
|
|
3
3
|
|
|
4
4
|
function expectedOpen(input) {
|
|
5
|
-
|
|
5
|
+
if (input.node?.state == null) throw new Error('metamask.perps.assert_orders requires state=open or state=none.');
|
|
6
|
+
const state = String(input.node.state).toLowerCase();
|
|
6
7
|
if (state === 'open' || state === 'present') return true;
|
|
7
8
|
if (state === 'none' || state === 'closed' || state === 'absent') return false;
|
|
8
9
|
throw new Error(`metamask.perps.assert_orders received unsupported state: ${state}`);
|
|
@@ -2,7 +2,8 @@ import { runAdapter } from '../platform/cdp.mjs';
|
|
|
2
2
|
import { assertPositions } from './perps.mjs';
|
|
3
3
|
|
|
4
4
|
function expectedOpen(input) {
|
|
5
|
-
|
|
5
|
+
if (input.node?.state == null) throw new Error('metamask.perps.assert_positions requires state=open or state=none.');
|
|
6
|
+
const state = String(input.node.state).toLowerCase();
|
|
6
7
|
if (state === 'open' || state === 'present') return true;
|
|
7
8
|
if (state === 'none' || state === 'closed' || state === 'absent') return false;
|
|
8
9
|
throw new Error(`metamask.perps.assert_positions received unsupported state: ${state}`);
|
|
@@ -10,7 +10,10 @@ function symbolForItem(item) {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
function sideForItem(item) {
|
|
13
|
-
|
|
13
|
+
const side = String(item?.side ?? item?.direction ?? '').toLowerCase();
|
|
14
|
+
if (side === 'buy' || side === 'b') return 'long';
|
|
15
|
+
if (side === 'sell' || side === 'a' || side === 's') return 'short';
|
|
16
|
+
return side;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
function uniqueSymbols(symbols) {
|
|
@@ -24,20 +27,68 @@ function configuredSymbols(input, items) {
|
|
|
24
27
|
const explicit = input.node?.markets ?? input.node?.symbols ?? selector.markets ?? selector.symbols;
|
|
25
28
|
if (Array.isArray(explicit) && explicit.length > 0) return uniqueSymbols(explicit.map(normalizeMarketSymbol));
|
|
26
29
|
if (typeof explicit === 'string' && explicit.length > 0) return uniqueSymbols(explicit.split(',').map((part) => normalizeMarketSymbol(part.trim())).filter(Boolean));
|
|
27
|
-
|
|
30
|
+
const single = input.node?.market ?? input.node?.symbol ?? selector.market ?? selector.symbol;
|
|
31
|
+
return single ? [normalizeMarketSymbol(single)] : [];
|
|
28
32
|
}
|
|
29
33
|
|
|
30
|
-
function selectedItems(input, items) {
|
|
31
|
-
const
|
|
34
|
+
export function selectedItems(input, items) {
|
|
35
|
+
const requested = configuredSymbols(input, items);
|
|
36
|
+
const symbols = requested.length > 0 ? new Set(requested) : null;
|
|
32
37
|
const selector = input.node?.selector && typeof input.node.selector === 'object' ? input.node.selector : {};
|
|
33
38
|
const side = input.node?.side ?? selector.side;
|
|
34
39
|
return items.filter((item) => {
|
|
35
|
-
if (!symbols.has(symbolForItem(item))) return false;
|
|
40
|
+
if (symbols && !symbols.has(symbolForItem(item))) return false;
|
|
36
41
|
if (side && sideForItem(item) && sideForItem(item) !== String(side).toLowerCase()) return false;
|
|
37
42
|
return true;
|
|
38
43
|
});
|
|
39
44
|
}
|
|
40
45
|
|
|
46
|
+
function requireExplicitSelection(input) {
|
|
47
|
+
const node = input.node ?? {};
|
|
48
|
+
const selector = node.selector && typeof node.selector === 'object' ? node.selector : {};
|
|
49
|
+
const values = [node.market, node.symbol, node.markets, node.symbols, selector.market, selector.symbol, selector.markets, selector.symbols];
|
|
50
|
+
const selected = values.some((value) => Array.isArray(value) ? value.length > 0 : value != null && String(value).trim() !== '');
|
|
51
|
+
if (!selected && node.mode !== 'all' && selector.mode !== 'all') {
|
|
52
|
+
throw new Error(`${input.action} requires market=<symbol> or mode=all.`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function requirePlaceOrderInputs(input) {
|
|
57
|
+
const node = input.node ?? {};
|
|
58
|
+
if (node.market == null && node.symbol == null) throw new Error(`${input.action} requires market=<symbol>.`);
|
|
59
|
+
if (node.side == null) throw new Error(`${input.action} requires side=long or side=short.`);
|
|
60
|
+
if (node.amount == null && node.notional == null) throw new Error(`${input.action} requires amount=<usd> or notional=<usd>.`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resolveOrderType(input) {
|
|
64
|
+
const value = String(input.node?.order_type ?? input.node?.orderType ?? 'market').toLowerCase();
|
|
65
|
+
if (value !== 'market' && value !== 'limit') {
|
|
66
|
+
throw new Error(`${input.action} received unsupported order_type: ${value}.`);
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resolveLimitPrice(input, isBuy, currentPrice) {
|
|
72
|
+
const explicit = input.node?.limit_price ?? input.node?.limitPrice ?? input.node?.price;
|
|
73
|
+
if (explicit != null && String(explicit).length > 0) {
|
|
74
|
+
const value = Number(explicit);
|
|
75
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
76
|
+
throw new Error(`${input.action} received invalid limit_price: ${explicit}.`);
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
const rawOffset = input.node?.offset_pct ?? input.node?.offsetPct;
|
|
81
|
+
const offset = rawOffset == null ? (isBuy ? -30 : 30) : Number(rawOffset);
|
|
82
|
+
if (!Number.isFinite(offset)) {
|
|
83
|
+
throw new Error(`${input.action} received invalid offset_pct: ${rawOffset}.`);
|
|
84
|
+
}
|
|
85
|
+
const value = currentPrice * (1 + offset / 100);
|
|
86
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
87
|
+
throw new Error(`${input.action} computed a non-positive limit price (${value}).`);
|
|
88
|
+
}
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
|
|
41
92
|
export function positionSelectionState(input, positions) {
|
|
42
93
|
return { hasMatching: selectedItems(input, positions).length > 0 };
|
|
43
94
|
}
|
|
@@ -78,40 +129,41 @@ export async function readPositions(input) {
|
|
|
78
129
|
|
|
79
130
|
export async function readOrders(input) {
|
|
80
131
|
return withExtensionPage(input, async (page) => {
|
|
81
|
-
const
|
|
82
|
-
const matching = selectedItems(input, orders);
|
|
83
|
-
return { action: input.action, source:
|
|
132
|
+
const state = await readOpenOrdersState(page);
|
|
133
|
+
const matching = selectedItems(input, state.orders);
|
|
134
|
+
return { action: input.action, source: state.source, count: state.orders.length, matchingCount: matching.length, orders: matching.map(redactOrder) };
|
|
84
135
|
});
|
|
85
136
|
}
|
|
86
137
|
|
|
87
138
|
export async function assertPositions(input, expectedOpen) {
|
|
139
|
+
requireExplicitSelection(input);
|
|
88
140
|
return withExtensionPage(input, async (page) => {
|
|
89
141
|
const timeoutMs = Number(input.node?.timeout_ms ?? 0);
|
|
90
|
-
|
|
91
|
-
await waitForPositionPresent(page, marketSymbol(input), timeoutMs);
|
|
92
|
-
}
|
|
93
|
-
const state = await page.readPositions();
|
|
142
|
+
const state = await waitForSelectedPositionState(page, input, expectedOpen, timeoutMs);
|
|
94
143
|
if (!state.available) throw new Error('stateHooks.submitRequestToBackground is unavailable; cannot assert live Perps positions.');
|
|
95
144
|
const matching = selectedItems(input, state.positions);
|
|
96
145
|
const hasPosition = matching.length > 0;
|
|
97
|
-
if (expectedOpen && !hasPosition) throw new Error(
|
|
98
|
-
if (!expectedOpen && hasPosition) throw new Error(`Expected no
|
|
146
|
+
if (expectedOpen && !hasPosition) throw new Error('Expected at least one matching open Perps position, but found none.');
|
|
147
|
+
if (!expectedOpen && hasPosition) throw new Error(`Expected no matching open Perps positions, but found ${matching.length}.`);
|
|
99
148
|
return { action: input.action, expectedOpen, matchingCount: matching.length, positions: matching.map(redactPosition), source: state.source ?? 'perps-stream-manager-cache' };
|
|
100
149
|
});
|
|
101
150
|
}
|
|
102
151
|
|
|
103
152
|
export async function assertOrders(input, expectedOpen) {
|
|
153
|
+
requireExplicitSelection(input);
|
|
104
154
|
return withExtensionPage(input, async (page) => {
|
|
105
|
-
const
|
|
106
|
-
const
|
|
155
|
+
const timeoutMs = Number(input.node?.timeout_ms ?? 0);
|
|
156
|
+
const state = await waitForSelectedOrderState(page, input, expectedOpen, timeoutMs);
|
|
157
|
+
const matching = selectedItems(input, state.orders);
|
|
107
158
|
const hasOrders = matching.length > 0;
|
|
108
|
-
if (expectedOpen && !hasOrders) throw new Error(
|
|
109
|
-
if (!expectedOpen && hasOrders) throw new Error(`Expected no
|
|
110
|
-
return { action: input.action, expectedOpen, matchingCount: matching.length, orders: matching.map(redactOrder), source:
|
|
159
|
+
if (expectedOpen && !hasOrders) throw new Error('Expected at least one matching open Perps order, but found none.');
|
|
160
|
+
if (!expectedOpen && hasOrders) throw new Error(`Expected no matching open Perps orders, but found ${matching.length}.`);
|
|
161
|
+
return { action: input.action, expectedOpen, matchingCount: matching.length, orders: matching.map(redactOrder), source: state.source };
|
|
111
162
|
});
|
|
112
163
|
}
|
|
113
164
|
|
|
114
165
|
export async function closePositions(input) {
|
|
166
|
+
requireExplicitSelection(input);
|
|
115
167
|
return withExtensionPage(input, async (page) => {
|
|
116
168
|
const timeoutMs = Number(input.node?.timeout_ms ?? 30000);
|
|
117
169
|
const state = await page.readPositions();
|
|
@@ -147,6 +199,7 @@ export async function closePositions(input) {
|
|
|
147
199
|
}
|
|
148
200
|
|
|
149
201
|
export async function closeOrders(input) {
|
|
202
|
+
requireExplicitSelection(input);
|
|
150
203
|
return withExtensionPage(input, async (page) => {
|
|
151
204
|
const timeoutMs = Number(input.node?.timeout_ms ?? 30000);
|
|
152
205
|
const before = await readOpenOrders(page);
|
|
@@ -282,6 +335,10 @@ async function readCurrentMarketPrice(page, symbol, explicitPrice) {
|
|
|
282
335
|
}
|
|
283
336
|
|
|
284
337
|
async function readOpenOrders(page) {
|
|
338
|
+
return (await readOpenOrdersState(page)).orders;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function readOpenOrdersState(page) {
|
|
285
342
|
const state = await page.evaluate(`(async () => {
|
|
286
343
|
const request = globalThis.stateHooks?.submitRequestToBackground;
|
|
287
344
|
const manager = globalThis.stateHooks?.getPerpsStreamManager?.();
|
|
@@ -302,7 +359,7 @@ async function readOpenOrders(page) {
|
|
|
302
359
|
};
|
|
303
360
|
})()`);
|
|
304
361
|
if (!state.available) throw new Error('Perps orders are unavailable from both background and stream manager cache.');
|
|
305
|
-
return state
|
|
362
|
+
return state;
|
|
306
363
|
}
|
|
307
364
|
|
|
308
365
|
async function readAccountState(page) {
|
|
@@ -369,11 +426,41 @@ async function waitForPositionPresent(page, symbol, timeoutMs) {
|
|
|
369
426
|
return lastState;
|
|
370
427
|
}
|
|
371
428
|
|
|
429
|
+
async function waitForSelectedPositionState(page, input, expectedOpen, timeoutMs) {
|
|
430
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
431
|
+
let state;
|
|
432
|
+
while (true) {
|
|
433
|
+
state = await page.readPositions();
|
|
434
|
+
if (!state.available) {
|
|
435
|
+
throw new Error('stateHooks.submitRequestToBackground is unavailable; cannot assert live Perps positions.');
|
|
436
|
+
}
|
|
437
|
+
const matching = selectedItems(input, state.positions);
|
|
438
|
+
if (expectedOpen ? matching.length > 0 : matching.length === 0) return state;
|
|
439
|
+
if (Date.now() >= deadline) return state;
|
|
440
|
+
await sleep(Math.min(500, Math.max(1, deadline - Date.now())));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function waitForSelectedOrderState(page, input, expectedOpen, timeoutMs) {
|
|
445
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
446
|
+
let state;
|
|
447
|
+
while (true) {
|
|
448
|
+
state = await readOpenOrdersState(page);
|
|
449
|
+
const matching = selectedItems(input, state.orders);
|
|
450
|
+
if (expectedOpen ? matching.length > 0 : matching.length === 0) return state;
|
|
451
|
+
if (Date.now() >= deadline) return state;
|
|
452
|
+
await sleep(Math.min(500, Math.max(1, deadline - Date.now())));
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
372
456
|
export async function placeOrder(input) {
|
|
457
|
+
requirePlaceOrderInputs(input);
|
|
373
458
|
const symbol = marketSymbol(input);
|
|
374
|
-
const side = String(input.node
|
|
375
|
-
const amount = String(input.node
|
|
459
|
+
const side = String(input.node.side).toLowerCase();
|
|
460
|
+
const amount = String(input.node.amount ?? input.node.notional);
|
|
376
461
|
const leverage = Number(input.node?.leverage ?? 3);
|
|
462
|
+
const isBuy = side !== 'short';
|
|
463
|
+
const orderType = resolveOrderType(input);
|
|
377
464
|
return withExtensionPage(input, async (page) => {
|
|
378
465
|
await page.navigateHash(`#/perps/market/${encodeURIComponent(symbol)}`);
|
|
379
466
|
await page.evaluate(`(async () => {
|
|
@@ -390,6 +477,9 @@ export async function placeOrder(input) {
|
|
|
390
477
|
symbol,
|
|
391
478
|
input.node?.current_price ?? input.node?.currentPrice,
|
|
392
479
|
);
|
|
480
|
+
const limitPrice = orderType === 'limit'
|
|
481
|
+
? resolveLimitPrice(input, isBuy, resolvedPrice)
|
|
482
|
+
: null;
|
|
393
483
|
await page.navigateHash(`#/perps/trade/${encodeURIComponent(symbol)}?direction=${encodeURIComponent(side)}&mode=new`);
|
|
394
484
|
await page.waitForSelector(dataTestId('perps-order-entry-page'), { timeoutMs: 20000 });
|
|
395
485
|
let order;
|
|
@@ -401,13 +491,27 @@ export async function placeOrder(input) {
|
|
|
401
491
|
if (!Number.isFinite(currentPrice) || currentPrice <= 0) throw new Error('Unable to determine current Perps price for order placement.');
|
|
402
492
|
const usdAmount = ${JSON.stringify(amount)};
|
|
403
493
|
const leverage = ${JSON.stringify(leverage)};
|
|
404
|
-
const
|
|
494
|
+
const orderType = ${JSON.stringify(orderType)};
|
|
495
|
+
const limitPrice = ${JSON.stringify(limitPrice)};
|
|
496
|
+
const orderParams = orderType === 'limit' ? {
|
|
405
497
|
symbol: ${JSON.stringify(symbol)},
|
|
406
|
-
isBuy: ${JSON.stringify(
|
|
498
|
+
isBuy: ${JSON.stringify(isBuy)},
|
|
499
|
+
size: ((Number(usdAmount) * Number(leverage)) / Number(limitPrice)).toString(),
|
|
500
|
+
orderType,
|
|
501
|
+
price: String(limitPrice),
|
|
502
|
+
timeInForce: 'GTC',
|
|
503
|
+
leverage,
|
|
504
|
+
currentPrice,
|
|
505
|
+
priceAtCalculation: currentPrice,
|
|
506
|
+
maxSlippageBps: ${JSON.stringify(Number(input.node?.max_slippage_bps ?? input.node?.maxSlippageBps ?? 300))},
|
|
507
|
+
} : {
|
|
508
|
+
symbol: ${JSON.stringify(symbol)},
|
|
509
|
+
isBuy: ${JSON.stringify(isBuy)},
|
|
407
510
|
size: ((Number(usdAmount) * Number(leverage)) / currentPrice).toString(),
|
|
408
|
-
orderType
|
|
511
|
+
orderType,
|
|
409
512
|
leverage,
|
|
410
513
|
currentPrice,
|
|
514
|
+
priceAtCalculation: currentPrice,
|
|
411
515
|
usdAmount,
|
|
412
516
|
maxSlippageBps: ${JSON.stringify(Number(input.node?.max_slippage_bps ?? input.node?.maxSlippageBps ?? 300))},
|
|
413
517
|
};
|
|
@@ -421,13 +525,19 @@ export async function placeOrder(input) {
|
|
|
421
525
|
const message = error instanceof Error ? error.message : String(error);
|
|
422
526
|
throw new Error(`${message} (resolvedPrice=${resolvedPrice})`);
|
|
423
527
|
}
|
|
424
|
-
|
|
425
|
-
|
|
528
|
+
if (orderType === 'limit') {
|
|
529
|
+
await waitForSelectedOrderState(page, input, true, Number(input.node?.timeout_ms ?? 30000));
|
|
530
|
+
} else {
|
|
531
|
+
await waitForPositionPresent(page, symbol, Number(input.node?.timeout_ms ?? 30000));
|
|
532
|
+
}
|
|
533
|
+
return { action: input.action, market: symbol, side, orderType, amount, leverage, limitPrice, submitted: true, order, proofPath: 'background-perpsPlaceOrder' };
|
|
426
534
|
});
|
|
427
535
|
}
|
|
428
536
|
|
|
429
537
|
export async function ensurePositions(input) {
|
|
430
|
-
|
|
538
|
+
if (input.node?.state == null) throw new Error('metamask.perps.ensure_positions requires state=open or state=none.');
|
|
539
|
+
requireExplicitSelection(input);
|
|
540
|
+
const state = String(input.node.state).toLowerCase();
|
|
431
541
|
if (state === 'none' || state === 'closed' || state === 'absent') {
|
|
432
542
|
const close = await closePositions(input);
|
|
433
543
|
return { ...(await assertPositions(input, false)), close };
|
|
@@ -445,12 +555,23 @@ export async function ensurePositions(input) {
|
|
|
445
555
|
}
|
|
446
556
|
|
|
447
557
|
export async function ensureOrders(input) {
|
|
448
|
-
|
|
558
|
+
if (input.node?.state == null) throw new Error('metamask.perps.ensure_orders requires state=open or state=none.');
|
|
559
|
+
requireExplicitSelection(input);
|
|
560
|
+
const state = String(input.node.state).toLowerCase();
|
|
449
561
|
if (state === 'none' || state === 'closed' || state === 'absent') {
|
|
450
562
|
const close = await closeOrders(input);
|
|
451
563
|
return { ...(await assertOrders(input, false)), close };
|
|
452
564
|
}
|
|
453
|
-
if (state === 'open' || state === 'present')
|
|
565
|
+
if (state === 'open' || state === 'present') {
|
|
566
|
+
const current = await withExtensionPage(input, async (page) => ({
|
|
567
|
+
orders: await readOpenOrders(page),
|
|
568
|
+
}));
|
|
569
|
+
let order = null;
|
|
570
|
+
if (selectedItems(input, current.orders).length === 0) {
|
|
571
|
+
order = await placeOrder({ ...input, node: { ...input.node, order_type: 'limit' } });
|
|
572
|
+
}
|
|
573
|
+
return { ...(await assertOrders(input, true)), order };
|
|
574
|
+
}
|
|
454
575
|
throw new Error(`metamask.perps.ensure_orders received unsupported state: ${state}`);
|
|
455
576
|
}
|
|
456
577
|
|
|
@@ -2,7 +2,8 @@ import { runAdapter } from '../platform/bridge.mjs';
|
|
|
2
2
|
import { assertOrders } from './perps.mjs';
|
|
3
3
|
|
|
4
4
|
function expectedOpen(input) {
|
|
5
|
-
|
|
5
|
+
if (input.node?.state == null) throw new Error('metamask.perps.assert_orders requires state=open or state=none.');
|
|
6
|
+
const state = String(input.node.state).toLowerCase();
|
|
6
7
|
if (state === 'open' || state === 'present') return true;
|
|
7
8
|
if (state === 'none' || state === 'closed' || state === 'absent') return false;
|
|
8
9
|
throw new Error(`metamask.perps.assert_orders received unsupported state: ${state}`);
|
|
@@ -2,7 +2,8 @@ import { runAdapter } from '../platform/bridge.mjs';
|
|
|
2
2
|
import { assertPositions } from './perps.mjs';
|
|
3
3
|
|
|
4
4
|
function expectedOpen(input) {
|
|
5
|
-
|
|
5
|
+
if (input.node?.state == null) throw new Error('metamask.perps.assert_positions requires state=open or state=none.');
|
|
6
|
+
const state = String(input.node.state).toLowerCase();
|
|
6
7
|
if (state === 'open' || state === 'present') return true;
|
|
7
8
|
if (state === 'none' || state === 'closed' || state === 'absent') return false;
|
|
8
9
|
throw new Error(`metamask.perps.assert_positions received unsupported state: ${state}`);
|