@walletwright/core 0.2.1 → 0.3.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 +48 -0
- package/README.md +2 -2
- package/dist/{cache-Bwv2i-hv.mjs → cache-BaWu5vyM.mjs} +242 -174
- package/dist/cache-BaWu5vyM.mjs.map +1 -0
- package/dist/cli.d.mts +23 -11
- package/dist/cli.mjs +97 -55
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +20 -30
- package/dist/index.mjs +88 -86
- package/dist/index.mjs.map +1 -1
- package/dist/mock-standard.d.mts +4 -19
- package/dist/mock-standard.mjs +16 -16
- package/dist/mock-standard.mjs.map +1 -1
- package/dist/mock.d.mts +3 -7
- package/dist/mock.mjs +14 -12
- package/dist/mock.mjs.map +1 -1
- package/dist/{types-CWxz_dJe.d.mts → types-CFQpWZhv.d.mts} +7 -30
- package/package.json +6 -5
- package/dist/cache-Bwv2i-hv.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import { cp, mkdtemp, rm } from "node:fs/promises";
|
|
1
|
+
import { a as walletKindsByEcosystem, c as DEFAULT_NOTIFICATION_MATCH, d as placeApprovalWindow, f as profileKey, i as isWalletKind, l as extensionIdFromPath, m as waitUntil, n as restorePreviousProfile, o as wallets, p as formatTimeout, r as extensionContextOptions, s as DEFAULT_CACHE_DIR, t as buildCache, u as findNotificationPopup } from "./cache-BaWu5vyM.mjs";
|
|
2
|
+
import { cp, mkdtemp, rm, stat } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { chromium, test } from "@playwright/test";
|
|
5
|
-
import { existsSync } from "node:fs";
|
|
6
5
|
import os from "node:os";
|
|
7
6
|
|
|
8
7
|
//#region src/internal/controller.ts
|
|
@@ -11,6 +10,22 @@ const POPUP_CLOSE_TIMEOUT_MS = 15e3;
|
|
|
11
10
|
const OPTIONAL_POPUP_TIMEOUT_MS = 1e4;
|
|
12
11
|
/** The MV3 service worker spawns a popup slowly once the wallet's own UI has been driven. */
|
|
13
12
|
const REQUIRED_POPUP_TIMEOUT_MS = 3e4;
|
|
13
|
+
/**
|
|
14
|
+
* Drive `popup` with `settle` and return the page that actually took the approval. A popup that
|
|
15
|
+
* closes mid-settle is the wallet respawning the request, so re-acquire once and drive the new one.
|
|
16
|
+
*/
|
|
17
|
+
const settleApproval = async (settle, popup, acquire) => {
|
|
18
|
+
try {
|
|
19
|
+
await settle(popup);
|
|
20
|
+
return popup;
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (!popup.isClosed()) throw error;
|
|
23
|
+
const fresh = await acquire();
|
|
24
|
+
if (!fresh) throw error;
|
|
25
|
+
await settle(fresh);
|
|
26
|
+
return fresh;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
14
29
|
/** Build the runtime controller that drives an unlocked wallet against the dapp under test. */
|
|
15
30
|
const createWallet = ({ context, definition, extensionId, home, password }) => {
|
|
16
31
|
const match = definition.notificationMatch ?? "notification.html";
|
|
@@ -20,60 +35,39 @@ const createWallet = ({ context, definition, extensionId, home, password }) => {
|
|
|
20
35
|
home,
|
|
21
36
|
password
|
|
22
37
|
};
|
|
23
|
-
/**
|
|
24
|
-
* Make the dapp the active tab. While an extension page holds that spot, MetaMask renders a new
|
|
25
|
-
* approval inline in it rather than opening a notification window, and the window that does open
|
|
26
|
-
* shows the wallet home instead of the request. The wallet's own page can end up fronted both
|
|
27
|
-
* after an action and after a previous approval settles.
|
|
28
|
-
*/
|
|
38
|
+
/** MetaMask opens requests inline while its extension page owns focus. */
|
|
29
39
|
const frontDapp = async () => {
|
|
30
40
|
await context.pages().find((page) => new RegExp("^https?:", "v").test(page.url()) && !page.isClosed())?.bringToFront().catch(() => {});
|
|
31
41
|
};
|
|
32
|
-
/**
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
/** Find and place the approval window before its controls are driven. */
|
|
43
|
+
const acquireApproval = async (optional) => {
|
|
44
|
+
const popup = await findNotificationPopup({
|
|
45
|
+
approvalControls: definition.approvalControls,
|
|
46
|
+
context,
|
|
47
|
+
extensionId,
|
|
48
|
+
match,
|
|
49
|
+
timeoutMs: optional ? OPTIONAL_POPUP_TIMEOUT_MS : REQUIRED_POPUP_TIMEOUT_MS
|
|
50
|
+
});
|
|
51
|
+
if (popup) await placeApprovalWindow(popup);
|
|
52
|
+
return popup;
|
|
53
|
+
};
|
|
54
|
+
/** Drive the pending approval popup with `settle`, then wait for that popup to close. */
|
|
41
55
|
const resolvePopup = async (settle, { optional = false }) => {
|
|
42
56
|
await frontDapp();
|
|
43
|
-
const
|
|
44
|
-
const popup = await
|
|
57
|
+
const acquire = () => acquireApproval(optional);
|
|
58
|
+
const popup = await acquire();
|
|
45
59
|
if (!popup) {
|
|
46
60
|
if (optional) return;
|
|
47
61
|
const open = context.pages().map((page) => page.url());
|
|
48
62
|
throw new Error(`[walletwright] approval popup did not appear. Open pages: ${open.join(" | ") || "none"}`);
|
|
49
63
|
}
|
|
50
|
-
await
|
|
51
|
-
|
|
52
|
-
await settle(popup);
|
|
53
|
-
} catch (error) {
|
|
54
|
-
if (!popup.isClosed()) throw error;
|
|
55
|
-
const fresh = await find();
|
|
56
|
-
if (!fresh) throw error;
|
|
57
|
-
await settle(fresh);
|
|
58
|
-
}
|
|
59
|
-
const closed = await waitUntil(() => !hasNotificationPopup(context, extensionId, match), { timeoutMs: POPUP_CLOSE_TIMEOUT_MS });
|
|
60
|
-
if (!optional && closed === void 0) throw new Error(`[walletwright] approval popup did not close after ${formatTimeout(POPUP_CLOSE_TIMEOUT_MS)} (the approval may not have registered)`);
|
|
64
|
+
const driven = await settleApproval(settle, popup, acquire);
|
|
65
|
+
if (await waitUntil(() => driven.isClosed() || void 0, { timeoutMs: POPUP_CLOSE_TIMEOUT_MS }) === void 0) throw new Error(`[walletwright] approval popup did not close after ${formatTimeout(POPUP_CLOSE_TIMEOUT_MS)} (the approval may not have registered)`);
|
|
61
66
|
};
|
|
62
67
|
const unsupported = (name) => /* @__PURE__ */ new Error(`[walletwright] ${definition.extensionName} does not support ${name}()`);
|
|
63
68
|
const approve = (options = {}) => resolvePopup((popup) => definition.approve(popup, password), options);
|
|
64
|
-
const reject =
|
|
65
|
-
|
|
66
|
-
if (!rejectPopup) throw unsupported("reject");
|
|
67
|
-
await resolvePopup((popup) => rejectPopup(popup), options);
|
|
68
|
-
};
|
|
69
|
-
/**
|
|
70
|
-
* Bind an optional capability, or fail loudly naming the wallet and action. A wallet declares only
|
|
71
|
-
* what has been driven against the real extension, so an undeclared action is a real gap rather
|
|
72
|
-
* than something to swallow. Extra args (e.g. a network config) forward after the context.
|
|
73
|
-
*
|
|
74
|
-
* After the action, focus returns to the dapp (see `frontDapp`), since actions drive the wallet's
|
|
75
|
-
* own page via `bringToFront`.
|
|
76
|
-
*/
|
|
69
|
+
const reject = (options = {}) => resolvePopup((popup) => definition.reject(popup), options);
|
|
70
|
+
/** Bind a verified wallet action and restore focus to the dapp when it finishes. */
|
|
77
71
|
const action = (fn, name) => async (...args) => {
|
|
78
72
|
if (!fn) throw unsupported(name);
|
|
79
73
|
if (home.isClosed()) throw new Error(`[walletwright] wallet home page is closed; cannot run ${name}()`);
|
|
@@ -111,50 +105,68 @@ const createWallet = ({ context, definition, extensionId, home, password }) => {
|
|
|
111
105
|
|
|
112
106
|
//#endregion
|
|
113
107
|
//#region src/internal/launch.ts
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
108
|
+
const closeLaunch = async (context, runDir) => {
|
|
109
|
+
const closed = await Promise.allSettled([context.close()]);
|
|
110
|
+
const removed = await Promise.allSettled([rm(runDir, {
|
|
111
|
+
force: true,
|
|
112
|
+
recursive: true
|
|
113
|
+
})]);
|
|
114
|
+
const closeResult = closed[0];
|
|
115
|
+
const removeResult = removed[0];
|
|
116
|
+
if (closeResult?.status === "rejected" && removeResult?.status === "rejected") throw new AggregateError([closeResult.reason, removeResult.reason], `[walletwright] failed to close the browser and remove ${runDir}`);
|
|
117
|
+
if (closeResult?.status === "rejected") throw closeResult.reason;
|
|
118
|
+
if (removeResult?.status === "rejected") throw removeResult.reason;
|
|
119
|
+
};
|
|
120
|
+
/** Close Chromium's blank tab and the extension's auto-opened tab. */
|
|
120
121
|
const closeStrayPages = async (context, home) => {
|
|
121
122
|
const isStray = (page) => page !== home && !page.isClosed() && (page.url() === "about:blank" || page.url().startsWith("chrome-extension://"));
|
|
122
123
|
const closing = [];
|
|
123
124
|
for (const page of context.pages()) if (isStray(page)) closing.push(page.close());
|
|
124
125
|
await Promise.allSettled(closing);
|
|
125
126
|
};
|
|
126
|
-
/**
|
|
127
|
-
* Launch a fresh persistent context from the onboarded cache and return an unlocked wallet
|
|
128
|
-
* controller. Headed by default; `headless` needs the wallet to declare `headlessApprovals`, since
|
|
129
|
-
* some wallets create an approval window headless that is never exposed as a page. Outside
|
|
130
|
-
* Playwright fixtures, `context.close()` is yours.
|
|
131
|
-
*/
|
|
127
|
+
/** Launch and unlock a disposable profile copy. Headless mode requires verified wallet support. */
|
|
132
128
|
const launchWallet = async (setup, { headless = false } = {}) => {
|
|
133
129
|
const definition = wallets[setup.wallet];
|
|
134
130
|
if (headless && definition.headlessApprovals !== true) throw new Error(`[walletwright] ${definition.extensionName} has no verified headless approval flow; run this suite headed (\`use: { headless: false }\`, or \`--headed\`).`);
|
|
135
131
|
const cacheDir = path.resolve(setup.cacheDir ?? ".walletwright");
|
|
136
132
|
const profileDir = path.join(cacheDir, profileKey(setup));
|
|
137
|
-
|
|
133
|
+
await restorePreviousProfile(profileDir);
|
|
134
|
+
try {
|
|
135
|
+
await stat(profileDir);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
throw new Error(`[walletwright] no cache for this setup at ${profileDir}. Build it first with buildCache() or \`walletwright cache\`.`, { cause: error });
|
|
138
|
+
}
|
|
138
139
|
const extensionPath = await definition.prepareExtension(cacheDir, setup.version);
|
|
140
|
+
const extensionId = await extensionIdFromPath(extensionPath);
|
|
141
|
+
/** Cleanup must cover copy and launch failures that occur before a context exists. */
|
|
139
142
|
const runDir = await mkdtemp(path.join(os.tmpdir(), "walletwright-"));
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
await rm(runDir, {
|
|
144
|
-
force: true,
|
|
145
|
-
recursive: true
|
|
146
|
-
}).catch(() => {});
|
|
143
|
+
const removeRunDir = () => rm(runDir, {
|
|
144
|
+
force: true,
|
|
145
|
+
recursive: true
|
|
147
146
|
});
|
|
148
|
-
const
|
|
147
|
+
const handleContextClose = async () => {
|
|
148
|
+
try {
|
|
149
|
+
await removeRunDir();
|
|
150
|
+
} catch {}
|
|
151
|
+
};
|
|
152
|
+
let context;
|
|
149
153
|
try {
|
|
150
|
-
await
|
|
151
|
-
|
|
154
|
+
await cp(profileDir, runDir, { recursive: true });
|
|
155
|
+
context = await chromium.launchPersistentContext(runDir, extensionContextOptions(extensionPath, headless));
|
|
156
|
+
const launched = context;
|
|
157
|
+
launched.on("close", handleContextClose);
|
|
158
|
+
await definition.prepareContext?.(launched);
|
|
159
|
+
const home = await definition.reachUnlockScreen(launched, extensionId);
|
|
152
160
|
await definition.unlock(home, setup.password);
|
|
153
|
-
await closeStrayPages(
|
|
161
|
+
await closeStrayPages(launched, home);
|
|
154
162
|
return {
|
|
155
|
-
|
|
163
|
+
close: async () => {
|
|
164
|
+
launched.off("close", handleContextClose);
|
|
165
|
+
await closeLaunch(launched, runDir);
|
|
166
|
+
},
|
|
167
|
+
context: launched,
|
|
156
168
|
wallet: createWallet({
|
|
157
|
-
context,
|
|
169
|
+
context: launched,
|
|
158
170
|
definition,
|
|
159
171
|
extensionId,
|
|
160
172
|
home,
|
|
@@ -162,26 +174,16 @@ const launchWallet = async (setup, { headless = false } = {}) => {
|
|
|
162
174
|
})
|
|
163
175
|
};
|
|
164
176
|
} catch (error) {
|
|
165
|
-
|
|
177
|
+
context?.off("close", handleContextClose);
|
|
178
|
+
await context?.close().catch(() => {});
|
|
179
|
+
await removeRunDir().catch(() => {});
|
|
166
180
|
throw error;
|
|
167
181
|
}
|
|
168
182
|
};
|
|
169
183
|
|
|
170
184
|
//#endregion
|
|
171
185
|
//#region src/fixtures.ts
|
|
172
|
-
/**
|
|
173
|
-
* Build a Playwright `test` whose `context`/`page` run inside a persistent Chromium with the wallet
|
|
174
|
-
* loaded and unlocked, exposing a `wallet` fixture to approve connect/sign popups.
|
|
175
|
-
*
|
|
176
|
-
* ```ts
|
|
177
|
-
* const test = createWalletFixtures(setup);
|
|
178
|
-
* test("connect", async ({ page, wallet }) => {
|
|
179
|
-
* await page.goto("/");
|
|
180
|
-
* await page.getByRole("button", { name: "Connect" }).click();
|
|
181
|
-
* await wallet.connectToDapp();
|
|
182
|
-
* });
|
|
183
|
-
* ```
|
|
184
|
-
*/
|
|
186
|
+
/** Create Playwright fixtures backed by one unlocked wallet launch per test. */
|
|
185
187
|
const createWalletFixtures = (setup) => test.extend({
|
|
186
188
|
context: async ({ walletLaunch }, use) => {
|
|
187
189
|
await use(walletLaunch.context);
|
|
@@ -192,10 +194,10 @@ const createWalletFixtures = (setup) => test.extend({
|
|
|
192
194
|
walletLaunch: async ({ headless }, use) => {
|
|
193
195
|
const launched = await launchWallet(setup, { headless });
|
|
194
196
|
await use(launched);
|
|
195
|
-
await launched.
|
|
197
|
+
await launched.close();
|
|
196
198
|
}
|
|
197
199
|
});
|
|
198
200
|
|
|
199
201
|
//#endregion
|
|
200
|
-
export { buildCache, createWalletFixtures, launchWallet, walletKindsByEcosystem, wallets };
|
|
202
|
+
export { buildCache, createWalletFixtures, isWalletKind, launchWallet, walletKindsByEcosystem, wallets };
|
|
201
203
|
//# sourceMappingURL=index.mjs.map
|
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 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":";;;;;;;;AAYA,MAAM,yBAAyB;;AAE/B,MAAM,4BAA4B;;AAElC,MAAM,4BAA4B;;AAalC,MAAa,gBAAgB,EAC3B,SACA,YACA,aACA,MACA,eACiC;CACjC,MAAM,QAAQ,WAAW;CACzB,MAAM,MAA2B;EAAE;EAAS;EAAa;EAAM;CAAS;;;;;;;CAQxE,MAAM,YAAY,YAA2B;EAE3C,MADa,QAAQ,MAAM,CAAC,CAAC,MAAM,SAAS,0BAAU,CAAC,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,CAAC,KAAK,SAAS,CAClF,CAAC,EAAE,aAAa,CAAC,CAAC,YAAY,CAAC,CAAC;CAC3C;;CAGA,MAAM,gBAAgB,aACpB,sBAAsB;EACpB,kBAAkB,WAAW;EAC7B;EACA;EACA;EACA,WAAW,WAAW,4BAA4B;CACpD,CAAC;;CAGH,MAAM,eAAe,OACnB,QACA,EAAE,WAAW,YACK;EAClB,MAAM,UAAU;EAChB,MAAM,aAAa,aAAa,QAAQ;EACxC,MAAM,QAAQ,MAAM,KAAK;EACzB,IAAI,CAAC,OAAO;GACV,IAAI,UACF;GAEF,MAAM,OAAO,QAAQ,MAAM,CAAC,CAAC,KAAK,SAAS,KAAK,IAAI,CAAC;GACrD,MAAM,IAAI,MACR,6DAA6D,KAAK,KAAK,KAAK,KAAK,QACnF;EACF;EACA,MAAM,oBAAoB,KAAK;EAC/B,IAAI;GACF,MAAM,OAAO,KAAK;EACpB,SAAS,OAAO;GACd,IAAI,CAAC,MAAM,SAAS,GAClB,MAAM;GAER,MAAM,QAAQ,MAAM,KAAK;GACzB,IAAI,CAAC,OACH,MAAM;GAER,MAAM,OAAO,KAAK;EACpB;EAEA,MAAM,SAAS,MAAM,gBAAgB,CAAC,qBAAqB,SAAS,aAAa,KAAK,GAAG,EACvF,WAAW,uBACb,CAAC;EACD,IAAI,CAAC,YAAY,WAAW,QAC1B,MAAM,IAAI,MACR,qDAAqD,cAAc,sBAAsB,EAAE,wCAC7F;CAEJ;CAEA,MAAM,eAAe,yBACnB,IAAI,MAAM,kBAAkB,WAAW,cAAc,oBAAoB,KAAK,GAAG;CAEnF,MAAM,WAAW,UAA0B,CAAC,MAC1C,cAAc,UAAU,WAAW,QAAQ,OAAO,QAAQ,GAAG,OAAO;CAEtE,MAAM,SAAS,OAAO,UAA0B,CAAC,MAAqB;EACpE,MAAM,EAAE,QAAQ,gBAAgB;EAChC,IAAI,CAAC,aACH,MAAM,YAAY,QAAQ;EAE5B,MAAM,cAAc,UAAU,YAAY,KAAK,GAAG,OAAO;CAC3D;;;;;;;;;CAUA,MAAM,UAEF,IACA,SAEF,OAAO,GAAG,SAA2B;EACnC,IAAI,CAAC,IACH,MAAM,YAAY,IAAI;EAExB,IAAI,KAAK,SAAS,GAChB,MAAM,IAAI,MAAM,yDAAyD,KAAK,GAAG;EAEnF,MAAM,KAAK,aAAa,CAAC,CAAC,YAAY,CAAC,CAAC;EACxC,MAAM,GAAG,KAAK,GAAG,IAAI;EACrB,MAAM,UAAU;CAClB;CAEF,OAAO;EACL,UAAU;GACR,KAAK,OAAO,WAAW,SAAS,UAAU,KAAK,cAAc;GAC7D,kBAAkB,OAChB,WAAW,SAAS,UAAU,kBAC9B,2BACF;GACA,QAAQ,OAAO,WAAW,SAAS,UAAU,QAAQ,iBAAiB;GACtE,QAAQ,OAAO,WAAW,SAAS,UAAU,QAAQ,iBAAiB;EACxE;EACA;EACA,wBAAwB,QAAQ;EAChC,0BAA0B,QAAQ;EAClC,qBAAqB,QAAQ,EAAE,UAAU,KAAK,CAAC;EAC/C;EACA;EACA,SAAS;GACP,KAAK,OAAO,WAAW,SAAS,SAAS,KAAK,aAAa;GAC3D,QAAQ,OAAO,WAAW,SAAS,SAAS,QAAQ,gBAAgB;EACtE;EACA;EACA,wBAAwB,OAAO;EAC/B,uBAAuB,OAAO;EAC9B,yBAAyB,OAAO;EAChC,UAAU;GACR,MAAM,OAAO,WAAW,SAAS,UAAU,MAAM,eAAe;GAChE,QAAQ,OAAO,WAAW,SAAS,UAAU,QAAQ,iBAAiB;EACxE;CACF;AACF;;;;;;;;;;AChJA,MAAM,kBAAkB,OAAO,SAAyB,SAA8B;CACpF,MAAM,WAAW,SACf,SAAS,QACT,CAAC,KAAK,SAAS,MACd,KAAK,IAAI,MAAM,iBAAiB,KAAK,IAAI,CAAC,CAAC,WAAW,qBAAqB;CAE9E,MAAM,UAAgC,CAAC;CACvC,KAAK,MAAM,QAAQ,QAAQ,MAAM,GAC/B,IAAI,QAAQ,IAAI,GACd,QAAQ,KAAK,KAAK,MAAM,CAAC;CAG7B,MAAM,QAAQ,WAAW,OAAO;AAClC;;;;;;;AAQA,MAAa,eAAe,OAC1B,OACA,EAAE,WAAW,UAAkC,CAAC,MACpB;CAC5B,MAAM,aAAa,QAAQ,MAAM;CACjC,IAAI,YAAY,WAAW,sBAAsB,MAC/C,MAAM,IAAI,MACR,kBAAkB,WAAW,cAAc,gHAC7C;CAEF,MAAM,WAAW,KAAK,QAAQ,MAAM,2BAA6B;CACjE,MAAM,aAAa,KAAK,KAAK,UAAU,WAAW,KAAK,CAAC;CACxD,IAAI,CAAC,WAAW,UAAU,GACxB,MAAM,IAAI,MACR,6CAA6C,WAAW,8DAC1D;CAGF,MAAM,gBAAgB,MAAM,WAAW,iBAAiB,UAAU,MAAM,OAAO;CAE/E,MAAM,SAAS,MAAM,QAAQ,KAAK,KAAK,GAAG,OAAO,GAAG,eAAe,CAAC;CACpE,MAAM,GAAG,YAAY,QAAQ,EAAE,WAAW,KAAK,CAAC;CAEhD,MAAM,UAAU,MAAM,SAAS,wBAC7B,QACA,wBAAwB,eAAe,QAAQ,CACjD;CAEA,QAAQ,GAAG,SAAS,YAAY;EAC9B,MAAM,GAAG,QAAQ;GAAE,OAAO;GAAM,WAAW;EAAK,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC;CACnE,CAAC;CAED,MAAM,cAAc,oBAAoB,aAAa;CAErD,IAAI;EACF,MAAM,WAAW,iBAAiB,OAAO;EACzC,MAAM,OAAO,MAAM,WAAW,kBAAkB,SAAS,WAAW;EACpE,MAAM,WAAW,OAAO,MAAM,MAAM,QAAQ;EAC5C,MAAM,gBAAgB,SAAS,IAAI;EAEnC,OAAO;GACL;GACA,QAAQ,aAAa;IACnB;IACA;IACA;IACA;IACA,UAAU,MAAM;GAClB,CAAC;EACH;CACF,SAAS,OAAO;EACd,MAAM,QAAQ,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;EACpC,MAAM;CACR;AACF;;;;;;;;;;;;;;;;;ACzEA,MAAa,wBAAwB,UACnCA,KAAK,OAAuC;CAC1C,SAAS,OAAO,EAAE,gBAAgB,QAAQ;EACxC,MAAM,IAAI,aAAa,OAAO;CAChC;CACA,QAAQ,OAAO,EAAE,gBAAgB,QAAQ;EACvC,MAAM,IAAI,aAAa,MAAM;CAC/B;CACA,cAAc,OAAO,EAAE,YAAY,QAAQ;EACzC,MAAM,WAAW,MAAM,aAAa,OAAO,EAAE,SAAS,CAAC;EACvD,MAAM,IAAI,QAAQ;EAClB,MAAM,SAAS,QAAQ,MAAM;CAC/B;AACF,CAAC"}
|
|
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 { DEFAULT_NOTIFICATION_MATCH, findNotificationPopup, placeApprovalWindow } 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\n/**\n * Drive `popup` with `settle` and return the page that actually took the approval. A popup that\n * closes mid-settle is the wallet respawning the request, so re-acquire once and drive the new one.\n */\nconst settleApproval = async (\n settle: (popup: Page) => Promise<void>,\n popup: Page,\n acquire: () => Promise<Page | undefined>,\n): Promise<Page> => {\n try {\n await settle(popup);\n return popup;\n } catch (error) {\n if (!popup.isClosed()) {\n throw error;\n }\n const fresh = await acquire();\n if (!fresh) {\n throw error;\n }\n await settle(fresh);\n return fresh;\n }\n};\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 /** MetaMask opens requests inline while its extension page owns focus. */\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 /** Find and place the approval window before its controls are driven. */\n const acquireApproval = async (optional: boolean): Promise<Page | undefined> => {\n const popup = await findNotificationPopup({\n approvalControls: definition.approvalControls,\n context,\n extensionId,\n match,\n timeoutMs: optional ? OPTIONAL_POPUP_TIMEOUT_MS : REQUIRED_POPUP_TIMEOUT_MS,\n });\n if (popup) {\n await placeApprovalWindow(popup);\n }\n return popup;\n };\n\n /** Drive the pending approval popup with `settle`, then wait for that popup to close. */\n const resolvePopup = async (\n settle: (popup: Page) => Promise<void>,\n { optional = false }: ResolveOptions,\n ): Promise<void> => {\n await frontDapp();\n const acquire = () => acquireApproval(optional);\n const popup = await acquire();\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\n const driven = await settleApproval(settle, popup, acquire);\n\n /** Wait for the exact popup we drove; chained popups may reuse the same URL. */\n const closed = await waitUntil(() => driven.isClosed() || undefined, {\n timeoutMs: POPUP_CLOSE_TIMEOUT_MS,\n });\n if (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 = (options: ResolveOptions = {}) =>\n resolvePopup((popup) => definition.reject(popup), options);\n\n /** Bind a verified wallet action and restore focus to the dapp when it finishes. */\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 { cp, mkdtemp, rm, stat } 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 { restorePreviousProfile } from \"./cache\";\nimport { extensionContextOptions } from \"./chromium\";\nimport { createWallet } from \"./controller\";\nimport { DEFAULT_CACHE_DIR, extensionIdFromPath, profileKey } from \"./utils\";\n\nexport type LaunchedWallet = {\n /**\n * Close the browser and remove the throwaway profile copy, in that order. Prefer this over\n * `context.close()`: the copy is a full onboarded profile (tens of MB), and awaiting its removal is\n * the only way to know it is gone before the process exits.\n */\n close: () => Promise<void>;\n context: BrowserContext;\n wallet: Wallet;\n};\n\nconst closeLaunch = async (context: BrowserContext, runDir: string): Promise<void> => {\n const closed = await Promise.allSettled([context.close()]);\n const removed = await Promise.allSettled([rm(runDir, { force: true, recursive: true })]);\n const closeResult = closed[0];\n const removeResult = removed[0];\n\n if (closeResult?.status === \"rejected\" && removeResult?.status === \"rejected\") {\n throw new AggregateError(\n [closeResult.reason, removeResult.reason],\n `[walletwright] failed to close the browser and remove ${runDir}`,\n );\n }\n if (closeResult?.status === \"rejected\") {\n throw closeResult.reason;\n }\n if (removeResult?.status === \"rejected\") {\n throw removeResult.reason;\n }\n};\n\n/** Close Chromium's blank tab and the extension's auto-opened tab. */\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/** Launch and unlock a disposable profile copy. Headless mode requires verified wallet support. */\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 await restorePreviousProfile(profileDir);\n try {\n await stat(profileDir);\n } catch (error) {\n throw new Error(\n `[walletwright] no cache for this setup at ${profileDir}. Build it first with buildCache() or \\`walletwright cache\\`.`,\n { cause: error },\n );\n }\n\n const extensionPath = await definition.prepareExtension(cacheDir, setup.version);\n\n const extensionId = await extensionIdFromPath(extensionPath);\n\n /** Cleanup must cover copy and launch failures that occur before a context exists. */\n const runDir = await mkdtemp(path.join(os.tmpdir(), \"walletwright-\"));\n const removeRunDir = (): Promise<void> => rm(runDir, { force: true, recursive: true });\n const handleContextClose = async (): Promise<void> => {\n try {\n await removeRunDir();\n } catch {\n // Playwright does not await close-event listeners; `close()` owns strict cleanup.\n }\n };\n\n let context: BrowserContext | undefined;\n try {\n await cp(profileDir, runDir, { recursive: true });\n\n context = await chromium.launchPersistentContext(\n runDir,\n extensionContextOptions(extensionPath, headless),\n );\n const launched = context;\n launched.on(\"close\", handleContextClose);\n\n await definition.prepareContext?.(launched);\n const home = await definition.reachUnlockScreen(launched, extensionId);\n await definition.unlock(home, setup.password);\n await closeStrayPages(launched, home);\n\n return {\n close: async () => {\n launched.off(\"close\", handleContextClose);\n await closeLaunch(launched, runDir);\n },\n context: launched,\n wallet: createWallet({\n context: launched,\n definition,\n extensionId,\n home,\n password: setup.password,\n }),\n };\n } catch (error) {\n context?.off(\"close\", handleContextClose);\n await context?.close().catch(() => {});\n await removeRunDir().catch(() => {});\n throw error;\n }\n};\n\nexport { closeLaunch };\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/** Create Playwright fixtures backed by one unlocked wallet launch per test. */\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.close();\n },\n });\n"],"mappings":"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"}
|
package/dist/mock-standard.d.mts
CHANGED
|
@@ -1,18 +1,7 @@
|
|
|
1
1
|
import { BrowserContext, Page } from "@playwright/test";
|
|
2
2
|
//#region src/mock-standard.d.ts
|
|
3
|
-
/**
|
|
4
|
-
* A headless Wallet-Standard fake for Solana dapp tests that don't need a real extension. It
|
|
5
|
-
* hand-rolls the Wallet-Standard registration protocol (no browser-side library) and answers
|
|
6
|
-
* `standard:connect` plus `solana:signMessage`, signing with a real ed25519 key so signatures
|
|
7
|
-
* verify. Isolated from the EVM `mock.ts`; `@walletwright/core/mock-standard` is its own entry
|
|
8
|
-
* point.
|
|
9
|
-
*
|
|
10
|
-
* ed25519 signing runs Node-side (viem is secp256k1-only), reusing the same exposeFunction bridge
|
|
11
|
-
* pattern as the EVM mock. No extra dependency: Node's built-in `crypto` signs ed25519 natively.
|
|
12
|
-
*/
|
|
13
|
-
type MockEcosystem = "svm";
|
|
3
|
+
/** Headless Wallet-Standard Solana mock that signs messages with Node's ed25519 implementation. */
|
|
14
4
|
type MockStandardWalletOptions = {
|
|
15
|
-
ecosystem?: MockEcosystem;
|
|
16
5
|
/** Wallet name announced over Wallet Standard. */
|
|
17
6
|
name?: string;
|
|
18
7
|
/** 32-byte ed25519 seed as hex. Defaults to a fixed test seed so the address is stable. */
|
|
@@ -25,17 +14,13 @@ type MockStandardAccount = {
|
|
|
25
14
|
};
|
|
26
15
|
declare const encodeBase58: (bytes: Uint8Array) => string;
|
|
27
16
|
declare const privateKeyFromSeed: (seed: Buffer) => import("node:crypto").KeyObject;
|
|
17
|
+
/** `message` is required: defaulting it would sign the empty message instead of rejecting. */
|
|
28
18
|
type BridgeRequest = {
|
|
29
|
-
message
|
|
19
|
+
message: Array<number>;
|
|
30
20
|
method: string;
|
|
31
21
|
};
|
|
32
22
|
declare const createStandardHandler: (seed: Buffer) => ({ message, method }: BridgeRequest) => Promise<Array<number>>;
|
|
33
|
-
/**
|
|
34
|
-
* Install the headless Wallet-Standard mock for Solana connect + sign on a context (every page) or a
|
|
35
|
-
* single page. Call before `goto`, so the wallet is registered when the dapp discovers wallets.
|
|
36
|
-
* Returns the account it announces so a test can verify signatures against the public key.
|
|
37
|
-
*/
|
|
38
23
|
declare const installMockStandardWallet: (target: BrowserContext | Page, options?: MockStandardWalletOptions) => Promise<MockStandardAccount>;
|
|
39
24
|
//#endregion
|
|
40
|
-
export { type
|
|
25
|
+
export { type MockStandardAccount, type MockStandardWalletOptions, createStandardHandler, encodeBase58, installMockStandardWallet, privateKeyFromSeed };
|
|
41
26
|
//# sourceMappingURL=mock-standard.d.mts.map
|
package/dist/mock-standard.mjs
CHANGED
|
@@ -35,7 +35,7 @@ const rawPublicKey = (seed) => {
|
|
|
35
35
|
};
|
|
36
36
|
const signEd25519 = (seed, message) => [...sign(null, Buffer.from(message), privateKeyFromSeed(seed))];
|
|
37
37
|
const SVM_CHAINS = ["solana:mainnet", "solana:devnet"];
|
|
38
|
-
const createStandardHandler = (seed) => ({ message
|
|
38
|
+
const createStandardHandler = (seed) => ({ message, method }) => {
|
|
39
39
|
switch (method) {
|
|
40
40
|
case "solana:signMessage": return Promise.resolve(signEd25519(seed, Uint8Array.from(message)));
|
|
41
41
|
default: return Promise.reject(/* @__PURE__ */ new Error(`[walletwright/mock-standard] unsupported method: ${method}`));
|
|
@@ -46,22 +46,28 @@ const createStandardHandler = (seed) => ({ message = [], method }) => {
|
|
|
46
46
|
* single page. Call before `goto`, so the wallet is registered when the dapp discovers wallets.
|
|
47
47
|
* Returns the account it announces so a test can verify signatures against the public key.
|
|
48
48
|
*/
|
|
49
|
+
/** Per install, for the same reason as the EVM mock: see the note on `installCount` in `mock.ts`. */
|
|
50
|
+
let installCount = 0;
|
|
49
51
|
const installMockStandardWallet = async (target, options = {}) => {
|
|
50
|
-
const {
|
|
51
|
-
if (ecosystem !== "svm") throw new Error(`[walletwright/mock-standard] prototype supports only svm, got ${String(ecosystem)}`);
|
|
52
|
+
const { name = "Walletwright Mock", seedHex = DEFAULT_SEED_HEX } = options;
|
|
52
53
|
const seed = Buffer.from(seedHex, "hex");
|
|
53
54
|
if (seed.length !== 32) throw new Error("[walletwright/mock-standard] seedHex must be 32 bytes");
|
|
54
55
|
const publicKey = rawPublicKey(seed);
|
|
55
56
|
const address = encodeBase58(publicKey);
|
|
56
57
|
const publicKeyHex = publicKey.toString("hex");
|
|
57
58
|
const handle = createStandardHandler(seed);
|
|
58
|
-
const bindingName =
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
const bindingName = `__walletwrightMockStandardSign_${installCount++}`;
|
|
60
|
+
await target.exposeFunction(bindingName, (rpc) => handle(rpc));
|
|
61
|
+
const initArgs = [bindingName, {
|
|
62
|
+
address,
|
|
63
|
+
chains: [...SVM_CHAINS],
|
|
64
|
+
icon: "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'/>",
|
|
65
|
+
name,
|
|
66
|
+
publicKeyHex
|
|
67
|
+
}];
|
|
64
68
|
await target.addInitScript(([binding, info]) => {
|
|
69
|
+
const hasBinding = (value) => typeof Object.getOwnPropertyDescriptor(value, binding)?.value === "function";
|
|
70
|
+
if (!hasBinding(window)) throw new TypeError(`Missing Playwright binding: ${binding}`);
|
|
65
71
|
const call = window[binding];
|
|
66
72
|
const publicKeyBytes = Uint8Array.from((info.publicKeyHex.match(new RegExp(".{2}", "gv")) ?? []).map((byte) => Number.parseInt(byte, 16)));
|
|
67
73
|
const account = {
|
|
@@ -107,13 +113,7 @@ const installMockStandardWallet = async (target, options = {}) => {
|
|
|
107
113
|
window.addEventListener("wallet-standard:app-ready", (event) => {
|
|
108
114
|
callback(event.detail);
|
|
109
115
|
});
|
|
110
|
-
},
|
|
111
|
-
address,
|
|
112
|
-
chains: [...SVM_CHAINS],
|
|
113
|
-
icon: "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'/>",
|
|
114
|
-
name,
|
|
115
|
-
publicKeyHex
|
|
116
|
-
}]);
|
|
116
|
+
}, initArgs);
|
|
117
117
|
return {
|
|
118
118
|
address,
|
|
119
119
|
publicKeyHex
|
|
@@ -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
|
|
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/** Headless Wallet-Standard Solana mock that signs messages with Node's ed25519 implementation. */\ntype MockStandardWalletOptions = {\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\n/** `message` is required: defaulting it would sign the empty message instead of rejecting. */\ntype BridgeRequest = { message: Array<number>; method: string };\ntype StandardInitInfo = {\n address: string;\n chains: ReadonlyArray<string>;\n icon: string;\n name: string;\n publicKeyHex: string;\n};\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 */\n/** Per install, for the same reason as the EVM mock: see the note on `installCount` in `mock.ts`. */\nlet installCount = 0;\n\nconst installMockStandardWallet = async (\n target: BrowserContext | Page,\n options: MockStandardWalletOptions = {},\n): Promise<MockStandardAccount> => {\n const { name = \"Walletwright Mock\", seedHex = DEFAULT_SEED_HEX } = options;\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_${installCount++}`;\n await target.exposeFunction(bindingName, (rpc: BridgeRequest) => handle(rpc));\n const initArgs = [\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 ] satisfies readonly [string, StandardInitInfo];\n\n await target.addInitScript(([binding, info]) => {\n const hasBinding = <Value extends object>(\n value: Value,\n ): value is Value & Record<string, (rpc: BridgeRequest) => Promise<Array<number>>> =>\n typeof Object.getOwnPropertyDescriptor(value, binding)?.value === \"function\";\n if (!hasBinding(window)) {\n throw new TypeError(`Missing Playwright binding: ${binding}`);\n }\n const call = window[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 // SAFETY: Wallet Standard dispatches this named event with the register callback contract.\n callback((event as CustomEvent<{ register: (w: typeof wallet) => void }>).detail);\n });\n }, initArgs);\n\n return { address, publicKeyHex };\n};\n\nexport { createStandardHandler, encodeBase58, installMockStandardWallet, privateKeyFromSeed };\nexport type { MockStandardAccount, MockStandardWalletOptions };\n"],"mappings":"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"}
|
package/dist/mock.d.mts
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import { BrowserContext, Page } from "@playwright/test";
|
|
2
2
|
import { PrivateKeyAccount } from "viem/accounts";
|
|
3
3
|
//#region src/mock.d.ts
|
|
4
|
-
/**
|
|
5
|
-
* A headless injected-wallet fake for dapp tests that don't need a real extension. It announces
|
|
6
|
-
* itself over EIP-6963 and answers the EIP-1193 requests a connect/sign flow makes, signing with a
|
|
7
|
-
* real key so signatures verify. `@walletwright/core/mock` is a separate entry point that needs
|
|
8
|
-
* `viem` (optional peer); the extension-driving core doesn't.
|
|
9
|
-
*/
|
|
4
|
+
/** Headless EIP-6963 and EIP-1193 mock that signs with a real key. */
|
|
10
5
|
type MockWalletOptions = {
|
|
11
6
|
chainId?: number;
|
|
12
7
|
/** Wallet name announced over EIP-6963. */
|
|
@@ -18,8 +13,9 @@ type Rpc = {
|
|
|
18
13
|
method: string;
|
|
19
14
|
params?: ReadonlyArray<unknown>;
|
|
20
15
|
};
|
|
16
|
+
type RpcResult = ReadonlyArray<string> | string | null;
|
|
21
17
|
/** The EIP-1193 handler a mock provider answers with. Exported for direct, browser-free testing. */
|
|
22
|
-
declare const createRpcHandler: (account: PrivateKeyAccount, chainIdHex: string) => ({ method, params }: Rpc) => Promise<
|
|
18
|
+
declare const createRpcHandler: (account: PrivateKeyAccount, chainIdHex: string) => ({ method, params }: Rpc) => Promise<RpcResult>;
|
|
23
19
|
/**
|
|
24
20
|
* Install the mock on a context (every page) or a single page. Call before `goto`, so the provider
|
|
25
21
|
* exists when the dapp looks for it. Returns the account address it announces.
|
package/dist/mock.mjs
CHANGED
|
@@ -20,6 +20,8 @@ const createRpcHandler = (account, chainIdHex) => ({ method, params = [] }) => {
|
|
|
20
20
|
default: return Promise.reject(/* @__PURE__ */ new Error(`[walletwright/mock] unsupported method: ${method}`));
|
|
21
21
|
}
|
|
22
22
|
};
|
|
23
|
+
/** Each install needs its own Playwright binding and EIP-6963 identity. */
|
|
24
|
+
let installCount = 0;
|
|
23
25
|
/**
|
|
24
26
|
* Install the mock on a context (every page) or a single page. Call before `goto`, so the provider
|
|
25
27
|
* exists when the dapp looks for it. Returns the account address it announces.
|
|
@@ -29,13 +31,18 @@ const installMockWallet = async (target, options = {}) => {
|
|
|
29
31
|
const account = privateKeyToAccount(privateKey);
|
|
30
32
|
const chainIdHex = toHex(chainId);
|
|
31
33
|
const handle = createRpcHandler(account, chainIdHex);
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
const install = installCount++;
|
|
35
|
+
const bindingName = `__walletwrightMockRpc_${install}`;
|
|
36
|
+
await target.exposeFunction(bindingName, (rpc) => handle(rpc));
|
|
37
|
+
const initArgs = [bindingName, {
|
|
38
|
+
icon: "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'/>",
|
|
39
|
+
name,
|
|
40
|
+
rdns: "sh.walletwright.mock",
|
|
41
|
+
uuid: `00000000-0000-0000-0000-${String(install).padStart(12, "0")}`
|
|
42
|
+
}];
|
|
38
43
|
await target.addInitScript(([binding, info]) => {
|
|
44
|
+
const hasBinding = (value) => typeof Object.getOwnPropertyDescriptor(value, binding)?.value === "function";
|
|
45
|
+
if (!hasBinding(window)) throw new TypeError(`Missing Playwright binding: ${binding}`);
|
|
39
46
|
const call = window[binding];
|
|
40
47
|
const provider = {
|
|
41
48
|
isMetaMask: true,
|
|
@@ -52,12 +59,7 @@ const installMockWallet = async (target, options = {}) => {
|
|
|
52
59
|
};
|
|
53
60
|
window.addEventListener("eip6963:requestProvider", announce);
|
|
54
61
|
announce();
|
|
55
|
-
},
|
|
56
|
-
icon: "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'/>",
|
|
57
|
-
name,
|
|
58
|
-
rdns: "sh.walletwright.mock",
|
|
59
|
-
uuid: "00000000-0000-0000-0000-000000000000"
|
|
60
|
-
}]);
|
|
62
|
+
}, initArgs);
|
|
61
63
|
return account.address;
|
|
62
64
|
};
|
|
63
65
|
|
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
|
|
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/** Headless EIP-6963 and EIP-1193 mock that signs with a real key. */\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> };\ntype RpcResult = ReadonlyArray<string> | string | null;\n\nconst toHex = (value: number): `0x${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<RpcResult> => {\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/** Each install needs its own Playwright binding and EIP-6963 identity. */\nlet installCount = 0;\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 install = installCount++;\n const bindingName = `__walletwrightMockRpc_${install}`;\n await target.exposeFunction(bindingName, (rpc: Rpc) => handle(rpc));\n const initArgs = [\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-${String(install).padStart(12, \"0\")}`,\n },\n ] satisfies readonly [string, { icon: string; name: string; rdns: string; uuid: string }];\n\n await target.addInitScript(([binding, info]) => {\n const hasBinding = <Value extends object>(\n value: Value,\n ): value is Value & Record<string, (rpc: Rpc) => Promise<RpcResult>> =>\n typeof Object.getOwnPropertyDescriptor(value, binding)?.value === \"function\";\n if (!hasBinding(window)) {\n throw new TypeError(`Missing Playwright binding: ${binding}`);\n }\n const call = window[binding];\n const provider = {\n isMetaMask: true,\n on: () => provider,\n removeListener: () => provider,\n request: (rpc: Rpc) => call(rpc),\n };\n // SAFETY: EIP-1193 providers own window.ethereum, which the DOM library does not declare.\n (window as Window & { ethereum?: typeof provider }).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 }, initArgs);\n\n return account.address;\n};\n\nexport { createRpcHandler, installMockWallet };\n"],"mappings":";;;AAaA,MAAM,cAAc;AAKpB,MAAM,SAAS,UAAiC,KAAK,MAAM,SAAS,EAAE;AAEtE,MAAM,gBAAgB,UAA0C,kCAAiB,CAAC,CAAC,KAAK,KAAK;;AAG7F,MAAM,oBACH,SAA4B,gBAC5B,EAAE,QAAQ,SAAS,CAAC,QAAiC;CACpD,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;;AAGF,IAAI,eAAe;;;;;AAMnB,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,UAAU;CAChB,MAAM,cAAc,yBAAyB;CAC7C,MAAM,OAAO,eAAe,cAAc,QAAa,OAAO,GAAG,CAAC;CAClE,MAAM,WAAW,CACf,aACA;EACE,MAAM;EACN;EACA,MAAM;EACN,MAAM,2BAA2B,OAAO,OAAO,CAAC,CAAC,SAAS,IAAI,GAAG;CACnE,CACF;CAEA,MAAM,OAAO,eAAe,CAAC,SAAS,UAAU;EAC9C,MAAM,cACJ,UAEA,OAAO,OAAO,yBAAyB,OAAO,OAAO,CAAC,EAAE,UAAU;EACpE,IAAI,CAAC,WAAW,MAAM,GACpB,MAAM,IAAI,UAAU,+BAA+B,SAAS;EAE9D,MAAM,OAAO,OAAO;EACpB,MAAM,WAAW;GACf,YAAY;GACZ,UAAU;GACV,sBAAsB;GACtB,UAAU,QAAa,KAAK,GAAG;EACjC;EAEA,AAAC,OAAmD,WAAW;EAE/D,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,GAAG,QAAQ;CAEX,OAAO,QAAQ;AACjB"}
|