@deeeed/metamask-harness 0.80.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 +26 -0
- package/adapters/manifest.json +14 -30
- 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/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 +112 -5
- 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/proof-failure.js +11 -0
- package/dist/adapters/terminal/actions.js +4 -1
- package/dist/adapters/terminal/doctor-checks.js +2 -0
- package/dist/adapters/terminal/launch.js +3 -0
- package/dist/adapters/terminal/overlay.js +8 -1
- package/dist/adapters/terminal/run.js +8 -3
- package/dist/adapters/terminal/surface.js +2 -0
- package/dist/adapters.js +20 -4
- package/dist/cli.js +31 -6
- package/dist/commands/check.js +30 -6
- package/dist/commands/completion-candidates.js +5 -5
- package/dist/commands/self-test.js +4 -3
- package/dist/engine.js +57 -0
- package/dist/funding-execution-context.js +81 -0
- package/dist/mm-harness-cli.js +3 -3
- package/dist/performance-observation.js +1 -3
- package/dist/recipe-assistance-context.js +6 -7
- package/dist/runner.js +2 -1
- package/docs/CONTRIBUTING.md +4 -2
- package/docs/RECIPES.md +31 -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/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/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/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 +6 -2
- package/library/manifests/extension.action-manifest.json +4 -0
- package/library/manifests/mobile.action-manifest.json +12 -0
- package/package.json +4 -4
- package/site/assets/help-recipes.json +1 -0
- 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/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/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.81.0 - 2026-10-07
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Core's headless `RewardsController:getPerpsDiscountForAccount` now asks the rewards API for the Perps fee discount, with the calls Mobile's RewardsController makes for the selected wallet fixture account: the opt-in check, `mobile-login` signed with personal_sign, then `/vip/fees`, and Mobile's discount math. Like Mobile, it logs in when the opt-in check itself fails, reads a 401 from login as not opted in, and gives each request 10 seconds. An account that never opted in, or an address outside the fixture, gets no discount (`null`). The session token stays in memory, and an error keeps only the HTTP status, never the response body. The backend defaults to UAT, as on Mobile dev builds; `CORE_PERPS_REWARDS_API_URL` selects another. `metamask.perps.quote_fees` reports `rewardsSource: "rewards-api"` with each call it answered; `rewards_discount_bips` still replaces the answer, to prove a discount the non-VIP fixture accounts cannot get.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- **BREAKING:** Perps orders follow one rule on Mobile, Extension and Core (F21). Testnet orders need no funding proof, so `run` and `call metamask.perps.place_order --adapter extension` work without the four `--funding-*` files, as on Mobile. A mainnet order is allowed only from a synced fixture account, as a 1x isolated market order with `close_immediately: true` and `network: mainnet` declared, whose worst-case fill (one size step plus max slippage over the request) stays within $15; anything else is refused before dispatch with `PERPS_MAINNET_ORDER_REFUSED`. This newly guards Mobile mainnet orders, which had no check. `place_order` takes `close_immediately` (and Mobile takes `network`): from a flat market (no position or resting order), the action closes the opened position itself, on the account and network the rule allowed, and proves the market flat. Extension background market placements now prove their postcondition on current builds (positions carry a signed size, no side). On the Extension, a run without funding files runs its mutations in a fixture-policy lane with a run-owned execution ledger; the funding files remain the stricter release lane. Core keeps `CORE_PERPS_ALLOW_MAINNET_WRITES=1`. The fixture account derivation behind `metamask.wallet.list_accounts` moved to `library/actions/shared/wallet/fixture-accounts.mjs`, shared with the order rule.
|
|
14
|
+
- Extension screenshots: a capture-helper that times out is killed with SIGKILL together with everything it started (the npm wrapper runs the native binary as a child). It can ignore SIGTERM and linger, making later captures hang. Before the next capture, this user's snapshot helpers orphaned by an earlier run (matched by executable and subcommand, including the configured path) are killed with their children; recording sessions and helpers whose harness is alive are left alone. A CDP fallback screenshot records `fallbackReason` (the capture-helper error) next to `fallbackFrom`, for `run`'s fallback listing in the next `@farmslot/recipe-cli` (deeeed/farmslot#829).
|
|
15
|
+
- Extension `check diff` also runs `yarn verify-locales --quiet` when a file under `app/_locales/**` changed, moved or was deleted, and `yarn circular-deps:check` when any JS/TS file changed, moved or was deleted; otherwise each reports `skip`. `changedFiles` now lists deleted files and both sides of a rename (lint, format and tests still run on existing files only).
|
|
16
|
+
- Help text only: the `help` quality guidance says to assert a test command's literal pass count with `assert_output`, since `jest -t` exits 0 when no test matches.
|
|
17
|
+
- The Web Terminal's venue policy lives in one module, `adapters/terminal/policy.mjs`: the blocked and served Hyperliquid hosts, link hosts, the launch probe, the testnet typed-data refusal, the start page (`/order/BTC`), the injected wallet's start chain (42161), the checkout dependencies and the dev-server testnet variable. The launch, verify, doctor and the wallet host take it as an argument instead of naming the venue themselves; the leaf scripts `adapters/terminal/{launch,verify,wallet-host}.mjs` require `--policy-module <path>`, which the Terminal adapter passes. Behaviour is unchanged.
|
|
18
|
+
- The Web Terminal's web3 part moves to `@farmslot/adapter-web/dapp` (`@farmslot/adapter-web` 0.3.0): the strict test wallet, the page script that logs the app's wallet requests (or injects the provider the wallet host answers), the wallet host's judgement of each request's frame and document, and the reading and assertion of the wallet request log behind `metamask.wallet.read_signatures` / `assert_signatures`. The exact-origin checks move to `@farmslot/adapter-web/origin`. The Terminal passes in its own parts: the Hyperliquid testnet refusal, the injected wallet's MetaMask identity, and the L1 and mainnet rules of the signature assertion. Log entries, refusals and action outputs are unchanged. The page's bindings are now `__farmslotWallet{Log,Request,Resolve}` and its marker `__farmslotDapp` (were `__mmTerminal*`).
|
|
19
|
+
- Depend on `@farmslot/recipe-cli` 0.5.0 and `@farmslot/adapter-sdk` 0.4.0 (were 0.4.1 and 0.3.1), which carry the run and call engine and action discovery. `@farmslot/adapter-web` moves 0.2.0 → 0.3.1 (`./dapp` from 0.3.0, the probe fix in 0.3.1); `@farmslot/adapter-rn` 0.17.0, `@farmslot/recipe-runner` 0.24.0, `@farmslot/agent-runtime` 0.17.0 and `@farmslot/protocol` 0.34.0 are unchanged, so the install still holds one `@farmslot/recipe-runner`.
|
|
20
|
+
- `run` and `call` move to `@farmslot/recipe-cli/harness` with their engine (recipe execution, preflight, bounded healing, recipe library resolution, static validation, network observation) and `describeManifestActions`, the action catalog entries they share with `actions`. mm-harness hands them one engine, `src/engine.ts`: the bundled `metamask` library, `src/manifest.ts`, the MetaMask action risk table, `createMetaMaskRunner`, funded mutations bound from the `--funding-*` flags, and the console classifier. Device targeting and the `fixture.file` plan step stay in mm-harness and are passed as command options. Flags, output and the CLI goldens are unchanged.
|
|
21
|
+
- The Mobile and Extension proof-runtime checks (`MOBILE_SOURCE_NOT_LOADED`, `EXTENSION_RUNTIME_*`) move into `src/adapters/mobile/run.ts` and `src/adapters/extension/run.ts`, and the Mobile and Extension network backends into `src/adapters/{mobile,extension}/network-observer.ts`. Platforms declare network and performance observation through the SDK's `observation`; the performance observer stays in mm-harness and starts through `observation.performance.start`.
|
|
22
|
+
- The Perps order rule (an order needs market, side and amount) runs in each platform's `actions.inputFindings` through `src/adapters/perps-order-inputs.ts`; `AdapterSurface` drops `actions.ensureChecksOnly` (the Extension passes it to the rule) and `observation` (now the SDK's). `src/metamask-action-validation.ts` is gone; the node loop is `@farmslot/recipe-cli`'s `validateActionInputs`.
|
|
23
|
+
- Discovery moves to `@farmslot/recipe-cli/harness` and runs on `src/engine.ts`'s catalog: `actions` (lookup, search, categories, `--raw`, `--matrix`), `run --list`, `run <recipe> --describe` and `call --list`. `src/commands/manifest.ts` and `src/commands/list-executables.ts` are gone. Flags, `--json` output and the CLI goldens are unchanged. Two human-output changes:
|
|
24
|
+
- `actions --action <name>` (one match) and `call <action> --help` print the same action detail: the call form, description, source and adapter, risk, result cases, typed fields, up to two example calls, and the first example as a runnable call with its recipe node. The example calls now carry only the action's fields (no `next=done`, `cases` or `default`), shell-quoted, and use the full name when the short one is ambiguous. `actions --action` used to print a one-line catalog entry; `call --help` gains the risk, result cases and runnable example.
|
|
25
|
+
- The `actions --matrix` columns come from the registered platforms (same four, same order), and a capability no platform declares reads `across Mobile, Extension, Core, or Terminal` (was `across Mobile, Extension, or Core`).
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- Browser probes stop when the resolver is stopped: `@farmslot/adapter-web` 0.3.1 (was 0.3.0) stops the probe browser and removes its profile when the resolver gets a signal (SIGTERM, SIGINT, SIGHUP, SIGQUIT); a probe browser left by a resolver killed with SIGKILL is reaped by the next probe.
|
|
30
|
+
|
|
5
31
|
## 0.80.0 - 2026-10-06
|
|
6
32
|
|
|
7
33
|
### Added
|
package/adapters/manifest.json
CHANGED
|
@@ -588,15 +588,15 @@
|
|
|
588
588
|
"entry": "adapters/terminal/launch.mjs",
|
|
589
589
|
"kind": "node",
|
|
590
590
|
"purpose": "Start or reuse the Web Terminal slot browser (Chrome for Testing unless TERMINAL_CHROME_BIN; MetaMask extension with the fixture imported, or the injected strict wallet) and its wallet host. On macOS the browser starts through LaunchServices with no startup window; in testnet mode the venue SDK's mainnet hosts are unresolvable for the whole browser.",
|
|
591
|
-
"inputs": "--target --cdp-port --app-port [--signer extension|injected] [--account] [--fresh-profile] [--headless|--headful] [--slow-mo] [--network mainnet --mainnet-confirmation REAL_FUNDS] [--json]; env TERMINAL_CHROME_BIN, TERMINAL_EXTENSION_DIR, TERMINAL_EXTENSION_CHECKOUT, RECIPE_WALLET_FIXTURE, TERMINAL_HEADLESS, TERMINAL_SLOW_MO, TERMINAL_WINDOW, TERMINAL_SCREEN",
|
|
591
|
+
"inputs": "--target --cdp-port --app-port --policy-module [--signer extension|injected] [--account] [--fresh-profile] [--headless|--headful] [--slow-mo] [--network mainnet --mainnet-confirmation REAL_FUNDS] [--json]; env TERMINAL_CHROME_BIN, TERMINAL_EXTENSION_DIR, TERMINAL_EXTENSION_CHECKOUT, RECIPE_WALLET_FIXTURE, TERMINAL_HEADLESS, TERMINAL_SLOW_MO, TERMINAL_WINDOW, TERMINAL_SCREEN",
|
|
592
592
|
"outputs": "<runtime>/terminal/browser.json + pid/log files; JSON on stdout; exit 0/1/2"
|
|
593
593
|
},
|
|
594
594
|
{
|
|
595
595
|
"id": "terminal/wallet-host",
|
|
596
596
|
"entry": "adapters/terminal/wallet-host.mjs",
|
|
597
597
|
"kind": "node",
|
|
598
|
-
"purpose": "Long-running CDP observer: opens the app tab blank, hooks it once per target, navigates it to the app and verifies the log binding before ready; logs every wallet request from the app top frame and every MetaMask confirmation shown; answers the injected strict wallet in injected mode (other frames and blank popups get 4100).",
|
|
599
|
-
"inputs": "--cdp-port --runtime-dir --signer --app-origin [--extension-id] [--target --account --start-chain] [--browser-app <.app>]",
|
|
598
|
+
"purpose": "Long-running CDP observer: opens the app tab blank, hooks it once per target, navigates it to the app and verifies the log binding before ready; logs every wallet request from the app top frame and every MetaMask confirmation shown; answers the injected strict wallet in injected mode (other frames and blank popups get 4100). The page script, the frame judgement and the strict wallet are @farmslot/adapter-web/dapp's; the host passes the testnet policy and the injected wallet's identity.",
|
|
599
|
+
"inputs": "--cdp-port --runtime-dir --signer --app-origin --policy-module [--extension-id] [--target --account --start-chain] [--browser-app <.app>]",
|
|
600
600
|
"outputs": "<runtime>/terminal/wallet-requests.jsonl, wallet-host.ready"
|
|
601
601
|
},
|
|
602
602
|
{
|
|
@@ -628,7 +628,7 @@
|
|
|
628
628
|
"entry": "adapters/terminal/verify.mjs",
|
|
629
629
|
"kind": "node",
|
|
630
630
|
"purpose": "Read-only terminal slot readiness: checkout, deps, dev server, forced testnet, fixture, browser start probe, extension copy, CDP port.",
|
|
631
|
-
"inputs": "--target [--cdp-port] [--watcher-port] [--signer] [--account] [--json]",
|
|
631
|
+
"inputs": "--policy-module --target [--cdp-port] [--watcher-port] [--signer] [--account] [--json]",
|
|
632
632
|
"outputs": "readiness report; exit 0 ready / 1 not ready"
|
|
633
633
|
},
|
|
634
634
|
{
|
|
@@ -639,22 +639,6 @@
|
|
|
639
639
|
"inputs": "CdpClient.connect(url)",
|
|
640
640
|
"outputs": "send/on"
|
|
641
641
|
},
|
|
642
|
-
{
|
|
643
|
-
"id": "terminal/lib-page-script",
|
|
644
|
-
"entry": "adapters/terminal/lib/page-script.mjs",
|
|
645
|
-
"kind": "lib",
|
|
646
|
-
"purpose": "Page script source: wallet request logging and the injected strict provider (EIP-6963 io.metamask).",
|
|
647
|
-
"inputs": "pageScriptSource({ signer, appOrigin })",
|
|
648
|
-
"outputs": "script source string"
|
|
649
|
-
},
|
|
650
|
-
{
|
|
651
|
-
"id": "terminal/lib-strict-wallet",
|
|
652
|
-
"entry": "adapters/terminal/lib/strict-wallet.mjs",
|
|
653
|
-
"kind": "lib",
|
|
654
|
-
"purpose": "EIP-1193 strict test wallet enforcing typed-data chainId == active chain.",
|
|
655
|
-
"inputs": "createStrictWallet({ account, chainId })",
|
|
656
|
-
"outputs": "request()"
|
|
657
|
-
},
|
|
658
642
|
{
|
|
659
643
|
"id": "terminal/lib-processes",
|
|
660
644
|
"entry": "adapters/terminal/lib/processes.mjs",
|
|
@@ -668,7 +652,7 @@
|
|
|
668
652
|
"entry": "adapters/terminal/lib/readiness.mjs",
|
|
669
653
|
"kind": "lib",
|
|
670
654
|
"purpose": "Terminal slot readiness checks shared by verify, doctor and the surface.",
|
|
671
|
-
"inputs": "terminalReadiness({ target, appPort, cdpPort, signer }), assertLaunchNetwork(target, { network, mainnetConfirmation }), devServerTestnetDiagnostic(target, { appPort }) (advisory)",
|
|
655
|
+
"inputs": "terminalReadiness({ policy, target, appPort, cdpPort, signer }), assertLaunchNetwork(target, { network, mainnetConfirmation }), devServerTestnetDiagnostic(target, { appPort, variable }) (advisory)",
|
|
672
656
|
"outputs": "checks[]"
|
|
673
657
|
},
|
|
674
658
|
{
|
|
@@ -687,22 +671,22 @@
|
|
|
687
671
|
"inputs": "nextHudState, hudRenderExpression",
|
|
688
672
|
"outputs": "HUD state, expression source"
|
|
689
673
|
},
|
|
690
|
-
{
|
|
691
|
-
"id": "terminal/lib-origin",
|
|
692
|
-
"entry": "adapters/terminal/lib/origin.mjs",
|
|
693
|
-
"kind": "lib",
|
|
694
|
-
"purpose": "Exact app-origin and app top-frame checks for targets, wallet requests and log entries.",
|
|
695
|
-
"inputs": "isAppUrl(url, appOrigin), isAppTopFrameContext(context, { targetId, appOrigin })",
|
|
696
|
-
"outputs": "boolean"
|
|
697
|
-
},
|
|
698
674
|
{
|
|
699
675
|
"id": "terminal/lib-mainnet-guard",
|
|
700
676
|
"entry": "adapters/terminal/lib/mainnet-guard.mjs",
|
|
701
677
|
"kind": "lib",
|
|
702
678
|
"purpose": "Testnet enforcement: the venue endpoint hosts (from the venue SDK, @metamask/perps-controller and the app source in the checkout), the browser's host-resolver block, and the mainnet typed-data classifier used by the page script and the wallet host.",
|
|
703
|
-
"inputs": "venueHosts(checkout), hostResolverRules(hosts), isMainnetVenueUrl(url, hosts), mainnetTypedDataReason(data)",
|
|
679
|
+
"inputs": "venueHosts(checkout), hostResolverRules(hosts), isMainnetVenueUrl(url, hosts), mainnetTypedDataReason(data), TESTNET_TYPED_DATA_POLICY",
|
|
704
680
|
"outputs": "host list, launch switch, refusal reason"
|
|
705
681
|
},
|
|
682
|
+
{
|
|
683
|
+
"id": "terminal/policy",
|
|
684
|
+
"entry": "adapters/terminal/policy.mjs",
|
|
685
|
+
"kind": "lib",
|
|
686
|
+
"purpose": "The Web Terminal's venue policy: blocked and served venue hosts, link hosts, the launch probe, the testnet typed-data refusal, the start page, the injected wallet's start chain, the checkout dependencies and the dev-server testnet variable. launch, verify, doctor and the wallet host take it as an argument (--policy-module for the leaf processes).",
|
|
687
|
+
"inputs": "policy.venueHosts(checkout)",
|
|
688
|
+
"outputs": "policy"
|
|
689
|
+
},
|
|
706
690
|
{
|
|
707
691
|
"id": "terminal/surface",
|
|
708
692
|
"entry": "src/adapters/terminal/surface.ts",
|
|
@@ -29,7 +29,7 @@ import { createRequire } from 'node:module';
|
|
|
29
29
|
import { appendFileSync, chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
30
30
|
import os from 'node:os';
|
|
31
31
|
import path from 'node:path';
|
|
32
|
-
import { fileURLToPath } from 'node:url';
|
|
32
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
33
33
|
|
|
34
34
|
import {
|
|
35
35
|
fixtureEntry,
|
|
@@ -42,7 +42,7 @@ import { CdpClient, browserWebSocketUrl } from './lib/cdp-client.mjs';
|
|
|
42
42
|
import { clearHudState } from './lib/hud.mjs';
|
|
43
43
|
import { browserDisplayArgs, resolveHeadless, resolveSlowMo, windowPlacement, writeProfileZoom } from './lib/display.mjs';
|
|
44
44
|
import { cdpListenerPids, ownedBrowserPids, ownerMarker, ownsProcess, pidAlive, stopTerminalBrowser, terminate } from './lib/processes.mjs';
|
|
45
|
-
import { hostResolverRules
|
|
45
|
+
import { hostResolverRules } from './lib/mainnet-guard.mjs';
|
|
46
46
|
import { assertLaunchNetwork, devServerTestnetDiagnostic } from './lib/readiness.mjs';
|
|
47
47
|
|
|
48
48
|
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -51,7 +51,7 @@ const HARNESS_ROOT = path.resolve(SCRIPT_DIR, '../..');
|
|
|
51
51
|
const FIXTURE_STATE_SCRIPT = path.join(HARNESS_ROOT, 'adapters/extension/wallet-fixture-state.cjs');
|
|
52
52
|
|
|
53
53
|
function usage() {
|
|
54
|
-
return 'Usage: launch.mjs --target <checkout> --cdp-port <port> --app-port <port> [--signer extension|injected] [--account <name>] [--fresh-profile] [--headless | --headful] [--slow-mo <ms>] [--network testnet|mainnet --mainnet-confirmation REAL_FUNDS] [--start-chain <id>] [--json]';
|
|
54
|
+
return 'Usage: launch.mjs --target <checkout> --cdp-port <port> --app-port <port> --policy-module <venue policy module> [--signer extension|injected] [--account <name>] [--fresh-profile] [--headless | --headful] [--slow-mo <ms>] [--network testnet|mainnet --mainnet-confirmation REAL_FUNDS] [--start-chain <id>] [--json]';
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
export function parseLaunchArgs(argv) {
|
|
@@ -67,7 +67,7 @@ export function parseLaunchArgs(argv) {
|
|
|
67
67
|
args[flag.slice(2)] = argv[index + 1];
|
|
68
68
|
index += 1;
|
|
69
69
|
}
|
|
70
|
-
for (const key of ['target', 'cdp-port', 'app-port']) {
|
|
70
|
+
for (const key of ['target', 'cdp-port', 'app-port', 'policy-module']) {
|
|
71
71
|
if (!args[key]) throw Object.assign(new Error(`Missing --${key}\n${usage()}`), { exitCode: 2 });
|
|
72
72
|
}
|
|
73
73
|
args.signer = resolveSigner(args.signer);
|
|
@@ -344,7 +344,9 @@ function writePrivateFile(file, text) {
|
|
|
344
344
|
// which waits for a cold dev-server compile.
|
|
345
345
|
export const LAUNCH_TIMEOUTS = Object.freeze({ browserStartMs: 90000, hostReadyMs: 180000 });
|
|
346
346
|
|
|
347
|
-
|
|
347
|
+
// `policy`: the venue policy (adapters/terminal/policy.mjs).
|
|
348
|
+
export async function launchTerminalBrowser(args, env = process.env, { timeouts = LAUNCH_TIMEOUTS, policy } = {}) {
|
|
349
|
+
if (!policy?.module) throw new Error('launchTerminalBrowser needs the venue policy (adapters/terminal/policy.mjs).');
|
|
348
350
|
const target = path.resolve(args.target);
|
|
349
351
|
const cdpPort = Number(args['cdp-port']);
|
|
350
352
|
const appPort = Number(args['app-port']);
|
|
@@ -353,12 +355,12 @@ export async function launchTerminalBrowser(args, env = process.env, { timeouts
|
|
|
353
355
|
// browser: the SDK's mainnet venue hosts are unresolvable for the whole
|
|
354
356
|
// browser, and the wallet host blocks, logs and refuses anything mainnet.
|
|
355
357
|
const network = assertLaunchNetwork(target, { network: args.network, mainnetConfirmation: args['mainnet-confirmation'] });
|
|
356
|
-
const venues = network === 'testnet' ? venueHosts(target) : {
|
|
357
|
-
const mainnetHosts = venues.
|
|
358
|
+
const venues = network === 'testnet' ? policy.venueHosts(target) : { blocked: [], served: [] };
|
|
359
|
+
const mainnetHosts = venues.blocked;
|
|
358
360
|
// What the running browser enforces; a browser enforcing anything else is
|
|
359
361
|
// never reused (its resolver rules are fixed at start).
|
|
360
|
-
const enforcementFingerprint = createHash('sha256').update(JSON.stringify({ network, mainnetHosts, testnetHosts: venues.
|
|
361
|
-
const devServer = network === 'testnet' ? devServerTestnetDiagnostic(target, { appPort }) : null;
|
|
362
|
+
const enforcementFingerprint = createHash('sha256').update(JSON.stringify({ network, mainnetHosts, testnetHosts: venues.served })).digest('hex').slice(0, 16);
|
|
363
|
+
const devServer = network === 'testnet' ? devServerTestnetDiagnostic(target, { appPort, variable: policy.testnetVariable }) : null;
|
|
362
364
|
if (devServer && !(devServer.known && devServer.forced)) {
|
|
363
365
|
log(`WARN dev server testnet check: ${devServer.detail} (advisory; the browser enforces testnet itself)`);
|
|
364
366
|
}
|
|
@@ -541,11 +543,12 @@ export async function launchTerminalBrowser(args, env = process.env, { timeouts
|
|
|
541
543
|
'--app-origin', appOrigin,
|
|
542
544
|
'--target', target,
|
|
543
545
|
'--account', args.account,
|
|
544
|
-
'--start-chain', String(args['start-chain'] ??
|
|
546
|
+
'--start-chain', String(args['start-chain'] ?? policy.startChain),
|
|
545
547
|
'--network', network,
|
|
548
|
+
'--policy-module', policy.module,
|
|
546
549
|
ownerMarker('wallet-host', runtime),
|
|
547
550
|
];
|
|
548
|
-
if (network === 'testnet') hostArgs.push('--mainnet-hosts', mainnetHosts.join(','), '--testnet-hosts', venues.
|
|
551
|
+
if (network === 'testnet') hostArgs.push('--mainnet-hosts', mainnetHosts.join(','), '--testnet-hosts', venues.served.join(','));
|
|
549
552
|
if (extension?.id) hostArgs.push('--extension-id', extension.id);
|
|
550
553
|
if (guardFocus) hostArgs.push('--observe-focus', '1');
|
|
551
554
|
if (launchMethod === LAUNCH_SERVICES) hostArgs.push('--window', [windowBounds.x, windowBounds.y, windowBounds.width, windowBounds.height].join(','));
|
|
@@ -568,7 +571,7 @@ export async function launchTerminalBrowser(args, env = process.env, { timeouts
|
|
|
568
571
|
signer,
|
|
569
572
|
network,
|
|
570
573
|
networkEnforcement: network === 'testnet'
|
|
571
|
-
? { mode: 'enforced', mainnetHosts, testnetHosts: venues.
|
|
574
|
+
? { mode: 'enforced', mainnetHosts, testnetHosts: venues.served, fingerprint: enforcementFingerprint, layers: ['host-resolver-rules', 'cdp-fetch-block', 'wallet-refusal', 'served-network-check'] }
|
|
572
575
|
: { mode: 'disabled', reason: 'network=mainnet with mainnet_confirmation=REAL_FUNDS', fingerprint: enforcementFingerprint },
|
|
573
576
|
devServerDiagnostic: devServer,
|
|
574
577
|
consoleOffsets,
|
|
@@ -618,7 +621,8 @@ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.me
|
|
|
618
621
|
process.exit(0);
|
|
619
622
|
}
|
|
620
623
|
try {
|
|
621
|
-
const
|
|
624
|
+
const { policy } = await import(pathToFileURL(path.resolve(args['policy-module'])).href);
|
|
625
|
+
const state = await launchTerminalBrowser(args, process.env, { policy });
|
|
622
626
|
process.stdout.write(`${JSON.stringify({ status: 'pass', ...state }, null, args.json ? 0 : 2)}\n`);
|
|
623
627
|
} catch (error) {
|
|
624
628
|
const message = String(error?.stderr ?? '').trim() || error.message;
|
|
@@ -175,3 +175,12 @@ export function mainnetTypedDataReason(data) {
|
|
|
175
175
|
if (data?.primaryType === 'Agent' && message.source === 'a') return 'L1 Agent source=a';
|
|
176
176
|
return null;
|
|
177
177
|
}
|
|
178
|
+
|
|
179
|
+
// The wallet refusal for testnet runs, as the @farmslot/adapter-web/dapp page
|
|
180
|
+
// script and wallet-request binding take it: mainnetTypedDataReason runs in
|
|
181
|
+
// the page too, so it stays self-contained.
|
|
182
|
+
export const TESTNET_TYPED_DATA_POLICY = Object.freeze({
|
|
183
|
+
reason: mainnetTypedDataReason,
|
|
184
|
+
kind: 'refused-mainnet',
|
|
185
|
+
message: 'Refused: this test run is held to Hyperliquid testnet and the request signs a mainnet action.',
|
|
186
|
+
});
|
|
@@ -34,8 +34,9 @@ async function httpStatus(url, timeoutMs = 20000) {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
// Diagnostic only. Whether the slot's dev server looks configured for testnet:
|
|
37
|
-
// the farm starts `next dev` (temp/farmslot/next-dev.pid) with
|
|
38
|
-
//
|
|
37
|
+
// the farm starts `next dev` (temp/farmslot/next-dev.pid) with the policy's
|
|
38
|
+
// testnet variable set to true (the Terminal's
|
|
39
|
+
// NEXT_PUBLIC_HYPERLIQUID_FORCE_TESTNET). Process inspection cannot prove
|
|
39
40
|
// what the browser talks to (macOS reports a process's initial environment,
|
|
40
41
|
// and lsof cannot see other users' sockets), so this only warns; the browser
|
|
41
42
|
// is held to testnet by the network and wallet layers (mainnet-guard.mjs, the
|
|
@@ -45,7 +46,6 @@ async function httpStatus(url, timeoutMs = 20000) {
|
|
|
45
46
|
// entries from sysctl KERN_PROCARGS2) and must carry the testnet variable. An
|
|
46
47
|
// unreadable process makes the result unknown.
|
|
47
48
|
export const DEV_SERVER_PID_FILE = 'temp/farmslot/next-dev.pid';
|
|
48
|
-
const FORCE_VARIABLE = 'NEXT_PUBLIC_HYPERLIQUID_FORCE_TESTNET';
|
|
49
49
|
|
|
50
50
|
function run(command, args) {
|
|
51
51
|
try {
|
|
@@ -142,7 +142,7 @@ function chainTo(listener, root, parents) {
|
|
|
142
142
|
return null;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
export function devServerTestnetDiagnostic(target, { appPort, probe = systemProbe } = {}) {
|
|
145
|
+
export function devServerTestnetDiagnostic(target, { appPort, variable, probe = systemProbe } = {}) {
|
|
146
146
|
const checkout = canonicalPath(target);
|
|
147
147
|
const unknown = (detail) => ({ known: false, forced: false, detail });
|
|
148
148
|
const pidFile = path.join(checkout, DEV_SERVER_PID_FILE);
|
|
@@ -170,14 +170,14 @@ export function devServerTestnetDiagnostic(target, { appPort, probe = systemProb
|
|
|
170
170
|
const processMode = nextServerMode(args.argv);
|
|
171
171
|
if (processMode === 'start') return { known: true, forced: false, detail: `pid ${pid} runs next start; its environment cannot prove what the build inlined` };
|
|
172
172
|
if (processMode === 'dev') mode = 'dev';
|
|
173
|
-
const value = environmentValue(args.env,
|
|
173
|
+
const value = environmentValue(args.env, variable);
|
|
174
174
|
if (value !== 'true') {
|
|
175
|
-
return { known: true, forced: false, detail: `pid ${pid} (between app port ${appPort} and next dev pid ${root}) has ${
|
|
175
|
+
return { known: true, forced: false, detail: `pid ${pid} (between app port ${appPort} and next dev pid ${root}) has ${variable}=${value ?? '<unset>'}` };
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
179
|
if (mode !== 'dev') return { known: true, forced: false, detail: `no process serving app port ${appPort} runs next dev` };
|
|
180
|
-
return { known: true, forced: true, detail: `every process from app port ${appPort}'s listeners (cwd ${checkout}) up to next dev pid ${root} has ${
|
|
180
|
+
return { known: true, forced: true, detail: `every process from app port ${appPort}'s listeners (cwd ${checkout}) up to next dev pid ${root} has ${variable}=true` };
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
export const MAINNET_CONFIRMATION = 'REAL_FUNDS';
|
|
@@ -195,7 +195,15 @@ export function assertLaunchNetwork(_target, { network, mainnetConfirmation } =
|
|
|
195
195
|
return requested;
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
// "a, b and c".
|
|
199
|
+
function listing(names) {
|
|
200
|
+
return names.length > 1 ? `${names.slice(0, -1).join(', ')} and ${names.at(-1)}` : names.join('');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// `policy`: the venue policy (adapters/terminal/policy.mjs): dependencies,
|
|
204
|
+
// startPath, testnetVariable.
|
|
198
205
|
export async function terminalReadiness({
|
|
206
|
+
policy,
|
|
199
207
|
target,
|
|
200
208
|
appPort,
|
|
201
209
|
cdpPort,
|
|
@@ -207,17 +215,17 @@ export async function terminalReadiness({
|
|
|
207
215
|
const checks = [];
|
|
208
216
|
const shape = isTerminalCheckout(target);
|
|
209
217
|
checks.push(check('checkout', shape, shape ? 'Next.js checkout with src/features/perpetuals' : `${target} is not a Web Terminal checkout`));
|
|
210
|
-
const deps =
|
|
211
|
-
checks.push(check('dependencies', deps.length === 0, deps.length === 0 ?
|
|
218
|
+
const deps = policy.dependencies.filter((name) => !existsSync(path.join(target, 'node_modules', name, 'package.json')));
|
|
219
|
+
checks.push(check('dependencies', deps.length === 0, deps.length === 0 ? `${listing(policy.dependencies)} installed` : `missing: ${deps.join(', ')} (run npm ci on Node 22)`));
|
|
212
220
|
|
|
213
221
|
if (appPort) {
|
|
214
|
-
const status = await httpStatus(`http://127.0.0.1:${appPort}
|
|
215
|
-
checks.push(check('app-dev-server', status === 200, `GET http://localhost:${appPort}
|
|
222
|
+
const status = await httpStatus(`http://127.0.0.1:${appPort}${policy.startPath}`);
|
|
223
|
+
checks.push(check('app-dev-server', status === 200, `GET http://localhost:${appPort}${policy.startPath} -> ${status}`));
|
|
216
224
|
} else {
|
|
217
225
|
checks.push(check('app-dev-server', false, 'no dev-server port; pass --watcher-port <port> or set TERMINAL_APP_PORT'));
|
|
218
226
|
}
|
|
219
227
|
// Advisory: the browser enforces testnet itself (mainnet-guard.mjs).
|
|
220
|
-
const testnet = devServerTestnetDiagnostic(target, { appPort });
|
|
228
|
+
const testnet = devServerTestnetDiagnostic(target, { appPort, variable: policy.testnetVariable });
|
|
221
229
|
checks.push(check('testnet-forced', testnet.known && testnet.forced, testnet.detail, {
|
|
222
230
|
required: false,
|
|
223
231
|
status: testnet.known && testnet.forced ? 'pass' : 'warn',
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// The Web Terminal's venue policy: what the generic web-dapp launch, wallet
|
|
2
|
+
// host and readiness need to know about Hyperliquid. They take it as an
|
|
3
|
+
// argument (the leaf scripts import it from --policy-module, reading its
|
|
4
|
+
// `policy` export), so none of them names the venue. It becomes the terminal
|
|
5
|
+
// plugin's `policy` once recipe libraries can declare adapters; the Terminal's
|
|
6
|
+
// detection and its signature-log rules (_signature-log.mjs) move with it.
|
|
7
|
+
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
import { LINK_HOSTS, TESTNET_TYPED_DATA_POLICY, venueHosts } from './lib/mainnet-guard.mjs';
|
|
11
|
+
|
|
12
|
+
export const policy = Object.freeze({
|
|
13
|
+
// network=testnet: the hosts the browser must never reach, and the hosts
|
|
14
|
+
// whose traffic proves the app serves testnet.
|
|
15
|
+
venueHosts(checkout) {
|
|
16
|
+
const { mainnet, testnet } = venueHosts(checkout);
|
|
17
|
+
return { blocked: mainnet, served: testnet };
|
|
18
|
+
},
|
|
19
|
+
// Venue web hosts (pages, coin icons): never blocked, never proof.
|
|
20
|
+
linkHosts: Object.freeze([...LINK_HOSTS]),
|
|
21
|
+
// The launch's proof that the block holds: requests to this blocked host
|
|
22
|
+
// (preferred; else the first blocked host) must fail.
|
|
23
|
+
probe: Object.freeze({ host: 'api.hyperliquid.xyz', httpPath: '/info', httpBody: '{"type":"meta"}', wsPath: '/ws' }),
|
|
24
|
+
// The chain the injected wallet starts on (Arbitrum One).
|
|
25
|
+
startChain: 42161,
|
|
26
|
+
// Typed data a testnet run never signs.
|
|
27
|
+
refuseTypedData: TESTNET_TYPED_DATA_POLICY,
|
|
28
|
+
// The app's first page: the launch opens it, readiness probes it.
|
|
29
|
+
startPath: '/order/BTC',
|
|
30
|
+
dependencies: Object.freeze(['next', '@nktkas/hyperliquid', '@metamask/perps-controller']),
|
|
31
|
+
// The dev-server environment variable that forces testnet (advisory check).
|
|
32
|
+
testnetVariable: 'NEXT_PUBLIC_HYPERLIQUID_FORCE_TESTNET',
|
|
33
|
+
// This module, for the wallet host (a separate process).
|
|
34
|
+
module: fileURLToPath(import.meta.url),
|
|
35
|
+
});
|
|
@@ -3,18 +3,18 @@
|
|
|
3
3
|
// Web Terminal slot (checkout, deps, dev server, forced testnet, fixture,
|
|
4
4
|
// browser start probe, MetaMask extension copy, CDP port).
|
|
5
5
|
//
|
|
6
|
-
// Inputs: --target <checkout> [--cdp-port <port>] [--watcher-port <app port>]
|
|
6
|
+
// Inputs: --policy-module <venue policy module> --target <checkout> [--cdp-port <port>] [--watcher-port <app port>]
|
|
7
7
|
// [--signer extension|injected] [--account <name>] [--json]
|
|
8
8
|
// Outputs: readiness JSON (or a human summary) on stdout. Exit 0 ready; 1 not ready.
|
|
9
9
|
// Never launches the slot browser or the dev server.
|
|
10
10
|
|
|
11
11
|
import path from 'node:path';
|
|
12
|
-
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
13
13
|
|
|
14
14
|
import { resolveBrowser } from './launch.mjs';
|
|
15
15
|
import { terminalReadiness } from './lib/readiness.mjs';
|
|
16
16
|
|
|
17
|
-
const usage = 'Usage: verify.mjs --target <checkout> [--cdp-port <port>] [--watcher-port <app port>] [--signer extension|injected] [--account <name>] [--json]';
|
|
17
|
+
const usage = 'Usage: verify.mjs --policy-module <venue policy module> --target <checkout> [--cdp-port <port>] [--watcher-port <app port>] [--signer extension|injected] [--account <name>] [--json]';
|
|
18
18
|
|
|
19
19
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
20
20
|
const argv = process.argv.slice(2);
|
|
@@ -27,7 +27,14 @@ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.me
|
|
|
27
27
|
return index >= 0 ? argv[index + 1] : undefined;
|
|
28
28
|
};
|
|
29
29
|
const num = (raw) => (raw && /^\d+$/u.test(raw) ? Number(raw) : undefined);
|
|
30
|
+
const policyModule = value('--policy-module');
|
|
31
|
+
if (!policyModule) {
|
|
32
|
+
process.stderr.write(`Missing --policy-module\n${usage}\n`);
|
|
33
|
+
process.exit(2);
|
|
34
|
+
}
|
|
35
|
+
const { policy } = await import(pathToFileURL(path.resolve(policyModule)).href);
|
|
30
36
|
const report = await terminalReadiness({
|
|
37
|
+
policy,
|
|
31
38
|
target: path.resolve(value('--target') ?? process.cwd()),
|
|
32
39
|
appPort: num(value('--watcher-port') ?? process.env.TERMINAL_APP_PORT ?? process.env.WATCHER_PORT),
|
|
33
40
|
cdpPort: num(value('--cdp-port') ?? process.env.RECIPE_CDP_PORT ?? process.env.CDP_PORT),
|