@deeeed/metamask-harness 0.80.0 → 0.82.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/adapters/manifest.json +48 -64
- package/adapters/{terminal → web-dapp}/cleanup.mjs +9 -9
- package/adapters/{terminal → web-dapp}/inject.mjs +14 -14
- package/adapters/{terminal → web-dapp}/launch.mjs +27 -24
- package/adapters/web-dapp/lib/blocked-hosts.mjs +24 -0
- package/adapters/{terminal → web-dapp}/lib/cdp-client.mjs +1 -1
- package/adapters/{terminal → web-dapp}/lib/display.mjs +1 -1
- package/adapters/{terminal → web-dapp}/lib/hud.mjs +1 -1
- package/adapters/web-dapp/lib/policy.mjs +79 -0
- package/adapters/{terminal → web-dapp}/lib/processes.mjs +8 -8
- package/adapters/{terminal → web-dapp}/lib/readiness.mjs +32 -34
- package/adapters/{terminal → web-dapp}/stop.mjs +5 -4
- package/adapters/{terminal → web-dapp}/verify.mjs +9 -6
- package/adapters/{terminal → web-dapp}/wallet-host.mjs +44 -227
- package/dist/adapters/core/actions.js +4 -1
- package/dist/adapters/extension/actions.js +6 -2
- package/dist/adapters/extension/network-observer.js +13 -0
- package/dist/adapters/extension/run.js +115 -31
- package/dist/adapters/mobile/actions.js +4 -1
- package/dist/adapters/mobile/network-observer.js +68 -0
- package/dist/adapters/mobile/performance-observer.js +1 -3
- package/dist/adapters/mobile/run.js +32 -9
- package/dist/adapters/perps-order-inputs.js +36 -0
- package/dist/adapters/plugin-surface.js +98 -0
- package/dist/adapters/proof-failure.js +11 -0
- package/dist/adapters/surface.js +1 -1
- package/dist/adapters/{terminal → web-dapp}/actions.js +11 -8
- package/dist/adapters/web-dapp/console-capture.js +63 -0
- package/dist/adapters/web-dapp/doctor-checks.js +33 -0
- package/dist/adapters/{terminal → web-dapp}/launch.js +21 -19
- package/dist/adapters/web-dapp/overlay.js +21 -0
- package/dist/adapters/web-dapp/readiness.js +10 -0
- package/dist/adapters/{terminal → web-dapp}/run.js +22 -17
- package/dist/adapters/{terminal → web-dapp}/stop.js +5 -4
- package/dist/adapters/web-dapp/surface.js +149 -0
- package/dist/adapters.js +20 -4
- package/dist/cli.js +34 -6
- package/dist/command-contract.js +29 -7
- package/dist/commands/check.js +32 -8
- package/dist/commands/completion-candidates.js +5 -5
- package/dist/commands/doctor.js +19 -2
- package/dist/commands/farmslot-ready.js +1 -0
- package/dist/commands/help.js +1 -1
- package/dist/commands/launch/device.js +5 -5
- package/dist/commands/self-test.js +10 -5
- package/dist/engine.js +47 -0
- package/dist/funding-execution-context.js +81 -0
- package/dist/manifest.js +48 -24
- package/dist/mm-harness-cli.js +20 -3
- package/dist/performance-observation.js +7 -15
- package/dist/recipe-assistance-context.js +6 -7
- package/dist/runner.js +28 -8
- package/docs/CONTRIBUTING.md +4 -2
- package/docs/RECIPES.md +31 -3
- package/docs/cli-contract.md +5 -3
- package/library/actions/core/perps/_controller.mjs +24 -28
- package/library/actions/core/perps/_rewards.mjs +186 -0
- package/library/actions/core/perps/close_positions.mjs +1 -1
- package/library/actions/core/perps/place_order.mjs +167 -45
- package/library/actions/core/perps/quote_fees.mjs +7 -4
- package/library/actions/core/wallet/list_accounts.mjs +1 -1
- package/library/actions/extension/perps/close_visible_position.mjs +5 -0
- package/library/actions/extension/perps/edit_margin.mjs +4 -0
- package/library/actions/extension/perps/mutation-receipt.mjs +95 -3
- package/library/actions/extension/perps/perps.mjs +320 -3
- package/library/actions/extension/perps/place_order.mjs +3 -1
- package/library/actions/extension/perps/update_position_tpsl.mjs +4 -0
- package/library/actions/extension/platform/cdp.mjs +71 -3
- package/library/actions/mobile/perps/perps.mjs +216 -11
- package/library/actions/mobile/perps/place_order.mjs +3 -1
- package/library/actions/shared/console-collector.mjs +1 -1
- package/library/actions/shared/perps/order-policy.mjs +177 -0
- package/library/actions/{core/wallet/_fixture-accounts.mjs → shared/wallet/fixture-accounts.mjs} +18 -3
- package/library/actions/{terminal → web-dapp}/app/assert_no_console_errors.mjs +4 -4
- package/library/actions/{terminal → web-dapp}/app/launch.mjs +8 -7
- package/library/actions/{terminal → web-dapp}/platform/hud.mjs +7 -6
- package/library/actions/{terminal → web-dapp}/platform/page.mjs +13 -12
- package/library/actions/{terminal → web-dapp}/platform/runtime.mjs +16 -12
- package/library/actions/web-dapp/ui/navigate.mjs +24 -0
- package/library/actions/web-dapp/wallet/_signature-log.mjs +32 -0
- package/library/actions/{terminal → web-dapp}/wallet/assert_signatures.mjs +8 -14
- package/library/actions/{terminal → web-dapp}/wallet/list_accounts.mjs +2 -2
- package/library/actions/web-dapp/wallet/read_signatures.mjs +38 -0
- package/library/manifests/core.action-manifest.json +6 -2
- package/library/manifests/extension.action-manifest.json +4 -0
- package/library/manifests/mobile.action-manifest.json +12 -0
- package/library/manifests/{terminal.action-manifest.json → web-dapp.action-manifest.json} +18 -702
- package/package.json +6 -6
- package/site/assets/help-recipes.json +1 -0
- package/adapters/terminal/lib/mainnet-guard.mjs +0 -177
- package/adapters/terminal/lib/origin.mjs +0 -30
- package/adapters/terminal/lib/page-script.mjs +0 -246
- package/adapters/terminal/lib/strict-wallet.mjs +0 -138
- package/dist/adapters/terminal/console-capture.js +0 -59
- package/dist/adapters/terminal/doctor-checks.js +0 -22
- package/dist/adapters/terminal/overlay.js +0 -18
- package/dist/adapters/terminal/readiness.js +0 -11
- package/dist/adapters/terminal/surface.js +0 -133
- package/dist/commands/call.js +0 -671
- package/dist/commands/list-executables.js +0 -245
- package/dist/commands/manifest.js +0 -477
- package/dist/commands/run-engine.js +0 -1301
- package/dist/commands/run.js +0 -730
- package/dist/discovery.js +0 -15
- package/dist/metamask-action-validation.js +0 -68
- package/dist/network-observation.js +0 -290
- package/library/actions/terminal/perps/_venue.mjs +0 -242
- package/library/actions/terminal/perps/assert_orders.mjs +0 -54
- package/library/actions/terminal/perps/assert_positions.mjs +0 -40
- package/library/actions/terminal/perps/read_orders.mjs +0 -26
- package/library/actions/terminal/perps/read_positions.mjs +0 -25
- package/library/actions/terminal/perps/read_snapshot.mjs +0 -40
- package/library/actions/terminal/perps/revoke_agent.mjs +0 -58
- package/library/actions/terminal/perps/teardown_state.mjs +0 -104
- package/library/actions/terminal/ui/navigate.mjs +0 -31
- package/library/actions/terminal/wallet/_signature-log.mjs +0 -177
- package/library/actions/terminal/wallet/read_signatures.mjs +0 -37
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deeeed/metamask-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.82.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mm-harness": "bin/mm-harness"
|
|
@@ -21,14 +21,14 @@
|
|
|
21
21
|
"check:syntax": "find . -name '*.mjs' -print0 | xargs -0 -n1 node --check"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@farmslot/adapter-rn": "0.17.
|
|
25
|
-
"@farmslot/adapter-sdk": "0.
|
|
26
|
-
"@farmslot/adapter-web": "0.
|
|
24
|
+
"@farmslot/adapter-rn": "0.17.1",
|
|
25
|
+
"@farmslot/adapter-sdk": "0.5.0",
|
|
26
|
+
"@farmslot/adapter-web": "0.3.1",
|
|
27
27
|
"@farmslot/agent-runtime": "0.17.0",
|
|
28
28
|
"@farmslot/handoff": "^0.3.1",
|
|
29
29
|
"@farmslot/protocol": "0.34.0",
|
|
30
|
-
"@farmslot/recipe-cli": "0.
|
|
31
|
-
"@farmslot/recipe-runner": "0.
|
|
30
|
+
"@farmslot/recipe-cli": "0.6.0",
|
|
31
|
+
"@farmslot/recipe-runner": "0.25.0",
|
|
32
32
|
"agent-device": "0.19.3",
|
|
33
33
|
"commander": "^12.0.0",
|
|
34
34
|
"esbuild": "0.28.1",
|
|
@@ -173,6 +173,7 @@
|
|
|
173
173
|
"Graph: no unconditional pass, generic intent, hidden start state, or opaque node that collapses preparation, action, assertion, evidence, and teardown.",
|
|
174
174
|
"Assertion modes such as require_unchanged must fail before any corrective action. Verify the negative path leaves the inspected state untouched; an assertion must not repair the behavior it is supposed to test.",
|
|
175
175
|
"Reuse structured reads with assert_output instead of adding a new domain action for every comparison: source names the earlier node, and assert can select $.matchingCount with operator eq and the expected parameter. A status-report action can succeed while reporting missing prerequisites; assert required values before the dependent action. Checking three rows exist does not prove there are exactly three, or that they belong to this task.",
|
|
176
|
+
"Test command nodes: assert_output on the literal pass count. An exit code alone is not proof: jest -t exits 0 when no test matches.",
|
|
176
177
|
"Evidence: inspect the recipe resolution, summary, trace, manifest, logs, and actual artifacts. Verify the values and state required by the claim.",
|
|
177
178
|
"For a change claim, compare the actual before/after values for the same subject. Two captures alone prove neither an update nor freshness. Preserve unchanged observations as unproven rather than retrying to obtain a pass.",
|
|
178
179
|
{
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
// Testnet enforcement for the terminal slot browser. In testnet mode nothing
|
|
2
|
-
// the browser runs may reach a mainnet venue (Hyperliquid, or Lighter when the
|
|
3
|
-
// controller carries it), whatever the dev server happens to serve:
|
|
4
|
-
// - the network layer: --host-resolver-rules makes every mainnet venue host
|
|
5
|
-
// unresolvable for the whole browser (pages, workers, WebSockets), and
|
|
6
|
-
// the wallet host fails and logs each attempt it sees (blocked-mainnet);
|
|
7
|
-
// - the wallet layer: a typed-data request for a mainnet action is refused
|
|
8
|
-
// before MetaMask or the injected signer sees it (refused-mainnet).
|
|
9
|
-
// The venue hosts are the endpoints the venue SDK, the perps controller and
|
|
10
|
-
// the app's source in the checkout connect to.
|
|
11
|
-
|
|
12
|
-
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
13
|
-
import { createRequire } from 'node:module';
|
|
14
|
-
import path from 'node:path';
|
|
15
|
-
|
|
16
|
-
const SDK_PACKAGE = '@nktkas/hyperliquid';
|
|
17
|
-
const CONTROLLER_PACKAGE = '@metamask/perps-controller';
|
|
18
|
-
// Venue API hosts of the perps providers the terminal can configure: the
|
|
19
|
-
// Hyperliquid API/RPC hosts and Lighter's zkLighter API.
|
|
20
|
-
const VENUE_HOST = /^(?:[a-z0-9-]+\.)*(?:hyperliquid(?:-testnet)?\.xyz|zklighter\.elliot\.ai)$/u;
|
|
21
|
-
// A URL in source text, any case; it is then parsed (canonicalUrl).
|
|
22
|
-
const URL_CANDIDATE = /\b(?:https?|wss?):\/\/[^\s'"`<>(){}\\]+/giu;
|
|
23
|
-
// Any mention of a venue host, URL or not: the cross-check of the SDK scan.
|
|
24
|
-
const VENUE_HOST_MENTION = /(?<![\w.-])(?:[a-z0-9-]+\.)*(?:hyperliquid(?:-testnet)?\.xyz|zklighter\.elliot\.ai)(?![\w-])/giu;
|
|
25
|
-
// Request paths of the venue APIs (info, exchange, WebSocket, EVM RPC,
|
|
26
|
-
// Lighter's stream and REST API).
|
|
27
|
-
const API_PATH = /^\/(?:ws|info|exchange|evm|stream|api)(?:\/|$)/u;
|
|
28
|
-
const SOURCE_FILE = /\.(?:m?js|cjs|tsx?)$/u;
|
|
29
|
-
// The venues' web hosts: the Hyperliquid app, which serves the coin icons the
|
|
30
|
-
// terminal shows and the explorer pages it links to. Never an API endpoint,
|
|
31
|
-
// on either network, so never blocked and never proof of the served network.
|
|
32
|
-
export const LINK_HOSTS = new Set(['app.hyperliquid.xyz', 'app.hyperliquid-testnet.xyz']);
|
|
33
|
-
|
|
34
|
-
function packageRoot(checkout, name) {
|
|
35
|
-
try {
|
|
36
|
-
const require = createRequire(path.join(checkout, 'package.json'));
|
|
37
|
-
return path.dirname(require.resolve(`${name}/package.json`));
|
|
38
|
-
} catch {
|
|
39
|
-
const direct = path.join(checkout, 'node_modules', name);
|
|
40
|
-
return existsSync(path.join(direct, 'package.json')) ? direct : null;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function* sourceFiles(dir, depth = 0) {
|
|
45
|
-
if (depth > 12) return;
|
|
46
|
-
for (const entry of readdirSync(dir)) {
|
|
47
|
-
if (entry === 'node_modules') continue;
|
|
48
|
-
const file = path.join(dir, entry);
|
|
49
|
-
const stat = statSync(file);
|
|
50
|
-
if (stat.isDirectory()) yield* sourceFiles(file, depth + 1);
|
|
51
|
-
else if (SOURCE_FILE.test(entry)) yield file;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function isTestnetHost(host) {
|
|
56
|
-
return /(?:^|[.-])testnet(?:[.-]|$)/u.test(host);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Venue URLs in source text, by kind: hosts used with an API path, hosts
|
|
60
|
-
// used bare (a base URL whose paths are appended elsewhere), and hosts with
|
|
61
|
-
// any other path (/rpc, /health, /coins/BTC.svg). Link hosts are left out,
|
|
62
|
-
// except that a link host with an API path is reported (linkApi).
|
|
63
|
-
export function venueUrlHosts(text) {
|
|
64
|
-
const found = { api: new Set(), bare: new Set(), other: new Set(), linkApi: new Set() };
|
|
65
|
-
for (const [candidate] of text.matchAll(URL_CANDIDATE)) {
|
|
66
|
-
const url = canonicalUrl(candidate);
|
|
67
|
-
if (!url || !VENUE_HOST.test(url.hostname)) continue;
|
|
68
|
-
const { hostname: host, pathname } = url;
|
|
69
|
-
// The path as a server reads it: decoded, any case.
|
|
70
|
-
const apiPath = API_PATH.test(safeDecode(pathname).toLowerCase());
|
|
71
|
-
if (LINK_HOSTS.has(host)) {
|
|
72
|
-
// A link host used as an API: the exemption would hide an endpoint.
|
|
73
|
-
if (apiPath) found.linkApi.add(`${host}${pathname}`);
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
if (apiPath) found.api.add(host);
|
|
77
|
-
else if (pathname === '/') found.bare.add(host);
|
|
78
|
-
else found.other.add(host);
|
|
79
|
-
}
|
|
80
|
-
return found;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
// Endpoint hosts in one text: with anyPath (the venue SDK) every venue host;
|
|
85
|
-
// otherwise API URLs and base URLs.
|
|
86
|
-
export function venueEndpointHosts(text, { anyPath = false } = {}) {
|
|
87
|
-
const { api, bare, other } = venueUrlHosts(text);
|
|
88
|
-
return new Set([...api, ...bare, ...(anyPath ? other : [])]);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// The venue hosts the checkout can connect to. { mainnet: [...], testnet: [...] }
|
|
92
|
-
// - the venue SDK: every venue host, whatever the path;
|
|
93
|
-
// - the perps controller and the app's own source: API URLs and base URLs.
|
|
94
|
-
// Link hosts (LINK_HOSTS) are never on either list, and a link host with an
|
|
95
|
-
// API path in any scanned source fails the launch. Every mainnet host the SDK
|
|
96
|
-
// mentions (as a URL or not) must be on the list, or the launch fails.
|
|
97
|
-
export function venueHosts(checkout) {
|
|
98
|
-
const sdk = packageRoot(checkout, SDK_PACKAGE);
|
|
99
|
-
if (!sdk) throw new Error(`${SDK_PACKAGE} is not installed in ${checkout}; the mainnet block list comes from it.`);
|
|
100
|
-
const sources = [
|
|
101
|
-
{ root: sdk, anyPath: true },
|
|
102
|
-
{ root: packageRoot(checkout, CONTROLLER_PACKAGE), anyPath: false },
|
|
103
|
-
{ root: path.join(checkout, 'src'), anyPath: false },
|
|
104
|
-
].filter(({ root }) => root && existsSync(root));
|
|
105
|
-
const hosts = new Set();
|
|
106
|
-
const sdkMentions = new Set();
|
|
107
|
-
for (const { root, anyPath } of sources) {
|
|
108
|
-
for (const file of sourceFiles(root)) {
|
|
109
|
-
const text = readFileSync(file, 'utf8');
|
|
110
|
-
const { linkApi } = venueUrlHosts(text);
|
|
111
|
-
if (linkApi.size > 0) throw new Error(`${file} uses the venue web host as an API (${[...linkApi].join(', ')}); it is exempt from the mainnet block, so refusing to launch.`);
|
|
112
|
-
for (const host of venueEndpointHosts(text, { anyPath })) hosts.add(host);
|
|
113
|
-
if (anyPath) {
|
|
114
|
-
for (const [mention] of text.matchAll(VENUE_HOST_MENTION)) {
|
|
115
|
-
const host = mention.toLowerCase();
|
|
116
|
-
if (!LINK_HOSTS.has(host)) sdkMentions.add(host);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
const mainnet = [...hosts].filter((host) => !isTestnetHost(host)).sort();
|
|
122
|
-
const testnet = [...hosts].filter((host) => isTestnetHost(host)).sort();
|
|
123
|
-
if (mainnet.length === 0) throw new Error(`no mainnet venue host found in ${SDK_PACKAGE} at ${sdk}; refusing to launch without a block list.`);
|
|
124
|
-
const missing = [...sdkMentions].filter((host) => !isTestnetHost(host) && !mainnet.includes(host));
|
|
125
|
-
if (missing.length > 0) throw new Error(`the mainnet block list misses ${missing.join(', ')}, which ${SDK_PACKAGE} names; refusing to launch.`);
|
|
126
|
-
return { mainnet, testnet };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export function mainnetVenueHosts(checkout) {
|
|
130
|
-
return venueHosts(checkout).mainnet;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export function hostResolverRules(hosts) {
|
|
134
|
-
// The host and its fully qualified form (trailing dot) alike.
|
|
135
|
-
return `--host-resolver-rules=${hosts.flatMap((host) => [`MAP ${host} ~NOTFOUND`, `MAP ${host}. ~NOTFOUND`]).join(', ')}`;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// A URL as the network sees it: new URL() lowercases the host, drops a
|
|
139
|
-
// default port and resolves dot segments; a trailing dot names the same host.
|
|
140
|
-
// { hostname, pathname }, or null.
|
|
141
|
-
export function canonicalUrl(value) {
|
|
142
|
-
try {
|
|
143
|
-
const url = new URL(String(value));
|
|
144
|
-
return { hostname: url.hostname.toLowerCase().replace(/\.$/u, ''), pathname: url.pathname };
|
|
145
|
-
} catch {
|
|
146
|
-
return null;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function safeDecode(value) {
|
|
151
|
-
try {
|
|
152
|
-
return decodeURIComponent(value);
|
|
153
|
-
} catch {
|
|
154
|
-
return value;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export function hostOf(url) {
|
|
159
|
-
return canonicalUrl(url)?.hostname ?? null;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export function isMainnetVenueUrl(url, hosts) {
|
|
163
|
-
const host = hostOf(url);
|
|
164
|
-
return host !== null && hosts.includes(host);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// A Hyperliquid action meant for mainnet: user-signed actions carry
|
|
168
|
-
// hyperliquidChain "Mainnet"; L1 actions are signed as a phantom Agent whose
|
|
169
|
-
// source is "a" on mainnet ("b" on testnet).
|
|
170
|
-
export function mainnetTypedDataReason(data) {
|
|
171
|
-
const message = data?.message ?? {};
|
|
172
|
-
if (typeof message.hyperliquidChain === 'string' && message.hyperliquidChain.toLowerCase() === 'mainnet') {
|
|
173
|
-
return 'hyperliquidChain=Mainnet';
|
|
174
|
-
}
|
|
175
|
-
if (data?.primaryType === 'Agent' && message.source === 'a') return 'L1 Agent source=a';
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
// Exact app-origin checks for the terminal slot. A prefix match would let
|
|
2
|
-
// http://localhost:30001 pass for http://localhost:3000, so every check parses
|
|
3
|
-
// the URL and compares origins.
|
|
4
|
-
|
|
5
|
-
export function originOf(url) {
|
|
6
|
-
try {
|
|
7
|
-
return new URL(String(url)).origin;
|
|
8
|
-
} catch {
|
|
9
|
-
return null;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function isAppUrl(url, appOrigin) {
|
|
14
|
-
const origin = originOf(url);
|
|
15
|
-
return origin !== null && origin !== 'null' && origin === originOf(appOrigin);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// A wallet request (or log entry) is accepted only from the default execution
|
|
19
|
-
// context of the tab's top frame, on the app origin, while that frame's
|
|
20
|
-
// committed document is an app URL. The committed URL matters because an
|
|
21
|
-
// about:blank document (a popup the app opened) inherits the app origin. For
|
|
22
|
-
// a page target the main frame id equals the target id.
|
|
23
|
-
export function isAppTopFrameContext(context, { targetId, appOrigin, committedUrl }) {
|
|
24
|
-
if (!context || !targetId) return false;
|
|
25
|
-
const aux = context.auxData ?? {};
|
|
26
|
-
return aux.isDefault === true &&
|
|
27
|
-
aux.frameId === targetId &&
|
|
28
|
-
isAppUrl(context.origin, appOrigin) &&
|
|
29
|
-
isAppUrl(committedUrl, appOrigin);
|
|
30
|
-
}
|
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
// Source of the script the wallet host evaluates in every app document before
|
|
2
|
-
// page scripts. It reports each wallet request the app makes (method, EIP-712
|
|
3
|
-
// primary type, domain/active chain, outcome; no params or signatures) through
|
|
4
|
-
// a CDP binding, and in injected mode also provides the strict test wallet
|
|
5
|
-
// announced over EIP-6963 as io.metamask. It installs itself only in the top
|
|
6
|
-
// frame of a document on the exact app origin: the CDP preload also runs in
|
|
7
|
-
// iframes and after the tab navigates elsewhere. Errors are logged as a code
|
|
8
|
-
// and a fixed category, never as provider text.
|
|
9
|
-
|
|
10
|
-
import { mainnetTypedDataReason } from './mainnet-guard.mjs';
|
|
11
|
-
|
|
12
|
-
export const LOG_BINDING = '__mmTerminalWalletLog';
|
|
13
|
-
export const REQUEST_BINDING = '__mmTerminalWalletRequest';
|
|
14
|
-
export const RESOLVE_FN = '__mmTerminalWalletResolve';
|
|
15
|
-
|
|
16
|
-
export const LOGGED_METHODS = Object.freeze([
|
|
17
|
-
'eth_requestAccounts',
|
|
18
|
-
'wallet_requestPermissions',
|
|
19
|
-
'wallet_revokePermissions',
|
|
20
|
-
'wallet_switchEthereumChain',
|
|
21
|
-
'wallet_addEthereumChain',
|
|
22
|
-
'eth_signTypedData',
|
|
23
|
-
'eth_signTypedData_v3',
|
|
24
|
-
'eth_signTypedData_v4',
|
|
25
|
-
'personal_sign',
|
|
26
|
-
'eth_sign',
|
|
27
|
-
'eth_sendTransaction',
|
|
28
|
-
]);
|
|
29
|
-
|
|
30
|
-
export const ERROR_CATEGORIES = Object.freeze({
|
|
31
|
-
4001: 'user-rejected',
|
|
32
|
-
4100: 'unauthorized',
|
|
33
|
-
4200: 'unsupported-method',
|
|
34
|
-
4900: 'disconnected',
|
|
35
|
-
4901: 'chain-disconnected',
|
|
36
|
-
4902: 'unrecognized-chain',
|
|
37
|
-
[-32602]: 'invalid-params',
|
|
38
|
-
[-32603]: 'internal',
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
function pageMain(config, mainnetTypedDataReason) {
|
|
42
|
-
if (window.top !== window || location.origin !== config.appOrigin) return;
|
|
43
|
-
if (window.__mmTerminalHarness) return;
|
|
44
|
-
// The wallet host's ready check reads this marker on the committed document:
|
|
45
|
-
// the preload ran, and the page's provider is wrapped (covers).
|
|
46
|
-
const wrapped = new WeakSet();
|
|
47
|
-
const harness = {
|
|
48
|
-
signer: config.signer,
|
|
49
|
-
network: config.network,
|
|
50
|
-
wrapped: 0,
|
|
51
|
-
wrapFailures: [],
|
|
52
|
-
covers: (provider) => Boolean(provider) && wrapped.has(provider),
|
|
53
|
-
};
|
|
54
|
-
window.__mmTerminalHarness = harness;
|
|
55
|
-
const logged = new Set(config.loggedMethods);
|
|
56
|
-
// Without the host's binding a wallet request would go unrecorded, so it is
|
|
57
|
-
// refused (fail closed) rather than passed through.
|
|
58
|
-
const unrecorded = () => Object.assign(
|
|
59
|
-
new Error('The harness wallet host is not attached to this document; the request was refused so it cannot go unrecorded.'),
|
|
60
|
-
{ code: 4100 },
|
|
61
|
-
);
|
|
62
|
-
const hasBinding = (binding) => typeof window[binding] === 'function';
|
|
63
|
-
const report = (entry) => {
|
|
64
|
-
try {
|
|
65
|
-
window[config.logBinding](JSON.stringify(entry));
|
|
66
|
-
} catch {
|
|
67
|
-
// Binding unavailable (host detached): the request still proceeds.
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
const describe = (method, params) => {
|
|
71
|
-
const entry = { kind: 'request', method, t: Date.now(), url: location.pathname };
|
|
72
|
-
if (/^eth_signTypedData/.test(method)) {
|
|
73
|
-
try {
|
|
74
|
-
const raw = params?.[1];
|
|
75
|
-
const data = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
76
|
-
entry.primaryType = data?.primaryType ?? null;
|
|
77
|
-
entry.domainName = data?.domain?.name ?? null;
|
|
78
|
-
entry.domainChainId = data?.domain?.chainId == null ? null : Number(BigInt(data.domain.chainId));
|
|
79
|
-
} catch {
|
|
80
|
-
entry.primaryType = null;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
if (method === 'wallet_switchEthereumChain' || method === 'wallet_addEthereumChain') {
|
|
84
|
-
try {
|
|
85
|
-
entry.toChainId = Number.parseInt(params[0].chainId, 16);
|
|
86
|
-
} catch {
|
|
87
|
-
entry.toChainId = null;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return entry;
|
|
91
|
-
};
|
|
92
|
-
const wrap = (provider, source) => {
|
|
93
|
-
if (!provider || typeof provider.request !== 'function' || wrapped.has(provider)) return provider;
|
|
94
|
-
const original = provider.request.bind(provider);
|
|
95
|
-
const request = async (args) => {
|
|
96
|
-
const method = args?.method;
|
|
97
|
-
if (!logged.has(method)) return original(args);
|
|
98
|
-
if (!hasBinding(config.logBinding)) throw unrecorded();
|
|
99
|
-
const entry = { ...describe(method, args?.params), source };
|
|
100
|
-
// Testnet runs never let a mainnet Hyperliquid action reach the signer.
|
|
101
|
-
if (config.network === 'testnet' && /^eth_signTypedData/.test(method)) {
|
|
102
|
-
let reason = null;
|
|
103
|
-
try {
|
|
104
|
-
const raw = args?.params?.[1];
|
|
105
|
-
reason = mainnetTypedDataReason(typeof raw === 'string' ? JSON.parse(raw) : raw);
|
|
106
|
-
} catch {
|
|
107
|
-
reason = null;
|
|
108
|
-
}
|
|
109
|
-
if (reason) {
|
|
110
|
-
report({ ...entry, kind: 'refused-mainnet', reason, outcome: 'refused', errorCode: 4100 });
|
|
111
|
-
throw Object.assign(new Error('Refused: this test run is held to Hyperliquid testnet and the request signs a mainnet action.'), { code: 4100 });
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
try {
|
|
115
|
-
entry.activeChainId = Number.parseInt(await original({ method: 'eth_chainId' }), 16);
|
|
116
|
-
} catch {
|
|
117
|
-
entry.activeChainId = null;
|
|
118
|
-
}
|
|
119
|
-
const started = Date.now();
|
|
120
|
-
try {
|
|
121
|
-
const result = await original(args);
|
|
122
|
-
entry.outcome = /sign/i.test(method) ? 'signed' : 'approved';
|
|
123
|
-
entry.durationMs = Date.now() - started;
|
|
124
|
-
report(entry);
|
|
125
|
-
return result;
|
|
126
|
-
} catch (error) {
|
|
127
|
-
const code = Number.isInteger(error?.code) ? error.code : null;
|
|
128
|
-
entry.outcome = code === 4001 ? 'rejected' : 'error';
|
|
129
|
-
entry.errorCode = code;
|
|
130
|
-
entry.errorCategory = config.errorCategories[code] ?? 'other';
|
|
131
|
-
entry.durationMs = Date.now() - started;
|
|
132
|
-
report(entry);
|
|
133
|
-
throw error;
|
|
134
|
-
}
|
|
135
|
-
};
|
|
136
|
-
try {
|
|
137
|
-
provider.request = request;
|
|
138
|
-
} catch {
|
|
139
|
-
// Read-only property: define it instead (below).
|
|
140
|
-
}
|
|
141
|
-
if (provider.request !== request) {
|
|
142
|
-
try {
|
|
143
|
-
Object.defineProperty(provider, 'request', { value: request, configurable: true, writable: true });
|
|
144
|
-
} catch {
|
|
145
|
-
// Frozen provider: recorded below.
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
// A provider whose request could not be replaced would sign unrecorded.
|
|
149
|
-
if (provider.request !== request) {
|
|
150
|
-
harness.wrapFailures.push(source);
|
|
151
|
-
return provider;
|
|
152
|
-
}
|
|
153
|
-
wrapped.add(provider);
|
|
154
|
-
harness.wrapped += 1;
|
|
155
|
-
return provider;
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
if (config.signer === 'injected') {
|
|
159
|
-
const listeners = {};
|
|
160
|
-
const emit = (event, ...args) => (listeners[event] ?? []).forEach((fn) => {
|
|
161
|
-
try { fn(...args); } catch { /* listener errors stay in the app */ }
|
|
162
|
-
});
|
|
163
|
-
const pending = new Map();
|
|
164
|
-
let nextId = 1;
|
|
165
|
-
window[config.resolveFn] = (id, outcome) => {
|
|
166
|
-
const entry = pending.get(id);
|
|
167
|
-
if (!entry) return;
|
|
168
|
-
pending.delete(id);
|
|
169
|
-
if (outcome.error) entry.reject(Object.assign(new Error(outcome.error.message), { code: outcome.error.code }));
|
|
170
|
-
else entry.resolve(outcome.result);
|
|
171
|
-
};
|
|
172
|
-
let lastChain = null;
|
|
173
|
-
const provider = {
|
|
174
|
-
isMetaMask: true,
|
|
175
|
-
_metamask: { isUnlocked: async () => true },
|
|
176
|
-
request: ({ method, params }) => new Promise((resolve, reject) => {
|
|
177
|
-
if (!hasBinding(config.requestBinding)) {
|
|
178
|
-
reject(unrecorded());
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
const id = nextId++;
|
|
182
|
-
pending.set(id, { resolve, reject });
|
|
183
|
-
window[config.requestBinding](JSON.stringify({ id, method, params }));
|
|
184
|
-
}).then((result) => {
|
|
185
|
-
if (method === 'wallet_switchEthereumChain' && params?.[0]?.chainId !== lastChain) {
|
|
186
|
-
lastChain = params[0].chainId;
|
|
187
|
-
emit('chainChanged', lastChain);
|
|
188
|
-
}
|
|
189
|
-
if (method === 'eth_chainId') lastChain = result;
|
|
190
|
-
if (method === 'eth_requestAccounts') emit('connect', { chainId: lastChain ?? '0x1' });
|
|
191
|
-
return result;
|
|
192
|
-
}),
|
|
193
|
-
on: (event, fn) => { (listeners[event] ??= []).push(fn); return provider; },
|
|
194
|
-
removeListener: (event, fn) => { listeners[event] = (listeners[event] ?? []).filter((f) => f !== fn); return provider; },
|
|
195
|
-
enable: () => provider.request({ method: 'eth_requestAccounts' }),
|
|
196
|
-
};
|
|
197
|
-
wrap(provider, 'injected-strict-wallet');
|
|
198
|
-
window.ethereum = provider;
|
|
199
|
-
const info = {
|
|
200
|
-
uuid: '5d1e5b4e-e2e0-4c3f-9a1d-000000000001',
|
|
201
|
-
name: 'MetaMask (strict test wallet)',
|
|
202
|
-
icon: "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'/>",
|
|
203
|
-
rdns: 'io.metamask',
|
|
204
|
-
};
|
|
205
|
-
const announce = () => window.dispatchEvent(new CustomEvent('eip6963:announceProvider', { detail: Object.freeze({ info, provider }) }));
|
|
206
|
-
window.addEventListener('eip6963:requestProvider', announce);
|
|
207
|
-
announce();
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Extension mode: wrap whatever provider MetaMask injects, however the app
|
|
212
|
-
// reaches it (window.ethereum or an EIP-6963 announcement).
|
|
213
|
-
let current = window.ethereum ? wrap(window.ethereum, 'window.ethereum') : undefined;
|
|
214
|
-
try {
|
|
215
|
-
Object.defineProperty(window, 'ethereum', {
|
|
216
|
-
configurable: true,
|
|
217
|
-
get: () => current,
|
|
218
|
-
set: (value) => { current = wrap(value, 'window.ethereum'); },
|
|
219
|
-
});
|
|
220
|
-
} catch {
|
|
221
|
-
// A non-configurable window.ethereum is still wrapped by the poll below.
|
|
222
|
-
}
|
|
223
|
-
window.addEventListener('eip6963:announceProvider', (event) => {
|
|
224
|
-
wrap(event.detail?.provider, `eip6963:${event.detail?.info?.rdns ?? 'unknown'}`);
|
|
225
|
-
}, true);
|
|
226
|
-
let polls = 0;
|
|
227
|
-
const poll = setInterval(() => {
|
|
228
|
-
if (window.ethereum) wrap(window.ethereum, 'window.ethereum');
|
|
229
|
-
if (++polls > 200) clearInterval(poll);
|
|
230
|
-
}, 50);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
export function pageScriptSource({ signer, appOrigin, network = 'testnet' }) {
|
|
234
|
-
const origin = new URL(appOrigin).origin;
|
|
235
|
-
const config = {
|
|
236
|
-
signer,
|
|
237
|
-
network,
|
|
238
|
-
appOrigin: origin,
|
|
239
|
-
errorCategories: ERROR_CATEGORIES,
|
|
240
|
-
loggedMethods: LOGGED_METHODS,
|
|
241
|
-
logBinding: LOG_BINDING,
|
|
242
|
-
requestBinding: REQUEST_BINDING,
|
|
243
|
-
resolveFn: RESOLVE_FN,
|
|
244
|
-
};
|
|
245
|
-
return `(${pageMain.toString()})(${JSON.stringify(config)}, ${mainnetTypedDataReason.toString()});`;
|
|
246
|
-
}
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
// EIP-1193 test wallet that enforces MetaMask's typed-data rule: reject
|
|
2
|
-
// eth_signTypedData_v4 whose domain chainId differs from the active chain.
|
|
3
|
-
// Signs with a viem account held by the wallet host; never exposes key
|
|
4
|
-
// material. Ported from the TAT-4032 migration e2e strict wallet.
|
|
5
|
-
|
|
6
|
-
export const DEFAULT_KNOWN_CHAINS = Object.freeze([1, 10, 137, 8453, 42161, 421614, 59144, 59141, 11155111]);
|
|
7
|
-
|
|
8
|
-
const READ_RPC = {
|
|
9
|
-
1: 'https://ethereum-rpc.publicnode.com',
|
|
10
|
-
42161: 'https://arbitrum-one-rpc.publicnode.com',
|
|
11
|
-
421614: 'https://arbitrum-sepolia-rpc.publicnode.com',
|
|
12
|
-
59144: 'https://linea-rpc.publicnode.com',
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
// Only these reads go to the public RPC. Anything else the wallet does not
|
|
16
|
-
// answer itself (eth_sendRawTransaction, account or filter methods, unknown
|
|
17
|
-
// wallet_* calls) is refused before any network request.
|
|
18
|
-
export const READ_ONLY_RPC_METHODS = Object.freeze([
|
|
19
|
-
'eth_blockNumber',
|
|
20
|
-
'eth_call',
|
|
21
|
-
'eth_estimateGas',
|
|
22
|
-
'eth_feeHistory',
|
|
23
|
-
'eth_gasPrice',
|
|
24
|
-
'eth_getBalance',
|
|
25
|
-
'eth_getBlockByHash',
|
|
26
|
-
'eth_getBlockByNumber',
|
|
27
|
-
'eth_getCode',
|
|
28
|
-
'eth_getLogs',
|
|
29
|
-
'eth_getStorageAt',
|
|
30
|
-
'eth_getTransactionByHash',
|
|
31
|
-
'eth_getTransactionCount',
|
|
32
|
-
'eth_getTransactionReceipt',
|
|
33
|
-
'eth_maxPriorityFeePerGas',
|
|
34
|
-
'web3_clientVersion',
|
|
35
|
-
]);
|
|
36
|
-
const READ_ONLY = new Set(READ_ONLY_RPC_METHODS);
|
|
37
|
-
|
|
38
|
-
function rpcError(message, code) {
|
|
39
|
-
return Object.assign(new Error(message), { code });
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function toBigIntDeep(types, primaryType, message) {
|
|
43
|
-
const out = {};
|
|
44
|
-
for (const field of types[primaryType] ?? []) {
|
|
45
|
-
const value = message[field.name];
|
|
46
|
-
if (value === undefined) continue;
|
|
47
|
-
if (/^u?int\d*$/u.test(field.type)) out[field.name] = BigInt(value);
|
|
48
|
-
else if (types[field.type]) out[field.name] = toBigIntDeep(types, field.type, value);
|
|
49
|
-
else out[field.name] = value;
|
|
50
|
-
}
|
|
51
|
-
return out;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function createStrictWallet({
|
|
55
|
-
account,
|
|
56
|
-
chainId = 42161,
|
|
57
|
-
knownChains = DEFAULT_KNOWN_CHAINS,
|
|
58
|
-
refuseSwitch = false,
|
|
59
|
-
fetchImpl = globalThis.fetch,
|
|
60
|
-
}) {
|
|
61
|
-
let activeChain = chainId;
|
|
62
|
-
const known = new Set(knownChains);
|
|
63
|
-
let rpcId = 1;
|
|
64
|
-
|
|
65
|
-
async function request({ method, params = [] }) {
|
|
66
|
-
switch (method) {
|
|
67
|
-
case 'eth_chainId':
|
|
68
|
-
return `0x${activeChain.toString(16)}`;
|
|
69
|
-
case 'net_version':
|
|
70
|
-
return String(activeChain);
|
|
71
|
-
case 'eth_accounts':
|
|
72
|
-
case 'eth_requestAccounts':
|
|
73
|
-
return [account.address];
|
|
74
|
-
case 'wallet_requestPermissions':
|
|
75
|
-
case 'wallet_getPermissions':
|
|
76
|
-
return [{ parentCapability: 'eth_accounts', caveats: [{ type: 'restrictReturnedAccounts', value: [account.address.toLowerCase()] }] }];
|
|
77
|
-
case 'wallet_revokePermissions':
|
|
78
|
-
return null;
|
|
79
|
-
case 'metamask_getProviderState':
|
|
80
|
-
return { accounts: [account.address.toLowerCase()], chainId: `0x${activeChain.toString(16)}`, isUnlocked: true, networkVersion: String(activeChain) };
|
|
81
|
-
case 'wallet_switchEthereumChain': {
|
|
82
|
-
const id = Number.parseInt(params[0].chainId, 16);
|
|
83
|
-
if (refuseSwitch) throw rpcError('User rejected the request.', 4001);
|
|
84
|
-
if (!known.has(id)) throw rpcError(`Unrecognized chain ID ${params[0].chainId}`, 4902);
|
|
85
|
-
activeChain = id;
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
case 'wallet_addEthereumChain':
|
|
89
|
-
known.add(Number.parseInt(params[0].chainId, 16));
|
|
90
|
-
return null;
|
|
91
|
-
case 'eth_signTypedData_v4': {
|
|
92
|
-
const [from, raw] = params;
|
|
93
|
-
const data = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
94
|
-
const domainChain = Number(BigInt(data.domain.chainId));
|
|
95
|
-
if (String(from).toLowerCase() !== account.address.toLowerCase()) throw rpcError('Unknown account', 4100);
|
|
96
|
-
if (domainChain !== activeChain) {
|
|
97
|
-
throw rpcError(`Provided chainId "${domainChain}" must match the active chainId "${activeChain}"`, -32602);
|
|
98
|
-
}
|
|
99
|
-
const types = { ...data.types };
|
|
100
|
-
delete types.EIP712Domain;
|
|
101
|
-
return account.signTypedData({
|
|
102
|
-
domain: { ...data.domain, chainId: domainChain },
|
|
103
|
-
types,
|
|
104
|
-
primaryType: data.primaryType,
|
|
105
|
-
message: toBigIntDeep(types, data.primaryType, data.message),
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
case 'personal_sign': {
|
|
109
|
-
const [message, from] = params;
|
|
110
|
-
if (String(from).toLowerCase() !== account.address.toLowerCase()) throw rpcError('Unknown account', 4100);
|
|
111
|
-
return account.signMessage({ message: { raw: message } });
|
|
112
|
-
}
|
|
113
|
-
case 'eth_sendTransaction':
|
|
114
|
-
case 'eth_sign':
|
|
115
|
-
throw rpcError(`${method} is not supported by the strict test wallet`, 4200);
|
|
116
|
-
default: {
|
|
117
|
-
if (!READ_ONLY.has(method)) throw rpcError(`${method} is not supported by the strict test wallet`, 4200);
|
|
118
|
-
const url = READ_RPC[activeChain];
|
|
119
|
-
if (!url) throw rpcError(`No read RPC for chain ${activeChain}`, 4200);
|
|
120
|
-
const response = await fetchImpl(url, {
|
|
121
|
-
method: 'POST',
|
|
122
|
-
headers: { 'content-type': 'application/json' },
|
|
123
|
-
body: JSON.stringify({ jsonrpc: '2.0', id: rpcId++, method, params }),
|
|
124
|
-
});
|
|
125
|
-
const body = await response.json();
|
|
126
|
-
if (body.error) throw rpcError(body.error.message, body.error.code);
|
|
127
|
-
return body.result;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
request,
|
|
134
|
-
get chainId() {
|
|
135
|
-
return activeChain;
|
|
136
|
-
},
|
|
137
|
-
};
|
|
138
|
-
}
|