@walletwright/core 0.2.0 → 0.2.1
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 +10 -0
- package/dist/{cache-Bqz3Mchl.mjs → cache-Bwv2i-hv.mjs} +18 -18
- package/dist/cache-Bwv2i-hv.mjs.map +1 -0
- package/dist/chain.mjs.map +1 -1
- package/dist/cli.mjs +2 -2
- package/dist/cli.mjs.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/dist/mock-standard.mjs +1 -1
- package/dist/mock-standard.mjs.map +1 -1
- package/dist/mock.mjs +3 -3
- package/dist/mock.mjs.map +1 -1
- package/package.json +8 -8
- package/dist/cache-Bqz3Mchl.mjs.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @walletwright/core
|
|
2
2
|
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 159f35d: Move the MetaMask onboarding patch to classic-level 3. A missing key no longer rejects with
|
|
8
|
+
`LEVEL_NOT_FOUND`; it resolves `undefined`, so the reads that probed for the `OnboardingController`
|
|
9
|
+
and `data` keys no longer swallow every error with a `.catch()`. A read that fails for a real reason
|
|
10
|
+
(a corrupt or locked profile database) now surfaces instead of being reported as "MetaMask persisted
|
|
11
|
+
no onboarding state". Both persisted state shapes and both failure paths are covered by tests.
|
|
12
|
+
|
|
3
13
|
## 0.2.0
|
|
4
14
|
|
|
5
15
|
### Minor Changes
|
|
@@ -473,15 +473,15 @@ const markMetaMaskOnboarded = async (profileDir, extensionId) => {
|
|
|
473
473
|
});
|
|
474
474
|
try {
|
|
475
475
|
await db.open();
|
|
476
|
-
const perController = await db.get("OnboardingController")
|
|
477
|
-
if (perController !==
|
|
476
|
+
const perController = await db.get("OnboardingController");
|
|
477
|
+
if (perController !== void 0 && perController !== "") {
|
|
478
478
|
const onboarding = asState(JSON.parse(perController));
|
|
479
479
|
if (onboarding === void 0) throw new Error(`[walletwright] MetaMask's OnboardingController state is not an object`);
|
|
480
480
|
if (onboarding.completedOnboarding !== true) await db.put("OnboardingController", JSON.stringify(markOnboarded(onboarding)));
|
|
481
481
|
return;
|
|
482
482
|
}
|
|
483
|
-
const raw = await db.get("data")
|
|
484
|
-
if (raw ===
|
|
483
|
+
const raw = await db.get("data");
|
|
484
|
+
if (raw === void 0 || raw === "") throw new Error(`[walletwright] MetaMask persisted no onboarding state in ${dbDir} (neither an OnboardingController key nor a data key)`);
|
|
485
485
|
const state = asState(JSON.parse(raw));
|
|
486
486
|
if (state === void 0) throw new Error(`[walletwright] MetaMask's "data" state is not an object`);
|
|
487
487
|
const nested = asState(state.data);
|
|
@@ -547,13 +547,13 @@ const metamask = {
|
|
|
547
547
|
approve,
|
|
548
548
|
ecosystems: ["evm", "svm"],
|
|
549
549
|
extensionName: "MetaMask",
|
|
550
|
+
finalizeCache: markMetaMaskOnboarded,
|
|
551
|
+
importWallet: importWallet$2,
|
|
552
|
+
onboardingPage: "home.html",
|
|
550
553
|
prepareContext: async (context) => {
|
|
551
554
|
await context.route(`**://${ACCOUNT_SYNC_HOST}/**`, (route) => route.abort());
|
|
552
555
|
},
|
|
553
556
|
prepareExtension: (cacheDir, version = DEFAULT_VERSION) => downloadAndExtractExtension(metamaskDownload(cacheDir, version)),
|
|
554
|
-
finalizeCache: markMetaMaskOnboarded,
|
|
555
|
-
importWallet: importWallet$2,
|
|
556
|
-
onboardingPage: "home.html",
|
|
557
557
|
reachUnlockScreen: reachUnlockScreen$4,
|
|
558
558
|
reject,
|
|
559
559
|
unlock: unlock$4
|
|
@@ -568,12 +568,15 @@ const { reachUnlockScreen: reachUnlockScreen$3, unlock: unlock$3 } = createUnloc
|
|
|
568
568
|
return await page.locator("button").first().isVisible().catch(() => false);
|
|
569
569
|
},
|
|
570
570
|
submit: async (page, field) => {
|
|
571
|
-
const unlockButton = page.getByRole("button", { name:
|
|
571
|
+
const unlockButton = page.getByRole("button", { name: new RegExp("unlock", "iv") });
|
|
572
572
|
await (await unlockButton.isVisible().catch(() => false) ? unlockButton.click() : field.press("Enter"));
|
|
573
573
|
},
|
|
574
574
|
wallet: "Phantom"
|
|
575
575
|
});
|
|
576
576
|
const phantom = {
|
|
577
|
+
approve: async (popup) => {
|
|
578
|
+
await popup.getByTestId("primary-button").click({ timeout: 15e3 });
|
|
579
|
+
},
|
|
577
580
|
ecosystems: ["evm", "svm"],
|
|
578
581
|
extensionName: "Phantom",
|
|
579
582
|
headlessApprovals: true,
|
|
@@ -583,7 +586,7 @@ const phantom = {
|
|
|
583
586
|
await sleep(1e3);
|
|
584
587
|
await page.locator("text=Import Recovery Phrase").click();
|
|
585
588
|
await sleep(1500);
|
|
586
|
-
const words = seedPhrase.trim().split(
|
|
589
|
+
const words = seedPhrase.trim().split(new RegExp("\\s+", "v"));
|
|
587
590
|
for (let i = 0; i < words.length; i++) await testId(`secret-recovery-phrase-word-input-${i}`).fill(words[i] ?? "");
|
|
588
591
|
await testId("onboarding-form-submit-button").click();
|
|
589
592
|
await testId("onboarding-form-secondary-button").waitFor({
|
|
@@ -607,13 +610,10 @@ const phantom = {
|
|
|
607
610
|
name: "phantom-chrome-latest"
|
|
608
611
|
}),
|
|
609
612
|
reachUnlockScreen: reachUnlockScreen$3,
|
|
610
|
-
unlock: unlock$3,
|
|
611
|
-
approve: async (popup) => {
|
|
612
|
-
await popup.getByTestId("primary-button").click({ timeout: 15e3 });
|
|
613
|
-
},
|
|
614
613
|
reject: async (popup) => {
|
|
615
614
|
await popup.getByTestId("secondary-button").click({ timeout: 15e3 });
|
|
616
|
-
}
|
|
615
|
+
},
|
|
616
|
+
unlock: unlock$3
|
|
617
617
|
};
|
|
618
618
|
|
|
619
619
|
//#endregion
|
|
@@ -656,7 +656,7 @@ const importWallet$1 = async (page, seedPhrase, password) => {
|
|
|
656
656
|
timeout: 3e4
|
|
657
657
|
});
|
|
658
658
|
await page.context().grantPermissions(["clipboard-read", "clipboard-write"]).catch(() => {});
|
|
659
|
-
const list = seedPhrase.trim().split(
|
|
659
|
+
const list = seedPhrase.trim().split(new RegExp("\\s+", "v"));
|
|
660
660
|
const lastFilled = async () => {
|
|
661
661
|
return (await words.nth(list.length - 1).inputValue().catch(() => "")).trim().length > 0;
|
|
662
662
|
};
|
|
@@ -795,7 +795,7 @@ const slush = {
|
|
|
795
795
|
await sleep(1e3);
|
|
796
796
|
await fclick(page, "Import existing from passphrase");
|
|
797
797
|
await sleep(1500);
|
|
798
|
-
const words = seedPhrase.trim().split(
|
|
798
|
+
const words = seedPhrase.trim().split(new RegExp("\\s+", "v"));
|
|
799
799
|
for (let i = 0; i < words.length; i++) await page.locator(`input[placeholder="Word ${i + 1}"]`).fill(words[i] ?? "");
|
|
800
800
|
await page.getByRole("button", { name: "Next" }).click();
|
|
801
801
|
await sleep(2e3);
|
|
@@ -842,7 +842,7 @@ const { reachUnlockScreen, unlock } = createUnlockScreen({
|
|
|
842
842
|
const importWallet = async (page, seedPhrase, password) => {
|
|
843
843
|
await page.getByTestId("btn-import-existing-wallet").click({ timeout: 3e4 });
|
|
844
844
|
await page.getByText("Recovery phrase", { exact: true }).first().click({ timeout: 3e4 });
|
|
845
|
-
const words = seedPhrase.trim().split(
|
|
845
|
+
const words = seedPhrase.trim().split(new RegExp("\\s+", "v"));
|
|
846
846
|
await page.getByTestId("input-recovery-phrase-1").waitFor({
|
|
847
847
|
state: "visible",
|
|
848
848
|
timeout: 3e4
|
|
@@ -969,4 +969,4 @@ const buildCache = async (setup, options = {}) => {
|
|
|
969
969
|
|
|
970
970
|
//#endregion
|
|
971
971
|
export { wallets as a, extensionIdFromPath as c, placeApprovalWindow as d, profileKey as f, walletKindsByEcosystem as i, findNotificationPopup as l, waitUntil as m, extensionContextOptions as n, DEFAULT_CACHE_DIR as o, formatTimeout as p, isWalletKind as r, DEFAULT_NOTIFICATION_MATCH as s, buildCache as t, hasNotificationPopup as u };
|
|
972
|
-
//# sourceMappingURL=cache-
|
|
972
|
+
//# sourceMappingURL=cache-Bwv2i-hv.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-Bwv2i-hv.mjs","names":["add","NAVIGATION_TIMEOUT_MS","importWallet","unlockScreen","importWallet"],"sources":["../src/internal/download.ts","../src/wallets/metamask/actions/accounts.ts","../src/wallets/metamask/actions/network.ts","../src/internal/wait.ts","../src/internal/unlock-screen.ts","../src/wallets/metamask/onboarding.ts","../src/wallets/metamask/actions/settings.ts","../src/wallets/metamask/approve.ts","../src/internal/utils.ts","../src/wallets/metamask/onboarding-patch.ts","../src/wallets/metamask.ts","../src/wallets/phantom.ts","../src/wallets/rabby.ts","../src/wallets/slush.ts","../src/wallets/solflare.ts","../src/wallets/index.ts","../src/internal/chromium.ts","../src/internal/cache.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { existsSync } from \"node:fs\";\nimport { mkdir, rm } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport AdmZip from \"adm-zip\";\n\nconst ZIP_SIGNATURE = Buffer.from([80, 75, 3, 4]);\n\n/**\n * Download an extension archive and extract it to `<cacheDir>/<name>`, returning that path. A `.crx`\n * is a ZIP with a binary header, we slice from the ZIP signature before unzipping. Reuses an\n * existing extraction (so you can pre-place the extension to skip the download).\n */\nexport const downloadAndExtractExtension = async (options: {\n cacheDir: string;\n kind: \"zip\" | \"crx\";\n name: string;\n /**\n * Expected sha256 of the downloaded bytes. Required, and `undefined` only where the bytes genuinely\n * cannot be pinned, so adding a download site forces a decision instead of quietly trusting it.\n */\n sha256: string | undefined;\n url: string;\n}): Promise<string> => {\n const { cacheDir, kind, name, sha256, url } = options;\n const cacheRoot = path.resolve(cacheDir);\n const outDir = path.resolve(cacheDir, name);\n if (outDir === cacheRoot || !outDir.startsWith(cacheRoot + path.sep)) {\n throw new Error(`[walletwright] invalid extension name: ${name}`);\n }\n if (existsSync(path.join(outDir, \"manifest.json\"))) {\n return outDir;\n }\n\n await mkdir(cacheDir, { recursive: true });\n\n const response = await fetch(url, { redirect: \"follow\" });\n if (!response.ok) {\n throw new Error(\n `[walletwright] failed to download ${url}: ${response.status} ${response.statusText}`,\n );\n }\n const bytes = Buffer.from(await response.arrayBuffer());\n\n if (sha256 !== undefined) {\n const actual = createHash(\"sha256\").update(bytes).digest(\"hex\");\n if (actual !== sha256.toLowerCase()) {\n throw new Error(\n `[walletwright] ${name} failed integrity check: expected ${sha256}, got ${actual}`,\n );\n }\n }\n\n let zipBytes = bytes;\n if (kind === \"crx\") {\n const start = bytes.indexOf(ZIP_SIGNATURE);\n if (start === -1) {\n throw new Error(`[walletwright] ${url} is not a valid CRX (no ZIP header found)`);\n }\n zipBytes = bytes.subarray(start);\n }\n\n await rm(outDir, { force: true, recursive: true });\n const zip = new AdmZip(zipBytes);\n const root = path.resolve(outDir);\n for (const entry of zip.getEntries()) {\n const target = path.resolve(root, entry.entryName);\n if (target !== root && !target.startsWith(root + path.sep)) {\n throw new Error(`[walletwright] refusing to extract ${entry.entryName}: escapes ${outDir}`);\n }\n }\n zip.extractAllTo(outDir, /* overwrite */ true);\n\n if (!existsSync(path.join(outDir, \"manifest.json\"))) {\n throw new Error(`[walletwright] extracted ${name} but no manifest.json found in ${outDir}`);\n }\n return outDir;\n};\n\n/** Build the Chrome Web Store CRX download URL for an extension id. */\nexport const chromeWebStoreCrxUrl = (extensionId: string): string =>\n `https://clients2.google.com/service/update2/crx?response=redirect&prodversion=130.0&acceptformat=crx2,crx3&x=id%3D${extensionId}%26uc`;\n\n/** Download and extract a Chrome Web Store extension (latest) into `<cacheDir>/<name>`. */\nexport const prepareWebStoreExtension = (options: {\n cacheDir: string;\n extensionId: string;\n name: string;\n}): Promise<string> =>\n downloadAndExtractExtension({\n cacheDir: options.cacheDir,\n kind: \"crx\",\n name: options.name,\n sha256: undefined,\n url: chromeWebStoreCrxUrl(options.extensionId),\n });\n","import type { Page } from \"@playwright/test\";\n\nimport type { AccountActions, WalletActionContext } from \"../../../types\";\n\nconst openAccountMenu = async (home: Page): Promise<void> => {\n await home.getByTestId(\"account-menu-icon\").click();\n await home.getByTestId(\"add-multichain-account-button\").waitFor({ state: \"visible\" });\n};\n\n/** The menu is a popover with no close affordance; going home is the reliable way out. */\nconst closeAccountMenu = async (home: Page): Promise<void> => {\n await home.goto(`${home.url().split(\"#\")[0]}#/`);\n await home.getByTestId(\"account-menu-icon\").waitFor({ state: \"visible\" });\n};\n\nconst add = async ({ home }: WalletActionContext): Promise<void> => {\n await openAccountMenu(home);\n const cells = home.getByTestId(\"account-cell-avatar\");\n const before = await cells.count();\n await home.getByTestId(\"add-multichain-account-button\").click();\n await cells.nth(before).waitFor({ state: \"visible\", timeout: 15_000 });\n await closeAccountMenu(home);\n};\n\nconst importPrivateKey = async (\n { home }: WalletActionContext,\n privateKey: string,\n): Promise<void> => {\n await openAccountMenu(home);\n await home.getByTestId(\"account-list-add-wallet-button\").click();\n const chooseImport = home.getByTestId(\"choose-wallet-type-import-account\");\n await chooseImport.waitFor({ state: \"visible\" });\n await chooseImport.click();\n const confirm = home.getByTestId(\"import-account-confirm-button\");\n await confirm.waitFor({ state: \"visible\" });\n await home.locator(\"input\").first().fill(privateKey);\n await confirm.click();\n await confirm.waitFor({ state: \"hidden\", timeout: 15_000 });\n await closeAccountMenu(home);\n};\n\nconst rename = async (\n { home }: WalletActionContext,\n options: { index: number; name: string },\n): Promise<void> => {\n await openAccountMenu(home);\n await home.getByTestId(\"multichain-account-cell-end-accessory\").nth(options.index).click();\n await home.getByTestId(\"multichain-account-menu-item-rename\").click();\n await home.getByTestId(\"account-name-input\").locator(\"input\").fill(options.name);\n await home.getByRole(\"button\", { name: \"Confirm\" }).click();\n await closeAccountMenu(home);\n};\n\nconst switchTo = async ({ home }: WalletActionContext, index: number): Promise<void> => {\n await openAccountMenu(home);\n await home.getByTestId(\"account-cell-avatar\").nth(index).click();\n const menuClosed = await home\n .getByTestId(\"add-multichain-account-button\")\n .waitFor({ state: \"hidden\", timeout: 10_000 })\n .then(() => true)\n .catch(() => false);\n if (!menuClosed) {\n throw new Error(`[walletwright] accounts.switch(${index}) did not switch (menu stayed open)`);\n }\n};\n\nexport const accounts: AccountActions = {\n add,\n importPrivateKey,\n rename,\n switch: switchTo,\n};\n","import type { Page } from \"@playwright/test\";\n\nimport type { NetworkActions, NetworkConfig, WalletActionContext } from \"../../../types\";\n\nconst closeButton = (home: Page) =>\n home.getByTestId(\"modal-header-close-button\").filter({ visible: true }).first();\n\nconst openNetworkManager = async (home: Page): Promise<void> => {\n await home.getByTestId(\"sort-by-networks\").click();\n await closeButton(home).waitFor({ state: \"visible\" });\n};\n\nconst add = async ({ home }: WalletActionContext, config: NetworkConfig): Promise<void> => {\n await openNetworkManager(home);\n\n await home.getByText(\"Custom\", { exact: true }).first().click();\n await home.getByRole(\"button\", { name: \"Add custom network\" }).first().click();\n\n await home.getByTestId(\"network-form-network-name\").fill(config.name);\n await home.getByTestId(\"network-form-chain-id\").fill(String(config.chainId));\n await home.getByTestId(\"network-form-ticker-input\").fill(config.symbol);\n\n await home.getByTestId(\"test-add-rpc-drop-down\").click();\n await home.getByText(\"Add RPC URL\", { exact: true }).first().click();\n await home.getByTestId(\"rpc-url-input-test\").fill(config.rpcUrl);\n await home.getByTestId(\"page-container-footer-next\").click();\n\n await home.getByTestId(\"network-form-network-name\").waitFor();\n await home.getByTestId(\"page-container-footer-next\").click();\n\n await closeButton(home).click();\n await closeButton(home).waitFor({ state: \"hidden\" });\n};\n\nexport const network: NetworkActions = {\n add,\n};\n","import type { Page } from \"@playwright/test\";\n\nconst DEFAULT_INTERVAL_MS = 200;\nconst DEFAULT_NAVIGATION_INTERVAL_MS = 1000;\n\nexport type WaitOptions = {\n /** Gap between checks. */\n intervalMs?: number;\n /** Total budget. `check` runs at least once, and never again once the budget has elapsed. */\n timeoutMs: number;\n};\n\nexport const sleep = (ms: number): Promise<void> =>\n new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n\n/** Render a timeout for an error message, so what a failure claims matches what it waited. */\nexport const formatTimeout = (ms: number): string => `${Math.round(ms / 100) / 10}s`;\n\n/**\n * Poll `check` until it yields anything other than `undefined`/`false`, then hand that value back.\n * Returns `undefined` when the budget elapses, so only a caller that can genuinely continue without\n * the condition (an optional approval popup, say) branches on the result; everything else uses\n * `waitUntilOrThrow` and gets a named failure instead of a wallet that looks ready and isn't.\n */\nexport const waitUntil = async <T>(\n check: () => Promise<T | false | undefined> | T | false | undefined,\n { intervalMs = DEFAULT_INTERVAL_MS, timeoutMs }: WaitOptions,\n): Promise<T | undefined> => {\n const deadline = Date.now() + timeoutMs;\n for (;;) {\n const result = await check();\n if (result !== undefined && result !== false) {\n return result;\n }\n if (Date.now() >= deadline) {\n return undefined;\n }\n await sleep(intervalMs);\n }\n};\n\nexport const waitUntilOrThrow = async <T>(\n check: () => Promise<T | false | undefined> | T | false | undefined,\n options: WaitOptions & { message: string },\n): Promise<T> => {\n const result = await waitUntil(check, options);\n if (result === undefined) {\n throw new Error(\n `[walletwright] ${options.message} (gave up after ${formatTimeout(options.timeoutMs)})`,\n );\n }\n return result;\n};\n\n/**\n * A `chrome-extension://` URL fails with `ERR_BLOCKED_BY_CLIENT` until the extension registers,\n * which takes seconds after launch, so reaching an extension page is a poll rather than a one-shot.\n */\nexport const gotoWithRetry = async (\n page: Page,\n url: string,\n {\n intervalMs = DEFAULT_NAVIGATION_INTERVAL_MS,\n label,\n timeoutMs,\n }: WaitOptions & { label: string },\n): Promise<void> => {\n await waitUntilOrThrow(\n () =>\n page\n .goto(url, { waitUntil: \"domcontentloaded\" })\n .then(() => true)\n .catch(() => undefined),\n { intervalMs, message: `${label} never loaded (${url})`, timeoutMs },\n );\n};\n","import type { BrowserContext, Locator, Page } from \"@playwright/test\";\n\nimport { formatTimeout, gotoWithRetry, waitUntil } from \"./wait\";\n\nconst PASSWORD_FIELD = 'input[type=\"password\"]';\n\nconst NAVIGATION_TIMEOUT_MS = 15_000;\nconst FIRST_SETTLE_TIMEOUT_MS = 20_000;\nconst RELOAD_ATTEMPTS = 5;\nconst RELOAD_SETTLE_TIMEOUT_MS = 5000;\nconst PROBE_INTERVAL_MS = 500;\nconst PASSWORD_CLEARED_TIMEOUT_MS = 15_000;\n\ntype UnlockScreenOptions = {\n /** Extension-relative page that renders the unlock screen (e.g. `home.html`). */\n entry: string;\n /**\n * Resolves true once the wallet's already-unlocked UI is up. Only wallets that can reopen unlocked\n * (Phantom, Slush) declare it: without a positive signal for that state, a warm launch is\n * indistinguishable from an extension page that rendered nothing at all, and the run continues\n * against a dead wallet until some later approval fails for no visible reason.\n */\n isUnlocked?: (page: Page) => Promise<boolean>;\n /** Submit the filled password. Defaults to pressing Enter in the field. */\n submit?: (page: Page, field: Locator) => Promise<void>;\n wallet: string;\n};\n\ntype UnlockScreen = {\n reachUnlockScreen: (context: BrowserContext, extensionId: string) => Promise<Page>;\n unlock: (page: Page, password: string) => Promise<void>;\n};\n\n/**\n * The shared unlock flow: open the wallet's own page, wait for it to settle into a state we can\n * name, and type the password. Every wallet differs only in its entry page, how it submits, and\n * whether it can come back already unlocked.\n */\nexport const createUnlockScreen = ({\n entry,\n isUnlocked,\n submit,\n wallet,\n}: UnlockScreenOptions): UnlockScreen => {\n const settled = async (page: Page): Promise<\"locked\" | \"unlocked\" | undefined> => {\n const locked = await page\n .locator(PASSWORD_FIELD)\n .isVisible()\n .catch(() => false);\n if (locked) {\n return \"locked\";\n }\n const unlocked = await isUnlocked?.(page).catch(() => false);\n return unlocked === true ? \"unlocked\" : undefined;\n };\n\n const settleWithin = (page: Page, timeoutMs: number) =>\n waitUntil(() => settled(page), { intervalMs: PROBE_INTERVAL_MS, timeoutMs });\n\n const reachUnlockScreen = async (context: BrowserContext, extensionId: string): Promise<Page> => {\n const page = await context.newPage();\n await gotoWithRetry(page, `chrome-extension://${extensionId}/${entry}`, {\n label: `${wallet} ${entry}`,\n timeoutMs: NAVIGATION_TIMEOUT_MS,\n });\n\n let state = await settleWithin(page, FIRST_SETTLE_TIMEOUT_MS);\n for (let attempt = 0; attempt < RELOAD_ATTEMPTS && state === undefined; attempt++) {\n await page.reload().catch(() => {});\n state = await settleWithin(page, RELOAD_SETTLE_TIMEOUT_MS);\n }\n if (state === undefined) {\n throw new Error(\n `[walletwright] ${wallet} never reached its unlock screen (gave up after ${formatTimeout(\n FIRST_SETTLE_TIMEOUT_MS + RELOAD_ATTEMPTS * RELOAD_SETTLE_TIMEOUT_MS,\n )})`,\n );\n }\n return page;\n };\n\n const unlock = async (page: Page, password: string): Promise<void> => {\n const field = page.locator(PASSWORD_FIELD);\n const locked = await field.isVisible().catch(() => false);\n if (!locked) {\n const unlocked = await isUnlocked?.(page).catch(() => false);\n if (unlocked === true) {\n return;\n }\n throw new Error(\n `[walletwright] ${wallet} showed neither an unlock screen nor an unlocked wallet`,\n );\n }\n\n await field.fill(password);\n await (submit === undefined ? field.press(\"Enter\") : submit(page, field));\n const cleared = await field\n .waitFor({ state: \"hidden\", timeout: PASSWORD_CLEARED_TIMEOUT_MS })\n .then(() => true)\n .catch(() => false);\n if (!cleared) {\n throw new Error(\n `[walletwright] ${wallet} unlock failed (password screen still visible after ${formatTimeout(\n PASSWORD_CLEARED_TIMEOUT_MS,\n )})`,\n );\n }\n };\n\n return { reachUnlockScreen, unlock };\n};\n","import type { Page } from \"@playwright/test\";\n\nimport { createUnlockScreen } from \"../../internal/unlock-screen\";\nimport { waitUntil } from \"../../internal/wait\";\n\nconst SRP_RETYPE_TIMEOUT_MS = 40_000;\nconst SRP_CONFIRM_TIMEOUT_MS = 5000;\n\nconst { reachUnlockScreen, unlock } = createUnlockScreen({\n entry: \"home.html\",\n wallet: \"MetaMask\",\n});\n\nexport const importWallet = async (\n page: Page,\n seedPhrase: string,\n password: string,\n): Promise<void> => {\n await page.getByTestId(\"onboarding-import-wallet\").click();\n await page.getByTestId(\"onboarding-import-with-srp-button\").click();\n\n const srpField = page.getByTestId(\"srp-input-import__srp-note\");\n const confirmEnabled = page.locator('[data-testid=\"import-srp-confirm\"]:not([disabled])');\n const accepted = await waitUntil(\n async () => {\n await srpField.click();\n await srpField.fill(\"\");\n await srpField.pressSequentially(seedPhrase, { delay: 50 });\n return confirmEnabled\n .waitFor({ state: \"visible\", timeout: SRP_CONFIRM_TIMEOUT_MS })\n .then(() => true)\n .catch(() => undefined);\n },\n { intervalMs: 0, timeoutMs: SRP_RETYPE_TIMEOUT_MS },\n );\n if (accepted === undefined) {\n throw new Error(\"[walletwright] MetaMask rejected the seed phrase (confirm stayed disabled)\");\n }\n await confirmEnabled.click();\n\n await page.getByTestId(\"create-password-new-input\").fill(password);\n await page.getByTestId(\"create-password-confirm-input\").fill(password);\n await page.getByTestId(\"create-password-terms\").click();\n await page.locator('[data-testid=\"create-password-submit\"]:not([disabled])').click();\n\n await page\n .getByTestId(\"passkey-maybe-later-button\")\n .click({ timeout: 10_000 })\n .catch(() => {});\n await page\n .getByTestId(\"metametrics-i-agree\")\n .click({ timeout: 10_000 })\n .catch(() => {});\n};\n\nexport { reachUnlockScreen, unlock };\n","import type { SettingsActions } from \"../../../types\";\nimport { unlock as unlockScreen } from \"../onboarding\";\n\nexport const settings: SettingsActions = {\n lock: async ({ home }) => {\n await home.getByTestId(\"account-options-menu-button\").click();\n await home.getByTestId(\"global-menu-lock\").filter({ visible: true }).first().click();\n await home.locator('input[type=\"password\"]').waitFor({ state: \"visible\", timeout: 15_000 });\n },\n\n unlock: async ({ home, password }) => {\n await unlockScreen(home, password);\n },\n};\n","import type { Locator, Page } from \"@playwright/test\";\n\n/**\n * How long a confirm or cancel button gets to become clickable, once the page is already showing\n * one. Generous because a loaded CI runner renders MetaMask's footer slowly.\n */\nconst BUTTON_TIMEOUT_MS = 45_000;\n/** The retry only covers a notice that rendered late, so it does not need the full budget again. */\nconst RETRY_TIMEOUT_MS = 10_000;\n\n/**\n * First-time requests that MetaMask routes through a protocol Snap (e.g. adding a custom chain)\n * open a \"Third-party software notice\" modal over the confirm footer, and every click is\n * intercepted until it is accepted. Its buttons carry no testids, only text, and Accept stays\n * disabled until the notice is scrolled to the bottom.\n */\nconst acceptThirdPartyNotice = async (popup: Page): Promise<void> => {\n const modal = popup.locator(\".snap-install-warning\");\n if ((await modal.count().catch(() => 0)) === 0) {\n return;\n }\n await modal\n .locator(\"p\")\n .last()\n .scrollIntoViewIfNeeded()\n .catch(() => {});\n await modal\n .getByRole(\"button\", { name: \"Accept\" })\n .click({ timeout: 3000 })\n .catch(() => {});\n};\n\n/**\n * Confirm buttons differ by request type: connect popups use `confirm-btn`, signature and\n * transaction popups use `confirm-footer-button`, older builds used `page-container-footer-next`,\n * and requests routed through a protocol Snap (e.g. Solana) use\n * `confirm-<type>-confirm-snap-footer-button`, matched by suffix so every snap confirmation type is\n * covered. A popup renders exactly one of these buttons; the `.or()` union resolves whichever one\n * is present by DOM order, not by the order it's written in below, so one locator covers every\n * popup.\n */\nconst confirmButton = (popup: Page): Locator =>\n popup\n .getByTestId(\"confirm-btn\")\n .or(popup.getByTestId(\"confirm-footer-button\"))\n .or(popup.locator('[data-testid=\"page-container-footer-next\"]'))\n .or(popup.locator('[data-testid$=\"-confirm-snap-footer-button\"]'))\n .first();\n\nconst cancelButton = (popup: Page): Locator =>\n popup\n .getByTestId(\"cancel-btn\")\n .or(popup.getByTestId(\"confirm-footer-cancel-button\"))\n .or(popup.locator('[data-testid=\"page-container-footer-cancel\"]'))\n .or(popup.locator('[data-testid$=\"-cancel-snap-footer-button\"]'))\n .first();\n\n/**\n * What \"this page is showing a request\" means for MetaMask, as opposed to its wallet home. The\n * distinction matters for the page walletwright opens itself: `notification.html` renders the home\n * screen (account header, Buy/Swap/Send, a news carousel) when nothing is pending, so buttons on\n * screen prove nothing. A CI run handed that home screen to `approve` and waited out the full click\n * budget against a confirm button that was never going to exist.\n */\nexport const approvalControls = (popup: Page): Locator =>\n confirmButton(popup).or(cancelButton(popup));\n\nexport const approve = async (popup: Page): Promise<void> => {\n const button = confirmButton(popup);\n await acceptThirdPartyNotice(popup);\n try {\n await button.click({ timeout: BUTTON_TIMEOUT_MS });\n } catch (error) {\n await acceptThirdPartyNotice(popup);\n await button.click({ timeout: RETRY_TIMEOUT_MS }).catch(() => {\n throw error;\n });\n }\n};\n\n/** The cancel counterparts of `approve`; same union-by-DOM-order resolution, not written order. */\nexport const reject = async (popup: Page): Promise<void> => {\n await cancelButton(popup).click({ timeout: BUTTON_TIMEOUT_MS });\n};\n","import { createHash } from \"node:crypto\";\nimport { readFileSync, realpathSync } from \"node:fs\";\nimport path from \"node:path\";\n\nimport type { BrowserContext, Locator, Page } from \"@playwright/test\";\n\nimport type { WalletSetup } from \"../types\";\n\nimport { waitUntil } from \"./wait\";\n\nexport const DEFAULT_CACHE_DIR = \".walletwright\";\n\n/** MetaMask/Phantom approval-popup URL token; Slush overrides it with `notificationMatch: \"isPopup=1\"`. */\nexport const DEFAULT_NOTIFICATION_MATCH = \"notification.html\";\n\n/** Stable per-setup profile directory name, so the same wallet+seed+password reuses one cache. */\nexport const profileKey = (setup: WalletSetup): string =>\n createHash(\"sha256\")\n .update(`${setup.wallet}:${setup.version ?? \"default\"}:${setup.seedPhrase}:${setup.password}`)\n .digest(\"hex\")\n .slice(0, 20);\n\n/**\n * Compute an extension's id without querying the browser (`chrome://extensions` is blocked headless,\n * and the service worker starts lazily). Chrome derives the id from the sha256 of either the\n * manifest's public `key` (if present, e.g. Phantom → its fixed Web Store id) or the absolute load\n * path (no key, e.g. MetaMask), taking the first 16 bytes and mapping each nibble 0-f → a-p.\n *\n * The path is resolved through symlinks (`realpathSync`) because Chrome hashes the *real* path, a\n * cache under a symlinked dir (e.g. macOS `/tmp` → `/private/tmp`) would otherwise yield a wrong id.\n */\nexport const extensionIdFromPath = (extensionPath: string): string => {\n const resolved = path.resolve(extensionPath);\n const abs = (() => {\n try {\n return realpathSync(resolved);\n } catch {\n return resolved; // path doesn't exist yet, fall back to the literal resolved path\n }\n })();\n let key: string | undefined;\n try {\n const manifest: unknown = JSON.parse(readFileSync(path.join(abs, \"manifest.json\"), \"utf8\"));\n if (typeof manifest === \"object\" && manifest !== null && \"key\" in manifest) {\n key = typeof manifest.key === \"string\" ? manifest.key : undefined;\n }\n } catch {\n // manifest not present yet, fall back to the path\n }\n const source =\n key === undefined || key === \"\" ? Buffer.from(abs, \"utf8\") : Buffer.from(key, \"base64\");\n const hex = createHash(\"sha256\").update(source).digest(\"hex\").slice(0, 32);\n return Array.from(hex, (nibble) => String.fromCodePoint(97 + Number.parseInt(nibble, 16))).join(\n \"\",\n );\n};\n\n/**\n * Approval popups open as `about:blank` and then navigate, so `waitForEvent('page', { predicate })`\n * misses them, poll the open pages instead. `match` is the URL token identifying the popup\n * (`notification.html` for MetaMask/Phantom; `isPopup=1` for Slush's single-page `index.html`).\n */\nexport const isApprovalPopup = (page: Page, extensionId: string, match: string): boolean =>\n page.url().startsWith(`chrome-extension://${extensionId}`) &&\n page.url().includes(match) &&\n !page.isClosed();\n\nconst isVisible = async (locator: Locator): Promise<boolean> => {\n try {\n return await locator.isVisible();\n } catch {\n return false; // the page can go while we ask\n }\n};\n\nconst hasVisibleButton = (page: Page): Promise<boolean> =>\n isVisible(page.locator(\"button\").first());\n\nexport const findNotificationPopup = ({\n approvalControls,\n context,\n extensionId,\n match = DEFAULT_NOTIFICATION_MATCH,\n timeoutMs = 10_000,\n}: {\n /** From `WalletDefinition.approvalControls`, for a wallet that can render its popup requestless. */\n approvalControls?: (page: Page) => Locator;\n context: BrowserContext;\n extensionId: string;\n match?: string;\n timeoutMs?: number;\n}): Promise<Page | undefined> =>\n waitUntil(\n async () => {\n const popup = context.pages().find((page) => isApprovalPopup(page, extensionId, match));\n if (!popup) {\n return undefined;\n }\n await popup.waitForLoadState(\"domcontentloaded\").catch(() => {});\n const ready =\n approvalControls === undefined\n ? await hasVisibleButton(popup)\n : await isVisible(approvalControls(popup).first());\n return ready ? popup : undefined;\n },\n { timeoutMs },\n );\n\n/** MetaMask's own popup dimensions, which its layout is built for. */\nconst APPROVAL_WINDOW = { height: 592, width: 360 };\n/** Far enough from the edge that a popup this size lands fully on any usable display. */\nconst APPROVAL_WINDOW_OFFSET = 50;\n\n/**\n * Put a wallet-spawned approval window somewhere it can be clicked. It can open partly off-screen\n * on a small or virtual display, and Playwright will not click what is out of view, so a confirm\n * button that is rendered and enabled still times out. Synpress carries the same workaround.\n *\n * Best-effort throughout: headless has no window to move, and nothing here is required for a window\n * that already sits on screen.\n */\nexport const placeApprovalWindow = async (page: Page): Promise<void> => {\n await page.setViewportSize(APPROVAL_WINDOW).catch(() => {});\n try {\n const session = await page.context().newCDPSession(page);\n const { targetInfo } = await session.send(\"Target.getTargetInfo\");\n const { windowId } = await session.send(\"Browser.getWindowForTarget\", {\n targetId: targetInfo.targetId,\n });\n await session.send(\"Browser.setWindowBounds\", {\n bounds: { left: APPROVAL_WINDOW_OFFSET, top: APPROVAL_WINDOW_OFFSET },\n windowId,\n });\n await session.detach();\n } catch {\n // no window to place (headless), or the page refused a CDP session\n }\n};\n\n/** Where Chrome persists an extension's `chrome.storage.local` inside a browser profile. */\nexport const extensionStateDir = (profileDir: string, extensionId: string): string =>\n path.join(profileDir, \"Default\", \"Local Extension Settings\", extensionId);\n\nexport const hasNotificationPopup = (\n context: BrowserContext,\n extensionId: string,\n match = DEFAULT_NOTIFICATION_MATCH,\n): boolean => context.pages().some((page) => isApprovalPopup(page, extensionId, match));\n","import { existsSync } from \"node:fs\";\n\nimport { ClassicLevel } from \"classic-level\";\n\nimport { extensionStateDir } from \"../../internal/utils\";\n\ntype OnboardingState = Record<string, unknown>;\n\nconst asState = (value: unknown): OnboardingState | undefined =>\n typeof value === \"object\" && value !== null ? { ...value } : undefined;\n\nconst markOnboarded = (onboarding: OnboardingState): OnboardingState => ({\n ...onboarding,\n completedOnboarding: true,\n firstTimeFlowType:\n typeof onboarding.firstTimeFlowType === \"string\" ? onboarding.firstTimeFlowType : \"import\",\n onboardingTabs: {},\n});\n\n/**\n * Force `completedOnboarding=true` in MetaMask's persisted state so the cached wallet boots straight\n * to the unlock screen instead of getting stuck on the \"wallet is ready\" screen (whose \"Open wallet\"\n * action goes through the MV3 service worker and is unreliable under automation).\n *\n * MetaMask 13.13.x stored all state under a single `data` key; 13.3x stores each controller under its\n * own key (`OnboardingController`). Both are handled. Must run while the browser is closed.\n *\n * Every branch that can't find the state it means to patch throws: leaving the cache unpatched means\n * a wallet that boots to \"wallet is ready\" and ignores every dapp request, which is far harder to\n * diagnose from a spec than a failed cache build.\n */\nexport const markMetaMaskOnboarded = async (\n profileDir: string,\n extensionId: string,\n): Promise<void> => {\n const dbDir = extensionStateDir(profileDir, extensionId);\n if (!existsSync(dbDir)) {\n throw new Error(`[walletwright] MetaMask wrote no extension state to ${dbDir}`);\n }\n\n const db = new ClassicLevel(dbDir, {\n createIfMissing: false,\n keyEncoding: \"utf8\",\n valueEncoding: \"utf8\",\n });\n try {\n await db.open();\n\n const perController = await db.get(\"OnboardingController\");\n if (perController !== undefined && perController !== \"\") {\n const onboarding = asState(JSON.parse(perController));\n if (onboarding === undefined) {\n throw new Error(`[walletwright] MetaMask's OnboardingController state is not an object`);\n }\n if (onboarding.completedOnboarding !== true) {\n await db.put(\"OnboardingController\", JSON.stringify(markOnboarded(onboarding)));\n }\n return;\n }\n\n const raw = await db.get(\"data\");\n if (raw === undefined || raw === \"\") {\n throw new Error(\n `[walletwright] MetaMask persisted no onboarding state in ${dbDir} (neither an OnboardingController key nor a data key)`,\n );\n }\n const state = asState(JSON.parse(raw));\n if (state === undefined) {\n throw new Error(`[walletwright] MetaMask's \"data\" state is not an object`);\n }\n const nested = asState(state.data);\n const controller = asState((nested ?? state).OnboardingController);\n if (controller === undefined) {\n throw new Error(`[walletwright] MetaMask's \"data\" state holds no OnboardingController`);\n }\n const patched = markOnboarded(controller);\n await db.put(\n \"data\",\n JSON.stringify(\n nested === undefined\n ? { ...state, OnboardingController: patched }\n : { ...state, data: { ...nested, OnboardingController: patched } },\n ),\n );\n } finally {\n await db.close();\n }\n};\n","import { downloadAndExtractExtension } from \"../internal/download\";\nimport type { WalletDefinition } from \"../types\";\n\nimport { accounts } from \"./metamask/actions/accounts\";\nimport { network } from \"./metamask/actions/network\";\nimport { settings } from \"./metamask/actions/settings\";\nimport { approvalControls, approve, reject } from \"./metamask/approve\";\nimport { importWallet, reachUnlockScreen, unlock } from \"./metamask/onboarding\";\nimport { markMetaMaskOnboarded } from \"./metamask/onboarding-patch\";\n\nconst DEFAULT_VERSION = \"13.35.1\";\n\n/**\n * sha256 of each pinned `metamask-chrome-<version>.zip` release asset, so a pinned download is\n * verified rather than trusted. This is the one place to fill in when bumping `DEFAULT_VERSION`:\n *\n * curl -sL https://github.com/MetaMask/metamask-extension/releases/download/v<v>/metamask-chrome-<v>.zip | shasum -a 256\n *\n * A version a caller pins through `setup.version` has no entry here and downloads unverified, and\n * the Web-Store wallets can't be pinned at all (that endpoint always serves the current version).\n */\nconst RELEASE_SHA256: Readonly<Record<string, string>> = {\n \"13.35.1\": \"4e0f8626df0ae9fb15f5f3ad6784a0b518f3ede067b2b0d4f539f9f457c5049c\",\n};\n\n/** Download inputs for a MetaMask release, split out so the integrity wiring is unit-testable. */\nexport const metamaskDownload = (cacheDir: string, version: string) => ({\n cacheDir,\n kind: \"zip\" as const,\n name: `metamask-chrome-${version}`,\n sha256: RELEASE_SHA256[version],\n url: `https://github.com/MetaMask/metamask-extension/releases/download/v${version}/metamask-chrome-${version}.zip`,\n});\n\n/**\n * MetaMask's backup-and-sync restores account names for whichever SRP a profile holds, and test\n * seeds are shared (the public `test test … junk` one by thousands), so a synced profile reports a\n * stranger's account names in place of the ones a test just set: CI runs showed accounts called\n * `dev1` and `personal`, holding a real balance, where `Account 2` was expected. It only happens\n * where the sync actually lands, which makes it look like a flake. MetaMask's own e2e suite mocks\n * its external services for the same reason.\n *\n * Just the one host that stores the names. Cutting the auth stack around it (`authentication`,\n * `oidc`) reaches further than intended: other features authenticate through it too, and a wallet\n * that cannot authenticate can leave a request's confirm button disabled with nothing to explain it.\n */\nconst ACCOUNT_SYNC_HOST = \"user-storage.api.cx.metamask.io\";\n\nexport const metamask: WalletDefinition = {\n actions: { accounts, network, settings },\n approvalControls,\n approve,\n ecosystems: [\"evm\", \"svm\"],\n extensionName: \"MetaMask\",\n\n finalizeCache: markMetaMaskOnboarded,\n\n importWallet,\n\n onboardingPage: \"home.html\",\n\n prepareContext: async (context) => {\n await context.route(`**://${ACCOUNT_SYNC_HOST}/**`, (route) => route.abort());\n },\n prepareExtension: (cacheDir, version = DEFAULT_VERSION) =>\n downloadAndExtractExtension(metamaskDownload(cacheDir, version)),\n reachUnlockScreen,\n reject,\n unlock,\n};\n","import { prepareWebStoreExtension } from \"../internal/download\";\nimport { createUnlockScreen } from \"../internal/unlock-screen\";\nimport { sleep } from \"../internal/wait\";\nimport type { WalletDefinition } from \"../types\";\n\nconst PHANTOM_EXTENSION_ID = \"bfnaelmomeimhlpmgjnjophhpkkoljpa\";\n\nconst { reachUnlockScreen, unlock } = createUnlockScreen({\n entry: \"popup.html\",\n isUnlocked: async (page) => {\n const rendered = await page\n .locator(\"button\")\n .first()\n .isVisible()\n .catch(() => false);\n return rendered;\n },\n submit: async (page, field) => {\n const unlockButton = page.getByRole(\"button\", { name: /unlock/iv });\n await ((await unlockButton.isVisible().catch(() => false))\n ? unlockButton.click()\n : field.press(\"Enter\"));\n },\n wallet: \"Phantom\",\n});\n\nexport const phantom: WalletDefinition = {\n approve: async (popup) => {\n await popup.getByTestId(\"primary-button\").click({ timeout: 15_000 });\n },\n ecosystems: [\"evm\", \"svm\"],\n\n extensionName: \"Phantom\",\n\n headlessApprovals: true,\n\n importWallet: async (page, seedPhrase, password) => {\n const testId = (id: string) => page.locator(`[data-testid=\"${id}\"]`);\n\n await page.locator(\"text=I already have a wallet\").click();\n await sleep(1000);\n await page.locator(\"text=Import Recovery Phrase\").click();\n await sleep(1500);\n\n const words = seedPhrase.trim().split(/\\s+/v);\n for (let i = 0; i < words.length; i++) {\n await testId(`secret-recovery-phrase-word-input-${i}`).fill(words[i] ?? \"\");\n }\n await testId(\"onboarding-form-submit-button\").click();\n\n await testId(\"onboarding-form-secondary-button\").waitFor({ state: \"visible\", timeout: 60_000 });\n await testId(\"onboarding-form-submit-button\").click();\n\n await testId(\"onboarding-form-password-input\").fill(password);\n await testId(\"onboarding-form-confirm-password-input\").fill(password);\n await testId(\"onboarding-form-terms-of-service-checkbox\").click();\n await testId(\"onboarding-form-submit-button\").click();\n await sleep(2500);\n\n const getStarted = page.locator(\"text=Get Started\");\n if (await getStarted.isVisible().catch(() => false)) {\n await getStarted.click();\n }\n await sleep(1500);\n },\n\n onboardingPage: \"onboarding.html\",\n\n prepareExtension: (cacheDir) =>\n prepareWebStoreExtension({\n cacheDir,\n extensionId: PHANTOM_EXTENSION_ID,\n name: \"phantom-chrome-latest\",\n }),\n\n reachUnlockScreen,\n\n reject: async (popup) => {\n await popup.getByTestId(\"secondary-button\").click({ timeout: 15_000 });\n },\n\n unlock,\n};\n","import type { Page } from \"@playwright/test\";\n\nimport { prepareWebStoreExtension } from \"../internal/download\";\nimport { createUnlockScreen } from \"../internal/unlock-screen\";\nimport { sleep, waitUntil, waitUntilOrThrow } from \"../internal/wait\";\nimport type { WalletDefinition } from \"../types\";\n\nconst ROUTE_TIMEOUT_MS = 30_000;\nconst SEED_PASTE_TIMEOUT_MS = 3000;\nconst APPROVAL_TIMEOUT_MS = 30_000;\n\nconst RABBY_EXTENSION_ID = \"acmacodkjbdgmoleebolmdjonilkdbch\";\n\nconst ONBOARDING_ROUTE = \"index.html#/new-user/guide\";\n\nconst { reachUnlockScreen, unlock } = createUnlockScreen({ entry: \"index.html\", wallet: \"Rabby\" });\n\nconst clickText = async (page: Page, text: string, timeoutMs = 30_000): Promise<void> => {\n const target = page.getByText(text, { exact: true }).first();\n await target.waitFor({ state: \"visible\", timeout: timeoutMs });\n await target.click();\n};\n\n/**\n * Rabby routes with a hash router, so `waitForURL` never settles (a hash change fires no navigation\n * event). Poll the URL instead. Anchoring each onboarding step on its route matters because several\n * screens share the same `input[type=\"password\"]` selector.\n */\nconst waitForRoute = async (\n page: Page,\n fragment: string,\n timeoutMs = ROUTE_TIMEOUT_MS,\n): Promise<void> => {\n await waitUntilOrThrow(() => page.url().includes(fragment), {\n message: `Rabby never reached ${fragment}`,\n timeoutMs,\n });\n};\n\nconst importWallet = async (page: Page, seedPhrase: string, password: string): Promise<void> => {\n await clickText(page, \"I already have an address\");\n await clickText(page, \"Seed Phrase or Private Key\");\n await waitForRoute(page, \"seed-or-key\");\n\n const words = page.locator('input[type=\"password\"]');\n await words.first().waitFor({ state: \"visible\", timeout: 30_000 });\n await page\n .context()\n .grantPermissions([\"clipboard-read\", \"clipboard-write\"])\n .catch(() => {});\n const list = seedPhrase.trim().split(/\\s+/v);\n const lastFilled = async (): Promise<boolean> => {\n const value = await words\n .nth(list.length - 1)\n .inputValue()\n .catch(() => \"\");\n return value.trim().length > 0;\n };\n\n await words.first().click();\n await page.evaluate((phrase) => navigator.clipboard.writeText(phrase), seedPhrase.trim());\n await page.keyboard.press(\"ControlOrMeta+v\");\n const pasted = await waitUntil(lastFilled, { timeoutMs: SEED_PASTE_TIMEOUT_MS });\n if (pasted === undefined) {\n for (let index = 0; index < list.length; index++) {\n const box = words.nth(index);\n await box.click();\n await box.fill(list[index] ?? \"\");\n }\n }\n await page.getByRole(\"button\", { exact: true, name: \"Next\" }).first().click();\n\n await waitForRoute(page, \"set-password\");\n const passwords = page.locator('input[type=\"password\"]');\n await passwords.nth(1).waitFor({ state: \"visible\", timeout: 30_000 });\n await passwords.nth(0).fill(password);\n await passwords.nth(1).fill(password);\n await page.getByRole(\"button\", { exact: true, name: \"Confirm\" }).first().click();\n\n await page\n .getByText(\"Address Imported\", { exact: false })\n .first()\n .waitFor({ state: \"visible\", timeout: 30_000 });\n await sleep(1000);\n};\n\nconst CONFIRM_LABELS = [\"Connect\", \"Sign\", \"Confirm\"];\nconst CANCEL_LABELS = [\"Cancel\", \"Reject\"];\n\n/**\n * Rabby's approval window is focus-fragile: it unmounts its contents a few seconds after losing\n * focus, and Playwright's click (which waits for actionability, then for the click to settle) loses\n * the window mid-action. Rabby reads the vanished window as a dismissal, so the dapp gets\n * \"User rejected the request\" even though the confirm button was visible and enabled. Dispatching\n * the click inside the page skips focus, actionability, and post-click bookkeeping entirely. Rabby\n * is a plain React app, so `evaluate` works here (unlike MetaMask, which scuttles the realm).\n *\n * Signing is a two-step footer: \"Sign\" swaps itself for \"Confirm\", which must be clicked too, and\n * both start disabled while Rabby analyses the request. So keep clicking whichever labelled button\n * is currently enabled until the window closes, which is the only signal the request was answered.\n */\nconst clickApprovalButton = async (\n popup: Page,\n labels: ReadonlyArray<string>,\n): Promise<boolean> => {\n let lastClicked = \"\";\n const answered = await waitUntil(\n async () => {\n if (popup.isClosed()) {\n return true;\n }\n const clicked = await popup\n .evaluate(\n (arg) => {\n const target = [...document.querySelectorAll(\"button\")].find((button) => {\n const text = (button.textContent ?? \"\").trim();\n return arg.names.includes(text) && !button.disabled && text !== arg.skip;\n });\n target?.click();\n return target ? (target.textContent ?? \"\").trim() : \"\";\n },\n { names: [...labels], skip: lastClicked },\n )\n .catch(() => \"\");\n if (clicked) {\n lastClicked = clicked;\n }\n return undefined;\n },\n { intervalMs: 300, timeoutMs: APPROVAL_TIMEOUT_MS },\n );\n return answered === true || popup.isClosed();\n};\n\nexport const rabby: WalletDefinition = {\n approve: async (popup) => {\n if (!(await clickApprovalButton(popup, CONFIRM_LABELS))) {\n throw new Error(\"[walletwright] Rabby approval: no confirm button became actionable\");\n }\n },\n\n ecosystems: [\"evm\"],\n\n extensionName: \"Rabby\",\n\n headlessApprovals: true,\n\n importWallet,\n\n onboardingPage: ONBOARDING_ROUTE,\n\n prepareExtension: (cacheDir) =>\n prepareWebStoreExtension({\n cacheDir,\n extensionId: RABBY_EXTENSION_ID,\n name: \"rabby-chrome-latest\",\n }),\n\n reachUnlockScreen,\n\n reject: async (popup) => {\n if (!(await clickApprovalButton(popup, CANCEL_LABELS))) {\n throw new Error(\"[walletwright] Rabby reject: no cancel button became actionable\");\n }\n },\n\n unlock,\n};\n","import type { Page } from \"@playwright/test\";\n\nimport { prepareWebStoreExtension } from \"../internal/download\";\nimport { createUnlockScreen } from \"../internal/unlock-screen\";\nimport { sleep, waitUntilOrThrow } from \"../internal/wait\";\nimport type { WalletDefinition } from \"../types\";\n\nconst SLUSH_EXTENSION_ID = \"opcgpfmipidbgpenhmajoajpbobppdil\";\n\nconst HOME_ROUTE = \"#/tokens\";\n\nconst IMPORT_HOME_TIMEOUT_MS = 30_000;\n\nconst atHome = async (page: Page): Promise<boolean> =>\n (await page.evaluate(() => globalThis.location.hash).catch(() => \"\")) === HOME_ROUTE;\n\nconst fclick = async (page: Page, text: string, timeoutMs = 15_000): Promise<boolean> => {\n const target = page.getByText(text, { exact: true }).first();\n const visible = await target\n .waitFor({ state: \"visible\", timeout: timeoutMs })\n .then(() => true)\n .catch(() => false);\n if (!visible) {\n return false;\n }\n await target.scrollIntoViewIfNeeded().catch(() => {});\n await target.click({ force: true, timeout: timeoutMs });\n return true;\n};\n\nconst { reachUnlockScreen, unlock } = createUnlockScreen({\n entry: \"index.html\",\n isUnlocked: atHome,\n submit: async (page) => {\n await fclick(page, \"Unlock\");\n },\n wallet: \"Slush\",\n});\n\nexport const slush: WalletDefinition = {\n approve: async (popup, password) => {\n await sleep(2000);\n const confirmed = (await fclick(popup, \"Approve\", 4000)) || (await fclick(popup, \"Sign\"));\n if (!confirmed) {\n throw new Error(\"[walletwright] Slush approval: neither Approve nor Sign was actionable\");\n }\n await sleep(1500);\n const input = popup.locator('input[type=\"password\"]');\n if (await input.isVisible().catch(() => false)) {\n await input.fill(password);\n await fclick(popup, \"Unlock\");\n }\n },\n\n ecosystems: [\"sui\"],\n\n extensionName: \"Slush\",\n\n importWallet: async (page, seedPhrase, password) => {\n await fclick(page, \"More options\");\n await sleep(1000);\n await fclick(page, \"Import existing from passphrase\");\n await sleep(1500);\n\n const words = seedPhrase.trim().split(/\\s+/v);\n for (let i = 0; i < words.length; i++) {\n await page.locator(`input[placeholder=\"Word ${i + 1}\"]`).fill(words[i] ?? \"\");\n }\n await page.getByRole(\"button\", { name: \"Next\" }).click();\n await sleep(2000);\n\n await page.locator('input[placeholder=\"Password\"]').fill(password);\n await page.locator('input[placeholder=\"Confirm Password\"]').fill(password);\n await page.getByRole(\"button\", { name: \"Next\" }).click();\n await sleep(2000);\n\n await page\n .getByRole(\"button\", { name: \"Next\" })\n .click()\n .catch(() => {});\n await waitUntilOrThrow(() => atHome(page), {\n intervalMs: 1000,\n message: `Slush import never reached the wallet home (${HOME_ROUTE})`,\n timeoutMs: IMPORT_HOME_TIMEOUT_MS,\n });\n },\n\n notificationMatch: \"isPopup=1\",\n\n onboardingPage: \"index.html\",\n\n prepareContext: async (context) => {\n await context.route(\"**://*.slush.app/**\", (route) =>\n route.fulfill({\n body: JSON.stringify({ data: {} }),\n contentType: \"application/json\",\n status: 200,\n }),\n );\n },\n\n prepareExtension: (cacheDir) =>\n prepareWebStoreExtension({\n cacheDir,\n extensionId: SLUSH_EXTENSION_ID,\n name: \"slush-chrome-latest\",\n }),\n\n reachUnlockScreen,\n\n reject: async (popup) => {\n await sleep(2000);\n if (!(await fclick(popup, \"Reject\"))) {\n throw new Error(\"[walletwright] Slush reject: no Reject control was actionable\");\n }\n },\n\n unlock,\n};\n","import type { Page } from \"@playwright/test\";\n\nimport { prepareWebStoreExtension } from \"../internal/download\";\nimport { createUnlockScreen } from \"../internal/unlock-screen\";\nimport type { WalletDefinition } from \"../types\";\n\nconst SOLFLARE_EXTENSION_ID = \"bhhhlbepdkbapadjdnnojkbgioiodbic\";\n\nconst { reachUnlockScreen, unlock } = createUnlockScreen({\n entry: \"wallet.html\",\n wallet: \"Solflare\",\n});\n\nconst importWallet = async (page: Page, seedPhrase: string, password: string): Promise<void> => {\n await page.getByTestId(\"btn-import-existing-wallet\").click({ timeout: 30_000 });\n await page.getByText(\"Recovery phrase\", { exact: true }).first().click({ timeout: 30_000 });\n\n const words = seedPhrase.trim().split(/\\s+/v);\n await page.getByTestId(\"input-recovery-phrase-1\").waitFor({ state: \"visible\", timeout: 30_000 });\n for (let index = 0; index < words.length; index++) {\n await page.getByTestId(`input-recovery-phrase-${index + 1}`).fill(words[index] ?? \"\");\n }\n await page.getByTestId(\"btn-continue\").click({ timeout: 30_000 });\n\n await page.getByTestId(\"input-new-password\").waitFor({ state: \"visible\", timeout: 30_000 });\n await page.getByTestId(\"input-new-password\").fill(password);\n await page.getByTestId(\"input-repeat-password\").fill(password);\n await page.getByTestId(\"btn-continue\").click({ timeout: 30_000 });\n\n await page.getByTestId(\"btn-quick-setup\").click({ timeout: 60_000 });\n\n await page.getByTestId(\"btn-explore\").click({ timeout: 60_000 });\n};\n\nexport const solflare: WalletDefinition = {\n approve: async (popup) => {\n await popup\n .getByTestId(\"btn-connect\")\n .or(popup.getByTestId(\"btn-approve\"))\n .first()\n .click({ timeout: 30_000 });\n },\n\n ecosystems: [\"svm\"],\n\n extensionName: \"Solflare\",\n\n importWallet,\n\n notificationMatch: \"confirm_popup.html\",\n\n onboardingPage: \"wallet.html\",\n\n prepareExtension: (cacheDir) =>\n prepareWebStoreExtension({\n cacheDir,\n extensionId: SOLFLARE_EXTENSION_ID,\n name: \"solflare-chrome-latest\",\n }),\n\n reachUnlockScreen,\n\n reject: async (popup) => {\n await popup\n .getByTestId(\"btn-cancel\")\n .or(popup.getByTestId(\"btn-reject\"))\n .first()\n .click({ timeout: 30_000 });\n },\n\n unlock,\n};\n","import type { Ecosystem, WalletDefinition, WalletKind } from \"../types\";\n\nimport { metamask } from \"./metamask\";\nimport { phantom } from \"./phantom\";\nimport { rabby } from \"./rabby\";\nimport { slush } from \"./slush\";\nimport { solflare } from \"./solflare\";\n\nexport const wallets: Record<WalletKind, WalletDefinition> = {\n metamask,\n phantom,\n rabby,\n slush,\n solflare,\n};\n\n/** Narrow an arbitrary string (CLI flag, setup file field) to a supported wallet. */\nexport const isWalletKind = (value: string): value is WalletKind => Object.hasOwn(wallets, value);\n\nconst walletKinds = Object.keys(wallets).filter(isWalletKind);\n\n/** Wallet kinds that can drive the given ecosystem (e.g. `\"evm\"` → MetaMask, Phantom). */\nexport const walletKindsByEcosystem = (ecosystem: Ecosystem): Array<WalletKind> =>\n walletKinds.filter((kind) => wallets[kind].ecosystems.includes(ecosystem));\n","/**\n * How walletwright launches Chromium with a wallet extension loaded. Shared by the cache build and\n * the test run so the two cannot drift: an extension that loads for onboarding but not for the\n * tests (or the reverse) fails far from its cause.\n */\nexport const extensionContextOptions = (\n extensionPath: string,\n headless: boolean,\n): { args: Array<string>; channel: string; headless: boolean } => ({\n args: [`--disable-extensions-except=${extensionPath}`, `--load-extension=${extensionPath}`],\n channel: \"chromium\",\n headless,\n});\n","import { mkdir, readdir, rm } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport { chromium } from \"@playwright/test\";\n\nimport type { WalletSetup } from \"../types\";\nimport { wallets } from \"../wallets/index\";\n\nimport { extensionContextOptions } from \"./chromium\";\nimport { DEFAULT_CACHE_DIR, extensionIdFromPath, extensionStateDir, profileKey } from \"./utils\";\nimport { gotoWithRetry, sleep, waitUntilOrThrow } from \"./wait\";\n\nconst NAVIGATION_TIMEOUT_MS = 15_000;\nconst FLUSH_SETTLE_MS = 3000;\nconst STATE_WRITE_TIMEOUT_MS = 10_000;\n\n/**\n * Chrome stores an extension's `chrome.storage.local` under `Local Extension Settings/<id>` and its\n * IndexedDB (what some single-page wallets persist their vault to) under\n * `IndexedDB/chrome-extension_<id>_0…`. Bytes in either one mean onboarding actually landed in the\n * profile; bytes in neither mean the \"cache\" is empty, and every later run would fail at unlock with\n * no hint that the build was the thing that went wrong.\n */\nconst hasPersistedState = async (profileDir: string, extensionId: string): Promise<boolean> => {\n const stores = [\n extensionStateDir(profileDir, extensionId),\n path.join(\n profileDir,\n \"Default\",\n \"IndexedDB\",\n `chrome-extension_${extensionId}_0.indexeddb.leveldb`,\n ),\n ];\n const listings = await Promise.allSettled(stores.map((dir) => readdir(dir)));\n return listings.some((listing) => listing.status === \"fulfilled\" && listing.value.length > 0);\n};\n\n/**\n * Import the wallet once and persist an onboarded browser profile to disk (the \"cache\"), so tests\n * launch a ready-to-unlock wallet instead of re-running onboarding each time. Returns the profile\n * directory. Idempotent per (wallet, version, seed, password).\n *\n * `headless` only affects this build step; the tests choose their own mode via `launchWallet`.\n */\nexport const buildCache = async (\n setup: WalletSetup,\n options: { headless?: boolean } = {},\n): Promise<string> => {\n const definition = wallets[setup.wallet];\n const cacheDir = path.resolve(setup.cacheDir ?? DEFAULT_CACHE_DIR);\n const extensionPath = await definition.prepareExtension(cacheDir, setup.version);\n\n const profileDir = path.join(cacheDir, profileKey(setup));\n await rm(profileDir, { force: true, recursive: true });\n await mkdir(profileDir, { recursive: true });\n\n const context = await chromium.launchPersistentContext(\n profileDir,\n extensionContextOptions(extensionPath, options.headless === true),\n );\n try {\n await definition.prepareContext?.(context);\n const extensionId = extensionIdFromPath(extensionPath);\n\n const page =\n context.pages().find((candidate) => candidate.url() === \"about:blank\") ??\n (await context.newPage());\n await gotoWithRetry(page, `chrome-extension://${extensionId}/${definition.onboardingPage}`, {\n label: `${definition.extensionName} onboarding page`,\n timeoutMs: NAVIGATION_TIMEOUT_MS,\n });\n await sleep(2000);\n\n await definition.importWallet(page, setup.seedPhrase, setup.password);\n await sleep(FLUSH_SETTLE_MS); // let the wallet flush state to disk before we close\n await waitUntilOrThrow(() => hasPersistedState(profileDir, extensionId), {\n intervalMs: 500,\n message: `${definition.extensionName} onboarding wrote no wallet state into ${profileDir}`,\n timeoutMs: STATE_WRITE_TIMEOUT_MS,\n });\n await context.close();\n await definition.finalizeCache?.(profileDir, extensionId);\n return profileDir;\n } catch (error) {\n await context.close().catch(() => {});\n throw error;\n }\n};\n"],"mappings":"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"}
|
package/dist/chain.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chain.mjs","names":[],"sources":["../src/chain.ts"],"sourcesContent":["import { Instance } from \"prool\";\n\n/**\n * A running local EVM node for transaction and network tests. `@walletwright/core/chain` is a\n * separate entry point: it needs `prool` and Foundry's `anvil` binary, which the core connect/sign\n * flow does not, so it stays out of the default install.\n */\nexport type LocalChain = {\n chainId: number;\n /** JSON-RPC endpoint to hand to the wallet (via `addNetwork` or `wallet_addEthereumChain`). */\n rpcUrl: string;\n stop: () => Promise<void>;\n};\n\nexport type LocalChainOptions = {\n /** `anvil` binary to run. Defaults to `anvil` on `PATH` (Foundry's standard install location). */\n binary?: string;\n chainId?: number;\n /** Seed for the pre-funded accounts. Defaults to the public Hardhat/Anvil test mnemonic. */\n mnemonic?: string;\n port?: number;\n};\n\
|
|
1
|
+
{"version":3,"file":"chain.mjs","names":[],"sources":["../src/chain.ts"],"sourcesContent":["import { Instance } from \"prool\";\n\n/**\n * A running local EVM node for transaction and network tests. `@walletwright/core/chain` is a\n * separate entry point: it needs `prool` and Foundry's `anvil` binary, which the core connect/sign\n * flow does not, so it stays out of the default install.\n */\nexport type LocalChain = {\n chainId: number;\n /** JSON-RPC endpoint to hand to the wallet (via `addNetwork` or `wallet_addEthereumChain`). */\n rpcUrl: string;\n stop: () => Promise<void>;\n};\n\nexport type LocalChainOptions = {\n /** `anvil` binary to run. Defaults to `anvil` on `PATH` (Foundry's standard install location). */\n binary?: string;\n chainId?: number;\n /** Seed for the pre-funded accounts. Defaults to the public Hardhat/Anvil test mnemonic. */\n mnemonic?: string;\n port?: number;\n};\n\nconst TEST_MNEMONIC = \"test test test test test test test test test test test junk\";\n\nconst createLocalChain = async (options: LocalChainOptions = {}): Promise<LocalChain> => {\n const { binary = \"anvil\", chainId = 31_337, mnemonic = TEST_MNEMONIC, port = 8545 } = options;\n const instance = Instance.anvil({ binary, chainId, mnemonic, port });\n await instance.start();\n\n return {\n chainId,\n rpcUrl: `http://127.0.0.1:${port}`,\n stop: async () => {\n await instance.stop();\n },\n };\n};\n\nexport { createLocalChain };\n"],"mappings":";;;AAuBA,MAAM,gBAAgB;AAEtB,MAAM,mBAAmB,OAAO,UAA6B,CAAC,MAA2B;CACvF,MAAM,EAAE,SAAS,SAAS,UAAU,OAAQ,WAAW,eAAe,OAAO,SAAS;CACtF,MAAM,WAAW,SAAS,MAAM;EAAE;EAAQ;EAAS;EAAU;CAAK,CAAC;CACnE,MAAM,SAAS,MAAM;CAErB,OAAO;EACL;EACA,QAAQ,oBAAoB;EAC5B,MAAM,YAAY;GAChB,MAAM,SAAS,KAAK;EACtB;CACF;AACF"}
|
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as wallets, r as isWalletKind, t as buildCache } from "./cache-
|
|
2
|
+
import { a as wallets, r as isWalletKind, t as buildCache } from "./cache-Bwv2i-hv.mjs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { realpathSync } from "node:fs";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
@@ -30,7 +30,7 @@ const parseFlags = (argv) => {
|
|
|
30
30
|
for (let i = 0; i < argv.length; i++) {
|
|
31
31
|
const token = argv[i];
|
|
32
32
|
if (!token?.startsWith("--") && token !== "-h") continue;
|
|
33
|
-
const key = token.replace(
|
|
33
|
+
const key = token.replace(new RegExp("^--?", "v"), "");
|
|
34
34
|
const next = argv[i + 1];
|
|
35
35
|
if (next && !next.startsWith("--")) {
|
|
36
36
|
flags[key] = next;
|
package/dist/cli.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.mjs","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { realpathSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\n\nimport { buildCache } from \"./internal/cache\";\nimport type { WalletSetup } from \"./types\";\nimport { isWalletKind, wallets } from \"./wallets/index\";\n\nconst KINDS = Object.keys(wallets);\n\nconst HELP = `walletwright: build the onboarded wallet cache for Playwright tests\n\nUsage:\n walletwright cache --setup <file> Build cache from a module's default-exported WalletSetup\n walletwright cache --wallet <${KINDS.join(\"|\")}> --seed \"<phrase>\" --password \"<pw>\" [--version <v>]\n\nOptions:\n --setup <file> A module whose default export is a WalletSetup (.ts works on modern Node)\n --wallet <kind> ${KINDS.join(\" | \")}\n --seed <phrase> seed phrase to import\n --password <pw> wallet password\n --version <v> pin an extension version\n --cache-dir <dir> cache directory (default: .walletwright)\n --headless build the cache headless\n -h, --help show this help\n\n--seed and --password are visible in shell history and process lists when passed as flags; use\ntest-only values, or prefer --setup <file> to keep them out of argv.\n`;\n\nconst parseFlags = (argv: Array<string>): Record<string, string | boolean> => {\n const flags: Record<string, string | boolean> = {};\n for (let i = 0; i < argv.length; i++) {\n const token = argv[i];\n if (!token?.startsWith(\"--\") && token !== \"-h\") {\n continue;\n }\n const key = token.replace(/^--?/v, \"\");\n const next = argv[i + 1];\n if (next && !next.startsWith(\"--\")) {\n flags[key] = next;\n i++;\n } else {\n flags[key] = true;\n }\n }\n return flags;\n};\n\nconst isNonEmptyString = (value: unknown): value is string =>\n typeof value === \"string\" && value.length > 0;\n\nconst isWalletSetup = (value: unknown): value is WalletSetup =>\n typeof value === \"object\" &&\n value !== null &&\n \"wallet\" in value &&\n typeof value.wallet === \"string\" &&\n isWalletKind(value.wallet) &&\n \"seedPhrase\" in value &&\n isNonEmptyString(value.seedPhrase) &&\n \"password\" in value &&\n isNonEmptyString(value.password);\n\nconst loadSetup = async (file: string): Promise<WalletSetup> => {\n const resolved = pathToFileURL(path.resolve(file)).href;\n const mod: unknown = await import(resolved);\n const setup =\n typeof mod === \"object\" && mod !== null && \"default\" in mod ? mod.default : undefined;\n if (!isWalletSetup(setup)) {\n throw new Error(`[walletwright] ${file} must default-export a WalletSetup`);\n }\n return setup;\n};\n\nconst resolveSetup = async (flags: Record<string, string | boolean>): Promise<WalletSetup> => {\n if (typeof flags.setup === \"string\") {\n const loaded = await loadSetup(flags.setup);\n return typeof flags[\"cache-dir\"] === \"string\"\n ? { ...loaded, cacheDir: flags[\"cache-dir\"] }\n : loaded;\n }\n const { password, seed, wallet } = flags;\n if (isNonEmptyString(wallet) && isNonEmptyString(seed) && isNonEmptyString(password)) {\n if (!isWalletKind(wallet)) {\n throw new Error(\n `[walletwright] unknown --wallet \"${wallet}\". Expected one of: ${Object.keys(wallets).join(\", \")}.`,\n );\n }\n return {\n password,\n seedPhrase: seed,\n wallet,\n ...(typeof flags.version === \"string\" ? { version: flags.version } : {}),\n ...(typeof flags[\"cache-dir\"] === \"string\" ? { cacheDir: flags[\"cache-dir\"] } : {}),\n };\n }\n throw new Error(\n \"[walletwright] provide --setup <file> or --wallet/--seed/--password. See --help.\",\n );\n};\n\nconst main = async (): Promise<void> => {\n const argv = process.argv.slice(2);\n const [command] = argv;\n const flags = parseFlags(argv);\n\n if (!command || command === \"help\" || flags.help !== undefined || flags.h !== undefined) {\n process.stdout.write(HELP);\n return;\n }\n if (command !== \"cache\") {\n throw new Error(`[walletwright] unknown command \"${command}\". Run \\`walletwright --help\\`.`);\n }\n\n const setup = await resolveSetup(flags);\n\n process.stdout.write(`[walletwright] building ${setup.wallet} cache…\\n`);\n const profileDir = await buildCache(setup, { headless: Boolean(flags.headless) });\n process.stdout.write(`[walletwright] cache ready: ${profileDir}\\n`);\n};\n\n/**\n * Node resolves `import.meta.url` through symlinks but leaves `process.argv[1]` as the caller\n * typed it, so comparing the two directly is false whenever the CLI is reached through a symlink,\n * which is every pnpm install: `node_modules/.bin/walletwright` execs the package through an\n * `.pnpm` symlink. The command then exits 0 having printed and done nothing.\n */\nconst isEntryPoint = (moduleUrl: string, entryPath: string | undefined): boolean =>\n entryPath !== undefined && moduleUrl === pathToFileURL(realpathSync(entryPath)).href;\n\nif (isEntryPoint(import.meta.url, process.argv[1])) {\n try {\n await main();\n } catch (error: unknown) {\n process.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n`);\n process.exit(1);\n }\n}\n\nexport { isEntryPoint, parseFlags, resolveSetup };\n"],"mappings":";;;;;;;AASA,MAAM,QAAQ,OAAO,KAAK,OAAO;AAEjC,MAAM,OAAO;;;;iCAIoB,MAAM,KAAK,GAAG,EAAE;;;;uBAI1B,MAAM,KAAK,KAAK,EAAE;;;;;;;;;;;AAYzC,MAAM,cAAc,SAA0D;CAC5E,MAAM,QAA0C,CAAC;CACjD,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,QAAQ,KAAK;EACnB,IAAI,CAAC,OAAO,WAAW,IAAI,KAAK,UAAU,MACxC;EAEF,MAAM,MAAM,MAAM,
|
|
1
|
+
{"version":3,"file":"cli.mjs","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { realpathSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\n\nimport { buildCache } from \"./internal/cache\";\nimport type { WalletSetup } from \"./types\";\nimport { isWalletKind, wallets } from \"./wallets/index\";\n\nconst KINDS = Object.keys(wallets);\n\nconst HELP = `walletwright: build the onboarded wallet cache for Playwright tests\n\nUsage:\n walletwright cache --setup <file> Build cache from a module's default-exported WalletSetup\n walletwright cache --wallet <${KINDS.join(\"|\")}> --seed \"<phrase>\" --password \"<pw>\" [--version <v>]\n\nOptions:\n --setup <file> A module whose default export is a WalletSetup (.ts works on modern Node)\n --wallet <kind> ${KINDS.join(\" | \")}\n --seed <phrase> seed phrase to import\n --password <pw> wallet password\n --version <v> pin an extension version\n --cache-dir <dir> cache directory (default: .walletwright)\n --headless build the cache headless\n -h, --help show this help\n\n--seed and --password are visible in shell history and process lists when passed as flags; use\ntest-only values, or prefer --setup <file> to keep them out of argv.\n`;\n\nconst parseFlags = (argv: Array<string>): Record<string, string | boolean> => {\n const flags: Record<string, string | boolean> = {};\n for (let i = 0; i < argv.length; i++) {\n const token = argv[i];\n if (!token?.startsWith(\"--\") && token !== \"-h\") {\n continue;\n }\n const key = token.replace(/^--?/v, \"\");\n const next = argv[i + 1];\n if (next && !next.startsWith(\"--\")) {\n flags[key] = next;\n i++;\n } else {\n flags[key] = true;\n }\n }\n return flags;\n};\n\nconst isNonEmptyString = (value: unknown): value is string =>\n typeof value === \"string\" && value.length > 0;\n\nconst isWalletSetup = (value: unknown): value is WalletSetup =>\n typeof value === \"object\" &&\n value !== null &&\n \"wallet\" in value &&\n typeof value.wallet === \"string\" &&\n isWalletKind(value.wallet) &&\n \"seedPhrase\" in value &&\n isNonEmptyString(value.seedPhrase) &&\n \"password\" in value &&\n isNonEmptyString(value.password);\n\nconst loadSetup = async (file: string): Promise<WalletSetup> => {\n const resolved = pathToFileURL(path.resolve(file)).href;\n const mod: unknown = await import(resolved);\n const setup =\n typeof mod === \"object\" && mod !== null && \"default\" in mod ? mod.default : undefined;\n if (!isWalletSetup(setup)) {\n throw new Error(`[walletwright] ${file} must default-export a WalletSetup`);\n }\n return setup;\n};\n\nconst resolveSetup = async (flags: Record<string, string | boolean>): Promise<WalletSetup> => {\n if (typeof flags.setup === \"string\") {\n const loaded = await loadSetup(flags.setup);\n return typeof flags[\"cache-dir\"] === \"string\"\n ? { ...loaded, cacheDir: flags[\"cache-dir\"] }\n : loaded;\n }\n const { password, seed, wallet } = flags;\n if (isNonEmptyString(wallet) && isNonEmptyString(seed) && isNonEmptyString(password)) {\n if (!isWalletKind(wallet)) {\n throw new Error(\n `[walletwright] unknown --wallet \"${wallet}\". Expected one of: ${Object.keys(wallets).join(\", \")}.`,\n );\n }\n return {\n password,\n seedPhrase: seed,\n wallet,\n ...(typeof flags.version === \"string\" ? { version: flags.version } : {}),\n ...(typeof flags[\"cache-dir\"] === \"string\" ? { cacheDir: flags[\"cache-dir\"] } : {}),\n };\n }\n throw new Error(\n \"[walletwright] provide --setup <file> or --wallet/--seed/--password. See --help.\",\n );\n};\n\nconst main = async (): Promise<void> => {\n const argv = process.argv.slice(2);\n const [command] = argv;\n const flags = parseFlags(argv);\n\n if (!command || command === \"help\" || flags.help !== undefined || flags.h !== undefined) {\n process.stdout.write(HELP);\n return;\n }\n if (command !== \"cache\") {\n throw new Error(`[walletwright] unknown command \"${command}\". Run \\`walletwright --help\\`.`);\n }\n\n const setup = await resolveSetup(flags);\n\n process.stdout.write(`[walletwright] building ${setup.wallet} cache…\\n`);\n const profileDir = await buildCache(setup, { headless: Boolean(flags.headless) });\n process.stdout.write(`[walletwright] cache ready: ${profileDir}\\n`);\n};\n\n/**\n * Node resolves `import.meta.url` through symlinks but leaves `process.argv[1]` as the caller\n * typed it, so comparing the two directly is false whenever the CLI is reached through a symlink,\n * which is every pnpm install: `node_modules/.bin/walletwright` execs the package through an\n * `.pnpm` symlink. The command then exits 0 having printed and done nothing.\n */\nconst isEntryPoint = (moduleUrl: string, entryPath: string | undefined): boolean =>\n entryPath !== undefined && moduleUrl === pathToFileURL(realpathSync(entryPath)).href;\n\nif (isEntryPoint(import.meta.url, process.argv[1])) {\n try {\n await main();\n } catch (error: unknown) {\n process.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n`);\n process.exit(1);\n }\n}\n\nexport { isEntryPoint, parseFlags, resolveSetup };\n"],"mappings":"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"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as wallets, c as extensionIdFromPath, d as placeApprovalWindow, f as profileKey, i as walletKindsByEcosystem, l as findNotificationPopup, m as waitUntil, n as extensionContextOptions, o as DEFAULT_CACHE_DIR, p as formatTimeout, s as DEFAULT_NOTIFICATION_MATCH, t as buildCache, u as hasNotificationPopup } from "./cache-
|
|
1
|
+
import { a as wallets, c as extensionIdFromPath, d as placeApprovalWindow, f as profileKey, i as walletKindsByEcosystem, l as findNotificationPopup, m as waitUntil, n as extensionContextOptions, o as DEFAULT_CACHE_DIR, p as formatTimeout, s as DEFAULT_NOTIFICATION_MATCH, t as buildCache, u as hasNotificationPopup } from "./cache-Bwv2i-hv.mjs";
|
|
2
2
|
import { cp, mkdtemp, rm } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { chromium, test } from "@playwright/test";
|
|
@@ -27,7 +27,7 @@ const createWallet = ({ context, definition, extensionId, home, password }) => {
|
|
|
27
27
|
* after an action and after a previous approval settles.
|
|
28
28
|
*/
|
|
29
29
|
const frontDapp = async () => {
|
|
30
|
-
await context.pages().find((page) =>
|
|
30
|
+
await context.pages().find((page) => new RegExp("^https?:", "v").test(page.url()) && !page.isClosed())?.bringToFront().catch(() => {});
|
|
31
31
|
};
|
|
32
32
|
/** Wait for the approval window the wallet opens for a pending request. */
|
|
33
33
|
const findApproval = (optional) => findNotificationPopup({
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["base"],"sources":["../src/internal/controller.ts","../src/internal/launch.ts","../src/fixtures.ts"],"sourcesContent":["import type { BrowserContext, Page } from \"@playwright/test\";\n\nimport type { Wallet, WalletActionContext, WalletDefinition } from \"../types\";\n\nimport {\n DEFAULT_NOTIFICATION_MATCH,\n findNotificationPopup,\n hasNotificationPopup,\n placeApprovalWindow,\n} from \"./utils\";\nimport { formatTimeout, waitUntil } from \"./wait\";\n\nconst POPUP_CLOSE_TIMEOUT_MS = 15_000;\n/** Optional approvals keep a short wait: \"no popup\" is a normal outcome, and the wait is latency. */\nconst OPTIONAL_POPUP_TIMEOUT_MS = 10_000;\n/** The MV3 service worker spawns a popup slowly once the wallet's own UI has been driven. */\nconst REQUIRED_POPUP_TIMEOUT_MS = 30_000;\ntype ResolveOptions = { optional?: boolean };\n\ntype CreateWalletOptions = {\n context: BrowserContext;\n definition: WalletDefinition;\n extensionId: string;\n /** The wallet's own page, left open after unlock, that the actions drive. */\n home: Page;\n password: string;\n};\n\n/** Build the runtime controller that drives an unlocked wallet against the dapp under test. */\nexport const createWallet = ({\n context,\n definition,\n extensionId,\n home,\n password,\n}: CreateWalletOptions): Wallet => {\n const match = definition.notificationMatch ?? DEFAULT_NOTIFICATION_MATCH;\n const ctx: WalletActionContext = { context, extensionId, home, password };\n\n /**\n * Make the dapp the active tab. While an extension page holds that spot, MetaMask renders a new\n * approval inline in it rather than opening a notification window, and the window that does open\n * shows the wallet home instead of the request. The wallet's own page can end up fronted both\n * after an action and after a previous approval settles.\n */\n const frontDapp = async (): Promise<void> => {\n const dapp = context.pages().find((page) => /^https?:/v.test(page.url()) && !page.isClosed());\n await dapp?.bringToFront().catch(() => {});\n };\n\n /** Wait for the approval window the wallet opens for a pending request. */\n const findApproval = (optional: boolean): Promise<Page | undefined> =>\n findNotificationPopup({\n approvalControls: definition.approvalControls,\n context,\n extensionId,\n match,\n timeoutMs: optional ? OPTIONAL_POPUP_TIMEOUT_MS : REQUIRED_POPUP_TIMEOUT_MS,\n });\n\n /** Drive the pending approval popup with `settle`, then wait for it to close. */\n const resolvePopup = async (\n settle: (popup: Page) => Promise<void>,\n { optional = false }: ResolveOptions,\n ): Promise<void> => {\n await frontDapp();\n const find = () => findApproval(optional);\n const popup = await find();\n if (!popup) {\n if (optional) {\n return; // e.g. Phantom auto-approves an already-trusted site (no popup)\n }\n // Naming the open pages separates \"the wallet never opened one\" from \"it opened one showing\n // something other than the request\", which look identical from the timeout alone.\n const open = context.pages().map((page) => page.url());\n throw new Error(\n `[walletwright] approval popup did not appear. Open pages: ${open.join(\" | \") || \"none\"}`,\n );\n }\n await placeApprovalWindow(popup); // it can open partly off a small or virtual display\n try {\n await settle(popup);\n } catch (error) {\n // The finder can grab the previous popup in its final moments (the window closes right after\n // its own approval resolves). If ours died under us, one fresh find gets the real popup.\n if (!popup.isClosed()) {\n throw error;\n }\n const fresh = await find();\n if (!fresh) {\n throw error;\n }\n await settle(fresh);\n }\n\n // Wait for the popup to close so the next approval doesn't grab a stale page.\n const closed = await waitUntil(() => !hasNotificationPopup(context, extensionId, match), {\n timeoutMs: POPUP_CLOSE_TIMEOUT_MS,\n });\n if (!optional && closed === undefined) {\n throw new Error(\n `[walletwright] approval popup did not close after ${formatTimeout(POPUP_CLOSE_TIMEOUT_MS)} (the approval may not have registered)`,\n );\n }\n };\n\n const unsupported = (name: string): Error =>\n new Error(`[walletwright] ${definition.extensionName} does not support ${name}()`);\n\n const approve = (options: ResolveOptions = {}) =>\n resolvePopup((popup) => definition.approve(popup, password), options);\n\n const reject = async (options: ResolveOptions = {}): Promise<void> => {\n const { reject: rejectPopup } = definition;\n if (!rejectPopup) {\n throw unsupported(\"reject\");\n }\n await resolvePopup((popup) => rejectPopup(popup), options);\n };\n\n /**\n * Bind an optional capability, or fail loudly naming the wallet and action. A wallet declares only\n * what has been driven against the real extension, so an undeclared action is a real gap rather\n * than something to swallow. Extra args (e.g. a network config) forward after the context.\n *\n * After the action, focus returns to the dapp (see `frontDapp`), since actions drive the wallet's\n * own page via `bringToFront`.\n */\n const action =\n <A extends ReadonlyArray<unknown>>(\n fn: ((ctx: WalletActionContext, ...args: A) => Promise<void>) | undefined,\n name: string,\n ) =>\n async (...args: A): Promise<void> => {\n if (!fn) {\n throw unsupported(name);\n }\n if (home.isClosed()) {\n throw new Error(`[walletwright] wallet home page is closed; cannot run ${name}()`);\n }\n // Actions drive the wallet's own page; front it first so clicks land on a visible tab, then\n // hand focus back to the dapp below so new approvals open as popups instead of inline.\n await home.bringToFront().catch(() => {});\n await fn(ctx, ...args);\n await frontDapp();\n };\n\n // A capability is declared once, on the *Api types in types.ts: WalletActions derives from those,\n // so a new method makes this object fail to compile until it is bound. Only the fn-to-slot pairing\n // below can still be wrong, and capability-wiring.test.ts guards that.\n return {\n accounts: {\n add: action(definition.actions?.accounts?.add, \"accounts.add\"),\n importPrivateKey: action(\n definition.actions?.accounts?.importPrivateKey,\n \"accounts.importPrivateKey\",\n ),\n rename: action(definition.actions?.accounts?.rename, \"accounts.rename\"),\n switch: action(definition.actions?.accounts?.switch, \"accounts.switch\"),\n },\n approve,\n confirmSignature: () => approve(),\n confirmTransaction: () => approve(),\n // Connect may auto-approve on some wallets, so a missing popup is not an error here.\n connectToDapp: () => approve({ optional: true }),\n extensionId,\n home,\n network: {\n add: action(definition.actions?.network?.add, \"network.add\"),\n switch: action(definition.actions?.network?.switch, \"network.switch\"),\n },\n reject,\n rejectConnection: () => reject(),\n rejectSignature: () => reject(),\n rejectTransaction: () => reject(),\n settings: {\n lock: action(definition.actions?.settings?.lock, \"settings.lock\"),\n unlock: action(definition.actions?.settings?.unlock, \"settings.unlock\"),\n },\n };\n};\n","import { existsSync } from \"node:fs\";\nimport { cp, mkdtemp, rm } from \"node:fs/promises\";\nimport os from \"node:os\";\nimport path from \"node:path\";\n\nimport { type BrowserContext, chromium, type Page } from \"@playwright/test\";\n\nimport type { Wallet, WalletSetup } from \"../types\";\nimport { wallets } from \"../wallets/index\";\n\nimport { extensionContextOptions } from \"./chromium\";\nimport { createWallet } from \"./controller\";\nimport { DEFAULT_CACHE_DIR, extensionIdFromPath, profileKey } from \"./utils\";\n\nexport type LaunchedWallet = {\n context: BrowserContext;\n wallet: Wallet;\n};\n\n/**\n * Drop the tabs nobody drives: Chromium's initial `about:blank` and the extension's own auto-opened\n * tab. We navigate a tab of our own to the wallet instead of adopting that one (it's unreliable, see\n * `reachUnlockScreen`), which otherwise leaves a second, still-locked wallet tab open all run.\n * Approval popups are unaffected: they're matched by URL, not by being the only extension page.\n */\nconst closeStrayPages = async (context: BrowserContext, home: Page): Promise<void> => {\n const isStray = (page: Page): boolean =>\n page !== home &&\n !page.isClosed() &&\n (page.url() === \"about:blank\" || page.url().startsWith(\"chrome-extension://\"));\n\n const closing: Array<Promise<void>> = [];\n for (const page of context.pages()) {\n if (isStray(page)) {\n closing.push(page.close());\n }\n }\n await Promise.allSettled(closing);\n};\n\n/**\n * Launch a fresh persistent context from the onboarded cache and return an unlocked wallet\n * controller. Headed by default; `headless` needs the wallet to declare `headlessApprovals`, since\n * some wallets create an approval window headless that is never exposed as a page. Outside\n * Playwright fixtures, `context.close()` is yours.\n */\nexport const launchWallet = async (\n setup: WalletSetup,\n { headless = false }: { headless?: boolean } = {},\n): Promise<LaunchedWallet> => {\n const definition = wallets[setup.wallet];\n // A wallet whose approval window never surfaces headless reaches no approval at all, and would\n // fail 30s later at the first connect with nothing pointing back at the mode as the cause.\n if (headless && definition.headlessApprovals !== true) {\n throw new Error(\n `[walletwright] ${definition.extensionName} has no verified headless approval flow; run this suite headed (\\`use: { headless: false }\\`, or \\`--headed\\`).`,\n );\n }\n const cacheDir = path.resolve(setup.cacheDir ?? DEFAULT_CACHE_DIR);\n const profileDir = path.join(cacheDir, profileKey(setup));\n if (!existsSync(profileDir)) {\n throw new Error(\n `[walletwright] no cache for this setup at ${profileDir}. Build it first with buildCache() or \\`walletwright cache\\`.`,\n );\n }\n\n const extensionPath = await definition.prepareExtension(cacheDir, setup.version);\n\n // Run from a throwaway copy so the cache stays pristine and parallel runs don't share a profile.\n const runDir = await mkdtemp(path.join(os.tmpdir(), \"walletwright-\"));\n await cp(profileDir, runDir, { recursive: true });\n\n const context = await chromium.launchPersistentContext(\n runDir,\n extensionContextOptions(extensionPath, headless),\n );\n\n // The throwaway profile copy is only needed while the context is live; drop it on close so a long\n // suite (workers x specs x retries) doesn't fill the temp dir with profile copies.\n context.on(\"close\", async () => {\n await rm(runDir, { force: true, recursive: true }).catch(() => {});\n });\n\n const extensionId = extensionIdFromPath(extensionPath);\n\n try {\n await definition.prepareContext?.(context);\n // Kept open (not closed after unlock): the settings/network/account actions drive this page.\n const home = await definition.reachUnlockScreen(context, extensionId);\n await definition.unlock(home, setup.password);\n await closeStrayPages(context, home);\n\n return {\n context,\n wallet: createWallet({\n context,\n definition,\n extensionId,\n home,\n password: setup.password,\n }),\n };\n } catch (error) {\n // Fires the context.on(\"close\") handler above, which removes the throwaway runDir.\n await context.close().catch(() => {});\n throw error;\n }\n};\n","import { test as base } from \"@playwright/test\";\n\nimport { type LaunchedWallet, launchWallet } from \"./internal/launch\";\nimport type { Wallet, WalletSetup } from \"./types\";\n\nexport type WalletFixtures = {\n wallet: Wallet;\n};\n\n/** One launch per test, which the `context` and `wallet` fixtures then read from. */\ntype LaunchFixture = {\n walletLaunch: LaunchedWallet;\n};\n\n/**\n * Build a Playwright `test` whose `context`/`page` run inside a persistent Chromium with the wallet\n * loaded and unlocked, exposing a `wallet` fixture to approve connect/sign popups.\n *\n * ```ts\n * const test = createWalletFixtures(setup);\n * test(\"connect\", async ({ page, wallet }) => {\n * await page.goto(\"/\");\n * await page.getByRole(\"button\", { name: \"Connect\" }).click();\n * await wallet.connectToDapp();\n * });\n * ```\n */\nexport const createWalletFixtures = (setup: WalletSetup) =>\n base.extend<LaunchFixture & WalletFixtures>({\n context: async ({ walletLaunch }, use) => {\n await use(walletLaunch.context);\n },\n wallet: async ({ walletLaunch }, use) => {\n await use(walletLaunch.wallet);\n },\n // Depends only on Playwright's `headless` option (so `--headed` and `use: { headless }` reach\n // the wallet): its own `browser` fixture would launch a second Chromium that no test ever\n // drives, since the wallet needs a persistent context of its own.\n walletLaunch: async ({ headless }, use) => {\n const launched = await launchWallet(setup, { headless });\n await use(launched);\n await launched.context.close();\n },\n });\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["base"],"sources":["../src/internal/controller.ts","../src/internal/launch.ts","../src/fixtures.ts"],"sourcesContent":["import type { BrowserContext, Page } from \"@playwright/test\";\n\nimport type { Wallet, WalletActionContext, WalletDefinition } from \"../types\";\n\nimport {\n DEFAULT_NOTIFICATION_MATCH,\n findNotificationPopup,\n hasNotificationPopup,\n placeApprovalWindow,\n} from \"./utils\";\nimport { formatTimeout, waitUntil } from \"./wait\";\n\nconst POPUP_CLOSE_TIMEOUT_MS = 15_000;\n/** Optional approvals keep a short wait: \"no popup\" is a normal outcome, and the wait is latency. */\nconst OPTIONAL_POPUP_TIMEOUT_MS = 10_000;\n/** The MV3 service worker spawns a popup slowly once the wallet's own UI has been driven. */\nconst REQUIRED_POPUP_TIMEOUT_MS = 30_000;\ntype ResolveOptions = { optional?: boolean };\n\ntype CreateWalletOptions = {\n context: BrowserContext;\n definition: WalletDefinition;\n extensionId: string;\n /** The wallet's own page, left open after unlock, that the actions drive. */\n home: Page;\n password: string;\n};\n\n/** Build the runtime controller that drives an unlocked wallet against the dapp under test. */\nexport const createWallet = ({\n context,\n definition,\n extensionId,\n home,\n password,\n}: CreateWalletOptions): Wallet => {\n const match = definition.notificationMatch ?? DEFAULT_NOTIFICATION_MATCH;\n const ctx: WalletActionContext = { context, extensionId, home, password };\n\n /**\n * Make the dapp the active tab. While an extension page holds that spot, MetaMask renders a new\n * approval inline in it rather than opening a notification window, and the window that does open\n * shows the wallet home instead of the request. The wallet's own page can end up fronted both\n * after an action and after a previous approval settles.\n */\n const frontDapp = async (): Promise<void> => {\n const dapp = context.pages().find((page) => /^https?:/v.test(page.url()) && !page.isClosed());\n await dapp?.bringToFront().catch(() => {});\n };\n\n /** Wait for the approval window the wallet opens for a pending request. */\n const findApproval = (optional: boolean): Promise<Page | undefined> =>\n findNotificationPopup({\n approvalControls: definition.approvalControls,\n context,\n extensionId,\n match,\n timeoutMs: optional ? OPTIONAL_POPUP_TIMEOUT_MS : REQUIRED_POPUP_TIMEOUT_MS,\n });\n\n /** Drive the pending approval popup with `settle`, then wait for it to close. */\n const resolvePopup = async (\n settle: (popup: Page) => Promise<void>,\n { optional = false }: ResolveOptions,\n ): Promise<void> => {\n await frontDapp();\n const find = () => findApproval(optional);\n const popup = await find();\n if (!popup) {\n if (optional) {\n return; // e.g. Phantom auto-approves an already-trusted site (no popup)\n }\n const open = context.pages().map((page) => page.url());\n throw new Error(\n `[walletwright] approval popup did not appear. Open pages: ${open.join(\" | \") || \"none\"}`,\n );\n }\n await placeApprovalWindow(popup); // it can open partly off a small or virtual display\n try {\n await settle(popup);\n } catch (error) {\n if (!popup.isClosed()) {\n throw error;\n }\n const fresh = await find();\n if (!fresh) {\n throw error;\n }\n await settle(fresh);\n }\n\n const closed = await waitUntil(() => !hasNotificationPopup(context, extensionId, match), {\n timeoutMs: POPUP_CLOSE_TIMEOUT_MS,\n });\n if (!optional && closed === undefined) {\n throw new Error(\n `[walletwright] approval popup did not close after ${formatTimeout(POPUP_CLOSE_TIMEOUT_MS)} (the approval may not have registered)`,\n );\n }\n };\n\n const unsupported = (name: string): Error =>\n new Error(`[walletwright] ${definition.extensionName} does not support ${name}()`);\n\n const approve = (options: ResolveOptions = {}) =>\n resolvePopup((popup) => definition.approve(popup, password), options);\n\n const reject = async (options: ResolveOptions = {}): Promise<void> => {\n const { reject: rejectPopup } = definition;\n if (!rejectPopup) {\n throw unsupported(\"reject\");\n }\n await resolvePopup((popup) => rejectPopup(popup), options);\n };\n\n /**\n * Bind an optional capability, or fail loudly naming the wallet and action. A wallet declares only\n * what has been driven against the real extension, so an undeclared action is a real gap rather\n * than something to swallow. Extra args (e.g. a network config) forward after the context.\n *\n * After the action, focus returns to the dapp (see `frontDapp`), since actions drive the wallet's\n * own page via `bringToFront`.\n */\n const action =\n <A extends ReadonlyArray<unknown>>(\n fn: ((ctx: WalletActionContext, ...args: A) => Promise<void>) | undefined,\n name: string,\n ) =>\n async (...args: A): Promise<void> => {\n if (!fn) {\n throw unsupported(name);\n }\n if (home.isClosed()) {\n throw new Error(`[walletwright] wallet home page is closed; cannot run ${name}()`);\n }\n await home.bringToFront().catch(() => {});\n await fn(ctx, ...args);\n await frontDapp();\n };\n\n return {\n accounts: {\n add: action(definition.actions?.accounts?.add, \"accounts.add\"),\n importPrivateKey: action(\n definition.actions?.accounts?.importPrivateKey,\n \"accounts.importPrivateKey\",\n ),\n rename: action(definition.actions?.accounts?.rename, \"accounts.rename\"),\n switch: action(definition.actions?.accounts?.switch, \"accounts.switch\"),\n },\n approve,\n confirmSignature: () => approve(),\n confirmTransaction: () => approve(),\n connectToDapp: () => approve({ optional: true }),\n extensionId,\n home,\n network: {\n add: action(definition.actions?.network?.add, \"network.add\"),\n switch: action(definition.actions?.network?.switch, \"network.switch\"),\n },\n reject,\n rejectConnection: () => reject(),\n rejectSignature: () => reject(),\n rejectTransaction: () => reject(),\n settings: {\n lock: action(definition.actions?.settings?.lock, \"settings.lock\"),\n unlock: action(definition.actions?.settings?.unlock, \"settings.unlock\"),\n },\n };\n};\n","import { existsSync } from \"node:fs\";\nimport { cp, mkdtemp, rm } from \"node:fs/promises\";\nimport os from \"node:os\";\nimport path from \"node:path\";\n\nimport { type BrowserContext, chromium, type Page } from \"@playwright/test\";\n\nimport type { Wallet, WalletSetup } from \"../types\";\nimport { wallets } from \"../wallets/index\";\n\nimport { extensionContextOptions } from \"./chromium\";\nimport { createWallet } from \"./controller\";\nimport { DEFAULT_CACHE_DIR, extensionIdFromPath, profileKey } from \"./utils\";\n\nexport type LaunchedWallet = {\n context: BrowserContext;\n wallet: Wallet;\n};\n\n/**\n * Drop the tabs nobody drives: Chromium's initial `about:blank` and the extension's own auto-opened\n * tab. We navigate a tab of our own to the wallet instead of adopting that one (it's unreliable, see\n * `reachUnlockScreen`), which otherwise leaves a second, still-locked wallet tab open all run.\n * Approval popups are unaffected: they're matched by URL, not by being the only extension page.\n */\nconst closeStrayPages = async (context: BrowserContext, home: Page): Promise<void> => {\n const isStray = (page: Page): boolean =>\n page !== home &&\n !page.isClosed() &&\n (page.url() === \"about:blank\" || page.url().startsWith(\"chrome-extension://\"));\n\n const closing: Array<Promise<void>> = [];\n for (const page of context.pages()) {\n if (isStray(page)) {\n closing.push(page.close());\n }\n }\n await Promise.allSettled(closing);\n};\n\n/**\n * Launch a fresh persistent context from the onboarded cache and return an unlocked wallet\n * controller. Headed by default; `headless` needs the wallet to declare `headlessApprovals`, since\n * some wallets create an approval window headless that is never exposed as a page. Outside\n * Playwright fixtures, `context.close()` is yours.\n */\nexport const launchWallet = async (\n setup: WalletSetup,\n { headless = false }: { headless?: boolean } = {},\n): Promise<LaunchedWallet> => {\n const definition = wallets[setup.wallet];\n if (headless && definition.headlessApprovals !== true) {\n throw new Error(\n `[walletwright] ${definition.extensionName} has no verified headless approval flow; run this suite headed (\\`use: { headless: false }\\`, or \\`--headed\\`).`,\n );\n }\n const cacheDir = path.resolve(setup.cacheDir ?? DEFAULT_CACHE_DIR);\n const profileDir = path.join(cacheDir, profileKey(setup));\n if (!existsSync(profileDir)) {\n throw new Error(\n `[walletwright] no cache for this setup at ${profileDir}. Build it first with buildCache() or \\`walletwright cache\\`.`,\n );\n }\n\n const extensionPath = await definition.prepareExtension(cacheDir, setup.version);\n\n const runDir = await mkdtemp(path.join(os.tmpdir(), \"walletwright-\"));\n await cp(profileDir, runDir, { recursive: true });\n\n const context = await chromium.launchPersistentContext(\n runDir,\n extensionContextOptions(extensionPath, headless),\n );\n\n context.on(\"close\", async () => {\n await rm(runDir, { force: true, recursive: true }).catch(() => {});\n });\n\n const extensionId = extensionIdFromPath(extensionPath);\n\n try {\n await definition.prepareContext?.(context);\n const home = await definition.reachUnlockScreen(context, extensionId);\n await definition.unlock(home, setup.password);\n await closeStrayPages(context, home);\n\n return {\n context,\n wallet: createWallet({\n context,\n definition,\n extensionId,\n home,\n password: setup.password,\n }),\n };\n } catch (error) {\n await context.close().catch(() => {});\n throw error;\n }\n};\n","import { test as base } from \"@playwright/test\";\n\nimport { type LaunchedWallet, launchWallet } from \"./internal/launch\";\nimport type { Wallet, WalletSetup } from \"./types\";\n\nexport type WalletFixtures = {\n wallet: Wallet;\n};\n\n/** One launch per test, which the `context` and `wallet` fixtures then read from. */\ntype LaunchFixture = {\n walletLaunch: LaunchedWallet;\n};\n\n/**\n * Build a Playwright `test` whose `context`/`page` run inside a persistent Chromium with the wallet\n * loaded and unlocked, exposing a `wallet` fixture to approve connect/sign popups.\n *\n * ```ts\n * const test = createWalletFixtures(setup);\n * test(\"connect\", async ({ page, wallet }) => {\n * await page.goto(\"/\");\n * await page.getByRole(\"button\", { name: \"Connect\" }).click();\n * await wallet.connectToDapp();\n * });\n * ```\n */\nexport const createWalletFixtures = (setup: WalletSetup) =>\n base.extend<LaunchFixture & WalletFixtures>({\n context: async ({ walletLaunch }, use) => {\n await use(walletLaunch.context);\n },\n wallet: async ({ walletLaunch }, use) => {\n await use(walletLaunch.wallet);\n },\n walletLaunch: async ({ headless }, use) => {\n const launched = await launchWallet(setup, { headless });\n await use(launched);\n await launched.context.close();\n },\n });\n"],"mappings":"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"}
|
package/dist/mock-standard.mjs
CHANGED
|
@@ -63,7 +63,7 @@ const installMockStandardWallet = async (target, options = {}) => {
|
|
|
63
63
|
}
|
|
64
64
|
await target.addInitScript(([binding, info]) => {
|
|
65
65
|
const call = window[binding];
|
|
66
|
-
const publicKeyBytes = Uint8Array.from((info.publicKeyHex.match(
|
|
66
|
+
const publicKeyBytes = Uint8Array.from((info.publicKeyHex.match(new RegExp(".{2}", "gv")) ?? []).map((byte) => Number.parseInt(byte, 16)));
|
|
67
67
|
const account = {
|
|
68
68
|
address: info.address,
|
|
69
69
|
chains: info.chains,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mock-standard.mjs","names":[],"sources":["../src/mock-standard.ts"],"sourcesContent":["import { createPrivateKey, createPublicKey, sign } from \"node:crypto\";\n\nimport type { BrowserContext, Page } from \"@playwright/test\";\n\n/**\n * A headless Wallet-Standard fake for Solana dapp tests that don't need a real extension. It\n * hand-rolls the Wallet-Standard registration protocol (no browser-side library) and answers\n * `standard:connect` plus `solana:signMessage`, signing with a real ed25519 key so signatures\n * verify. Isolated from the EVM `mock.ts`; `@walletwright/core/mock-standard` is its own entry\n * point.\n *\n * ed25519 signing runs Node-side (viem is secp256k1-only), reusing the same exposeFunction bridge\n * pattern as the EVM mock. No extra dependency: Node's built-in `crypto` signs ed25519 natively.\n */\ntype MockEcosystem = \"svm\";\n\ntype MockStandardWalletOptions = {\n ecosystem?: MockEcosystem;\n /** Wallet name announced over Wallet Standard. */\n name?: string;\n /** 32-byte ed25519 seed as hex. Defaults to a fixed test seed so the address is stable. */\n seedHex?: string;\n};\n\ntype MockStandardAccount = {\n address: string;\n /** Raw 32-byte ed25519 public key, hex-encoded. */\n publicKeyHex: string;\n};\n\n// A fixed 32-byte seed so the derived Solana address is stable across runs.\nconst DEFAULT_SEED_HEX = \"0101010101010101010101010101010101010101010101010101010101010101\";\n\nconst PKCS8_ED25519_PREFIX = Buffer.from(\"302e020100300506032b657004220420\", \"hex\");\n\nconst BASE58_ALPHABET = \"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\";\n\nconst encodeBase58 = (bytes: Uint8Array): string => {\n let leadingZeros = 0;\n while (leadingZeros < bytes.length && bytes[leadingZeros] === 0) {\n leadingZeros += 1;\n }\n const digits: Array<number> = [];\n for (const byte of bytes) {\n let carry = byte;\n for (let index = 0; index < digits.length; index += 1) {\n carry += digits[index] * 256;\n digits[index] = carry % 58;\n carry = Math.floor(carry / 58);\n }\n while (carry > 0) {\n digits.push(carry % 58);\n carry = Math.floor(carry / 58);\n }\n }\n const prefix = \"1\".repeat(leadingZeros);\n const body = digits\n .toReversed()\n .map((digit) => BASE58_ALPHABET[digit])\n .join(\"\");\n return prefix + body;\n};\n\nconst privateKeyFromSeed = (seed: Buffer) =>\n createPrivateKey({\n format: \"der\",\n key: Buffer.concat([PKCS8_ED25519_PREFIX, seed]),\n type: \"pkcs8\",\n });\n\nconst rawPublicKey = (seed: Buffer): Buffer => {\n const spki = createPublicKey(privateKeyFromSeed(seed)).export({ format: \"der\", type: \"spki\" });\n return spki.subarray(-32);\n};\n\nconst signEd25519 = (seed: Buffer, message: Uint8Array): Array<number> => [\n ...sign(null, Buffer.from(message), privateKeyFromSeed(seed)),\n];\n\nconst SVM_CHAINS = [\"solana:mainnet\", \"solana:devnet\"] as const;\n\ntype BridgeRequest = { message?: Array<number>; method: string };\n\nconst createStandardHandler =\n (seed: Buffer) =>\n ({ message = [], method }: BridgeRequest): Promise<Array<number>> => {\n switch (method) {\n case \"solana:signMessage\": {\n return Promise.resolve(signEd25519(seed, Uint8Array.from(message)));\n }\n default: {\n return Promise.reject(\n new Error(`[walletwright/mock-standard] unsupported method: ${method}`),\n );\n }\n }\n };\n\n/**\n * Install the headless Wallet-Standard mock for Solana connect + sign on a context (every page) or a\n * single page. Call before `goto`, so the wallet is registered when the dapp discovers wallets.\n * Returns the account it announces so a test can verify signatures against the public key.\n */\nconst installMockStandardWallet = async (\n target: BrowserContext | Page,\n options: MockStandardWalletOptions = {},\n): Promise<MockStandardAccount> => {\n const { ecosystem = \"svm\", name = \"Walletwright Mock\", seedHex = DEFAULT_SEED_HEX } = options;\n if (ecosystem !== \"svm\") {\n throw new Error(\n `[walletwright/mock-standard] prototype supports only svm, got ${String(ecosystem)}`,\n );\n }\n const seed = Buffer.from(seedHex, \"hex\");\n if (seed.length !== 32) {\n throw new Error(\"[walletwright/mock-standard] seedHex must be 32 bytes\");\n }\n const publicKey = rawPublicKey(seed);\n const address = encodeBase58(publicKey);\n const publicKeyHex = publicKey.toString(\"hex\");\n const handle = createStandardHandler(seed);\n\n const bindingName = \"__walletwrightMockStandardSign\";\n try {\n await target.exposeFunction(bindingName, (rpc: BridgeRequest) => handle(rpc));\n } catch (error) {\n if (!(error instanceof Error && error.message.includes(\"already registered\"))) {\n throw error;\n }\n }\n\n await target.addInitScript(\n ([binding, info]) => {\n const call = (\n window as unknown as Record<string, (rpc: BridgeRequest) => Promise<Array<number>>>\n )[binding];\n const publicKeyBytes = Uint8Array.from(\n (info.publicKeyHex.match(/.{2}/gv) ?? []).map((byte) => Number.parseInt(byte, 16)),\n );\n const account = {\n address: info.address,\n chains: info.chains,\n features: [\"solana:signMessage\"],\n label: info.name,\n publicKey: publicKeyBytes,\n };\n const wallet = {\n accounts: [account],\n chains: info.chains,\n features: {\n \"solana:signMessage\": {\n signMessage: (...inputs: Array<{ message: Uint8Array }>) =>\n Promise.all(\n inputs.map(async ({ message }) => ({\n signature: Uint8Array.from(\n await call({ message: [...message], method: \"solana:signMessage\" }),\n ),\n signedMessage: message,\n })),\n ),\n version: \"1.0.0\",\n },\n \"standard:connect\": {\n connect: () => Promise.resolve({ accounts: [account] }),\n version: \"1.0.0\",\n },\n \"standard:events\": {\n on: () => () => {},\n version: \"1.0.0\",\n },\n },\n icon: info.icon,\n name: info.name,\n version: \"1.0.0\" as const,\n };\n\n const callback = (api: { register: (w: typeof wallet) => void }) => {\n api.register(wallet);\n };\n try {\n window.dispatchEvent(\n new CustomEvent(\"wallet-standard:register-wallet\", { detail: callback }),\n );\n } catch {\n // The app may not be listening yet; the app-ready handler below covers that case.\n }\n window.addEventListener(\"wallet-standard:app-ready\", (event) => {\n callback((event as CustomEvent<{ register: (w: typeof wallet) => void }>).detail);\n });\n },\n [\n bindingName,\n {\n address,\n chains: [...SVM_CHAINS],\n icon: \"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'/>\",\n name,\n publicKeyHex,\n },\n ] as const,\n );\n\n return { address, publicKeyHex };\n};\n\nexport { createStandardHandler, encodeBase58, installMockStandardWallet, privateKeyFromSeed };\nexport type { MockEcosystem, MockStandardAccount, MockStandardWalletOptions };\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mock-standard.mjs","names":[],"sources":["../src/mock-standard.ts"],"sourcesContent":["import { createPrivateKey, createPublicKey, sign } from \"node:crypto\";\n\nimport type { BrowserContext, Page } from \"@playwright/test\";\n\n/**\n * A headless Wallet-Standard fake for Solana dapp tests that don't need a real extension. It\n * hand-rolls the Wallet-Standard registration protocol (no browser-side library) and answers\n * `standard:connect` plus `solana:signMessage`, signing with a real ed25519 key so signatures\n * verify. Isolated from the EVM `mock.ts`; `@walletwright/core/mock-standard` is its own entry\n * point.\n *\n * ed25519 signing runs Node-side (viem is secp256k1-only), reusing the same exposeFunction bridge\n * pattern as the EVM mock. No extra dependency: Node's built-in `crypto` signs ed25519 natively.\n */\ntype MockEcosystem = \"svm\";\n\ntype MockStandardWalletOptions = {\n ecosystem?: MockEcosystem;\n /** Wallet name announced over Wallet Standard. */\n name?: string;\n /** 32-byte ed25519 seed as hex. Defaults to a fixed test seed so the address is stable. */\n seedHex?: string;\n};\n\ntype MockStandardAccount = {\n address: string;\n /** Raw 32-byte ed25519 public key, hex-encoded. */\n publicKeyHex: string;\n};\n\nconst DEFAULT_SEED_HEX = \"0101010101010101010101010101010101010101010101010101010101010101\";\n\nconst PKCS8_ED25519_PREFIX = Buffer.from(\"302e020100300506032b657004220420\", \"hex\");\n\nconst BASE58_ALPHABET = \"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\";\n\nconst encodeBase58 = (bytes: Uint8Array): string => {\n let leadingZeros = 0;\n while (leadingZeros < bytes.length && bytes[leadingZeros] === 0) {\n leadingZeros += 1;\n }\n const digits: Array<number> = [];\n for (const byte of bytes) {\n let carry = byte;\n for (let index = 0; index < digits.length; index += 1) {\n carry += digits[index] * 256;\n digits[index] = carry % 58;\n carry = Math.floor(carry / 58);\n }\n while (carry > 0) {\n digits.push(carry % 58);\n carry = Math.floor(carry / 58);\n }\n }\n const prefix = \"1\".repeat(leadingZeros);\n const body = digits\n .toReversed()\n .map((digit) => BASE58_ALPHABET[digit])\n .join(\"\");\n return prefix + body;\n};\n\nconst privateKeyFromSeed = (seed: Buffer) =>\n createPrivateKey({\n format: \"der\",\n key: Buffer.concat([PKCS8_ED25519_PREFIX, seed]),\n type: \"pkcs8\",\n });\n\nconst rawPublicKey = (seed: Buffer): Buffer => {\n const spki = createPublicKey(privateKeyFromSeed(seed)).export({ format: \"der\", type: \"spki\" });\n return spki.subarray(-32);\n};\n\nconst signEd25519 = (seed: Buffer, message: Uint8Array): Array<number> => [\n ...sign(null, Buffer.from(message), privateKeyFromSeed(seed)),\n];\n\nconst SVM_CHAINS = [\"solana:mainnet\", \"solana:devnet\"] as const;\n\ntype BridgeRequest = { message?: Array<number>; method: string };\n\nconst createStandardHandler =\n (seed: Buffer) =>\n ({ message = [], method }: BridgeRequest): Promise<Array<number>> => {\n switch (method) {\n case \"solana:signMessage\": {\n return Promise.resolve(signEd25519(seed, Uint8Array.from(message)));\n }\n default: {\n return Promise.reject(\n new Error(`[walletwright/mock-standard] unsupported method: ${method}`),\n );\n }\n }\n };\n\n/**\n * Install the headless Wallet-Standard mock for Solana connect + sign on a context (every page) or a\n * single page. Call before `goto`, so the wallet is registered when the dapp discovers wallets.\n * Returns the account it announces so a test can verify signatures against the public key.\n */\nconst installMockStandardWallet = async (\n target: BrowserContext | Page,\n options: MockStandardWalletOptions = {},\n): Promise<MockStandardAccount> => {\n const { ecosystem = \"svm\", name = \"Walletwright Mock\", seedHex = DEFAULT_SEED_HEX } = options;\n if (ecosystem !== \"svm\") {\n throw new Error(\n `[walletwright/mock-standard] prototype supports only svm, got ${String(ecosystem)}`,\n );\n }\n const seed = Buffer.from(seedHex, \"hex\");\n if (seed.length !== 32) {\n throw new Error(\"[walletwright/mock-standard] seedHex must be 32 bytes\");\n }\n const publicKey = rawPublicKey(seed);\n const address = encodeBase58(publicKey);\n const publicKeyHex = publicKey.toString(\"hex\");\n const handle = createStandardHandler(seed);\n\n const bindingName = \"__walletwrightMockStandardSign\";\n try {\n await target.exposeFunction(bindingName, (rpc: BridgeRequest) => handle(rpc));\n } catch (error) {\n if (!(error instanceof Error && error.message.includes(\"already registered\"))) {\n throw error;\n }\n }\n\n await target.addInitScript(\n ([binding, info]) => {\n const call = (\n window as unknown as Record<string, (rpc: BridgeRequest) => Promise<Array<number>>>\n )[binding];\n const publicKeyBytes = Uint8Array.from(\n (info.publicKeyHex.match(/.{2}/gv) ?? []).map((byte) => Number.parseInt(byte, 16)),\n );\n const account = {\n address: info.address,\n chains: info.chains,\n features: [\"solana:signMessage\"],\n label: info.name,\n publicKey: publicKeyBytes,\n };\n const wallet = {\n accounts: [account],\n chains: info.chains,\n features: {\n \"solana:signMessage\": {\n signMessage: (...inputs: Array<{ message: Uint8Array }>) =>\n Promise.all(\n inputs.map(async ({ message }) => ({\n signature: Uint8Array.from(\n await call({ message: [...message], method: \"solana:signMessage\" }),\n ),\n signedMessage: message,\n })),\n ),\n version: \"1.0.0\",\n },\n \"standard:connect\": {\n connect: () => Promise.resolve({ accounts: [account] }),\n version: \"1.0.0\",\n },\n \"standard:events\": {\n on: () => () => {},\n version: \"1.0.0\",\n },\n },\n icon: info.icon,\n name: info.name,\n version: \"1.0.0\" as const,\n };\n\n const callback = (api: { register: (w: typeof wallet) => void }) => {\n api.register(wallet);\n };\n try {\n window.dispatchEvent(\n new CustomEvent(\"wallet-standard:register-wallet\", { detail: callback }),\n );\n } catch {\n // The app may not be listening yet; the app-ready handler below covers that case.\n }\n window.addEventListener(\"wallet-standard:app-ready\", (event) => {\n callback((event as CustomEvent<{ register: (w: typeof wallet) => void }>).detail);\n });\n },\n [\n bindingName,\n {\n address,\n chains: [...SVM_CHAINS],\n icon: \"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'/>\",\n name,\n publicKeyHex,\n },\n ] as const,\n );\n\n return { address, publicKeyHex };\n};\n\nexport { createStandardHandler, encodeBase58, installMockStandardWallet, privateKeyFromSeed };\nexport type { MockEcosystem, MockStandardAccount, MockStandardWalletOptions };\n"],"mappings":";;;AA8BA,MAAM,mBAAmB;AAEzB,MAAM,uBAAuB,OAAO,KAAK,oCAAoC,KAAK;AAElF,MAAM,kBAAkB;AAExB,MAAM,gBAAgB,UAA8B;CAClD,IAAI,eAAe;CACnB,OAAO,eAAe,MAAM,UAAU,MAAM,kBAAkB,GAC5D,gBAAgB;CAElB,MAAM,SAAwB,CAAC;CAC/B,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,QAAQ;EACZ,KAAK,IAAI,QAAQ,GAAG,QAAQ,OAAO,QAAQ,SAAS,GAAG;GACrD,SAAS,OAAO,SAAS;GACzB,OAAO,SAAS,QAAQ;GACxB,QAAQ,KAAK,MAAM,QAAQ,EAAE;EAC/B;EACA,OAAO,QAAQ,GAAG;GAChB,OAAO,KAAK,QAAQ,EAAE;GACtB,QAAQ,KAAK,MAAM,QAAQ,EAAE;EAC/B;CACF;CAMA,OALe,IAAI,OAAO,YAKd,IAJC,OACV,WAAW,CAAC,CACZ,KAAK,UAAU,gBAAgB,MAAM,CAAC,CACtC,KAAK,EACW;AACrB;AAEA,MAAM,sBAAsB,SAC1B,iBAAiB;CACf,QAAQ;CACR,KAAK,OAAO,OAAO,CAAC,sBAAsB,IAAI,CAAC;CAC/C,MAAM;AACR,CAAC;AAEH,MAAM,gBAAgB,SAAyB;CAE7C,OADa,gBAAgB,mBAAmB,IAAI,CAAC,CAAC,CAAC,OAAO;EAAE,QAAQ;EAAO,MAAM;CAAO,CAClF,CAAC,CAAC,SAAS,GAAG;AAC1B;AAEA,MAAM,eAAe,MAAc,YAAuC,CACxE,GAAG,KAAK,MAAM,OAAO,KAAK,OAAO,GAAG,mBAAmB,IAAI,CAAC,CAC9D;AAEA,MAAM,aAAa,CAAC,kBAAkB,eAAe;AAIrD,MAAM,yBACH,UACA,EAAE,UAAU,CAAC,GAAG,aAAoD;CACnE,QAAQ,QAAR;EACE,KAAK,sBACH,OAAO,QAAQ,QAAQ,YAAY,MAAM,WAAW,KAAK,OAAO,CAAC,CAAC;EAEpE,SACE,OAAO,QAAQ,uBACb,IAAI,MAAM,oDAAoD,QAAQ,CACxE;CAEJ;AACF;;;;;;AAOF,MAAM,4BAA4B,OAChC,QACA,UAAqC,CAAC,MACL;CACjC,MAAM,EAAE,YAAY,OAAO,OAAO,qBAAqB,UAAU,qBAAqB;CACtF,IAAI,cAAc,OAChB,MAAM,IAAI,MACR,iEAAiE,OAAO,SAAS,GACnF;CAEF,MAAM,OAAO,OAAO,KAAK,SAAS,KAAK;CACvC,IAAI,KAAK,WAAW,IAClB,MAAM,IAAI,MAAM,uDAAuD;CAEzE,MAAM,YAAY,aAAa,IAAI;CACnC,MAAM,UAAU,aAAa,SAAS;CACtC,MAAM,eAAe,UAAU,SAAS,KAAK;CAC7C,MAAM,SAAS,sBAAsB,IAAI;CAEzC,MAAM,cAAc;CACpB,IAAI;EACF,MAAM,OAAO,eAAe,cAAc,QAAuB,OAAO,GAAG,CAAC;CAC9E,SAAS,OAAO;EACd,IAAI,EAAE,iBAAiB,SAAS,MAAM,QAAQ,SAAS,oBAAoB,IACzE,MAAM;CAEV;CAEA,MAAM,OAAO,eACV,CAAC,SAAS,UAAU;EACnB,MAAM,OACJ,OACA;EACF,MAAM,iBAAiB,WAAW,MAC/B,KAAK,aAAa,MAAM,uBAAO,CAAC,KAAK,CAAC,EAAC,CAAE,KAAK,SAAS,OAAO,SAAS,MAAM,EAAE,CAAC,CACnF;EACA,MAAM,UAAU;GACd,SAAS,KAAK;GACd,QAAQ,KAAK;GACb,UAAU,CAAC,oBAAoB;GAC/B,OAAO,KAAK;GACZ,WAAW;EACb;EACA,MAAM,SAAS;GACb,UAAU,CAAC,OAAO;GAClB,QAAQ,KAAK;GACb,UAAU;IACR,sBAAsB;KACpB,cAAc,GAAG,WACf,QAAQ,IACN,OAAO,IAAI,OAAO,EAAE,eAAe;MACjC,WAAW,WAAW,KACpB,MAAM,KAAK;OAAE,SAAS,CAAC,GAAG,OAAO;OAAG,QAAQ;MAAqB,CAAC,CACpE;MACA,eAAe;KACjB,EAAE,CACJ;KACF,SAAS;IACX;IACA,oBAAoB;KAClB,eAAe,QAAQ,QAAQ,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC;KACtD,SAAS;IACX;IACA,mBAAmB;KACjB,gBAAgB,CAAC;KACjB,SAAS;IACX;GACF;GACA,MAAM,KAAK;GACX,MAAM,KAAK;GACX,SAAS;EACX;EAEA,MAAM,YAAY,QAAkD;GAClE,IAAI,SAAS,MAAM;EACrB;EACA,IAAI;GACF,OAAO,cACL,IAAI,YAAY,mCAAmC,EAAE,QAAQ,SAAS,CAAC,CACzE;EACF,QAAQ,CAER;EACA,OAAO,iBAAiB,8BAA8B,UAAU;GAC9D,SAAU,MAAgE,MAAM;EAClF,CAAC;CACH,GACA,CACE,aACA;EACE;EACA,QAAQ,CAAC,GAAG,UAAU;EACtB,MAAM;EACN;EACA;CACF,CACF,CACF;CAEA,OAAO;EAAE;EAAS;CAAa;AACjC"}
|
package/dist/mock.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { privateKeyToAccount } from "viem/accounts";
|
|
|
3
3
|
//#region src/mock.ts
|
|
4
4
|
const DEFAULT_KEY = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
|
|
5
5
|
const toHex = (value) => `0x${value.toString(16)}`;
|
|
6
|
-
const isHexMessage = (value) =>
|
|
6
|
+
const isHexMessage = (value) => new RegExp("^0x[\\da-fA-F]*$", "v").test(value);
|
|
7
7
|
/** The EIP-1193 handler a mock provider answers with. Exported for direct, browser-free testing. */
|
|
8
8
|
const createRpcHandler = (account, chainIdHex) => ({ method, params = [] }) => {
|
|
9
9
|
switch (method) {
|
|
@@ -39,9 +39,9 @@ const installMockWallet = async (target, options = {}) => {
|
|
|
39
39
|
const call = window[binding];
|
|
40
40
|
const provider = {
|
|
41
41
|
isMetaMask: true,
|
|
42
|
-
request: (rpc) => call(rpc),
|
|
43
42
|
on: () => provider,
|
|
44
|
-
removeListener: () => provider
|
|
43
|
+
removeListener: () => provider,
|
|
44
|
+
request: (rpc) => call(rpc)
|
|
45
45
|
};
|
|
46
46
|
window.ethereum = provider;
|
|
47
47
|
const announce = () => {
|
package/dist/mock.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mock.mjs","names":[],"sources":["../src/mock.ts"],"sourcesContent":["import type { BrowserContext, Page } from \"@playwright/test\";\nimport type { PrivateKeyAccount } from \"viem/accounts\";\nimport { privateKeyToAccount } from \"viem/accounts\";\n\n/**\n * A headless injected-wallet fake for dapp tests that don't need a real extension. It announces\n * itself over EIP-6963 and answers the EIP-1193 requests a connect/sign flow makes, signing with a\n * real key so signatures verify. `@walletwright/core/mock` is a separate entry point that needs\n * `viem` (optional peer); the extension-driving core doesn't.\n */\nexport type MockWalletOptions = {\n chainId?: number;\n /** Wallet name announced over EIP-6963. */\n name?: string;\n /** The account's private key. Defaults to anvil/hardhat account #0. */\n privateKey?: `0x${string}`;\n};\n\
|
|
1
|
+
{"version":3,"file":"mock.mjs","names":[],"sources":["../src/mock.ts"],"sourcesContent":["import type { BrowserContext, Page } from \"@playwright/test\";\nimport type { PrivateKeyAccount } from \"viem/accounts\";\nimport { privateKeyToAccount } from \"viem/accounts\";\n\n/**\n * A headless injected-wallet fake for dapp tests that don't need a real extension. It announces\n * itself over EIP-6963 and answers the EIP-1193 requests a connect/sign flow makes, signing with a\n * real key so signatures verify. `@walletwright/core/mock` is a separate entry point that needs\n * `viem` (optional peer); the extension-driving core doesn't.\n */\nexport type MockWalletOptions = {\n chainId?: number;\n /** Wallet name announced over EIP-6963. */\n name?: string;\n /** The account's private key. Defaults to anvil/hardhat account #0. */\n privateKey?: `0x${string}`;\n};\n\nconst DEFAULT_KEY = \"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80\";\n\ntype Rpc = { method: string; params?: ReadonlyArray<unknown> };\n\nconst toHex = (value: number): string => `0x${value.toString(16)}`;\n\nconst isHexMessage = (value: string): value is `0x${string}` => /^0x[\\da-fA-F]*$/v.test(value);\n\n/** The EIP-1193 handler a mock provider answers with. Exported for direct, browser-free testing. */\nconst createRpcHandler =\n (account: PrivateKeyAccount, chainIdHex: string) =>\n ({ method, params = [] }: Rpc): Promise<unknown> => {\n switch (method) {\n case \"eth_requestAccounts\":\n case \"eth_accounts\": {\n return Promise.resolve([account.address]);\n }\n case \"eth_chainId\": {\n return Promise.resolve(chainIdHex);\n }\n case \"personal_sign\": {\n const [message] = params;\n if (typeof message !== \"string\") {\n return Promise.reject(\n new Error(\"[walletwright/mock] personal_sign expects a string message\"),\n );\n }\n return account.signMessage({\n message: isHexMessage(message) ? { raw: message } : message,\n });\n }\n case \"wallet_switchEthereumChain\":\n case \"wallet_addEthereumChain\": {\n return Promise.resolve(null);\n }\n default: {\n return Promise.reject(new Error(`[walletwright/mock] unsupported method: ${method}`));\n }\n }\n };\n\n/**\n * Install the mock on a context (every page) or a single page. Call before `goto`, so the provider\n * exists when the dapp looks for it. Returns the account address it announces.\n */\nconst installMockWallet = async (\n target: BrowserContext | Page,\n options: MockWalletOptions = {},\n): Promise<string> => {\n const { chainId = 31_337, name = \"Walletwright Mock\", privateKey = DEFAULT_KEY } = options;\n const account = privateKeyToAccount(privateKey);\n const chainIdHex = toHex(chainId);\n const handle = createRpcHandler(account, chainIdHex);\n\n const bindingName = \"__walletwrightMockRpc\";\n try {\n await target.exposeFunction(bindingName, (rpc: Rpc) => handle(rpc));\n } catch (error) {\n if (!(error instanceof Error && error.message.includes(\"already registered\"))) {\n throw error;\n }\n }\n\n await target.addInitScript(\n ([binding, info]) => {\n const call = (window as unknown as Record<string, (rpc: Rpc) => Promise<unknown>>)[binding];\n const provider = {\n isMetaMask: true,\n on: () => provider,\n removeListener: () => provider,\n request: (rpc: Rpc) => call(rpc),\n };\n (window as unknown as { ethereum?: unknown }).ethereum = provider;\n\n const announce = () => {\n window.dispatchEvent(\n new CustomEvent(\"eip6963:announceProvider\", {\n detail: Object.freeze({ info, provider }),\n }),\n );\n };\n window.addEventListener(\"eip6963:requestProvider\", announce);\n announce();\n },\n [\n bindingName,\n {\n icon: \"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'/>\",\n name,\n rdns: \"sh.walletwright.mock\",\n uuid: \"00000000-0000-0000-0000-000000000000\",\n },\n ] as const,\n );\n\n return account.address;\n};\n\nexport { createRpcHandler, installMockWallet };\n"],"mappings":";;;AAkBA,MAAM,cAAc;AAIpB,MAAM,SAAS,UAA0B,KAAK,MAAM,SAAS,EAAE;AAE/D,MAAM,gBAAgB,UAA0C,kCAAiB,CAAC,CAAC,KAAK,KAAK;;AAG7F,MAAM,oBACH,SAA4B,gBAC5B,EAAE,QAAQ,SAAS,CAAC,QAA+B;CAClD,QAAQ,QAAR;EACE,KAAK;EACL,KAAK,gBACH,OAAO,QAAQ,QAAQ,CAAC,QAAQ,OAAO,CAAC;EAE1C,KAAK,eACH,OAAO,QAAQ,QAAQ,UAAU;EAEnC,KAAK,iBAAiB;GACpB,MAAM,CAAC,WAAW;GAClB,IAAI,OAAO,YAAY,UACrB,OAAO,QAAQ,uBACb,IAAI,MAAM,4DAA4D,CACxE;GAEF,OAAO,QAAQ,YAAY,EACzB,SAAS,aAAa,OAAO,IAAI,EAAE,KAAK,QAAQ,IAAI,QACtD,CAAC;EACH;EACA,KAAK;EACL,KAAK,2BACH,OAAO,QAAQ,QAAQ,IAAI;EAE7B,SACE,OAAO,QAAQ,uBAAO,IAAI,MAAM,2CAA2C,QAAQ,CAAC;CAExF;AACF;;;;;AAMF,MAAM,oBAAoB,OACxB,QACA,UAA6B,CAAC,MACV;CACpB,MAAM,EAAE,UAAU,OAAQ,OAAO,qBAAqB,aAAa,gBAAgB;CACnF,MAAM,UAAU,oBAAoB,UAAU;CAC9C,MAAM,aAAa,MAAM,OAAO;CAChC,MAAM,SAAS,iBAAiB,SAAS,UAAU;CAEnD,MAAM,cAAc;CACpB,IAAI;EACF,MAAM,OAAO,eAAe,cAAc,QAAa,OAAO,GAAG,CAAC;CACpE,SAAS,OAAO;EACd,IAAI,EAAE,iBAAiB,SAAS,MAAM,QAAQ,SAAS,oBAAoB,IACzE,MAAM;CAEV;CAEA,MAAM,OAAO,eACV,CAAC,SAAS,UAAU;EACnB,MAAM,OAAQ,OAAqE;EACnF,MAAM,WAAW;GACf,YAAY;GACZ,UAAU;GACV,sBAAsB;GACtB,UAAU,QAAa,KAAK,GAAG;EACjC;EACA,AAAC,OAA6C,WAAW;EAEzD,MAAM,iBAAiB;GACrB,OAAO,cACL,IAAI,YAAY,4BAA4B,EAC1C,QAAQ,OAAO,OAAO;IAAE;IAAM;GAAS,CAAC,EAC1C,CAAC,CACH;EACF;EACA,OAAO,iBAAiB,2BAA2B,QAAQ;EAC3D,SAAS;CACX,GACA,CACE,aACA;EACE,MAAM;EACN;EACA,MAAM;EACN,MAAM;CACR,CACF,CACF;CAEA,OAAO,QAAQ;AACjB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@walletwright/core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Playwright wallet automation for MetaMask, Phantom, Rabby, Solflare, and Slush across EVM, Solana, and Sui. Connect and sign in real browser extensions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"e2e",
|
|
@@ -56,18 +56,18 @@
|
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"adm-zip": "^0.6.0",
|
|
59
|
-
"classic-level": "^
|
|
59
|
+
"classic-level": "^3.0.0"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@playwright/test": "1.62.1",
|
|
63
63
|
"@types/adm-zip": "^0.5.7",
|
|
64
|
-
"@types/node": "^
|
|
65
|
-
"prool": "^0.2.
|
|
64
|
+
"@types/node": "^26.2.0",
|
|
65
|
+
"prool": "^0.2.14",
|
|
66
66
|
"typescript": "^6.0.3",
|
|
67
|
-
"viem": "^2.
|
|
68
|
-
"vitest": "^4.1.
|
|
69
|
-
"@repo/config
|
|
70
|
-
"@repo/
|
|
67
|
+
"viem": "^2.55.11",
|
|
68
|
+
"vitest": "^4.1.10",
|
|
69
|
+
"@repo/typescript-config": "0.0.0",
|
|
70
|
+
"@repo/config-vitest": "0.0.0"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
73
|
"@playwright/test": ">=1.48 <2",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cache-Bqz3Mchl.mjs","names":["add","NAVIGATION_TIMEOUT_MS","importWallet","unlockScreen","importWallet"],"sources":["../src/internal/download.ts","../src/wallets/metamask/actions/accounts.ts","../src/wallets/metamask/actions/network.ts","../src/internal/wait.ts","../src/internal/unlock-screen.ts","../src/wallets/metamask/onboarding.ts","../src/wallets/metamask/actions/settings.ts","../src/wallets/metamask/approve.ts","../src/internal/utils.ts","../src/wallets/metamask/onboarding-patch.ts","../src/wallets/metamask.ts","../src/wallets/phantom.ts","../src/wallets/rabby.ts","../src/wallets/slush.ts","../src/wallets/solflare.ts","../src/wallets/index.ts","../src/internal/chromium.ts","../src/internal/cache.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { existsSync } from \"node:fs\";\nimport { mkdir, rm } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport AdmZip from \"adm-zip\";\n\n// ZIP local-file-header signature (\"PK\\x03\\x04\") as decimal bytes. It marks where the embedded ZIP\n// begins inside a CRX. Decimal (not hex) avoids the oxfmt/number-literal-case casing conflict.\nconst ZIP_SIGNATURE = Buffer.from([80, 75, 3, 4]);\n\n/**\n * Download an extension archive and extract it to `<cacheDir>/<name>`, returning that path. A `.crx`\n * is a ZIP with a binary header, we slice from the ZIP signature before unzipping. Reuses an\n * existing extraction (so you can pre-place the extension to skip the download).\n */\nexport const downloadAndExtractExtension = async (options: {\n cacheDir: string;\n kind: \"zip\" | \"crx\";\n name: string;\n /**\n * Expected sha256 of the downloaded bytes. Required, and `undefined` only where the bytes genuinely\n * cannot be pinned, so adding a download site forces a decision instead of quietly trusting it.\n */\n sha256: string | undefined;\n url: string;\n}): Promise<string> => {\n const { cacheDir, kind, name, sha256, url } = options;\n const cacheRoot = path.resolve(cacheDir);\n const outDir = path.resolve(cacheDir, name);\n // A wallet-supplied `name` (e.g. built from a version string) must resolve to a directory\n // strictly inside the cache dir; this outDir gets `rm -rf`'d below, so a \"../\"-bearing name must\n // never escape it, and a name resolving to the cache root itself would wipe the whole cache.\n if (outDir === cacheRoot || !outDir.startsWith(cacheRoot + path.sep)) {\n throw new Error(`[walletwright] invalid extension name: ${name}`);\n }\n if (existsSync(path.join(outDir, \"manifest.json\"))) {\n return outDir;\n }\n\n await mkdir(cacheDir, { recursive: true });\n\n const response = await fetch(url, { redirect: \"follow\" });\n if (!response.ok) {\n throw new Error(\n `[walletwright] failed to download ${url}: ${response.status} ${response.statusText}`,\n );\n }\n const bytes = Buffer.from(await response.arrayBuffer());\n\n if (sha256 !== undefined) {\n const actual = createHash(\"sha256\").update(bytes).digest(\"hex\");\n if (actual !== sha256.toLowerCase()) {\n throw new Error(\n `[walletwright] ${name} failed integrity check: expected ${sha256}, got ${actual}`,\n );\n }\n }\n\n let zipBytes = bytes;\n if (kind === \"crx\") {\n const start = bytes.indexOf(ZIP_SIGNATURE);\n if (start === -1) {\n throw new Error(`[walletwright] ${url} is not a valid CRX (no ZIP header found)`);\n }\n zipBytes = bytes.subarray(start);\n }\n\n await rm(outDir, { force: true, recursive: true });\n const zip = new AdmZip(zipBytes);\n const root = path.resolve(outDir);\n for (const entry of zip.getEntries()) {\n const target = path.resolve(root, entry.entryName);\n // Reject zip-slip: an entry like \"../../x\" must not resolve outside the extraction root.\n if (target !== root && !target.startsWith(root + path.sep)) {\n throw new Error(`[walletwright] refusing to extract ${entry.entryName}: escapes ${outDir}`);\n }\n }\n zip.extractAllTo(outDir, /* overwrite */ true);\n\n if (!existsSync(path.join(outDir, \"manifest.json\"))) {\n throw new Error(`[walletwright] extracted ${name} but no manifest.json found in ${outDir}`);\n }\n return outDir;\n};\n\n/** Build the Chrome Web Store CRX download URL for an extension id. */\nexport const chromeWebStoreCrxUrl = (extensionId: string): string =>\n `https://clients2.google.com/service/update2/crx?response=redirect&prodversion=130.0&acceptformat=crx2,crx3&x=id%3D${extensionId}%26uc`;\n\n/** Download and extract a Chrome Web Store extension (latest) into `<cacheDir>/<name>`. */\nexport const prepareWebStoreExtension = (options: {\n cacheDir: string;\n extensionId: string;\n name: string;\n}): Promise<string> =>\n downloadAndExtractExtension({\n cacheDir: options.cacheDir,\n kind: \"crx\",\n name: options.name,\n // Unpinnable by construction: the Web Store endpoint always serves the current version, so its\n // bytes change under us. Only versioned release URLs (MetaMask's) can carry a hash.\n sha256: undefined,\n url: chromeWebStoreCrxUrl(options.extensionId),\n });\n","import type { Page } from \"@playwright/test\";\n\nimport type { AccountActions, WalletActionContext } from \"../../../types\";\n\nconst openAccountMenu = async (home: Page): Promise<void> => {\n await home.getByTestId(\"account-menu-icon\").click();\n await home.getByTestId(\"add-multichain-account-button\").waitFor({ state: \"visible\" });\n};\n\n/** The menu is a popover with no close affordance; going home is the reliable way out. */\nconst closeAccountMenu = async (home: Page): Promise<void> => {\n await home.goto(`${home.url().split(\"#\")[0]}#/`);\n await home.getByTestId(\"account-menu-icon\").waitFor({ state: \"visible\" });\n};\n\nconst add = async ({ home }: WalletActionContext): Promise<void> => {\n await openAccountMenu(home);\n const cells = home.getByTestId(\"account-cell-avatar\");\n const before = await cells.count();\n // One click derives the next HD account from the seed; no dialog follows.\n await home.getByTestId(\"add-multichain-account-button\").click();\n await cells.nth(before).waitFor({ state: \"visible\", timeout: 15_000 });\n await closeAccountMenu(home);\n};\n\nconst importPrivateKey = async (\n { home }: WalletActionContext,\n privateKey: string,\n): Promise<void> => {\n await openAccountMenu(home);\n await home.getByTestId(\"account-list-add-wallet-button\").click();\n const chooseImport = home.getByTestId(\"choose-wallet-type-import-account\");\n await chooseImport.waitFor({ state: \"visible\" });\n await chooseImport.click();\n const confirm = home.getByTestId(\"import-account-confirm-button\");\n await confirm.waitFor({ state: \"visible\" });\n // The key field is the first of the view's inputs and carries no testid; filling it enables the\n // confirm button.\n await home.locator(\"input\").first().fill(privateKey);\n await confirm.click();\n // A successful import returns to the wallet-type chooser, not home.\n await confirm.waitFor({ state: \"hidden\", timeout: 15_000 });\n await closeAccountMenu(home);\n};\n\nconst rename = async (\n { home }: WalletActionContext,\n options: { index: number; name: string },\n): Promise<void> => {\n await openAccountMenu(home);\n await home.getByTestId(\"multichain-account-cell-end-accessory\").nth(options.index).click();\n await home.getByTestId(\"multichain-account-menu-item-rename\").click();\n await home.getByTestId(\"account-name-input\").locator(\"input\").fill(options.name);\n await home.getByRole(\"button\", { name: \"Confirm\" }).click();\n await closeAccountMenu(home);\n};\n\nconst switchTo = async ({ home }: WalletActionContext, index: number): Promise<void> => {\n await openAccountMenu(home);\n await home.getByTestId(\"account-cell-avatar\").nth(index).click();\n // Selecting an account closes the menu and makes it active; if the menu is still open, the\n // switch did not happen (bad index, missed click), which must be an error, not a silent no-op.\n const menuClosed = await home\n .getByTestId(\"add-multichain-account-button\")\n .waitFor({ state: \"hidden\", timeout: 10_000 })\n .then(() => true)\n .catch(() => false);\n if (!menuClosed) {\n throw new Error(`[walletwright] accounts.switch(${index}) did not switch (menu stayed open)`);\n }\n};\n\nexport const accounts: AccountActions = {\n add,\n importPrivateKey,\n rename,\n switch: switchTo,\n};\n","import type { Page } from \"@playwright/test\";\n\nimport type { NetworkActions, NetworkConfig, WalletActionContext } from \"../../../types\";\n\nconst closeButton = (home: Page) =>\n home.getByTestId(\"modal-header-close-button\").filter({ visible: true }).first();\n\nconst openNetworkManager = async (home: Page): Promise<void> => {\n await home.getByTestId(\"sort-by-networks\").click();\n // The manager is a modal; its close button is present whether the Popular or Custom tab is shown.\n await closeButton(home).waitFor({ state: \"visible\" });\n};\n\nconst add = async ({ home }: WalletActionContext, config: NetworkConfig): Promise<void> => {\n await openNetworkManager(home);\n\n // The custom-network form lives behind the \"Custom\" tab; \"Add custom network\" opens the form.\n await home.getByText(\"Custom\", { exact: true }).first().click();\n await home.getByRole(\"button\", { name: \"Add custom network\" }).first().click();\n\n await home.getByTestId(\"network-form-network-name\").fill(config.name);\n await home.getByTestId(\"network-form-chain-id\").fill(String(config.chainId));\n await home.getByTestId(\"network-form-ticker-input\").fill(config.symbol);\n\n // The RPC URL is a nested sub-form: open the dropdown, add a URL, fill it, confirm.\n await home.getByTestId(\"test-add-rpc-drop-down\").click();\n await home.getByText(\"Add RPC URL\", { exact: true }).first().click();\n await home.getByTestId(\"rpc-url-input-test\").fill(config.rpcUrl);\n await home.getByTestId(\"page-container-footer-next\").click();\n\n await home.getByTestId(\"network-form-network-name\").waitFor();\n await home.getByTestId(\"page-container-footer-next\").click();\n\n // Saving returns to the manager modal; close it so the wallet is left on a clean home screen.\n await closeButton(home).click();\n await closeButton(home).waitFor({ state: \"hidden\" });\n};\n\n// No `switch`: MetaMask 13.x scopes the active chain per dapp and has no wallet-side selector\n// (the network manager is an asset filter; toggling a site's permitted networks doesn't change its\n// active chain, verified empirically). Switching is dapp-initiated (EIP-3326\n// `wallet_switchEthereumChain`), and `wallet.approve()` drives the popup when one appears.\nexport const network: NetworkActions = {\n add,\n};\n","import type { Page } from \"@playwright/test\";\n\nconst DEFAULT_INTERVAL_MS = 200;\nconst DEFAULT_NAVIGATION_INTERVAL_MS = 1000;\n\nexport type WaitOptions = {\n /** Gap between checks. */\n intervalMs?: number;\n /** Total budget. `check` runs at least once, and never again once the budget has elapsed. */\n timeoutMs: number;\n};\n\nexport const sleep = (ms: number): Promise<void> =>\n new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n\n/** Render a timeout for an error message, so what a failure claims matches what it waited. */\nexport const formatTimeout = (ms: number): string => `${Math.round(ms / 100) / 10}s`;\n\n/**\n * Poll `check` until it yields anything other than `undefined`/`false`, then hand that value back.\n * Returns `undefined` when the budget elapses, so only a caller that can genuinely continue without\n * the condition (an optional approval popup, say) branches on the result; everything else uses\n * `waitUntilOrThrow` and gets a named failure instead of a wallet that looks ready and isn't.\n */\nexport const waitUntil = async <T>(\n check: () => Promise<T | false | undefined> | T | false | undefined,\n { intervalMs = DEFAULT_INTERVAL_MS, timeoutMs }: WaitOptions,\n): Promise<T | undefined> => {\n const deadline = Date.now() + timeoutMs;\n for (;;) {\n const result = await check();\n if (result !== undefined && result !== false) {\n return result;\n }\n if (Date.now() >= deadline) {\n return undefined;\n }\n await sleep(intervalMs);\n }\n};\n\nexport const waitUntilOrThrow = async <T>(\n check: () => Promise<T | false | undefined> | T | false | undefined,\n options: WaitOptions & { message: string },\n): Promise<T> => {\n const result = await waitUntil(check, options);\n if (result === undefined) {\n throw new Error(\n `[walletwright] ${options.message} (gave up after ${formatTimeout(options.timeoutMs)})`,\n );\n }\n return result;\n};\n\n/**\n * A `chrome-extension://` URL fails with `ERR_BLOCKED_BY_CLIENT` until the extension registers,\n * which takes seconds after launch, so reaching an extension page is a poll rather than a one-shot.\n */\nexport const gotoWithRetry = async (\n page: Page,\n url: string,\n {\n intervalMs = DEFAULT_NAVIGATION_INTERVAL_MS,\n label,\n timeoutMs,\n }: WaitOptions & { label: string },\n): Promise<void> => {\n await waitUntilOrThrow(\n () =>\n page\n .goto(url, { waitUntil: \"domcontentloaded\" })\n .then(() => true)\n .catch(() => undefined),\n { intervalMs, message: `${label} never loaded (${url})`, timeoutMs },\n );\n};\n","import type { BrowserContext, Locator, Page } from \"@playwright/test\";\n\nimport { formatTimeout, gotoWithRetry, waitUntil } from \"./wait\";\n\nconst PASSWORD_FIELD = 'input[type=\"password\"]';\n\nconst NAVIGATION_TIMEOUT_MS = 15_000;\nconst FIRST_SETTLE_TIMEOUT_MS = 20_000;\nconst RELOAD_ATTEMPTS = 5;\nconst RELOAD_SETTLE_TIMEOUT_MS = 5000;\nconst PROBE_INTERVAL_MS = 500;\nconst PASSWORD_CLEARED_TIMEOUT_MS = 15_000;\n\ntype UnlockScreenOptions = {\n /** Extension-relative page that renders the unlock screen (e.g. `home.html`). */\n entry: string;\n /**\n * Resolves true once the wallet's already-unlocked UI is up. Only wallets that can reopen unlocked\n * (Phantom, Slush) declare it: without a positive signal for that state, a warm launch is\n * indistinguishable from an extension page that rendered nothing at all, and the run continues\n * against a dead wallet until some later approval fails for no visible reason.\n */\n isUnlocked?: (page: Page) => Promise<boolean>;\n /** Submit the filled password. Defaults to pressing Enter in the field. */\n submit?: (page: Page, field: Locator) => Promise<void>;\n wallet: string;\n};\n\ntype UnlockScreen = {\n reachUnlockScreen: (context: BrowserContext, extensionId: string) => Promise<Page>;\n unlock: (page: Page, password: string) => Promise<void>;\n};\n\n/**\n * The shared unlock flow: open the wallet's own page, wait for it to settle into a state we can\n * name, and type the password. Every wallet differs only in its entry page, how it submits, and\n * whether it can come back already unlocked.\n */\nexport const createUnlockScreen = ({\n entry,\n isUnlocked,\n submit,\n wallet,\n}: UnlockScreenOptions): UnlockScreen => {\n const settled = async (page: Page): Promise<\"locked\" | \"unlocked\" | undefined> => {\n const locked = await page\n .locator(PASSWORD_FIELD)\n .isVisible()\n .catch(() => false);\n if (locked) {\n return \"locked\";\n }\n const unlocked = await isUnlocked?.(page).catch(() => false);\n return unlocked === true ? \"unlocked\" : undefined;\n };\n\n const settleWithin = (page: Page, timeoutMs: number) =>\n waitUntil(() => settled(page), { intervalMs: PROBE_INTERVAL_MS, timeoutMs });\n\n const reachUnlockScreen = async (context: BrowserContext, extensionId: string): Promise<Page> => {\n const page = await context.newPage();\n await gotoWithRetry(page, `chrome-extension://${extensionId}/${entry}`, {\n label: `${wallet} ${entry}`,\n timeoutMs: NAVIGATION_TIMEOUT_MS,\n });\n\n // The MV3 service worker needs a few seconds to restore the vault; be patient, then reload\n // (re-navigating resets the page before the worker responds).\n let state = await settleWithin(page, FIRST_SETTLE_TIMEOUT_MS);\n for (let attempt = 0; attempt < RELOAD_ATTEMPTS && state === undefined; attempt++) {\n await page.reload().catch(() => {});\n state = await settleWithin(page, RELOAD_SETTLE_TIMEOUT_MS);\n }\n if (state === undefined) {\n throw new Error(\n `[walletwright] ${wallet} never reached its unlock screen (gave up after ${formatTimeout(\n FIRST_SETTLE_TIMEOUT_MS + RELOAD_ATTEMPTS * RELOAD_SETTLE_TIMEOUT_MS,\n )})`,\n );\n }\n return page;\n };\n\n const unlock = async (page: Page, password: string): Promise<void> => {\n const field = page.locator(PASSWORD_FIELD);\n const locked = await field.isVisible().catch(() => false);\n if (!locked) {\n const unlocked = await isUnlocked?.(page).catch(() => false);\n if (unlocked === true) {\n return;\n }\n throw new Error(\n `[walletwright] ${wallet} showed neither an unlock screen nor an unlocked wallet`,\n );\n }\n\n await field.fill(password);\n await (submit === undefined ? field.press(\"Enter\") : submit(page, field));\n const cleared = await field\n .waitFor({ state: \"hidden\", timeout: PASSWORD_CLEARED_TIMEOUT_MS })\n .then(() => true)\n .catch(() => false);\n if (!cleared) {\n throw new Error(\n `[walletwright] ${wallet} unlock failed (password screen still visible after ${formatTimeout(\n PASSWORD_CLEARED_TIMEOUT_MS,\n )})`,\n );\n }\n };\n\n return { reachUnlockScreen, unlock };\n};\n","import type { Page } from \"@playwright/test\";\n\nimport { createUnlockScreen } from \"../../internal/unlock-screen\";\nimport { waitUntil } from \"../../internal/wait\";\n\nconst SRP_RETYPE_TIMEOUT_MS = 40_000;\nconst SRP_CONFIRM_TIMEOUT_MS = 5000;\n\nconst { reachUnlockScreen, unlock } = createUnlockScreen({\n entry: \"home.html\",\n wallet: \"MetaMask\",\n});\n\nexport const importWallet = async (\n page: Page,\n seedPhrase: string,\n password: string,\n): Promise<void> => {\n await page.getByTestId(\"onboarding-import-wallet\").click();\n await page.getByTestId(\"onboarding-import-with-srp-button\").click();\n\n // The SRP field only enables the confirm button after its change handler parses a valid phrase.\n // `fill` doesn't fire those events; typing can drop keystrokes, so retype until it enables. The\n // budget bounds the retries rather than a count: one retype is ~5s of keystrokes plus the wait.\n const srpField = page.getByTestId(\"srp-input-import__srp-note\");\n const confirmEnabled = page.locator('[data-testid=\"import-srp-confirm\"]:not([disabled])');\n const accepted = await waitUntil(\n async () => {\n await srpField.click();\n await srpField.fill(\"\");\n await srpField.pressSequentially(seedPhrase, { delay: 50 });\n return confirmEnabled\n .waitFor({ state: \"visible\", timeout: SRP_CONFIRM_TIMEOUT_MS })\n .then(() => true)\n .catch(() => undefined);\n },\n { intervalMs: 0, timeoutMs: SRP_RETYPE_TIMEOUT_MS },\n );\n if (accepted === undefined) {\n throw new Error(\"[walletwright] MetaMask rejected the seed phrase (confirm stayed disabled)\");\n }\n await confirmEnabled.click();\n\n await page.getByTestId(\"create-password-new-input\").fill(password);\n await page.getByTestId(\"create-password-confirm-input\").fill(password);\n await page.getByTestId(\"create-password-terms\").click();\n await page.locator('[data-testid=\"create-password-submit\"]:not([disabled])').click();\n\n // Post-password steps vary by build; skip whichever isn't shown. The \"wallet is ready\" screen is\n // handled out of band via finalizeCache (completedOnboarding patch).\n await page\n .getByTestId(\"passkey-maybe-later-button\")\n .click({ timeout: 10_000 })\n .catch(() => {});\n await page\n .getByTestId(\"metametrics-i-agree\")\n .click({ timeout: 10_000 })\n .catch(() => {});\n};\n\nexport { reachUnlockScreen, unlock };\n","import type { SettingsActions } from \"../../../types\";\nimport { unlock as unlockScreen } from \"../onboarding\";\n\nexport const settings: SettingsActions = {\n lock: async ({ home }) => {\n await home.getByTestId(\"account-options-menu-button\").click();\n // The menu renders the item several times (responsive variants), only one of which is visible.\n await home.getByTestId(\"global-menu-lock\").filter({ visible: true }).first().click();\n await home.locator('input[type=\"password\"]').waitFor({ state: \"visible\", timeout: 15_000 });\n },\n\n unlock: async ({ home, password }) => {\n await unlockScreen(home, password);\n },\n};\n","import type { Locator, Page } from \"@playwright/test\";\n\n/**\n * How long a confirm or cancel button gets to become clickable, once the page is already showing\n * one. Generous because a loaded CI runner renders MetaMask's footer slowly.\n */\nconst BUTTON_TIMEOUT_MS = 45_000;\n/** The retry only covers a notice that rendered late, so it does not need the full budget again. */\nconst RETRY_TIMEOUT_MS = 10_000;\n\n/**\n * First-time requests that MetaMask routes through a protocol Snap (e.g. adding a custom chain)\n * open a \"Third-party software notice\" modal over the confirm footer, and every click is\n * intercepted until it is accepted. Its buttons carry no testids, only text, and Accept stays\n * disabled until the notice is scrolled to the bottom.\n */\nconst acceptThirdPartyNotice = async (popup: Page): Promise<void> => {\n const modal = popup.locator(\".snap-install-warning\");\n if ((await modal.count().catch(() => 0)) === 0) {\n return;\n }\n await modal\n .locator(\"p\")\n .last()\n .scrollIntoViewIfNeeded()\n .catch(() => {});\n await modal\n .getByRole(\"button\", { name: \"Accept\" })\n .click({ timeout: 3000 })\n .catch(() => {});\n};\n\n/**\n * Confirm buttons differ by request type: connect popups use `confirm-btn`, signature and\n * transaction popups use `confirm-footer-button`, older builds used `page-container-footer-next`,\n * and requests routed through a protocol Snap (e.g. Solana) use\n * `confirm-<type>-confirm-snap-footer-button`, matched by suffix so every snap confirmation type is\n * covered. A popup renders exactly one of these buttons; the `.or()` union resolves whichever one\n * is present by DOM order, not by the order it's written in below, so one locator covers every\n * popup.\n */\nconst confirmButton = (popup: Page): Locator =>\n popup\n .getByTestId(\"confirm-btn\")\n .or(popup.getByTestId(\"confirm-footer-button\"))\n .or(popup.locator('[data-testid=\"page-container-footer-next\"]'))\n .or(popup.locator('[data-testid$=\"-confirm-snap-footer-button\"]'))\n .first();\n\nconst cancelButton = (popup: Page): Locator =>\n popup\n .getByTestId(\"cancel-btn\")\n .or(popup.getByTestId(\"confirm-footer-cancel-button\"))\n .or(popup.locator('[data-testid=\"page-container-footer-cancel\"]'))\n .or(popup.locator('[data-testid$=\"-cancel-snap-footer-button\"]'))\n .first();\n\n/**\n * What \"this page is showing a request\" means for MetaMask, as opposed to its wallet home. The\n * distinction matters for the page walletwright opens itself: `notification.html` renders the home\n * screen (account header, Buy/Swap/Send, a news carousel) when nothing is pending, so buttons on\n * screen prove nothing. A CI run handed that home screen to `approve` and waited out the full click\n * budget against a confirm button that was never going to exist.\n */\nexport const approvalControls = (popup: Page): Locator =>\n confirmButton(popup).or(cancelButton(popup));\n\nexport const approve = async (popup: Page): Promise<void> => {\n const button = confirmButton(popup);\n await acceptThirdPartyNotice(popup);\n try {\n await button.click({ timeout: BUTTON_TIMEOUT_MS });\n } catch (error) {\n // The notice can render after the first accept attempt; clear it once and retry.\n await acceptThirdPartyNotice(popup);\n await button.click({ timeout: RETRY_TIMEOUT_MS }).catch(() => {\n throw error;\n });\n }\n};\n\n/** The cancel counterparts of `approve`; same union-by-DOM-order resolution, not written order. */\nexport const reject = async (popup: Page): Promise<void> => {\n await cancelButton(popup).click({ timeout: BUTTON_TIMEOUT_MS });\n};\n","import { createHash } from \"node:crypto\";\nimport { readFileSync, realpathSync } from \"node:fs\";\nimport path from \"node:path\";\n\nimport type { BrowserContext, Locator, Page } from \"@playwright/test\";\n\nimport type { WalletSetup } from \"../types\";\n\nimport { waitUntil } from \"./wait\";\n\nexport const DEFAULT_CACHE_DIR = \".walletwright\";\n\n/** MetaMask/Phantom approval-popup URL token; Slush overrides it with `notificationMatch: \"isPopup=1\"`. */\nexport const DEFAULT_NOTIFICATION_MATCH = \"notification.html\";\n\n/** Stable per-setup profile directory name, so the same wallet+seed+password reuses one cache. */\nexport const profileKey = (setup: WalletSetup): string =>\n createHash(\"sha256\")\n .update(`${setup.wallet}:${setup.version ?? \"default\"}:${setup.seedPhrase}:${setup.password}`)\n .digest(\"hex\")\n .slice(0, 20);\n\n/**\n * Compute an extension's id without querying the browser (`chrome://extensions` is blocked headless,\n * and the service worker starts lazily). Chrome derives the id from the sha256 of either the\n * manifest's public `key` (if present, e.g. Phantom → its fixed Web Store id) or the absolute load\n * path (no key, e.g. MetaMask), taking the first 16 bytes and mapping each nibble 0-f → a-p.\n *\n * The path is resolved through symlinks (`realpathSync`) because Chrome hashes the *real* path, a\n * cache under a symlinked dir (e.g. macOS `/tmp` → `/private/tmp`) would otherwise yield a wrong id.\n */\nexport const extensionIdFromPath = (extensionPath: string): string => {\n const resolved = path.resolve(extensionPath);\n const abs = (() => {\n try {\n return realpathSync(resolved);\n } catch {\n return resolved; // path doesn't exist yet, fall back to the literal resolved path\n }\n })();\n let key: string | undefined;\n try {\n const manifest: unknown = JSON.parse(readFileSync(path.join(abs, \"manifest.json\"), \"utf8\"));\n if (typeof manifest === \"object\" && manifest !== null && \"key\" in manifest) {\n key = typeof manifest.key === \"string\" ? manifest.key : undefined;\n }\n } catch {\n // manifest not present yet, fall back to the path\n }\n const source =\n key === undefined || key === \"\" ? Buffer.from(abs, \"utf8\") : Buffer.from(key, \"base64\");\n const hex = createHash(\"sha256\").update(source).digest(\"hex\").slice(0, 32);\n return Array.from(hex, (nibble) => String.fromCodePoint(97 + Number.parseInt(nibble, 16))).join(\n \"\",\n );\n};\n\n/**\n * Approval popups open as `about:blank` and then navigate, so `waitForEvent('page', { predicate })`\n * misses them, poll the open pages instead. `match` is the URL token identifying the popup\n * (`notification.html` for MetaMask/Phantom; `isPopup=1` for Slush's single-page `index.html`).\n */\nexport const isApprovalPopup = (page: Page, extensionId: string, match: string): boolean =>\n page.url().startsWith(`chrome-extension://${extensionId}`) &&\n page.url().includes(match) &&\n !page.isClosed();\n\nconst isVisible = async (locator: Locator): Promise<boolean> => {\n try {\n return await locator.isVisible();\n } catch {\n return false; // the page can go while we ask\n }\n};\n\nconst hasVisibleButton = (page: Page): Promise<boolean> =>\n isVisible(page.locator(\"button\").first());\n\nexport const findNotificationPopup = ({\n approvalControls,\n context,\n extensionId,\n match = DEFAULT_NOTIFICATION_MATCH,\n timeoutMs = 10_000,\n}: {\n /** From `WalletDefinition.approvalControls`, for a wallet that can render its popup requestless. */\n approvalControls?: (page: Page) => Locator;\n context: BrowserContext;\n extensionId: string;\n match?: string;\n timeoutMs?: number;\n}): Promise<Page | undefined> =>\n waitUntil(\n async () => {\n const popup = context.pages().find((page) => isApprovalPopup(page, extensionId, match));\n if (!popup) {\n return undefined;\n }\n await popup.waitForLoadState(\"domcontentloaded\").catch(() => {});\n // The window opens before the approval renders (bare URL, zero buttons) and routes later,\n // sometimes tens of seconds later under a busy MV3 worker. \"Found\" must mean \"usable\", so\n // keep polling the same shell rather than handing back a page that cannot be settled. A\n // rendered button is the weakest form of that: MetaMask's popup can also render its home\n // screen, buttons and all, so it says which controls mean \"a request is on screen\".\n const ready =\n approvalControls === undefined\n ? await hasVisibleButton(popup)\n : await isVisible(approvalControls(popup).first());\n return ready ? popup : undefined;\n },\n { timeoutMs },\n );\n\n/** MetaMask's own popup dimensions, which its layout is built for. */\nconst APPROVAL_WINDOW = { height: 592, width: 360 };\n/** Far enough from the edge that a popup this size lands fully on any usable display. */\nconst APPROVAL_WINDOW_OFFSET = 50;\n\n/**\n * Put a wallet-spawned approval window somewhere it can be clicked. It can open partly off-screen\n * on a small or virtual display, and Playwright will not click what is out of view, so a confirm\n * button that is rendered and enabled still times out. Synpress carries the same workaround.\n *\n * Best-effort throughout: headless has no window to move, and nothing here is required for a window\n * that already sits on screen.\n */\nexport const placeApprovalWindow = async (page: Page): Promise<void> => {\n await page.setViewportSize(APPROVAL_WINDOW).catch(() => {});\n try {\n const session = await page.context().newCDPSession(page);\n const { targetInfo } = await session.send(\"Target.getTargetInfo\");\n const { windowId } = await session.send(\"Browser.getWindowForTarget\", {\n targetId: targetInfo.targetId,\n });\n await session.send(\"Browser.setWindowBounds\", {\n bounds: { left: APPROVAL_WINDOW_OFFSET, top: APPROVAL_WINDOW_OFFSET },\n windowId,\n });\n await session.detach();\n } catch {\n // no window to place (headless), or the page refused a CDP session\n }\n};\n\n/** Where Chrome persists an extension's `chrome.storage.local` inside a browser profile. */\nexport const extensionStateDir = (profileDir: string, extensionId: string): string =>\n path.join(profileDir, \"Default\", \"Local Extension Settings\", extensionId);\n\nexport const hasNotificationPopup = (\n context: BrowserContext,\n extensionId: string,\n match = DEFAULT_NOTIFICATION_MATCH,\n): boolean => context.pages().some((page) => isApprovalPopup(page, extensionId, match));\n","import { existsSync } from \"node:fs\";\n\nimport { ClassicLevel } from \"classic-level\";\n\nimport { extensionStateDir } from \"../../internal/utils\";\n\ntype OnboardingState = Record<string, unknown>;\n\nconst asState = (value: unknown): OnboardingState | undefined =>\n typeof value === \"object\" && value !== null ? { ...value } : undefined;\n\nconst markOnboarded = (onboarding: OnboardingState): OnboardingState => ({\n ...onboarding,\n completedOnboarding: true,\n firstTimeFlowType:\n typeof onboarding.firstTimeFlowType === \"string\" ? onboarding.firstTimeFlowType : \"import\",\n onboardingTabs: {},\n});\n\n/**\n * Force `completedOnboarding=true` in MetaMask's persisted state so the cached wallet boots straight\n * to the unlock screen instead of getting stuck on the \"wallet is ready\" screen (whose \"Open wallet\"\n * action goes through the MV3 service worker and is unreliable under automation).\n *\n * MetaMask 13.13.x stored all state under a single `data` key; 13.3x stores each controller under its\n * own key (`OnboardingController`). Both are handled. Must run while the browser is closed.\n *\n * Every branch that can't find the state it means to patch throws: leaving the cache unpatched means\n * a wallet that boots to \"wallet is ready\" and ignores every dapp request, which is far harder to\n * diagnose from a spec than a failed cache build.\n */\nexport const markMetaMaskOnboarded = async (\n profileDir: string,\n extensionId: string,\n): Promise<void> => {\n const dbDir = extensionStateDir(profileDir, extensionId);\n if (!existsSync(dbDir)) {\n throw new Error(`[walletwright] MetaMask wrote no extension state to ${dbDir}`);\n }\n\n const db = new ClassicLevel(dbDir, {\n createIfMissing: false,\n keyEncoding: \"utf8\",\n valueEncoding: \"utf8\",\n });\n try {\n await db.open();\n\n const perController = await db.get(\"OnboardingController\").catch(() => null);\n if (perController !== null && perController !== \"\") {\n const onboarding = asState(JSON.parse(perController));\n if (onboarding === undefined) {\n throw new Error(`[walletwright] MetaMask's OnboardingController state is not an object`);\n }\n if (onboarding.completedOnboarding !== true) {\n await db.put(\"OnboardingController\", JSON.stringify(markOnboarded(onboarding)));\n }\n return;\n }\n\n const raw = await db.get(\"data\").catch(() => null);\n if (raw === null || raw === \"\") {\n throw new Error(\n `[walletwright] MetaMask persisted no onboarding state in ${dbDir} (neither an OnboardingController key nor a data key)`,\n );\n }\n const state = asState(JSON.parse(raw));\n if (state === undefined) {\n throw new Error(`[walletwright] MetaMask's \"data\" state is not an object`);\n }\n const nested = asState(state.data);\n const controller = asState((nested ?? state).OnboardingController);\n if (controller === undefined) {\n throw new Error(`[walletwright] MetaMask's \"data\" state holds no OnboardingController`);\n }\n const patched = markOnboarded(controller);\n await db.put(\n \"data\",\n JSON.stringify(\n nested === undefined\n ? { ...state, OnboardingController: patched }\n : { ...state, data: { ...nested, OnboardingController: patched } },\n ),\n );\n } finally {\n await db.close();\n }\n};\n","import { downloadAndExtractExtension } from \"../internal/download\";\nimport type { WalletDefinition } from \"../types\";\n\nimport { accounts } from \"./metamask/actions/accounts\";\nimport { network } from \"./metamask/actions/network\";\nimport { settings } from \"./metamask/actions/settings\";\nimport { approvalControls, approve, reject } from \"./metamask/approve\";\nimport { importWallet, reachUnlockScreen, unlock } from \"./metamask/onboarding\";\nimport { markMetaMaskOnboarded } from \"./metamask/onboarding-patch\";\n\nconst DEFAULT_VERSION = \"13.35.1\";\n\n/**\n * sha256 of each pinned `metamask-chrome-<version>.zip` release asset, so a pinned download is\n * verified rather than trusted. This is the one place to fill in when bumping `DEFAULT_VERSION`:\n *\n * curl -sL https://github.com/MetaMask/metamask-extension/releases/download/v<v>/metamask-chrome-<v>.zip | shasum -a 256\n *\n * A version a caller pins through `setup.version` has no entry here and downloads unverified, and\n * the Web-Store wallets can't be pinned at all (that endpoint always serves the current version).\n */\nconst RELEASE_SHA256: Readonly<Record<string, string>> = {\n \"13.35.1\": \"4e0f8626df0ae9fb15f5f3ad6784a0b518f3ede067b2b0d4f539f9f457c5049c\",\n};\n\n/** Download inputs for a MetaMask release, split out so the integrity wiring is unit-testable. */\nexport const metamaskDownload = (cacheDir: string, version: string) => ({\n cacheDir,\n kind: \"zip\" as const,\n name: `metamask-chrome-${version}`,\n sha256: RELEASE_SHA256[version],\n url: `https://github.com/MetaMask/metamask-extension/releases/download/v${version}/metamask-chrome-${version}.zip`,\n});\n\n/**\n * MetaMask's backup-and-sync restores account names for whichever SRP a profile holds, and test\n * seeds are shared (the public `test test … junk` one by thousands), so a synced profile reports a\n * stranger's account names in place of the ones a test just set: CI runs showed accounts called\n * `dev1` and `personal`, holding a real balance, where `Account 2` was expected. It only happens\n * where the sync actually lands, which makes it look like a flake. MetaMask's own e2e suite mocks\n * its external services for the same reason.\n *\n * Just the one host that stores the names. Cutting the auth stack around it (`authentication`,\n * `oidc`) reaches further than intended: other features authenticate through it too, and a wallet\n * that cannot authenticate can leave a request's confirm button disabled with nothing to explain it.\n */\nconst ACCOUNT_SYNC_HOST = \"user-storage.api.cx.metamask.io\";\n\nexport const metamask: WalletDefinition = {\n actions: { accounts, network, settings },\n approvalControls,\n approve,\n ecosystems: [\"evm\", \"svm\"],\n extensionName: \"MetaMask\",\n\n prepareContext: async (context) => {\n await context.route(`**://${ACCOUNT_SYNC_HOST}/**`, (route) => route.abort());\n },\n\n prepareExtension: (cacheDir, version = DEFAULT_VERSION) =>\n downloadAndExtractExtension(metamaskDownload(cacheDir, version)),\n\n // Fresh install of home.html redirects to the onboarding welcome screen.\n finalizeCache: markMetaMaskOnboarded,\n\n // No `headlessApprovals`: MetaMask creates its approval window headless but never exposes it as a\n // page, so there is nothing for the engine to find, and it renders its home screen rather than the\n // request when reached any other way.\n\n importWallet,\n onboardingPage: \"home.html\",\n reachUnlockScreen,\n reject,\n unlock,\n};\n","import { prepareWebStoreExtension } from \"../internal/download\";\nimport { createUnlockScreen } from \"../internal/unlock-screen\";\nimport { sleep } from \"../internal/wait\";\nimport type { WalletDefinition } from \"../types\";\n\n// Phantom's old crx-backup host is dead; pull from the Chrome Web Store (stable extension id).\nconst PHANTOM_EXTENSION_ID = \"bfnaelmomeimhlpmgjnjophhpkkoljpa\";\n\nconst { reachUnlockScreen, unlock } = createUnlockScreen({\n entry: \"popup.html\",\n // Phantom reopens either locked (password screen) or straight onto the account home, which\n // carries no stable test id of its own. Actionable UI with no password field is that home; what\n // the probe rules out is the third state, a popup that rendered nothing, which used to pass as\n // \"unlocked\" and only surfaced later as an approval that never arrived.\n isUnlocked: async (page) => {\n const rendered = await page\n .locator(\"button\")\n .first()\n .isVisible()\n .catch(() => false);\n return rendered;\n },\n submit: async (page, field) => {\n const unlockButton = page.getByRole(\"button\", { name: /unlock/iv });\n await ((await unlockButton.isVisible().catch(() => false))\n ? unlockButton.click()\n : field.press(\"Enter\"));\n },\n wallet: \"Phantom\",\n});\n\nexport const phantom: WalletDefinition = {\n ecosystems: [\"evm\", \"svm\"],\n extensionName: \"Phantom\",\n\n // Its approval window surfaces as a page headless, verified end to end on Linux CI and macOS.\n headlessApprovals: true,\n\n // Phantom only publishes \"latest\" via the Web Store, so `version` is ignored.\n importWallet: async (page, seedPhrase, password) => {\n const testId = (id: string) => page.locator(`[data-testid=\"${id}\"]`);\n\n await page.locator(\"text=I already have a wallet\").click();\n await sleep(1000);\n await page.locator(\"text=Import Recovery Phrase\").click();\n await sleep(1500);\n\n const words = seedPhrase.trim().split(/\\s+/v);\n for (let i = 0; i < words.length; i++) {\n await testId(`secret-recovery-phrase-word-input-${i}`).fill(words[i] ?? \"\");\n }\n await testId(\"onboarding-form-submit-button\").click();\n\n // \"import accounts\" success screen → continue\n await testId(\"onboarding-form-secondary-button\").waitFor({ state: \"visible\", timeout: 60_000 });\n await testId(\"onboarding-form-submit-button\").click();\n\n await testId(\"onboarding-form-password-input\").fill(password);\n await testId(\"onboarding-form-confirm-password-input\").fill(password);\n await testId(\"onboarding-form-terms-of-service-checkbox\").click();\n await testId(\"onboarding-form-submit-button\").click();\n await sleep(2500);\n\n const getStarted = page.locator(\"text=Get Started\");\n if (await getStarted.isVisible().catch(() => false)) {\n await getStarted.click();\n }\n await sleep(1500);\n },\n\n onboardingPage: \"onboarding.html\",\n\n prepareExtension: (cacheDir) =>\n prepareWebStoreExtension({\n cacheDir,\n extensionId: PHANTOM_EXTENSION_ID,\n name: \"phantom-chrome-latest\",\n }),\n\n reachUnlockScreen,\n\n unlock,\n\n // Connect and signature popups both confirm with `primary-button` (reject = `secondary-button`).\n approve: async (popup) => {\n await popup.getByTestId(\"primary-button\").click({ timeout: 15_000 });\n },\n\n reject: async (popup) => {\n await popup.getByTestId(\"secondary-button\").click({ timeout: 15_000 });\n },\n};\n","import type { Page } from \"@playwright/test\";\n\nimport { prepareWebStoreExtension } from \"../internal/download\";\nimport { createUnlockScreen } from \"../internal/unlock-screen\";\nimport { sleep, waitUntil, waitUntilOrThrow } from \"../internal/wait\";\nimport type { WalletDefinition } from \"../types\";\n\nconst ROUTE_TIMEOUT_MS = 30_000;\nconst SEED_PASTE_TIMEOUT_MS = 3000;\nconst APPROVAL_TIMEOUT_MS = 30_000;\n\n// Rabby, by DeBank. Pulled from the Chrome Web Store. No manifest `key`, so its id is path-derived.\nconst RABBY_EXTENSION_ID = \"acmacodkjbdgmoleebolmdjonilkdbch\";\n\n// Rabby is a single-page app: onboarding, unlock, and the dashboard all live in index.html. A fresh\n// profile must enter at the new-user guide; plain index.html lands on a marketing carousel whose\n// \"Get Started\" leads to the add-address menu, which reopens this route in a second tab.\nconst ONBOARDING_ROUTE = \"index.html#/new-user/guide\";\n\nconst { reachUnlockScreen, unlock } = createUnlockScreen({ entry: \"index.html\", wallet: \"Rabby\" });\n\nconst clickText = async (page: Page, text: string, timeoutMs = 30_000): Promise<void> => {\n const target = page.getByText(text, { exact: true }).first();\n await target.waitFor({ state: \"visible\", timeout: timeoutMs });\n await target.click();\n};\n\n/**\n * Rabby routes with a hash router, so `waitForURL` never settles (a hash change fires no navigation\n * event). Poll the URL instead. Anchoring each onboarding step on its route matters because several\n * screens share the same `input[type=\"password\"]` selector.\n */\nconst waitForRoute = async (\n page: Page,\n fragment: string,\n timeoutMs = ROUTE_TIMEOUT_MS,\n): Promise<void> => {\n await waitUntilOrThrow(() => page.url().includes(fragment), {\n message: `Rabby never reached ${fragment}`,\n timeoutMs,\n });\n};\n\nconst importWallet = async (page: Page, seedPhrase: string, password: string): Promise<void> => {\n await clickText(page, \"I already have an address\");\n await clickText(page, \"Seed Phrase or Private Key\");\n await waitForRoute(page, \"seed-or-key\");\n\n // The 12 word boxes are unlabelled `type=password` inputs that re-render as they fill, so filling\n // them one by one drops words. Pasting the whole phrase into the first box is the path Rabby\n // supports: it splits on whitespace and distributes across every box at once.\n const words = page.locator('input[type=\"password\"]');\n await words.first().waitFor({ state: \"visible\", timeout: 30_000 });\n await page\n .context()\n .grantPermissions([\"clipboard-read\", \"clipboard-write\"])\n .catch(() => {});\n const list = seedPhrase.trim().split(/\\s+/v);\n const lastFilled = async (): Promise<boolean> => {\n const value = await words\n .nth(list.length - 1)\n .inputValue()\n .catch(() => \"\");\n return value.trim().length > 0;\n };\n\n await words.first().click();\n await page.evaluate((phrase) => navigator.clipboard.writeText(phrase), seedPhrase.trim());\n await page.keyboard.press(\"ControlOrMeta+v\");\n const pasted = await waitUntil(lastFilled, { timeoutMs: SEED_PASTE_TIMEOUT_MS });\n // Clipboard access can be denied outright depending on how the profile was launched; typing each\n // box is slower but always works, so fall back rather than submitting a half-filled phrase.\n if (pasted === undefined) {\n for (let index = 0; index < list.length; index++) {\n const box = words.nth(index);\n await box.click();\n await box.fill(list[index] ?? \"\");\n }\n }\n await page.getByRole(\"button\", { exact: true, name: \"Next\" }).first().click();\n\n // \"Set Password\" screen: the new password and its confirmation, then Confirm. Wait for the route,\n // not just for two password inputs: the seed screen still shows twelve of them, so a bare\n // `nth(1)` would match word 2 and overwrite the phrase.\n await waitForRoute(page, \"set-password\");\n const passwords = page.locator('input[type=\"password\"]');\n await passwords.nth(1).waitFor({ state: \"visible\", timeout: 30_000 });\n await passwords.nth(0).fill(password);\n await passwords.nth(1).fill(password);\n await page.getByRole(\"button\", { exact: true, name: \"Confirm\" }).first().click();\n\n // The success screen is where the keyring is persisted; \"Open Wallet\" only navigates to the\n // dashboard, so the cache is complete without it.\n await page\n .getByText(\"Address Imported\", { exact: false })\n .first()\n .waitFor({ state: \"visible\", timeout: 30_000 });\n await sleep(1000);\n};\n\nconst CONFIRM_LABELS = [\"Connect\", \"Sign\", \"Confirm\"];\nconst CANCEL_LABELS = [\"Cancel\", \"Reject\"];\n\n/**\n * Rabby's approval window is focus-fragile: it unmounts its contents a few seconds after losing\n * focus, and Playwright's click (which waits for actionability, then for the click to settle) loses\n * the window mid-action. Rabby reads the vanished window as a dismissal, so the dapp gets\n * \"User rejected the request\" even though the confirm button was visible and enabled. Dispatching\n * the click inside the page skips focus, actionability, and post-click bookkeeping entirely. Rabby\n * is a plain React app, so `evaluate` works here (unlike MetaMask, which scuttles the realm).\n *\n * Signing is a two-step footer: \"Sign\" swaps itself for \"Confirm\", which must be clicked too, and\n * both start disabled while Rabby analyses the request. So keep clicking whichever labelled button\n * is currently enabled until the window closes, which is the only signal the request was answered.\n */\nconst clickApprovalButton = async (\n popup: Page,\n labels: ReadonlyArray<string>,\n): Promise<boolean> => {\n let lastClicked = \"\";\n const answered = await waitUntil(\n async () => {\n // The popup closing is the only real signal the request was answered.\n if (popup.isClosed()) {\n return true;\n }\n // Click each distinct label once: re-clicking a still-open \"Connect\" re-issues the request and\n // the dapp ends up with nothing, while \"Sign\" legitimately needs a follow-up \"Confirm\".\n const clicked = await popup\n .evaluate(\n (arg) => {\n const target = [...document.querySelectorAll(\"button\")].find((button) => {\n const text = (button.textContent ?? \"\").trim();\n return arg.names.includes(text) && !button.disabled && text !== arg.skip;\n });\n target?.click();\n return target ? (target.textContent ?? \"\").trim() : \"\";\n },\n { names: [...labels], skip: lastClicked },\n )\n .catch(() => \"\");\n if (clicked) {\n lastClicked = clicked;\n }\n return undefined;\n },\n { intervalMs: 300, timeoutMs: APPROVAL_TIMEOUT_MS },\n );\n return answered === true || popup.isClosed();\n};\n\nexport const rabby: WalletDefinition = {\n approve: async (popup) => {\n if (!(await clickApprovalButton(popup, CONFIRM_LABELS))) {\n throw new Error(\"[walletwright] Rabby approval: no confirm button became actionable\");\n }\n },\n\n ecosystems: [\"evm\"],\n\n extensionName: \"Rabby\",\n\n // Its approval window surfaces as a page headless, verified end to end on Linux CI and macOS.\n headlessApprovals: true,\n\n importWallet,\n\n onboardingPage: ONBOARDING_ROUTE,\n\n // Latest from the Web Store, so `version` is ignored.\n prepareExtension: (cacheDir) =>\n prepareWebStoreExtension({\n cacheDir,\n extensionId: RABBY_EXTENSION_ID,\n name: \"rabby-chrome-latest\",\n }),\n\n reachUnlockScreen,\n\n reject: async (popup) => {\n if (!(await clickApprovalButton(popup, CANCEL_LABELS))) {\n throw new Error(\"[walletwright] Rabby reject: no cancel button became actionable\");\n }\n },\n\n unlock,\n};\n","import type { Page } from \"@playwright/test\";\n\nimport { prepareWebStoreExtension } from \"../internal/download\";\nimport { createUnlockScreen } from \"../internal/unlock-screen\";\nimport { sleep, waitUntilOrThrow } from \"../internal/wait\";\nimport type { WalletDefinition } from \"../types\";\n\n// Slush (formerly Sui Wallet), by Mysten Labs. Pulled from the Chrome Web Store.\nconst SLUSH_EXTENSION_ID = \"opcgpfmipidbgpenhmajoajpbobppdil\";\n\n// Slush is a single-page app: popup, onboarding, unlock, and approvals all live in index.html.\nconst HOME_ROUTE = \"#/tokens\";\n\nconst IMPORT_HOME_TIMEOUT_MS = 30_000;\n\nconst atHome = async (page: Page): Promise<boolean> =>\n (await page.evaluate(() => globalThis.location.hash).catch(() => \"\")) === HOME_ROUTE;\n\nconst fclick = async (page: Page, text: string, timeoutMs = 15_000): Promise<boolean> => {\n const target = page.getByText(text, { exact: true }).first();\n // isVisible() reports the *current* state without waiting; Slush's single-page UI mounts its React\n // tree a few seconds after domcontentloaded, so an instant check reads false and the click is\n // silently skipped. waitFor blocks until the element is actually visible (or the budget elapses).\n const visible = await target\n .waitFor({ state: \"visible\", timeout: timeoutMs })\n .then(() => true)\n .catch(() => false);\n if (!visible) {\n return false;\n }\n await target.scrollIntoViewIfNeeded().catch(() => {});\n // Slush's popup buttons need a forced click; do NOT swallow a real click failure here.\n await target.click({ force: true, timeout: timeoutMs });\n return true;\n};\n\nconst { reachUnlockScreen, unlock } = createUnlockScreen({\n entry: \"index.html\",\n // Slush's single-page UI mounts a few seconds after the navigation resolves and settles into one\n // of two states: the password screen (cold launch, locked) or the home route (warm launch, already\n // unlocked). Anything else is a page that never mounted, and driving it would only fail later, at\n // the first approval.\n isUnlocked: atHome,\n submit: async (page) => {\n await fclick(page, \"Unlock\");\n },\n wallet: \"Slush\",\n});\n\nexport const slush: WalletDefinition = {\n // Connect confirms with \"Approve\"; signing confirms with \"Sign\" and then re-prompts for the\n // password (\"Unlock\") before it completes.\n approve: async (popup, password) => {\n // Slush's popup reports the button visible before its React handlers are wired, a click that\n // lands too early is a silent no-op. Let it settle first.\n await sleep(2000);\n // Probe Approve with a short budget so a signing popup (no Approve button) falls through to\n // Sign quickly instead of waiting out fclick's full budget on an element that never appears.\n const confirmed = (await fclick(popup, \"Approve\", 4000)) || (await fclick(popup, \"Sign\"));\n if (!confirmed) {\n throw new Error(\"[walletwright] Slush approval: neither Approve nor Sign was actionable\");\n }\n await sleep(1500);\n const input = popup.locator('input[type=\"password\"]');\n if (await input.isVisible().catch(() => false)) {\n await input.fill(password);\n await fclick(popup, \"Unlock\");\n }\n },\n\n ecosystems: [\"sui\"],\n\n extensionName: \"Slush\",\n\n importWallet: async (page, seedPhrase, password) => {\n await fclick(page, \"More options\");\n await sleep(1000);\n await fclick(page, \"Import existing from passphrase\");\n await sleep(1500);\n\n const words = seedPhrase.trim().split(/\\s+/v);\n for (let i = 0; i < words.length; i++) {\n await page.locator(`input[placeholder=\"Word ${i + 1}\"]`).fill(words[i] ?? \"\");\n }\n await page.getByRole(\"button\", { name: \"Next\" }).click();\n await sleep(2000);\n\n await page.locator('input[placeholder=\"Password\"]').fill(password);\n await page.locator('input[placeholder=\"Confirm Password\"]').fill(password);\n await page.getByRole(\"button\", { name: \"Next\" }).click();\n await sleep(2000);\n\n // \"OnboardingSecurity\" info screen → Next → \"CreateWallet\" spinner → home. The spinner must\n // finish or the wallet never persists, so reaching home is the only proof the import took.\n await page\n .getByRole(\"button\", { name: \"Next\" })\n .click()\n .catch(() => {});\n await waitUntilOrThrow(() => atHome(page), {\n intervalMs: 1000,\n message: `Slush import never reached the wallet home (${HOME_ROUTE})`,\n timeoutMs: IMPORT_HOME_TIMEOUT_MS,\n });\n },\n\n // Approvals open as index.html popups marked with `isPopup=1` (no separate notification.html).\n notificationMatch: \"isPopup=1\",\n\n onboardingPage: \"index.html\",\n\n // Slush's own backend (api.slush.app, initialize.slush.app) answers every request from an\n // automated browser with a Cloudflare 403 whose body is the Mysten Labs marketing page. Slush's\n // GraphQL client throws `NonGraphQLResponseError` on that HTML and renders a \"Reload App\" error\n // screen instead of the wallet, so onboarding never reaches its first screen. A well-formed empty\n // response is all it needs to boot; onboarding, unlock, connect, and sign need nothing from the\n // API. Drop this once the endpoint answers automated requests again.\n prepareContext: async (context) => {\n await context.route(\"**://*.slush.app/**\", (route) =>\n route.fulfill({\n body: JSON.stringify({ data: {} }),\n contentType: \"application/json\",\n status: 200,\n }),\n );\n },\n\n // Latest from the Web Store, so `version` is ignored.\n prepareExtension: (cacheDir) =>\n prepareWebStoreExtension({\n cacheDir,\n extensionId: SLUSH_EXTENSION_ID,\n name: \"slush-chrome-latest\",\n }),\n\n reachUnlockScreen,\n\n // Both the connect and sign popups cancel via a text \"Reject\" button. Like approve, the popup\n // reports the button visible before its React handlers wire up, so settle first or the click\n // no-ops silently.\n reject: async (popup) => {\n await sleep(2000);\n if (!(await fclick(popup, \"Reject\"))) {\n throw new Error(\"[walletwright] Slush reject: no Reject control was actionable\");\n }\n },\n\n unlock,\n};\n","import type { Page } from \"@playwright/test\";\n\nimport { prepareWebStoreExtension } from \"../internal/download\";\nimport { createUnlockScreen } from \"../internal/unlock-screen\";\nimport type { WalletDefinition } from \"../types\";\n\n// Solflare. Pulled from the Chrome Web Store. No manifest `key`, so its id is path-derived.\nconst SOLFLARE_EXTENSION_ID = \"bhhhlbepdkbapadjdnnojkbgioiodbic\";\n\nconst { reachUnlockScreen, unlock } = createUnlockScreen({\n entry: \"wallet.html\",\n wallet: \"Solflare\",\n});\n\nconst importWallet = async (page: Page, seedPhrase: string, password: string): Promise<void> => {\n await page.getByTestId(\"btn-import-existing-wallet\").click({ timeout: 30_000 });\n await page.getByText(\"Recovery phrase\", { exact: true }).first().click({ timeout: 30_000 });\n\n const words = seedPhrase.trim().split(/\\s+/v);\n await page.getByTestId(\"input-recovery-phrase-1\").waitFor({ state: \"visible\", timeout: 30_000 });\n for (let index = 0; index < words.length; index++) {\n await page.getByTestId(`input-recovery-phrase-${index + 1}`).fill(words[index] ?? \"\");\n }\n await page.getByTestId(\"btn-continue\").click({ timeout: 30_000 });\n\n await page.getByTestId(\"input-new-password\").waitFor({ state: \"visible\", timeout: 30_000 });\n await page.getByTestId(\"input-new-password\").fill(password);\n await page.getByTestId(\"input-repeat-password\").fill(password);\n await page.getByTestId(\"btn-continue\").click({ timeout: 30_000 });\n\n // An unfunded seed derives no \"active\" (SOL-holding) accounts, so Solflare asks how to pick them.\n // Quick setup takes the default account, which is the one the tests drive.\n await page.getByTestId(\"btn-quick-setup\").click({ timeout: 60_000 });\n\n // \"You're All Set!\" - accepting here is what leaves onboarding in a completed state.\n await page.getByTestId(\"btn-explore\").click({ timeout: 60_000 });\n};\n\nexport const solflare: WalletDefinition = {\n // Connect popups confirm with `btn-connect`, signature popups with `btn-approve`; a popup renders\n // exactly one of them, so the union resolves whichever is present.\n approve: async (popup) => {\n await popup\n .getByTestId(\"btn-connect\")\n .or(popup.getByTestId(\"btn-approve\"))\n .first()\n .click({ timeout: 30_000 });\n },\n\n ecosystems: [\"svm\"],\n\n extensionName: \"Solflare\",\n\n importWallet,\n\n // Approvals open in their own confirm_popup.html window.\n notificationMatch: \"confirm_popup.html\",\n\n // No `headlessApprovals`: Solflare rejects a connection outright when the browser runs headless,\n // the dapp getting \"Connection rejected\" within ~2s, before any approval UI exists.\n\n onboardingPage: \"wallet.html\",\n\n // Latest from the Web Store, so `version` is ignored.\n prepareExtension: (cacheDir) =>\n prepareWebStoreExtension({\n cacheDir,\n extensionId: SOLFLARE_EXTENSION_ID,\n name: \"solflare-chrome-latest\",\n }),\n\n reachUnlockScreen,\n\n reject: async (popup) => {\n await popup\n .getByTestId(\"btn-cancel\")\n .or(popup.getByTestId(\"btn-reject\"))\n .first()\n .click({ timeout: 30_000 });\n },\n\n unlock,\n};\n","import type { Ecosystem, WalletDefinition, WalletKind } from \"../types\";\n\nimport { metamask } from \"./metamask\";\nimport { phantom } from \"./phantom\";\nimport { rabby } from \"./rabby\";\nimport { slush } from \"./slush\";\nimport { solflare } from \"./solflare\";\n\nexport const wallets: Record<WalletKind, WalletDefinition> = {\n metamask,\n phantom,\n rabby,\n slush,\n solflare,\n};\n\n/** Narrow an arbitrary string (CLI flag, setup file field) to a supported wallet. */\nexport const isWalletKind = (value: string): value is WalletKind => Object.hasOwn(wallets, value);\n\nconst walletKinds = Object.keys(wallets).filter(isWalletKind);\n\n/** Wallet kinds that can drive the given ecosystem (e.g. `\"evm\"` → MetaMask, Phantom). */\nexport const walletKindsByEcosystem = (ecosystem: Ecosystem): Array<WalletKind> =>\n walletKinds.filter((kind) => wallets[kind].ecosystems.includes(ecosystem));\n","/**\n * How walletwright launches Chromium with a wallet extension loaded. Shared by the cache build and\n * the test run so the two cannot drift: an extension that loads for onboarding but not for the\n * tests (or the reverse) fails far from its cause.\n */\nexport const extensionContextOptions = (\n extensionPath: string,\n headless: boolean,\n): { args: Array<string>; channel: string; headless: boolean } => ({\n args: [\n `--disable-extensions-except=${extensionPath}`,\n // Required: current Chromium does NOT auto-load the cached unpacked extension without this.\n `--load-extension=${extensionPath}`,\n ],\n // Playwright's default headless build is the headless *shell*, which cannot load an extension at\n // all. The `chromium` channel is the full browser, whose headless mode can. Headed is unaffected:\n // it is the same Chromium build either way.\n channel: \"chromium\",\n headless,\n});\n","import { mkdir, readdir, rm } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport { chromium } from \"@playwright/test\";\n\nimport type { WalletSetup } from \"../types\";\nimport { wallets } from \"../wallets/index\";\n\nimport { extensionContextOptions } from \"./chromium\";\nimport { DEFAULT_CACHE_DIR, extensionIdFromPath, extensionStateDir, profileKey } from \"./utils\";\nimport { gotoWithRetry, sleep, waitUntilOrThrow } from \"./wait\";\n\nconst NAVIGATION_TIMEOUT_MS = 15_000;\nconst FLUSH_SETTLE_MS = 3000;\nconst STATE_WRITE_TIMEOUT_MS = 10_000;\n\n/**\n * Chrome stores an extension's `chrome.storage.local` under `Local Extension Settings/<id>` and its\n * IndexedDB (what some single-page wallets persist their vault to) under\n * `IndexedDB/chrome-extension_<id>_0…`. Bytes in either one mean onboarding actually landed in the\n * profile; bytes in neither mean the \"cache\" is empty, and every later run would fail at unlock with\n * no hint that the build was the thing that went wrong.\n */\nconst hasPersistedState = async (profileDir: string, extensionId: string): Promise<boolean> => {\n const stores = [\n extensionStateDir(profileDir, extensionId),\n path.join(\n profileDir,\n \"Default\",\n \"IndexedDB\",\n `chrome-extension_${extensionId}_0.indexeddb.leveldb`,\n ),\n ];\n const listings = await Promise.allSettled(stores.map((dir) => readdir(dir)));\n return listings.some((listing) => listing.status === \"fulfilled\" && listing.value.length > 0);\n};\n\n/**\n * Import the wallet once and persist an onboarded browser profile to disk (the \"cache\"), so tests\n * launch a ready-to-unlock wallet instead of re-running onboarding each time. Returns the profile\n * directory. Idempotent per (wallet, version, seed, password).\n *\n * `headless` only affects this build step; the tests choose their own mode via `launchWallet`.\n */\nexport const buildCache = async (\n setup: WalletSetup,\n options: { headless?: boolean } = {},\n): Promise<string> => {\n const definition = wallets[setup.wallet];\n const cacheDir = path.resolve(setup.cacheDir ?? DEFAULT_CACHE_DIR);\n const extensionPath = await definition.prepareExtension(cacheDir, setup.version);\n\n const profileDir = path.join(cacheDir, profileKey(setup));\n await rm(profileDir, { force: true, recursive: true });\n await mkdir(profileDir, { recursive: true });\n\n const context = await chromium.launchPersistentContext(\n profileDir,\n extensionContextOptions(extensionPath, options.headless === true),\n );\n try {\n await definition.prepareContext?.(context);\n const extensionId = extensionIdFromPath(extensionPath);\n\n // Navigate to the onboarding entry ourselves (the extension's auto-opened tab is unreliable,\n // especially headless).\n const page =\n context.pages().find((candidate) => candidate.url() === \"about:blank\") ??\n (await context.newPage());\n await gotoWithRetry(page, `chrome-extension://${extensionId}/${definition.onboardingPage}`, {\n label: `${definition.extensionName} onboarding page`,\n timeoutMs: NAVIGATION_TIMEOUT_MS,\n });\n await sleep(2000);\n\n await definition.importWallet(page, setup.seedPhrase, setup.password);\n await sleep(FLUSH_SETTLE_MS); // let the wallet flush state to disk before we close\n // A cache that holds no wallet state is worse than a failed build: it launches, unlocks nothing,\n // and turns into a mystery failure in someone's spec. Fail here, where the cause is visible.\n await waitUntilOrThrow(() => hasPersistedState(profileDir, extensionId), {\n intervalMs: 500,\n message: `${definition.extensionName} onboarding wrote no wallet state into ${profileDir}`,\n timeoutMs: STATE_WRITE_TIMEOUT_MS,\n });\n await context.close();\n // Runs while the browser is closed (the leveldb is not locked).\n await definition.finalizeCache?.(profileDir, extensionId);\n return profileDir;\n } catch (error) {\n await context.close().catch(() => {});\n throw error;\n }\n};\n"],"mappings":"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"}
|