@deeeed/metamask-harness 0.21.0 → 0.23.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 +23 -0
- package/dist/adapters/extension/ensure-ready.js +44 -15
- package/dist/cli-commands.js +1 -0
- package/dist/cli.js +2 -0
- package/dist/command-contract.js +57 -0
- package/dist/commands/execution-template.js +28 -0
- package/dist/commands/run-engine.js +17 -8
- package/dist/library-provenance.js +33 -0
- package/dist/mm-harness-cli.js +35 -1
- package/dist/recipe-security.js +2 -0
- package/library/actions/core/perps/_controller.mjs +7 -0
- package/library/actions/core/perps/assert_orders.mjs +92 -0
- package/library/actions/core/perps/place_order.mjs +273 -15
- package/library/actions/core/perps/update_position_tpsl.mjs +185 -0
- package/library/actions/extension/ui/navigate.mjs +16 -6
- package/library/manifests/core.action-manifest.json +324 -5
- package/package.json +3 -3
- package/scripts/completions.sh +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.23.0 - 2026-07-28
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Core Perps actions support trigger orders, attached and partial TP/SL, and position TP/SL updates.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Core advanced-order mutations verify the submitted order fields, use executable trigger-limit defaults, honor bounded evidence polling, and classify mutation capabilities correctly.
|
|
14
|
+
- Core advanced-order camelCase aliases validate consistently with their documented snake_case forms.
|
|
15
|
+
|
|
16
|
+
## 0.22.0 - 2026-07-26
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- Added `execution-template` as the thin public boundary for shared checklist discovery and materialization.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- `ui.navigate page=home` recognizes both the current bottom navigation and the selected legacy Tokens tab.
|
|
25
|
+
- Extension readiness recovers deterministically from transient target-list failures.
|
|
26
|
+
- Team-library calls retain their source provenance.
|
|
27
|
+
|
|
5
28
|
## 0.21.0 - 2026-07-24
|
|
6
29
|
|
|
7
30
|
### Changed
|
|
@@ -36,9 +36,11 @@ async function openHome(port, extensionId) {
|
|
|
36
36
|
try {
|
|
37
37
|
let res = await fetch(endpoint, { method: "PUT", signal: AbortSignal.timeout(8e3) });
|
|
38
38
|
if (res.status === 404 || res.status === 405) res = await fetch(endpoint, { signal: AbortSignal.timeout(8e3) });
|
|
39
|
-
|
|
39
|
+
if (!res.ok) return null;
|
|
40
|
+
const value = await res.json();
|
|
41
|
+
return value && typeof value === "object" ? value : {};
|
|
40
42
|
} catch {
|
|
41
|
-
return
|
|
43
|
+
return null;
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
46
|
function homePages(targets, extensionId) {
|
|
@@ -94,7 +96,7 @@ async function ensureExtensionReady(target, options) {
|
|
|
94
96
|
continue;
|
|
95
97
|
}
|
|
96
98
|
action = "opened";
|
|
97
|
-
opened = await openHome(cdpPort, extensionId) || opened;
|
|
99
|
+
opened = Boolean(await openHome(cdpPort, extensionId)) || opened;
|
|
98
100
|
await sleep(1500);
|
|
99
101
|
} else {
|
|
100
102
|
action = "pruned";
|
|
@@ -149,20 +151,47 @@ async function ensureExtensionReady(target, options) {
|
|
|
149
151
|
if (action === "pruned" && (health.status !== "PASS" || after !== 1)) {
|
|
150
152
|
const listing = await jsonList(cdpPort);
|
|
151
153
|
if (listing.ok) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
await sleep(500);
|
|
158
|
-
opened = await openHome(cdpPort, extensionId) || opened;
|
|
154
|
+
const existingHomeIds = new Set(
|
|
155
|
+
homePages(listing.targets, extensionId).map((home) => String(home.id))
|
|
156
|
+
);
|
|
157
|
+
const replacement = await openHome(cdpPort, extensionId);
|
|
158
|
+
opened = Boolean(replacement) || opened;
|
|
159
159
|
await sleep(1500);
|
|
160
|
-
const
|
|
161
|
-
if (!
|
|
162
|
-
return base({
|
|
160
|
+
const replacementListing = await jsonList(cdpPort);
|
|
161
|
+
if (!replacementListing.ok) {
|
|
162
|
+
return base({
|
|
163
|
+
extensionId,
|
|
164
|
+
opened,
|
|
165
|
+
action,
|
|
166
|
+
homeTabs: { before, closed, after },
|
|
167
|
+
reasonCode: "cdp-unreachable"
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
const replacementHomes = homePages(replacementListing.targets, extensionId);
|
|
171
|
+
const replacementId = replacement?.id && replacementHomes.some((home) => home.id === replacement.id) ? String(replacement.id) : replacementHomes.find((home) => !existingHomeIds.has(String(home.id)))?.id;
|
|
172
|
+
if (replacementId) {
|
|
173
|
+
action = "reopened";
|
|
174
|
+
for (const home of replacementHomes) {
|
|
175
|
+
if (String(home.id) === replacementId) continue;
|
|
176
|
+
await closeTab(cdpPort, String(home.id));
|
|
177
|
+
closed += 1;
|
|
178
|
+
}
|
|
179
|
+
await sleep(500);
|
|
180
|
+
const relisted = await jsonList(cdpPort);
|
|
181
|
+
if (!relisted.ok) {
|
|
182
|
+
return base({
|
|
183
|
+
extensionId,
|
|
184
|
+
opened,
|
|
185
|
+
action,
|
|
186
|
+
homeTabs: { before, closed, after },
|
|
187
|
+
reasonCode: "cdp-unreachable"
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
after = homePages(relisted.targets, extensionId).length;
|
|
191
|
+
health = await checkHealth();
|
|
192
|
+
} else {
|
|
193
|
+
after = replacementHomes.length;
|
|
163
194
|
}
|
|
164
|
-
after = homePages(relisted.targets, extensionId).length;
|
|
165
|
-
health = await checkHealth();
|
|
166
195
|
}
|
|
167
196
|
}
|
|
168
197
|
let slotTitle;
|
package/dist/cli-commands.js
CHANGED
|
@@ -18,6 +18,7 @@ const SPEC = {
|
|
|
18
18
|
{ name: "actions", desc: "List runnable recipe actions", flags: ["--json", "--categories", "--category", "--action", "--library"] },
|
|
19
19
|
{ name: "doctor", desc: "Check harness/orchestration health", flags: ["--json", "--target", "--adapter", "--runtime-dir", "--expect-live", "--print-ready", "--cdp-port", "--device"] },
|
|
20
20
|
{ name: "run", desc: "Execute a proof recipe (path or library name, e.g. run perps.smoke)", args: ["recipe.json|name"], flags: ["--list", "--device"] },
|
|
21
|
+
{ name: "execution-template", desc: "Discover and materialize shared agent checklists", args: ["list", "materialize", "lint", "new"], flags: ["--dir", "--domain-dir", "--project-worker", "--project-name", "--package-templates", "--package-id", "--flow", "--run-mode", "--platform", "--domain", "--id", "--provenance", "--include-shadowed", "--no-include-shadowed", "--title", "--force", "--json"] },
|
|
21
22
|
{ name: "recipe-quality", desc: "Build the recipe-quality artifact from compact JSON", args: ["build"], flags: ["--input", "--output", "--json"] },
|
|
22
23
|
{ name: "interactive", aliases: ["menu"], desc: "Interactive command menu" },
|
|
23
24
|
{ name: "prepare", desc: "Install harness (+ optional validate)", flags: ["--target", "--runtime-dir", "--json"] },
|
package/dist/cli.js
CHANGED
|
@@ -20,6 +20,7 @@ import { handleRecipeQuality } from "./commands/recipe-quality.js";
|
|
|
20
20
|
import { handleStatus } from "./commands/status.js";
|
|
21
21
|
import { handleCheck } from "./commands/check.js";
|
|
22
22
|
import { handleChecklist } from "./commands/checklist.js";
|
|
23
|
+
import { handleExecutionTemplate } from "./commands/execution-template.js";
|
|
23
24
|
import { handleLast } from "./commands/last.js";
|
|
24
25
|
import { parseArgs, targetPath } from "./commands/parse-args.js";
|
|
25
26
|
const COMMANDS = {
|
|
@@ -121,6 +122,7 @@ async function main(argv) {
|
|
|
121
122
|
if (command === "recipe-quality") return handleRecipeQuality(argv.slice(1));
|
|
122
123
|
if (command === "check") return handleCheck(argv.slice(1));
|
|
123
124
|
if (command === "checklist") return handleChecklist(argv.slice(1));
|
|
125
|
+
if (command === "execution-template") return handleExecutionTemplate(argv.slice(1));
|
|
124
126
|
if (command === "last") return handleLast(parseArgs(argv.slice(1), command));
|
|
125
127
|
const handler = COMMANDS[command];
|
|
126
128
|
if (!handler) throw new Error(`Unknown command: ${command}`);
|
package/dist/command-contract.js
CHANGED
|
@@ -88,6 +88,32 @@ const PUBLIC_COMMAND_CONTRACTS = {
|
|
|
88
88
|
],
|
|
89
89
|
minimumPositionals: 2
|
|
90
90
|
},
|
|
91
|
+
"execution-template": {
|
|
92
|
+
usage: "mm-harness execution-template <list|materialize|lint|new> [target] [options]",
|
|
93
|
+
options: options(HELP, JSON, {
|
|
94
|
+
"--dir": value(),
|
|
95
|
+
"--domain-dir": value(),
|
|
96
|
+
"--project-worker": value(),
|
|
97
|
+
"--project-name": value(),
|
|
98
|
+
"--package-templates": value(),
|
|
99
|
+
"--package-id": value(),
|
|
100
|
+
"--flow": value(),
|
|
101
|
+
"--run-mode": value(["autonomous", "interactive", "validation"]),
|
|
102
|
+
"--platform": value(),
|
|
103
|
+
"--domain": value(),
|
|
104
|
+
"--id": value(),
|
|
105
|
+
"--provenance": value(),
|
|
106
|
+
"--include-shadowed": bool(),
|
|
107
|
+
"--no-include-shadowed": bool(),
|
|
108
|
+
"--title": value(),
|
|
109
|
+
"--force": bool()
|
|
110
|
+
}),
|
|
111
|
+
positionals: [
|
|
112
|
+
{ label: "action", choices: ["list", "materialize", "lint", "new"] }
|
|
113
|
+
],
|
|
114
|
+
minimumPositionals: 1,
|
|
115
|
+
variadic: { label: "target", validate: (value2) => value2.length > 0 }
|
|
116
|
+
},
|
|
91
117
|
actions: {
|
|
92
118
|
options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, {
|
|
93
119
|
"--action": value(),
|
|
@@ -360,6 +386,37 @@ function validatePublicInvocation(argv, examples = {}) {
|
|
|
360
386
|
);
|
|
361
387
|
}
|
|
362
388
|
}
|
|
389
|
+
if (name === "execution-template") {
|
|
390
|
+
const action = positionals[0];
|
|
391
|
+
const validAction = ["list", "materialize", "lint", "new"].includes(action ?? "");
|
|
392
|
+
const targetCount = Math.max(0, positionals.length - 1);
|
|
393
|
+
if (validAction && action === "list" && targetCount > 0) {
|
|
394
|
+
return usageFailure(
|
|
395
|
+
"CLI_EXCESS_POSITIONAL",
|
|
396
|
+
name,
|
|
397
|
+
`execution-template list does not accept positional '${positionals[1]}'.`,
|
|
398
|
+
contract,
|
|
399
|
+
examples
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
if (validAction && (action === "materialize" || action === "lint" || action === "new") && targetCount === 0) {
|
|
403
|
+
return missingPositionalFailure(
|
|
404
|
+
name,
|
|
405
|
+
`execution-template ${action} requires <target>.`,
|
|
406
|
+
contract,
|
|
407
|
+
examples
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
if (validAction && targetCount > 1) {
|
|
411
|
+
return usageFailure(
|
|
412
|
+
"CLI_EXCESS_POSITIONAL",
|
|
413
|
+
name,
|
|
414
|
+
`unexpected positional '${positionals[2]}'; execution-template ${action} accepts one <target>.`,
|
|
415
|
+
contract,
|
|
416
|
+
examples
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
363
420
|
if (contract.leadingPositionals && !contract.requiredUnless?.some((option) => seenOptions.has(option)) && tokens.slice(0, contract.leadingPositionals).some((argument) => !argument || argument.startsWith("-"))) {
|
|
364
421
|
const missing = contract.positionals?.[0]?.label ?? "argument";
|
|
365
422
|
return missingPositionalFailure(name, `${name} requires <${missing}> first.`, contract, examples);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
const require2 = createRequire(import.meta.url);
|
|
4
|
+
async function handleExecutionTemplate(argv) {
|
|
5
|
+
let entrypoint;
|
|
6
|
+
try {
|
|
7
|
+
entrypoint = require2.resolve(
|
|
8
|
+
"@farmslot/agent-runtime/scripts/execution-template-cli.mjs"
|
|
9
|
+
);
|
|
10
|
+
} catch (error) {
|
|
11
|
+
console.error(
|
|
12
|
+
`mm-harness execution-template: installed @farmslot/agent-runtime does not expose the catalog command: ${error instanceof Error ? error.message : String(error)}`
|
|
13
|
+
);
|
|
14
|
+
return 1;
|
|
15
|
+
}
|
|
16
|
+
const result = spawnSync(process.execPath, [entrypoint, ...argv], {
|
|
17
|
+
env: process.env,
|
|
18
|
+
stdio: "inherit"
|
|
19
|
+
});
|
|
20
|
+
if (result.error) {
|
|
21
|
+
console.error(`mm-harness execution-template: ${result.error.message}`);
|
|
22
|
+
return 1;
|
|
23
|
+
}
|
|
24
|
+
return result.status ?? 1;
|
|
25
|
+
}
|
|
26
|
+
export {
|
|
27
|
+
handleExecutionTemplate
|
|
28
|
+
};
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import { captureHelperSupportsRecordSessionSnapshots } from "../recording-target.js";
|
|
22
22
|
import { startRecipeRecording, stopRecipeRecording } from "../run-recording.js";
|
|
23
23
|
import { validateMetaMaskActionInputs } from "../metamask-action-validation.js";
|
|
24
|
+
import { gitLibraryProvenance } from "../library-provenance.js";
|
|
24
25
|
import {
|
|
25
26
|
beginRunDiagnostics,
|
|
26
27
|
finishRunDiagnostics
|
|
@@ -655,14 +656,22 @@ async function resolveMetaMaskLibrarySources(libraryEntry, recipePath) {
|
|
|
655
656
|
if (!sources.some((source) => path.resolve(source.root) === canonicalRoot)) {
|
|
656
657
|
sources.push({ name: "metamask", root: canonicalRoot });
|
|
657
658
|
}
|
|
658
|
-
return
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
659
|
+
return Promise.all(
|
|
660
|
+
sources.map(async (source) => {
|
|
661
|
+
const isBundled = source.name === "metamask";
|
|
662
|
+
const detected = isBundled ? {} : await gitLibraryProvenance(source.root);
|
|
663
|
+
return {
|
|
664
|
+
...source,
|
|
665
|
+
provenance: {
|
|
666
|
+
kind: isBundled ? "bundled" : "library",
|
|
667
|
+
trust: isBundled ? "trusted" : "unknown",
|
|
668
|
+
name: source.name ?? path.basename(source.root),
|
|
669
|
+
...detected,
|
|
670
|
+
...source.provenance
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
})
|
|
674
|
+
);
|
|
666
675
|
}
|
|
667
676
|
function synthesizeOneNodeRecipe(action, args) {
|
|
668
677
|
return {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
async function gitLibraryProvenance(root) {
|
|
5
|
+
try {
|
|
6
|
+
const { stdout: tracked } = await execFileAsync("git", ["-C", root, "ls-files", "--", "."], {
|
|
7
|
+
encoding: "utf8",
|
|
8
|
+
timeout: 5e3
|
|
9
|
+
});
|
|
10
|
+
if (!tracked.trim()) return {};
|
|
11
|
+
const [{ stdout: revision }, { stdout: status }] = await Promise.all([
|
|
12
|
+
execFileAsync("git", ["-C", root, "rev-parse", "HEAD"], {
|
|
13
|
+
encoding: "utf8",
|
|
14
|
+
timeout: 5e3
|
|
15
|
+
}),
|
|
16
|
+
execFileAsync(
|
|
17
|
+
"git",
|
|
18
|
+
["-C", root, "status", "--porcelain=v1", "--untracked-files=normal", "--", "."],
|
|
19
|
+
{
|
|
20
|
+
encoding: "utf8",
|
|
21
|
+
timeout: 5e3
|
|
22
|
+
}
|
|
23
|
+
)
|
|
24
|
+
]);
|
|
25
|
+
const trimmedRevision = revision.trim();
|
|
26
|
+
return trimmedRevision ? { revision: trimmedRevision, dirty: status.trim().length > 0 } : {};
|
|
27
|
+
} catch {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
gitLibraryProvenance
|
|
33
|
+
};
|
package/dist/mm-harness-cli.js
CHANGED
|
@@ -77,6 +77,40 @@ Example:
|
|
|
77
77
|
mm-harness checklist mark temp/tasks/recipe-cook/<task> complete --mark-last
|
|
78
78
|
mm-harness checklist closeout temp/tasks/recipe-cook/<task> --share`
|
|
79
79
|
},
|
|
80
|
+
{
|
|
81
|
+
name: "execution-template",
|
|
82
|
+
summary: "Discover, validate, and materialize shared agent checklists.",
|
|
83
|
+
example: "mm-harness execution-template list --package-templates <path> --json",
|
|
84
|
+
helpText: `mm-harness execution-template <list|materialize|lint|new> [options]
|
|
85
|
+
|
|
86
|
+
Shared Markdown checklist catalog used by direct and orchestrated workflows.
|
|
87
|
+
Sources use the flow-tree shape <root>/<flow>/<variant>.md.
|
|
88
|
+
|
|
89
|
+
list List compatible templates and portable source provenance.
|
|
90
|
+
materialize Select one template, copy its immutable snapshot, and optionally
|
|
91
|
+
write a provenance record.
|
|
92
|
+
lint Validate one template file or flow-tree directory.
|
|
93
|
+
new Create a minimal starter template.
|
|
94
|
+
|
|
95
|
+
Common source/filter options:
|
|
96
|
+
--dir <path> Custom flow-tree source (repeatable)
|
|
97
|
+
--domain-dir <domain=path> Domain-scoped team source (repeatable)
|
|
98
|
+
--package-templates <path> Canonical package flow-tree root
|
|
99
|
+
--package-id <id> Source label for the package root
|
|
100
|
+
--project-worker <path> Project template directory
|
|
101
|
+
--project-name <name> Source label for project templates
|
|
102
|
+
--flow <flow> Exact flow
|
|
103
|
+
--platform <platform> Exact platform
|
|
104
|
+
--run-mode <mode> autonomous|interactive|validation
|
|
105
|
+
--domain <domain> Exact domain
|
|
106
|
+
--id <id> Exact template id
|
|
107
|
+
--json Machine-readable output
|
|
108
|
+
|
|
109
|
+
Materialize:
|
|
110
|
+
mm-harness execution-template materialize <output> --flow fix-bug \\
|
|
111
|
+
--platform mobile --run-mode autonomous --id fix-bug/autonomous.mobile \\
|
|
112
|
+
--package-templates <path> --provenance <path> --json`
|
|
113
|
+
},
|
|
80
114
|
{
|
|
81
115
|
name: "actions",
|
|
82
116
|
summary: "Discover typed single operations and their fields.",
|
|
@@ -552,7 +586,7 @@ const HELP_GROUPS = [
|
|
|
552
586
|
{
|
|
553
587
|
title: "DISCOVER",
|
|
554
588
|
blurb: "discover atomic actions and reusable recipes (--json is the agent-primary form)",
|
|
555
|
-
commands: ["actions", "call"]
|
|
589
|
+
commands: ["actions", "call", "execution-template"]
|
|
556
590
|
},
|
|
557
591
|
{
|
|
558
592
|
title: "PROVE",
|
package/dist/recipe-security.js
CHANGED
|
@@ -18,6 +18,7 @@ const APP_MUTATION_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
18
18
|
"metamask.perps.close_positions",
|
|
19
19
|
"metamask.perps.close_orders",
|
|
20
20
|
"metamask.perps.place_order",
|
|
21
|
+
"metamask.perps.update_position_tpsl",
|
|
21
22
|
"metamask.perps.ensure_positions",
|
|
22
23
|
"metamask.perps.ensure_orders",
|
|
23
24
|
"metamask.perps.start_state",
|
|
@@ -27,6 +28,7 @@ const EXTERNAL_MUTATION_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
27
28
|
"metamask.perps.close_positions",
|
|
28
29
|
"metamask.perps.close_orders",
|
|
29
30
|
"metamask.perps.place_order",
|
|
31
|
+
"metamask.perps.update_position_tpsl",
|
|
30
32
|
"metamask.perps.ensure_positions",
|
|
31
33
|
"metamask.perps.ensure_orders",
|
|
32
34
|
"metamask.perps.start_state",
|
|
@@ -625,6 +625,13 @@ export function redactOrder(order) {
|
|
|
625
625
|
size: order.size ?? order.sz ?? order.szi ?? null,
|
|
626
626
|
price: order.price ?? order.limitPx ?? order.px ?? null,
|
|
627
627
|
type: order.orderType ?? order.type ?? null,
|
|
628
|
+
// Trigger data, so evidence shows what a stop / take-profit placement
|
|
629
|
+
// actually round-tripped from the exchange.
|
|
630
|
+
triggerOrderType: order.triggerOrderType ?? null,
|
|
631
|
+
triggerPrice: order.triggerPrice ?? order.triggerPx ?? null,
|
|
632
|
+
detailedOrderType: order.detailedOrderType ?? null,
|
|
633
|
+
isTrigger: order.isTrigger ?? null,
|
|
634
|
+
reduceOnly: order.reduceOnly ?? null,
|
|
628
635
|
};
|
|
629
636
|
}
|
|
630
637
|
|
|
@@ -11,6 +11,13 @@ import {
|
|
|
11
11
|
// over the controller's standalone getOpenOrders path (no signer / provider init
|
|
12
12
|
// needed) — throws on mismatch so the recipe fails loudly. Mirrors
|
|
13
13
|
// assert_positions.mjs.
|
|
14
|
+
//
|
|
15
|
+
// Optional expect_* fields additionally assert the TRIGGER DATA the exchange
|
|
16
|
+
// round-tripped for every matching order: expect_trigger_order_type (the
|
|
17
|
+
// normalized placement type, e.g. stop_market), expect_trigger_price,
|
|
18
|
+
// expect_execution (market | limit once triggered), expect_reduce_only, and
|
|
19
|
+
// expect_size (proves a partial TP/SL quantity). They use the controller's own
|
|
20
|
+
// field names (triggerOrderType, triggerPrice, orderType, reduceOnly, size).
|
|
14
21
|
|
|
15
22
|
export function expectedOpen(input) {
|
|
16
23
|
if (input.node?.state == null) throw new Error('metamask.perps.assert_orders requires state=open or state=none.');
|
|
@@ -20,6 +27,79 @@ export function expectedOpen(input) {
|
|
|
20
27
|
throw new Error(`metamask.perps.assert_orders received unsupported state: ${state}`);
|
|
21
28
|
}
|
|
22
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Collect the optional trigger-data expectations from the node.
|
|
32
|
+
*
|
|
33
|
+
* @param input - Adapter input.
|
|
34
|
+
* @returns The expectations the node set, keyed by controller field name.
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* Whether the node narrowed matching to trigger orders only.
|
|
38
|
+
*
|
|
39
|
+
* @param input - Adapter input.
|
|
40
|
+
* @returns True when only trigger orders should be considered.
|
|
41
|
+
*/
|
|
42
|
+
export function onlyTriggerOrders(input) {
|
|
43
|
+
const value = input.node?.only_trigger_orders ?? input.node?.onlyTriggerOrders;
|
|
44
|
+
return value === true || String(value).toLowerCase() === 'true';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function expectedTriggerData(input) {
|
|
48
|
+
const node = input.node ?? {};
|
|
49
|
+
const expectations = {
|
|
50
|
+
triggerOrderType:
|
|
51
|
+
node.expect_trigger_order_type ?? node.expectTriggerOrderType,
|
|
52
|
+
triggerPrice: node.expect_trigger_price ?? node.expectTriggerPrice,
|
|
53
|
+
orderType: node.expect_execution ?? node.expectExecution,
|
|
54
|
+
reduceOnly: node.expect_reduce_only ?? node.expectReduceOnly,
|
|
55
|
+
size: node.expect_size ?? node.expectSize,
|
|
56
|
+
};
|
|
57
|
+
return Object.fromEntries(
|
|
58
|
+
Object.entries(expectations).filter(
|
|
59
|
+
([, value]) => value !== undefined && value !== null,
|
|
60
|
+
),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Assert every matching order carries the expected trigger data.
|
|
66
|
+
*
|
|
67
|
+
* @param orders - Matching open orders.
|
|
68
|
+
* @param expectations - Expectations from `expectedTriggerData`.
|
|
69
|
+
*/
|
|
70
|
+
export function assertTriggerData(orders, expectations) {
|
|
71
|
+
const fields = Object.keys(expectations);
|
|
72
|
+
if (fields.length === 0) return;
|
|
73
|
+
|
|
74
|
+
for (const order of orders) {
|
|
75
|
+
for (const field of fields) {
|
|
76
|
+
const expected = expectations[field];
|
|
77
|
+
const actual = order[field];
|
|
78
|
+
const expectedNumber = Number(expected);
|
|
79
|
+
const actualNumber = Number(actual);
|
|
80
|
+
const numeric =
|
|
81
|
+
typeof expected !== 'boolean' &&
|
|
82
|
+
expected !== '' &&
|
|
83
|
+
actual !== null &&
|
|
84
|
+
actual !== undefined &&
|
|
85
|
+
Number.isFinite(expectedNumber) &&
|
|
86
|
+
Number.isFinite(actualNumber);
|
|
87
|
+
const matches =
|
|
88
|
+
typeof expected === 'boolean'
|
|
89
|
+
? Boolean(actual) === expected
|
|
90
|
+
: // Prices and sizes round-trip with exchange formatting
|
|
91
|
+
// ('44000' -> '44000.0'), so compare them numerically.
|
|
92
|
+
(numeric && expectedNumber === actualNumber) ||
|
|
93
|
+
String(actual) === String(expected);
|
|
94
|
+
if (!matches) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`Open Perps order ${order.orderId ?? '?'} (${order.symbol ?? '?'}) has ${field}=${JSON.stringify(actual)}, expected ${JSON.stringify(expected)}.`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
23
103
|
export async function assertOrders(input, expectOpen = expectedOpen(input)) {
|
|
24
104
|
requireExplicitSelection(input);
|
|
25
105
|
const { controller, accountAddress, network } = await getCoreController(input);
|
|
@@ -33,6 +113,11 @@ export async function assertOrders(input, expectOpen = expectedOpen(input)) {
|
|
|
33
113
|
userAddress: accountAddress,
|
|
34
114
|
});
|
|
35
115
|
matching = selectedItems(input, orders);
|
|
116
|
+
if (onlyTriggerOrders(input)) {
|
|
117
|
+
// Narrow to the trigger orders on the market so expectations are not
|
|
118
|
+
// applied to an unrelated parent order resting alongside them.
|
|
119
|
+
matching = matching.filter((order) => order.isTrigger === true);
|
|
120
|
+
}
|
|
36
121
|
if (expectOpen ? matching.length > 0 : matching.length === 0) break;
|
|
37
122
|
if (Date.now() >= deadline) break;
|
|
38
123
|
await new Promise((resolve) =>
|
|
@@ -44,6 +129,11 @@ export async function assertOrders(input, expectOpen = expectedOpen(input)) {
|
|
|
44
129
|
if (expectOpen && !hasOrder) {
|
|
45
130
|
throw new Error('Expected at least one matching open Perps order, but found none.');
|
|
46
131
|
}
|
|
132
|
+
|
|
133
|
+
const triggerExpectations = expectedTriggerData(input);
|
|
134
|
+
if (expectOpen) {
|
|
135
|
+
assertTriggerData(matching, triggerExpectations);
|
|
136
|
+
}
|
|
47
137
|
if (!expectOpen && hasOrder) {
|
|
48
138
|
throw new Error(`Expected no matching open Perps orders, but found ${matching.length}.`);
|
|
49
139
|
}
|
|
@@ -54,6 +144,8 @@ export async function assertOrders(input, expectOpen = expectedOpen(input)) {
|
|
|
54
144
|
network,
|
|
55
145
|
account: accountAddress,
|
|
56
146
|
expectedOpen: expectOpen,
|
|
147
|
+
expectedTrigger:
|
|
148
|
+
Object.keys(triggerExpectations).length === 0 ? null : triggerExpectations,
|
|
57
149
|
matchingCount: matching.length,
|
|
58
150
|
orders: matching.map(redactOrder),
|
|
59
151
|
proofPath: 'perps-controller-getOpenOrders',
|