@deeeed/metamask-harness 0.80.0 → 0.82.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/adapters/manifest.json +48 -64
  3. package/adapters/{terminal → web-dapp}/cleanup.mjs +9 -9
  4. package/adapters/{terminal → web-dapp}/inject.mjs +14 -14
  5. package/adapters/{terminal → web-dapp}/launch.mjs +27 -24
  6. package/adapters/web-dapp/lib/blocked-hosts.mjs +24 -0
  7. package/adapters/{terminal → web-dapp}/lib/cdp-client.mjs +1 -1
  8. package/adapters/{terminal → web-dapp}/lib/display.mjs +1 -1
  9. package/adapters/{terminal → web-dapp}/lib/hud.mjs +1 -1
  10. package/adapters/web-dapp/lib/policy.mjs +79 -0
  11. package/adapters/{terminal → web-dapp}/lib/processes.mjs +8 -8
  12. package/adapters/{terminal → web-dapp}/lib/readiness.mjs +32 -34
  13. package/adapters/{terminal → web-dapp}/stop.mjs +5 -4
  14. package/adapters/{terminal → web-dapp}/verify.mjs +9 -6
  15. package/adapters/{terminal → web-dapp}/wallet-host.mjs +44 -227
  16. package/dist/adapters/core/actions.js +4 -1
  17. package/dist/adapters/extension/actions.js +6 -2
  18. package/dist/adapters/extension/network-observer.js +13 -0
  19. package/dist/adapters/extension/run.js +115 -31
  20. package/dist/adapters/mobile/actions.js +4 -1
  21. package/dist/adapters/mobile/network-observer.js +68 -0
  22. package/dist/adapters/mobile/performance-observer.js +1 -3
  23. package/dist/adapters/mobile/run.js +32 -9
  24. package/dist/adapters/perps-order-inputs.js +36 -0
  25. package/dist/adapters/plugin-surface.js +98 -0
  26. package/dist/adapters/proof-failure.js +11 -0
  27. package/dist/adapters/surface.js +1 -1
  28. package/dist/adapters/{terminal → web-dapp}/actions.js +11 -8
  29. package/dist/adapters/web-dapp/console-capture.js +63 -0
  30. package/dist/adapters/web-dapp/doctor-checks.js +33 -0
  31. package/dist/adapters/{terminal → web-dapp}/launch.js +21 -19
  32. package/dist/adapters/web-dapp/overlay.js +21 -0
  33. package/dist/adapters/web-dapp/readiness.js +10 -0
  34. package/dist/adapters/{terminal → web-dapp}/run.js +22 -17
  35. package/dist/adapters/{terminal → web-dapp}/stop.js +5 -4
  36. package/dist/adapters/web-dapp/surface.js +149 -0
  37. package/dist/adapters.js +20 -4
  38. package/dist/cli.js +34 -6
  39. package/dist/command-contract.js +29 -7
  40. package/dist/commands/check.js +32 -8
  41. package/dist/commands/completion-candidates.js +5 -5
  42. package/dist/commands/doctor.js +19 -2
  43. package/dist/commands/farmslot-ready.js +1 -0
  44. package/dist/commands/help.js +1 -1
  45. package/dist/commands/launch/device.js +5 -5
  46. package/dist/commands/self-test.js +10 -5
  47. package/dist/engine.js +47 -0
  48. package/dist/funding-execution-context.js +81 -0
  49. package/dist/manifest.js +48 -24
  50. package/dist/mm-harness-cli.js +20 -3
  51. package/dist/performance-observation.js +7 -15
  52. package/dist/recipe-assistance-context.js +6 -7
  53. package/dist/runner.js +28 -8
  54. package/docs/CONTRIBUTING.md +4 -2
  55. package/docs/RECIPES.md +31 -3
  56. package/docs/cli-contract.md +5 -3
  57. package/library/actions/core/perps/_controller.mjs +24 -28
  58. package/library/actions/core/perps/_rewards.mjs +186 -0
  59. package/library/actions/core/perps/close_positions.mjs +1 -1
  60. package/library/actions/core/perps/place_order.mjs +167 -45
  61. package/library/actions/core/perps/quote_fees.mjs +7 -4
  62. package/library/actions/core/wallet/list_accounts.mjs +1 -1
  63. package/library/actions/extension/perps/close_visible_position.mjs +5 -0
  64. package/library/actions/extension/perps/edit_margin.mjs +4 -0
  65. package/library/actions/extension/perps/mutation-receipt.mjs +95 -3
  66. package/library/actions/extension/perps/perps.mjs +320 -3
  67. package/library/actions/extension/perps/place_order.mjs +3 -1
  68. package/library/actions/extension/perps/update_position_tpsl.mjs +4 -0
  69. package/library/actions/extension/platform/cdp.mjs +71 -3
  70. package/library/actions/mobile/perps/perps.mjs +216 -11
  71. package/library/actions/mobile/perps/place_order.mjs +3 -1
  72. package/library/actions/shared/console-collector.mjs +1 -1
  73. package/library/actions/shared/perps/order-policy.mjs +177 -0
  74. package/library/actions/{core/wallet/_fixture-accounts.mjs → shared/wallet/fixture-accounts.mjs} +18 -3
  75. package/library/actions/{terminal → web-dapp}/app/assert_no_console_errors.mjs +4 -4
  76. package/library/actions/{terminal → web-dapp}/app/launch.mjs +8 -7
  77. package/library/actions/{terminal → web-dapp}/platform/hud.mjs +7 -6
  78. package/library/actions/{terminal → web-dapp}/platform/page.mjs +13 -12
  79. package/library/actions/{terminal → web-dapp}/platform/runtime.mjs +16 -12
  80. package/library/actions/web-dapp/ui/navigate.mjs +24 -0
  81. package/library/actions/web-dapp/wallet/_signature-log.mjs +32 -0
  82. package/library/actions/{terminal → web-dapp}/wallet/assert_signatures.mjs +8 -14
  83. package/library/actions/{terminal → web-dapp}/wallet/list_accounts.mjs +2 -2
  84. package/library/actions/web-dapp/wallet/read_signatures.mjs +38 -0
  85. package/library/manifests/core.action-manifest.json +6 -2
  86. package/library/manifests/extension.action-manifest.json +4 -0
  87. package/library/manifests/mobile.action-manifest.json +12 -0
  88. package/library/manifests/{terminal.action-manifest.json → web-dapp.action-manifest.json} +18 -702
  89. package/package.json +6 -6
  90. package/site/assets/help-recipes.json +1 -0
  91. package/adapters/terminal/lib/mainnet-guard.mjs +0 -177
  92. package/adapters/terminal/lib/origin.mjs +0 -30
  93. package/adapters/terminal/lib/page-script.mjs +0 -246
  94. package/adapters/terminal/lib/strict-wallet.mjs +0 -138
  95. package/dist/adapters/terminal/console-capture.js +0 -59
  96. package/dist/adapters/terminal/doctor-checks.js +0 -22
  97. package/dist/adapters/terminal/overlay.js +0 -18
  98. package/dist/adapters/terminal/readiness.js +0 -11
  99. package/dist/adapters/terminal/surface.js +0 -133
  100. package/dist/commands/call.js +0 -671
  101. package/dist/commands/list-executables.js +0 -245
  102. package/dist/commands/manifest.js +0 -477
  103. package/dist/commands/run-engine.js +0 -1301
  104. package/dist/commands/run.js +0 -730
  105. package/dist/discovery.js +0 -15
  106. package/dist/metamask-action-validation.js +0 -68
  107. package/dist/network-observation.js +0 -290
  108. package/library/actions/terminal/perps/_venue.mjs +0 -242
  109. package/library/actions/terminal/perps/assert_orders.mjs +0 -54
  110. package/library/actions/terminal/perps/assert_positions.mjs +0 -40
  111. package/library/actions/terminal/perps/read_orders.mjs +0 -26
  112. package/library/actions/terminal/perps/read_positions.mjs +0 -25
  113. package/library/actions/terminal/perps/read_snapshot.mjs +0 -40
  114. package/library/actions/terminal/perps/revoke_agent.mjs +0 -58
  115. package/library/actions/terminal/perps/teardown_state.mjs +0 -104
  116. package/library/actions/terminal/ui/navigate.mjs +0 -31
  117. package/library/actions/terminal/wallet/_signature-log.mjs +0 -177
  118. package/library/actions/terminal/wallet/read_signatures.mjs +0 -37
