@deeeed/metamask-harness 0.50.1 → 0.50.3
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 +10 -0
- package/README.md +4 -0
- package/dist/adapters/extension/runtime-decision.js +17 -0
- package/dist/adapters/extension/runtime.js +18 -4
- package/dist/commands/help.js +10 -8
- package/dist/commands/launch/index.js +10 -0
- package/docs/QA.md +4 -4
- package/docs/RECIPES.md +21 -0
- package/library/actions/mobile/perps/assert_orders.mjs +4 -4
- package/library/actions/mobile/perps/assert_positions.mjs +4 -4
- package/library/actions/mobile/perps/close_orders.mjs +1 -2
- package/library/actions/mobile/perps/close_positions.mjs +1 -2
- package/library/manifests/mobile.action-manifest.json +0 -30
- package/package.json +2 -2
- package/site/assets/help-recipes.json +5 -0
- package/library/actions/mobile/perps/order-receipt.mjs +0 -241
- package/library/actions/mobile/perps/read_order_input.mjs +0 -33
- package/library/actions/mobile/perps/read_order_receipt.mjs +0 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.50.3 - 2026-09-09
|
|
6
|
+
|
|
7
|
+
- Recognize enabled form controls through recipe-harness 0.16.1; report Extension startup crashes without reload-based RPC probes or automatic relaunch recovery.
|
|
8
|
+
- Color human recipe help while keeping JSON plain; clarify recipe composition, Extension timing capture, acceptance criteria, and team-library updates.
|
|
9
|
+
- Make architecture topics expandable and correct cheatsheet platform, recording, and recipe examples.
|
|
10
|
+
|
|
11
|
+
## 0.50.2 - 2026-09-08
|
|
12
|
+
|
|
13
|
+
- Remove Mobile receipt actions that require unpublished app/SDK APIs; reject stale receipt-based assertion and cleanup inputs before execution.
|
|
14
|
+
|
|
5
15
|
## 0.50.1 - 2026-09-08
|
|
6
16
|
|
|
7
17
|
- Hand Extension builds to a fresh background worker before readiness, including watched edits and quick reloads, while preserving wallet profiles and native sidepanels. Refuse development copies over release artifacts.
|
package/README.md
CHANGED
|
@@ -158,6 +158,10 @@ mm-harness run --list
|
|
|
158
158
|
Shared libraries hold durable actions and composable recipes. Task acceptance
|
|
159
159
|
criteria remain task-local. See [Recipes](docs/RECIPES.md).
|
|
160
160
|
|
|
161
|
+
Teams maintain their library checkouts. Update them before starting a task;
|
|
162
|
+
the harness records local revisions and recipe digests but does not fetch or
|
|
163
|
+
check for upstream updates. Keep the selected library unchanged during a run.
|
|
164
|
+
|
|
161
165
|
## Recover
|
|
162
166
|
|
|
163
167
|
```bash
|
|
@@ -441,6 +441,16 @@ async function decideExtensionReadiness(target, options = {}) {
|
|
|
441
441
|
reasons: ["Build is fresh and the extension runtime is healthy over CDP."],
|
|
442
442
|
actions: []
|
|
443
443
|
},
|
|
444
|
+
{
|
|
445
|
+
when: cdp.errorCode === "EXTENSION_BACKGROUND_UNRESPONSIVE",
|
|
446
|
+
decision: "blocked",
|
|
447
|
+
reasonCode: "background-unresponsive",
|
|
448
|
+
reasons: [
|
|
449
|
+
"Extension startup failed; inspect the background error before relaunching or resetting wallet state.",
|
|
450
|
+
...cdp.findings?.slice(0, 3) ?? []
|
|
451
|
+
],
|
|
452
|
+
actions: []
|
|
453
|
+
},
|
|
444
454
|
{
|
|
445
455
|
when: cdp.errorCode === "EVM_RPC_EXECUTION_FAILED",
|
|
446
456
|
decision: "blocked",
|
|
@@ -492,6 +502,13 @@ async function decideExtensionReadiness(target, options = {}) {
|
|
|
492
502
|
reasons: [`Release artifact ${releaseArtifact.expectedVersion} is intact and healthy over CDP.`],
|
|
493
503
|
actions: []
|
|
494
504
|
},
|
|
505
|
+
{
|
|
506
|
+
when: cdp.errorCode === "EXTENSION_BACKGROUND_UNRESPONSIVE",
|
|
507
|
+
decision: "blocked",
|
|
508
|
+
reasonCode: "background-unresponsive",
|
|
509
|
+
reasons: ["Extension startup failed; inspect the background error before relaunching or resetting wallet state.", ...cdp.findings?.slice(0, 3) ?? []],
|
|
510
|
+
actions: []
|
|
511
|
+
},
|
|
495
512
|
{
|
|
496
513
|
when: cdp.errorCode === "EVM_RPC_EXECUTION_FAILED",
|
|
497
514
|
decision: "blocked",
|
|
@@ -51,7 +51,7 @@ async function assertHealthyExtensionRuntime(options) {
|
|
|
51
51
|
pageMode: options.pageMode
|
|
52
52
|
});
|
|
53
53
|
if (lastReport.status === "PASS") return lastReport;
|
|
54
|
-
if (lastReport.errorCode === "UI_COMPOSITOR_SUSPENDED") {
|
|
54
|
+
if (lastReport.errorCode === "UI_COMPOSITOR_SUSPENDED" || lastReport.errorCode === "EXTENSION_BACKGROUND_UNRESPONSIVE") {
|
|
55
55
|
throw new Error(formatHealthFailure(lastReport, options.projectRoot));
|
|
56
56
|
}
|
|
57
57
|
await sleep(500);
|
|
@@ -151,6 +151,23 @@ async function checkExtensionRuntimeHealth(projectRoot, cdpPort, options = {}) {
|
|
|
151
151
|
};
|
|
152
152
|
}
|
|
153
153
|
const runtime = await evaluateHealth(session, cdpCallTimeoutMs);
|
|
154
|
+
if (runtime.backgroundUnresponsive === true) {
|
|
155
|
+
return {
|
|
156
|
+
status: "FAIL",
|
|
157
|
+
errorCode: "EXTENSION_BACKGROUND_UNRESPONSIVE",
|
|
158
|
+
userAction: `mm-harness logs --target ${shellQuote(projectRoot)} --full # inspect the startup error before restarting or resetting an authorized dev fixture`,
|
|
159
|
+
cdpPort,
|
|
160
|
+
targetUrl: target.url,
|
|
161
|
+
extensionId: safeExtensionId(target),
|
|
162
|
+
extensionPageTargets: extensionTargets.length,
|
|
163
|
+
findings: ["EXTENSION_BACKGROUND_UNRESPONSIVE: Extension UI reports a startup failure or unresponsive background; RPC readiness cannot be established."],
|
|
164
|
+
details: {
|
|
165
|
+
projectRoot,
|
|
166
|
+
targetUrls: targets.map((entry) => entry.url).filter(Boolean),
|
|
167
|
+
runtime
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
154
171
|
if (runtime.hasSubmitRequest !== true) {
|
|
155
172
|
const providerProbe = await probeUiEthereumProvider(session, cdpCallTimeoutMs);
|
|
156
173
|
runtime.evmRpcProbeOk = providerProbe.ok;
|
|
@@ -160,9 +177,6 @@ async function checkExtensionRuntimeHealth(projectRoot, cdpPort, options = {}) {
|
|
|
160
177
|
if (runtime.href && !String(runtime.href).startsWith("chrome-extension://")) {
|
|
161
178
|
findings.push(`Extension page href is not an extension URL: ${runtime.href}`);
|
|
162
179
|
}
|
|
163
|
-
if (runtime.backgroundUnresponsive === true) {
|
|
164
|
-
findings.push("Extension UI reports background connection unresponsive.");
|
|
165
|
-
}
|
|
166
180
|
if (runtime.ethereumConnectionUnavailable === true) {
|
|
167
181
|
findings.push('Extension UI reports "Unable to connect to Ethereum".');
|
|
168
182
|
}
|
package/dist/commands/help.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { color } from "../cli-color.js";
|
|
3
4
|
import { detectAdapter } from "../harness.js";
|
|
4
5
|
import { optionString, parseArgs, targetPath } from "./parse-args.js";
|
|
5
6
|
const UNSCOPED_NEXT = "cd into a MetaMask checkout or pass --adapter <mobile|extension|core>";
|
|
@@ -48,21 +49,22 @@ function recipeHelpPayload(topic, harnessVersion, adapter, target) {
|
|
|
48
49
|
};
|
|
49
50
|
}
|
|
50
51
|
function renderRecipeHelp(topic, harnessVersion, adapter) {
|
|
52
|
+
const paint = (style, text) => color(style, text, { stream: process.stdout });
|
|
51
53
|
const lines = [
|
|
52
|
-
`mm-harness ${harnessVersion} recipe guide
|
|
53
|
-
`Context: Recipe Protocol v${topic.recipeProtocolVersion}, adapter ${adapter}
|
|
54
|
+
paint("bold", `mm-harness ${harnessVersion} recipe guide`),
|
|
55
|
+
paint("dim", `Context: Recipe Protocol v${topic.recipeProtocolVersion}, adapter ${adapter}`),
|
|
54
56
|
topic.summary,
|
|
55
57
|
topic.capabilities,
|
|
56
|
-
topic.scopeNotice,
|
|
57
|
-
|
|
58
|
+
paint("warn", topic.scopeNotice),
|
|
59
|
+
`Visual tutorial: ${paint("cmd", "mm-harness tutorial")}`
|
|
58
60
|
];
|
|
59
|
-
if (adapter === "unscoped") lines.push(`No MetaMask checkout detected. Next: ${UNSCOPED_NEXT}.`);
|
|
61
|
+
if (adapter === "unscoped") lines.push(paint("warn", `No MetaMask checkout detected. Next: ${UNSCOPED_NEXT}.`));
|
|
60
62
|
for (const [index, section] of scopedSections(topic, adapter).entries()) {
|
|
61
|
-
lines.push("", `${index + 1}. ${section.title}
|
|
63
|
+
lines.push("", paint("label", `${index + 1}. ${section.title}`), ` ${section.instruction}`);
|
|
62
64
|
for (const detail of section.details) lines.push(` - ${detail}`);
|
|
63
|
-
for (const command of section.commands) lines.push(` $ ${command}`);
|
|
65
|
+
for (const command of section.commands) lines.push(` $ ${paint("cmd", command)}`);
|
|
64
66
|
}
|
|
65
|
-
lines.push("",
|
|
67
|
+
lines.push("", `${paint("warn", "Safety:")} ${topic.safety}`);
|
|
66
68
|
return `${lines.join("\n")}
|
|
67
69
|
`;
|
|
68
70
|
}
|
|
@@ -310,6 +310,16 @@ async function handleLaunchLocked(argv, stream) {
|
|
|
310
310
|
exitCode: EXIT.infra
|
|
311
311
|
});
|
|
312
312
|
}
|
|
313
|
+
if (adapter === "extension" && attempt.output.includes("EXTENSION_BACKGROUND_UNRESPONSIVE:")) {
|
|
314
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
315
|
+
code: "EXTENSION_BACKGROUND_UNRESPONSIVE",
|
|
316
|
+
message: "Extension startup failed; inspect the background error before restarting or resetting wallet state.",
|
|
317
|
+
recoverable: false,
|
|
318
|
+
userAction: `mm-harness logs --target ${shellQuote(target)} --full`,
|
|
319
|
+
exitCode: EXIT.runtime,
|
|
320
|
+
originalError: attempt.output.trim() || void 0
|
|
321
|
+
});
|
|
322
|
+
}
|
|
313
323
|
if (adapter === "extension" && attempt.output.includes("EVM_RPC_EXECUTION_FAILED:")) {
|
|
314
324
|
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
315
325
|
code: "EVM_RPC_EXECUTION_FAILED",
|
package/docs/QA.md
CHANGED
|
@@ -56,8 +56,8 @@ mm-harness verify
|
|
|
56
56
|
- [ ] Bad input fails before runtime work with a stable code and one exact next
|
|
57
57
|
action.
|
|
58
58
|
- [ ] `doctor --fix` is idempotent and does not choose a fixture or launch.
|
|
59
|
-
- [ ] With `capture-helper` absent, Extension reports CDP screenshots and
|
|
60
|
-
reports
|
|
59
|
+
- [ ] With `capture-helper` absent, Extension reports CDP screenshots and no
|
|
60
|
+
Extension video; Mobile reports its independent simctl/ADB capture readiness.
|
|
61
61
|
|
|
62
62
|
Run the bounded multi-platform preflight when all three checkouts are available:
|
|
63
63
|
|
|
@@ -177,8 +177,8 @@ Before release:
|
|
|
177
177
|
Core scenarios.
|
|
178
178
|
- [ ] Independent review approves the exact final diff.
|
|
179
179
|
- [ ] Known limits are explicit: Extension requires product Infura setup;
|
|
180
|
-
Mobile requires its normal dev-client/device setup; video requires
|
|
181
|
-
`capture-helper
|
|
180
|
+
Mobile requires its normal dev-client/device setup; Extension video requires
|
|
181
|
+
`capture-helper`, while Mobile uses platform recorders; the harness never invents funded fixtures.
|
|
182
182
|
- [ ] Existing [Farmslot](https://farmslot.io) slots remain compatible.
|
|
183
183
|
|
|
184
184
|
When a slot manager is available, repeat `runner.smoke` in one existing managed
|
package/docs/RECIPES.md
CHANGED
|
@@ -96,6 +96,23 @@ detached-DOM check, and idle-control comparison. A one-screen slope does not
|
|
|
96
96
|
prove those broader claims. Check the suite's reference markers against the
|
|
97
97
|
tested build before running it.
|
|
98
98
|
|
|
99
|
+
### Extension timing observation
|
|
100
|
+
|
|
101
|
+
First inspect `app.performance_capture` and `app.network_capture`. When the
|
|
102
|
+
`recipe-performance` skill is installed, its
|
|
103
|
+
`scripts/capture-extension-window.mjs --help` describes capture around a single
|
|
104
|
+
command. Run that wrapper outside the recipe; a command node cannot start a
|
|
105
|
+
second `mm-harness run/call` while its parent owns the checkout sandbox.
|
|
106
|
+
|
|
107
|
+
For a custom observer blocked by LavaMoat, register a pre-boot script with
|
|
108
|
+
`Page.addScriptToEvaluateOnNewDocument` to retain only the required native API
|
|
109
|
+
reference, such as a bound `requestAnimationFrame`. Initialize it during explicit
|
|
110
|
+
setup, remove the preload after use, and clean up the observer afterward. Do not
|
|
111
|
+
disable scuttling, pause the debugger, or reload inside the measured window.
|
|
112
|
+
Record lifecycle identity and use the application's clock for UI timing; CLI
|
|
113
|
+
duration is a different measurement. Keep endpoint and readiness predicates in
|
|
114
|
+
the team recipe, not a new harness action.
|
|
115
|
+
|
|
99
116
|
### Extension Perps release recipes
|
|
100
117
|
|
|
101
118
|
Discover these identifiers with `run --list --adapter extension --json` and
|
|
@@ -189,6 +206,10 @@ export RECIPE_LIBRARY_PATH="perps=/path/to/experimental-metamask-recipe-perps"
|
|
|
189
206
|
|
|
190
207
|
`perps.pro-order-setup` preserves existing orders, positions, and strategies by
|
|
191
208
|
default. Its Scale, Chase, and TWAP placement callers inherit that policy.
|
|
209
|
+
All three placement helpers require explicit `account` and `notional` inputs.
|
|
210
|
+
Scale also requires `start_price` and `end_price`; Chase requires `max_distance`.
|
|
211
|
+
Choose values for the current market and approved budget. Preserve JSON quotes
|
|
212
|
+
for string parameters, for example `'notional="15"'`; inspect `--describe` first.
|
|
192
213
|
When a proof requires a clean testnet market, reserve the account/market and
|
|
193
214
|
compose explicit setup with `trading_state=clean_selected_testnet_market`.
|
|
194
215
|
This parameter never authorizes cleanup of another task's trades.
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// Assert that selected Perps orders are open or absent using the Mobile controller.
|
|
2
2
|
import { runAdapter } from '../platform/bridge.mjs';
|
|
3
3
|
import { assertOrders } from './perps.mjs';
|
|
4
|
-
import { useOrderReceipt } from './order-receipt.mjs';
|
|
5
4
|
|
|
6
5
|
function expectedOpen(input) {
|
|
7
6
|
if (input.node?.state == null) throw new Error('metamask.perps.assert_orders requires state=open or state=none.');
|
|
@@ -11,6 +10,7 @@ function expectedOpen(input) {
|
|
|
11
10
|
throw new Error(`metamask.perps.assert_orders received unsupported state: ${state}`);
|
|
12
11
|
}
|
|
13
12
|
|
|
14
|
-
runAdapter((input) =>
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
runAdapter((input) => {
|
|
14
|
+
if (Object.hasOwn(input.node ?? {}, 'confirmation')) throw new Error('Receipt-based Mobile assertions are unsupported. Next: mm-harness help');
|
|
15
|
+
return assertOrders(input, expectedOpen(input));
|
|
16
|
+
});
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// Assert that selected Perps positions are open or absent using the Mobile controller.
|
|
2
2
|
import { runAdapter } from '../platform/bridge.mjs';
|
|
3
3
|
import { assertPositions } from './perps.mjs';
|
|
4
|
-
import { useOrderReceipt } from './order-receipt.mjs';
|
|
5
4
|
|
|
6
5
|
function expectedOpen(input) {
|
|
7
6
|
if (input.node?.state == null) throw new Error('metamask.perps.assert_positions requires state=open or state=none.');
|
|
@@ -11,6 +10,7 @@ function expectedOpen(input) {
|
|
|
11
10
|
throw new Error(`metamask.perps.assert_positions received unsupported state: ${state}`);
|
|
12
11
|
}
|
|
13
12
|
|
|
14
|
-
runAdapter((input) =>
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
runAdapter((input) => {
|
|
14
|
+
if (Object.hasOwn(input.node ?? {}, 'confirmation')) throw new Error('Receipt-based Mobile assertions are unsupported. Next: mm-harness help');
|
|
15
|
+
return assertPositions(input, expectedOpen(input));
|
|
16
|
+
});
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
// Cancel selected Perps orders through the Mobile controller and assert their absence.
|
|
2
2
|
import { runAdapter } from '../platform/bridge.mjs';
|
|
3
3
|
import { assertOrders, closeOrders } from './perps.mjs';
|
|
4
|
-
import { useOrderReceipt } from './order-receipt.mjs';
|
|
5
4
|
|
|
6
5
|
runAdapter(async (input) => {
|
|
7
|
-
if (Object.hasOwn(input.node ?? {}, 'confirmation'))
|
|
6
|
+
if (Object.hasOwn(input.node ?? {}, 'confirmation')) throw new Error('Receipt-based Mobile cleanup is unsupported; no orders were cancelled. Next: mm-harness help');
|
|
8
7
|
const close = await closeOrders(input);
|
|
9
8
|
const assertion = await assertOrders(input, false);
|
|
10
9
|
return { ...assertion, close };
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
// Close selected Perps positions through the Mobile controller and assert their absence.
|
|
2
2
|
import { runAdapter } from '../platform/bridge.mjs';
|
|
3
3
|
import { assertPositions, closePositions } from './perps.mjs';
|
|
4
|
-
import { useOrderReceipt } from './order-receipt.mjs';
|
|
5
4
|
|
|
6
5
|
runAdapter(async (input) => {
|
|
7
|
-
if (Object.hasOwn(input.node ?? {}, 'confirmation'))
|
|
6
|
+
if (Object.hasOwn(input.node ?? {}, 'confirmation')) throw new Error('Receipt-based Mobile cleanup is unsupported; no positions were closed. Next: mm-harness help');
|
|
8
7
|
const close = await closePositions(input);
|
|
9
8
|
const assertion = await assertPositions(input, false);
|
|
10
9
|
return { ...assertion, close };
|
|
@@ -2399,7 +2399,6 @@
|
|
|
2399
2399
|
"schema": {
|
|
2400
2400
|
"type": "object",
|
|
2401
2401
|
"properties": {
|
|
2402
|
-
"confirmation": { "type": "object", "description": "Frozen read_order_input output; requires exact live UI receipt identity." },
|
|
2403
2402
|
"market": {
|
|
2404
2403
|
"type": "string",
|
|
2405
2404
|
"description": "Single market symbol, e.g. BTC or ETH. Alias: symbol."
|
|
@@ -2491,7 +2490,6 @@
|
|
|
2491
2490
|
"schema": {
|
|
2492
2491
|
"type": "object",
|
|
2493
2492
|
"properties": {
|
|
2494
|
-
"confirmation": { "type": "object", "description": "Frozen read_order_input output; requires exact live UI receipt identity." },
|
|
2495
2493
|
"market": {
|
|
2496
2494
|
"type": "string",
|
|
2497
2495
|
"description": "Single market symbol, e.g. BTC or ETH. Alias: symbol."
|
|
@@ -2578,32 +2576,6 @@
|
|
|
2578
2576
|
],
|
|
2579
2577
|
"execution_capabilities": ["app-mutation", "external-mutation"]
|
|
2580
2578
|
},
|
|
2581
|
-
"metamask.perps.read_order_input": {
|
|
2582
|
-
"description": "Freeze committed unsigned Mobile inputs and require a receipt observer before UI submission.",
|
|
2583
|
-
"examples": [{ "action": "metamask.perps.read_order_input", "amount": "10", "leverage": 1, "protection": "none", "intent": "Freeze the exact unsigned order before its UI submission", "next": "submit-order" }],
|
|
2584
|
-
"schema": {
|
|
2585
|
-
"type": "object", "required": ["amount", "leverage"], "additionalProperties": false,
|
|
2586
|
-
"properties": {
|
|
2587
|
-
"amount": { "type": ["string", "number"] },
|
|
2588
|
-
"leverage": { "type": "number" },
|
|
2589
|
-
"identity": { "type": "object" },
|
|
2590
|
-
"protection": { "type": "string", "enum": ["none", "take_profit", "stop_loss", "both"], "default": "none" }
|
|
2591
|
-
}
|
|
2592
|
-
},
|
|
2593
|
-
"execution_capabilities": []
|
|
2594
|
-
},
|
|
2595
|
-
"metamask.perps.read_order_receipt": {
|
|
2596
|
-
"description": "Observe the actual Mobile UI submission result including failure and accepted IDs; never dispatch.",
|
|
2597
|
-
"examples": [{ "action": "metamask.perps.read_order_receipt", "confirmation": "{{outputs.capture-input.confirmation}}", "intent": "Observe the result of the frozen UI submission", "next": "done" }],
|
|
2598
|
-
"schema": {
|
|
2599
|
-
"type": "object", "required": ["confirmation"], "additionalProperties": false,
|
|
2600
|
-
"properties": {
|
|
2601
|
-
"confirmation": { "type": "object" },
|
|
2602
|
-
"timeout_ms": { "type": "number", "minimum": 1, "default": 30000 }
|
|
2603
|
-
}
|
|
2604
|
-
},
|
|
2605
|
-
"execution_capabilities": []
|
|
2606
|
-
},
|
|
2607
2579
|
"metamask.perps.configure_order": {
|
|
2608
2580
|
"description": "Set USD notional and visible leverage on an already-open instrumented Mobile confirmation; verify exact mounted values and native readback against its unchanged unsigned request, without submitting.",
|
|
2609
2581
|
"schema": {
|
|
@@ -2734,7 +2706,6 @@
|
|
|
2734
2706
|
"schema": {
|
|
2735
2707
|
"type": "object",
|
|
2736
2708
|
"properties": {
|
|
2737
|
-
"confirmation": { "type": "object", "description": "Frozen read_order_input output; requires exact live UI receipt identity." },
|
|
2738
2709
|
"market": {
|
|
2739
2710
|
"type": "string",
|
|
2740
2711
|
"description": "Single market symbol, e.g. BTC or ETH. Alias: symbol."
|
|
@@ -2827,7 +2798,6 @@
|
|
|
2827
2798
|
"schema": {
|
|
2828
2799
|
"type": "object",
|
|
2829
2800
|
"properties": {
|
|
2830
|
-
"confirmation": { "type": "object", "description": "Frozen read_order_input output; requires exact live UI receipt identity." },
|
|
2831
2801
|
"market": {
|
|
2832
2802
|
"type": "string",
|
|
2833
2803
|
"description": "Single market symbol, e.g. BTC or ETH. Alias: symbol."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deeeed/metamask-harness",
|
|
3
|
-
"version": "0.50.
|
|
3
|
+
"version": "0.50.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mm-harness": "bin/mm-harness"
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"@farmslot/expo-recipe": "0.9.0",
|
|
25
25
|
"@farmslot/handoff": "^0.3.1",
|
|
26
26
|
"@farmslot/protocol": "0.21.0",
|
|
27
|
-
"@farmslot/recipe-harness": "0.16.
|
|
27
|
+
"@farmslot/recipe-harness": "0.16.1",
|
|
28
28
|
"agent-device": "0.19.3",
|
|
29
29
|
"commander": "^12.0.0",
|
|
30
30
|
"es-module-lexer": "2.3.1",
|
|
@@ -94,6 +94,7 @@
|
|
|
94
94
|
"instruction": "Use static call references for unchanged sub-journeys. Separate preparation, action, assertions, and evidence; add workflow.teardown when cleanup must run after success or failure.",
|
|
95
95
|
"details": [
|
|
96
96
|
"Starting conditions are ordinary nodes at the beginning of workflow, not a preconditions field. Use idempotent ensure actions and independently assert their postconditions.",
|
|
97
|
+
"Do not invoke mm-harness run/call from a command node on the same checkout: the outer run owns its sandbox. Compose actions or call child recipes instead. External measurement wrappers may invoke one root run, with preparation outside it; never bypass the lock.",
|
|
97
98
|
"Inspect existing state before creating or cleaning fixtures. Establish only prerequisites required by the claim; a history or display check may already have usable records and need no trade or cleanup. Record the selected records and verify they still satisfy the proof boundary.",
|
|
98
99
|
"Match history-query ranges to the dates of existing visible records before declaring fixtures missing. Sample only sections needed for change detection; fetch one-time corroborating history once, and count background calls as well as graph nodes.",
|
|
99
100
|
"Actions translate UI, CDP, or controller operations. Recipes compose them. Product code owns business rules. Teams own specialized journeys in their libraries; the harness retains shared capabilities and a small set of composable examples.",
|
|
@@ -188,6 +189,10 @@
|
|
|
188
189
|
]
|
|
189
190
|
},
|
|
190
191
|
"Performance: define a measured window, exclude setup, and compare equivalent builds, devices, and lifecycle conditions; action duration alone is not app latency.",
|
|
192
|
+
{
|
|
193
|
+
"text": "For Extension timing, inspect app.performance_capture/app.network_capture and the installed recipe-performance capture-extension-window.mjs helper before writing a collector. If LavaMoat blocks an observation API, capture its reference before initialization via Page.addScriptToEvaluateOnNewDocument during explicit setup, not inside the measured window. Keep domain predicates in the team recipe; see docs/RECIPES.md#extension-timing-observation.",
|
|
194
|
+
"adapters": ["extension"]
|
|
195
|
+
},
|
|
191
196
|
"Retain samples and declared limits when a measurement fails. For memory growth, compare an equal-duration idle control before attributing it to navigation; do not raise the budget to make the run pass.",
|
|
192
197
|
"Analytics: verify the collection destination and existing consent before capture. Changing participation or marketing consent requires operator approval and restoration of the prior settings; a recipe's hardcoded opt-in is not permission. Missing collection prerequisites are not missing product events.",
|
|
193
198
|
"Flake risk: wait on observable state instead of sleeping, keep device and runtime identity explicit, and never overwrite a prior run's artifacts.",
|
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
// Bind live order results and cleanup to the exact UI approval, request and accepted fills.
|
|
2
|
-
import { bridgeCommand } from '../platform/bridge.mjs';
|
|
3
|
-
|
|
4
|
-
function confirmationFor(input) {
|
|
5
|
-
const confirmation = input.node?.confirmation;
|
|
6
|
-
const identity = confirmation?.identity;
|
|
7
|
-
if (!identity || ['approvalId', 'account', 'provider', 'chainId', 'routeKey', 'asset', 'direction']
|
|
8
|
-
.some((field) => typeof identity[field] !== 'string' || !identity[field])
|
|
9
|
-
|| !Number.isSafeInteger(identity.transactionTime) || identity.isTestnet !== true
|
|
10
|
-
|| identity.provider !== 'hyperliquid' || confirmation.form?.leverage !== 1
|
|
11
|
-
|| !(Number(confirmation.form?.amount) > 0 && Number(confirmation.form.amount) <= 20)) {
|
|
12
|
-
throw new Error('Mobile receipt requires a frozen Hyperliquid testnet confirmation at 1x and at most $20.');
|
|
13
|
-
}
|
|
14
|
-
return confirmation;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function receiptExpression(confirmation, allowMissing = false) {
|
|
18
|
-
return `
|
|
19
|
-
const expected = ${JSON.stringify(confirmation)};
|
|
20
|
-
const controller = Engine.context.PerpsController;
|
|
21
|
-
function assertIdentity() {
|
|
22
|
-
const state = controller.state;
|
|
23
|
-
const account = Engine.context.AccountsController.getSelectedAccount();
|
|
24
|
-
if (account.address.toLowerCase() !== expected.identity.account.toLowerCase()
|
|
25
|
-
|| state.activeProvider !== expected.identity.provider || state.isTestnet !== true) {
|
|
26
|
-
throw new Error('Mobile receipt account, provider or network changed.');
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
assertIdentity();
|
|
30
|
-
if (typeof __AGENTIC__.readPerpsOrderReceipts !== 'function') throw new Error('Mobile UI receipt observer is unavailable.');
|
|
31
|
-
const receipts = __AGENTIC__.readPerpsOrderReceipts(expected.identity.approvalId);
|
|
32
|
-
if (${allowMissing} && Array.isArray(receipts) && receipts.length === 0) return null;
|
|
33
|
-
if (!Array.isArray(receipts) || receipts.length !== 1) throw new Error('Exactly one UI submission receipt is required.');
|
|
34
|
-
const receipt = receipts[0];
|
|
35
|
-
for (const key of ['approvalId', 'transactionTime', 'account', 'provider', 'isTestnet', 'chainId', 'routeKey']) {
|
|
36
|
-
if (receipt.identity[key] !== expected.identity[key]) throw new Error('UI receipt identity differs from the frozen confirmation.');
|
|
37
|
-
}
|
|
38
|
-
for (const key of ['asset', 'direction']) if (receipt.request[key] !== expected.identity[key]) throw new Error('UI receipt market or direction differs.');
|
|
39
|
-
for (const key of ['amount', 'leverage', 'orderType', 'limitPrice', 'takeProfitPrice', 'stopLossPrice']) {
|
|
40
|
-
if ((receipt.request[key] ?? null) !== (expected.form[key] ?? null)) throw new Error('UI receipt request differs from committed ' + key + '.');
|
|
41
|
-
}
|
|
42
|
-
const ids = [...new Set([receipt.result?.orderId, ...(receipt.result?.childOrderIds ?? [])].filter(id => typeof id === 'string' && id))];
|
|
43
|
-
`;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export async function readOrderReceipt(input) {
|
|
47
|
-
const confirmation = confirmationFor(input);
|
|
48
|
-
const deadline = Date.now() + Number(input.node.timeout_ms ?? 30000);
|
|
49
|
-
input = { ...input, node: { ...input.node, _action_deadline_epoch_ms: deadline } };
|
|
50
|
-
for (;;) {
|
|
51
|
-
const receipt = await bridgeCommand(input, ['eval', `(() => { ${receiptExpression(confirmation, true)} return receipt; })()`]);
|
|
52
|
-
if (receipt?.status === 'settled') {
|
|
53
|
-
const protectionCount = ['takeProfitPrice', 'stopLossPrice'].filter((key) => receipt.request[key] != null).length;
|
|
54
|
-
if (receipt.result?.success === true && protectionCount) {
|
|
55
|
-
if (receipt.request.orderType !== 'market') throw new Error('Protected limit receipts remain unsupported.');
|
|
56
|
-
if (!receipt.protectionResult) {
|
|
57
|
-
if (Date.now() >= deadline) throw new Error('Mobile protection result is unknown; do not submit again.');
|
|
58
|
-
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
const protectionIds = receipt.protectionResult.childOrderIds;
|
|
62
|
-
if (receipt.protectionResult.success !== true || !Array.isArray(protectionIds)
|
|
63
|
-
|| protectionIds.length !== protectionCount || new Set(protectionIds).size !== protectionCount
|
|
64
|
-
|| protectionIds.some((id) => typeof id !== 'string' || !/^[1-9][0-9]*$/u.test(id)
|
|
65
|
-
|| id === receipt.result.orderId || receipt.result.childOrderIds?.includes(id))) {
|
|
66
|
-
throw new Error(`Mobile protection result lacks complete accepted IDs: ${JSON.stringify(receipt.protectionResult)}.`);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return { action: input.action, receipt };
|
|
70
|
-
}
|
|
71
|
-
if (Date.now() >= deadline) throw new Error('Mobile UI submission result is unknown; do not submit again.');
|
|
72
|
-
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export async function useOrderReceipt(input, operation) {
|
|
77
|
-
const confirmation = confirmationFor(input);
|
|
78
|
-
const deadline = Date.now() + Number(input.node.timeout_ms ?? 30000);
|
|
79
|
-
input = { ...input, node: { ...input.node, _action_deadline_epoch_ms: deadline } };
|
|
80
|
-
if ((input.node.market && input.node.market !== confirmation.identity.asset)
|
|
81
|
-
|| (input.node.side && input.node.side !== confirmation.identity.direction)
|
|
82
|
-
|| (['positions', 'orders'].includes(operation) && input.node.state !== 'open')) {
|
|
83
|
-
throw new Error('Receipt assertion requires the same market/direction and state=open.');
|
|
84
|
-
}
|
|
85
|
-
const mutation = operation === 'cancel' || operation === 'close';
|
|
86
|
-
const result = await bridgeCommand(input, ['eval-async', `(() => {
|
|
87
|
-
${receiptExpression(confirmation)}
|
|
88
|
-
if (receipt.status !== 'settled' || receipt.result?.success !== true || !ids.length) throw new Error('Accepted UI receipt IDs are required before state assertion or cleanup.');
|
|
89
|
-
if (receipt.result.providerId && receipt.result.providerId !== expected.identity.provider) throw new Error('Accepted provider differs from UI receipt identity.');
|
|
90
|
-
if (!['market', 'limit'].includes(receipt.request.orderType)) throw new Error('Receipt cleanup supports ordinary market and limit orders only.');
|
|
91
|
-
const market = receipt.request.asset;
|
|
92
|
-
const protectionKeys = ['takeProfitPrice', 'stopLossPrice'].filter(key => receipt.request[key] != null);
|
|
93
|
-
const protectionIds = protectionKeys.length ? receipt.protectionResult?.childOrderIds : [];
|
|
94
|
-
if (protectionKeys.length && (receipt.request.orderType !== 'market' || receipt.protectionResult?.success !== true
|
|
95
|
-
|| !Array.isArray(protectionIds) || protectionIds.length !== protectionKeys.length
|
|
96
|
-
|| new Set(protectionIds).size !== protectionKeys.length
|
|
97
|
-
|| protectionIds.some(id => typeof id !== 'string' || !/^[1-9][0-9]*$/.test(id) || ids.includes(id)))) {
|
|
98
|
-
throw new Error('Protected cleanup requires complete distinct accepted market protection IDs.');
|
|
99
|
-
}
|
|
100
|
-
const ownedIds = ids.concat(protectionIds);
|
|
101
|
-
const sign = receipt.request.direction === 'long' ? 1n : -1n;
|
|
102
|
-
function decimal(value) {
|
|
103
|
-
const match = /^(-?)(0|[1-9][0-9]{0,19})(?:\\.([0-9]{1,8}))?$/.exec(String(value));
|
|
104
|
-
if (!match) throw new Error('Receipt fill decimal is invalid.');
|
|
105
|
-
return BigInt(match[1] + match[2] + (match[3] ?? '').padEnd(8, '0'));
|
|
106
|
-
}
|
|
107
|
-
function readSnapshot() {
|
|
108
|
-
assertIdentity();
|
|
109
|
-
if (typeof controller.getUserDataSnapshot !== 'function') throw new Error('Complete user-data snapshots are unavailable.');
|
|
110
|
-
return controller.getUserDataSnapshot().then(snapshot => {
|
|
111
|
-
assertIdentity();
|
|
112
|
-
const identity = snapshot?.identity;
|
|
113
|
-
if (!Array.isArray(snapshot?.positions) || !Array.isArray(snapshot?.orders)
|
|
114
|
-
|| typeof identity?.address !== 'string' || identity.address.toLowerCase() !== expected.identity.account.toLowerCase()
|
|
115
|
-
|| identity.provider !== expected.identity.provider || identity.network !== 'testnet') {
|
|
116
|
-
throw new Error('Receipt snapshot is incomplete or belongs to another identity.');
|
|
117
|
-
}
|
|
118
|
-
const orders = snapshot.orders.filter(order => order.symbol === market);
|
|
119
|
-
if (orders.some(order => !ownedIds.includes(String(order.orderId)))
|
|
120
|
-
|| new Set(orders.map(order => String(order.orderId))).size !== orders.length) {
|
|
121
|
-
throw new Error('Extra or unowned market orders prevent receipt assertion or cleanup.');
|
|
122
|
-
}
|
|
123
|
-
return {orders, positions: snapshot.positions.filter(position => position.symbol === market)};
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
function verifyProtection(position, orders, present) {
|
|
127
|
-
const matchedIds = [];
|
|
128
|
-
for (const [priceKey, ordersKey] of [['takeProfitPrice','takeProfitOrders'],['stopLossPrice','stopLossOrders']]) {
|
|
129
|
-
const wanted = present ? receipt.request[priceKey] : null;
|
|
130
|
-
const triggers = position[ordersKey];
|
|
131
|
-
const count = position[priceKey === 'takeProfitPrice' ? 'takeProfitCount' : 'stopLossCount'];
|
|
132
|
-
if (!Array.isArray(triggers)) throw new Error('Exact position protection arrays are unavailable.');
|
|
133
|
-
if (wanted == null) {
|
|
134
|
-
if (triggers.length || count !== 0 || position[priceKey] != null) throw new Error('Unexpected position protection remains.');
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
const direction = priceKey === 'takeProfitPrice' ? 'take_profit' : 'stop';
|
|
138
|
-
const type = direction === 'take_profit' ? 'take_profit_limit' : 'stop_market';
|
|
139
|
-
if (triggers.length !== 1 || count !== 1) throw new Error('Position protection count differs from the frozen UI request.');
|
|
140
|
-
const trigger = triggers[0];
|
|
141
|
-
const order = orders.find(candidate => String(candidate.orderId) === trigger.orderId);
|
|
142
|
-
if (!protectionIds.includes(trigger.orderId) || matchedIds.includes(trigger.orderId)
|
|
143
|
-
|| trigger.direction !== direction || trigger.orderType !== type || decimal(trigger.triggerPrice) !== decimal(wanted)
|
|
144
|
-
|| decimal(trigger.size) !== sign * decimal(position.size) || trigger.reduceOnly !== true || trigger.isPartial !== false
|
|
145
|
-
|| !order || order.side !== (sign === 1n ? 'sell' : 'buy') || order.reduceOnly !== true
|
|
146
|
-
|| order.isTrigger !== true || order.isPositionTpsl !== true || order.triggerOrderType !== type
|
|
147
|
-
|| decimal(order.triggerPrice) !== decimal(wanted) || decimal(order.size) !== decimal(trigger.size)) {
|
|
148
|
-
throw new Error('Exact protection ID, price, size, side or type differs from the UI receipt.');
|
|
149
|
-
}
|
|
150
|
-
matchedIds.push(trigger.orderId);
|
|
151
|
-
}
|
|
152
|
-
if (present && matchedIds.length !== protectionIds.length) throw new Error('Accepted protection IDs are not all represented in the position.');
|
|
153
|
-
}
|
|
154
|
-
function verifyLineage(position) {
|
|
155
|
-
return controller.getOrderFills({aggregateByTime: false, startTime: receipt.identity.transactionTime, endTime: Date.now()}).then(fills => {
|
|
156
|
-
assertIdentity();
|
|
157
|
-
if (!Array.isArray(fills) || fills.length >= 2000) throw new Error('Receipt fill history is incomplete.');
|
|
158
|
-
const marketFills = fills.filter(fill => fill.symbol === market);
|
|
159
|
-
if (!marketFills.length || marketFills.some(fill => !ids.includes(String(fill.orderId)))) throw new Error('Another order changed the market or parent accepted fills are unavailable.');
|
|
160
|
-
marketFills.sort((a,b) => { const difference = sign * (decimal(a.startPosition) - decimal(b.startPosition)); return difference < 0n ? -1 : difference > 0n ? 1 : 0; });
|
|
161
|
-
let size = 0n;
|
|
162
|
-
for (const fill of marketFills) {
|
|
163
|
-
if (fill.side !== (sign === 1n ? 'buy' : 'sell') || decimal(fill.size) <= 0n || decimal(fill.startPosition) !== size) throw new Error('Parent accepted fills do not prove a position starting from zero.');
|
|
164
|
-
size += sign * decimal(fill.size);
|
|
165
|
-
}
|
|
166
|
-
if (size !== decimal(position.size) || position.leverage?.value !== receipt.request.leverage) throw new Error('Current position differs from exact parent fills or leverage.');
|
|
167
|
-
return marketFills;
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
function cancelOwned(orders, baseline) {
|
|
171
|
-
const results = [];
|
|
172
|
-
function waitForCancellation() {
|
|
173
|
-
return readSnapshot().then(after => {
|
|
174
|
-
if (baseline && (after.positions.length !== 1 || after.positions[0].size !== baseline.size
|
|
175
|
-
|| after.positions[0].entryPrice !== baseline.entryPrice || after.positions[0].leverage?.value !== baseline.leverage.value)) {
|
|
176
|
-
throw new Error('Owned position changed during protective cancellation.');
|
|
177
|
-
}
|
|
178
|
-
if (!after.orders.length) {
|
|
179
|
-
if (baseline) verifyProtection(after.positions[0], after.orders, false);
|
|
180
|
-
return {after, results};
|
|
181
|
-
}
|
|
182
|
-
if (Date.now() >= ${deadline}) throw new Error('Accepted order cancellation is not independently confirmed.');
|
|
183
|
-
return new Promise(resolve => setTimeout(resolve, 250)).then(waitForCancellation);
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
return orders.reduce((previous, order) => previous.then(() => {
|
|
187
|
-
if (Date.now() >= ${deadline}) throw new Error('Receipt cleanup deadline elapsed before cancellation.');
|
|
188
|
-
assertIdentity();
|
|
189
|
-
return controller.cancelOrder({symbol: market, orderId: String(order.orderId)}).then(result => {
|
|
190
|
-
results.push({orderId: order.orderId, result});
|
|
191
|
-
if (result?.success !== true) throw new Error('Exact receipt cancellation failed: ' + JSON.stringify(results));
|
|
192
|
-
});
|
|
193
|
-
}), Promise.resolve()).then(waitForCancellation);
|
|
194
|
-
}
|
|
195
|
-
return readSnapshot().then(snapshot => {
|
|
196
|
-
const orders = snapshot.orders;
|
|
197
|
-
if (${JSON.stringify(operation)} === 'orders') {
|
|
198
|
-
if (!orders.length || orders.some(order => !ids.includes(String(order.orderId))
|
|
199
|
-
|| order.side !== (sign === 1n ? 'buy' : 'sell'))) throw new Error('No matching exact accepted UI order remains open.');
|
|
200
|
-
return {receipt, orders};
|
|
201
|
-
}
|
|
202
|
-
if (${JSON.stringify(operation)} === 'cancel' && !protectionKeys.length) {
|
|
203
|
-
return cancelOwned(orders).then(({results}) => ({receipt, results, remainingOwnedOrders: 0}));
|
|
204
|
-
}
|
|
205
|
-
if (!snapshot.positions.length && ${JSON.stringify(operation)} === 'close' && !orders.length) return {receipt, closed: false, alreadyAbsent: true};
|
|
206
|
-
if (snapshot.positions.length !== 1) throw new Error('Receipt position is absent or ambiguous.');
|
|
207
|
-
const position = snapshot.positions[0];
|
|
208
|
-
if (protectionKeys.length && orders.some(order => !protectionIds.includes(String(order.orderId)))) throw new Error('Extra parent orders prevent protected position assertion or cleanup.');
|
|
209
|
-
return verifyLineage(position).then(fills => {
|
|
210
|
-
const protectionPresent = protectionIds.some(id => orders.some(order => String(order.orderId) === id));
|
|
211
|
-
verifyProtection(position, orders, ${JSON.stringify(operation)} === 'positions' || protectionPresent);
|
|
212
|
-
if (${JSON.stringify(operation)} === 'positions') return {receipt, position, fills};
|
|
213
|
-
if (orders.some(order => !protectionIds.includes(String(order.orderId)))) throw new Error('Parent order remainder prevents owned position cleanup.');
|
|
214
|
-
return cancelOwned(orders, position).then(({after, results}) => {
|
|
215
|
-
if (${JSON.stringify(operation)} === 'cancel') return {receipt, results, remainingOwnedOrders: 0};
|
|
216
|
-
const unchanged = after.positions[0];
|
|
217
|
-
return verifyLineage(unchanged).then(readSnapshot).then(beforeClose => {
|
|
218
|
-
if (beforeClose.positions.length !== 1 || beforeClose.orders.length
|
|
219
|
-
|| beforeClose.positions[0].size !== unchanged.size || beforeClose.positions[0].entryPrice !== unchanged.entryPrice
|
|
220
|
-
|| beforeClose.positions[0].leverage?.value !== unchanged.leverage.value) throw new Error('Owned position changed before close.');
|
|
221
|
-
verifyProtection(beforeClose.positions[0], beforeClose.orders, false);
|
|
222
|
-
if (Date.now() >= ${deadline}) throw new Error('Receipt cleanup deadline elapsed before close.');
|
|
223
|
-
assertIdentity();
|
|
224
|
-
return controller.closePosition({symbol: market, size: String(sign === 1n ? unchanged.size : String(unchanged.size).slice(1)), orderType: 'market', providerId: 'hyperliquid', position: unchanged}).then(closeResult => {
|
|
225
|
-
if (closeResult?.success !== true || !closeResult.orderId) throw new Error('Receipt-owned close has no accepted result: ' + JSON.stringify(closeResult));
|
|
226
|
-
function waitForClose() {
|
|
227
|
-
return readSnapshot().then(final => {
|
|
228
|
-
if (!final.positions.length && !final.orders.length) return {receipt, results, closeResult, closed: true};
|
|
229
|
-
if (Date.now() >= ${deadline}) throw new Error('Receipt-owned close is not independently confirmed.');
|
|
230
|
-
return new Promise(resolve => setTimeout(resolve, 250)).then(waitForClose);
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
return waitForClose();
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
});
|
|
237
|
-
});
|
|
238
|
-
});
|
|
239
|
-
})()`]);
|
|
240
|
-
return { action: input.action, ...result, mutation };
|
|
241
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
// Freeze the visible unsigned order inputs and refuse a repeated or unobservable submission.
|
|
2
|
-
import { bridgeCommand, runAdapter } from '../platform/bridge.mjs';
|
|
3
|
-
import { readOrderInputState } from './order-input-state.mjs';
|
|
4
|
-
import { observeNativeUi } from '../platform/observe-ui.mjs';
|
|
5
|
-
|
|
6
|
-
runAdapter(async (input) => {
|
|
7
|
-
const confirmation = await readOrderInputState(input, input.node?.identity);
|
|
8
|
-
const { identity, form } = confirmation;
|
|
9
|
-
const observation = await observeNativeUi({ refs: ['ui.visible'], node: input.node }, input.context);
|
|
10
|
-
const visible = observation.observations?.['ui.visible'];
|
|
11
|
-
const payRows = visible?.items?.filter((item) => item.test_id === 'pay-with') ?? [];
|
|
12
|
-
if (!visible || visible.truncated || payRows.length !== 1 || payRows[0].label !== 'Pay with, Perps balance') {
|
|
13
|
-
throw new Error('Receipt proof requires the visible Perps balance payment source; funding is not permitted.');
|
|
14
|
-
}
|
|
15
|
-
if (identity.provider !== 'hyperliquid' || identity.isTestnet !== true
|
|
16
|
-
|| form.leverage !== 1 || !(Number(form.amount) > 0 && Number(form.amount) <= 20)
|
|
17
|
-
|| !['market', 'limit'].includes(form.orderType)) {
|
|
18
|
-
throw new Error('Mobile receipt proof requires Hyperliquid testnet, 1x and at most $20 on a market or limit order.');
|
|
19
|
-
}
|
|
20
|
-
if (Number(form.amount) !== Number(input.node.amount) || form.leverage !== Number(input.node.leverage)) {
|
|
21
|
-
throw new Error('Committed Mobile order inputs differ from the requested amount or leverage.');
|
|
22
|
-
}
|
|
23
|
-
const protection = input.node.protection ?? 'none';
|
|
24
|
-
for (const [field, required] of [['takeProfitPrice', ['take_profit', 'both'].includes(protection)], ['stopLossPrice', ['stop_loss', 'both'].includes(protection)]]) {
|
|
25
|
-
if (required ? !(Number(form[field]) > 0) : form[field] !== null) throw new Error(`Committed Mobile ${field} does not match requested protection.`);
|
|
26
|
-
}
|
|
27
|
-
const prior = await bridgeCommand(input, ['eval', `(() => {
|
|
28
|
-
if (typeof __AGENTIC__.readPerpsOrderReceipts !== 'function') throw new Error('Mobile UI receipt observer is unavailable.');
|
|
29
|
-
return __AGENTIC__.readPerpsOrderReceipts(${JSON.stringify(identity.approvalId)});
|
|
30
|
-
})()`]);
|
|
31
|
-
if (!Array.isArray(prior) || prior.length) throw new Error('This unsigned confirmation already has a submission attempt; do not submit it again.');
|
|
32
|
-
return { action: input.action, confirmation, submitted: false };
|
|
33
|
-
});
|