@deeeed/metamask-harness 0.82.0 → 0.83.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/adapters/core/cleanup.sh +5 -29
- package/adapters/core/controller-import-probe.mjs +26 -0
- package/adapters/manifest.json +16 -0
- package/adapters/web-dapp/lib/policy-fence.mjs +119 -0
- package/adapters/web-dapp/lib/policy.mjs +7 -2
- package/dist/adapters/core/actions.js +0 -3
- package/dist/adapters/core/overlay.js +1 -5
- package/dist/adapters/core/readiness.js +133 -2
- package/dist/adapters/core/surface.js +53 -41
- package/dist/adapters/extension/extension-id.js +1 -0
- package/dist/adapters/extension/fixtures.js +60 -1
- package/dist/adapters/extension/network-observer.js +19 -206
- package/dist/adapters/extension/run.js +1 -1
- package/dist/adapters/extension/runtime.js +42 -3
- package/dist/adapters/mobile/android-gfxinfo-performance.js +3 -1
- package/dist/adapters/mobile/performance-observer.js +9 -6
- package/dist/adapters/performance/trace-kinds.js +36 -0
- package/dist/adapters/plugin-surface.js +29 -1
- package/dist/adapters/web-dapp/run.js +6 -1
- package/dist/command-contract.js +34 -0
- package/dist/commands/check.js +3 -2
- package/dist/commands/core-readiness.js +1 -9
- package/dist/performance-observation.js +28 -5
- package/library/actions/core/perps/_controller.mjs +10 -0
- package/library/actions/extension/wallet/import.mjs +21 -4
- package/library/actions/extension/wallet/visible-session.mjs +32 -12
- package/package.json +7 -6
- package/dist/adapters/core/live-script-env.js +0 -36
- package/dist/adapters/extension/browser-cdp.js +0 -174
- package/dist/adapters/extension/performance-observer.js +0 -75
- package/dist/adapters/performance/cdp-trace.js +0 -406
- package/dist/adapters/performance/js-task-metrics.js +0 -35
- package/dist/async.js +0 -19
|
@@ -47,13 +47,6 @@ function coreDependencyBlock(target) {
|
|
|
47
47
|
}
|
|
48
48
|
return null;
|
|
49
49
|
}
|
|
50
|
-
function corePnpNodeOptions(target, current) {
|
|
51
|
-
if (readYarnNodeLinker(target) !== "pnp") return current;
|
|
52
|
-
const pnp = path.join(path.resolve(target), ".pnp.cjs");
|
|
53
|
-
if (!fs.existsSync(pnp)) return current;
|
|
54
|
-
const pnpOption = `--require ${pnp}`;
|
|
55
|
-
return current ? `${current} ${pnpOption}` : pnpOption;
|
|
56
|
-
}
|
|
57
50
|
function canResolveFromTarget(specifier, target) {
|
|
58
51
|
try {
|
|
59
52
|
requireFromHarness.resolve(specifier, { paths: [target] });
|
|
@@ -75,6 +68,5 @@ function shellQuote(value) {
|
|
|
75
68
|
return `'${value.replace(/'/gu, `'\\''`)}'`;
|
|
76
69
|
}
|
|
77
70
|
export {
|
|
78
|
-
coreDependencyBlock
|
|
79
|
-
corePnpNodeOptions
|
|
71
|
+
coreDependencyBlock
|
|
80
72
|
};
|
|
@@ -1,11 +1,17 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import { promisify } from "node:util";
|
|
3
4
|
import { gzip } from "node:zlib";
|
|
4
5
|
import { getAdapterSurface } from "./adapters/surface.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
import { resolveExtensionId } from "./adapters/extension/extension-id.js";
|
|
7
|
+
import {
|
|
8
|
+
TRACE_KINDS,
|
|
9
|
+
TRACE_MARKER_PREFIX
|
|
10
|
+
} from "./adapters/performance/trace-kinds.js";
|
|
11
|
+
import {
|
|
12
|
+
summarizeFrames,
|
|
13
|
+
summarizeJavaScriptTasks
|
|
14
|
+
} from "@farmslot/recipe-runner/runtime/cdp-trace";
|
|
9
15
|
import {
|
|
10
16
|
indexArtifactManifest,
|
|
11
17
|
readContainedJsonArtifact,
|
|
@@ -512,7 +518,24 @@ function unavailableCapture(session, window, reason) {
|
|
|
512
518
|
);
|
|
513
519
|
}
|
|
514
520
|
async function createExtensionPerformanceObserver(target, env) {
|
|
515
|
-
|
|
521
|
+
const cdpPort = extensionCdpPort(env);
|
|
522
|
+
const extensionId = (await resolveExtensionId(target, { cdpPort })).extensionId;
|
|
523
|
+
if (!extensionId) {
|
|
524
|
+
throw new Error("Extension performance observation could not resolve the MetaMask extension id.");
|
|
525
|
+
}
|
|
526
|
+
const { createExtensionPerformanceBackend } = createRequire(import.meta.url)(
|
|
527
|
+
"@farmslot/adapter-web/performance-observer"
|
|
528
|
+
);
|
|
529
|
+
return createExtensionPerformanceBackend({
|
|
530
|
+
cdpPort,
|
|
531
|
+
extensionId,
|
|
532
|
+
uiPaths: ["/home.html", "/sidepanel.html"],
|
|
533
|
+
kind: TRACE_KINDS.extension,
|
|
534
|
+
platform: "extension",
|
|
535
|
+
markerPrefix: TRACE_MARKER_PREFIX,
|
|
536
|
+
connectTimeoutMs: 1e4,
|
|
537
|
+
commandTimeoutMs: 1e4
|
|
538
|
+
});
|
|
516
539
|
}
|
|
517
540
|
function extensionCdpPort(env) {
|
|
518
541
|
const port = Number(env.CDP_PORT ?? env.RECIPE_CDP_PORT);
|
|
@@ -904,6 +904,16 @@ export async function runAdapter(callback) {
|
|
|
904
904
|
// Surface the failure; still tear down so a half-open WebSocket from a
|
|
905
905
|
// failed write doesn't leave the process wedged open.
|
|
906
906
|
process.stderr.write(`[core/perps] adapter failed: ${fmtError(error)}\n`);
|
|
907
|
+
// A module the checkout cannot resolve is its environment (a partial
|
|
908
|
+
// workspace build), not app logic: say so on its own line, with the
|
|
909
|
+
// build step, for the runner's environment classification to match.
|
|
910
|
+
if (error?.code === 'ERR_MODULE_NOT_FOUND' || error?.code === 'MODULE_NOT_FOUND') {
|
|
911
|
+
const root = input.context?.projectRoot ?? process.cwd();
|
|
912
|
+
const missing = error.url ?? String(error.message ?? '').split('\n')[0];
|
|
913
|
+
process.stderr.write(
|
|
914
|
+
`[core/perps] environment: cannot resolve ${missing}\n Next: cd '${root.replace(/'/gu, `'\\''`)}' && yarn build\n`,
|
|
915
|
+
);
|
|
916
|
+
}
|
|
907
917
|
}
|
|
908
918
|
} finally {
|
|
909
919
|
await disconnectController(exitCode);
|
|
@@ -100,6 +100,23 @@ async function unlockProfile(page, credentials, timeoutMs) {
|
|
|
100
100
|
await waitForSelectorToDisappear(page, SELECTORS.unlockPassword, timeoutMs);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
// A locked profile is unlocked before the account proof. A fresh profile can
|
|
104
|
+
// also lock right after onboarding while the sidepanel attaches, stranding the
|
|
105
|
+
// proof on #/unlock: that relock is recovered once with the fixture password and
|
|
106
|
+
// reported as relockRecovered; a further lock fails with the route error.
|
|
107
|
+
async function ensureUnlockedSelectedAccount(page, credentials, timeoutMs) {
|
|
108
|
+
if (await hasVisibleSelector(page, SELECTORS.unlockPassword)) await unlockProfile(page, credentials, timeoutMs);
|
|
109
|
+
try {
|
|
110
|
+
const visibleSession = await ensureVisibleSelectedAccount(page, credentials.expectedAddress, timeoutMs);
|
|
111
|
+
return { ...visibleSession, relockRecovered: false };
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (!(await hasVisibleSelector(page, SELECTORS.unlockPassword))) throw error;
|
|
114
|
+
await unlockProfile(page, credentials, timeoutMs);
|
|
115
|
+
const visibleSession = await ensureVisibleSelectedAccount(page, credentials.expectedAddress, timeoutMs);
|
|
116
|
+
return { ...visibleSession, relockRecovered: true };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
103
120
|
async function importThroughUi(page, input, timeoutMs) {
|
|
104
121
|
const credentials = await resolveWalletImportCredentials(input);
|
|
105
122
|
const metametricsEnabled = input.node?.metametrics === true || input.node?.metametrics === 'true';
|
|
@@ -149,7 +166,7 @@ async function importThroughUi(page, input, timeoutMs) {
|
|
|
149
166
|
await waitForSelector(page, SELECTORS.complete, timeoutMs);
|
|
150
167
|
await page.click(SELECTORS.complete);
|
|
151
168
|
|
|
152
|
-
const visibleSession = await
|
|
169
|
+
const visibleSession = await ensureUnlockedSelectedAccount(page, credentials, timeoutMs);
|
|
153
170
|
return { credentials, visibleSession, metametricsEnabled };
|
|
154
171
|
}
|
|
155
172
|
|
|
@@ -168,9 +185,7 @@ runAdapter((input) => withExtensionPage(input, async (page) => {
|
|
|
168
185
|
if (!onboarding) {
|
|
169
186
|
const credentials = await resolveWalletImportCredentials(input);
|
|
170
187
|
const completionTriggered = await dismissVisibleOnboardingCompletion(page);
|
|
171
|
-
const
|
|
172
|
-
if (locked) await unlockProfile(page, credentials, timeoutMs);
|
|
173
|
-
const visibleSession = await ensureVisibleSelectedAccount(page, credentials.expectedAddress, timeoutMs);
|
|
188
|
+
const visibleSession = await ensureUnlockedSelectedAccount(page, credentials, timeoutMs);
|
|
174
189
|
return {
|
|
175
190
|
action: input.action,
|
|
176
191
|
method: 'ui-visible-session',
|
|
@@ -191,6 +206,7 @@ runAdapter((input) => withExtensionPage(input, async (page) => {
|
|
|
191
206
|
selectedIdentitySha256: imported.visibleSession.selectedIdentitySha256,
|
|
192
207
|
selectedAddressMatched: imported.visibleSession.selectedAddressMatched,
|
|
193
208
|
selectionPerformed: imported.visibleSession.selectionPerformed,
|
|
209
|
+
relockRecovered: imported.visibleSession.relockRecovered,
|
|
194
210
|
completedOnboarding: true,
|
|
195
211
|
metametricsEnabled: imported.metametricsEnabled,
|
|
196
212
|
interests: [],
|
|
@@ -213,6 +229,7 @@ runAdapter((input) => withExtensionPage(input, async (page) => {
|
|
|
213
229
|
selectedIdentitySha256: imported.visibleSession.selectedIdentitySha256,
|
|
214
230
|
selectedAddressMatched: imported.visibleSession.selectedAddressMatched,
|
|
215
231
|
selectionPerformed: imported.visibleSession.selectionPerformed,
|
|
232
|
+
relockRecovered: imported.visibleSession.relockRecovered,
|
|
216
233
|
completedOnboarding: true,
|
|
217
234
|
metametricsEnabled: imported.metametricsEnabled,
|
|
218
235
|
interests: [],
|
|
@@ -44,7 +44,7 @@ async function currentRoute(page, deadline) {
|
|
|
44
44
|
return route.replace(/0x[0-9a-f]{40}/giu, '<redacted-address>').slice(0, 200);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
async function routeError(page, message, deadline) {
|
|
47
|
+
async function routeError(page, message, deadline, cause) {
|
|
48
48
|
let route = '<deadline-expired>';
|
|
49
49
|
if (Date.now() < deadline) {
|
|
50
50
|
try {
|
|
@@ -53,7 +53,35 @@ async function routeError(page, message, deadline) {
|
|
|
53
53
|
route = '<unavailable>';
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
-
|
|
56
|
+
const detail = cause
|
|
57
|
+
? `; last input error: ${String(cause.message ?? cause).replace(/0x[0-9a-f]{40}/giu, '<redacted-address>').slice(0, 200)}`
|
|
58
|
+
: '';
|
|
59
|
+
return new Error(`${message} (route ${JSON.stringify(route)})${detail}.`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Entry can fail transiently while the account list settles after onboarding,
|
|
63
|
+
// so it is retried in place until the shared deadline; leaving the list (a
|
|
64
|
+
// lock, a redirect) fails at once with the last input error. Callers still
|
|
65
|
+
// assert the rendered result, so a retry can never pass a wrong account.
|
|
66
|
+
async function enterAccountSearch(page, value, deadline, message) {
|
|
67
|
+
let lastError;
|
|
68
|
+
while (Date.now() <= deadline) {
|
|
69
|
+
try {
|
|
70
|
+
await withinDeadline(page.setInput(ACCOUNT_SEARCH, value), deadline);
|
|
71
|
+
return;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
lastError = error;
|
|
74
|
+
}
|
|
75
|
+
let route;
|
|
76
|
+
try {
|
|
77
|
+
route = await currentRoute(page, deadline);
|
|
78
|
+
} catch {
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
if (route !== '#/account-list') break;
|
|
82
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
83
|
+
}
|
|
84
|
+
throw await routeError(page, message, deadline, lastError);
|
|
57
85
|
}
|
|
58
86
|
|
|
59
87
|
async function visibleAccountMatch(page, deadline) {
|
|
@@ -140,11 +168,7 @@ async function openAccountList(page, deadline) {
|
|
|
140
168
|
|
|
141
169
|
async function proveAddressSearchControl(page, expectedAddress, deadline) {
|
|
142
170
|
const probeAddress = accountProbeAddress(expectedAddress);
|
|
143
|
-
|
|
144
|
-
await withinDeadline(page.setInput(ACCOUNT_SEARCH, probeAddress), deadline);
|
|
145
|
-
} catch {
|
|
146
|
-
throw await routeError(page, 'Visible wallet account search could not enter the alternate-address control', deadline);
|
|
147
|
-
}
|
|
171
|
+
await enterAccountSearch(page, probeAddress, deadline, 'Visible wallet account search could not enter the alternate-address control');
|
|
148
172
|
let consecutiveEmpty = 0;
|
|
149
173
|
while (Date.now() <= deadline) {
|
|
150
174
|
let observed;
|
|
@@ -166,11 +190,7 @@ async function proveAddressSearchControl(page, expectedAddress, deadline) {
|
|
|
166
190
|
|
|
167
191
|
async function queryExpectedAccount(page, expectedAddress, deadline, requireSelected) {
|
|
168
192
|
await proveAddressSearchControl(page, expectedAddress, deadline);
|
|
169
|
-
|
|
170
|
-
await withinDeadline(page.setInput(ACCOUNT_SEARCH, expectedAddress), deadline);
|
|
171
|
-
} catch {
|
|
172
|
-
throw await routeError(page, 'Visible wallet account search could not enter the derived release identity', deadline);
|
|
173
|
-
}
|
|
193
|
+
await enterAccountSearch(page, expectedAddress, deadline, 'Visible wallet account search could not enter the derived release identity');
|
|
174
194
|
let observed;
|
|
175
195
|
while (Date.now() <= deadline) {
|
|
176
196
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deeeed/metamask-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.83.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mm-harness": "bin/mm-harness"
|
|
@@ -21,14 +21,15 @@
|
|
|
21
21
|
"check:syntax": "find . -name '*.mjs' -print0 | xargs -0 -n1 node --check"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@farmslot/adapter-
|
|
25
|
-
"@farmslot/adapter-
|
|
26
|
-
"@farmslot/adapter-
|
|
24
|
+
"@farmslot/adapter-node": "0.1.2",
|
|
25
|
+
"@farmslot/adapter-rn": "0.18.0",
|
|
26
|
+
"@farmslot/adapter-sdk": "0.6.0",
|
|
27
|
+
"@farmslot/adapter-web": "0.4.1",
|
|
27
28
|
"@farmslot/agent-runtime": "0.17.0",
|
|
28
29
|
"@farmslot/handoff": "^0.3.1",
|
|
29
30
|
"@farmslot/protocol": "0.34.0",
|
|
30
|
-
"@farmslot/recipe-cli": "0.
|
|
31
|
-
"@farmslot/recipe-runner": "0.
|
|
31
|
+
"@farmslot/recipe-cli": "0.8.0",
|
|
32
|
+
"@farmslot/recipe-runner": "0.26.0",
|
|
32
33
|
"agent-device": "0.19.3",
|
|
33
34
|
"commander": "^12.0.0",
|
|
34
35
|
"esbuild": "0.28.1",
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { writeFile } from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { corePnpNodeOptions } from "../../commands/core-readiness.js";
|
|
5
|
-
const CORE_WORKSPACE_PACKAGES = [
|
|
6
|
-
"@metamask/base-controller",
|
|
7
|
-
"@metamask/messenger",
|
|
8
|
-
"@metamask/controller-utils",
|
|
9
|
-
"@metamask/keyring-controller"
|
|
10
|
-
];
|
|
11
|
-
async function coreLiveScriptEnv(projectRoot, tempDir) {
|
|
12
|
-
const paths = {};
|
|
13
|
-
for (const pkg of CORE_WORKSPACE_PACKAGES) {
|
|
14
|
-
const packageDir = path.join(projectRoot, "packages", pkg.replace("@metamask/", ""));
|
|
15
|
-
const distEntry = path.join(packageDir, "dist/index.cjs");
|
|
16
|
-
if (existsSync(distEntry)) continue;
|
|
17
|
-
const srcDir = path.join(packageDir, "src");
|
|
18
|
-
if (!existsSync(path.join(srcDir, "index.ts"))) continue;
|
|
19
|
-
paths[pkg] = [path.join(srcDir, "index.ts")];
|
|
20
|
-
paths[`${pkg}/*`] = [path.join(srcDir, "*")];
|
|
21
|
-
}
|
|
22
|
-
if (Object.keys(paths).length === 0) return {};
|
|
23
|
-
const tsconfigPath = path.join(tempDir, "core-adapter.tsconfig.json");
|
|
24
|
-
await writeFile(
|
|
25
|
-
tsconfigPath,
|
|
26
|
-
`${JSON.stringify({ compilerOptions: { baseUrl: projectRoot, paths } }, null, 2)}
|
|
27
|
-
`
|
|
28
|
-
);
|
|
29
|
-
return {
|
|
30
|
-
TSX_TSCONFIG_PATH: tsconfigPath,
|
|
31
|
-
NODE_OPTIONS: corePnpNodeOptions(projectRoot, process.env.NODE_OPTIONS)
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
export {
|
|
35
|
-
coreLiveScriptEnv
|
|
36
|
-
};
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
import { WebSocket } from "ws";
|
|
2
|
-
import { withTimeout } from "../../async.js";
|
|
3
|
-
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
4
|
-
async function createBrowserCdpClient(cdpPort) {
|
|
5
|
-
if (!Number.isInteger(cdpPort) || cdpPort <= 0) {
|
|
6
|
-
throw new Error("Extension CDP requires a valid port.");
|
|
7
|
-
}
|
|
8
|
-
const version = asRecord(
|
|
9
|
-
await fetchJson(`http://127.0.0.1:${cdpPort}/json/version`)
|
|
10
|
-
);
|
|
11
|
-
const webSocketUrl = String(version.webSocketDebuggerUrl ?? "");
|
|
12
|
-
if (!webSocketUrl) {
|
|
13
|
-
throw new Error("Extension browser CDP WebSocket is unavailable.");
|
|
14
|
-
}
|
|
15
|
-
const socket = new WebSocket(webSocketUrl);
|
|
16
|
-
await waitForOpen(socket);
|
|
17
|
-
let nextId = 0;
|
|
18
|
-
const pending = /* @__PURE__ */ new Map();
|
|
19
|
-
const handlers = /* @__PURE__ */ new Set();
|
|
20
|
-
const closeHandlers = /* @__PURE__ */ new Set();
|
|
21
|
-
socket.on("message", (data) => {
|
|
22
|
-
let message;
|
|
23
|
-
try {
|
|
24
|
-
message = JSON.parse(String(data));
|
|
25
|
-
} catch {
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
if (message.id && pending.has(message.id)) {
|
|
29
|
-
const command = pending.get(message.id);
|
|
30
|
-
pending.delete(message.id);
|
|
31
|
-
if (!command) return;
|
|
32
|
-
clearTimeout(command.timer);
|
|
33
|
-
if (message.error) {
|
|
34
|
-
command.reject(
|
|
35
|
-
new Error(
|
|
36
|
-
String(message.error.message ?? "Extension CDP command failed")
|
|
37
|
-
)
|
|
38
|
-
);
|
|
39
|
-
} else {
|
|
40
|
-
command.resolve(message.result ?? {});
|
|
41
|
-
}
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
for (const handler of handlers) handler(message);
|
|
45
|
-
});
|
|
46
|
-
socket.on("close", () => {
|
|
47
|
-
for (const command of pending.values()) {
|
|
48
|
-
clearTimeout(command.timer);
|
|
49
|
-
command.reject(new Error("Extension browser CDP connection closed"));
|
|
50
|
-
}
|
|
51
|
-
pending.clear();
|
|
52
|
-
handlers.clear();
|
|
53
|
-
for (const handler of closeHandlers) handler();
|
|
54
|
-
closeHandlers.clear();
|
|
55
|
-
});
|
|
56
|
-
return {
|
|
57
|
-
send(method, params = {}, sessionId, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
58
|
-
return new Promise((resolve, reject) => {
|
|
59
|
-
const id = ++nextId;
|
|
60
|
-
const timer = setTimeout(() => {
|
|
61
|
-
pending.delete(id);
|
|
62
|
-
reject(new Error(`Extension CDP command timed out: ${method}`));
|
|
63
|
-
}, timeoutMs);
|
|
64
|
-
pending.set(id, { resolve, reject, timer });
|
|
65
|
-
socket.send(
|
|
66
|
-
JSON.stringify({
|
|
67
|
-
id,
|
|
68
|
-
method,
|
|
69
|
-
params,
|
|
70
|
-
...sessionId ? { sessionId } : {}
|
|
71
|
-
})
|
|
72
|
-
);
|
|
73
|
-
});
|
|
74
|
-
},
|
|
75
|
-
onMessage(handler) {
|
|
76
|
-
handlers.add(handler);
|
|
77
|
-
return () => handlers.delete(handler);
|
|
78
|
-
},
|
|
79
|
-
onClose(handler) {
|
|
80
|
-
closeHandlers.add(handler);
|
|
81
|
-
return () => closeHandlers.delete(handler);
|
|
82
|
-
},
|
|
83
|
-
close() {
|
|
84
|
-
socket.close();
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
async function fetchCdpJson(cdpPort, path = "/json") {
|
|
89
|
-
return fetchJson(`http://127.0.0.1:${cdpPort}${path}`);
|
|
90
|
-
}
|
|
91
|
-
function asBrowserCdpTarget(value) {
|
|
92
|
-
const target = asRecord(value);
|
|
93
|
-
const targetId = String(target.targetId ?? target.id ?? "");
|
|
94
|
-
const type = String(target.type ?? "");
|
|
95
|
-
const url = String(target.url ?? "");
|
|
96
|
-
return targetId && type && url ? { targetId, type, url } : null;
|
|
97
|
-
}
|
|
98
|
-
function extensionIdFromCdpTargets(value) {
|
|
99
|
-
const targets = Array.isArray(value) ? value : [];
|
|
100
|
-
for (const candidate of targets) {
|
|
101
|
-
const target = asBrowserCdpTarget(candidate);
|
|
102
|
-
if (!target) continue;
|
|
103
|
-
try {
|
|
104
|
-
const url = new URL(target.url);
|
|
105
|
-
if (url.protocol === "chrome-extension:" && url.hostname) {
|
|
106
|
-
return url.hostname;
|
|
107
|
-
}
|
|
108
|
-
} catch {
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
function selectExtensionUiTarget(value, extensionId) {
|
|
115
|
-
const targets = Array.isArray(value) ? value : [];
|
|
116
|
-
const candidates = targets.map(asBrowserCdpTarget).filter((candidate) => {
|
|
117
|
-
if (!candidate || !["page", "other"].includes(candidate.type)) return false;
|
|
118
|
-
try {
|
|
119
|
-
const url = new URL(candidate.url);
|
|
120
|
-
return url.protocol === "chrome-extension:" && url.hostname === extensionId;
|
|
121
|
-
} catch {
|
|
122
|
-
return false;
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
const matchingPath = (target, pathname) => {
|
|
126
|
-
try {
|
|
127
|
-
return new URL(target.url).pathname === pathname;
|
|
128
|
-
} catch {
|
|
129
|
-
return false;
|
|
130
|
-
}
|
|
131
|
-
};
|
|
132
|
-
const home = candidates.filter((target) => matchingPath(target, "/home.html"));
|
|
133
|
-
if (home.length > 0) return home.length === 1 ? home[0] : null;
|
|
134
|
-
const sidepanel = candidates.filter(
|
|
135
|
-
(target) => matchingPath(target, "/sidepanel.html")
|
|
136
|
-
);
|
|
137
|
-
return sidepanel.length === 1 ? sidepanel[0] : null;
|
|
138
|
-
}
|
|
139
|
-
async function fetchJson(url) {
|
|
140
|
-
const response = await fetch(url);
|
|
141
|
-
if (!response.ok) {
|
|
142
|
-
throw new Error(`Extension CDP returned HTTP ${response.status}.`);
|
|
143
|
-
}
|
|
144
|
-
return response.json();
|
|
145
|
-
}
|
|
146
|
-
function waitForOpen(socket) {
|
|
147
|
-
const onOpen = (resolve) => () => resolve();
|
|
148
|
-
let openHandler;
|
|
149
|
-
let errorHandler;
|
|
150
|
-
const opened = new Promise((resolve, reject) => {
|
|
151
|
-
openHandler = onOpen(resolve);
|
|
152
|
-
errorHandler = reject;
|
|
153
|
-
socket.once("open", openHandler);
|
|
154
|
-
socket.once("error", errorHandler);
|
|
155
|
-
});
|
|
156
|
-
return withTimeout(opened, {
|
|
157
|
-
message: "Extension browser CDP connection timed out.",
|
|
158
|
-
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
159
|
-
onTimeout: () => socket.close()
|
|
160
|
-
}).finally(() => {
|
|
161
|
-
if (openHandler) socket.off("open", openHandler);
|
|
162
|
-
if (errorHandler) socket.off("error", errorHandler);
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
function asRecord(value) {
|
|
166
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
167
|
-
}
|
|
168
|
-
export {
|
|
169
|
-
asBrowserCdpTarget,
|
|
170
|
-
createBrowserCdpClient,
|
|
171
|
-
extensionIdFromCdpTargets,
|
|
172
|
-
fetchCdpJson,
|
|
173
|
-
selectExtensionUiTarget
|
|
174
|
-
};
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createCdpTraceCollector
|
|
3
|
-
} from "../performance/cdp-trace.js";
|
|
4
|
-
import {
|
|
5
|
-
createBrowserCdpClient,
|
|
6
|
-
fetchCdpJson,
|
|
7
|
-
selectExtensionUiTarget
|
|
8
|
-
} from "./browser-cdp.js";
|
|
9
|
-
import { resolveExtensionId } from "./extension-id.js";
|
|
10
|
-
async function createExtensionPerformanceBackend(cdpPort, target) {
|
|
11
|
-
const extensionId = (await resolveExtensionId(target, { cdpPort })).extensionId;
|
|
12
|
-
if (!extensionId) {
|
|
13
|
-
throw new Error("Extension performance observation could not resolve the MetaMask extension id.");
|
|
14
|
-
}
|
|
15
|
-
const uiTarget = selectExtensionUiTarget(
|
|
16
|
-
await fetchCdpJson(cdpPort),
|
|
17
|
-
extensionId
|
|
18
|
-
);
|
|
19
|
-
if (!uiTarget) {
|
|
20
|
-
throw new Error("Extension performance observation requires an open MetaMask UI target.");
|
|
21
|
-
}
|
|
22
|
-
const browser = await createBrowserCdpClient(cdpPort);
|
|
23
|
-
const attached = asRecord(
|
|
24
|
-
await browser.send(
|
|
25
|
-
"Target.attachToTarget",
|
|
26
|
-
{ targetId: uiTarget.targetId, flatten: true },
|
|
27
|
-
void 0,
|
|
28
|
-
1e4
|
|
29
|
-
)
|
|
30
|
-
);
|
|
31
|
-
const sessionId = String(attached.sessionId ?? "");
|
|
32
|
-
if (!sessionId) {
|
|
33
|
-
browser.close();
|
|
34
|
-
throw new Error("Extension performance observation could not attach to the MetaMask UI target.");
|
|
35
|
-
}
|
|
36
|
-
const client = browserTraceClient(browser);
|
|
37
|
-
return createCdpTraceCollector(client, "extension", async (name) => {
|
|
38
|
-
const before = Date.now();
|
|
39
|
-
await browser.send(
|
|
40
|
-
"Runtime.evaluate",
|
|
41
|
-
{
|
|
42
|
-
expression: `performance.mark(${JSON.stringify(name)})`,
|
|
43
|
-
returnByValue: true
|
|
44
|
-
},
|
|
45
|
-
sessionId,
|
|
46
|
-
1e4
|
|
47
|
-
);
|
|
48
|
-
const after = Date.now();
|
|
49
|
-
return {
|
|
50
|
-
hostEpochMs: (before + after) / 2,
|
|
51
|
-
uncertaintyMs: (after - before) / 2
|
|
52
|
-
};
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
function browserTraceClient(browser) {
|
|
56
|
-
return {
|
|
57
|
-
send(method, params = {}, timeoutMs) {
|
|
58
|
-
return browser.send(method, params, void 0, timeoutMs);
|
|
59
|
-
},
|
|
60
|
-
on(method, handler) {
|
|
61
|
-
return browser.onMessage((message) => {
|
|
62
|
-
if (message.method === method) handler(message.params ?? {});
|
|
63
|
-
});
|
|
64
|
-
},
|
|
65
|
-
close() {
|
|
66
|
-
browser.close();
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
function asRecord(value) {
|
|
71
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
72
|
-
}
|
|
73
|
-
export {
|
|
74
|
-
createExtensionPerformanceBackend
|
|
75
|
-
};
|