@@ -1,40 +0,0 @@
1
- // Assert presence, absence or exact count of the selected venue positions,
2
- // polling up to timeout_ms for the expected state.
3
-
4
- import { isDirectRun, resolveAccountAddress, runAdapter } from '../platform/runtime.mjs';
5
- import { expectedOpen, pollUntil, readVenueState, resolveNetwork, selectItems } from './_venue.mjs';
6
-
7
- export async function assertPositions(input, deps = {}) {
8
- const node = input.node ?? {};
9
- const expectOpen = expectedOpen(node, 'metamask.perps.assert_positions');
10
- const network = resolveNetwork(node);
11
- const { name, address } = await resolveAccountAddress(input);
12
- const expectCount = node.expect_count ?? node.expectCount;
13
- const reached = (matching) => {
14
- if (!expectOpen) return matching.length === 0;
15
- return expectCount == null ? matching.length > 0 : matching.length === Number(expectCount);
16
- };
17
- const matching = await pollUntil(
18
- async () => selectItems(node, (await readVenueState({ network, user: address, fetchImpl: deps.fetchImpl })).positions, 'position'),
19
- reached,
20
- node.timeout_ms,
21
- );
22
- if (!reached(matching)) {
23
- const wanted = !expectOpen ? 'no' : expectCount == null ? 'at least one' : `exactly ${expectCount}`;
24
- throw new Error(`Expected ${wanted} matching venue position(s), found ${matching.length}: ${JSON.stringify(matching)}`);
25
- }
26
- return {
27
- action: input.action,
28
- source: 'hyperliquid-info-clearinghouseState',
29
- network,
30
- account: address,
31
- accountName: name,
32
- expectedOpen: expectOpen,
33
- matchingCount: matching.length,
34
- expectedCount: expectCount == null ? null : Number(expectCount),
35
- positions: matching,
36
- proofPath: 'venue-clearinghouseState',
37
- };
38
- }
39
-
40
- if (isDirectRun(import.meta.url)) runAdapter((input) => assertPositions(input));
@@ -1,26 +0,0 @@
1
- // Read the fixture account's Hyperliquid open orders for the selected markets.
2
-
3
- import { isDirectRun, resolveAccountAddress, runAdapter } from '../platform/runtime.mjs';
4
- import { filterOrderKind, readVenueState, resolveNetwork, selectItems } from './_venue.mjs';
5
-
6
- export async function readOrders(input, deps = {}) {
7
- const node = input.node ?? {};
8
- const network = resolveNetwork(node);
9
- const { name, address } = await resolveAccountAddress(input);
10
- const state = await readVenueState({ network, user: address, fetchImpl: deps.fetchImpl });
11
- const matching = filterOrderKind(node, selectItems(node, state.orders, 'order'));
12
- return {
13
- action: input.action,
14
- source: 'hyperliquid-info-frontendOpenOrders',
15
- network,
16
- account: address,
17
- accountName: name,
18
- count: state.orders.length,
19
- matchingCount: matching.length,
20
- orders: matching,
21
- orderIds: matching.map((order) => order.orderId),
22
- proofPath: 'venue-frontendOpenOrders',
23
- };
24
- }
25
-
26
- if (isDirectRun(import.meta.url)) runAdapter((input) => readOrders(input));
@@ -1,25 +0,0 @@
1
- // Read the fixture account's Hyperliquid positions for the selected markets.
2
-
3
- import { isDirectRun, resolveAccountAddress, runAdapter } from '../platform/runtime.mjs';
4
- import { readVenueState, resolveNetwork, selectItems } from './_venue.mjs';
5
-
6
- export async function readPositions(input, deps = {}) {
7
- const node = input.node ?? {};
8
- const network = resolveNetwork(node);
9
- const { name, address } = await resolveAccountAddress(input);
10
- const state = await readVenueState({ network, user: address, fetchImpl: deps.fetchImpl });
11
- const matching = selectItems(node, state.positions, 'position');
12
- return {
13
- action: input.action,
14
- source: 'hyperliquid-info-clearinghouseState',
15
- network,
16
- account: address,
17
- accountName: name,
18
- count: state.positions.length,
19
- matchingCount: matching.length,
20
- positions: matching,
21
- proofPath: 'venue-clearinghouseState',
22
- };
23
- }
24
-
25
- if (isDirectRun(import.meta.url)) runAdapter((input) => readPositions(input));
@@ -1,40 +0,0 @@
1
- // Snapshot the fixture account's venue state: positions, open orders, agents and
2
- // balances, optionally scoped to markets. Used for before/after comparison.
3
-
4
- import { isDirectRun, resolveAccountAddress, runAdapter } from '../platform/runtime.mjs';
5
- import { readVenueState, resolveNetwork, selectItems } from './_venue.mjs';
6
-
7
- const MARKET_KEYS = ['market', 'markets', 'symbol', 'symbols'];
8
-
9
- // The snapshot covers every market unless the node names some, through any of
10
- // the selector spellings selectItems accepts.
11
- export function snapshotSelector(raw = {}) {
12
- const node = { ...raw };
13
- const selector = node.selector && typeof node.selector === 'object' ? node.selector : {};
14
- const named = MARKET_KEYS.some((key) => node[key] != null || selector[key] != null);
15
- if (node.mode == null && selector.mode == null && !named) node.mode = 'all';
16
- return node;
17
- }
18
-
19
- export async function readSnapshot(input, deps = {}) {
20
- const node = snapshotSelector(input.node ?? {});
21
- const network = resolveNetwork(node);
22
- const { name, address } = await resolveAccountAddress(input);
23
- const state = await readVenueState({ network, user: address, fetchImpl: deps.fetchImpl });
24
- return {
25
- action: input.action,
26
- source: 'hyperliquid-info',
27
- network,
28
- account: address,
29
- accountName: name,
30
- at: new Date().toISOString(),
31
- positions: selectItems(node, state.positions, 'position'),
32
- orders: selectItems(node, state.orders, 'order'),
33
- agents: state.agents,
34
- accountValue: state.accountValue,
35
- withdrawable: state.withdrawable,
36
- proofPath: 'venue-snapshot',
37
- };
38
- }
39
-
40
- if (isDirectRun(import.meta.url)) runAdapter((input) => readSnapshot(input));
@@ -1,58 +0,0 @@
1
- // Lifecycle step (testnet; mainnet only with mainnet_confirmation=REAL_FUNDS): revoke the Web Terminal's Hyperliquid agent(s) for the
2
- // fixture account outside the UI (approveAgent to the zero address under the
3
- // same name, user-signed in Node), then assert none is left. The next UI write
4
- // must therefore ask the wallet for a fresh ApproveAgent.
5
-
6
- import { accountName, fixtureAccount, isDirectRun, readBrowserState, runAdapter } from '../platform/runtime.mjs';
7
- import { TERMINAL_AGENT_PREFIX, loadTargetHyperliquid, pollUntil, readVenueState, requireTestnet } from './_venue.mjs';
8
-
9
- const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
10
-
11
- export function agentBaseName(name) {
12
- return String(name).replace(/\s+valid_until\s+\d+$/u, '');
13
- }
14
-
15
- export function terminalAgents(agents, prefix = TERMINAL_AGENT_PREFIX) {
16
- return agents.filter((agent) => agentBaseName(agent.name) === prefix);
17
- }
18
-
19
- export async function revokeAgent(input, deps = {}) {
20
- const node = input.node ?? {};
21
- const network = requireTestnet(node, 'metamask.perps.revoke_agent');
22
- const prefix = node.agent_name ?? TERMINAL_AGENT_PREFIX;
23
- const projectRoot = input.context?.projectRoot;
24
- const name = accountName(input, await readBrowserState(projectRoot));
25
- const account = deps.account ?? (await fixtureAccount(projectRoot, name));
26
- const read = () => readVenueState({ network, user: account.address, fetchImpl: deps.fetchImpl });
27
- const before = terminalAgents((await read()).agents, prefix);
28
- const responses = [];
29
- if (before.length > 0) {
30
- const hl = deps.hl ?? (await loadTargetHyperliquid(projectRoot));
31
- const exchange = new hl.ExchangeClient({ wallet: account, transport: new hl.HttpTransport({ isTestnet: network === 'testnet' }) });
32
- for (const agentName of new Set(before.map((agent) => agentBaseName(agent.name)))) {
33
- const response = await exchange.approveAgent({ agentAddress: ZERO_ADDRESS, agentName });
34
- responses.push({ agentName, status: response?.status ?? null });
35
- }
36
- }
37
- const after = await pollUntil(
38
- async () => terminalAgents((await read()).agents, prefix),
39
- (agents) => agents.length === 0,
40
- node.timeout_ms ?? 20000,
41
- );
42
- if (after.length > 0) {
43
- throw new Error(`terminal agent still approved after revoke: ${JSON.stringify(after.map((agent) => agent.address))}`);
44
- }
45
- return {
46
- action: input.action,
47
- source: 'hyperliquid-exchange-fixture-signer',
48
- network,
49
- account: account.address,
50
- accountName: name,
51
- revokedAgents: before.map((agent) => ({ name: agent.name, address: agent.address })),
52
- responses,
53
- remainingAgents: after.length,
54
- proofPath: 'venue-extraAgents',
55
- };
56
- }
57
-
58
- if (isDirectRun(import.meta.url)) runAdapter((input) => revokeAgent(input));
@@ -1,104 +0,0 @@
1
- // Testnet cleanup for one reserved market: cancel its open orders (or only the
2
- // listed order_ids) and close its position with a reduce-only IOC order, signed
3
- // in Node by the fixture account, then assert the requested end state. Testnet by
4
- // default; mainnet only with mainnet_confirmation=REAL_FUNDS. Refuses
5
- // account-wide selection.
6
-
7
- import { fixtureAccount, isDirectRun, readBrowserState, accountName, runAdapter } from '../platform/runtime.mjs';
8
- import {
9
- filterOrderKind,
10
- info,
11
- loadTargetHyperliquid,
12
- pollUntil,
13
- readVenueState,
14
- requireTestnet,
15
- requestedMarkets,
16
- selectItems,
17
- venuePrice,
18
- } from './_venue.mjs';
19
-
20
- const CLOSE_SLIPPAGE = 0.05;
21
-
22
- function wantsNone(spec, label) {
23
- if (spec == null) return false;
24
- const state = String(spec.state ?? spec).toLowerCase();
25
- if (state === 'none' || state === 'closed' || state === 'absent') return true;
26
- throw new Error(`metamask.perps.teardown_state only converges ${label} to state=none, got ${state}.`);
27
- }
28
-
29
- export async function teardownState(input, deps = {}) {
30
- const node = input.node ?? {};
31
- const network = requireTestnet(node, 'metamask.perps.teardown_state');
32
- const markets = requestedMarkets(node);
33
- if (!markets) throw new Error('metamask.perps.teardown_state requires an explicit market; mode=all is refused.');
34
- const closePositions = wantsNone(node.positions ?? { state: 'none' }, 'positions');
35
- const cancelOrders = wantsNone(node.orders ?? { state: 'none' }, 'orders');
36
- const projectRoot = input.context?.projectRoot;
37
- const name = accountName(input, await readBrowserState(projectRoot));
38
- const account = deps.account ?? (await fixtureAccount(projectRoot, name));
39
- const user = account.address;
40
- const read = () => readVenueState({ network, user, fetchImpl: deps.fetchImpl });
41
- const before = await read();
42
- const orders = cancelOrders ? filterOrderKind(node, selectItems(node, before.orders, 'order')) : [];
43
- const positions = closePositions ? selectItems(node, before.positions, 'position') : [];
44
- const actions = { canceledOrderIds: [], closedPositions: [], responses: [] };
45
-
46
- if (orders.length > 0 || positions.length > 0) {
47
- const hl = deps.hl ?? (await loadTargetHyperliquid(projectRoot));
48
- const exchange = new hl.ExchangeClient({ wallet: account, transport: new hl.HttpTransport({ isTestnet: network === 'testnet' }) });
49
- const [meta, mids] = await Promise.all([
50
- info(network, { type: 'meta' }, deps.fetchImpl),
51
- info(network, { type: 'allMids' }, deps.fetchImpl),
52
- ]);
53
- const assetOf = (market) => {
54
- const index = meta.universe.findIndex((asset) => asset.name.toUpperCase() === market);
55
- if (index < 0) throw new Error(`market ${market} is not in the ${network} perps universe.`);
56
- return { index, szDecimals: meta.universe[index].szDecimals };
57
- };
58
- if (orders.length > 0) {
59
- const response = await exchange.cancel({
60
- cancels: orders.map((order) => ({ a: assetOf(order.market).index, o: Number(order.orderId) })),
61
- });
62
- actions.responses.push({ cancel: response?.response?.data ?? response });
63
- actions.canceledOrderIds.push(...orders.map((order) => order.orderId));
64
- }
65
- for (const position of positions) {
66
- const { index, szDecimals } = assetOf(position.market);
67
- const mid = Number(mids[position.market]);
68
- if (!Number.isFinite(mid)) throw new Error(`no ${network} mid price for ${position.market}.`);
69
- const isBuy = position.side === 'short';
70
- const price = venuePrice(mid * (isBuy ? 1 + CLOSE_SLIPPAGE : 1 - CLOSE_SLIPPAGE), szDecimals);
71
- const response = await exchange.order({
72
- orders: [{ a: index, b: isBuy, p: price, s: position.size, r: true, t: { limit: { tif: 'Ioc' } } }],
73
- grouping: 'na',
74
- });
75
- actions.responses.push({ close: response?.response?.data ?? response });
76
- actions.closedPositions.push({ market: position.market, side: position.side, size: position.size });
77
- }
78
- }
79
-
80
- const done = (state) =>
81
- (!cancelOrders || filterOrderKind(node, selectItems(node, state.orders, 'order')).length === 0) &&
82
- (!closePositions || selectItems(node, state.positions, 'position').length === 0);
83
- const after = await pollUntil(read, done, node.timeout_ms ?? 30000);
84
- const remaining = {
85
- orders: cancelOrders ? filterOrderKind(node, selectItems(node, after.orders, 'order')) : [],
86
- positions: closePositions ? selectItems(node, after.positions, 'position') : [],
87
- };
88
- if (!done(after)) {
89
- throw new Error(`testnet cleanup left state behind for ${[...markets].join(',')}: ${JSON.stringify(remaining)}`);
90
- }
91
- return {
92
- action: input.action,
93
- source: 'hyperliquid-exchange-fixture-signer',
94
- network,
95
- account: user,
96
- accountName: name,
97
- markets: [...markets],
98
- ...actions,
99
- remaining,
100
- proofPath: 'venue-cleanup',
101
- };
102
- }
103
-
104
- if (isDirectRun(import.meta.url)) runAdapter((input) => teardownState(input));
@@ -1,31 +0,0 @@
1
- // Navigate the terminal app tab to a product page (order/<MARKET>, portfolio)
2
- // or an explicit app path, and wait for the document to be ready.
3
-
4
- import { appOrigin, isDirectRun, runAdapter } from '../platform/runtime.mjs';
5
- import { withTerminalPage } from '../platform/page.mjs';
6
-
7
- export function pathFor(node = {}) {
8
- if (typeof node.path === 'string' && node.path.startsWith('/')) return node.path;
9
- const page = node.page == null ? 'order' : String(node.page);
10
- if (page === 'order' || page === 'market') {
11
- const market = String(node.market ?? node.symbol ?? 'BTC').toUpperCase();
12
- if (!/^[A-Z0-9:_-]+$/u.test(market)) throw new Error(`unsupported market symbol ${JSON.stringify(market)}`);
13
- return `/order/${market}`;
14
- }
15
- if (page === 'portfolio') return '/portfolio';
16
- if (page === 'home') return '/';
17
- throw new Error(`ui.navigate page must be order, portfolio or home (or pass path), got ${JSON.stringify(page)}.`);
18
- }
19
-
20
- export async function navigate(input) {
21
- const node = input.node ?? {};
22
- const url = `${appOrigin(input)}${pathFor(node)}`;
23
- return withTerminalPage({ ...input, node: { ...node, surface: 'app' } }, async (page) => {
24
- await page.navigate(url, Number(node.timeout_ms ?? 120000));
25
- await page.waitForDocumentReady({ expectedUrl: url, timeoutMs: Number(node.timeout_ms ?? 120000) });
26
- if (node.settle_ms) await new Promise((resolve) => setTimeout(resolve, Number(node.settle_ms)));
27
- return { action: input.action, url, page: node.page ?? 'order', market: node.market ?? null };
28
- });
29
- }
30
-
31
- if (isDirectRun(import.meta.url)) runAdapter((input) => navigate(input));
@@ -1,177 +0,0 @@
1
- // Read, window and assert the terminal wallet request log the wallet host
2
- // writes: one JSON line per request the app sent to the main wallet (method,
3
- // EIP-712 primary type, domain/active chain, outcome) and, in extension mode,
4
- // one line per confirmation window MetaMask opened. Never holds params,
5
- // messages or signatures.
6
-
7
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
8
- import path from 'node:path';
9
-
10
- import { terminalRuntimePath } from '../platform/runtime.mjs';
11
-
12
- export const L1_PRIMARY_TYPE = 'Agent';
13
- export const L1_CHAIN_ID = 1337;
14
-
15
- export function signatureLogPath(projectRoot) {
16
- return terminalRuntimePath(projectRoot, 'wallet-requests.jsonl');
17
- }
18
-
19
- export function signatureCursorPath(projectRoot) {
20
- return terminalRuntimePath(projectRoot, 'wallet-requests.cursor.json');
21
- }
22
-
23
- export function parseLog(text) {
24
- return text
25
- .split('\n')
26
- .filter((line) => line.trim())
27
- .map((line, index) => {
28
- try {
29
- return { seq: index, ...JSON.parse(line) };
30
- } catch {
31
- return { seq: index, kind: 'unparseable' };
32
- }
33
- });
34
- }
35
-
36
- export async function readLog(projectRoot) {
37
- try {
38
- return parseLog(await readFile(signatureLogPath(projectRoot), 'utf8'));
39
- } catch (error) {
40
- if (error?.code === 'ENOENT') return [];
41
- throw error;
42
- }
43
- }
44
-
45
- export async function readCursor(projectRoot) {
46
- try {
47
- const cursor = JSON.parse(await readFile(signatureCursorPath(projectRoot), 'utf8'));
48
- return Number.isInteger(cursor.seq) ? cursor.seq : 0;
49
- } catch (error) {
50
- if (error?.code === 'ENOENT') return 0;
51
- throw error;
52
- }
53
- }
54
-
55
- export async function writeCursor(projectRoot, seq) {
56
- await writeFile(signatureCursorPath(projectRoot), `${JSON.stringify({ seq, at: new Date().toISOString() })}\n`, { mode: 0o600 });
57
- }
58
-
59
- // "HyperliquidTransaction:ApproveAgent" → "ApproveAgent".
60
- export function shortPrimaryType(primaryType) {
61
- if (primaryType == null) return null;
62
- const raw = String(primaryType);
63
- return raw.includes(':') ? raw.slice(raw.lastIndexOf(':') + 1) : raw;
64
- }
65
-
66
- export function isTypedDataRequest(entry) {
67
- return entry.kind === 'request' && /^eth_signTypedData/u.test(String(entry.method));
68
- }
69
-
70
- export function isL1Request(entry) {
71
- return isTypedDataRequest(entry) &&
72
- (shortPrimaryType(entry.primaryType) === L1_PRIMARY_TYPE || Number(entry.domainChainId) === L1_CHAIN_ID);
73
- }
74
-
75
- export function summarize(entries) {
76
- const requests = entries.filter((entry) => entry.kind === 'request');
77
- const typed = requests.filter(isTypedDataRequest);
78
- const byPrimaryType = {};
79
- for (const entry of typed) {
80
- const key = shortPrimaryType(entry.primaryType) ?? '<none>';
81
- byPrimaryType[key] ??= { requested: 0, signed: 0, rejected: 0, chainIds: [] };
82
- byPrimaryType[key].requested += 1;
83
- if (entry.outcome === 'signed') byPrimaryType[key].signed += 1;
84
- if (entry.outcome === 'rejected' || entry.outcome === 'error') byPrimaryType[key].rejected += 1;
85
- if (!byPrimaryType[key].chainIds.includes(entry.domainChainId)) byPrimaryType[key].chainIds.push(entry.domainChainId);
86
- }
87
- const byMethod = {};
88
- for (const entry of requests) byMethod[entry.method] = (byMethod[entry.method] ?? 0) + 1;
89
- return {
90
- requests: requests.length,
91
- typedDataRequests: typed.length,
92
- l1Requests: typed.filter(isL1Request).length,
93
- confirmationsShown: entries.filter((entry) => entry.kind === 'confirmation-shown').length,
94
- // Testnet enforcement and evidence gaps: each one fails the assertion.
95
- blockedMainnet: entries.filter((entry) => entry.kind === 'blocked-mainnet' && !entry.probe).length,
96
- refusedMainnet: entries.filter((entry) => entry.kind === 'refused-mainnet').length,
97
- unattributed: entries.filter((entry) => entry.kind === 'unattributed').length,
98
- // Calls from an iframe, a foreign page or a blank document: refused, kept as evidence.
99
- outsideAppFrame: entries.filter((entry) => entry.kind === 'outside-app-frame').length,
100
- byPrimaryType,
101
- byMethod,
102
- };
103
- }
104
-
105
- function exact(label, actual, expected, failures) {
106
- if (actual !== expected) failures.push(`${label}: expected ${expected}, got ${actual}`);
107
- }
108
-
109
- // Every expectation is optional; max_l1_requests defaults to 0 because an L1
110
- // (agent-signed) payload must never reach the main wallet.
111
- export function evaluateSignatureLog(entries, node = {}) {
112
- const summary = summarize(entries);
113
- const typed = entries.filter(isTypedDataRequest);
114
- const failures = [];
115
- if (summary.blockedMainnet > 0) failures.push(`the browser tried to reach a mainnet venue ${summary.blockedMainnet} time(s) (blocked-mainnet)`);
116
- if (summary.refusedMainnet > 0) failures.push(`the app asked the wallet to sign ${summary.refusedMainnet} mainnet action(s) (refused-mainnet)`);
117
- if (summary.unattributed > 0) failures.push(`${summary.unattributed} wallet log entr(y/ies) could not be attributed to the app's top frame (unattributed)`);
118
- const maxL1 = node.max_l1_requests == null ? 0 : Number(node.max_l1_requests);
119
- if (summary.l1Requests > maxL1) {
120
- failures.push(`L1 requests reached the main wallet: ${summary.l1Requests} > ${maxL1}`);
121
- }
122
- if (Array.isArray(node.allowed_primary_types)) {
123
- const allowed = new Set(node.allowed_primary_types.map(shortPrimaryType));
124
- for (const entry of typed) {
125
- const type = shortPrimaryType(entry.primaryType);
126
- if (!allowed.has(type)) failures.push(`primary type ${type} (seq ${entry.seq}) is not allowed`);
127
- }
128
- }
129
- if (node.counts && typeof node.counts === 'object') {
130
- for (const [type, expected] of Object.entries(node.counts)) {
131
- exact(`signed ${shortPrimaryType(type)}`, summary.byPrimaryType[shortPrimaryType(type)]?.signed ?? 0, Number(expected), failures);
132
- }
133
- }
134
- if (node.request_counts && typeof node.request_counts === 'object') {
135
- for (const [type, expected] of Object.entries(node.request_counts)) {
136
- exact(`requested ${shortPrimaryType(type)}`, summary.byPrimaryType[shortPrimaryType(type)]?.requested ?? 0, Number(expected), failures);
137
- }
138
- }
139
- if (node.methods && typeof node.methods === 'object') {
140
- for (const [method, expected] of Object.entries(node.methods)) {
141
- exact(`method ${method}`, summary.byMethod[method] ?? 0, Number(expected), failures);
142
- }
143
- }
144
- if (node.max_typed_data_requests != null && summary.typedDataRequests > Number(node.max_typed_data_requests)) {
145
- failures.push(`typed-data requests ${summary.typedDataRequests} > ${node.max_typed_data_requests}`);
146
- }
147
- if (node.expect_confirmations != null) {
148
- exact('confirmations shown', summary.confirmationsShown, Number(node.expect_confirmations), failures);
149
- }
150
- if (node.require_active_chain === true) {
151
- for (const entry of typed.filter((item) => item.outcome === 'signed')) {
152
- if (Number(entry.domainChainId) !== Number(entry.activeChainId)) {
153
- failures.push(`seq ${entry.seq} ${shortPrimaryType(entry.primaryType)} signed with domain chainId ${entry.domainChainId} while the wallet was on ${entry.activeChainId}`);
154
- }
155
- }
156
- }
157
- if (node.max_rejected != null) {
158
- const rejected = typed.filter((entry) => entry.outcome === 'rejected' || entry.outcome === 'error').length;
159
- if (rejected > Number(node.max_rejected)) failures.push(`rejected typed-data requests ${rejected} > ${node.max_rejected}`);
160
- }
161
- return { ok: failures.length === 0, failures, summary };
162
- }
163
-
164
- export async function windowSinceCursor(projectRoot) {
165
- const [entries, cursor] = await Promise.all([readLog(projectRoot), readCursor(projectRoot)]);
166
- return { cursor, entries: entries.filter((entry) => entry.seq >= cursor), total: entries.length };
167
- }
168
-
169
- export async function writeLogArtifact(input, payload) {
170
- const artifactsDir = input.context?.artifactsDir;
171
- if (!artifactsDir) return [];
172
- const nodeId = String(input.context?.nodeId ?? input.action).replaceAll('/', '__');
173
- const relative = `terminal-wallet/${nodeId}-signature-log.json`;
174
- await mkdir(path.join(artifactsDir, 'terminal-wallet'), { recursive: true });
175
- await writeFile(path.join(artifactsDir, relative), `${JSON.stringify(payload, null, 2)}\n`);
176
- return [{ path: relative, type: 'report', nodeId: String(input.context?.nodeId ?? input.action) }];
177
- }
@@ -1,37 +0,0 @@
1
- // Return the wallet request log since the last reset and retain it as run
2
- // evidence. reset=true moves the window start to the end of the log so the next
3
- // read covers only what follows (an explicit setup step, recorded in the trace).
4
-
5
- import { isDirectRun, runAdapter } from '../platform/runtime.mjs';
6
- import { readLog, summarize, windowSinceCursor, writeCursor, writeLogArtifact } from './_signature-log.mjs';
7
-
8
- export async function readSignatures(input) {
9
- const projectRoot = input.context?.projectRoot;
10
- if (input.node?.reset === true) {
11
- const entries = await readLog(projectRoot);
12
- await writeCursor(projectRoot, entries.length);
13
- return {
14
- action: input.action,
15
- source: 'terminal-wallet-request-log',
16
- reset: true,
17
- cursor: entries.length,
18
- previousEntries: entries.length,
19
- proofPath: 'terminal-wallet-request-log',
20
- };
21
- }
22
- const { cursor, entries, total } = await windowSinceCursor(projectRoot);
23
- const summary = summarize(entries);
24
- const artifacts = await writeLogArtifact(input, { cursor, total, summary, entries });
25
- return {
26
- action: input.action,
27
- source: 'terminal-wallet-request-log',
28
- cursor,
29
- count: entries.length,
30
- summary,
31
- entries,
32
- artifacts,
33
- proofPath: 'terminal-wallet-request-log',
34
- };
35
- }
36
-
37
- if (isDirectRun(import.meta.url)) runAdapter((input) => readSignatures(input));