@deeeed/metamask-harness 0.80.0 → 0.82.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 +43 -0
- package/adapters/manifest.json +48 -64
- package/adapters/{terminal → web-dapp}/cleanup.mjs +9 -9
- package/adapters/{terminal → web-dapp}/inject.mjs +14 -14
- package/adapters/{terminal → web-dapp}/launch.mjs +27 -24
- package/adapters/web-dapp/lib/blocked-hosts.mjs +24 -0
- package/adapters/{terminal → web-dapp}/lib/cdp-client.mjs +1 -1
- package/adapters/{terminal → web-dapp}/lib/display.mjs +1 -1
- package/adapters/{terminal → web-dapp}/lib/hud.mjs +1 -1
- package/adapters/web-dapp/lib/policy.mjs +79 -0
- package/adapters/{terminal → web-dapp}/lib/processes.mjs +8 -8
- package/adapters/{terminal → web-dapp}/lib/readiness.mjs +32 -34
- package/adapters/{terminal → web-dapp}/stop.mjs +5 -4
- package/adapters/{terminal → web-dapp}/verify.mjs +9 -6
- package/adapters/{terminal → web-dapp}/wallet-host.mjs +44 -227
- package/dist/adapters/core/actions.js +4 -1
- package/dist/adapters/extension/actions.js +6 -2
- package/dist/adapters/extension/network-observer.js +13 -0
- package/dist/adapters/extension/run.js +115 -31
- package/dist/adapters/mobile/actions.js +4 -1
- package/dist/adapters/mobile/network-observer.js +68 -0
- package/dist/adapters/mobile/performance-observer.js +1 -3
- package/dist/adapters/mobile/run.js +32 -9
- package/dist/adapters/perps-order-inputs.js +36 -0
- package/dist/adapters/plugin-surface.js +98 -0
- package/dist/adapters/proof-failure.js +11 -0
- package/dist/adapters/surface.js +1 -1
- package/dist/adapters/{terminal → web-dapp}/actions.js +11 -8
- package/dist/adapters/web-dapp/console-capture.js +63 -0
- package/dist/adapters/web-dapp/doctor-checks.js +33 -0
- package/dist/adapters/{terminal → web-dapp}/launch.js +21 -19
- package/dist/adapters/web-dapp/overlay.js +21 -0
- package/dist/adapters/web-dapp/readiness.js +10 -0
- package/dist/adapters/{terminal → web-dapp}/run.js +22 -17
- package/dist/adapters/{terminal → web-dapp}/stop.js +5 -4
- package/dist/adapters/web-dapp/surface.js +149 -0
- package/dist/adapters.js +20 -4
- package/dist/cli.js +34 -6
- package/dist/command-contract.js +29 -7
- package/dist/commands/check.js +32 -8
- package/dist/commands/completion-candidates.js +5 -5
- package/dist/commands/doctor.js +19 -2
- package/dist/commands/farmslot-ready.js +1 -0
- package/dist/commands/help.js +1 -1
- package/dist/commands/launch/device.js +5 -5
- package/dist/commands/self-test.js +10 -5
- package/dist/engine.js +47 -0
- package/dist/funding-execution-context.js +81 -0
- package/dist/manifest.js +48 -24
- package/dist/mm-harness-cli.js +20 -3
- package/dist/performance-observation.js +7 -15
- package/dist/recipe-assistance-context.js +6 -7
- package/dist/runner.js +28 -8
- package/docs/CONTRIBUTING.md +4 -2
- package/docs/RECIPES.md +31 -3
- package/docs/cli-contract.md +5 -3
- package/library/actions/core/perps/_controller.mjs +24 -28
- package/library/actions/core/perps/_rewards.mjs +186 -0
- package/library/actions/core/perps/close_positions.mjs +1 -1
- package/library/actions/core/perps/place_order.mjs +167 -45
- package/library/actions/core/perps/quote_fees.mjs +7 -4
- package/library/actions/core/wallet/list_accounts.mjs +1 -1
- package/library/actions/extension/perps/close_visible_position.mjs +5 -0
- package/library/actions/extension/perps/edit_margin.mjs +4 -0
- package/library/actions/extension/perps/mutation-receipt.mjs +95 -3
- package/library/actions/extension/perps/perps.mjs +320 -3
- package/library/actions/extension/perps/place_order.mjs +3 -1
- package/library/actions/extension/perps/update_position_tpsl.mjs +4 -0
- package/library/actions/extension/platform/cdp.mjs +71 -3
- package/library/actions/mobile/perps/perps.mjs +216 -11
- package/library/actions/mobile/perps/place_order.mjs +3 -1
- package/library/actions/shared/console-collector.mjs +1 -1
- package/library/actions/shared/perps/order-policy.mjs +177 -0
- package/library/actions/{core/wallet/_fixture-accounts.mjs → shared/wallet/fixture-accounts.mjs} +18 -3
- package/library/actions/{terminal → web-dapp}/app/assert_no_console_errors.mjs +4 -4
- package/library/actions/{terminal → web-dapp}/app/launch.mjs +8 -7
- package/library/actions/{terminal → web-dapp}/platform/hud.mjs +7 -6
- package/library/actions/{terminal → web-dapp}/platform/page.mjs +13 -12
- package/library/actions/{terminal → web-dapp}/platform/runtime.mjs +16 -12
- package/library/actions/web-dapp/ui/navigate.mjs +24 -0
- package/library/actions/web-dapp/wallet/_signature-log.mjs +32 -0
- package/library/actions/{terminal → web-dapp}/wallet/assert_signatures.mjs +8 -14
- package/library/actions/{terminal → web-dapp}/wallet/list_accounts.mjs +2 -2
- package/library/actions/web-dapp/wallet/read_signatures.mjs +38 -0
- package/library/manifests/core.action-manifest.json +6 -2
- package/library/manifests/extension.action-manifest.json +4 -0
- package/library/manifests/mobile.action-manifest.json +12 -0
- package/library/manifests/{terminal.action-manifest.json → web-dapp.action-manifest.json} +18 -702
- package/package.json +6 -6
- package/site/assets/help-recipes.json +1 -0
- package/adapters/terminal/lib/mainnet-guard.mjs +0 -177
- package/adapters/terminal/lib/origin.mjs +0 -30
- package/adapters/terminal/lib/page-script.mjs +0 -246
- package/adapters/terminal/lib/strict-wallet.mjs +0 -138
- package/dist/adapters/terminal/console-capture.js +0 -59
- package/dist/adapters/terminal/doctor-checks.js +0 -22
- package/dist/adapters/terminal/overlay.js +0 -18
- package/dist/adapters/terminal/readiness.js +0 -11
- package/dist/adapters/terminal/surface.js +0 -133
- package/dist/commands/call.js +0 -671
- package/dist/commands/list-executables.js +0 -245
- package/dist/commands/manifest.js +0 -477
- package/dist/commands/run-engine.js +0 -1301
- package/dist/commands/run.js +0 -730
- package/dist/discovery.js +0 -15
- package/dist/metamask-action-validation.js +0 -68
- package/dist/network-observation.js +0 -290
- package/library/actions/terminal/perps/_venue.mjs +0 -242
- package/library/actions/terminal/perps/assert_orders.mjs +0 -54
- package/library/actions/terminal/perps/assert_positions.mjs +0 -40
- package/library/actions/terminal/perps/read_orders.mjs +0 -26
- package/library/actions/terminal/perps/read_positions.mjs +0 -25
- package/library/actions/terminal/perps/read_snapshot.mjs +0 -40
- package/library/actions/terminal/perps/revoke_agent.mjs +0 -58
- package/library/actions/terminal/perps/teardown_state.mjs +0 -104
- package/library/actions/terminal/ui/navigate.mjs +0 -31
- package/library/actions/terminal/wallet/_signature-log.mjs +0 -177
- package/library/actions/terminal/wallet/read_signatures.mjs +0 -37
package/dist/discovery.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { assessRecipe } from "@farmslot/recipe-cli";
|
|
2
|
-
import {
|
|
3
|
-
loadRecipeLibraries
|
|
4
|
-
} from "@farmslot/recipe-runner";
|
|
5
|
-
import { resolveActionManifest } from "./manifest.js";
|
|
6
|
-
async function runnableLibraryRecipes(adapter, sources) {
|
|
7
|
-
const resolution = await loadRecipeLibraries(sources, { adapter });
|
|
8
|
-
const { manifest } = await resolveActionManifest(adapter, void 0, sources);
|
|
9
|
-
return [...resolution.recipes.values()].filter(
|
|
10
|
-
(recipe) => assessRecipe({ resolution, manifest }, recipe).length === 0
|
|
11
|
-
);
|
|
12
|
-
}
|
|
13
|
-
export {
|
|
14
|
-
runnableLibraryRecipes
|
|
15
|
-
};
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { adapterRegistry } from "./adapters/registry.js";
|
|
2
|
-
import { getAdapterSurface } from "./adapters/surface.js";
|
|
3
|
-
function isRecord(value) {
|
|
4
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
5
|
-
}
|
|
6
|
-
function resolveMetaMaskParameterValue(value, params) {
|
|
7
|
-
if (typeof value === "string") {
|
|
8
|
-
const exact = /^\{\{params\.([A-Za-z0-9_.-]+)\}\}$/u.exec(value);
|
|
9
|
-
if (!exact) return value;
|
|
10
|
-
let current = params;
|
|
11
|
-
for (const segment of exact[1].split(".")) {
|
|
12
|
-
if (!isRecord(current) || !Object.hasOwn(current, segment)) return value;
|
|
13
|
-
current = current[segment];
|
|
14
|
-
}
|
|
15
|
-
return current;
|
|
16
|
-
}
|
|
17
|
-
if (Array.isArray(value)) return value.map((entry) => resolveMetaMaskParameterValue(entry, params));
|
|
18
|
-
if (!isRecord(value)) return value;
|
|
19
|
-
return Object.fromEntries(
|
|
20
|
-
Object.entries(value).map(([key, entry]) => [key, resolveMetaMaskParameterValue(entry, params)])
|
|
21
|
-
);
|
|
22
|
-
}
|
|
23
|
-
function validateMetaMaskActionInputs(recipe, adapter, params) {
|
|
24
|
-
if (!isRecord(recipe) || !isRecord(recipe.workflow)) return [];
|
|
25
|
-
const nodes = isRecord(recipe.workflow.nodes) ? recipe.workflow.nodes : void 0;
|
|
26
|
-
if (!nodes) return [];
|
|
27
|
-
const surface = adapter && adapterRegistry.has(adapter) ? getAdapterSurface(adapter) : void 0;
|
|
28
|
-
const findings = [];
|
|
29
|
-
for (const [nodeId, rawNode] of Object.entries(nodes)) {
|
|
30
|
-
const node = params ? resolveMetaMaskParameterValue(rawNode, params) : rawNode;
|
|
31
|
-
if (!isRecord(node)) continue;
|
|
32
|
-
const action = node.action;
|
|
33
|
-
findings.push(...surface?.actions.inputFindings?.(nodeId, node) ?? []);
|
|
34
|
-
const state = typeof node.state === "string" ? node.state.toLowerCase() : void 0;
|
|
35
|
-
const createsOrder = action === "metamask.perps.place_order" || !surface?.actions.ensureChecksOnly && (action === "metamask.perps.ensure_positions" || action === "metamask.perps.ensure_orders") && (state === "open" || state === "present");
|
|
36
|
-
if (!createsOrder) continue;
|
|
37
|
-
const requireOneOf = (names, message) => {
|
|
38
|
-
const present = names.some((name) => {
|
|
39
|
-
const value = node[name];
|
|
40
|
-
return value !== void 0 && value !== null && value !== "";
|
|
41
|
-
});
|
|
42
|
-
if (present) return;
|
|
43
|
-
findings.push({
|
|
44
|
-
severity: "error",
|
|
45
|
-
code: "recipe.missing_param",
|
|
46
|
-
path: `workflow.nodes.${nodeId}.${names[0]}`,
|
|
47
|
-
message
|
|
48
|
-
});
|
|
49
|
-
};
|
|
50
|
-
requireOneOf(
|
|
51
|
-
["market", "symbol"],
|
|
52
|
-
`${action} requires market=<symbol> (or symbol=<symbol>) before it may create an order`
|
|
53
|
-
);
|
|
54
|
-
requireOneOf(
|
|
55
|
-
["side"],
|
|
56
|
-
`${action} requires side=long or side=short before it may create an order`
|
|
57
|
-
);
|
|
58
|
-
requireOneOf(
|
|
59
|
-
["amount", "notional"],
|
|
60
|
-
`${action} requires amount=<usd> or notional=<usd> before it may create an order`
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
return findings;
|
|
64
|
-
}
|
|
65
|
-
export {
|
|
66
|
-
resolveMetaMaskParameterValue,
|
|
67
|
-
validateMetaMaskActionInputs
|
|
68
|
-
};
|
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import brokerModule from "@farmslot/recipe-runner/cdp-broker";
|
|
3
|
-
import configModule from "@farmslot/adapter-rn/bridge-runtime/lib/config.cjs";
|
|
4
|
-
import {
|
|
5
|
-
createExtensionNetworkObserver
|
|
6
|
-
} from "./adapters/extension/network-observer.js";
|
|
7
|
-
import { getAdapterSurface } from "./adapters/surface.js";
|
|
8
|
-
import {
|
|
9
|
-
indexArtifactManifest,
|
|
10
|
-
writeContainedArtifact
|
|
11
|
-
} from "@farmslot/recipe-cli/harness";
|
|
12
|
-
const AUTO_CAPTURE_ID = "run-network";
|
|
13
|
-
const AUTO_ARTIFACT_PATH = "network/run-summary.json";
|
|
14
|
-
const { brokerSocketPath, createBrokerClient } = brokerModule;
|
|
15
|
-
const { resolvePort } = configModule;
|
|
16
|
-
const sessions = /* @__PURE__ */ new Map();
|
|
17
|
-
async function startRunNetworkObservation(adapter, target, artifactsDir, env, ports = {}) {
|
|
18
|
-
const network = getAdapterSurface(adapter).observation?.network;
|
|
19
|
-
if (!network) return void 0;
|
|
20
|
-
const runtimeEnv = {
|
|
21
|
-
...env,
|
|
22
|
-
...ports.cdpPort ? { CDP_PORT: ports.cdpPort, RECIPE_CDP_PORT: ports.cdpPort } : {},
|
|
23
|
-
...ports.watcherPort ? { WATCHER_PORT: ports.watcherPort } : {}
|
|
24
|
-
};
|
|
25
|
-
const key = path.resolve(artifactsDir);
|
|
26
|
-
const session = {
|
|
27
|
-
artifactsDir: key,
|
|
28
|
-
autoStarted: false,
|
|
29
|
-
autoStartedAt: Date.now(),
|
|
30
|
-
nodeEvents: []
|
|
31
|
-
};
|
|
32
|
-
sessions.set(key, session);
|
|
33
|
-
try {
|
|
34
|
-
session.backend = await network.backend(target, runtimeEnv, key);
|
|
35
|
-
} catch (error) {
|
|
36
|
-
session.setupError = boundedError(error);
|
|
37
|
-
}
|
|
38
|
-
if (runtimeEnv.MM_HARNESS_AUTO_NETWORK_CAPTURE !== "0") {
|
|
39
|
-
session.autoStartedAt = Date.now();
|
|
40
|
-
if (session.backend) {
|
|
41
|
-
try {
|
|
42
|
-
await session.backend.start({
|
|
43
|
-
id: AUTO_CAPTURE_ID,
|
|
44
|
-
bodyJsonFields: ["type"],
|
|
45
|
-
maxDurationMs: 60 * 60 * 1e3,
|
|
46
|
-
maxRequests: 1e4,
|
|
47
|
-
methods: [],
|
|
48
|
-
urlIncludes: []
|
|
49
|
-
});
|
|
50
|
-
session.autoStarted = true;
|
|
51
|
-
} catch (error) {
|
|
52
|
-
session.setupError = boundedError(error);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return {
|
|
57
|
-
onActionEvent(event) {
|
|
58
|
-
session.nodeEvents.push({ ...event, epochMs: Date.now() });
|
|
59
|
-
},
|
|
60
|
-
async finalize(artifactManifestPath) {
|
|
61
|
-
try {
|
|
62
|
-
if (runtimeEnv.MM_HARNESS_AUTO_NETWORK_CAPTURE !== "0") {
|
|
63
|
-
const summary = await automaticSummary(session);
|
|
64
|
-
await writeSummary(key, AUTO_ARTIFACT_PATH, summary);
|
|
65
|
-
if (artifactManifestPath) {
|
|
66
|
-
await indexArtifact(
|
|
67
|
-
artifactManifestPath,
|
|
68
|
-
AUTO_ARTIFACT_PATH,
|
|
69
|
-
"Automatic network observation"
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
} finally {
|
|
74
|
-
sessions.delete(key);
|
|
75
|
-
await session.backend?.close();
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
async function handleRunNetworkAction(platform, action, node, context) {
|
|
81
|
-
if (!getAdapterSurface(platform).observation?.network?.actions) return null;
|
|
82
|
-
if (action === "app.network_assert") {
|
|
83
|
-
const assertion = await import("../library/actions/shared/app/network-assert.mjs");
|
|
84
|
-
return {
|
|
85
|
-
output: await assertion.assertNetwork({ action, node, context })
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
if (action !== "app.network_capture") return null;
|
|
89
|
-
const phase = String(node.phase ?? "").toLowerCase();
|
|
90
|
-
const id = String(node.id ?? "").trim();
|
|
91
|
-
if (!["start", "end"].includes(phase) || !id) {
|
|
92
|
-
throw new Error(
|
|
93
|
-
"app.network_capture requires phase=start|end and a non-empty id."
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
const session = sessions.get(path.resolve(context.artifactsDir));
|
|
97
|
-
if (!session?.backend) {
|
|
98
|
-
throw new Error(
|
|
99
|
-
`Extension network observation is unavailable: ${session?.setupError ?? "run observer was not started"}.`
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
if (phase === "start") {
|
|
103
|
-
const result = await session.backend.start(captureParams(id, node));
|
|
104
|
-
return {
|
|
105
|
-
output: {
|
|
106
|
-
action,
|
|
107
|
-
phase,
|
|
108
|
-
...asRecord(result)
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
const artifactPath = String(
|
|
113
|
-
node.artifact_path ?? `network/${id}-summary.json`
|
|
114
|
-
);
|
|
115
|
-
const summary = await session.backend.end(id);
|
|
116
|
-
await writeSummary(context.artifactsDir, artifactPath, summary);
|
|
117
|
-
return {
|
|
118
|
-
output: {
|
|
119
|
-
action,
|
|
120
|
-
phase,
|
|
121
|
-
...summary
|
|
122
|
-
},
|
|
123
|
-
artifacts: [
|
|
124
|
-
{
|
|
125
|
-
path: artifactPath,
|
|
126
|
-
type: "report",
|
|
127
|
-
nodeId: context.nodeId
|
|
128
|
-
}
|
|
129
|
-
]
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
async function createMobileNetworkBackend(target, env) {
|
|
133
|
-
const { client } = await connectMobileBroker(target, env);
|
|
134
|
-
return {
|
|
135
|
-
start(params) {
|
|
136
|
-
return client.control("capture-start", params, 1e4);
|
|
137
|
-
},
|
|
138
|
-
async end(id) {
|
|
139
|
-
return asRecord(await client.control("capture-end", { id }, 1e4));
|
|
140
|
-
},
|
|
141
|
-
async close() {
|
|
142
|
-
client.close();
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
async function connectMobileBroker(target, env) {
|
|
147
|
-
const runtimeDir = env.RECIPE_RUNTIME_DIR ? path.resolve(target, env.RECIPE_RUNTIME_DIR) : path.join(target, "temp", "recipe", "runtime");
|
|
148
|
-
const socketPath = brokerSocketPath(
|
|
149
|
-
runtimeDir,
|
|
150
|
-
resolvePort(env, target)
|
|
151
|
-
);
|
|
152
|
-
const discoveryClient = await createBrokerClient(socketPath, "", 1e4);
|
|
153
|
-
let targets;
|
|
154
|
-
try {
|
|
155
|
-
targets = await discoveryClient.control(
|
|
156
|
-
"resolve-targets",
|
|
157
|
-
{ nameIncludes: mobileTargetPin(env) },
|
|
158
|
-
1e4
|
|
159
|
-
);
|
|
160
|
-
} finally {
|
|
161
|
-
discoveryClient.close();
|
|
162
|
-
}
|
|
163
|
-
const deviceId = selectMobileBrokerTarget(targets, env);
|
|
164
|
-
const client = await createBrokerClient(socketPath, deviceId, 1e4);
|
|
165
|
-
return { client, deviceId };
|
|
166
|
-
}
|
|
167
|
-
function selectMobileBrokerTarget(value, env) {
|
|
168
|
-
const targets = Array.isArray(value) ? value.map(asRecord).filter((target) => target.deviceId) : [];
|
|
169
|
-
const pin = mobileTargetPin(env);
|
|
170
|
-
const candidates = pin ? targets.filter(
|
|
171
|
-
(target) => String(target.name ?? "").toLowerCase().includes(pin.toLowerCase())
|
|
172
|
-
) : targets;
|
|
173
|
-
if (candidates.length !== 1) {
|
|
174
|
-
throw new Error(
|
|
175
|
-
`Mobile network observation requires one broker target; found ${candidates.length}.`
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
return String(candidates[0].deviceId);
|
|
179
|
-
}
|
|
180
|
-
function mobileTargetPin(env) {
|
|
181
|
-
const platform = String(
|
|
182
|
-
env.RECIPE_RN_EXPLICIT_PLATFORM ?? env.RECIPE_HARNESS_PLATFORM ?? ""
|
|
183
|
-
).toLowerCase();
|
|
184
|
-
return String(
|
|
185
|
-
platform === "android" ? env.ANDROID_TARGET_DEVICE_NAME ?? env.ANDROID_DEVICE ?? "" : platform === "ios" ? env.IOS_SIMULATOR ?? "" : env.IOS_SIMULATOR ?? env.ANDROID_TARGET_DEVICE_NAME ?? env.ANDROID_DEVICE ?? ""
|
|
186
|
-
).trim();
|
|
187
|
-
}
|
|
188
|
-
async function automaticSummary(session) {
|
|
189
|
-
let summary;
|
|
190
|
-
if (session.autoStarted && session.backend) {
|
|
191
|
-
try {
|
|
192
|
-
summary = await session.backend.end(AUTO_CAPTURE_ID);
|
|
193
|
-
} catch (error) {
|
|
194
|
-
summary = unavailableSummary(session.autoStartedAt, boundedError(error));
|
|
195
|
-
}
|
|
196
|
-
} else {
|
|
197
|
-
summary = unavailableSummary(
|
|
198
|
-
session.autoStartedAt,
|
|
199
|
-
session.setupError ?? "Network observer did not start."
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
const startedAt = Number(summary.startedAtEpochMs ?? session.autoStartedAt);
|
|
203
|
-
return {
|
|
204
|
-
...summary,
|
|
205
|
-
nodeEvents: session.nodeEvents.map((event) => ({
|
|
206
|
-
action: event.action,
|
|
207
|
-
elapsedMs: Math.max(0, event.epochMs - startedAt),
|
|
208
|
-
nodeId: event.nodeId,
|
|
209
|
-
status: event.status
|
|
210
|
-
}))
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
function unavailableSummary(startedAtEpochMs, reason) {
|
|
214
|
-
return {
|
|
215
|
-
schemaVersion: 1,
|
|
216
|
-
id: AUTO_CAPTURE_ID,
|
|
217
|
-
status: "unavailable",
|
|
218
|
-
startedAtEpochMs,
|
|
219
|
-
endedAtEpochMs: Date.now(),
|
|
220
|
-
maxDurationMs: 60 * 60 * 1e3,
|
|
221
|
-
reconnects: 0,
|
|
222
|
-
droppedRequests: 0,
|
|
223
|
-
unavailableReasons: [reason],
|
|
224
|
-
coverageGapReasons: [],
|
|
225
|
-
uninspectableBodyRequests: 0,
|
|
226
|
-
projectedBodyFields: ["type"],
|
|
227
|
-
totalRequests: 0,
|
|
228
|
-
requestsByMethod: {},
|
|
229
|
-
requestsByHost: {},
|
|
230
|
-
requestsByType: {},
|
|
231
|
-
requests: []
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
function captureParams(id, node) {
|
|
235
|
-
return {
|
|
236
|
-
id,
|
|
237
|
-
urlIncludes: node.url_includes ?? [],
|
|
238
|
-
methods: node.methods ?? [],
|
|
239
|
-
bodyJsonFields: node.body_json_fields ?? [],
|
|
240
|
-
maxRequests: node.max_requests,
|
|
241
|
-
maxDurationMs: node.max_duration_ms
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
async function writeSummary(artifactsDir, relativePath, summary) {
|
|
245
|
-
await writeContainedArtifact(
|
|
246
|
-
artifactsDir,
|
|
247
|
-
relativePath,
|
|
248
|
-
`${JSON.stringify(summary, null, 2)}
|
|
249
|
-
`,
|
|
250
|
-
"Network artifact"
|
|
251
|
-
);
|
|
252
|
-
}
|
|
253
|
-
async function indexArtifact(artifactManifestPath, relativePath, label) {
|
|
254
|
-
await indexArtifactManifest(
|
|
255
|
-
artifactManifestPath,
|
|
256
|
-
[
|
|
257
|
-
{
|
|
258
|
-
path: relativePath,
|
|
259
|
-
type: "report",
|
|
260
|
-
label,
|
|
261
|
-
category: "diagnostic"
|
|
262
|
-
}
|
|
263
|
-
]
|
|
264
|
-
);
|
|
265
|
-
}
|
|
266
|
-
async function createExtensionNetworkBackend(env, artifactsDir) {
|
|
267
|
-
return createExtensionNetworkObserver(extensionCdpPort(env), artifactsDir);
|
|
268
|
-
}
|
|
269
|
-
function extensionCdpPort(env) {
|
|
270
|
-
const port = Number(
|
|
271
|
-
env.CDP_PORT ?? env.RECIPE_CDP_PORT ?? process.env.CDP_PORT
|
|
272
|
-
);
|
|
273
|
-
if (!Number.isInteger(port) || port <= 0) {
|
|
274
|
-
throw new Error("Extension network observation requires CDP_PORT.");
|
|
275
|
-
}
|
|
276
|
-
return port;
|
|
277
|
-
}
|
|
278
|
-
function boundedError(error) {
|
|
279
|
-
return String(error instanceof Error ? error.message : error).slice(0, 256);
|
|
280
|
-
}
|
|
281
|
-
function asRecord(value) {
|
|
282
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
283
|
-
}
|
|
284
|
-
export {
|
|
285
|
-
connectMobileBroker,
|
|
286
|
-
createExtensionNetworkBackend,
|
|
287
|
-
createMobileNetworkBackend,
|
|
288
|
-
handleRunNetworkAction,
|
|
289
|
-
startRunNetworkObservation
|
|
290
|
-
};
|
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
// Read and clean up Hyperliquid venue state for a terminal fixture account.
|
|
2
|
-
// Reads use the public info API. Writes (cleanup, agent revoke) are testnet-only,
|
|
3
|
-
// signed in this process by the fixture account through the target checkout's
|
|
4
|
-
// own @nktkas/hyperliquid, and never pass through the browser wallet.
|
|
5
|
-
|
|
6
|
-
import { createRequire } from 'node:module';
|
|
7
|
-
import path from 'node:path';
|
|
8
|
-
import { pathToFileURL } from 'node:url';
|
|
9
|
-
|
|
10
|
-
export const INFO_URLS = Object.freeze({
|
|
11
|
-
testnet: 'https://api.hyperliquid-testnet.xyz/info',
|
|
12
|
-
mainnet: 'https://api.hyperliquid.xyz/info',
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
// Agents the Web Terminal approves are named "mm-terminal" plus an optional
|
|
16
|
-
// " valid_until <ms>" expiry suffix.
|
|
17
|
-
export const TERMINAL_AGENT_PREFIX = 'mm-terminal';
|
|
18
|
-
|
|
19
|
-
export function resolveNetwork(node = {}) {
|
|
20
|
-
const network = node.network == null ? 'testnet' : String(node.network);
|
|
21
|
-
if (network !== 'testnet' && network !== 'mainnet') {
|
|
22
|
-
throw new Error(`network must be testnet or mainnet, got ${JSON.stringify(node.network)}.`);
|
|
23
|
-
}
|
|
24
|
-
return network;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export const MAINNET_CONFIRMATION = 'REAL_FUNDS';
|
|
28
|
-
|
|
29
|
-
// Venue mutations default to testnet. Mainnet needs the explicit
|
|
30
|
-
// mainnet_confirmation=REAL_FUNDS on the node, as every library mutation does.
|
|
31
|
-
export function requireTestnet(node = {}, action = 'this action') {
|
|
32
|
-
const network = resolveNetwork(node);
|
|
33
|
-
if (network === 'mainnet' && node.mainnet_confirmation === MAINNET_CONFIRMATION) return network;
|
|
34
|
-
if (network !== 'testnet') {
|
|
35
|
-
throw new Error(
|
|
36
|
-
`${action} mutates venue state; refusing network=${network} without mainnet_confirmation=${MAINNET_CONFIRMATION}.`,
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
return network;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export async function info(network, body, fetchImpl = globalThis.fetch) {
|
|
43
|
-
const response = await fetchImpl(INFO_URLS[network], {
|
|
44
|
-
method: 'POST',
|
|
45
|
-
headers: { 'content-type': 'application/json' },
|
|
46
|
-
body: JSON.stringify(body),
|
|
47
|
-
});
|
|
48
|
-
if (!response.ok) {
|
|
49
|
-
throw new Error(`Hyperliquid ${network} info ${body.type} returned HTTP ${response.status}.`);
|
|
50
|
-
}
|
|
51
|
-
return response.json();
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function normalizePosition(entry) {
|
|
55
|
-
const position = entry?.position ?? entry;
|
|
56
|
-
const size = Number(position.szi);
|
|
57
|
-
return {
|
|
58
|
-
market: String(position.coin).toUpperCase(),
|
|
59
|
-
side: size >= 0 ? 'long' : 'short',
|
|
60
|
-
size: String(Math.abs(size)),
|
|
61
|
-
entryPrice: position.entryPx ?? null,
|
|
62
|
-
// Numeric entry notional in USD, for cap assertions.
|
|
63
|
-
notional: position.entryPx == null ? null : Number((Math.abs(size) * Number(position.entryPx)).toFixed(2)),
|
|
64
|
-
leverage: position.leverage?.value ?? null,
|
|
65
|
-
marginMode: position.leverage?.type ?? null,
|
|
66
|
-
liquidationPrice: position.liquidationPx ?? null,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const TRIGGER_TYPES = {
|
|
71
|
-
'Stop Market': 'stop_market',
|
|
72
|
-
'Stop Limit': 'stop_limit',
|
|
73
|
-
'Take Profit Market': 'take_profit_market',
|
|
74
|
-
'Take Profit Limit': 'take_profit_limit',
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export function normalizeOrder(order) {
|
|
78
|
-
return {
|
|
79
|
-
orderId: String(order.oid),
|
|
80
|
-
market: String(order.coin).toUpperCase(),
|
|
81
|
-
side: order.side === 'B' ? 'buy' : 'sell',
|
|
82
|
-
price: order.limitPx ?? null,
|
|
83
|
-
size: order.sz ?? null,
|
|
84
|
-
orderType: order.orderType ?? null,
|
|
85
|
-
triggerType: TRIGGER_TYPES[order.orderType] ?? null,
|
|
86
|
-
isTrigger: Boolean(order.isTrigger),
|
|
87
|
-
triggerPrice: order.isTrigger ? order.triggerPx ?? null : null,
|
|
88
|
-
reduceOnly: Boolean(order.reduceOnly),
|
|
89
|
-
isPositionTpsl: Boolean(order.isPositionTpsl),
|
|
90
|
-
timestamp: order.timestamp ?? null,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Only a public EVM address ever leaves this process for the info API.
|
|
95
|
-
export function venueUser(address) {
|
|
96
|
-
const value = String(address ?? '');
|
|
97
|
-
if (!/^0x[0-9a-fA-F]{40}$/u.test(value)) throw new Error('venue reads need a 0x-prefixed 20-byte account address.');
|
|
98
|
-
return `0x${value.slice(2).toLowerCase()}`;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export async function readVenueState({ network, user: rawUser, fetchImpl }) {
|
|
102
|
-
const user = venueUser(rawUser);
|
|
103
|
-
const [clearinghouse, openOrders, agents] = await Promise.all([
|
|
104
|
-
info(network, { type: 'clearinghouseState', user }, fetchImpl),
|
|
105
|
-
info(network, { type: 'frontendOpenOrders', user }, fetchImpl),
|
|
106
|
-
info(network, { type: 'extraAgents', user }, fetchImpl),
|
|
107
|
-
]);
|
|
108
|
-
return {
|
|
109
|
-
network,
|
|
110
|
-
user,
|
|
111
|
-
positions: (clearinghouse.assetPositions ?? []).map(normalizePosition),
|
|
112
|
-
orders: (openOrders ?? []).map(normalizeOrder),
|
|
113
|
-
agents: (agents ?? []).map((agent) => ({
|
|
114
|
-
name: agent.name,
|
|
115
|
-
address: agent.address,
|
|
116
|
-
validUntil: agent.validUntil ?? null,
|
|
117
|
-
})),
|
|
118
|
-
accountValue: clearinghouse.marginSummary?.accountValue ?? null,
|
|
119
|
-
withdrawable: clearinghouse.withdrawable ?? null,
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Market selection mirrors the core adapter: market/symbol or markets/symbols,
|
|
124
|
-
// with mode=all selecting every item. Positions match side long|short, orders
|
|
125
|
-
// match the side they would open (buy=long, sell=short).
|
|
126
|
-
export function requestedMarkets(node = {}) {
|
|
127
|
-
const selector = node.selector && typeof node.selector === 'object' ? node.selector : {};
|
|
128
|
-
if ((node.mode ?? selector.mode) === 'all') return null;
|
|
129
|
-
const list = [
|
|
130
|
-
node.market,
|
|
131
|
-
node.symbol,
|
|
132
|
-
selector.market,
|
|
133
|
-
selector.symbol,
|
|
134
|
-
...(Array.isArray(node.markets) ? node.markets : []),
|
|
135
|
-
...(Array.isArray(node.symbols) ? node.symbols : []),
|
|
136
|
-
...(Array.isArray(selector.markets) ? selector.markets : []),
|
|
137
|
-
...(Array.isArray(selector.symbols) ? selector.symbols : []),
|
|
138
|
-
]
|
|
139
|
-
.filter((value) => typeof value === 'string' && value.trim())
|
|
140
|
-
.map((value) => value.trim().toUpperCase());
|
|
141
|
-
if (list.length === 0) {
|
|
142
|
-
throw new Error('Select a market (market, markets) or pass mode=all; refusing an implicit account-wide selection.');
|
|
143
|
-
}
|
|
144
|
-
return new Set(list);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export function selectItems(node, items, kind) {
|
|
148
|
-
const markets = requestedMarkets(node);
|
|
149
|
-
const rawSide = node.side ?? node.selector?.side;
|
|
150
|
-
const side = rawSide == null ? null : String(rawSide).toLowerCase();
|
|
151
|
-
return items.filter((item) => {
|
|
152
|
-
if (markets && !markets.has(item.market)) return false;
|
|
153
|
-
if (!side) return true;
|
|
154
|
-
if (kind === 'position') return item.side === side;
|
|
155
|
-
return (item.side === 'buy' ? 'long' : 'short') === side;
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export function expectedOpen(node, action) {
|
|
160
|
-
if (node.state == null) throw new Error(`${action} requires state=open or state=none.`);
|
|
161
|
-
const state = String(node.state).toLowerCase();
|
|
162
|
-
if (state === 'open' || state === 'present') return true;
|
|
163
|
-
if (state === 'none' || state === 'closed' || state === 'absent') return false;
|
|
164
|
-
throw new Error(`${action} received unsupported state: ${state}`);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function sameNumber(actual, expected, tolerancePct = 0) {
|
|
168
|
-
const a = Number(actual);
|
|
169
|
-
const e = Number(expected);
|
|
170
|
-
if (!Number.isFinite(a) || !Number.isFinite(e)) return false;
|
|
171
|
-
if (tolerancePct <= 0) return a === e;
|
|
172
|
-
return Math.abs(a - e) <= Math.abs(e) * (tolerancePct / 100);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Order expectations beyond presence; each failing order is reported.
|
|
176
|
-
export function orderExpectationFailures(node, orders) {
|
|
177
|
-
const failures = [];
|
|
178
|
-
const tolerance = Number(node.price_tolerance_pct ?? 0);
|
|
179
|
-
for (const order of orders) {
|
|
180
|
-
if (node.expect_limit_price != null && !sameNumber(order.price, node.expect_limit_price, tolerance)) {
|
|
181
|
-
failures.push(`order ${order.orderId} limit price ${order.price} != ${node.expect_limit_price}`);
|
|
182
|
-
}
|
|
183
|
-
if (node.expect_trigger_price != null && !sameNumber(order.triggerPrice, node.expect_trigger_price, tolerance)) {
|
|
184
|
-
failures.push(`order ${order.orderId} trigger price ${order.triggerPrice} != ${node.expect_trigger_price}`);
|
|
185
|
-
}
|
|
186
|
-
if (node.expect_trigger_order_type != null && order.triggerType !== node.expect_trigger_order_type) {
|
|
187
|
-
failures.push(`order ${order.orderId} trigger type ${order.triggerType} != ${node.expect_trigger_order_type}`);
|
|
188
|
-
}
|
|
189
|
-
if (node.expect_reduce_only != null && order.reduceOnly !== Boolean(node.expect_reduce_only)) {
|
|
190
|
-
failures.push(`order ${order.orderId} reduceOnly ${order.reduceOnly} != ${node.expect_reduce_only}`);
|
|
191
|
-
}
|
|
192
|
-
if (node.expect_size != null && !sameNumber(order.size, node.expect_size)) {
|
|
193
|
-
failures.push(`order ${order.orderId} size ${order.size} != ${node.expect_size}`);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
return failures;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
export function filterOrderKind(node, orders) {
|
|
200
|
-
let selected = orders;
|
|
201
|
-
if (node.only_trigger_orders === true || node.order_kind === 'trigger') selected = selected.filter((order) => order.isTrigger);
|
|
202
|
-
if (node.order_kind === 'limit') selected = selected.filter((order) => !order.isTrigger);
|
|
203
|
-
if (node.trigger_kind === 'take_profit') selected = selected.filter((order) => order.triggerType?.startsWith('take_profit'));
|
|
204
|
-
if (node.trigger_kind === 'stop_loss') selected = selected.filter((order) => order.triggerType?.startsWith('stop'));
|
|
205
|
-
if (Array.isArray(node.order_ids) && node.order_ids.length > 0) {
|
|
206
|
-
const ids = new Set(node.order_ids.map(String));
|
|
207
|
-
selected = selected.filter((order) => ids.has(order.orderId));
|
|
208
|
-
}
|
|
209
|
-
return selected;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
export async function pollUntil(read, done, timeoutMs, intervalMs = 750) {
|
|
213
|
-
const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0);
|
|
214
|
-
let value = await read();
|
|
215
|
-
while (!done(value) && Date.now() < deadline) {
|
|
216
|
-
await new Promise((resolve) => setTimeout(resolve, Math.min(intervalMs, Math.max(1, deadline - Date.now()))));
|
|
217
|
-
value = await read();
|
|
218
|
-
}
|
|
219
|
-
return value;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// The terminal checkout ships the SDK it trades with; use that exact copy.
|
|
223
|
-
export async function loadTargetHyperliquid(projectRoot) {
|
|
224
|
-
const require = createRequire(path.join(projectRoot, 'package.json'));
|
|
225
|
-
let entry;
|
|
226
|
-
try {
|
|
227
|
-
entry = require.resolve('@nktkas/hyperliquid');
|
|
228
|
-
} catch {
|
|
229
|
-
throw new Error(
|
|
230
|
-
`@nktkas/hyperliquid is not installed in ${projectRoot}.\nNext: npm ci in the terminal checkout.`,
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
return import(pathToFileURL(entry).href);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Hyperliquid prices: at most 5 significant figures and (6 - szDecimals)
|
|
237
|
-
// decimals for perps.
|
|
238
|
-
export function venuePrice(raw, szDecimals) {
|
|
239
|
-
const decimals = Math.max(0, 6 - szDecimals);
|
|
240
|
-
const significant = Number(Number(raw).toPrecision(5));
|
|
241
|
-
return String(Number(significant.toFixed(decimals)));
|
|
242
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
// Assert presence, absence, count and field expectations (limit/trigger price,
|
|
2
|
-
// trigger type, reduce-only, size) of the selected venue open orders.
|
|
3
|
-
|
|
4
|
-
import { isDirectRun, resolveAccountAddress, runAdapter } from '../platform/runtime.mjs';
|
|
5
|
-
import {
|
|
6
|
-
expectedOpen,
|
|
7
|
-
filterOrderKind,
|
|
8
|
-
orderExpectationFailures,
|
|
9
|
-
pollUntil,
|
|
10
|
-
readVenueState,
|
|
11
|
-
resolveNetwork,
|
|
12
|
-
selectItems,
|
|
13
|
-
} from './_venue.mjs';
|
|
14
|
-
|
|
15
|
-
export async function assertOrders(input, deps = {}) {
|
|
16
|
-
const node = input.node ?? {};
|
|
17
|
-
const expectOpen = expectedOpen(node, 'metamask.perps.assert_orders');
|
|
18
|
-
const network = resolveNetwork(node);
|
|
19
|
-
const { name, address } = await resolveAccountAddress(input);
|
|
20
|
-
const expectCount = node.expect_count ?? node.expectCount;
|
|
21
|
-
const reached = (matching) => {
|
|
22
|
-
if (!expectOpen) return matching.length === 0;
|
|
23
|
-
const countOk = expectCount == null ? matching.length > 0 : matching.length === Number(expectCount);
|
|
24
|
-
return countOk && orderExpectationFailures(node, matching).length === 0;
|
|
25
|
-
};
|
|
26
|
-
const matching = await pollUntil(
|
|
27
|
-
async () => filterOrderKind(node, selectItems(node, (await readVenueState({ network, user: address, fetchImpl: deps.fetchImpl })).orders, 'order')),
|
|
28
|
-
reached,
|
|
29
|
-
node.timeout_ms,
|
|
30
|
-
);
|
|
31
|
-
if (!reached(matching)) {
|
|
32
|
-
const failures = expectOpen ? orderExpectationFailures(node, matching) : [];
|
|
33
|
-
const wanted = !expectOpen ? 'no' : expectCount == null ? 'at least one' : `exactly ${expectCount}`;
|
|
34
|
-
throw new Error(
|
|
35
|
-
`Expected ${wanted} matching venue order(s), found ${matching.length}` +
|
|
36
|
-
`${failures.length ? `; ${failures.join('; ')}` : ''}: ${JSON.stringify(matching)}`,
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
return {
|
|
40
|
-
action: input.action,
|
|
41
|
-
source: 'hyperliquid-info-frontendOpenOrders',
|
|
42
|
-
network,
|
|
43
|
-
account: address,
|
|
44
|
-
accountName: name,
|
|
45
|
-
expectedOpen: expectOpen,
|
|
46
|
-
matchingCount: matching.length,
|
|
47
|
-
expectedCount: expectCount == null ? null : Number(expectCount),
|
|
48
|
-
orders: matching,
|
|
49
|
-
orderIds: matching.map((order) => order.orderId),
|
|
50
|
-
proofPath: 'venue-frontendOpenOrders',
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (isDirectRun(import.meta.url)) runAdapter((input) => assertOrders(input));
|