@deeeed/metamask-harness 0.27.0 → 0.29.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/README.md +41 -0
- package/adapters/extension/build-lavamoat.sh +2 -1
- package/adapters/extension/ensure-browser.sh +82 -9
- package/adapters/extension/inject.mjs +1 -0
- package/adapters/extension/launch-browser.cjs +83 -1
- package/adapters/extension/lib/chrome-args.cjs +325 -1
- package/adapters/extension/lib/playwright-cdp.cjs +34 -0
- package/adapters/extension/lib/slot-title.cjs +2 -4
- package/adapters/extension/lib/validation-launch-supervisor.cjs +292 -0
- package/adapters/extension/lib/validation-process-ownership.cjs +69 -0
- package/adapters/extension/reattach.sh +2 -1
- package/adapters/extension/sidepanel-toggle.sh +14 -96
- package/adapters/extension/wallet-fixture-state.cjs +8 -31
- package/adapters/manifest.json +16 -0
- package/adapters/shared/private-atomic-write.cjs +47 -0
- package/adapters/shared/setup-base.sh +864 -0
- package/dist/adapters/extension/runtime.js +367 -24
- package/dist/adapters/extension/validation-process-ownership.js +10 -0
- package/dist/adapters.js +0 -11
- package/dist/cli-commands.js +2 -1
- package/dist/command-contract.js +13 -0
- package/dist/commands/call.js +38 -1
- package/dist/commands/launch/extension.js +130 -19
- package/dist/commands/manifest.js +201 -6
- package/dist/commands/parse-args.js +1 -0
- package/dist/commands/run.js +74 -8
- package/dist/commands/setup-base.js +24 -0
- package/dist/mm-harness-cli.js +30 -2
- package/dist/recipe-security.js +1 -0
- package/library/actions/extension/analytics/consent.mjs +203 -0
- package/library/actions/extension/analytics/set_consent.mjs +19 -143
- package/library/actions/extension/perps/perps.mjs +2 -16
- package/library/actions/extension/perps/state.mjs +20 -0
- package/library/actions/extension/ui/locators.mjs +13 -0
- package/library/actions/extension/wallet/list_accounts.mjs +3 -25
- package/library/actions/extension/wallet/read_state.mjs +3 -23
- package/library/actions/extension/wallet/select_account.mjs +6 -33
- package/library/actions/extension/wallet/setup.mjs +2 -20
- package/library/actions/extension/wallet/state.mjs +111 -0
- package/library/actions/mobile/platform/observe-ui.mjs +21 -0
- package/library/actions/mobile/ui/locators.mjs +17 -0
- package/library/actions/shared/ui/locators.mjs +160 -0
- package/library/manifests/extension.action-manifest.json +18 -0
- package/library/manifests/mobile.action-manifest.json +18 -0
- package/library/recipes/runner/action-validation.extension.recipe.json +1 -1
- package/library/recipes/runner/action-validation.mobile.recipe.json +1 -1
- package/package.json +7 -4
- package/scripts/site-contrast.mjs +538 -0
- package/site/architecture.html +415 -0
- package/site/assets/progress.mjs +272 -0
- package/site/assets/style.css +808 -0
- package/site/cheatsheet.html +305 -0
- package/site/index.html +643 -0
- package/site/recipes.html +396 -0
- package/site/reviewers.html +374 -0
- package/site/tutorials/index.html +180 -0
- package/site/tutorials/v1.html +211 -0
- package/site/tutorials/v2.html +207 -0
- package/site/tutorials/v3.html +214 -0
- package/site/tutorials/v4.html +195 -0
- package/site/tutorials/v5.html +163 -0
- package/site/tutorials/v6.html +165 -0
- package/site/tutorials/v7.html +184 -0
package/dist/mm-harness-cli.js
CHANGED
|
@@ -8,6 +8,7 @@ import { withCommandJournal } from "./command-journal.js";
|
|
|
8
8
|
import { JsonStreamWriter } from "./json-stream.js";
|
|
9
9
|
import { handleUpdate, maybeNudge } from "./commands/update.js";
|
|
10
10
|
import { handleCallHelp } from "./commands/call.js";
|
|
11
|
+
import { handleSetupBase } from "./commands/setup-base.js";
|
|
11
12
|
import { getAdapterSurface } from "./adapters/surface.js";
|
|
12
13
|
import { detectAdapter } from "./harness.js";
|
|
13
14
|
import {
|
|
@@ -20,6 +21,28 @@ globalThis.__MM_HARNESS_WRAPPER__ = true;
|
|
|
20
21
|
const { main: recipeMain } = await import("./cli.js");
|
|
21
22
|
const rawArgv = process.argv.slice(2);
|
|
22
23
|
const REAL = [
|
|
24
|
+
{
|
|
25
|
+
name: "setup-base",
|
|
26
|
+
summary: "Bootstrap numbered MetaMask product checkouts and install their dependencies.",
|
|
27
|
+
example: "mm-harness setup-base --only core --counts core=1",
|
|
28
|
+
helpText: `mm-harness setup-base [flags]
|
|
29
|
+
|
|
30
|
+
Bootstrap numbered MetaMask Extension, Mobile, and Core checkouts under one
|
|
31
|
+
base directory, then run each checkout's pinned dependency install.
|
|
32
|
+
|
|
33
|
+
--dir <base> Destination root
|
|
34
|
+
--counts <repo=n,...> Copies per selected repository
|
|
35
|
+
--only <extension,mobile,core> Repository subset
|
|
36
|
+
--dry-run Print the plan without changing anything
|
|
37
|
+
--force Override the free-space refusal
|
|
38
|
+
--json Machine-readable terminal summary
|
|
39
|
+
--show-config Print saved preferences and exit
|
|
40
|
+
--reset-config Remove saved preferences and exit
|
|
41
|
+
--skip-harness-update Skip the installed-version notice
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
mm-harness setup-base --only core --counts core=1`
|
|
45
|
+
},
|
|
23
46
|
{
|
|
24
47
|
name: "status",
|
|
25
48
|
aliases: ["health", "home"],
|
|
@@ -121,6 +144,7 @@ Materialize:
|
|
|
121
144
|
|
|
122
145
|
query Search names, categories, fields, and descriptions (typo-tolerant)
|
|
123
146
|
--action <name> Describe one action; fuzzy-resolves like call (short or full name)
|
|
147
|
+
--matrix Compare action availability across Mobile, Extension, and Core
|
|
124
148
|
--categories List compact action categories and counts
|
|
125
149
|
--category <name> List only one category (for example ui, wallet, or perps)
|
|
126
150
|
--adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
|
|
@@ -130,6 +154,7 @@ Materialize:
|
|
|
130
154
|
|
|
131
155
|
Example:
|
|
132
156
|
mm-harness actions --adapter mobile
|
|
157
|
+
mm-harness actions --matrix --json
|
|
133
158
|
mm-harness actions positions --adapter mobile
|
|
134
159
|
mm-harness actions --adapter mobile --categories --json
|
|
135
160
|
mm-harness actions --adapter mobile --category ui --json
|
|
@@ -601,7 +626,7 @@ const HELP_GROUPS = [
|
|
|
601
626
|
{
|
|
602
627
|
title: "MAINTAIN",
|
|
603
628
|
blurb: "keep the installed harness current with the npm registry",
|
|
604
|
-
commands: ["update"]
|
|
629
|
+
commands: ["setup-base", "update"]
|
|
605
630
|
}
|
|
606
631
|
];
|
|
607
632
|
function commandMeta(name) {
|
|
@@ -730,7 +755,7 @@ program.command("completions").description("Install/print bundled shell tab-comp
|
|
|
730
755
|
const result = spawnSync("bash", [script, ...rawArgv.slice(1)], { stdio: "inherit" });
|
|
731
756
|
process.exit(result.status ?? 1);
|
|
732
757
|
});
|
|
733
|
-
const NUDGE_SKIP = ["update", "completions", "completion-candidates"];
|
|
758
|
+
const NUDGE_SKIP = ["setup-base", "update", "completions", "completion-candidates"];
|
|
734
759
|
if (rawArgv.length > 0 && !NUDGE_SKIP.includes(rawArgv[0])) {
|
|
735
760
|
maybeNudge();
|
|
736
761
|
}
|
|
@@ -792,6 +817,9 @@ if (rawArgv.length === 0) {
|
|
|
792
817
|
process.stdout.write(groupedHelp());
|
|
793
818
|
process.exit(0);
|
|
794
819
|
}
|
|
820
|
+
if (rawArgv[0] === "setup-base") {
|
|
821
|
+
process.exit(handleSetupBase(rawArgv.slice(1)));
|
|
822
|
+
}
|
|
795
823
|
const preflightBypass = /* @__PURE__ */ new Set([
|
|
796
824
|
...HIDDEN,
|
|
797
825
|
"completions"
|
package/dist/recipe-security.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { lstat, readFile, readdir, realpath } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
const READ_ONLY_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
|
|
5
|
+
"ui.locators",
|
|
5
6
|
"metamask.wallet.fixture_status",
|
|
6
7
|
"metamask.wallet.list_accounts",
|
|
7
8
|
"metamask.wallet.read_state",
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
export function consentControllerExpression({
|
|
2
|
+
bridgeSymbol,
|
|
3
|
+
participate,
|
|
4
|
+
marketing,
|
|
5
|
+
timeoutMs,
|
|
6
|
+
}) {
|
|
7
|
+
return `(async () => {
|
|
8
|
+
const submit = globalThis.stateHooks?.submitRequestToBackground;
|
|
9
|
+
const bridgeKey = Symbol.for(${JSON.stringify(bridgeSymbol)});
|
|
10
|
+
const capturedChrome = globalThis[bridgeKey];
|
|
11
|
+
let port;
|
|
12
|
+
let nextId = Date.now();
|
|
13
|
+
// Popup connections are ref-counted, so temporary ports cannot erase the
|
|
14
|
+
// live fullscreen or side-panel registration for the same tab.
|
|
15
|
+
const connectionName = 'popup';
|
|
16
|
+
|
|
17
|
+
const rawSubmit = (method, params) => new Promise((resolve, reject) => {
|
|
18
|
+
if (!port) {
|
|
19
|
+
if (typeof capturedChrome?.runtime?.connect !== 'function') {
|
|
20
|
+
reject(new Error('Extension consent setup could not reach the background controller.'));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
port = capturedChrome.runtime.connect({ name: connectionName });
|
|
24
|
+
}
|
|
25
|
+
const id = nextId++;
|
|
26
|
+
const timer = setTimeout(() => {
|
|
27
|
+
port.onMessage.removeListener(listener);
|
|
28
|
+
reject(new Error(method + ' timed out after ${Number(timeoutMs)}ms'));
|
|
29
|
+
}, ${Number(timeoutMs)});
|
|
30
|
+
const listener = (message) => {
|
|
31
|
+
const data = message?.name === 'controller' ? message.data : null;
|
|
32
|
+
if (data?.id !== id) return;
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
port.onMessage.removeListener(listener);
|
|
35
|
+
if (data.error) reject(new Error(JSON.stringify(data.error)));
|
|
36
|
+
else resolve(data.result);
|
|
37
|
+
};
|
|
38
|
+
port.onMessage.addListener(listener);
|
|
39
|
+
try {
|
|
40
|
+
port.postMessage({
|
|
41
|
+
name: 'controller',
|
|
42
|
+
data: { jsonrpc: '2.0', id, method, params }
|
|
43
|
+
});
|
|
44
|
+
} catch (error) {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
port.onMessage.removeListener(listener);
|
|
47
|
+
reject(error);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const callController = typeof submit === 'function' ? submit : rawSubmit;
|
|
52
|
+
const readBackgroundState = () => new Promise((resolve, reject) => {
|
|
53
|
+
if (typeof capturedChrome?.runtime?.connect !== 'function') {
|
|
54
|
+
reject(new Error('Extension consent state is unavailable.'));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const statePort = capturedChrome.runtime.connect({ name: connectionName });
|
|
58
|
+
const timer = setTimeout(() => {
|
|
59
|
+
statePort.onMessage.removeListener(listener);
|
|
60
|
+
statePort.disconnect();
|
|
61
|
+
reject(new Error('Extension consent state timed out after ${Number(timeoutMs)}ms.'));
|
|
62
|
+
}, ${Number(timeoutMs)});
|
|
63
|
+
const listener = (message) => {
|
|
64
|
+
const data = message?.name === 'controller' ? message.data : null;
|
|
65
|
+
if (data?.method !== 'START_UI_SYNC') return;
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
statePort.onMessage.removeListener(listener);
|
|
68
|
+
statePort.disconnect();
|
|
69
|
+
resolve(data.params?.[0]);
|
|
70
|
+
};
|
|
71
|
+
statePort.onMessage.addListener(listener);
|
|
72
|
+
});
|
|
73
|
+
const readConsentState = async () => {
|
|
74
|
+
const hooks = globalThis.stateHooks || {};
|
|
75
|
+
const storeState = hooks.store?.getState?.();
|
|
76
|
+
const cleanState = typeof hooks.getCleanAppState === 'function'
|
|
77
|
+
? await hooks.getCleanAppState()
|
|
78
|
+
: undefined;
|
|
79
|
+
const root = capturedChrome
|
|
80
|
+
? await readBackgroundState()
|
|
81
|
+
: storeState ?? cleanState;
|
|
82
|
+
if (!root) {
|
|
83
|
+
throw new Error('Extension consent state is unavailable.');
|
|
84
|
+
}
|
|
85
|
+
const metamask = root?.metamask ?? root ?? {};
|
|
86
|
+
if (
|
|
87
|
+
typeof metamask.optedIn !== 'boolean' ||
|
|
88
|
+
typeof metamask.dataCollectionForMarketing !== 'boolean'
|
|
89
|
+
) {
|
|
90
|
+
throw new Error('Extension consent state did not contain boolean consent fields.');
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
state: {
|
|
94
|
+
optedIn: metamask.optedIn,
|
|
95
|
+
dataCollectionForMarketing: metamask.dataCollectionForMarketing,
|
|
96
|
+
analyticsId: metamask.analyticsId ? 'set' : null
|
|
97
|
+
},
|
|
98
|
+
source: capturedChrome
|
|
99
|
+
? 'background-controller-flat-state'
|
|
100
|
+
: storeState
|
|
101
|
+
? 'debug-page-store'
|
|
102
|
+
: 'debug-clean-app-state'
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
const waitForConsentState = async () => {
|
|
106
|
+
const expected = {
|
|
107
|
+
optedIn: ${JSON.stringify(participate)},
|
|
108
|
+
dataCollectionForMarketing: ${JSON.stringify(marketing)}
|
|
109
|
+
};
|
|
110
|
+
const deadline = Date.now() + ${Number(timeoutMs)};
|
|
111
|
+
let result;
|
|
112
|
+
do {
|
|
113
|
+
result = await readConsentState();
|
|
114
|
+
if (Object.entries(expected).every(([key, value]) => result.state[key] === value)) {
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
118
|
+
} while (Date.now() < deadline);
|
|
119
|
+
throw new Error(
|
|
120
|
+
'Consent state did not settle to ' + JSON.stringify(expected) +
|
|
121
|
+
'; got ' + JSON.stringify(result?.state) + '.',
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
const setMarketing = async (value) => {
|
|
125
|
+
await callController('setDataCollectionForMarketing', [value]);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
if (!${JSON.stringify(participate)}) await setMarketing(false);
|
|
130
|
+
const analyticsId = await callController(
|
|
131
|
+
'setParticipateInMetaMetrics',
|
|
132
|
+
[${JSON.stringify(participate)}],
|
|
133
|
+
);
|
|
134
|
+
if (${JSON.stringify(participate)}) {
|
|
135
|
+
await setMarketing(${JSON.stringify(marketing)});
|
|
136
|
+
}
|
|
137
|
+
const settled = await waitForConsentState();
|
|
138
|
+
return {
|
|
139
|
+
analyticsId: analyticsId ? 'set' : null,
|
|
140
|
+
state: settled.state,
|
|
141
|
+
stateSource: settled.source
|
|
142
|
+
};
|
|
143
|
+
} finally {
|
|
144
|
+
port?.disconnect();
|
|
145
|
+
if (capturedChrome) delete globalThis[bridgeKey];
|
|
146
|
+
}
|
|
147
|
+
})()`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function withConsentChromeBridge(page, {
|
|
151
|
+
bridgeSymbol,
|
|
152
|
+
timeoutMs,
|
|
153
|
+
}, operation) {
|
|
154
|
+
const preload = await page.session.call('Page.addScriptToEvaluateOnNewDocument', {
|
|
155
|
+
source: `Object.defineProperty(
|
|
156
|
+
globalThis,
|
|
157
|
+
Symbol.for(${JSON.stringify(bridgeSymbol)}),
|
|
158
|
+
{ value: globalThis.chrome, configurable: true }
|
|
159
|
+
);`,
|
|
160
|
+
});
|
|
161
|
+
if (!preload?.identifier) {
|
|
162
|
+
throw new Error('Extension consent setup could not register its temporary Chrome bridge.');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
let notifyLoaded;
|
|
167
|
+
const loaded = new Promise((resolve) => {
|
|
168
|
+
notifyLoaded = resolve;
|
|
169
|
+
});
|
|
170
|
+
const unsubscribe = page.session.on('Page.loadEventFired', () => notifyLoaded?.());
|
|
171
|
+
let reloadTimeout;
|
|
172
|
+
try {
|
|
173
|
+
await page.session.call('Page.reload', { ignoreCache: false });
|
|
174
|
+
await Promise.race([
|
|
175
|
+
loaded,
|
|
176
|
+
new Promise((_, reject) => {
|
|
177
|
+
reloadTimeout = setTimeout(
|
|
178
|
+
() => reject(new Error(`Extension consent reload timed out after ${timeoutMs}ms.`)),
|
|
179
|
+
timeoutMs,
|
|
180
|
+
);
|
|
181
|
+
}),
|
|
182
|
+
]);
|
|
183
|
+
} finally {
|
|
184
|
+
clearTimeout(reloadTimeout);
|
|
185
|
+
unsubscribe();
|
|
186
|
+
}
|
|
187
|
+
await page.waitForExpression(
|
|
188
|
+
`typeof globalThis[Symbol.for(${JSON.stringify(bridgeSymbol)})]?.runtime?.connect === 'function'`,
|
|
189
|
+
{ timeoutMs },
|
|
190
|
+
);
|
|
191
|
+
return await operation();
|
|
192
|
+
} finally {
|
|
193
|
+
try {
|
|
194
|
+
await page.session.call('Page.removeScriptToEvaluateOnNewDocument', {
|
|
195
|
+
identifier: preload.identifier,
|
|
196
|
+
});
|
|
197
|
+
} finally {
|
|
198
|
+
await page.evaluate(
|
|
199
|
+
`delete globalThis[Symbol.for(${JSON.stringify(bridgeSymbol)})]`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -1,155 +1,31 @@
|
|
|
1
1
|
import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
2
|
import { consentParams } from '../../shared/analytics/consent.mjs';
|
|
3
|
+
import {
|
|
4
|
+
consentControllerExpression,
|
|
5
|
+
withConsentChromeBridge,
|
|
6
|
+
} from './consent.mjs';
|
|
3
7
|
|
|
4
8
|
runAdapter((input) => withExtensionPage(input, async (page) => {
|
|
5
9
|
const { participate, marketing, timeoutMs } = consentParams(input.node);
|
|
6
10
|
const bridgeSymbol = 'metamask-harness-consent-bridge';
|
|
7
11
|
|
|
8
|
-
async function readConsentState() {
|
|
9
|
-
return page.evaluate(`(async () => {
|
|
10
|
-
const hooks = globalThis.stateHooks || {};
|
|
11
|
-
const storeState = hooks.store?.getState?.();
|
|
12
|
-
const cleanState = typeof hooks.getCleanAppState === 'function'
|
|
13
|
-
? await hooks.getCleanAppState()
|
|
14
|
-
: undefined;
|
|
15
|
-
if (!storeState && !cleanState) {
|
|
16
|
-
throw new Error('Extension consent state is unavailable.');
|
|
17
|
-
}
|
|
18
|
-
const metamask = storeState?.metamask ?? cleanState?.metamask ?? {};
|
|
19
|
-
return {
|
|
20
|
-
optedIn: Boolean(metamask.optedIn),
|
|
21
|
-
dataCollectionForMarketing: Boolean(metamask.dataCollectionForMarketing),
|
|
22
|
-
analyticsId: metamask.analyticsId ? 'set' : null
|
|
23
|
-
};
|
|
24
|
-
})()`, { awaitPromise: true });
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async function waitForConsentState(expected) {
|
|
28
|
-
const deadline = Date.now() + timeoutMs;
|
|
29
|
-
let state;
|
|
30
|
-
do {
|
|
31
|
-
state = await readConsentState();
|
|
32
|
-
if (Object.entries(expected).every(([key, value]) => state[key] === value)) {
|
|
33
|
-
return state;
|
|
34
|
-
}
|
|
35
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
36
|
-
} while (Date.now() < deadline);
|
|
37
|
-
throw new Error(`Consent state did not settle to ${JSON.stringify(expected)}; got ${JSON.stringify(state)}.`);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
12
|
const hasDebugBridge = await page.evaluate(
|
|
41
13
|
`typeof globalThis.stateHooks?.submitRequestToBackground === 'function'`,
|
|
42
14
|
);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
});
|
|
58
|
-
const unsubscribe = page.session.on('Page.loadEventFired', () => notifyLoaded?.());
|
|
59
|
-
let reloadTimeout;
|
|
60
|
-
try {
|
|
61
|
-
await page.session.call('Page.reload', { ignoreCache: false });
|
|
62
|
-
await Promise.race([
|
|
63
|
-
loaded,
|
|
64
|
-
new Promise((_, reject) => {
|
|
65
|
-
reloadTimeout = setTimeout(
|
|
66
|
-
() => reject(new Error(`Extension consent reload timed out after ${timeoutMs}ms.`)),
|
|
67
|
-
timeoutMs,
|
|
68
|
-
);
|
|
69
|
-
}),
|
|
70
|
-
]);
|
|
71
|
-
} finally {
|
|
72
|
-
clearTimeout(reloadTimeout);
|
|
73
|
-
unsubscribe();
|
|
74
|
-
}
|
|
75
|
-
await page.waitForExpression(
|
|
76
|
-
`typeof globalThis[Symbol.for(${JSON.stringify(bridgeSymbol)})]?.runtime?.connect === 'function'`,
|
|
77
|
-
{ timeoutMs },
|
|
78
|
-
);
|
|
79
|
-
} finally {
|
|
80
|
-
if (preloadIdentifier) {
|
|
81
|
-
await page.session.call('Page.removeScriptToEvaluateOnNewDocument', {
|
|
82
|
-
identifier: preloadIdentifier,
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const controllerResult = await page.evaluate(`(async () => {
|
|
89
|
-
const submit = globalThis.stateHooks?.submitRequestToBackground;
|
|
90
|
-
const bridgeKey = Symbol.for(${JSON.stringify(bridgeSymbol)});
|
|
91
|
-
const capturedChrome = globalThis[bridgeKey];
|
|
92
|
-
let port;
|
|
93
|
-
let nextId = Date.now();
|
|
94
|
-
|
|
95
|
-
const rawSubmit = (method, params) => new Promise((resolve, reject) => {
|
|
96
|
-
if (!port) {
|
|
97
|
-
const connectionName = location.pathname.includes('sidepanel')
|
|
98
|
-
? 'sidepanel'
|
|
99
|
-
: location.pathname.includes('popup')
|
|
100
|
-
? 'popup'
|
|
101
|
-
: 'fullscreen';
|
|
102
|
-
port = capturedChrome.runtime.connect({ name: connectionName });
|
|
103
|
-
}
|
|
104
|
-
const id = nextId++;
|
|
105
|
-
const timer = setTimeout(
|
|
106
|
-
() => reject(new Error(method + ' timed out after ${timeoutMs}ms')),
|
|
107
|
-
${timeoutMs},
|
|
108
|
-
);
|
|
109
|
-
const listener = (message) => {
|
|
110
|
-
const data = message?.name === 'controller' ? message.data : null;
|
|
111
|
-
if (data?.id !== id) return;
|
|
112
|
-
clearTimeout(timer);
|
|
113
|
-
port.onMessage.removeListener(listener);
|
|
114
|
-
if (data.error) reject(new Error(JSON.stringify(data.error)));
|
|
115
|
-
else resolve(data.result);
|
|
116
|
-
};
|
|
117
|
-
port.onMessage.addListener(listener);
|
|
118
|
-
port.postMessage({
|
|
119
|
-
name: 'controller',
|
|
120
|
-
data: { jsonrpc: '2.0', id, method, params }
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
const callController = typeof submit === 'function' ? submit : rawSubmit;
|
|
125
|
-
if (typeof callController !== 'function') {
|
|
126
|
-
throw new Error('Extension consent setup could not reach the background controller.');
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const setMarketing = async (value) => {
|
|
130
|
-
await callController('setDataCollectionForMarketing', [value]);
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
try {
|
|
134
|
-
if (!${JSON.stringify(participate)}) await setMarketing(false);
|
|
135
|
-
const analyticsId = await callController(
|
|
136
|
-
'setParticipateInMetaMetrics',
|
|
137
|
-
[${JSON.stringify(participate)}],
|
|
138
|
-
);
|
|
139
|
-
if (${JSON.stringify(participate)}) {
|
|
140
|
-
await setMarketing(${JSON.stringify(marketing)});
|
|
141
|
-
}
|
|
142
|
-
return { analyticsId: analyticsId ? 'set' : null };
|
|
143
|
-
} finally {
|
|
144
|
-
port?.disconnect();
|
|
145
|
-
if (capturedChrome) delete globalThis[bridgeKey];
|
|
146
|
-
}
|
|
147
|
-
})()`, { awaitPromise: true });
|
|
148
|
-
|
|
149
|
-
const state = await waitForConsentState({
|
|
150
|
-
optedIn: participate,
|
|
151
|
-
dataCollectionForMarketing: marketing,
|
|
152
|
-
});
|
|
15
|
+
const runController = () => page.evaluate(consentControllerExpression({
|
|
16
|
+
bridgeSymbol,
|
|
17
|
+
participate,
|
|
18
|
+
marketing,
|
|
19
|
+
timeoutMs,
|
|
20
|
+
}), { awaitPromise: true });
|
|
21
|
+
const controllerResult = hasDebugBridge
|
|
22
|
+
? await runController()
|
|
23
|
+
: await withConsentChromeBridge(
|
|
24
|
+
page,
|
|
25
|
+
{ bridgeSymbol, timeoutMs },
|
|
26
|
+
runController,
|
|
27
|
+
);
|
|
28
|
+
const state = controllerResult.state;
|
|
153
29
|
|
|
154
30
|
if (state.optedIn !== participate) {
|
|
155
31
|
throw new Error(`Expected optedIn=${participate}, got ${state.optedIn}.`);
|
|
@@ -166,6 +42,6 @@ runAdapter((input) => withExtensionPage(input, async (page) => {
|
|
|
166
42
|
...state,
|
|
167
43
|
analyticsId: controllerResult.analyticsId,
|
|
168
44
|
},
|
|
169
|
-
proofPath:
|
|
45
|
+
proofPath: `extension-${controllerResult.stateSource}`,
|
|
170
46
|
};
|
|
171
47
|
}));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { pathToFileURL } from 'node:url';
|
|
2
2
|
import { dataTestId, marketSymbol, normalizeMarketSymbol, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
3
|
+
import { readPerpsRuntimeStateExpression } from './state.mjs';
|
|
3
4
|
|
|
4
5
|
function sleep(ms) {
|
|
5
6
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -704,22 +705,7 @@ async function applyStateNavigation(input, config) {
|
|
|
704
705
|
|
|
705
706
|
|
|
706
707
|
async function readPerpsRuntimeState(page) {
|
|
707
|
-
return page.evaluate(
|
|
708
|
-
const hooks = globalThis.stateHooks || {};
|
|
709
|
-
const request = hooks.submitRequestToBackground;
|
|
710
|
-
const reduxState = hooks.store?.getState?.() || {};
|
|
711
|
-
const cleanState = (await hooks.getCleanAppState?.()) || hooks.getState?.() || {};
|
|
712
|
-
const metamask = reduxState.metamask || cleanState.metamask || {};
|
|
713
|
-
const nestedPerps = metamask.PerpsController || cleanState.PerpsController || cleanState.perps || {};
|
|
714
|
-
return {
|
|
715
|
-
available: typeof request === 'function',
|
|
716
|
-
activeProvider: metamask.activeProvider || nestedPerps.activeProvider || 'hyperliquid',
|
|
717
|
-
networkKnown: typeof nestedPerps.isTestnet === 'boolean',
|
|
718
|
-
isTestnet: Boolean(nestedPerps.isTestnet),
|
|
719
|
-
initializationState: metamask.initializationState || nestedPerps.initializationState || null,
|
|
720
|
-
isEligible: Boolean(metamask.isEligible ?? nestedPerps.isEligible),
|
|
721
|
-
};
|
|
722
|
-
})()`);
|
|
708
|
+
return page.evaluate(readPerpsRuntimeStateExpression());
|
|
723
709
|
}
|
|
724
710
|
|
|
725
711
|
async function ensureProvider(input, config) {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function readPerpsRuntimeStateExpression() {
|
|
2
|
+
return `(async () => {
|
|
3
|
+
const hooks = globalThis.stateHooks || {};
|
|
4
|
+
const request = hooks.submitRequestToBackground;
|
|
5
|
+
const reduxState = hooks.store?.getState?.() || {};
|
|
6
|
+
const cleanState = (await hooks.getCleanAppState?.()) || hooks.getState?.() || {};
|
|
7
|
+
const reduxMetamask = reduxState.metamask || {};
|
|
8
|
+
const cleanMetamask = cleanState.metamask || {};
|
|
9
|
+
const metamask = { ...cleanMetamask, ...reduxMetamask };
|
|
10
|
+
const nestedPerps = reduxMetamask.PerpsController || cleanMetamask.PerpsController || cleanState.PerpsController || cleanState.perps || {};
|
|
11
|
+
return {
|
|
12
|
+
available: typeof request === 'function',
|
|
13
|
+
activeProvider: metamask.activeProvider || nestedPerps.activeProvider || 'hyperliquid',
|
|
14
|
+
networkKnown: typeof metamask.isTestnet === 'boolean' || typeof nestedPerps.isTestnet === 'boolean',
|
|
15
|
+
isTestnet: Boolean(metamask.isTestnet ?? nestedPerps.isTestnet),
|
|
16
|
+
initializationState: metamask.initializationState || nestedPerps.initializationState || null,
|
|
17
|
+
isEligible: Boolean(metamask.isEligible ?? nestedPerps.isEligible),
|
|
18
|
+
};
|
|
19
|
+
})()`;
|
|
20
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
|
+
import {
|
|
3
|
+
locatorsFromObservation,
|
|
4
|
+
requireVisibleObservation,
|
|
5
|
+
} from '../../shared/ui/locators.mjs';
|
|
6
|
+
|
|
7
|
+
runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
8
|
+
const observed = await page.observe(['ui.visible']);
|
|
9
|
+
return {
|
|
10
|
+
action: input.action,
|
|
11
|
+
...locatorsFromObservation(requireVisibleObservation(observed)),
|
|
12
|
+
};
|
|
13
|
+
}));
|
|
@@ -1,37 +1,15 @@
|
|
|
1
1
|
import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
|
+
import { listWalletAccountsExpression } from './state.mjs';
|
|
2
3
|
|
|
3
4
|
runAdapter((input) => withExtensionPage(input, async (page) => {
|
|
4
5
|
const scope = input.node?.scope == null ? 'named' : String(input.node.scope);
|
|
5
6
|
if (scope !== 'selected' && scope !== 'named' && scope !== 'evm' && scope !== 'all') {
|
|
6
7
|
throw new Error('metamask.wallet.list_accounts scope must be selected, named, evm, or all.');
|
|
7
8
|
}
|
|
8
|
-
const result = await page.evaluate(
|
|
9
|
-
const store = globalThis.stateHooks?.store;
|
|
10
|
-
if (!store || typeof store.getState !== 'function') {
|
|
11
|
-
throw new Error('stateHooks.store.getState is unavailable.');
|
|
12
|
-
}
|
|
13
|
-
const internal = store.getState()?.metamask?.internalAccounts || {};
|
|
14
|
-
const selectedId = internal.selectedAccount || null;
|
|
15
|
-
const scope = ${JSON.stringify(scope)};
|
|
16
|
-
const accounts = Object.entries(internal.accounts || {})
|
|
17
|
-
.map(([id, account]) => ({
|
|
18
|
-
id,
|
|
19
|
-
address: account?.address || null,
|
|
20
|
-
name: account?.metadata?.name || null,
|
|
21
|
-
type: account?.type || null,
|
|
22
|
-
selected: id === selectedId
|
|
23
|
-
}))
|
|
24
|
-
.filter((account) => {
|
|
25
|
-
if (scope === 'selected') return account.selected;
|
|
26
|
-
if (scope === 'all') return true;
|
|
27
|
-
if (scope === 'evm') return /^0x[0-9a-f]{40}$/iu.test(account.address || '');
|
|
28
|
-
return account.selected || Boolean(String(account.name || '').trim());
|
|
29
|
-
});
|
|
30
|
-
return { accounts };
|
|
31
|
-
})()`);
|
|
9
|
+
const result = await page.evaluate(listWalletAccountsExpression(scope), { awaitPromise: true });
|
|
32
10
|
return {
|
|
33
11
|
action: input.action,
|
|
34
|
-
source: 'extension-stateHooks-
|
|
12
|
+
source: 'extension-stateHooks-clean-state',
|
|
35
13
|
scope,
|
|
36
14
|
...result,
|
|
37
15
|
count: result.accounts.length,
|
|
@@ -1,27 +1,7 @@
|
|
|
1
1
|
import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
|
+
import { readWalletStateExpression } from './state.mjs';
|
|
2
3
|
|
|
3
4
|
runAdapter((input) => withExtensionPage(input, async (page) => {
|
|
4
|
-
const state = await page.evaluate(
|
|
5
|
-
|
|
6
|
-
const store = hooks && hooks.store;
|
|
7
|
-
const root = store && typeof store.getState === 'function' ? store.getState() : {};
|
|
8
|
-
const metamask = root.metamask || {};
|
|
9
|
-
const internal = metamask.internalAccounts || {};
|
|
10
|
-
const accounts = internal.accounts || {};
|
|
11
|
-
const selectedId = internal.selectedAccount || null;
|
|
12
|
-
const selected = selectedId && accounts[selectedId] ? accounts[selectedId] : null;
|
|
13
|
-
const metadata = selected && selected.metadata ? selected.metadata : {};
|
|
14
|
-
return {
|
|
15
|
-
href: String(globalThis.location && globalThis.location.href || ''),
|
|
16
|
-
selectedAccount: selected ? {
|
|
17
|
-
id: selectedId,
|
|
18
|
-
address: selected.address || null,
|
|
19
|
-
name: metadata.name || null,
|
|
20
|
-
type: selected.type || null
|
|
21
|
-
} : null,
|
|
22
|
-
completedOnboarding: Boolean(metamask.completedOnboarding),
|
|
23
|
-
selectedNetworkClientId: metamask.selectedNetworkClientId || null
|
|
24
|
-
};
|
|
25
|
-
})()`);
|
|
26
|
-
return { action: input.action, source: 'extension-stateHooks-store', state, redacted: true, proofPath: 'extension-wallet-state' };
|
|
5
|
+
const state = await page.evaluate(readWalletStateExpression(), { awaitPromise: true });
|
|
6
|
+
return { action: input.action, source: 'extension-stateHooks-clean-state', state, redacted: true, proofPath: 'extension-wallet-state' };
|
|
27
7
|
}));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
|
+
import { selectWalletAccountExpression } from './state.mjs';
|
|
2
3
|
|
|
3
4
|
function normalize(value) {
|
|
4
5
|
return String(value ?? '').toLowerCase();
|
|
@@ -11,38 +12,10 @@ runAdapter((input) => withExtensionPage(input, async (page) => {
|
|
|
11
12
|
if (!requestedAddress && !requestedId && !requestedName) {
|
|
12
13
|
throw new Error('metamask.wallet.select_account extension live adapter requires node.address, node.id, or node.name.');
|
|
13
14
|
}
|
|
14
|
-
const selected = await page.evaluate(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (typeof request !== 'function') throw new Error('stateHooks.submitRequestToBackground is unavailable.');
|
|
20
|
-
const state = store.getState() || {};
|
|
21
|
-
const internal = state.metamask?.internalAccounts || {};
|
|
22
|
-
const accounts = internal.accounts || {};
|
|
23
|
-
const requestedAddress = ${JSON.stringify(requestedAddress)};
|
|
24
|
-
const requestedId = ${JSON.stringify(requestedId)};
|
|
25
|
-
const requestedName = ${JSON.stringify(requestedName)};
|
|
26
|
-
const entries = Object.entries(accounts);
|
|
27
|
-
const match = entries.find(([id, account]) => {
|
|
28
|
-
const metadata = account?.metadata || {};
|
|
29
|
-
return (requestedId && id === requestedId) ||
|
|
30
|
-
(requestedAddress && String(account?.address || '').toLowerCase() === requestedAddress) ||
|
|
31
|
-
(requestedName && metadata.name === requestedName);
|
|
32
|
-
});
|
|
33
|
-
if (!match) throw new Error('Requested account was not found in internalAccounts.');
|
|
34
|
-
const [id, account] = match;
|
|
35
|
-
await request('setSelectedInternalAccount', [id]);
|
|
36
|
-
const after = store.getState()?.metamask?.internalAccounts || {};
|
|
37
|
-
const selectedId = after.selectedAccount;
|
|
38
|
-
const selected = after.accounts?.[selectedId];
|
|
39
|
-
if (selectedId !== id) throw new Error('Expected selected account ' + id + ', got ' + selectedId);
|
|
40
|
-
return {
|
|
41
|
-
id: selectedId,
|
|
42
|
-
address: selected?.address || null,
|
|
43
|
-
name: selected?.metadata?.name || null,
|
|
44
|
-
type: selected?.type || null
|
|
45
|
-
};
|
|
46
|
-
})()`, { awaitPromise: true });
|
|
15
|
+
const selected = await page.evaluate(selectWalletAccountExpression({
|
|
16
|
+
address: requestedAddress,
|
|
17
|
+
id: requestedId,
|
|
18
|
+
name: requestedName,
|
|
19
|
+
}), { awaitPromise: true });
|
|
47
20
|
return { action: input.action, selected, proofPath: 'extension-account-selection' };
|
|
48
21
|
}));
|