@deeeed/metamask-harness 0.79.0 → 0.81.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 +73 -0
- package/README.md +1 -1
- package/adapters/extension/ensure-browser.sh +2 -2
- package/adapters/extension/inject.mjs +0 -1
- package/adapters/extension/launch-browser.cjs +39 -613
- package/adapters/extension/readiness.mjs +2 -5
- package/adapters/extension/reattach.sh +2 -2
- package/adapters/extension/stamp-runtime-title.cjs +1 -1
- package/adapters/manifest.json +16 -64
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +57 -1358
- package/adapters/mobile/bridge-runtime/teaching.cjs +20 -0
- package/adapters/mobile/inject.sh +16 -0
- package/adapters/mobile/launch-console-forwarder.cjs +1 -2
- package/adapters/mobile/open-device.sh +2 -2
- package/adapters/mobile/reload-app.mjs +4 -4
- package/adapters/mobile/reset-app-data.sh +1 -1
- package/adapters/mobile/start-console-forwarder.sh +3 -2
- package/adapters/mobile/start-metro.sh +17 -12
- package/adapters/mobile/wait-for-bridge.cjs +2 -2
- package/adapters/shared/harness-source-fingerprint.mjs +13 -8
- package/adapters/shared/open-debug.mjs +2 -2
- package/adapters/shared/sync-wallet-fixture.sh +8 -5
- package/adapters/terminal/cleanup.mjs +2 -1
- package/adapters/terminal/inject.mjs +2 -1
- package/adapters/terminal/launch.mjs +17 -13
- package/adapters/terminal/lib/mainnet-guard.mjs +9 -0
- package/adapters/terminal/lib/readiness.mjs +20 -12
- package/adapters/terminal/policy.mjs +35 -0
- package/adapters/terminal/verify.mjs +10 -3
- package/adapters/terminal/wallet-host.mjs +43 -225
- package/bin/mm-harness +9 -28
- package/dist/adapters/checkout-files.js +16 -0
- package/dist/adapters/console-capture.js +14 -1
- package/dist/adapters/core/actions.js +8 -4
- package/dist/adapters/core/overlay.js +1 -1
- package/dist/adapters/core/readiness.js +28 -1
- package/dist/adapters/core/run.js +2 -2
- package/dist/adapters/core/surface.js +16 -1
- package/dist/adapters/extension/actions.js +6 -2
- package/dist/adapters/extension/browser-pid.js +56 -0
- package/dist/adapters/extension/console-capture.js +1 -1
- package/dist/adapters/extension/doctor-checks.js +1 -1
- package/dist/adapters/extension/ensure-ready.js +5 -4
- package/dist/adapters/extension/extension-id.js +1 -1
- package/dist/adapters/extension/feature-flags.js +1 -1
- package/dist/adapters/extension/fixtures.js +1 -2
- package/dist/adapters/extension/launch.js +4 -5
- package/dist/adapters/extension/launched-browser.js +1 -2
- package/dist/adapters/extension/network-observer.js +14 -1
- package/dist/adapters/extension/overlay.js +1 -1
- package/dist/adapters/extension/product-config.js +1 -1
- package/dist/adapters/extension/readiness.js +1 -1
- package/dist/adapters/extension/reload.js +1 -1
- package/dist/adapters/extension/run.js +135 -14
- package/dist/adapters/extension/runtime-decision.js +2 -1
- package/dist/adapters/extension/runtime.js +9 -5
- package/dist/adapters/extension/surface.js +15 -2
- package/dist/adapters/extension/verify.js +2 -2
- package/dist/adapters/harness-freshness.js +2 -1
- package/dist/adapters/mobile/actions.js +4 -1
- package/dist/adapters/mobile/android-gfxinfo-performance.js +1 -3
- package/dist/adapters/mobile/app-identity.js +10 -0
- package/dist/adapters/mobile/doctor-checks.js +1 -1
- package/dist/adapters/mobile/fixtures.js +1 -1
- package/dist/adapters/mobile/launch.js +27 -28
- package/dist/adapters/mobile/lifecycle.js +1 -1
- package/dist/adapters/mobile/metro-env.js +9 -59
- package/dist/adapters/mobile/network-observer.js +68 -0
- package/dist/adapters/mobile/performance-observer.js +2 -4
- package/dist/adapters/mobile/prepare-target.js +1 -1
- package/dist/adapters/mobile/prepare.js +3 -4
- package/dist/adapters/mobile/provision.js +1 -1
- package/dist/adapters/mobile/readiness.js +1 -1
- package/dist/adapters/mobile/recording-target.js +15 -0
- package/dist/adapters/mobile/release-artifact-state.js +1 -1
- package/dist/adapters/mobile/reload.js +1 -1
- package/dist/adapters/mobile/run.js +39 -15
- package/dist/adapters/mobile/runtime-decision.js +2 -4
- package/dist/adapters/mobile/source-freshness.js +22 -126
- package/dist/adapters/mobile/surface.js +26 -3
- package/dist/adapters/mobile/ui.js +16 -5
- package/dist/adapters/mobile/verify.js +2 -3
- package/dist/adapters/performance/cdp-trace.js +1 -1
- package/dist/adapters/performance/device-platform.js +1 -1
- package/dist/adapters/perps-order-inputs.js +36 -0
- package/dist/adapters/proof-failure.js +11 -0
- package/dist/adapters/slot-ports.js +1 -1
- package/dist/adapters/surface.js +1 -0
- package/dist/adapters/terminal/actions.js +4 -1
- package/dist/adapters/terminal/console-capture.js +1 -1
- package/dist/adapters/terminal/doctor-checks.js +2 -0
- package/dist/adapters/terminal/launch.js +4 -1
- package/dist/adapters/terminal/overlay.js +8 -1
- package/dist/adapters/terminal/run.js +33 -5
- package/dist/adapters/terminal/surface.js +9 -1
- package/dist/adapters.js +27 -7
- package/dist/advisor-policy.js +1 -1
- package/dist/checkout-view.js +1 -2
- package/dist/cli.js +59 -21
- package/dist/command-contract.js +1 -26
- package/dist/commands/check.js +33 -10
- package/dist/commands/checklist.js +1 -1
- package/dist/commands/completion-candidates.js +6 -14
- package/dist/commands/config.js +2 -2
- package/dist/commands/core-readiness.js +12 -7
- package/dist/commands/debug.js +3 -3
- package/dist/commands/device-target.js +5 -5
- package/dist/commands/doctor.js +2 -14
- package/dist/commands/domain.js +2 -2
- package/dist/commands/ensure-ready.js +1 -7
- package/dist/commands/fixtures.js +5 -6
- package/dist/commands/help.js +3 -4
- package/dist/commands/launch/device.js +10 -17
- package/dist/commands/launch/extension.js +2 -8
- package/dist/commands/launch/mobile.js +1 -1
- package/dist/commands/logs.js +3 -3
- package/dist/commands/mobile-device-view.js +4 -2
- package/dist/commands/overlay.js +110 -0
- package/dist/commands/pin.js +1 -1
- package/dist/commands/pr-body.js +2 -2
- package/dist/commands/prepare.js +4 -9
- package/dist/commands/provision.js +3 -12
- package/dist/commands/recipe-advice.js +1 -2
- package/dist/commands/recipe-quality.js +2 -3
- package/dist/commands/resolve-extension.js +1 -7
- package/dist/commands/review.js +4 -4
- package/dist/commands/runtime-decision.js +1 -6
- package/dist/commands/runtime-health.js +1 -7
- package/dist/commands/runtime-launch.js +1 -13
- package/dist/commands/self-test.js +5 -8
- package/dist/commands/status-watch.js +1 -2
- package/dist/commands/status.js +2 -5
- package/dist/commands/task-init.js +2 -3
- package/dist/commands/tutorial.js +2 -2
- package/dist/commands/update.js +1 -1
- package/dist/completions-cache.js +1 -1
- package/dist/console-classifier.js +19 -0
- package/dist/doctor.js +2 -3
- package/dist/engine.js +57 -0
- package/dist/funding-execution-context.js +96 -1
- package/dist/host.js +32 -0
- package/dist/index.js +1 -6
- package/dist/live-tsx.js +49 -0
- package/dist/mm-harness-cli.js +4 -6
- package/dist/paths.js +34 -64
- package/dist/performance-observation.js +3 -5
- package/dist/progress.js +1 -1
- package/dist/recipe-assistance-context.js +6 -9
- package/dist/recipe-security.js +2 -59
- package/dist/review/knowledge.js +2 -2
- package/dist/runner.js +7 -4
- package/dist/runtime-context.js +1 -1
- package/dist/task-view.js +2 -3
- package/docs/CONTRIBUTING.md +5 -3
- package/docs/RECIPES.md +31 -3
- package/docs/cli-contract.md +2 -2
- package/docs/feature-guard.md +90 -0
- package/library/README.md +3 -1
- package/library/actions/core/perps/_controller.mjs +153 -134
- 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 +61 -0
- package/library/actions/core/wallet/list_accounts.mjs +14 -57
- package/library/actions/extension/feature_flags/state.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 +73 -11
- package/library/actions/harness-exports.mjs +1 -6
- package/library/actions/mobile/app/network-control.mjs +6 -8
- package/library/actions/mobile/perps/perps.mjs +216 -11
- package/library/actions/mobile/perps/place_order.mjs +3 -1
- package/library/actions/mobile/perps/prepare_local_snapshot_endpoint.mjs +1 -1
- package/library/actions/mobile/platform/bridge.mjs +8 -8
- package/library/actions/mobile/platform/native-session-name.mjs +21 -1
- package/library/actions/mobile/platform/native-session.mjs +3 -4
- package/library/actions/mobile/platform/observe-ui.mjs +12 -6
- package/library/actions/mobile/ui/native-navigation.mjs +4 -3
- package/library/actions/mobile/wallet/native-ui.mjs +3 -2
- package/library/actions/shared/analytics/_adapter.mjs +1 -1
- package/library/actions/shared/console-findings.mjs +1 -1
- package/library/actions/shared/perps/order-policy.mjs +177 -0
- package/library/actions/shared/wallet/fixture-accounts.mjs +84 -0
- package/library/actions/terminal/app/launch.mjs +3 -2
- package/library/actions/terminal/platform/hud.mjs +2 -1
- package/library/actions/terminal/platform/page.mjs +2 -1
- package/library/actions/terminal/platform/runtime.mjs +2 -1
- package/library/actions/terminal/wallet/_signature-log.mjs +40 -161
- package/library/actions/terminal/wallet/assert_signatures.mjs +6 -12
- package/library/actions/terminal/wallet/read_signatures.mjs +10 -9
- package/library/manifests/core.action-manifest.json +87 -13
- package/library/manifests/extension.action-manifest.json +4 -0
- package/library/manifests/mobile.action-manifest.json +12 -0
- package/package.json +8 -9
- package/site/assets/help-recipes.json +1 -0
- package/adapters/extension/lib/slot-title.cjs +0 -292
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +0 -472
- package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +0 -125
- package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +0 -1055
- package/adapters/mobile/bridge-runtime/lib/cdp-eval.cjs +0 -110
- package/adapters/mobile/bridge-runtime/lib/config.cjs +0 -61
- package/adapters/mobile/bridge-runtime/lib/console-format.cjs +0 -53
- package/adapters/mobile/bridge-runtime/lib/devtools-proxy.cjs +0 -177
- package/adapters/mobile/bridge-runtime/lib/issue-capture.cjs +0 -446
- package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +0 -84
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +0 -403
- package/adapters/mobile/bridge-runtime/lib/ws-client.cjs +0 -116
- package/adapters/mobile/coalesce-metro-log.cjs +0 -24
- package/adapters/mobile/launch-metro.cjs +0 -77
- package/adapters/mobile/metro-config.cjs +0 -98
- package/adapters/mobile/metro-log-generation.cjs +0 -106
- 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/mobile/frame-metrics.js +0 -118
- package/dist/adapters/mobile/video-recorder.js +0 -289
- package/dist/artifact-files.js +0 -92
- package/dist/checkout-lock.js +0 -149
- package/dist/cli-color.js +0 -98
- package/dist/command-journal.js +0 -308
- package/dist/commands/call.js +0 -685
- package/dist/commands/last.js +0 -60
- package/dist/commands/launch/index.js +0 -109
- package/dist/commands/list-executables.js +0 -247
- package/dist/commands/manifest.js +0 -488
- package/dist/commands/parse-args.js +0 -279
- package/dist/commands/reload.js +0 -41
- package/dist/commands/run-engine.js +0 -1340
- package/dist/commands/run-report.js +0 -241
- package/dist/commands/run.js +0 -751
- package/dist/commands/shared.js +0 -323
- package/dist/commands/stop.js +0 -60
- package/dist/devices.js +0 -100
- package/dist/discovery.js +0 -15
- package/dist/execution-provenance.js +0 -332
- package/dist/harness.js +0 -451
- package/dist/heal-bounds.js +0 -137
- package/dist/json-stream.js +0 -60
- package/dist/leaf-invoke.js +0 -19
- package/dist/library-provenance.js +0 -33
- package/dist/live-adapter-contract.js +0 -380
- package/dist/metamask-action-validation.js +0 -68
- package/dist/network-observation.js +0 -290
- package/dist/recording-target.js +0 -120
- package/dist/run-diagnostics.js +0 -364
- package/dist/run-recording.js +0 -481
- package/dist/runtime-proof.js +0 -90
- package/library/actions/mobile/platform/tool-paths.mjs +0 -123
|
@@ -27,19 +27,20 @@
|
|
|
27
27
|
import { appendFileSync, chmodSync, existsSync, unwatchFile, watchFile, writeFileSync } from 'node:fs';
|
|
28
28
|
import { createRequire } from 'node:module';
|
|
29
29
|
import path from 'node:path';
|
|
30
|
+
import { pathToFileURL } from 'node:url';
|
|
31
|
+
|
|
32
|
+
import { createStrictWallet, createWalletRequestBinding, pageReadyExpression, pageScriptSource } from '@farmslot/adapter-web/dapp';
|
|
33
|
+
import { isAppUrl as isExactAppUrl, shortUrl } from '@farmslot/adapter-web/origin';
|
|
30
34
|
|
|
31
35
|
import { CdpClient, browserWebSocketUrl } from './lib/cdp-client.mjs';
|
|
32
36
|
import { hudFile, hudRenderExpression, readHudState } from './lib/hud.mjs';
|
|
33
|
-
import {
|
|
37
|
+
import { hostOf, isMainnetVenueUrl } from './lib/mainnet-guard.mjs';
|
|
34
38
|
import { ownedBrowserPids } from './lib/processes.mjs';
|
|
35
|
-
import { isAppTopFrameContext, isAppUrl as isExactAppUrl } from './lib/origin.mjs';
|
|
36
|
-
import { LOG_BINDING, REQUEST_BINDING, RESOLVE_FN, pageScriptSource } from './lib/page-script.mjs';
|
|
37
|
-
import { createStrictWallet } from './lib/strict-wallet.mjs';
|
|
38
39
|
|
|
39
40
|
const { captureMacFrontmost, macFocusDisabled } = createRequire(import.meta.url)('@farmslot/adapter-web/macos-focus');
|
|
40
41
|
|
|
41
42
|
function usage() {
|
|
42
|
-
return 'Usage: wallet-host.mjs --cdp-port <port> --runtime-dir <dir> --signer <extension|injected> --app-origin <url> [--network testnet|mainnet] [--mainnet-hosts <h1,h2>] [--testnet-hosts <h1,h2>] [--extension-id <id>] [--target <checkout> --account <name> --start-chain <id>] [--observe-focus 1] [--window x,y,w,h] [--start-path <path>] [--mm-harness-owner=<marker>]';
|
|
43
|
+
return 'Usage: wallet-host.mjs --cdp-port <port> --runtime-dir <dir> --signer <extension|injected> --app-origin <url> --policy-module <venue policy module> [--network testnet|mainnet] [--mainnet-hosts <h1,h2>] [--testnet-hosts <h1,h2>] [--extension-id <id>] [--target <checkout> --account <name> --start-chain <id>] [--observe-focus 1] [--window x,y,w,h] [--start-path <path>] [--mm-harness-owner=<marker>]';
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
function parseArgs(argv) {
|
|
@@ -58,13 +59,17 @@ function parseArgs(argv) {
|
|
|
58
59
|
args[flag.slice(2)] = argv[index + 1];
|
|
59
60
|
index += 1;
|
|
60
61
|
}
|
|
61
|
-
for (const key of ['cdp-port', 'runtime-dir', 'signer', 'app-origin']) {
|
|
62
|
+
for (const key of ['cdp-port', 'runtime-dir', 'signer', 'app-origin', 'policy-module']) {
|
|
62
63
|
if (!args[key]) throw new Error(`Missing --${key}\n${usage()}`);
|
|
63
64
|
}
|
|
64
65
|
return args;
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
const args = parseArgs(process.argv.slice(2));
|
|
69
|
+
// The venue policy (the Terminal's adapters/terminal/policy.mjs): link hosts,
|
|
70
|
+
// the probe host, the typed-data refusal and the start page.
|
|
71
|
+
const { policy } = await import(pathToFileURL(path.resolve(args['policy-module'])).href);
|
|
72
|
+
const LINK_HOSTS = new Set(policy.linkHosts);
|
|
68
73
|
const runtimeDir = path.resolve(args['runtime-dir']);
|
|
69
74
|
const logFile = path.join(runtimeDir, 'wallet-requests.jsonl');
|
|
70
75
|
const signer = args.signer;
|
|
@@ -79,6 +84,19 @@ if (network === 'testnet' && mainnetHosts.length === 0) throw new Error('testnet
|
|
|
79
84
|
// only, never a link host (app.hyperliquid-testnet.xyz serves pages and icons).
|
|
80
85
|
const testnetHosts = String(args['testnet-hosts'] ?? '').split(',').filter((host) => host && !LINK_HOSTS.has(host));
|
|
81
86
|
const PROBE_MARK = 'mm-harness-probe=1';
|
|
87
|
+
// A testnet run never lets the typed data the policy refuses reach the signer.
|
|
88
|
+
const refuseTypedData = network === 'testnet' ? policy.refuseTypedData : null;
|
|
89
|
+
// The injected strict wallet presents as MetaMask, so the app connects to it
|
|
90
|
+
// the way it connects to the extension.
|
|
91
|
+
const INJECTED_WALLET = {
|
|
92
|
+
info: {
|
|
93
|
+
uuid: '5d1e5b4e-e2e0-4c3f-9a1d-000000000001',
|
|
94
|
+
name: 'MetaMask (strict test wallet)',
|
|
95
|
+
icon: "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'/>",
|
|
96
|
+
rdns: 'io.metamask',
|
|
97
|
+
},
|
|
98
|
+
isMetaMask: true,
|
|
99
|
+
};
|
|
82
100
|
|
|
83
101
|
// The request log is evidence; keep it owner-only even when it already exists.
|
|
84
102
|
if (existsSync(logFile)) chmodSync(logFile, 0o600);
|
|
@@ -96,29 +114,23 @@ if (signer === 'injected') {
|
|
|
96
114
|
const { fixtureAccount } = await import('../../library/actions/terminal/platform/runtime.mjs');
|
|
97
115
|
if (!args.target || !args.account) throw new Error('injected mode requires --target and --account');
|
|
98
116
|
const account = await fixtureAccount(path.resolve(args.target), args.account);
|
|
99
|
-
wallet = createStrictWallet({ account, chainId: Number(args['start-chain'] ??
|
|
117
|
+
wallet = createStrictWallet({ account, chainId: Number(args['start-chain'] ?? policy.startChain) });
|
|
100
118
|
say(`injected strict wallet ready for ${args.account} (${account.address}) on chain ${wallet.chainId}`);
|
|
101
119
|
}
|
|
102
120
|
|
|
103
|
-
const source = pageScriptSource({ signer, appOrigin,
|
|
121
|
+
const source = pageScriptSource({ signer, appOrigin, refuseTypedData, injectedWallet: INJECTED_WALLET });
|
|
104
122
|
const client = await CdpClient.connect(await browserWebSocketUrl(Number(args['cdp-port'])));
|
|
123
|
+
// Judges each binding call against the frame and document that made it, logs
|
|
124
|
+
// it and, in injected mode, answers it with the strict wallet.
|
|
125
|
+
const binding = createWalletRequestBinding({ client, appOrigin, signer, wallet, refuseTypedData, record: appendEntry, say });
|
|
105
126
|
// Hooks are installed once per target: a second session for the same tab
|
|
106
127
|
// (auto-attach plus a restored tab) would log every request twice.
|
|
107
128
|
const hookedTargets = new Map();
|
|
108
129
|
const appSessions = new Set();
|
|
109
|
-
// Per app session: its target id (= main frame id)
|
|
110
|
-
// committed
|
|
111
|
-
// the frame, document and origin that made it.
|
|
130
|
+
// Per app session: its target id (= main frame id) and the URL its top frame
|
|
131
|
+
// has committed.
|
|
112
132
|
const sessionTargets = new Map();
|
|
113
133
|
const sessionCommittedUrls = new Map();
|
|
114
|
-
const sessionContexts = new Map();
|
|
115
|
-
// Binding calls whose execution context, or that context's document, is not
|
|
116
|
-
// known yet.
|
|
117
|
-
const pendingCalls = new Map();
|
|
118
|
-
// Per app session: the URL of a top-frame commit no execution context has
|
|
119
|
-
// been tied to yet.
|
|
120
|
-
const awaitingDocuments = new Map();
|
|
121
|
-
const PENDING_CALL_TTL_MS = 5000;
|
|
122
134
|
// App sessions whose target runs with domains enabled; the HUD is drawn only
|
|
123
135
|
// there so no evaluate ever waits on a tab paused for setup.
|
|
124
136
|
const hudSessions = new Set();
|
|
@@ -129,41 +141,6 @@ function isAppUrl(url) {
|
|
|
129
141
|
return url === 'about:blank' || url === '' || isExactAppUrl(url, appOrigin);
|
|
130
142
|
}
|
|
131
143
|
|
|
132
|
-
function isTopDefaultContext(sessionId, context) {
|
|
133
|
-
return context?.auxData?.isDefault === true && context.auxData.frameId === sessionTargets.get(sessionId);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// A top-frame commit is tied to the top frame's default context of its
|
|
137
|
-
// document: one created before the commit and still untied, or else the next
|
|
138
|
-
// one created.
|
|
139
|
-
// A commit is applied once: Page.getFrameTree and Page.frameNavigated can
|
|
140
|
-
// both report the same document (its loaderId); a second report would leave a
|
|
141
|
-
// stale commit waiting for the next document's context.
|
|
142
|
-
const appliedCommits = new Map();
|
|
143
|
-
function assignDocumentUrl(sessionId, url, loaderId) {
|
|
144
|
-
if (loaderId) {
|
|
145
|
-
if (appliedCommits.get(sessionId) === loaderId) return;
|
|
146
|
-
appliedCommits.set(sessionId, loaderId);
|
|
147
|
-
}
|
|
148
|
-
const untied = [...(sessionContexts.get(sessionId)?.values() ?? [])]
|
|
149
|
-
.filter((context) => isTopDefaultContext(sessionId, context) && context.documentUrl === null);
|
|
150
|
-
if (untied.length === 0) {
|
|
151
|
-
awaitingDocuments.set(sessionId, url);
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
for (const context of untied) context.documentUrl = url;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Judged against the caller's own document, not the tab's latest commit.
|
|
158
|
-
function callerIsAppTopFrame(sessionId, executionContextId) {
|
|
159
|
-
const context = sessionContexts.get(sessionId)?.get(executionContextId);
|
|
160
|
-
return isAppTopFrameContext(context, {
|
|
161
|
-
targetId: sessionTargets.get(sessionId),
|
|
162
|
-
appOrigin,
|
|
163
|
-
committedUrl: context?.documentUrl ?? null,
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
|
|
167
144
|
// MetaMask shows dapp requests in its side panel (the fixture default) or a
|
|
168
145
|
// notification/popup window; only request routes count as confirmations.
|
|
169
146
|
const WALLET_SURFACES = ['sidepanel.html', 'notification.html', 'popup.html'];
|
|
@@ -233,9 +210,7 @@ function bounded(promise, label, ms = 10000) {
|
|
|
233
210
|
async function installAppHooks(sessionId, targetId) {
|
|
234
211
|
appSessions.add(sessionId);
|
|
235
212
|
sessionTargets.set(sessionId, targetId);
|
|
236
|
-
|
|
237
|
-
await bounded(client.send('Runtime.addBinding', { name: LOG_BINDING }, sessionId), 'Runtime.addBinding');
|
|
238
|
-
if (signer === 'injected') await bounded(client.send('Runtime.addBinding', { name: REQUEST_BINDING }, sessionId), 'Runtime.addBinding');
|
|
213
|
+
await bounded(binding.install(sessionId, targetId), 'Runtime.addBinding');
|
|
239
214
|
await bounded(client.send('Page.addScriptToEvaluateOnNewDocument', { source, runImmediately: true }, sessionId), 'Page.addScriptToEvaluateOnNewDocument');
|
|
240
215
|
if (network === 'testnet') {
|
|
241
216
|
await bounded(client.send('Fetch.enable', {
|
|
@@ -244,16 +219,6 @@ async function installAppHooks(sessionId, targetId) {
|
|
|
244
219
|
}
|
|
245
220
|
}
|
|
246
221
|
|
|
247
|
-
// Where a request went, without its query string or body.
|
|
248
|
-
function shortUrl(url) {
|
|
249
|
-
try {
|
|
250
|
-
const parsed = new URL(url);
|
|
251
|
-
return `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
|
|
252
|
-
} catch {
|
|
253
|
-
return String(url).slice(0, 120);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
222
|
// Venue traffic per app session: the served-network check. Only venue
|
|
258
223
|
// endpoints count (the block list and the testnet endpoints); other hosts on a
|
|
259
224
|
// venue domain, such as the coin icons on app.hyperliquid.xyz, are ignored.
|
|
@@ -288,10 +253,10 @@ async function enableAppDomains(sessionId) {
|
|
|
288
253
|
const { frameTree } = await bounded(client.send('Page.getFrameTree', {}, sessionId), 'Page.getFrameTree');
|
|
289
254
|
if (frameTree?.frame?.url !== undefined) {
|
|
290
255
|
sessionCommittedUrls.set(sessionId, frameTree.frame.url);
|
|
291
|
-
|
|
256
|
+
binding.commit(sessionId, frameTree.frame.url, frameTree.frame.loaderId);
|
|
292
257
|
}
|
|
293
258
|
if (appSessions.has(sessionId)) hudSessions.add(sessionId);
|
|
294
|
-
|
|
259
|
+
binding.drain(sessionId);
|
|
295
260
|
}
|
|
296
261
|
|
|
297
262
|
// Redraw the HUD on app pages. The expression is a no-op when the same state
|
|
@@ -344,18 +309,13 @@ client.on('Target.targetCreated', ({ targetInfo }) => {
|
|
|
344
309
|
if (targetInfo.type === 'page') observeWalletSurface(targetInfo.targetId, targetInfo.url);
|
|
345
310
|
});
|
|
346
311
|
client.on('Target.detachedFromTarget', ({ sessionId }) => {
|
|
347
|
-
|
|
348
|
-
for (const call of pendingCalls.get(sessionId) ?? []) recordUnattributed(call, 'its tab detached first');
|
|
349
|
-
awaitingDocuments.delete(sessionId);
|
|
350
|
-
appliedCommits.delete(sessionId);
|
|
312
|
+
binding.detach(sessionId);
|
|
351
313
|
const targetId = sessionTargets.get(sessionId);
|
|
352
314
|
if (targetId && hookedTargets.get(targetId)?.sessionId === sessionId) hookedTargets.delete(targetId);
|
|
353
315
|
appSessions.delete(sessionId);
|
|
354
316
|
hudSessions.delete(sessionId);
|
|
355
317
|
sessionTargets.delete(sessionId);
|
|
356
318
|
sessionCommittedUrls.delete(sessionId);
|
|
357
|
-
sessionContexts.delete(sessionId);
|
|
358
|
-
pendingCalls.delete(sessionId);
|
|
359
319
|
});
|
|
360
320
|
// What the app tab's first navigation did, for the diagnostic when it never
|
|
361
321
|
// commits: main-frame commits, and the document's response or failure.
|
|
@@ -370,8 +330,7 @@ client.on('Page.frameNavigated', ({ frame }, sessionId) => {
|
|
|
370
330
|
if (sessionId === appNavigation.session && !frame.parentId) appNavigation.commits.push(pathOf(frame.url));
|
|
371
331
|
if (appSessions.has(sessionId) && !frame.parentId) {
|
|
372
332
|
sessionCommittedUrls.set(sessionId, frame.url);
|
|
373
|
-
|
|
374
|
-
drainPendingCalls(sessionId);
|
|
333
|
+
binding.commit(sessionId, frame.url, frame.loaderId);
|
|
375
334
|
}
|
|
376
335
|
});
|
|
377
336
|
client.on('Fetch.requestPaused', ({ requestId, request, resourceType }, sessionId) => {
|
|
@@ -407,25 +366,6 @@ client.on('Network.webSocketCreated', ({ requestId, url }, sessionId) => {
|
|
|
407
366
|
say(`blocked a mainnet WebSocket to ${shortUrl(url)}`);
|
|
408
367
|
}
|
|
409
368
|
});
|
|
410
|
-
client.on('Runtime.executionContextCreated', ({ context }, sessionId) => {
|
|
411
|
-
const contexts = sessionContexts.get(sessionId);
|
|
412
|
-
if (!contexts) return;
|
|
413
|
-
contexts.set(context.id, context);
|
|
414
|
-
if (isTopDefaultContext(sessionId, context)) {
|
|
415
|
-
// The navigation this document belongs to may already have committed
|
|
416
|
-
// (frameNavigated first) or not yet (the context first).
|
|
417
|
-
const awaiting = awaitingDocuments.get(sessionId);
|
|
418
|
-
context.documentUrl = awaiting ?? null;
|
|
419
|
-
awaitingDocuments.delete(sessionId);
|
|
420
|
-
}
|
|
421
|
-
drainPendingCalls(sessionId);
|
|
422
|
-
});
|
|
423
|
-
client.on('Runtime.executionContextDestroyed', ({ executionContextId }, sessionId) => {
|
|
424
|
-
sessionContexts.get(sessionId)?.delete(executionContextId);
|
|
425
|
-
});
|
|
426
|
-
client.on('Runtime.executionContextsCleared', (_params, sessionId) => {
|
|
427
|
-
sessionContexts.get(sessionId)?.clear();
|
|
428
|
-
});
|
|
429
369
|
for (const event of ['Page.domContentEventFired', 'Page.loadEventFired', 'Page.navigatedWithinDocument']) {
|
|
430
370
|
client.on(event, (_params, sessionId) => {
|
|
431
371
|
if (hudSessions.has(sessionId)) drawHud([sessionId]);
|
|
@@ -436,123 +376,6 @@ for (const event of ['Page.domContentEventFired', 'Page.loadEventFired', 'Page.n
|
|
|
436
376
|
watchFile(hudFile(runtimeDir), { interval: 500 }, () => drawHud());
|
|
437
377
|
const hudRepeat = setInterval(() => drawHud(), 2000);
|
|
438
378
|
|
|
439
|
-
// Answer a request the host refuses, so the page's promise settles instead of
|
|
440
|
-
// hanging; harmless where the preload (and its resolver) is absent.
|
|
441
|
-
function refuse(sessionId, executionContextId, id, message) {
|
|
442
|
-
const outcome = { error: { message, code: 4100 } };
|
|
443
|
-
const expression = `typeof window.${RESOLVE_FN} === 'function' && window.${RESOLVE_FN}(${JSON.stringify(id)}, ${JSON.stringify(outcome)})`;
|
|
444
|
-
return client.send('Runtime.evaluate', { expression, contextId: executionContextId }, sessionId).catch(() => {});
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function requestId(name, payload) {
|
|
448
|
-
if (name !== REQUEST_BINDING) return null;
|
|
449
|
-
try {
|
|
450
|
-
return JSON.parse(payload).id ?? null;
|
|
451
|
-
} catch {
|
|
452
|
-
return null;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
async function handleBindingCall({ name, payload, executionContextId }, sessionId) {
|
|
457
|
-
// Only the app's own top frame may log or ask the injected wallet to sign;
|
|
458
|
-
// anything else (an iframe, a blank popup, a page the tab navigated to) is
|
|
459
|
-
// dropped, and a refused request is answered with 4100.
|
|
460
|
-
if (!callerIsAppTopFrame(sessionId, executionContextId)) {
|
|
461
|
-
const context = sessionContexts.get(sessionId)?.get(executionContextId);
|
|
462
|
-
appendEntry({ kind: 'outside-app-frame', t: Date.now(), binding: name === REQUEST_BINDING ? 'request' : 'log', origin: context?.origin ?? null, document: context?.documentUrl ? shortUrl(context.documentUrl) : null });
|
|
463
|
-
say(`ignored ${name === REQUEST_BINDING ? 'wallet request' : 'log entry'} from outside the app top frame`);
|
|
464
|
-
const id = requestId(name, payload);
|
|
465
|
-
if (id !== null) await refuse(sessionId, executionContextId, id, 'Wallet requests are accepted only from the app top frame.');
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
|
-
if (name === LOG_BINDING) {
|
|
469
|
-
try {
|
|
470
|
-
appendEntry(JSON.parse(payload));
|
|
471
|
-
} catch (error) {
|
|
472
|
-
say(`unparseable log payload: ${error.message}`);
|
|
473
|
-
}
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
476
|
-
if (name !== REQUEST_BINDING || !wallet) return;
|
|
477
|
-
const { id, method, params } = JSON.parse(payload);
|
|
478
|
-
if (network === 'testnet' && /^eth_signTypedData/u.test(String(method))) {
|
|
479
|
-
let reason = null;
|
|
480
|
-
try {
|
|
481
|
-
reason = mainnetTypedDataReason(typeof params?.[1] === 'string' ? JSON.parse(params[1]) : params?.[1]);
|
|
482
|
-
} catch {
|
|
483
|
-
reason = null;
|
|
484
|
-
}
|
|
485
|
-
if (reason) {
|
|
486
|
-
appendEntry({ kind: 'refused-mainnet', t: Date.now(), method, reason, layer: 'wallet-host' });
|
|
487
|
-
await refuse(sessionId, executionContextId, id, 'Refused: this test run is held to Hyperliquid testnet and the request signs a mainnet action.');
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
let outcome;
|
|
492
|
-
try {
|
|
493
|
-
outcome = { result: await wallet.request({ method, params }) };
|
|
494
|
-
} catch (error) {
|
|
495
|
-
outcome = { error: { message: String(error.message), code: error.code ?? -32603 } };
|
|
496
|
-
}
|
|
497
|
-
await client.send(
|
|
498
|
-
'Runtime.evaluate',
|
|
499
|
-
{ expression: `window.${RESOLVE_FN}(${JSON.stringify(id)}, ${JSON.stringify(outcome)})`, contextId: executionContextId },
|
|
500
|
-
sessionId,
|
|
501
|
-
).catch((error) => say(`resolve ${method} failed: ${error.message}`));
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// A binding call can arrive before its execution context is reported (the
|
|
505
|
-
// domains are enabled after the tab resumes), or before its document's commit.
|
|
506
|
-
// Hold it until both are known; after a short wait it cannot be judged: it is
|
|
507
|
-
// logged as unattributed (and a request is refused).
|
|
508
|
-
function drainPendingCalls(sessionId) {
|
|
509
|
-
const queue = pendingCalls.get(sessionId);
|
|
510
|
-
if (!queue) return;
|
|
511
|
-
for (const call of [...queue]) {
|
|
512
|
-
const known = callAttributable(sessionId, call.executionContextId);
|
|
513
|
-
if (!known && Date.now() - call.at < PENDING_CALL_TTL_MS) continue;
|
|
514
|
-
queue.splice(queue.indexOf(call), 1);
|
|
515
|
-
if (known) {
|
|
516
|
-
handleBindingCall(call, sessionId);
|
|
517
|
-
continue;
|
|
518
|
-
}
|
|
519
|
-
recordUnattributed(call, 'its document was never attributed');
|
|
520
|
-
const id = requestId(call.name, call.payload);
|
|
521
|
-
if (id !== null) refuse(sessionId, call.executionContextId, id, 'Wallet request from a document the harness could not attribute.');
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
function recordUnattributed(call, why) {
|
|
526
|
-
let entry = {};
|
|
527
|
-
try {
|
|
528
|
-
entry = JSON.parse(call.payload);
|
|
529
|
-
} catch {
|
|
530
|
-
entry = {};
|
|
531
|
-
}
|
|
532
|
-
const binding = call.name === REQUEST_BINDING ? 'request' : 'log';
|
|
533
|
-
appendEntry({ kind: 'unattributed', t: Date.now(), binding, method: typeof entry.method === 'string' ? entry.method : null, loggedKind: typeof entry.kind === 'string' ? entry.kind : null, reason: why });
|
|
534
|
-
say(`a wallet ${binding === 'request' ? 'request' : 'log entry'} could not be judged (${why}); recorded as unattributed`);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
// A binding call can be judged once its execution context is known and, for
|
|
538
|
-
// the top frame's default context, the URL of that context's own document.
|
|
539
|
-
function callAttributable(sessionId, executionContextId) {
|
|
540
|
-
const context = sessionContexts.get(sessionId)?.get(executionContextId);
|
|
541
|
-
if (!context) return false;
|
|
542
|
-
return !isTopDefaultContext(sessionId, context) || context.documentUrl !== null;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
client.on('Runtime.bindingCalled', (call, sessionId) => {
|
|
546
|
-
if (callAttributable(sessionId, call.executionContextId)) {
|
|
547
|
-
handleBindingCall(call, sessionId);
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
const queue = pendingCalls.get(sessionId) ?? [];
|
|
551
|
-
queue.push({ ...call, at: Date.now() });
|
|
552
|
-
pendingCalls.set(sessionId, queue);
|
|
553
|
-
setTimeout(() => drainPendingCalls(sessionId), PENDING_CALL_TTL_MS + 50);
|
|
554
|
-
});
|
|
555
|
-
|
|
556
379
|
client.on('close', () => {
|
|
557
380
|
clearInterval(hudRepeat);
|
|
558
381
|
unwatchFile(hudFile(runtimeDir));
|
|
@@ -632,7 +455,7 @@ for (const info of startupTabs) {
|
|
|
632
455
|
// The first app document is a market page: it streams from the venue in the
|
|
633
456
|
// browser, which the served-network check below observes (the home page's
|
|
634
457
|
// data comes from the server render).
|
|
635
|
-
const startUrl = new URL(args['start-path'] ||
|
|
458
|
+
const startUrl = new URL(args['start-path'] || policy.startPath, `${appOrigin}/`).href;
|
|
636
459
|
appNavigation.session = appSession;
|
|
637
460
|
const navigation = await bounded(client.send('Page.navigate', { url: startUrl }, appSession), 'Page.navigate', 60000).catch((error) => fail(error.message));
|
|
638
461
|
const navigateStart = Date.now();
|
|
@@ -652,13 +475,7 @@ const evaluate = (expression, timeoutMs = 5000) => bounded(
|
|
|
652
475
|
// The first app document must carry the log binding and the installed
|
|
653
476
|
// preload (its marker), with the page's provider wrapped, or requests on it
|
|
654
477
|
// would go unrecorded.
|
|
655
|
-
const readyExpression =
|
|
656
|
-
`typeof window.${LOG_BINDING} === 'function'`,
|
|
657
|
-
...(signer === 'injected' ? [`typeof window.${REQUEST_BINDING} === 'function'`] : []),
|
|
658
|
-
`Boolean(window.__mmTerminalHarness && window.__mmTerminalHarness.signer === ${JSON.stringify(signer)} && window.__mmTerminalHarness.network === ${JSON.stringify(network)})`,
|
|
659
|
-
// The provider the app sees is the wrapped one, and no wrap failed.
|
|
660
|
-
'window.__mmTerminalHarness.wrapFailures.length === 0 && window.__mmTerminalHarness.covers(window.ethereum)',
|
|
661
|
-
].join(' && ');
|
|
478
|
+
const readyExpression = pageReadyExpression({ signer, refusesTypedData: refuseTypedData !== null });
|
|
662
479
|
let hooksReady = false;
|
|
663
480
|
for (let attempt = 0; attempt < 50 && !hooksReady; attempt += 1) {
|
|
664
481
|
hooksReady = (await evaluate(readyExpression)) === true;
|
|
@@ -670,10 +487,11 @@ if (network === 'testnet') {
|
|
|
670
487
|
// The block must hold before anything runs: a probe to the mainnet venue
|
|
671
488
|
// from the app page has to fail (HTTP through the request block and the
|
|
672
489
|
// resolver rule, WebSocket through the resolver rule).
|
|
673
|
-
const
|
|
674
|
-
const
|
|
675
|
-
const
|
|
676
|
-
const
|
|
490
|
+
const { probe } = policy;
|
|
491
|
+
const host = mainnetHosts.includes(probe.host) ? probe.host : mainnetHosts[0];
|
|
492
|
+
const httpProbe = await evaluate(`fetch('https://${host}${probe.httpPath}?${PROBE_MARK}', { method: 'POST', headers: { 'content-type': 'application/json' }, body: ${JSON.stringify(probe.httpBody)} }).then(() => 'reached', () => 'blocked')`, 20000);
|
|
493
|
+
const resolverProbe = await evaluate(`fetch('https://${host}:${RESOLVER_PROBE_PORT}${probe.httpPath}?${PROBE_MARK}', { method: 'POST' }).then(() => 'reached', () => 'blocked')`, 20000);
|
|
494
|
+
const wsProbe = await evaluate(`new Promise((resolve) => { const socket = new WebSocket('wss://${host}${probe.wsPath}?${PROBE_MARK}'); const done = (value) => { try { socket.close(); } catch {} resolve(value); }; socket.onopen = () => done('reached'); socket.onerror = () => done('blocked'); setTimeout(() => done('timeout'), 10000); })`, 20000);
|
|
677
495
|
if (httpProbe !== 'blocked' || resolverProbe !== 'blocked' || wsProbe !== 'blocked') {
|
|
678
496
|
fail(`the mainnet venue is reachable from the app (http ${httpProbe}, resolver ${resolverProbe}, websocket ${wsProbe})`);
|
|
679
497
|
}
|
package/bin/mm-harness
CHANGED
|
@@ -90,24 +90,6 @@ if [ -f "$ENSURE_DEPS" ]; then
|
|
|
90
90
|
bash "$ENSURE_DEPS" "$RUNNER_DIR"
|
|
91
91
|
fi
|
|
92
92
|
|
|
93
|
-
find_protocol_root() {
|
|
94
|
-
local start="$1"
|
|
95
|
-
local dir
|
|
96
|
-
dir="$(cd "$start" && pwd -P)"
|
|
97
|
-
while [ "$dir" != "/" ]; do
|
|
98
|
-
if [ -f "$dir/packages/recipe-runner/package.json" ] && [ -f "$dir/packages/protocol/package.json" ]; then
|
|
99
|
-
printf '%s\n' "$dir"
|
|
100
|
-
return 0
|
|
101
|
-
fi
|
|
102
|
-
if [ -f "$dir/farmslot/packages/recipe-runner/package.json" ] && [ -f "$dir/farmslot/packages/protocol/package.json" ]; then
|
|
103
|
-
printf '%s\n' "$dir/farmslot"
|
|
104
|
-
return 0
|
|
105
|
-
fi
|
|
106
|
-
dir="$(dirname "$dir")"
|
|
107
|
-
done
|
|
108
|
-
return 1
|
|
109
|
-
}
|
|
110
|
-
|
|
111
93
|
node_can_run_source_typescript() {
|
|
112
94
|
# Node refuses type-stripping for files under node_modules (ERR_UNSUPPORTED_NODE_
|
|
113
95
|
# MODULES_TYPE_STRIPPING). When this runner is installed as a dependency, the
|
|
@@ -183,20 +165,19 @@ if [ ! -f "$ENTRY_TS" ]; then
|
|
|
183
165
|
exit 1
|
|
184
166
|
fi
|
|
185
167
|
|
|
186
|
-
TSX_BIN
|
|
187
|
-
|
|
188
|
-
|
|
168
|
+
# ENTRY_TSX, not TSX_BIN: TSX_BIN is the caller's choice of runner for live
|
|
169
|
+
# adapter scripts too, so this fallback must not overwrite the exported value.
|
|
170
|
+
ENTRY_TSX="${TSX_BIN:-$RUNNER_DIR/node_modules/.bin/tsx}"
|
|
171
|
+
if [ ! -x "$ENTRY_TSX" ] && [ -x "$RUNNER_DIR/../../.bin/tsx" ]; then
|
|
172
|
+
ENTRY_TSX="$RUNNER_DIR/../../.bin/tsx"
|
|
189
173
|
fi
|
|
190
|
-
if [ ! -x "$
|
|
174
|
+
if [ ! -x "$ENTRY_TSX" ]; then
|
|
191
175
|
if [ -z "${FARMSLOT_ROOT:-}" ] && [ -f "$RUNNER_DIR/.farmslot-root" ]; then
|
|
192
176
|
FARMSLOT_ROOT="$(cat "$RUNNER_DIR/.farmslot-root")"
|
|
193
177
|
fi
|
|
194
|
-
if [ -z "${FARMSLOT_ROOT:-}" ]; then
|
|
195
|
-
FARMSLOT_ROOT="$(find_protocol_root "$RUNNER_DIR" || find_protocol_root "$PWD" || true)"
|
|
196
|
-
fi
|
|
197
178
|
if [ -n "${FARMSLOT_ROOT:-}" ]; then
|
|
198
179
|
FARMSLOT_ROOT="$(cd "$FARMSLOT_ROOT" && pwd -P)"
|
|
199
|
-
|
|
180
|
+
ENTRY_TSX="$FARMSLOT_ROOT/node_modules/.bin/tsx"
|
|
200
181
|
fi
|
|
201
182
|
fi
|
|
202
183
|
|
|
@@ -204,8 +185,8 @@ if node_can_run_source_typescript; then
|
|
|
204
185
|
exec node --no-warnings "$ENTRY_TS" "$@"
|
|
205
186
|
fi
|
|
206
187
|
|
|
207
|
-
if [ -x "$
|
|
208
|
-
exec "$
|
|
188
|
+
if [ -x "$ENTRY_TSX" ]; then
|
|
189
|
+
exec "$ENTRY_TSX" "$ENTRY_TS" "$@"
|
|
209
190
|
fi
|
|
210
191
|
|
|
211
192
|
echo "mm-harness: source checkout requires dist/mm-harness-cli.js, Node.js TypeScript type stripping, or a local tsx binary." >&2
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
function checkoutHas(target, relative) {
|
|
4
|
+
return fs.existsSync(path.join(target, relative));
|
|
5
|
+
}
|
|
6
|
+
function checkoutHasDir(target, relative) {
|
|
7
|
+
try {
|
|
8
|
+
return fs.statSync(path.join(target, relative)).isDirectory();
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export {
|
|
14
|
+
checkoutHas,
|
|
15
|
+
checkoutHasDir
|
|
16
|
+
};
|
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import {
|
|
5
5
|
collectorMatches,
|
|
6
6
|
collectorOwnerMarker,
|
|
7
|
+
collectorStatus,
|
|
7
8
|
isLegacyCollector,
|
|
8
9
|
isOwnedCollector,
|
|
9
10
|
isPriorInstallCollector,
|
|
@@ -80,6 +81,17 @@ async function stopProcess(pid) {
|
|
|
80
81
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
81
82
|
}
|
|
82
83
|
}
|
|
84
|
+
async function verifyOwnedCollectorControl(capture, control) {
|
|
85
|
+
const status = collectorStatus({
|
|
86
|
+
pidFile: capture.pidFile,
|
|
87
|
+
extensionLog: capture.extensionLog,
|
|
88
|
+
marker: collectorOwnerMarker(path.dirname(capture.pidFile)),
|
|
89
|
+
cdpPort: capture.cdpPort
|
|
90
|
+
});
|
|
91
|
+
if (!status.ok) return { ok: false, detail: status.detail };
|
|
92
|
+
const result = await control();
|
|
93
|
+
return { ok: result.ok, detail: result.ok ? `${status.detail}; ${result.detail}` : result.detail };
|
|
94
|
+
}
|
|
83
95
|
const ROTATE_LOG_BYTES = 32 * 1024 * 1024;
|
|
84
96
|
function rotateLargeLog(file) {
|
|
85
97
|
if (fileSize(file) < ROTATE_LOG_BYTES) return;
|
|
@@ -118,5 +130,6 @@ function containsAttachment(from) {
|
|
|
118
130
|
export {
|
|
119
131
|
COLLECTOR_SCRIPT,
|
|
120
132
|
ROTATE_LOG_BYTES,
|
|
121
|
-
ensureConsoleCollector
|
|
133
|
+
ensureConsoleCollector,
|
|
134
|
+
verifyOwnedCollectorControl
|
|
122
135
|
};
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import { manifestPath } from "../../paths.js";
|
|
2
|
+
import { perpsOrderInputFindings } from "../perps-order-inputs.js";
|
|
2
3
|
const coreActions = {
|
|
3
4
|
manifestPath: () => manifestPath("core"),
|
|
4
5
|
semantic: [
|
|
5
6
|
"metamask.perps.close_positions",
|
|
6
7
|
"metamask.perps.start_state",
|
|
7
8
|
"metamask.perps.read_snapshot",
|
|
8
|
-
"metamask.perps.read_account"
|
|
9
|
+
"metamask.perps.read_account",
|
|
10
|
+
"metamask.perps.quote_fees"
|
|
9
11
|
],
|
|
10
|
-
// read_account
|
|
11
|
-
liveOnly: ["metamask.perps.read_account"],
|
|
12
|
+
// read_account and quote_fees are Core's alone, and only its live adapter implements them.
|
|
13
|
+
liveOnly: ["metamask.perps.read_account", "metamask.perps.quote_fees"],
|
|
12
14
|
tsxLiveScripts: {
|
|
13
15
|
env: async (projectRoot, tempDir) => (await import("./live-script-env.js")).coreLiveScriptEnv(projectRoot, tempDir)
|
|
14
16
|
},
|
|
15
17
|
// Core declares no cdp.target; the probe matches the Extension's.
|
|
16
|
-
cdpTarget: { transport: "chrome-extension-cdp", probePath: "json/version" }
|
|
18
|
+
cdpTarget: { transport: "chrome-extension-cdp", probePath: "json/version" },
|
|
19
|
+
// ensure_positions and ensure_orders with an open or present state place an order.
|
|
20
|
+
inputFindings: (nodeId, node) => perpsOrderInputFindings(nodeId, node, { ensureChecksOnly: false })
|
|
17
21
|
};
|
|
18
22
|
export {
|
|
19
23
|
coreActions
|
|
@@ -1,8 +1,35 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { coreDependencyBlock } from "../../commands/core-readiness.js";
|
|
4
|
+
import { resolveLiveTsx } from "../../live-tsx.js";
|
|
3
5
|
const coreReadiness = {
|
|
4
|
-
compatibilityMode: (target) => fs.existsSync(path.join(target, "packages/perps-controller/src/index.ts")) ? "headless controller (no bridge)" : "unsupported/no bridge"
|
|
6
|
+
compatibilityMode: (target) => fs.existsSync(path.join(target, "packages/perps-controller/src/index.ts")) ? "headless controller (no bridge)" : "unsupported/no bridge",
|
|
7
|
+
checks: (target) => [coreControllerActionsCheck(target)]
|
|
5
8
|
};
|
|
9
|
+
function coreControllerActionsCheck(target) {
|
|
10
|
+
const block = coreDependencyBlock(target);
|
|
11
|
+
if (block) {
|
|
12
|
+
return {
|
|
13
|
+
id: "core-controller-actions",
|
|
14
|
+
status: "fail",
|
|
15
|
+
// A checkout with no dependencies installed is reported, not failed: the
|
|
16
|
+
// runtime check already says it is not ready and how to install. Once
|
|
17
|
+
// dependencies are installed, an action that still cannot run fails doctor.
|
|
18
|
+
required: block.code !== "CORE_DEPS_MISSING",
|
|
19
|
+
message: `metamask.perps.* actions cannot run: ${block.message}`,
|
|
20
|
+
detail: block.code,
|
|
21
|
+
userAction: block.userAction
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const runner = resolveLiveTsx(target);
|
|
25
|
+
return {
|
|
26
|
+
id: "core-controller-actions",
|
|
27
|
+
status: "pass",
|
|
28
|
+
required: true,
|
|
29
|
+
message: "metamask.perps.* actions can run.",
|
|
30
|
+
detail: runner ? `tsx (${runner.source}): ${runner.bin}` : void 0
|
|
31
|
+
};
|
|
32
|
+
}
|
|
6
33
|
export {
|
|
7
34
|
coreReadiness
|
|
8
35
|
};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
import { isRecord } from "@farmslot/recipe-cli/harness";
|
|
1
2
|
import { coreDependencyBlock } from "../../commands/core-readiness.js";
|
|
2
|
-
import { isRecord } from "../../commands/parse-args.js";
|
|
3
3
|
const coreRun = {
|
|
4
4
|
async dependencyBlock(target, { recipe, librarySources, action }) {
|
|
5
5
|
const usesController = action !== void 0 ? actionUsesCoreController(action) : await recipeUsesCoreController(recipe, librarySources);
|
|
6
6
|
return usesController ? coreDependencyBlock(target) : null;
|
|
7
7
|
},
|
|
8
|
-
violationUserAction: (violation) => violation.code === "WALLET_STATE_REQUIRED" ? "for signing,
|
|
8
|
+
violationUserAction: (violation) => violation.code === "WALLET_STATE_REQUIRED" ? "for signing, select a wallet-fixture.json account by name or by its address; an address outside the fixture works only for read-only actions" : void 0
|
|
9
9
|
};
|
|
10
10
|
async function recipeUsesCoreController(recipe, librarySources) {
|
|
11
11
|
if (objectUsesCoreController(recipe)) return true;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
import { shellQuote } from "@farmslot/recipe-cli/harness";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import { depsCheck } from "@farmslot/recipe-runner/runtime/deps-readiness";
|
|
3
4
|
import { defineAdapter } from "@farmslot/adapter-sdk";
|
|
4
|
-
import { shellQuote } from "../../commands/parse-args.js";
|
|
5
5
|
import { adapterRegistry } from "../registry.js";
|
|
6
6
|
import { coreActions } from "./actions.js";
|
|
7
7
|
import { coreHarness, corePrepare, coreRuntimeContext } from "./overlay.js";
|
|
8
8
|
import { coreReadiness } from "./readiness.js";
|
|
9
9
|
import { coreRun } from "./run.js";
|
|
10
|
+
import { coreDependencyBlock } from "../../commands/core-readiness.js";
|
|
11
|
+
import { checkoutHas, checkoutHasDir } from "../checkout-files.js";
|
|
10
12
|
const coreSurface = defineAdapter({
|
|
11
13
|
id: "core",
|
|
12
14
|
sdkVersion: 1,
|
|
@@ -15,6 +17,16 @@ const coreSurface = defineAdapter({
|
|
|
15
17
|
},
|
|
16
18
|
async runtimeStatus(target) {
|
|
17
19
|
const deps = depsCheck(path.resolve(target));
|
|
20
|
+
const block = deps.status === "current" ? coreDependencyBlock(target) : null;
|
|
21
|
+
if (block) {
|
|
22
|
+
return {
|
|
23
|
+
decision: "blocked",
|
|
24
|
+
reasonCode: block.code,
|
|
25
|
+
reasons: [`Core is headless; controller actions cannot run: ${block.message}`],
|
|
26
|
+
nextAction: block.userAction,
|
|
27
|
+
deps: deps.status
|
|
28
|
+
};
|
|
29
|
+
}
|
|
18
30
|
const ready = deps.status === "current";
|
|
19
31
|
return {
|
|
20
32
|
decision: ready ? "ready" : "install",
|
|
@@ -56,6 +68,9 @@ const coreSurface = defineAdapter({
|
|
|
56
68
|
appLogSource() {
|
|
57
69
|
return null;
|
|
58
70
|
},
|
|
71
|
+
detect: {
|
|
72
|
+
files: (target) => checkoutHasDir(target, "packages/perps-controller") && checkoutHas(target, "yarn.lock")
|
|
73
|
+
},
|
|
59
74
|
hints: {
|
|
60
75
|
launch: "mm-harness run <recipe> # run recipes against the headless core",
|
|
61
76
|
relaunch: "mm-harness verify",
|