@deeeed/metamask-harness 0.86.2 → 0.88.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 +49 -0
- package/adapters/extension/ensure-browser.sh +6 -10
- package/adapters/extension/inject.mjs +1 -0
- package/adapters/extension/launch-browser.cjs +31 -4
- package/adapters/extension/profile-extension-state.cjs +156 -0
- package/adapters/extension/readiness.mjs +21 -2
- package/adapters/manifest.json +8 -0
- package/adapters/mobile/wait-for-bridge.cjs +9 -3
- package/dist/adapters/extension/actions.js +2 -1
- package/dist/adapters/extension/run.js +6 -0
- package/dist/adapters/mobile/action-inputs.js +20 -0
- package/dist/adapters/mobile/actions.js +18 -1
- package/dist/adapters/mobile/android-screenrecord.js +274 -0
- package/dist/adapters/mobile/readiness.js +2 -1
- package/dist/adapters/mobile/recording-outcome.js +54 -0
- package/dist/adapters/mobile/recording-target.js +11 -0
- package/dist/adapters/mobile/run.js +58 -15
- package/dist/adapters/mobile/ui.js +96 -2
- package/dist/adapters/perps-mainnet-cleanup.js +122 -0
- package/dist/adapters/perps-order-inputs.js +43 -28
- package/dist/adapters.js +4 -1
- package/dist/commands/doctor.js +33 -14
- package/dist/commands/status-probe.js +2 -1
- package/dist/engine.js +11 -2
- package/dist/funding-execution-context.js +13 -8
- package/dist/host.js +6 -2
- package/dist/index.js +3 -0
- package/dist/mm-harness-cli.js +4 -2
- package/dist/provider.js +72 -0
- package/dist/recipe-security.js +1 -0
- package/docs/CONTRIBUTING.md +7 -0
- package/docs/RECIPES.md +96 -9
- package/docs/RELEASE-QA-CAPABILITY-MAP.md +5 -1
- package/docs/cli-contract.md +1 -1
- package/library/actions/core/perps/read_account.mjs +4 -2
- package/library/actions/core/perps/read_orders.mjs +4 -2
- package/library/actions/core/perps/read_positions.mjs +4 -2
- package/library/actions/extension/analytics/consent.mjs +7 -2
- package/library/actions/extension/perps/compare_provider_market.mjs +12 -8
- package/library/actions/extension/perps/mutation-receipt.mjs +124 -26
- package/library/actions/extension/perps/perps.mjs +298 -39
- package/library/actions/extension/perps/read_snapshot.mjs +6 -2
- package/library/actions/extension/perps/set_network.mjs +6 -0
- package/library/actions/extension/platform/cdp.mjs +6 -2
- package/library/actions/extension/ui/navigate.mjs +8 -0
- package/library/actions/mobile/perps/perps.mjs +264 -102
- package/library/actions/mobile/perps/place_order.mjs +7 -4
- package/library/actions/mobile/platform/bridge.mjs +171 -10
- package/library/actions/mobile/wallet/home.mjs +7 -2
- package/library/actions/mobile/wallet/select_account.mjs +4 -0
- package/library/actions/shared/app/live-adapter-deadline.mjs +8 -0
- package/library/actions/shared/perps/mainnet-order-ledger.mjs +378 -0
- package/library/actions/shared/perps/order-policy.mjs +56 -19
- package/library/actions/shared/perps/rate-limit.mjs +93 -0
- package/library/actions/shared/perps/visible-state.mjs +3 -2
- package/library/actions/shared/wallet/fixture-accounts.mjs +9 -3
- package/library/manifests/extension.action-manifest.json +36 -5
- package/library/manifests/mobile.action-manifest.json +48 -10
- package/package.json +14 -9
- package/scripts/validate-release-report.mjs +14 -2
- package/site/assets/help-recipes.json +2 -1
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
VENUE_ORDER_FIELDS,
|
|
3
|
+
cleanupOwns,
|
|
4
|
+
clearMainnetSwitch,
|
|
5
|
+
ledgerOrderTargets,
|
|
6
|
+
mainnetOrderLeftOpenError,
|
|
7
|
+
markRunAborting,
|
|
8
|
+
readHyperliquidVenue,
|
|
9
|
+
readMainnetOrderLedger,
|
|
10
|
+
settleMainnetOrder
|
|
11
|
+
} from "../../library/actions/shared/perps/mainnet-order-ledger.mjs";
|
|
12
|
+
const PENDING_SETTLE_MS = 1e4;
|
|
13
|
+
async function platformPerps(platform) {
|
|
14
|
+
if (platform === "mobile") return import("../../library/actions/mobile/perps/perps.mjs");
|
|
15
|
+
if (platform === "extension") return import("../../library/actions/extension/perps/perps.mjs");
|
|
16
|
+
throw new Error(`no mainnet order cleanup for platform ${platform}`);
|
|
17
|
+
}
|
|
18
|
+
async function currentEntry(projectRoot, id) {
|
|
19
|
+
return (await readMainnetOrderLedger(projectRoot)).entries.find((entry) => entry.id === id);
|
|
20
|
+
}
|
|
21
|
+
async function venueRead(entry) {
|
|
22
|
+
const venue = await readHyperliquidVenue(entry.address, entry.symbol);
|
|
23
|
+
const open = ledgerOrderTargets(entry, venue.openOrders, VENUE_ORDER_FIELDS);
|
|
24
|
+
return { source: venue.source, open: open.map((order) => order.orderId), positionSize: venue.positionSize };
|
|
25
|
+
}
|
|
26
|
+
async function closeOut(projectRoot, env, via, id) {
|
|
27
|
+
const deadline = Date.now() + PENDING_SETTLE_MS;
|
|
28
|
+
for (; ; ) {
|
|
29
|
+
const entry = await currentEntry(projectRoot, id);
|
|
30
|
+
if (!entry) return;
|
|
31
|
+
const cancelInApp = async () => (await platformPerps(entry.platform)).cancelMainnetLedgerOrder(entry, { projectRoot, env });
|
|
32
|
+
let proof = entry.state === "cancelled" ? { canceled: entry.cancelProof.canceled, read: null } : await cancelInApp();
|
|
33
|
+
let venue = await venueRead(entry);
|
|
34
|
+
if (entry.state === "cancelled" && venue.open.length > 0) {
|
|
35
|
+
proof = await cancelInApp();
|
|
36
|
+
venue = await venueRead(entry);
|
|
37
|
+
}
|
|
38
|
+
for (let wait = 0; venue.open.length > 0 && proof.canceled.length > 0 && wait < 5; wait += 1) {
|
|
39
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
40
|
+
venue = await venueRead(entry);
|
|
41
|
+
}
|
|
42
|
+
if (venue.open.length > 0) {
|
|
43
|
+
throw new Error(`the venue still lists order(s) ${venue.open.join(", ")} after the app ${proof.read?.notOpen ? "reported none open" : "cancelled"}`);
|
|
44
|
+
}
|
|
45
|
+
if (proof.read?.notOpen && entry.state === "pending" && Date.now() < deadline) {
|
|
46
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const recorded = { via: entry.cancelProof?.via ?? via, canceled: proof.canceled, app: proof.read, venue };
|
|
50
|
+
const evidencePath = await settleMainnetOrder(projectRoot, entry, recorded);
|
|
51
|
+
process.stderr.write(
|
|
52
|
+
`mainnet ${entry.symbol} order ${entry.orderId ?? "(pending)"}: ${proof.canceled.length > 0 ? "cancelled" : "not open"} at run ${via}, gone at the venue; proof in ${evidencePath}
|
|
53
|
+
`
|
|
54
|
+
);
|
|
55
|
+
if (venue.positionSize !== Number(entry.baselinePositionSize ?? 0)) {
|
|
56
|
+
throw new Error(`the ${entry.symbol} position is ${venue.positionSize}, was ${entry.baselinePositionSize ?? 0} before the order: it filled; close that position now`);
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function cleanup(projectRoot, env, via) {
|
|
62
|
+
const ledger = await readMainnetOrderLedger(projectRoot);
|
|
63
|
+
const failures = [];
|
|
64
|
+
for (const entry of ledger.entries.filter((record) => cleanupOwns(record, process.pid))) {
|
|
65
|
+
try {
|
|
66
|
+
await closeOut(projectRoot, env, via, entry.id);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
failures.push({ entry, error });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (failures.length > 0) throw mainnetOrderLeftOpenError(failures);
|
|
72
|
+
if (ledger.networkSwitch && cleanupOwns(ledger.networkSwitch, process.pid)) {
|
|
73
|
+
const { restoreTestnetAfterRun } = await import("../../library/actions/extension/perps/perps.mjs");
|
|
74
|
+
await restoreTestnetAfterRun(ledger.networkSwitch, { projectRoot });
|
|
75
|
+
await clearMainnetSwitch(projectRoot);
|
|
76
|
+
process.stderr.write(`Perps network switched back to testnet at run ${via}.
|
|
77
|
+
`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
let inFlight;
|
|
81
|
+
function cancelRunMainnetOrders(projectRoot, env, via) {
|
|
82
|
+
inFlight ??= cleanup(projectRoot, env, via).finally(() => {
|
|
83
|
+
inFlight = void 0;
|
|
84
|
+
});
|
|
85
|
+
return inFlight;
|
|
86
|
+
}
|
|
87
|
+
const ABORT_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
88
|
+
async function abortRunMainnetOrders(projectRoot) {
|
|
89
|
+
await markRunAborting(projectRoot, process.pid);
|
|
90
|
+
await cancelRunMainnetOrders(projectRoot, process.env, "abort");
|
|
91
|
+
}
|
|
92
|
+
async function withMainnetOrderAbort(projectRoot, run) {
|
|
93
|
+
let aborting;
|
|
94
|
+
const remove = () => {
|
|
95
|
+
for (const signal of ABORT_SIGNALS) process.off(signal, onSignal);
|
|
96
|
+
};
|
|
97
|
+
function onSignal(signal) {
|
|
98
|
+
if (aborting) return;
|
|
99
|
+
aborting = abortRunMainnetOrders(projectRoot).then(
|
|
100
|
+
() => void 0,
|
|
101
|
+
(error) => {
|
|
102
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}
|
|
103
|
+
`);
|
|
104
|
+
}
|
|
105
|
+
).then(() => {
|
|
106
|
+
remove();
|
|
107
|
+
process.kill(process.pid, signal);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
for (const signal of ABORT_SIGNALS) process.on(signal, onSignal);
|
|
111
|
+
try {
|
|
112
|
+
return await run();
|
|
113
|
+
} finally {
|
|
114
|
+
if (aborting) await aborting;
|
|
115
|
+
else remove();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export {
|
|
119
|
+
abortRunMainnetOrders,
|
|
120
|
+
cancelRunMainnetOrders,
|
|
121
|
+
withMainnetOrderAbort
|
|
122
|
+
};
|
|
@@ -1,35 +1,50 @@
|
|
|
1
|
+
const ORDER_INPUTS = [
|
|
2
|
+
[["market", "symbol"], "market=<symbol> (or symbol=<symbol>)"],
|
|
3
|
+
[["side"], "side=long or side=short"],
|
|
4
|
+
[["amount", "notional"], "amount=<usd> or notional=<usd>"]
|
|
5
|
+
];
|
|
6
|
+
function isRecord(value) {
|
|
7
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
function opensState(value) {
|
|
10
|
+
const state = typeof value === "string" ? value.toLowerCase() : void 0;
|
|
11
|
+
return state === "open" || state === "present";
|
|
12
|
+
}
|
|
13
|
+
function hasValue(source, names) {
|
|
14
|
+
return names.some((name) => {
|
|
15
|
+
const value = source[name];
|
|
16
|
+
return value !== void 0 && value !== null && value !== "";
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function missingOrderInputs(label, has, pathFor) {
|
|
20
|
+
return ORDER_INPUTS.filter(([names]) => !has(names)).map(([names, wanted]) => ({
|
|
21
|
+
severity: "error",
|
|
22
|
+
code: "recipe.missing_param",
|
|
23
|
+
path: pathFor(names[0]),
|
|
24
|
+
message: `${label} requires ${wanted} before it may create an order`
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
1
27
|
function perpsOrderInputFindings(nodeId, node, { ensureChecksOnly }) {
|
|
2
28
|
const action = node.action;
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
29
|
+
if (action === "metamask.perps.start_state") {
|
|
30
|
+
if (ensureChecksOnly) return [];
|
|
31
|
+
return ["positions", "orders"].flatMap((key) => {
|
|
32
|
+
const block = node[key];
|
|
33
|
+
if (!isRecord(block) || !opensState(block.state)) return [];
|
|
34
|
+
return missingOrderInputs(
|
|
35
|
+
`${action} ${key}.state=${String(block.state)}`,
|
|
36
|
+
(names) => hasValue(block, names) || names[0] !== "amount" && hasValue(node, names),
|
|
37
|
+
(name) => `workflow.nodes.${nodeId}.${key}.${name}`
|
|
38
|
+
);
|
|
11
39
|
});
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
requireOneOf(
|
|
21
|
-
["market", "symbol"],
|
|
22
|
-
`${action} requires market=<symbol> (or symbol=<symbol>) before it may create an order`
|
|
23
|
-
);
|
|
24
|
-
requireOneOf(
|
|
25
|
-
["side"],
|
|
26
|
-
`${action} requires side=long or side=short before it may create an order`
|
|
27
|
-
);
|
|
28
|
-
requireOneOf(
|
|
29
|
-
["amount", "notional"],
|
|
30
|
-
`${action} requires amount=<usd> or notional=<usd> before it may create an order`
|
|
40
|
+
}
|
|
41
|
+
const createsOrder = action === "metamask.perps.place_order" || !ensureChecksOnly && (action === "metamask.perps.ensure_positions" || action === "metamask.perps.ensure_orders") && opensState(node.state);
|
|
42
|
+
if (!createsOrder) return [];
|
|
43
|
+
return missingOrderInputs(
|
|
44
|
+
String(action),
|
|
45
|
+
(names) => hasValue(node, names),
|
|
46
|
+
(name) => `workflow.nodes.${nodeId}.${name}`
|
|
31
47
|
);
|
|
32
|
-
return findings;
|
|
33
48
|
}
|
|
34
49
|
export {
|
|
35
50
|
perpsOrderInputFindings
|
package/dist/adapters.js
CHANGED
|
@@ -118,7 +118,10 @@ async function runLiveFirst(platform, action, node, context, forceLive = false,
|
|
|
118
118
|
platform,
|
|
119
119
|
action,
|
|
120
120
|
namespace: LIBRARY_ACTION_NAMESPACE,
|
|
121
|
-
node: withImmediateCloseBudget(
|
|
121
|
+
node: withImmediateCloseBudget(
|
|
122
|
+
action,
|
|
123
|
+
await getAdapterSurface(platform).actions.liveProcessNode?.(action, node, { ...process.env, ...context.env }) ?? node
|
|
124
|
+
),
|
|
122
125
|
context,
|
|
123
126
|
prepared: preparedLiveAdapters?.get(action),
|
|
124
127
|
contextExtras: trustedMutationContextExtras(context),
|
package/dist/commands/doctor.js
CHANGED
|
@@ -85,11 +85,14 @@ async function handleDoctor({ options }) {
|
|
|
85
85
|
} catch (error2) {
|
|
86
86
|
runtimeProbeError2 = error2 instanceof Error ? error2.message : String(error2);
|
|
87
87
|
}
|
|
88
|
-
const
|
|
88
|
+
const recorder2 = runtime2?.decision === "ready" ? await readiness.recorder?.() : void 0;
|
|
89
|
+
const ready2 = doctorReady(runtime2, recorder2);
|
|
89
90
|
const block = readiness.runtimeBlock?.(target, runtime2);
|
|
90
91
|
if (block) failed.push(block.id);
|
|
92
|
+
if (recorder2?.status === "fail") failed.push(recorder2.id);
|
|
91
93
|
const nextActions = doctorFixNextActions(failed, result2.fixture, adapter, target, block);
|
|
92
|
-
if (
|
|
94
|
+
if (recorder2?.status === "fail" && recorder2.userAction) nextActions.push(recorder2.userAction);
|
|
95
|
+
if (failed.length === 0 && runtime2?.decision !== "ready" && runtime2?.nextAction) nextActions.push(runtime2.nextAction);
|
|
93
96
|
const status = result2.status === "pass" && failed.length === 0 ? "pass" : "fail";
|
|
94
97
|
const retryDoctor = `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
|
|
95
98
|
const fixUserAction = failed.includes("wallet-fixture") ? `choose one wallet fixture source from nextActions, run it, then retry: ${retryDoctor}` : nextActions[0] ?? retryDoctor;
|
|
@@ -98,15 +101,16 @@ async function handleDoctor({ options }) {
|
|
|
98
101
|
message: "doctor --fix could not make every required check pass",
|
|
99
102
|
userAction: fixUserAction
|
|
100
103
|
} : void 0;
|
|
101
|
-
if (json) console.log(JSON.stringify({ ...result2, status, fixed, failed, ready, runtime: runtime2, ...runtimeProbeError2 ? { runtimeProbeError: runtimeProbeError2 } : {}, nextActions, ...error ? { error } : {} }, null, 2));
|
|
104
|
+
if (json) console.log(JSON.stringify({ ...result2, status, fixed, failed, ready: ready2, runtime: runtime2, ...recorder2 ? { recorder: recorder2 } : {}, ...runtimeProbeError2 ? { runtimeProbeError: runtimeProbeError2 } : {}, nextActions, ...error ? { error } : {} }, null, 2));
|
|
102
105
|
else {
|
|
103
|
-
console.log(`${status} ${adapter} ${result2.compatibilityMode} ready=${
|
|
106
|
+
console.log(`${status} ${adapter} ${result2.compatibilityMode} ready=${ready2} manifest=${actionManifestPath} fixed=[${fixed.join(",")}] failed=[${failed.join(",")}]`);
|
|
104
107
|
if (runtime2) {
|
|
105
108
|
console.log(`runtime: decision=${runtime2.decision}${runtime2.reasonCode ? ` (${runtime2.reasonCode})` : ""}`);
|
|
106
109
|
for (const reason of runtime2.reasons) console.log(` ${reason}`);
|
|
107
110
|
} else if (runtimeProbeError2) {
|
|
108
111
|
console.log(`runtime probe failed: ${runtimeProbeError2}`);
|
|
109
112
|
}
|
|
113
|
+
if (recorder2) console.log(`recorder: ${recorder2.status} ${recorder2.message}`);
|
|
110
114
|
for (const action of nextActions) console.error(` Next: ${action}`);
|
|
111
115
|
}
|
|
112
116
|
return status === "pass" ? 0 : 1;
|
|
@@ -154,10 +158,12 @@ async function handleDoctor({ options }) {
|
|
|
154
158
|
runtimeProbeError = error instanceof Error ? error.message : String(error);
|
|
155
159
|
}
|
|
156
160
|
const runtimeCheck = doctorRuntimeCheck(runtime, runtimeProbeError, expectLive);
|
|
161
|
+
const recorder = runtime?.decision === "ready" ? await readiness.recorder?.() : void 0;
|
|
157
162
|
const checks = [
|
|
158
163
|
...result.checks,
|
|
159
164
|
runtimeCheck,
|
|
160
165
|
...await readiness.liveChecks?.(target) ?? [],
|
|
166
|
+
...recorder ? [recorder] : [],
|
|
161
167
|
...await platformDoctorChecks(adapter, target),
|
|
162
168
|
// Which library plugin code this process loaded: library, module and digest.
|
|
163
169
|
...adapterPluginChecks()
|
|
@@ -182,6 +188,7 @@ async function handleDoctor({ options }) {
|
|
|
182
188
|
adapter,
|
|
183
189
|
target,
|
|
184
190
|
runtime,
|
|
191
|
+
recorder,
|
|
185
192
|
doctorResult,
|
|
186
193
|
orphanMetros,
|
|
187
194
|
capture,
|
|
@@ -201,7 +208,8 @@ async function handleDoctor({ options }) {
|
|
|
201
208
|
)?.userAction ?? `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
|
|
202
209
|
const doctorError = doctorResult.status === "fail" ? { code: "DOCTOR_CHECKS_FAILED", message: "one or more required doctor checks failed", userAction: doctorUserAction } : void 0;
|
|
203
210
|
const next = doctorError?.userAction ?? discovery.deviceDiscoveryErrors?.[0]?.userAction ?? (runtime?.decision === "ready" ? void 0 : runtime?.nextAction);
|
|
204
|
-
|
|
211
|
+
const ready = doctorReady(runtime, recorder);
|
|
212
|
+
if (json) console.log(JSON.stringify({ ...doctorResult, ready, runtime, ...recorder ? { recorder } : {}, view, orphanMetros, capture, skills, update, featureFlags, devices, additionalReachableDevices, ...discovery, ...next ? { next } : {}, ...doctorError ? { error: doctorError } : {} }, null, 2));
|
|
205
213
|
else {
|
|
206
214
|
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
207
215
|
const stateStyle = (value, good) => value === good ? "ok" : "warn";
|
|
@@ -222,6 +230,7 @@ async function handleDoctor({ options }) {
|
|
|
222
230
|
} else if (runtimeProbeError) {
|
|
223
231
|
console.log(`${out("err", "runtime probe failed:")} ${out("dim", runtimeProbeError)}`);
|
|
224
232
|
}
|
|
233
|
+
if (recorder) console.log(`${out("label", "recorder:")} ${out(recorder.status === "pass" ? "ok" : "err", recorder.status)} ${recorder.message}`);
|
|
225
234
|
console.log(renderCheckoutView(view));
|
|
226
235
|
console.log(`${out("label", "feature flags:")} ${out(
|
|
227
236
|
featureFlags.error || (featureFlags.overrideCount ?? 0) > 0 ? "warn" : "dim",
|
|
@@ -246,6 +255,9 @@ async function handleDoctor({ options }) {
|
|
|
246
255
|
}
|
|
247
256
|
return doctorResult.status === "pass" ? 0 : 1;
|
|
248
257
|
}
|
|
258
|
+
function doctorReady(runtime, recorder) {
|
|
259
|
+
return runtime?.decision === "ready" && recorder?.status !== "fail";
|
|
260
|
+
}
|
|
249
261
|
async function platformDoctorChecks(adapter, target) {
|
|
250
262
|
try {
|
|
251
263
|
return await getAdapterSurface(adapter).doctor?.(target) ?? [];
|
|
@@ -297,17 +309,23 @@ function doctorFixNextActions(failed, fixture, adapter, target, block) {
|
|
|
297
309
|
if (block?.userAction && failed.includes(block.id)) actions.push(block.userAction);
|
|
298
310
|
return actions;
|
|
299
311
|
}
|
|
300
|
-
function emitExpectLiveVerdict(adapter, target, runtime, verdict) {
|
|
312
|
+
function emitExpectLiveVerdict(adapter, target, runtime, verdict, recorder) {
|
|
301
313
|
const out = (style, text) => color(style, text, { stream: process.stderr });
|
|
302
314
|
const detail = runtime ? `${runtime.decision}${runtime.reasonCode ? ` (${runtime.reasonCode})` : ""}` : "runtime probe unavailable";
|
|
303
315
|
console.error(`${out("err", verdict)} ${out("bold", adapter)} ${detail} ${out("dim", target)}`);
|
|
304
316
|
for (const reason of runtime?.reasons ?? []) console.error(` ${out("dim", reason)}`);
|
|
317
|
+
if (recorder?.status === "fail") {
|
|
318
|
+
console.error(` ${out("err", "recorder:")} ${recorder.message}${recorder.detail ? ` ${out("dim", recorder.detail)}` : ""}`);
|
|
319
|
+
console.error(` Next: ${recorder.userAction}`);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
305
322
|
console.error(` Next: ${runtime?.nextAction ?? getAdapterSurface(adapter).hints.runtimeProbeRecovery(target)}`);
|
|
306
323
|
}
|
|
307
|
-
function emitExpectLive(adapter, target, runtime, result, orphanMetros, capture, devices, additionalReachableDevices, json, printReady, discovery) {
|
|
324
|
+
function emitExpectLive(adapter, target, runtime, recorder, result, orphanMetros, capture, devices, additionalReachableDevices, json, printReady, discovery) {
|
|
308
325
|
const live = runtime?.decision === "ready";
|
|
326
|
+
const ready = doctorReady(runtime, recorder);
|
|
309
327
|
if (printReady) {
|
|
310
|
-
if (
|
|
328
|
+
if (ready) {
|
|
311
329
|
const indicator = farmslotReadyIndicator(adapter, devices);
|
|
312
330
|
if (indicator) {
|
|
313
331
|
console.log(indicator);
|
|
@@ -316,21 +334,21 @@ function emitExpectLive(adapter, target, runtime, result, orphanMetros, capture,
|
|
|
316
334
|
emitExpectLiveVerdict(adapter, target, runtime, "not-ready");
|
|
317
335
|
return EXIT.runtime;
|
|
318
336
|
}
|
|
319
|
-
emitExpectLiveVerdict(adapter, target, runtime, "not-live");
|
|
337
|
+
emitExpectLiveVerdict(adapter, target, runtime, live ? "not-ready" : "not-live", recorder);
|
|
320
338
|
return EXIT.runtime;
|
|
321
339
|
}
|
|
322
340
|
if (json) {
|
|
323
341
|
const userAction = runtime?.nextAction ?? getAdapterSurface(adapter).hints.runtimeProbeRecovery(target);
|
|
324
|
-
const error = live ?
|
|
325
|
-
console.log(JSON.stringify({ ...result, ready
|
|
326
|
-
return
|
|
342
|
+
const error = !live ? { code: "RUNTIME_NOT_LIVE", message: `${adapter} runtime is not live`, userAction } : !ready ? { code: "RECORDER_UNAVAILABLE", message: recorder.message, userAction: recorder.userAction } : void 0;
|
|
343
|
+
console.log(JSON.stringify({ ...result, ready, runtime, ...recorder ? { recorder } : {}, orphanMetros, capture, devices, additionalReachableDevices, ...discovery, ...error ? { error } : {} }, null, 2));
|
|
344
|
+
return ready ? EXIT.ok : EXIT.runtime;
|
|
327
345
|
}
|
|
328
346
|
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
329
|
-
if (
|
|
347
|
+
if (ready) {
|
|
330
348
|
console.log(`${out("ok", "live")} ${out("bold", adapter)} runtime ready`);
|
|
331
349
|
return EXIT.ok;
|
|
332
350
|
}
|
|
333
|
-
emitExpectLiveVerdict(adapter, target, runtime, "not-live");
|
|
351
|
+
emitExpectLiveVerdict(adapter, target, runtime, live ? "not-ready" : "not-live", recorder);
|
|
334
352
|
return EXIT.runtime;
|
|
335
353
|
}
|
|
336
354
|
function captureHealth(providers) {
|
|
@@ -457,6 +475,7 @@ async function runDoctorFix(adapter, target, manifestValidation, json) {
|
|
|
457
475
|
}
|
|
458
476
|
export {
|
|
459
477
|
doctorFixNextActions,
|
|
478
|
+
doctorReady,
|
|
460
479
|
handleDoctor,
|
|
461
480
|
skillsView
|
|
462
481
|
};
|
|
@@ -3,6 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import { fixtureSummary } from "../doctor.js";
|
|
4
4
|
import { normalizeFeatureFlagRecord } from "../feature-flags.js";
|
|
5
5
|
import { mobileBridgeScript } from "../adapters/mobile/bridge-script.js";
|
|
6
|
+
import { isAndroidEmulatorSerial } from "../../library/actions/mobile/platform/bridge.mjs";
|
|
6
7
|
const PROBE_TIMEOUT_MS = 2e3;
|
|
7
8
|
function truncateAddress(address) {
|
|
8
9
|
if (!address || address.length <= 10) return address;
|
|
@@ -51,7 +52,7 @@ function parseBridgeEntries(raw) {
|
|
|
51
52
|
function androidModelMatches(deviceName, model, device, devices) {
|
|
52
53
|
if (deviceName === model || deviceName.startsWith(`${model} -`)) return true;
|
|
53
54
|
if (!deviceName.endsWith(` ${model}`) && !deviceName.includes(` ${model} -`)) return false;
|
|
54
|
-
const physical = (d) => d.platform === "android" && !d.id
|
|
55
|
+
const physical = (d) => d.platform === "android" && !isAndroidEmulatorSerial(d.id);
|
|
55
56
|
if (!physical(device)) return false;
|
|
56
57
|
return devices.filter((d) => physical(d) && d.name === device.name).length === 1;
|
|
57
58
|
}
|
package/dist/engine.js
CHANGED
|
@@ -6,11 +6,15 @@ import {
|
|
|
6
6
|
fixturePolicyMutationBase,
|
|
7
7
|
fundingOptionsFromCli,
|
|
8
8
|
isFixturePolicyBase,
|
|
9
|
-
loadTrustedMutationBase
|
|
9
|
+
loadTrustedMutationBase,
|
|
10
|
+
validateTrustedMutationPlan
|
|
10
11
|
} from "./funding-execution-context.js";
|
|
11
12
|
import { resolveActionManifest, validateManifest } from "./manifest.js";
|
|
12
13
|
import { LIBRARY_ACTION_NAMESPACE, runnerDir } from "./paths.js";
|
|
13
14
|
import { metaMaskActionExecutionCapabilities } from "./recipe-security.js";
|
|
15
|
+
async function loadEngineMutation({ cli, adapter, ...run }) {
|
|
16
|
+
return await loadTrustedMutationBase({ ...fundingOptionsFromCli(cli), ...run }) ?? (getAdapterSurface(adapter).actions.trustedMutation === true ? fixturePolicyMutationBase({ artifactsDir: run.artifactsDir, rootRecipe: run.rootRecipe }) : void 0);
|
|
17
|
+
}
|
|
14
18
|
const metaMaskEngine = {
|
|
15
19
|
bundledLibrary: {
|
|
16
20
|
name: "metamask",
|
|
@@ -24,12 +28,17 @@ const metaMaskEngine = {
|
|
|
24
28
|
const { createMetaMaskRunner } = await import("./runner.js");
|
|
25
29
|
return createMetaMaskRunner(adapter, manifest, options);
|
|
26
30
|
},
|
|
31
|
+
// Preflight checks policy; execution also claims the plan and checks claim conflicts.
|
|
27
32
|
// Funding files from the run's command line, bound to the run's plan. Without
|
|
28
33
|
// them, a platform whose actions take a trusted mutation (the Extension) runs a
|
|
29
34
|
// plan with external mutations in the fixture-policy lane, where each action
|
|
30
35
|
// applies the shared order policy; any other plan runs without one.
|
|
31
36
|
trustedMutation: {
|
|
32
|
-
|
|
37
|
+
preflight: async (input, plan) => {
|
|
38
|
+
const base = await loadEngineMutation(input);
|
|
39
|
+
if (base) validateTrustedMutationPlan(base, plan);
|
|
40
|
+
},
|
|
41
|
+
load: loadEngineMutation,
|
|
33
42
|
// Nothing to bind (undefined): recipe-cli keeps the runner it planned with.
|
|
34
43
|
authorize: async (base, plan) => {
|
|
35
44
|
if (!base) return void 0;
|
|
@@ -181,10 +181,9 @@ function fixturePolicyMutationBase(options) {
|
|
|
181
181
|
function isFixturePolicyBase(base) {
|
|
182
182
|
return base?.proofLane === FIXTURE_POLICY_LANE;
|
|
183
183
|
}
|
|
184
|
-
async function authorizeFixturePolicyPlan(base,
|
|
184
|
+
async function authorizeFixturePolicyPlan(base, executionPlanSha256) {
|
|
185
185
|
const source = fixturePolicySources.get(base);
|
|
186
186
|
if (!source) throw new Error("Fixture-policy plan authorization requires the runner-created context.");
|
|
187
|
-
const executionPlanSha256 = normalizeRecipeDigest(plan.digest);
|
|
188
187
|
await mkdir(source.artifactsDir, { recursive: true, mode: 448 });
|
|
189
188
|
const artifactsDir = await realpath(source.artifactsDir);
|
|
190
189
|
const artifactsIdentitySha256 = await directoryIdentity(artifactsDir);
|
|
@@ -204,11 +203,9 @@ async function authorizeFixturePolicyPlan(base, plan) {
|
|
|
204
203
|
});
|
|
205
204
|
return authorized;
|
|
206
205
|
}
|
|
207
|
-
|
|
208
|
-
if (isFixturePolicyBase(base)) return authorizeFixturePolicyPlan(base, plan);
|
|
209
|
-
const source = fundingSources.get(base);
|
|
210
|
-
if (!source) throw new Error("Funding plan authorization requires the original runner-verified context.");
|
|
206
|
+
function validateTrustedMutationPlan(base, plan) {
|
|
211
207
|
const executionPlanSha256 = normalizeRecipeDigest(plan.digest);
|
|
208
|
+
if (isFixturePolicyBase(base)) return { executionPlanSha256, operationCounts: [] };
|
|
212
209
|
const nodes = Array.isArray(plan.nodes) ? plan.nodes : [];
|
|
213
210
|
const actual = /* @__PURE__ */ new Map();
|
|
214
211
|
for (const node of nodes) {
|
|
@@ -235,9 +232,16 @@ async function authorizeTrustedMutationPlan(base, plan, testIo = {}) {
|
|
|
235
232
|
}
|
|
236
233
|
}
|
|
237
234
|
if (actual.size === 0) throw new Error("Funding documents require a resolved external-mutation plan.");
|
|
235
|
+
const operationCounts = [...expected.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([action, count]) => ({ action, count }));
|
|
236
|
+
return { executionPlanSha256, operationCounts };
|
|
237
|
+
}
|
|
238
|
+
async function authorizeTrustedMutationPlan(base, plan, testIo = {}) {
|
|
239
|
+
const { executionPlanSha256, operationCounts } = validateTrustedMutationPlan(base, plan);
|
|
240
|
+
if (isFixturePolicyBase(base)) return authorizeFixturePolicyPlan(base, executionPlanSha256);
|
|
241
|
+
const source = fundingSources.get(base);
|
|
242
|
+
if (!source) throw new Error("Funding plan authorization requires the original runner-verified context.");
|
|
238
243
|
await mkdir(source.artifactsDir, { recursive: true, mode: 448 });
|
|
239
244
|
const artifactsIdentitySha256 = await directoryIdentity(source.artifactsDir);
|
|
240
|
-
const operationCounts = [...expected.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([action, count]) => ({ action, count }));
|
|
241
245
|
const runId = sha256(canonical({
|
|
242
246
|
requestSha256: base.requestSha256,
|
|
243
247
|
preflightSha256: base.preflightSha256,
|
|
@@ -1529,5 +1533,6 @@ export {
|
|
|
1529
1533
|
selectedIdentityFromWalletImport,
|
|
1530
1534
|
serializeTrustedMutationContext,
|
|
1531
1535
|
trustedMutationContextExtras,
|
|
1532
|
-
trustedMutationForAction
|
|
1536
|
+
trustedMutationForAction,
|
|
1537
|
+
validateTrustedMutationPlan
|
|
1533
1538
|
};
|
package/dist/host.js
CHANGED
|
@@ -3,7 +3,7 @@ import { fileURLToPath } from "node:url";
|
|
|
3
3
|
import { configureHarnessAdapters, configureHarnessHost } from "@farmslot/recipe-cli/harness";
|
|
4
4
|
import { adapterRegistry } from "./adapters/registry.js";
|
|
5
5
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
6
|
-
|
|
6
|
+
const providerHost = {
|
|
7
7
|
name: "mm-harness",
|
|
8
8
|
product: "MetaMask",
|
|
9
9
|
envPrefix: "MM_HARNESS",
|
|
@@ -28,5 +28,9 @@ configureHarnessHost({
|
|
|
28
28
|
"stop",
|
|
29
29
|
"verify"
|
|
30
30
|
]
|
|
31
|
-
}
|
|
31
|
+
};
|
|
32
|
+
configureHarnessHost(providerHost);
|
|
32
33
|
configureHarnessAdapters(adapterRegistry);
|
|
34
|
+
export {
|
|
35
|
+
providerHost
|
|
36
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import "./host.js";
|
|
2
|
+
import { createProvider, providerHost } from "./provider.js";
|
|
2
3
|
import { createDoctorReport } from "./doctor.js";
|
|
3
4
|
import { loadActionManifest, validateManifest } from "./manifest.js";
|
|
4
5
|
import { createMetaMaskExtensionRunner, createMetaMaskMobileRunner, createMetaMaskRunner } from "./runner.js";
|
|
@@ -13,10 +14,12 @@ export {
|
|
|
13
14
|
createMetaMaskExtensionRunner,
|
|
14
15
|
createMetaMaskMobileRunner,
|
|
15
16
|
createMetaMaskRunner,
|
|
17
|
+
createProvider,
|
|
16
18
|
extensionIdPath,
|
|
17
19
|
importRecipeHarnessRuntimeBrowserExtension,
|
|
18
20
|
importRecipeHarnessRuntimeCdp,
|
|
19
21
|
loadActionManifest,
|
|
22
|
+
providerHost,
|
|
20
23
|
validateManifest,
|
|
21
24
|
walletFixturePath
|
|
22
25
|
};
|
package/dist/mm-harness-cli.js
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from "@farmslot/recipe-cli/harness";
|
|
23
23
|
import { stopProjectCollector } from "../library/actions/shared/analytics/collector.mjs";
|
|
24
24
|
import { adoptPluginAdapter, afterAdapterLoad, configuredLibrarySources } from "./adapters/plugin-surface.js";
|
|
25
|
+
import { withMainnetOrderAbort } from "./adapters/perps-mainnet-cleanup.js";
|
|
25
26
|
import { adapterRegistry } from "./adapters/registry.js";
|
|
26
27
|
import {
|
|
27
28
|
explainMissingAdapterLibrary,
|
|
@@ -155,8 +156,9 @@ const PUBLIC_RUN = {
|
|
|
155
156
|
"execution-template": { run: handleExecutionTemplate },
|
|
156
157
|
actions: { run: (args) => handleActions(parseArgs(args), { catalog: metaMaskEngine }) },
|
|
157
158
|
stop: { run: (args) => handleStop(args, stopOptions) },
|
|
158
|
-
|
|
159
|
-
|
|
159
|
+
// A signal during run or call still cancels the run's mainnet orders.
|
|
160
|
+
call: { run: (args) => withMainnetOrderAbort(targetPath(parseArgs(args).options), () => handleCall(args, callOptions)) },
|
|
161
|
+
run: { run: (args) => withMainnetOrderAbort(targetPath(parseArgs(args).options), () => handleRun(args, runOptions)) },
|
|
160
162
|
last: { run: (args) => handleLast(parseArgs(args)) },
|
|
161
163
|
doctor: { run: (args) => handleDoctor(parseArgs(args)) },
|
|
162
164
|
check: { run: handleCheck },
|
package/dist/provider.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import "./adapters/surface.js";
|
|
2
|
+
import {
|
|
3
|
+
configureHarnessAdapters,
|
|
4
|
+
adapterForPlatform,
|
|
5
|
+
applyRuntimeDirOption,
|
|
6
|
+
configureHarnessHost,
|
|
7
|
+
ensureAdapterLoaded,
|
|
8
|
+
optionFlag,
|
|
9
|
+
optionStrings,
|
|
10
|
+
usageError
|
|
11
|
+
} from "@farmslot/recipe-cli/harness";
|
|
12
|
+
import { MAINNET_RUN_OWNER_ENV } from "../library/actions/shared/perps/mainnet-order-ledger.mjs";
|
|
13
|
+
import { adoptPluginAdapter, afterAdapterLoad, configuredLibrarySources, operatorLibraryEnv } from "./adapters/plugin-surface.js";
|
|
14
|
+
import { abortRunMainnetOrders, cancelRunMainnetOrders } from "./adapters/perps-mainnet-cleanup.js";
|
|
15
|
+
import { adapterRegistry } from "./adapters/registry.js";
|
|
16
|
+
import { getAdapterSurface } from "./adapters/surface.js";
|
|
17
|
+
import { applyDeviceTargeting } from "./commands/device-target.js";
|
|
18
|
+
import { metaMaskEngine } from "./engine.js";
|
|
19
|
+
import { providerHost } from "./host.js";
|
|
20
|
+
import { providerHost as providerHost2 } from "./host.js";
|
|
21
|
+
async function createProvider(context) {
|
|
22
|
+
configureHarnessHost(context.host ?? providerHost);
|
|
23
|
+
configureHarnessAdapters(adapterRegistry);
|
|
24
|
+
const selection = context.adapter?.value;
|
|
25
|
+
if (!selection) throw usageError("The MetaMask provider requires a resolved adapter.");
|
|
26
|
+
const adapter = adapterForPlatform(selection) ?? selection;
|
|
27
|
+
const options = {
|
|
28
|
+
...context.options,
|
|
29
|
+
...!context.options?.platform && (selection === "ios" || selection === "android") ? { platform: selection } : {}
|
|
30
|
+
};
|
|
31
|
+
await ensureAdapterLoaded(adapter, {
|
|
32
|
+
libraries: optionStrings(options, "library"),
|
|
33
|
+
configured: context.libraries ?? configuredLibrarySources(),
|
|
34
|
+
env: operatorLibraryEnv(),
|
|
35
|
+
adopt: adoptPluginAdapter
|
|
36
|
+
});
|
|
37
|
+
await afterAdapterLoad();
|
|
38
|
+
const runtime = getAdapterSurface(adapter);
|
|
39
|
+
const executes = (context.command === "run" || context.command === "call") && !["plan", "list", "describe", "help"].some((flag) => optionFlag(options, flag));
|
|
40
|
+
if (executes) {
|
|
41
|
+
applyRuntimeDirOption(options);
|
|
42
|
+
runtime.resolveSlotPorts(context.target.value);
|
|
43
|
+
const selected = applyDeviceTargeting(context.command, adapter, {
|
|
44
|
+
...options,
|
|
45
|
+
target: context.target.value
|
|
46
|
+
}, { gate: true, rerun: "" });
|
|
47
|
+
if (selected.ok === false) throw Object.assign(usageError(selected.message), { code: selected.code, userAction: selected.userAction });
|
|
48
|
+
}
|
|
49
|
+
const previousOwner = process.env[MAINNET_RUN_OWNER_ENV];
|
|
50
|
+
if (executes) process.env[MAINNET_RUN_OWNER_ENV] = String(process.pid);
|
|
51
|
+
return {
|
|
52
|
+
runtime,
|
|
53
|
+
engine: metaMaskEngine,
|
|
54
|
+
...executes ? {
|
|
55
|
+
async cancel() {
|
|
56
|
+
await abortRunMainnetOrders(context.target.value);
|
|
57
|
+
},
|
|
58
|
+
async finalize() {
|
|
59
|
+
try {
|
|
60
|
+
await cancelRunMainnetOrders(context.target.value, process.env, "teardown");
|
|
61
|
+
} finally {
|
|
62
|
+
if (previousOwner === void 0) delete process.env[MAINNET_RUN_OWNER_ENV];
|
|
63
|
+
else process.env[MAINNET_RUN_OWNER_ENV] = previousOwner;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} : {}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export {
|
|
70
|
+
createProvider,
|
|
71
|
+
providerHost2 as providerHost
|
|
72
|
+
};
|
package/dist/recipe-security.js
CHANGED
|
@@ -76,6 +76,7 @@ const APP_MUTATION_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
76
76
|
"metamask.perps.open_balance_action",
|
|
77
77
|
"metamask.perps.assert_visible_consistency",
|
|
78
78
|
"metamask.perps.start_state",
|
|
79
|
+
"metamask.perps.set_network",
|
|
79
80
|
"metamask.perps.teardown_state",
|
|
80
81
|
// Consent changes wallet state and is therefore an app mutation.
|
|
81
82
|
"metamask.analytics.set_consent",
|
package/docs/CONTRIBUTING.md
CHANGED
|
@@ -199,6 +199,13 @@ when packed, so `agent-runtime` and `adapter-rn` can retain older copies until
|
|
|
199
199
|
republished. Verify the complete install with
|
|
200
200
|
`bash tests/contract/packaging-dist-install.test.sh` before releasing the harness.
|
|
201
201
|
|
|
202
|
+
`yarn install` hardlinks every `node_modules` file to Yarn's store
|
|
203
|
+
(`~/.yarn/berry/store`), shared by all worktrees. Never edit an installed
|
|
204
|
+
dependency in place: a write into an existing file reaches the store and every
|
|
205
|
+
other worktree, and a reinstall does not undo it. Use `yarn dev:link-farmslot`
|
|
206
|
+
for local Farmslot builds. To repair a package, remove `node_modules/<package>`
|
|
207
|
+
in one worktree and run `yarn install`.
|
|
208
|
+
|
|
202
209
|
Docs describe only current behavior. History belongs in the changelog and Git;
|
|
203
210
|
do not add handovers, audits, speculative roadmaps, or duplicate command guides
|
|
204
211
|
under `docs/`.
|