@deeeed/metamask-harness 0.18.0 → 0.19.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 +187 -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/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/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/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 +52 -91
- package/library/README.md +5 -5
- package/library/actions/core/perps/_controller.mjs +33 -1
- package/library/actions/core/perps/assert_orders.mjs +6 -7
- package/library/actions/core/perps/assert_positions.mjs +6 -7
- 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 +43 -14
- 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 +40 -12
- package/library/library.json +1 -1
- package/library/manifests/core.action-manifest.json +1170 -413
- package/library/manifests/extension.action-manifest.json +1495 -641
- package/library/manifests/mobile.action-manifest.json +1704 -769
- 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
|
@@ -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,13 +13,15 @@ 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
26
|
const orders = await controller.getOpenOrders({
|
|
24
27
|
standalone: true,
|
|
@@ -28,14 +31,10 @@ export async function assertOrders(input, expectOpen = expectedOpen(input)) {
|
|
|
28
31
|
const hasOrder = matching.length > 0;
|
|
29
32
|
|
|
30
33
|
if (expectOpen && !hasOrder) {
|
|
31
|
-
throw new Error(
|
|
32
|
-
`Expected selected Perps open order(s), but none matched ${JSON.stringify(input.node)}.`,
|
|
33
|
-
);
|
|
34
|
+
throw new Error('Expected at least one matching open Perps order, but found none.');
|
|
34
35
|
}
|
|
35
36
|
if (!expectOpen && hasOrder) {
|
|
36
|
-
throw new Error(
|
|
37
|
-
`Expected no selected Perps open orders, but ${matching.length} matched ${JSON.stringify(input.node)}.`,
|
|
38
|
-
);
|
|
37
|
+
throw new Error(`Expected no matching open Perps orders, but found ${matching.length}.`);
|
|
39
38
|
}
|
|
40
39
|
|
|
41
40
|
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,13 +12,15 @@ 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
25
|
const positions = await controller.getPositions({
|
|
23
26
|
standalone: true,
|
|
@@ -27,14 +30,10 @@ export async function assertPositions(input, expectOpen = expectedOpen(input)) {
|
|
|
27
30
|
const hasPosition = matching.length > 0;
|
|
28
31
|
|
|
29
32
|
if (expectOpen && !hasPosition) {
|
|
30
|
-
throw new Error(
|
|
31
|
-
`Expected selected Perps position(s), but none matched ${JSON.stringify(input.node)}.`,
|
|
32
|
-
);
|
|
33
|
+
throw new Error('Expected at least one matching open Perps position, but found none.');
|
|
33
34
|
}
|
|
34
35
|
if (!expectOpen && hasPosition) {
|
|
35
|
-
throw new Error(
|
|
36
|
-
`Expected no selected Perps positions, but ${matching.length} matched ${JSON.stringify(input.node)}.`,
|
|
37
|
-
);
|
|
36
|
+
throw new Error(`Expected no matching open Perps positions, but found ${matching.length}.`);
|
|
38
37
|
}
|
|
39
38
|
|
|
40
39
|
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}`);
|
|
@@ -24,20 +24,39 @@ function configuredSymbols(input, items) {
|
|
|
24
24
|
const explicit = input.node?.markets ?? input.node?.symbols ?? selector.markets ?? selector.symbols;
|
|
25
25
|
if (Array.isArray(explicit) && explicit.length > 0) return uniqueSymbols(explicit.map(normalizeMarketSymbol));
|
|
26
26
|
if (typeof explicit === 'string' && explicit.length > 0) return uniqueSymbols(explicit.split(',').map((part) => normalizeMarketSymbol(part.trim())).filter(Boolean));
|
|
27
|
-
|
|
27
|
+
const single = input.node?.market ?? input.node?.symbol ?? selector.market ?? selector.symbol;
|
|
28
|
+
return single ? [normalizeMarketSymbol(single)] : [];
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
function selectedItems(input, items) {
|
|
31
|
-
const
|
|
31
|
+
export function selectedItems(input, items) {
|
|
32
|
+
const requested = configuredSymbols(input, items);
|
|
33
|
+
const symbols = requested.length > 0 ? new Set(requested) : null;
|
|
32
34
|
const selector = input.node?.selector && typeof input.node.selector === 'object' ? input.node.selector : {};
|
|
33
35
|
const side = input.node?.side ?? selector.side;
|
|
34
36
|
return items.filter((item) => {
|
|
35
|
-
if (!symbols.has(symbolForItem(item))) return false;
|
|
37
|
+
if (symbols && !symbols.has(symbolForItem(item))) return false;
|
|
36
38
|
if (side && sideForItem(item) && sideForItem(item) !== String(side).toLowerCase()) return false;
|
|
37
39
|
return true;
|
|
38
40
|
});
|
|
39
41
|
}
|
|
40
42
|
|
|
43
|
+
function requireExplicitSelection(input) {
|
|
44
|
+
const node = input.node ?? {};
|
|
45
|
+
const selector = node.selector && typeof node.selector === 'object' ? node.selector : {};
|
|
46
|
+
const values = [node.market, node.symbol, node.markets, node.symbols, selector.market, selector.symbol, selector.markets, selector.symbols];
|
|
47
|
+
const selected = values.some((value) => Array.isArray(value) ? value.length > 0 : value != null && String(value).trim() !== '');
|
|
48
|
+
if (!selected && node.mode !== 'all' && selector.mode !== 'all') {
|
|
49
|
+
throw new Error(`${input.action} requires market=<symbol> or mode=all.`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function requirePlaceOrderInputs(input) {
|
|
54
|
+
const node = input.node ?? {};
|
|
55
|
+
if (node.market == null && node.symbol == null) throw new Error(`${input.action} requires market=<symbol>.`);
|
|
56
|
+
if (node.side == null) throw new Error(`${input.action} requires side=long or side=short.`);
|
|
57
|
+
if (node.amount == null && node.notional == null) throw new Error(`${input.action} requires amount=<usd> or notional=<usd>.`);
|
|
58
|
+
}
|
|
59
|
+
|
|
41
60
|
export function positionSelectionState(input, positions) {
|
|
42
61
|
return { hasMatching: selectedItems(input, positions).length > 0 };
|
|
43
62
|
}
|
|
@@ -85,33 +104,37 @@ export async function readOrders(input) {
|
|
|
85
104
|
}
|
|
86
105
|
|
|
87
106
|
export async function assertPositions(input, expectedOpen) {
|
|
107
|
+
requireExplicitSelection(input);
|
|
88
108
|
return withExtensionPage(input, async (page) => {
|
|
89
109
|
const timeoutMs = Number(input.node?.timeout_ms ?? 0);
|
|
90
|
-
|
|
91
|
-
|
|
110
|
+
const requested = configuredSymbols(input, []);
|
|
111
|
+
if (expectedOpen && timeoutMs > 0 && requested.length === 1) {
|
|
112
|
+
await waitForPositionPresent(page, requested[0], timeoutMs);
|
|
92
113
|
}
|
|
93
114
|
const state = await page.readPositions();
|
|
94
115
|
if (!state.available) throw new Error('stateHooks.submitRequestToBackground is unavailable; cannot assert live Perps positions.');
|
|
95
116
|
const matching = selectedItems(input, state.positions);
|
|
96
117
|
const hasPosition = matching.length > 0;
|
|
97
|
-
if (expectedOpen && !hasPosition) throw new Error(
|
|
98
|
-
if (!expectedOpen && hasPosition) throw new Error(`Expected no
|
|
118
|
+
if (expectedOpen && !hasPosition) throw new Error('Expected at least one matching open Perps position, but found none.');
|
|
119
|
+
if (!expectedOpen && hasPosition) throw new Error(`Expected no matching open Perps positions, but found ${matching.length}.`);
|
|
99
120
|
return { action: input.action, expectedOpen, matchingCount: matching.length, positions: matching.map(redactPosition), source: state.source ?? 'perps-stream-manager-cache' };
|
|
100
121
|
});
|
|
101
122
|
}
|
|
102
123
|
|
|
103
124
|
export async function assertOrders(input, expectedOpen) {
|
|
125
|
+
requireExplicitSelection(input);
|
|
104
126
|
return withExtensionPage(input, async (page) => {
|
|
105
127
|
const orders = await readOpenOrders(page);
|
|
106
128
|
const matching = selectedItems(input, orders);
|
|
107
129
|
const hasOrders = matching.length > 0;
|
|
108
|
-
if (expectedOpen && !hasOrders) throw new Error(
|
|
109
|
-
if (!expectedOpen && hasOrders) throw new Error(`Expected no
|
|
130
|
+
if (expectedOpen && !hasOrders) throw new Error('Expected at least one matching open Perps order, but found none.');
|
|
131
|
+
if (!expectedOpen && hasOrders) throw new Error(`Expected no matching open Perps orders, but found ${matching.length}.`);
|
|
110
132
|
return { action: input.action, expectedOpen, matchingCount: matching.length, orders: matching.map(redactOrder), source: 'perps-stream-manager-cache' };
|
|
111
133
|
});
|
|
112
134
|
}
|
|
113
135
|
|
|
114
136
|
export async function closePositions(input) {
|
|
137
|
+
requireExplicitSelection(input);
|
|
115
138
|
return withExtensionPage(input, async (page) => {
|
|
116
139
|
const timeoutMs = Number(input.node?.timeout_ms ?? 30000);
|
|
117
140
|
const state = await page.readPositions();
|
|
@@ -147,6 +170,7 @@ export async function closePositions(input) {
|
|
|
147
170
|
}
|
|
148
171
|
|
|
149
172
|
export async function closeOrders(input) {
|
|
173
|
+
requireExplicitSelection(input);
|
|
150
174
|
return withExtensionPage(input, async (page) => {
|
|
151
175
|
const timeoutMs = Number(input.node?.timeout_ms ?? 30000);
|
|
152
176
|
const before = await readOpenOrders(page);
|
|
@@ -370,9 +394,10 @@ async function waitForPositionPresent(page, symbol, timeoutMs) {
|
|
|
370
394
|
}
|
|
371
395
|
|
|
372
396
|
export async function placeOrder(input) {
|
|
397
|
+
requirePlaceOrderInputs(input);
|
|
373
398
|
const symbol = marketSymbol(input);
|
|
374
|
-
const side = String(input.node
|
|
375
|
-
const amount = String(input.node
|
|
399
|
+
const side = String(input.node.side).toLowerCase();
|
|
400
|
+
const amount = String(input.node.amount ?? input.node.notional);
|
|
376
401
|
const leverage = Number(input.node?.leverage ?? 3);
|
|
377
402
|
return withExtensionPage(input, async (page) => {
|
|
378
403
|
await page.navigateHash(`#/perps/market/${encodeURIComponent(symbol)}`);
|
|
@@ -427,7 +452,9 @@ export async function placeOrder(input) {
|
|
|
427
452
|
}
|
|
428
453
|
|
|
429
454
|
export async function ensurePositions(input) {
|
|
430
|
-
|
|
455
|
+
if (input.node?.state == null) throw new Error('metamask.perps.ensure_positions requires state=open or state=none.');
|
|
456
|
+
requireExplicitSelection(input);
|
|
457
|
+
const state = String(input.node.state).toLowerCase();
|
|
431
458
|
if (state === 'none' || state === 'closed' || state === 'absent') {
|
|
432
459
|
const close = await closePositions(input);
|
|
433
460
|
return { ...(await assertPositions(input, false)), close };
|
|
@@ -445,7 +472,9 @@ export async function ensurePositions(input) {
|
|
|
445
472
|
}
|
|
446
473
|
|
|
447
474
|
export async function ensureOrders(input) {
|
|
448
|
-
|
|
475
|
+
if (input.node?.state == null) throw new Error('metamask.perps.ensure_orders requires state=open or state=none.');
|
|
476
|
+
requireExplicitSelection(input);
|
|
477
|
+
const state = String(input.node.state).toLowerCase();
|
|
449
478
|
if (state === 'none' || state === 'closed' || state === 'absent') {
|
|
450
479
|
const close = await closeOrders(input);
|
|
451
480
|
return { ...(await assertOrders(input, false)), close };
|
|
@@ -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}`);
|
|
@@ -34,20 +34,39 @@ function configuredSymbols(input, items) {
|
|
|
34
34
|
const explicit = input.node?.markets ?? input.node?.symbols ?? selector.markets ?? selector.symbols;
|
|
35
35
|
if (Array.isArray(explicit) && explicit.length > 0) return uniqueSymbols(explicit.map(normalizeMarketSymbol));
|
|
36
36
|
if (typeof explicit === 'string' && explicit.length > 0) return uniqueSymbols(explicit.split(',').map((part) => normalizeMarketSymbol(part.trim())).filter(Boolean));
|
|
37
|
-
|
|
37
|
+
const single = input.node?.market ?? input.node?.symbol ?? selector.market ?? selector.symbol;
|
|
38
|
+
return single ? [normalizeMarketSymbol(single)] : [];
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
function selectedItems(input, items) {
|
|
41
|
-
const
|
|
41
|
+
export function selectedItems(input, items) {
|
|
42
|
+
const requested = configuredSymbols(input, items);
|
|
43
|
+
const symbols = requested.length > 0 ? new Set(requested) : null;
|
|
42
44
|
const selector = input.node?.selector && typeof input.node.selector === 'object' ? input.node.selector : {};
|
|
43
45
|
const side = input.node?.side ?? selector.side;
|
|
44
46
|
return items.filter((item) => {
|
|
45
|
-
if (!symbols.has(symbolForItem(item))) return false;
|
|
47
|
+
if (symbols && !symbols.has(symbolForItem(item))) return false;
|
|
46
48
|
if (side && sideForItem(item) && sideForItem(item) !== String(side).toLowerCase()) return false;
|
|
47
49
|
return true;
|
|
48
50
|
});
|
|
49
51
|
}
|
|
50
52
|
|
|
53
|
+
function requireExplicitSelection(input) {
|
|
54
|
+
const node = input.node ?? {};
|
|
55
|
+
const selector = node.selector && typeof node.selector === 'object' ? node.selector : {};
|
|
56
|
+
const values = [node.market, node.symbol, node.markets, node.symbols, selector.market, selector.symbol, selector.markets, selector.symbols];
|
|
57
|
+
const selected = values.some((value) => Array.isArray(value) ? value.length > 0 : value != null && String(value).trim() !== '');
|
|
58
|
+
if (!selected && node.mode !== 'all' && selector.mode !== 'all') {
|
|
59
|
+
throw new Error(`${input.action} requires market=<symbol> or mode=all.`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function requirePlaceOrderInputs(input) {
|
|
64
|
+
const node = input.node ?? {};
|
|
65
|
+
if (node.market == null && node.symbol == null) throw new Error(`${input.action} requires market=<symbol>.`);
|
|
66
|
+
if (node.side == null) throw new Error(`${input.action} requires side=long or side=short.`);
|
|
67
|
+
if (node.amount == null && node.notional == null) throw new Error(`${input.action} requires amount=<usd> or notional=<usd>.`);
|
|
68
|
+
}
|
|
69
|
+
|
|
51
70
|
function uniqueSymbols(symbols) {
|
|
52
71
|
return Array.from(new Set(symbols.filter(Boolean)));
|
|
53
72
|
}
|
|
@@ -250,20 +269,22 @@ export async function readOrders(input) {
|
|
|
250
269
|
}
|
|
251
270
|
|
|
252
271
|
export async function assertPositions(input, expectedOpen) {
|
|
272
|
+
requireExplicitSelection(input);
|
|
253
273
|
const positions = await readPositions(input);
|
|
254
274
|
const matching = selectedItems(input, positions);
|
|
255
275
|
const hasPosition = matching.length > 0;
|
|
256
|
-
if (expectedOpen && !hasPosition) throw new Error(
|
|
257
|
-
if (!expectedOpen && hasPosition) throw new Error(`Expected no
|
|
276
|
+
if (expectedOpen && !hasPosition) throw new Error('Expected at least one matching open Perps position, but found none.');
|
|
277
|
+
if (!expectedOpen && hasPosition) throw new Error(`Expected no matching open Perps positions, but found ${matching.length}.`);
|
|
258
278
|
return { action: input.action, expectedOpen, matchingCount: matching.length, positions: matching.map(redactPosition), source: 'mobile-perps-controller' };
|
|
259
279
|
}
|
|
260
280
|
|
|
261
281
|
export async function assertOrders(input, expectedOpen) {
|
|
282
|
+
requireExplicitSelection(input);
|
|
262
283
|
const orders = await readOpenOrders(input);
|
|
263
284
|
const matching = selectedItems(input, orders);
|
|
264
285
|
const hasOrders = matching.length > 0;
|
|
265
|
-
if (expectedOpen && !hasOrders) throw new Error(
|
|
266
|
-
if (!expectedOpen && hasOrders) throw new Error(`Expected no
|
|
286
|
+
if (expectedOpen && !hasOrders) throw new Error('Expected at least one matching open Perps order, but found none.');
|
|
287
|
+
if (!expectedOpen && hasOrders) throw new Error(`Expected no matching open Perps orders, but found ${matching.length}.`);
|
|
267
288
|
return { action: input.action, expectedOpen, matchingCount: matching.length, orders: matching.map(redactOrder), source: 'mobile-perps-controller-open-orders' };
|
|
268
289
|
}
|
|
269
290
|
|
|
@@ -315,6 +336,7 @@ async function closePositionItem(input, position) {
|
|
|
315
336
|
}
|
|
316
337
|
|
|
317
338
|
export async function closePositions(input) {
|
|
339
|
+
requireExplicitSelection(input);
|
|
318
340
|
const before = await readPositions(input);
|
|
319
341
|
const matching = selectedItems(input, before);
|
|
320
342
|
const symbols = uniqueSymbols(matching.map(symbolForItem));
|
|
@@ -336,6 +358,7 @@ export async function closePositions(input) {
|
|
|
336
358
|
}
|
|
337
359
|
|
|
338
360
|
export async function closeOrders(input) {
|
|
361
|
+
requireExplicitSelection(input);
|
|
339
362
|
const before = await readOpenOrders(input);
|
|
340
363
|
const matching = selectedItems(input, before);
|
|
341
364
|
const symbols = uniqueSymbols(matching.map(symbolForItem));
|
|
@@ -380,9 +403,10 @@ export async function closeOrders(input) {
|
|
|
380
403
|
}
|
|
381
404
|
|
|
382
405
|
export async function placeOrder(input) {
|
|
406
|
+
requirePlaceOrderInputs(input);
|
|
383
407
|
const symbol = marketSymbol(input);
|
|
384
|
-
const side = String(input.node
|
|
385
|
-
const amount = String(input.node
|
|
408
|
+
const side = String(input.node.side).toLowerCase();
|
|
409
|
+
const amount = String(input.node.amount ?? input.node.notional);
|
|
386
410
|
const size = String(input.node?.size ?? '0.0001');
|
|
387
411
|
const leverage = Number(input.node?.leverage ?? 3);
|
|
388
412
|
const maxSlippageBps = Number(input.node?.max_slippage_bps ?? input.node?.maxSlippageBps ?? 300);
|
|
@@ -401,7 +425,9 @@ export async function placeOrder(input) {
|
|
|
401
425
|
}
|
|
402
426
|
|
|
403
427
|
export async function ensurePositions(input) {
|
|
404
|
-
|
|
428
|
+
if (input.node?.state == null) throw new Error('metamask.perps.ensure_positions requires state=open or state=none.');
|
|
429
|
+
requireExplicitSelection(input);
|
|
430
|
+
const state = String(input.node.state).toLowerCase();
|
|
405
431
|
if (state === 'none' || state === 'closed' || state === 'absent') {
|
|
406
432
|
const close = await closePositions(input);
|
|
407
433
|
return { ...(await assertPositions(input, false)), close };
|
|
@@ -415,7 +441,9 @@ export async function ensurePositions(input) {
|
|
|
415
441
|
}
|
|
416
442
|
|
|
417
443
|
export async function ensureOrders(input) {
|
|
418
|
-
|
|
444
|
+
if (input.node?.state == null) throw new Error('metamask.perps.ensure_orders requires state=open or state=none.');
|
|
445
|
+
requireExplicitSelection(input);
|
|
446
|
+
const state = String(input.node.state).toLowerCase();
|
|
419
447
|
if (state === 'none' || state === 'closed' || state === 'absent') {
|
|
420
448
|
const close = await closeOrders(input);
|
|
421
449
|
return { ...(await assertOrders(input, false)), close };
|
package/library/library.json
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
"schema_version": 1,
|
|
4
4
|
"name": "metamask",
|
|
5
5
|
"owner": "mm-harness-runner",
|
|
6
|
-
"description": "Canonical MetaMask
|
|
6
|
+
"description": "Canonical MetaMask library of reusable, parameterized recipes. Team and personal libraries layer on top and win resolution by default."
|
|
7
7
|
}
|