@deeeed/metamask-harness 0.19.0 → 0.20.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 +31 -0
- package/adapters/extension/ensure-browser.sh +6 -4
- package/adapters/extension/launch-browser.cjs +6 -1
- package/adapters/extension/lib/chrome-args.cjs +11 -1
- package/adapters/extension/start-watch.sh +1 -1
- package/adapters/extension/verify.sh +17 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +33 -0
- package/adapters/mobile/open-device.sh +12 -1
- package/adapters/mobile/wait-for-bridge.sh +1 -1
- package/dist/adapters/extension/ensure-ready.js +7 -2
- package/dist/adapters/extension/runtime-decision.js +4 -6
- package/dist/adapters/extension/runtime.js +114 -17
- package/dist/adapters/mobile/prepare.js +40 -6
- package/dist/adapters.js +6 -1
- package/dist/command-contract.js +58 -5
- package/dist/commands/call.js +6 -3
- package/dist/commands/check.js +9 -2
- package/dist/commands/checklist.js +117 -14
- package/dist/commands/launch/extension.js +8 -3
- package/dist/commands/launch/index.js +38 -25
- package/dist/commands/launch/mobile.js +2 -2
- package/dist/commands/manifest.js +72 -5
- package/dist/commands/run-engine.js +7 -2
- package/dist/heal-bounds.js +1 -1
- package/dist/live-adapter-contract.js +15 -2
- package/dist/metamask-action-validation.js +45 -0
- package/dist/mm-harness-cli.js +7 -4
- package/docs/CONTRIBUTING.md +8 -0
- package/docs/RECIPES.md +15 -0
- package/library/actions/core/perps/assert_orders.mjs +16 -5
- package/library/actions/core/perps/assert_positions.mjs +16 -5
- package/library/actions/extension/perps/perps.mjs +111 -19
- package/library/actions/extension/platform/cdp.mjs +8 -3
- package/library/actions/extension/ui/navigate.mjs +239 -16
- package/library/actions/mobile/perps/perps.mjs +108 -10
- package/library/actions/mobile/ui/navigate.mjs +1 -1
- package/library/manifests/core.action-manifest.json +100 -11
- package/library/manifests/extension.action-manifest.json +80 -12
- package/library/manifests/mobile.action-manifest.json +85 -10
- package/library/recipes/perps/lifecycle.recipe.json +3 -9
- package/library/recipes/runner/action-validation.extension.recipe.json +5 -4
- package/package.json +5 -4
- package/scripts/completions.sh +2 -2
|
@@ -312,6 +312,7 @@ async function runLiveAdapterScript({ platform, action, node, context, prepared
|
|
|
312
312
|
`);
|
|
313
313
|
const command = prepared ? commandForPrepared(context.projectRoot, platform) : commandFor(script, context.projectRoot, platform);
|
|
314
314
|
const platformEnv = await platformAdapterEnv(platform, context.projectRoot, tempDir);
|
|
315
|
+
const processTimeoutMs = liveAdapterProcessTimeoutMs(node);
|
|
315
316
|
const result = await runProcess(command.command, [...command.args, inputPath], {
|
|
316
317
|
cwd: context.projectRoot,
|
|
317
318
|
env: {
|
|
@@ -321,12 +322,12 @@ async function runLiveAdapterScript({ platform, action, node, context, prepared
|
|
|
321
322
|
METAMASK_RECIPE_ADAPTER_INPUT: inputPath,
|
|
322
323
|
METAMASK_RECIPE_ADAPTER_OUTPUT: outputPath
|
|
323
324
|
},
|
|
324
|
-
timeoutMs:
|
|
325
|
+
timeoutMs: processTimeoutMs,
|
|
325
326
|
...prepared ? { stdin: prepared.sourceText } : {}
|
|
326
327
|
});
|
|
327
328
|
try {
|
|
328
329
|
if (result.timedOut) {
|
|
329
|
-
throw new Error(`Live adapter ${script} timed out after ${
|
|
330
|
+
throw new Error(`Live adapter ${script} timed out after ${processTimeoutMs}ms.`);
|
|
330
331
|
}
|
|
331
332
|
if (result.exitCode !== 0) {
|
|
332
333
|
throw new Error(`Live adapter ${script} exited ${result.exitCode}: ${result.stderr || result.stdout}`);
|
|
@@ -344,6 +345,17 @@ async function runLiveAdapterScript({ platform, action, node, context, prepared
|
|
|
344
345
|
await rm(tempDir, { recursive: true, force: true });
|
|
345
346
|
}
|
|
346
347
|
}
|
|
348
|
+
function liveAdapterProcessTimeoutMs(node) {
|
|
349
|
+
if (node.live_adapter_timeout_ms != null) {
|
|
350
|
+
const timeoutMs = Number(node.live_adapter_timeout_ms);
|
|
351
|
+
return Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 6e4;
|
|
352
|
+
}
|
|
353
|
+
if (node.timeout_ms != null) {
|
|
354
|
+
const timeoutMs = Number(node.timeout_ms);
|
|
355
|
+
return Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs + 5e3 : 6e4;
|
|
356
|
+
}
|
|
357
|
+
return 6e4;
|
|
358
|
+
}
|
|
347
359
|
function commandForPrepared(projectRoot, platform) {
|
|
348
360
|
if (platform !== "core") {
|
|
349
361
|
return { command: process.execPath, args: ["--input-type=module", "-"] };
|
|
@@ -364,6 +376,7 @@ function isPathWithin(root, candidate) {
|
|
|
364
376
|
return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
365
377
|
}
|
|
366
378
|
export {
|
|
379
|
+
liveAdapterProcessTimeoutMs,
|
|
367
380
|
prepareLiveAdapterScript,
|
|
368
381
|
resolveLiveAdapter,
|
|
369
382
|
runLiveAdapterScript
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
function isRecord(value) {
|
|
2
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
function validateMetaMaskActionInputs(recipe) {
|
|
5
|
+
if (!isRecord(recipe) || !isRecord(recipe.workflow)) return [];
|
|
6
|
+
const nodes = isRecord(recipe.workflow.nodes) ? recipe.workflow.nodes : void 0;
|
|
7
|
+
if (!nodes) return [];
|
|
8
|
+
const findings = [];
|
|
9
|
+
for (const [nodeId, node] of Object.entries(nodes)) {
|
|
10
|
+
if (!isRecord(node)) continue;
|
|
11
|
+
const action = node.action;
|
|
12
|
+
const state = typeof node.state === "string" ? node.state.toLowerCase() : void 0;
|
|
13
|
+
const createsOrder = action === "metamask.perps.place_order" || (action === "metamask.perps.ensure_positions" || action === "metamask.perps.ensure_orders") && (state === "open" || state === "present");
|
|
14
|
+
if (!createsOrder) continue;
|
|
15
|
+
const requireOneOf = (names, message) => {
|
|
16
|
+
const present = names.some((name) => {
|
|
17
|
+
const value = node[name];
|
|
18
|
+
return value !== void 0 && value !== null && value !== "";
|
|
19
|
+
});
|
|
20
|
+
if (present) return;
|
|
21
|
+
findings.push({
|
|
22
|
+
severity: "error",
|
|
23
|
+
code: "recipe.missing_param",
|
|
24
|
+
path: `workflow.nodes.${nodeId}.${names[0]}`,
|
|
25
|
+
message
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
requireOneOf(
|
|
29
|
+
["market", "symbol"],
|
|
30
|
+
`${action} requires market=<symbol> (or symbol=<symbol>) before it may create an order`
|
|
31
|
+
);
|
|
32
|
+
requireOneOf(
|
|
33
|
+
["side"],
|
|
34
|
+
`${action} requires side=long or side=short before it may create an order`
|
|
35
|
+
);
|
|
36
|
+
requireOneOf(
|
|
37
|
+
["amount", "notional"],
|
|
38
|
+
`${action} requires amount=<usd> or notional=<usd> before it may create an order`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return findings;
|
|
42
|
+
}
|
|
43
|
+
export {
|
|
44
|
+
validateMetaMaskActionInputs
|
|
45
|
+
};
|
package/dist/mm-harness-cli.js
CHANGED
|
@@ -60,11 +60,13 @@ Example:
|
|
|
60
60
|
},
|
|
61
61
|
{
|
|
62
62
|
name: "checklist",
|
|
63
|
-
summary: "Mark
|
|
63
|
+
summary: "Mark checklist progress and close out scrubbed learning packages.",
|
|
64
64
|
example: "mm-harness checklist mark <task-dir> start",
|
|
65
|
-
helpText: `mm-harness checklist mark <task-dir>
|
|
65
|
+
helpText: `mm-harness checklist <mark|closeout> <task-dir> [step] [options]
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
mark delegates checklist state to @farmslot/agent-runtime. Successful complete
|
|
68
|
+
and no-change marks stage a learning package through @farmslot/handoff.
|
|
69
|
+
closeout exposes that same Handoff boundary directly; --share remains explicit.
|
|
68
70
|
The task directory must contain CHECKLIST.md and checklist-target.json.
|
|
69
71
|
|
|
70
72
|
Steps: start | 1 | 2 | ... | complete | no-change | blocked
|
|
@@ -72,7 +74,8 @@ Example:
|
|
|
72
74
|
Example:
|
|
73
75
|
mm-harness checklist mark temp/tasks/recipe-cook/<task> start
|
|
74
76
|
mm-harness checklist mark temp/tasks/recipe-cook/<task> 1
|
|
75
|
-
mm-harness checklist mark temp/tasks/recipe-cook/<task> complete --mark-last
|
|
77
|
+
mm-harness checklist mark temp/tasks/recipe-cook/<task> complete --mark-last
|
|
78
|
+
mm-harness checklist closeout temp/tasks/recipe-cook/<task> --share`
|
|
76
79
|
},
|
|
77
80
|
{
|
|
78
81
|
name: "actions",
|
package/docs/CONTRIBUTING.md
CHANGED
|
@@ -8,6 +8,8 @@ both the machine contract and the visible human result.
|
|
|
8
8
|
```text
|
|
9
9
|
bin/mm-harness
|
|
10
10
|
-> src/ typed CLI and product decisions
|
|
11
|
+
-> @farmslot/agent-runtime checklist state
|
|
12
|
+
-> @farmslot/handoff scrubbed learning capture/share
|
|
11
13
|
-> @farmslot/recipe-harness generic execution, UI transports, evidence
|
|
12
14
|
-> @farmslot/protocol schemas
|
|
13
15
|
-> adapters/ focused host/browser/device leaves
|
|
@@ -18,6 +20,8 @@ bin/mm-harness
|
|
|
18
20
|
| Layer | Owns |
|
|
19
21
|
|---|---|
|
|
20
22
|
| `@farmslot/protocol` | recipe and evidence schemas |
|
|
23
|
+
| `@farmslot/agent-runtime` | task-local checklist state and terminal contracts |
|
|
24
|
+
| `@farmslot/handoff` | learning assembly, scrubbing, validation, approval, and publication |
|
|
21
25
|
| `@farmslot/recipe-harness` | generic execution, recovery, traces, artifacts, `ui.*` |
|
|
22
26
|
| `mm-harness` | MetaMask runtime control, diagnostics, durable domain actions |
|
|
23
27
|
| skills/checklists | task workflow and proof expectations |
|
|
@@ -27,6 +31,10 @@ Generic bootstrap, trust, receipts, and recovery belong upstream. MetaMask
|
|
|
27
31
|
platform behavior and risk classification belong here. Ticket assertions stay
|
|
28
32
|
task-local.
|
|
29
33
|
|
|
34
|
+
`mm-harness checklist` is only the MetaMask-facing dispatch layer. Terminal
|
|
35
|
+
marks stage learnings through Handoff; sharing always requires a separate
|
|
36
|
+
explicit `--share` call.
|
|
37
|
+
|
|
30
38
|
## Repository map
|
|
31
39
|
|
|
32
40
|
| Path | Responsibility |
|
package/docs/RECIPES.md
CHANGED
|
@@ -67,6 +67,21 @@ Rules:
|
|
|
67
67
|
- Parameterize repeated behavior instead of multiplying names.
|
|
68
68
|
- Keep secrets out of recipes, libraries, and evidence.
|
|
69
69
|
|
|
70
|
+
Keep responsibilities narrow:
|
|
71
|
+
|
|
72
|
+
- **Parameters** expose caller choices that may vary without changing the
|
|
73
|
+
journey's meaning.
|
|
74
|
+
- **Recipes** compose actions and own journey order, safe defaults, invariants,
|
|
75
|
+
and proof.
|
|
76
|
+
- **Actions** translate one stable operation to existing UI, CDP, or controller
|
|
77
|
+
capabilities; they do not reimplement product business logic.
|
|
78
|
+
- **Product/controllers** remain the source of truth for state transitions,
|
|
79
|
+
validation, transactions, and domain behavior.
|
|
80
|
+
|
|
81
|
+
If changing a value would violate the recipe's postcondition, keep it as a
|
|
82
|
+
documented recipe invariant rather than a parameter. Add a shared action only
|
|
83
|
+
when repeated direct access has a stable cross-task contract.
|
|
84
|
+
|
|
70
85
|
The protocol is authoritative:
|
|
71
86
|
<https://farmslot.io/docs/reference/recipe-protocol-v1>.
|
|
72
87
|
|
|
@@ -23,11 +23,22 @@ export function expectedOpen(input) {
|
|
|
23
23
|
export async function assertOrders(input, expectOpen = expectedOpen(input)) {
|
|
24
24
|
requireExplicitSelection(input);
|
|
25
25
|
const { controller, accountAddress, network } = await getCoreController(input);
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
+
}
|
|
31
42
|
const hasOrder = matching.length > 0;
|
|
32
43
|
|
|
33
44
|
if (expectOpen && !hasOrder) {
|
|
@@ -22,11 +22,22 @@ export function expectedOpen(input) {
|
|
|
22
22
|
export async function assertPositions(input, expectOpen = expectedOpen(input)) {
|
|
23
23
|
requireExplicitSelection(input);
|
|
24
24
|
const { controller, accountAddress, network } = await getCoreController(input);
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
+
}
|
|
30
41
|
const hasPosition = matching.length > 0;
|
|
31
42
|
|
|
32
43
|
if (expectOpen && !hasPosition) {
|
|
@@ -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) {
|
|
@@ -57,6 +60,35 @@ function requirePlaceOrderInputs(input) {
|
|
|
57
60
|
if (node.amount == null && node.notional == null) throw new Error(`${input.action} requires amount=<usd> or notional=<usd>.`);
|
|
58
61
|
}
|
|
59
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
|
+
|
|
60
92
|
export function positionSelectionState(input, positions) {
|
|
61
93
|
return { hasMatching: selectedItems(input, positions).length > 0 };
|
|
62
94
|
}
|
|
@@ -97,9 +129,9 @@ export async function readPositions(input) {
|
|
|
97
129
|
|
|
98
130
|
export async function readOrders(input) {
|
|
99
131
|
return withExtensionPage(input, async (page) => {
|
|
100
|
-
const
|
|
101
|
-
const matching = selectedItems(input, orders);
|
|
102
|
-
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) };
|
|
103
135
|
});
|
|
104
136
|
}
|
|
105
137
|
|
|
@@ -107,11 +139,7 @@ export async function assertPositions(input, expectedOpen) {
|
|
|
107
139
|
requireExplicitSelection(input);
|
|
108
140
|
return withExtensionPage(input, async (page) => {
|
|
109
141
|
const timeoutMs = Number(input.node?.timeout_ms ?? 0);
|
|
110
|
-
const
|
|
111
|
-
if (expectedOpen && timeoutMs > 0 && requested.length === 1) {
|
|
112
|
-
await waitForPositionPresent(page, requested[0], timeoutMs);
|
|
113
|
-
}
|
|
114
|
-
const state = await page.readPositions();
|
|
142
|
+
const state = await waitForSelectedPositionState(page, input, expectedOpen, timeoutMs);
|
|
115
143
|
if (!state.available) throw new Error('stateHooks.submitRequestToBackground is unavailable; cannot assert live Perps positions.');
|
|
116
144
|
const matching = selectedItems(input, state.positions);
|
|
117
145
|
const hasPosition = matching.length > 0;
|
|
@@ -124,12 +152,13 @@ export async function assertPositions(input, expectedOpen) {
|
|
|
124
152
|
export async function assertOrders(input, expectedOpen) {
|
|
125
153
|
requireExplicitSelection(input);
|
|
126
154
|
return withExtensionPage(input, async (page) => {
|
|
127
|
-
const
|
|
128
|
-
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);
|
|
129
158
|
const hasOrders = matching.length > 0;
|
|
130
159
|
if (expectedOpen && !hasOrders) throw new Error('Expected at least one matching open Perps order, but found none.');
|
|
131
160
|
if (!expectedOpen && hasOrders) throw new Error(`Expected no matching open Perps orders, but found ${matching.length}.`);
|
|
132
|
-
return { action: input.action, expectedOpen, matchingCount: matching.length, orders: matching.map(redactOrder), source:
|
|
161
|
+
return { action: input.action, expectedOpen, matchingCount: matching.length, orders: matching.map(redactOrder), source: state.source };
|
|
133
162
|
});
|
|
134
163
|
}
|
|
135
164
|
|
|
@@ -306,6 +335,10 @@ async function readCurrentMarketPrice(page, symbol, explicitPrice) {
|
|
|
306
335
|
}
|
|
307
336
|
|
|
308
337
|
async function readOpenOrders(page) {
|
|
338
|
+
return (await readOpenOrdersState(page)).orders;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function readOpenOrdersState(page) {
|
|
309
342
|
const state = await page.evaluate(`(async () => {
|
|
310
343
|
const request = globalThis.stateHooks?.submitRequestToBackground;
|
|
311
344
|
const manager = globalThis.stateHooks?.getPerpsStreamManager?.();
|
|
@@ -326,7 +359,7 @@ async function readOpenOrders(page) {
|
|
|
326
359
|
};
|
|
327
360
|
})()`);
|
|
328
361
|
if (!state.available) throw new Error('Perps orders are unavailable from both background and stream manager cache.');
|
|
329
|
-
return state
|
|
362
|
+
return state;
|
|
330
363
|
}
|
|
331
364
|
|
|
332
365
|
async function readAccountState(page) {
|
|
@@ -393,12 +426,41 @@ async function waitForPositionPresent(page, symbol, timeoutMs) {
|
|
|
393
426
|
return lastState;
|
|
394
427
|
}
|
|
395
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
|
+
|
|
396
456
|
export async function placeOrder(input) {
|
|
397
457
|
requirePlaceOrderInputs(input);
|
|
398
458
|
const symbol = marketSymbol(input);
|
|
399
459
|
const side = String(input.node.side).toLowerCase();
|
|
400
460
|
const amount = String(input.node.amount ?? input.node.notional);
|
|
401
461
|
const leverage = Number(input.node?.leverage ?? 3);
|
|
462
|
+
const isBuy = side !== 'short';
|
|
463
|
+
const orderType = resolveOrderType(input);
|
|
402
464
|
return withExtensionPage(input, async (page) => {
|
|
403
465
|
await page.navigateHash(`#/perps/market/${encodeURIComponent(symbol)}`);
|
|
404
466
|
await page.evaluate(`(async () => {
|
|
@@ -415,6 +477,9 @@ export async function placeOrder(input) {
|
|
|
415
477
|
symbol,
|
|
416
478
|
input.node?.current_price ?? input.node?.currentPrice,
|
|
417
479
|
);
|
|
480
|
+
const limitPrice = orderType === 'limit'
|
|
481
|
+
? resolveLimitPrice(input, isBuy, resolvedPrice)
|
|
482
|
+
: null;
|
|
418
483
|
await page.navigateHash(`#/perps/trade/${encodeURIComponent(symbol)}?direction=${encodeURIComponent(side)}&mode=new`);
|
|
419
484
|
await page.waitForSelector(dataTestId('perps-order-entry-page'), { timeoutMs: 20000 });
|
|
420
485
|
let order;
|
|
@@ -426,13 +491,27 @@ export async function placeOrder(input) {
|
|
|
426
491
|
if (!Number.isFinite(currentPrice) || currentPrice <= 0) throw new Error('Unable to determine current Perps price for order placement.');
|
|
427
492
|
const usdAmount = ${JSON.stringify(amount)};
|
|
428
493
|
const leverage = ${JSON.stringify(leverage)};
|
|
429
|
-
const
|
|
494
|
+
const orderType = ${JSON.stringify(orderType)};
|
|
495
|
+
const limitPrice = ${JSON.stringify(limitPrice)};
|
|
496
|
+
const orderParams = orderType === 'limit' ? {
|
|
430
497
|
symbol: ${JSON.stringify(symbol)},
|
|
431
|
-
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)},
|
|
432
510
|
size: ((Number(usdAmount) * Number(leverage)) / currentPrice).toString(),
|
|
433
|
-
orderType
|
|
511
|
+
orderType,
|
|
434
512
|
leverage,
|
|
435
513
|
currentPrice,
|
|
514
|
+
priceAtCalculation: currentPrice,
|
|
436
515
|
usdAmount,
|
|
437
516
|
maxSlippageBps: ${JSON.stringify(Number(input.node?.max_slippage_bps ?? input.node?.maxSlippageBps ?? 300))},
|
|
438
517
|
};
|
|
@@ -446,8 +525,12 @@ export async function placeOrder(input) {
|
|
|
446
525
|
const message = error instanceof Error ? error.message : String(error);
|
|
447
526
|
throw new Error(`${message} (resolvedPrice=${resolvedPrice})`);
|
|
448
527
|
}
|
|
449
|
-
|
|
450
|
-
|
|
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' };
|
|
451
534
|
});
|
|
452
535
|
}
|
|
453
536
|
|
|
@@ -479,7 +562,16 @@ export async function ensureOrders(input) {
|
|
|
479
562
|
const close = await closeOrders(input);
|
|
480
563
|
return { ...(await assertOrders(input, false)), close };
|
|
481
564
|
}
|
|
482
|
-
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
|
+
}
|
|
483
575
|
throw new Error(`metamask.perps.ensure_orders received unsupported state: ${state}`);
|
|
484
576
|
}
|
|
485
577
|
|
|
@@ -575,10 +575,15 @@ export class ExtensionPage extends CdpWebPage {
|
|
|
575
575
|
this.port = port;
|
|
576
576
|
}
|
|
577
577
|
|
|
578
|
-
async navigateHash(hash) {
|
|
578
|
+
async navigateHash(hash, timeoutMs = 10000) {
|
|
579
579
|
const normalizedHash = String(hash || '').startsWith('#') ? hash : `#${hash || '/'}`;
|
|
580
|
-
const
|
|
581
|
-
|
|
580
|
+
const navigation = await this.evaluate(`(() => {
|
|
581
|
+
const before = location.href;
|
|
582
|
+
location.hash = ${JSON.stringify(normalizedHash)};
|
|
583
|
+
return { before, href: location.href };
|
|
584
|
+
})()`);
|
|
585
|
+
await this.waitForDocumentReady({ expectedUrl: navigation.href, timeoutMs });
|
|
586
|
+
return { ...navigation, sameDocument: true };
|
|
582
587
|
}
|
|
583
588
|
|
|
584
589
|
async readPositions() {
|