w3wallets 0.10.2 → 1.0.0-beta.10

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/dist/index.mjs CHANGED
@@ -1,111 +1,678 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
1
8
  // src/withWallets.ts
9
+ import path3 from "path";
10
+ import fs3 from "fs";
11
+ import {
12
+ chromium as chromium2
13
+ } from "@playwright/test";
14
+
15
+ // src/cache/types.ts
16
+ function isCachedConfig(config2) {
17
+ return "__cached" in config2 && config2.__cached === true;
18
+ }
19
+
20
+ // src/cache/buildCache.ts
21
+ import path2 from "path";
22
+ import fs2 from "fs";
23
+ import { chromium } from "@playwright/test";
24
+
25
+ // src/cache/constants.ts
26
+ var CACHE_DIR = ".w3wallets/cache";
27
+
28
+ // src/core/utils.ts
2
29
  import path from "path";
3
30
  import fs from "fs";
31
+ import crypto from "crypto";
32
+ function sleep(ms) {
33
+ return new Promise((resolve) => setTimeout(resolve, ms));
34
+ }
35
+ function getExtensionId(extensionPath) {
36
+ const absolutePath = path.resolve(extensionPath);
37
+ const manifestPath = path.join(absolutePath, "manifest.json");
38
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
39
+ let dataToHash;
40
+ if (manifest.key) {
41
+ dataToHash = Buffer.from(manifest.key, "base64");
42
+ } else {
43
+ dataToHash = Buffer.from(absolutePath);
44
+ }
45
+ const hash = crypto.createHash("sha256").update(dataToHash).digest();
46
+ const ALPHABET = "abcdefghijklmnop";
47
+ let extensionId = "";
48
+ for (let i = 0; i < 16; i++) {
49
+ const byte = hash[i];
50
+ extensionId += ALPHABET[byte >> 4 & 15];
51
+ extensionId += ALPHABET[byte & 15];
52
+ }
53
+ return extensionId;
54
+ }
55
+
56
+ // src/timeouts.ts
57
+ var SERVICE_WORKER_TIMEOUT = 3e4;
58
+ var SERVICE_WORKER_POLL_INTERVAL = 500;
59
+ var POPUP_VISIBILITY_TIMEOUT = 2e3;
60
+ var SHIELD_CLOSE_TIMEOUT = 1e3;
61
+ var ARIA_CLOSE_TIMEOUT = 500;
62
+ var POPUP_HIDDEN_TIMEOUT = 3e3;
63
+ var POST_UNLOCK_TIMEOUT = 3e4;
64
+ var NOTIFICATION_CHECK_TIMEOUT = 5e3;
65
+ var POST_CLICK_TIMEOUT = 1e4;
66
+ var BUTTON_OR_POPUP_TIMEOUT = 3e4;
67
+ var LAST_RESORT_CLICK_TIMEOUT = 1e4;
68
+ var LOCK_SCREEN_TIMEOUT = 3e4;
69
+ var MENU_BUTTON_TIMEOUT = 3e4;
70
+ var ONBOARD_VISIBLE_TIMEOUT = 3e4;
71
+ var ROUTE_RETRY_TIMEOUT = 5e3;
72
+ var MAX_ROUTE_ATTEMPTS = 5;
73
+ var MNEMONIC_KEY_DELAY = 5;
74
+ var MNEMONIC_WORD_DELAY = 100;
4
75
 
5
- // tests/utils/sleep.ts
6
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
76
+ // src/debug.ts
77
+ var isDebug = () => process.env.W3WALLETS_DEBUG === "true";
78
+ function debug(message) {
79
+ if (!isDebug()) return;
80
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
81
+ console.log(`[w3wallets ${ts}] ${message}`);
82
+ }
83
+
84
+ // src/cache/buildCache.ts
85
+ function findCacheDir(walletName) {
86
+ const cacheRoot = path2.join(process.cwd(), CACHE_DIR);
87
+ if (!fs2.existsSync(cacheRoot)) return null;
88
+ const entries = fs2.readdirSync(cacheRoot, { withFileTypes: true });
89
+ for (const entry of entries) {
90
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
91
+ const metaPath = path2.join(cacheRoot, entry.name, ".meta.json");
92
+ if (!fs2.existsSync(metaPath)) continue;
93
+ try {
94
+ const meta = JSON.parse(fs2.readFileSync(metaPath, "utf-8"));
95
+ if (meta.name === walletName) {
96
+ return path2.join(cacheRoot, entry.name);
97
+ }
98
+ } catch {
99
+ continue;
100
+ }
101
+ }
102
+ return null;
103
+ }
7
104
 
8
105
  // src/withWallets.ts
9
- import {
10
- chromium
11
- } from "@playwright/test";
106
+ var W3WALLETS_DIR = ".w3wallets";
107
+ var MIN_PLAYWRIGHT_VERSION = "1.57.0";
108
+ function checkPlaywrightVersion() {
109
+ try {
110
+ const pkgPath = __require.resolve("@playwright/test/package.json");
111
+ const { version } = __require(pkgPath);
112
+ const [minMajor, minMinor, minPatch] = MIN_PLAYWRIGHT_VERSION.split(".").map(Number);
113
+ const [curMajor, curMinor, curPatch] = version.split(".").map(Number);
114
+ const isBelow = curMajor < minMajor || curMajor === minMajor && curMinor < minMinor || curMajor === minMajor && curMinor === minMinor && curPatch < minPatch;
115
+ if (isBelow) {
116
+ throw new Error(
117
+ `w3wallets requires @playwright/test >= ${MIN_PLAYWRIGHT_VERSION}, but found ${version}.
118
+ Upgrade: npm install -D @playwright/test@latest`
119
+ );
120
+ }
121
+ } catch (err) {
122
+ if (err instanceof Error && err.message.startsWith("w3wallets requires")) {
123
+ throw err;
124
+ }
125
+ }
126
+ }
127
+ function withWallets(test, ...wallets) {
128
+ checkPlaywrightVersion();
129
+ const cachedCount = wallets.filter((w) => isCachedConfig(w)).length;
130
+ if (cachedCount > 0 && cachedCount < wallets.length) {
131
+ throw new Error(
132
+ "Mixing cached and non-cached wallet configs is not supported. All wallets must be either cached (via prepareWallet) or non-cached."
133
+ );
134
+ }
135
+ const useCachedContext = cachedCount > 0;
136
+ debug(`withWallets: ${wallets.length} wallet(s), cached=${useCachedContext}`);
137
+ const extensionInfo = wallets.map((w) => {
138
+ const extPath = path3.join(process.cwd(), W3WALLETS_DIR, w.extensionDir);
139
+ ensureWalletExtensionExists(extPath, w.name);
140
+ const extensionId = w.extensionId ?? getExtensionId(extPath);
141
+ return { path: extPath, id: extensionId, name: w.name };
142
+ });
143
+ const extensionPaths = extensionInfo.map((e) => e.path);
144
+ const fixtures = {
145
+ context: async ({}, use, testInfo) => {
146
+ const userDataDir = path3.join(
147
+ process.cwd(),
148
+ W3WALLETS_DIR,
149
+ ".context",
150
+ testInfo.testId
151
+ );
152
+ cleanUserDataDir(userDataDir);
153
+ if (useCachedContext) {
154
+ const wallet = wallets[0];
155
+ const cacheDir = findCacheDir(wallet.name);
156
+ if (!cacheDir) {
157
+ throw new Error(
158
+ `Cache not found for wallet "${wallet.name}".
159
+ Searched: ${path3.join(process.cwd(), CACHE_DIR)}/
160
+ Rebuild: npx w3wallets cache --force <your-cache-dir>
161
+ Ensure your *.cache.ts setup file exports prepareWallet(...).`
162
+ );
163
+ }
164
+ fs3.cpSync(cacheDir, userDataDir, { recursive: true });
165
+ }
166
+ debug(`Launching persistent context: ${userDataDir}`);
167
+ const context = await chromium2.launchPersistentContext(userDataDir, {
168
+ headless: testInfo.project.use.headless ?? true,
169
+ channel: "chromium",
170
+ args: [
171
+ `--disable-extensions-except=${extensionPaths.join(",")}`,
172
+ `--load-extension=${extensionPaths.join(",")}`
173
+ ]
174
+ });
175
+ debug(`Waiting for ${extensionPaths.length} service worker(s)...`);
176
+ const swDeadline = Date.now() + SERVICE_WORKER_TIMEOUT;
177
+ while (context.serviceWorkers().length < extensionPaths.length) {
178
+ if (Date.now() > swDeadline) {
179
+ const found = context.serviceWorkers().length;
180
+ throw new Error(
181
+ `Service worker initialization timed out after ${SERVICE_WORKER_TIMEOUT / 1e3}s.
182
+ Expected: ${extensionPaths.length} extension(s), found: ${found} service worker(s).
183
+ Extension paths: ${extensionPaths.map((p) => path3.relative(process.cwd(), p)).join(", ")}
184
+ Suggestions:
185
+ - Check extension path exists and contains manifest.json
186
+ - Try headed mode to see what's happening: headless: false
187
+ - Ensure extension is compatible with the installed Chromium version`
188
+ );
189
+ }
190
+ await Promise.race([
191
+ context.waitForEvent("serviceworker", {
192
+ timeout: SERVICE_WORKER_TIMEOUT
193
+ }),
194
+ sleep(SERVICE_WORKER_POLL_INTERVAL)
195
+ ]);
196
+ }
197
+ debug(`All ${extensionPaths.length} service worker(s) detected`);
198
+ await use(context);
199
+ await context.close();
200
+ }
201
+ };
202
+ for (let i = 0; i < wallets.length; i++) {
203
+ const wallet = wallets[i];
204
+ const info = extensionInfo[i];
205
+ fixtures[wallet.name] = async ({ context }, use) => {
206
+ if (isCachedConfig(wallet)) {
207
+ debug(`Initializing cached wallet: ${wallet.name} (ID: ${info.id})`);
208
+ const instance = await findCachedExtension(
209
+ context,
210
+ wallet.WalletClass,
211
+ info.id,
212
+ wallet.name,
213
+ wallet.homeUrl
214
+ );
215
+ await use(instance);
216
+ } else {
217
+ debug(`Initializing fresh wallet: ${wallet.name} (ID: ${info.id})`);
218
+ const instance = await initializeExtension(
219
+ context,
220
+ wallet.WalletClass,
221
+ info.id,
222
+ wallet.name
223
+ );
224
+ await use(instance);
225
+ }
226
+ };
227
+ }
228
+ return test.extend(fixtures);
229
+ }
230
+ function cleanUserDataDir(userDataDir) {
231
+ if (fs3.existsSync(userDataDir)) {
232
+ fs3.rmSync(userDataDir, { recursive: true });
233
+ }
234
+ }
235
+ function ensureWalletExtensionExists(walletPath, walletName) {
236
+ const manifestPath = path3.join(walletPath, "manifest.json");
237
+ if (!fs3.existsSync(manifestPath)) {
238
+ const cliAlias = walletName.toLowerCase();
239
+ throw new Error(
240
+ `Cannot find ${walletName} extension.
241
+ Checked: ${manifestPath}
242
+ Download it: npx w3wallets ${cliAlias}
243
+ Custom dir: npx w3wallets -o <dir> ${cliAlias}`
244
+ );
245
+ }
246
+ }
247
+ async function findCachedExtension(context, ExtensionClass, expectedExtensionId, walletName, homeUrl) {
248
+ const expectedUrl = `chrome-extension://${expectedExtensionId}/`;
249
+ const worker = context.serviceWorkers().find((w) => w.url().startsWith(expectedUrl));
250
+ if (!worker) {
251
+ const availableIds = context.serviceWorkers().map((w) => w.url().split("/")[2]).filter(Boolean);
252
+ throw new Error(
253
+ `Service worker for ${walletName} (ID: ${expectedExtensionId}) not found in cached context.
254
+ Available IDs: [${availableIds.join(", ")}]
255
+ The cache may be stale. Rebuild: npx w3wallets cache --force
256
+ Also check extension path: ${W3WALLETS_DIR}/${walletName}/`
257
+ );
258
+ }
259
+ const page = await context.newPage();
260
+ if (homeUrl) {
261
+ await page.goto(`chrome-extension://${expectedExtensionId}/${homeUrl}`);
262
+ }
263
+ const extension = new ExtensionClass(page, expectedExtensionId);
264
+ return extension;
265
+ }
266
+ async function initializeExtension(context, ExtensionClass, expectedExtensionId, walletName) {
267
+ const expectedUrl = `chrome-extension://${expectedExtensionId}/`;
268
+ const worker = context.serviceWorkers().find((w) => w.url().startsWith(expectedUrl));
269
+ if (!worker) {
270
+ const availableIds = context.serviceWorkers().map((w) => w.url().split("/")[2]).filter(Boolean);
271
+ throw new Error(
272
+ `Service worker for ${walletName} (ID: ${expectedExtensionId}) not found. Available extension IDs: [${availableIds.join(", ")}]`
273
+ );
274
+ }
275
+ const page = await context.newPage();
276
+ const extension = new ExtensionClass(page, expectedExtensionId);
277
+ return extension;
278
+ }
12
279
 
13
- // src/backpack/backpack.ts
280
+ // src/core/types.ts
281
+ function createWallet(config2) {
282
+ return config2;
283
+ }
284
+
285
+ // src/wallets/metamask/metamask.ts
14
286
  import { expect } from "@playwright/test";
15
287
 
16
- // src/wallet.ts
288
+ // src/config.ts
289
+ var config = {
290
+ /**
291
+ * Timeout for actions like click, fill, waitFor, goto.
292
+ * Set via W3WALLETS_ACTION_TIMEOUT env variable.
293
+ * @default 30000 (30 seconds)
294
+ */
295
+ get actionTimeout() {
296
+ const value = process.env.W3WALLETS_ACTION_TIMEOUT;
297
+ return value ? parseInt(value, 10) : void 0;
298
+ },
299
+ /**
300
+ * Timeout for expect assertions like toBeVisible, toContainText.
301
+ * Set via W3WALLETS_EXPECT_TIMEOUT env variable.
302
+ * @default undefined (uses Playwright's default of 5000ms)
303
+ */
304
+ get expectTimeout() {
305
+ const value = process.env.W3WALLETS_EXPECT_TIMEOUT;
306
+ return value ? parseInt(value, 10) : void 0;
307
+ }
308
+ };
309
+
310
+ // src/core/wallet.ts
17
311
  var Wallet = class {
18
312
  constructor(page, extensionId) {
19
313
  this.page = page;
20
314
  this.extensionId = extensionId;
315
+ if (config.actionTimeout) {
316
+ page.setDefaultTimeout(config.actionTimeout);
317
+ }
21
318
  }
22
319
  };
23
320
 
24
- // src/backpack/backpack.ts
25
- var Backpack = class extends Wallet {
26
- defaultPassword = "11111111";
27
- currentAccountId = 0;
28
- maxAccountId = 0;
321
+ // src/wallets/metamask/metamask.ts
322
+ var Metamask = class extends Wallet {
323
+ defaultPassword = "TestPassword123!";
29
324
  async gotoOnboardPage() {
325
+ await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
326
+ await expect(
327
+ this.page.getByRole("button", { name: "I have an existing wallet" })
328
+ ).toBeVisible({ timeout: ONBOARD_VISIBLE_TIMEOUT });
329
+ }
330
+ /**
331
+ * Onboard MetaMask with a mnemonic phrase
332
+ * @param mnemonic - 12 or 24 word recovery phrase
333
+ * @param password - Optional password (defaults to TestPassword123!)
334
+ */
335
+ async onboard(mnemonic, password) {
336
+ debug("metamask.onboard: starting");
337
+ const pwd = password ?? this.defaultPassword;
338
+ await this.gotoOnboardPage();
339
+ await this.page.getByRole("button", { name: "I have an existing wallet" }).click();
340
+ await this.page.getByRole("button", { name: "Import using Secret Recovery Phrase" }).click();
341
+ const srpTextarea = this.page.getByTestId("srp-input-import__srp-note");
342
+ await srpTextarea.click();
343
+ const words = mnemonic.split(" ");
344
+ for (let i = 0; i < words.length; i++) {
345
+ await this.page.keyboard.type(words[i], { delay: MNEMONIC_KEY_DELAY });
346
+ if (i < words.length - 1) {
347
+ await this.page.keyboard.press("Space");
348
+ await this.page.waitForTimeout(MNEMONIC_WORD_DELAY);
349
+ }
350
+ }
351
+ const continueBtn = this.page.getByTestId("import-srp-confirm");
352
+ await expect(continueBtn).toBeEnabled({ timeout: config.expectTimeout });
353
+ await continueBtn.click();
354
+ const passwordInputs = this.page.locator('input[type="password"]');
355
+ await passwordInputs.nth(0).fill(pwd);
356
+ await passwordInputs.nth(1).fill(pwd);
357
+ await this.page.getByRole("checkbox").click({ force: true });
358
+ await this.page.getByRole("button", { name: "Create password" }).click();
359
+ const metametricsBtn = this.page.getByTestId("metametrics-i-agree");
360
+ await metametricsBtn.click();
361
+ const openWalletBtn = this.page.getByRole("button", {
362
+ name: /open wallet/i
363
+ });
364
+ await openWalletBtn.click();
365
+ await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
30
366
  await this.page.goto(
31
- `chrome-extension://${this.extensionId}/onboarding.html`
367
+ `chrome-extension://${this.extensionId}/sidepanel.html`
32
368
  );
33
- await expect(this.page.getByText("Welcome to Backpack")).toBeVisible();
369
+ debug("metamask.onboard: complete");
34
370
  }
35
- async onboard(network, privateKey) {
36
- this.currentAccountId++;
37
- this.maxAccountId++;
38
- return this._importAccount(network, privateKey, true);
371
+ /**
372
+ * Dismiss MetaMask promotional popups (e.g., "Transaction Shield")
373
+ * that may overlay the confirmation UI.
374
+ */
375
+ async dismissPopups() {
376
+ debug("metamask.dismissPopups: checking for popups");
377
+ const popup = this.page.getByText(/Transaction Shield|free trial/i);
378
+ if (!await popup.first().isVisible({ timeout: POPUP_VISIBILITY_TIMEOUT }).catch(() => false)) {
379
+ return;
380
+ }
381
+ const shieldClose = this.page.getByTestId(
382
+ "shield-entry-modal-close-button"
383
+ );
384
+ if (await shieldClose.isVisible({ timeout: SHIELD_CLOSE_TIMEOUT }).catch(() => false)) {
385
+ await shieldClose.click();
386
+ if (await this.waitForPopupHidden(popup)) return;
387
+ }
388
+ const closeByAria = this.page.locator('button[aria-label="close"]').first();
389
+ if (await closeByAria.isVisible({ timeout: ARIA_CLOSE_TIMEOUT }).catch(() => false)) {
390
+ await closeByAria.click();
391
+ if (await this.waitForPopupHidden(popup)) return;
392
+ }
393
+ await this.page.keyboard.press("Escape");
394
+ await this.waitForPopupHidden(popup);
39
395
  }
40
- async addAccount(network, privateKey) {
41
- this.maxAccountId++;
42
- this.currentAccountId = this.maxAccountId;
43
- await this.page.goto(
44
- `chrome-extension://${this.extensionId}/onboarding.html?add-user-account=true`
396
+ async waitForPopupHidden(popup) {
397
+ try {
398
+ await popup.first().waitFor({ state: "hidden", timeout: POPUP_HIDDEN_TIMEOUT });
399
+ return true;
400
+ } catch {
401
+ return false;
402
+ }
403
+ }
404
+ /**
405
+ * After unlock, MetaMask may show onboarding screens, queued
406
+ * notifications, or go straight to the wallet UI. Race all possible
407
+ * states in a single wait to avoid sequential timeout penalties.
408
+ */
409
+ async stabilizePostUnlock() {
410
+ debug("metamask.stabilizePostUnlock: racing post-unlock states");
411
+ const metametricsBtn = this.page.getByTestId("metametrics-i-agree");
412
+ const openWalletBtn = this.page.getByRole("button", {
413
+ name: /open wallet/i
414
+ });
415
+ const readyIndicator = this.page.getByTestId("account-options-menu-button");
416
+ const rejectAllBtn = this.page.getByText("Reject all");
417
+ const notificationCancelBtn = this.page.getByTestId(
418
+ "confirmation-cancel-button"
45
419
  );
46
- await this._importAccount(network, privateKey, false);
420
+ const state = await Promise.race([
421
+ metametricsBtn.waitFor({ state: "visible", timeout: POST_UNLOCK_TIMEOUT }).then(() => "metametrics"),
422
+ openWalletBtn.waitFor({ state: "visible", timeout: POST_UNLOCK_TIMEOUT }).then(() => "openWallet"),
423
+ rejectAllBtn.waitFor({ state: "visible", timeout: POST_UNLOCK_TIMEOUT }).then(() => "rejectAll"),
424
+ notificationCancelBtn.waitFor({ state: "visible", timeout: POST_UNLOCK_TIMEOUT }).then(() => "notification"),
425
+ readyIndicator.waitFor({ state: "visible", timeout: POST_UNLOCK_TIMEOUT }).then(() => "ready")
426
+ ]).catch(() => "timeout");
427
+ debug(`metamask.stabilizePostUnlock: state=${state}`);
428
+ if (state === "timeout") {
429
+ debug(
430
+ `metamask.stabilizePostUnlock: timeout after ${POST_UNLOCK_TIMEOUT}ms. URL: ${this.page.url()}. Checked: metametrics-i-agree, open-wallet button, Reject all, confirmation-cancel-button, account-options-menu-button`
431
+ );
432
+ }
433
+ if (state === "metametrics") {
434
+ await metametricsBtn.click();
435
+ if (await openWalletBtn.isVisible({ timeout: POPUP_HIDDEN_TIMEOUT }).catch(() => false)) {
436
+ await openWalletBtn.click();
437
+ }
438
+ }
439
+ if (state === "openWallet") {
440
+ await openWalletBtn.click();
441
+ }
442
+ if (state !== "ready") {
443
+ await this.dismissQueuedNotifications();
444
+ }
47
445
  }
48
446
  /**
49
- * Switch account
50
- * @param id The first added account has id 1, the second 2, and so on
447
+ * Dismiss all queued MetaMask notifications (e.g., Solana/Tron account
448
+ * removal). These use the templated confirmation flow at #/confirmation/...
449
+ * If multiple are queued, "Reject all" appears; if only one, just
450
+ * confirmation-cancel-button is available.
51
451
  */
52
- async switchAccount(id) {
53
- await this.page.getByRole("button", { name: `A${this.currentAccountId}` }).click();
54
- await this.page.getByRole("button", { name: `Account ${id}` }).click();
55
- this.currentAccountId = id;
56
- }
57
- async unlock() {
58
- await this.page.getByPlaceholder("Password").fill(this.defaultPassword);
59
- await this.page.getByRole("button", { name: "Unlock" }).click();
60
- }
61
- async setRPC(network, rpc) {
62
- await this._clickOnAccount();
63
- await this.page.getByRole("button", { name: "Settings" }).click();
64
- await this.page.getByRole("button", { name: network }).click();
65
- await this.page.getByRole("button", { name: "RPC Connection" }).click();
66
- await this.page.getByRole("button", { name: "Custom" }).click();
67
- await this.page.getByPlaceholder("RPC URL").fill(rpc);
68
- await this.page.keyboard.press("Enter");
69
- }
70
- async ignoreAndProceed() {
71
- const ignoreButton = this.page.getByText("Ignore and proceed anyway.");
72
- await ignoreButton.click();
452
+ async dismissQueuedNotifications() {
453
+ const homeUrl = `chrome-extension://${this.extensionId}/home.html`;
454
+ const readyIndicator = this.page.getByTestId("account-options-menu-button");
455
+ const rejectAllBtn = this.page.getByText("Reject all");
456
+ const notificationCancelBtn = this.page.getByTestId(
457
+ "confirmation-cancel-button"
458
+ );
459
+ await this.page.goto(homeUrl);
460
+ for (let i = 0; i < 10; i++) {
461
+ const state = await Promise.race([
462
+ readyIndicator.waitFor({ state: "visible", timeout: NOTIFICATION_CHECK_TIMEOUT }).then(() => "ready"),
463
+ rejectAllBtn.waitFor({ state: "visible", timeout: NOTIFICATION_CHECK_TIMEOUT }).then(() => "rejectAll"),
464
+ notificationCancelBtn.waitFor({ state: "visible", timeout: NOTIFICATION_CHECK_TIMEOUT }).then(() => "notification")
465
+ ]).catch(() => "timeout");
466
+ if (state === "ready") return;
467
+ if (state === "rejectAll") {
468
+ await rejectAllBtn.click();
469
+ await this.page.goto(homeUrl);
470
+ continue;
471
+ }
472
+ if (state === "notification") {
473
+ await notificationCancelBtn.click();
474
+ await this.page.goto(homeUrl);
475
+ continue;
476
+ }
477
+ debug(
478
+ `metamask.dismissQueuedNotifications: timeout at iteration ${i}. URL: ${this.page.url()}`
479
+ );
480
+ break;
481
+ }
482
+ await expect(readyIndicator).toBeVisible({ timeout: POST_UNLOCK_TIMEOUT });
483
+ }
484
+ /**
485
+ * Wait for a target button while handling the Transaction Shield popup.
486
+ * Always navigates to sidepanel.html fresh so MetaMask's
487
+ * ConfirmationHandler can route to the pending approval.
488
+ */
489
+ async waitAndClickButton(btnLocator) {
490
+ debug(`metamask.waitAndClickButton: navigating to sidepanel`);
491
+ const popup = this.page.getByText(/Transaction Shield|free trial/i);
492
+ const sidepanelUrl = `chrome-extension://${this.extensionId}/sidepanel.html`;
493
+ const confirmRoutePattern = /#\/(confirm-transaction|connect|confirmation)\b/;
494
+ const waitForButtonOrPopup = (timeout) => Promise.race([
495
+ btnLocator.first().waitFor({ state: "visible", timeout }).then(() => "button"),
496
+ popup.first().waitFor({ state: "visible", timeout }).then(() => "popup")
497
+ ]).catch(() => "timeout");
498
+ const handlePopupAndClick = async () => {
499
+ await this.dismissPopups();
500
+ await btnLocator.first().waitFor({ state: "visible", timeout: BUTTON_OR_POPUP_TIMEOUT });
501
+ await btnLocator.first().click();
502
+ };
503
+ let routeFound = false;
504
+ for (let attempt = 0; attempt < MAX_ROUTE_ATTEMPTS; attempt++) {
505
+ await this.page.goto(sidepanelUrl);
506
+ try {
507
+ await this.page.waitForURL(confirmRoutePattern, {
508
+ timeout: ROUTE_RETRY_TIMEOUT
509
+ });
510
+ routeFound = true;
511
+ break;
512
+ } catch {
513
+ debug(
514
+ `metamask.waitAndClickButton: route attempt ${attempt + 1}/${MAX_ROUTE_ATTEMPTS} failed. URL: ${this.page.url()}`
515
+ );
516
+ }
517
+ }
518
+ if (!routeFound) {
519
+ console.warn(
520
+ `[w3wallets] confirmation route not found after ${MAX_ROUTE_ATTEMPTS} attempts. URL: ${this.page.url()}`
521
+ );
522
+ }
523
+ const result = await waitForButtonOrPopup(BUTTON_OR_POPUP_TIMEOUT);
524
+ debug(
525
+ `metamask.waitAndClickButton: result=${result}, URL=${this.page.url()}`
526
+ );
527
+ if (result === "button") {
528
+ await btnLocator.first().click();
529
+ await this.page.waitForURL((url) => !confirmRoutePattern.test(url.toString()), {
530
+ timeout: POST_CLICK_TIMEOUT
531
+ }).catch(() => {
532
+ console.warn(
533
+ `[w3wallets] still on confirmation route after click. URL: ${this.page.url()}`
534
+ );
535
+ });
536
+ return;
537
+ }
538
+ if (result === "popup") {
539
+ await handlePopupAndClick();
540
+ await this.page.waitForURL((url) => !confirmRoutePattern.test(url.toString()), {
541
+ timeout: POST_CLICK_TIMEOUT
542
+ }).catch(() => {
543
+ console.warn(
544
+ `[w3wallets] still on confirmation route after popup dismiss. URL: ${this.page.url()}`
545
+ );
546
+ });
547
+ return;
548
+ }
549
+ const isOnConfirmRoute = confirmRoutePattern.test(this.page.url());
550
+ debug(
551
+ `metamask.waitAndClickButton: timeout after ${BUTTON_OR_POPUP_TIMEOUT}ms. URL: ${this.page.url()}, onConfirmRoute: ${isOnConfirmRoute}`
552
+ );
553
+ console.warn(
554
+ `[w3wallets] no button or popup found after ${BUTTON_OR_POPUP_TIMEOUT / 1e3}s. URL: ${this.page.url()}`
555
+ );
556
+ await btnLocator.first().click({ timeout: LAST_RESORT_CLICK_TIMEOUT });
73
557
  }
74
558
  async approve() {
75
- await this.page.getByText("Approve", { exact: true }).click();
559
+ debug("metamask.approve: starting");
560
+ const confirmBtn = this.page.getByTestId("confirm-btn").or(this.page.getByTestId("confirm-footer-button")).or(this.page.getByTestId("page-container-footer-next")).or(this.page.getByRole("button", { name: /^confirm$/i }));
561
+ await this.waitAndClickButton(confirmBtn);
76
562
  }
77
563
  async deny() {
78
- await this.page.getByText("Deny", { exact: true }).click();
564
+ debug("metamask.deny: starting");
565
+ const cancelBtn = this.page.getByTestId("cancel-btn").or(this.page.getByTestId("confirm-footer-cancel-button")).or(this.page.getByTestId("page-container-footer-cancel")).or(this.page.getByRole("button", { name: /^cancel$/i })).or(this.page.getByRole("button", { name: /^reject$/i }));
566
+ await this.waitAndClickButton(cancelBtn);
79
567
  }
80
- async _clickOnAccount() {
81
- return this.page.getByRole("button", { name: `A${this.currentAccountId}`, exact: true }).click();
568
+ /**
569
+ * Lock the MetaMask wallet
570
+ */
571
+ async lock() {
572
+ debug("metamask.lock: starting");
573
+ await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
574
+ const menuBtn = this.page.getByTestId("account-options-menu-button");
575
+ await menuBtn.waitFor({ state: "visible", timeout: MENU_BUTTON_TIMEOUT });
576
+ await menuBtn.click({ force: true });
577
+ await this.page.locator("text=Log out").click();
82
578
  }
83
- async _importAccount(network, privateKey, isOnboard) {
84
- {
85
- await this.page.waitForTimeout(2e3);
86
- if (await this.page.getByText("You're all good!").isVisible()) {
87
- await this.page.getByLabel("Go back").click();
88
- }
89
- }
90
- await this.page.getByTestId("terms-of-service-checkbox").click();
91
- await this.page.getByText("I already have a wallet").click();
92
- await this.page.getByText(network).click();
93
- await this.page.getByText("Private key").click();
94
- await this.page.getByPlaceholder("Private key").fill(privateKey);
95
- await this.page.waitForTimeout(1e3);
96
- await this.page.getByText("Import", { exact: true }).click();
97
- if (isOnboard) {
98
- await this.page.getByRole("textbox").nth(0).fill(this.defaultPassword);
99
- await this.page.getByRole("textbox").nth(1).fill(this.defaultPassword);
100
- await this.page.getByText("Next", { exact: true }).click();
101
- await expect(this.page.getByText("You're all good!")).toBeVisible();
579
+ /**
580
+ * Unlock MetaMask with password.
581
+ * After unlocking, stabilizes the wallet UI by handling post-unlock
582
+ * screens (metametrics, onboarding completion) and dismissing queued
583
+ * notifications. Ends on home.html with the wallet UI ready.
584
+ */
585
+ async unlock(password) {
586
+ debug("metamask.unlock: starting");
587
+ const pwd = password ?? this.defaultPassword;
588
+ await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
589
+ const passwordInput = this.page.getByTestId("unlock-password");
590
+ await passwordInput.fill(pwd);
591
+ await this.page.getByTestId("unlock-submit").click();
592
+ await this.page.waitForSelector('[data-testid="unlock-password"]', {
593
+ state: "hidden",
594
+ timeout: LOCK_SCREEN_TIMEOUT
595
+ });
596
+ await this.stabilizePostUnlock();
597
+ debug("metamask.unlock: complete");
598
+ }
599
+ /**
600
+ * Switch to an existing network in MetaMask
601
+ * @param networkName - Name of the network to switch to (e.g., "Ethereum Mainnet", "Sepolia")
602
+ */
603
+ async switchNetwork(networkName, networkType = "Popular") {
604
+ debug(`metamask.switchNetwork: ${networkName} (${networkType})`);
605
+ await this.page.getByTestId("sort-by-networks").click();
606
+ if (networkType === "Custom") {
607
+ await this.page.getByRole("tab", { name: "Custom" }).click({ force: true });
102
608
  }
103
- await this.page.goto(`chrome-extension://${this.extensionId}/popup.html`);
104
- await this.page.getByTestId("__CAROUSEL_ITEM_0__").waitFor({ state: "visible" });
609
+ await this.page.getByText(networkName).click();
610
+ await expect(this.page.getByTestId("sort-by-networks")).toHaveText(
611
+ networkName,
612
+ { timeout: config.expectTimeout }
613
+ );
614
+ }
615
+ async switchAccount(accountName) {
616
+ await this.page.getByTestId("account-menu-icon").click();
617
+ await this.page.getByText(accountName, { exact: true }).click();
618
+ }
619
+ /**
620
+ * Add a custom network to MetaMask
621
+ */
622
+ async addNetwork(network) {
623
+ await this.page.goto(
624
+ `chrome-extension://${this.extensionId}/home.html#settings/networks/add-network`
625
+ );
626
+ await this.page.getByTestId("network-form-network-name").fill(network.name);
627
+ await this.page.getByTestId("network-form-rpc-url").fill(network.rpc);
628
+ await this.page.getByTestId("network-form-chain-id").fill(network.chainId.toString());
629
+ await this.page.getByTestId("network-form-ticker-input").fill(network.currencySymbol);
630
+ await this.page.getByRole("button", { name: /save/i }).click();
631
+ }
632
+ async addCustomNetwork(settings) {
633
+ await this.page.getByTestId("account-options-menu-button").click({ force: true });
634
+ await this.page.getByTestId("global-menu-networks").click();
635
+ await this.page.getByRole("button", { name: "Add a custom network" }).click();
636
+ await this.page.getByTestId("network-form-network-name").fill(settings.name);
637
+ await this.page.getByTestId("network-form-chain-id").fill(settings.chainId.toString());
638
+ await this.page.getByTestId("network-form-ticker-input").fill(settings.currencySymbol);
639
+ await this.page.getByTestId("test-add-rpc-drop-down").click();
640
+ await this.page.getByRole("button", { name: "Add RPC URL" }).click();
641
+ await this.page.getByTestId("rpc-url-input-test").fill(settings.rpc);
642
+ await this.page.getByRole("button", { name: "Add URL" }).click();
643
+ await this.page.getByRole("button", { name: "Save" }).click();
644
+ await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
645
+ await this.page.waitForLoadState("domcontentloaded");
646
+ }
647
+ async enableTestNetworks() {
648
+ await this.page.getByTestId("account-options-menu-button").click({ force: true });
649
+ await this.page.getByTestId("global-menu-networks").click();
650
+ const toggle = this.page.locator(
651
+ "text=Show test networks >> xpath=following-sibling::label"
652
+ );
653
+ await expect(toggle).toBeVisible({ timeout: config.expectTimeout });
654
+ await toggle.click();
655
+ await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
656
+ }
657
+ async importAccount(privateKey) {
658
+ debug("metamask.importAccount: starting");
659
+ await this.page.getByTestId("account-menu-icon").click();
660
+ await this.page.getByTestId("account-list-add-wallet-button").click();
661
+ await this.page.getByTestId("choose-wallet-type-import-account").click();
662
+ await this.page.locator("#private-key-box").fill(privateKey);
663
+ await this.page.getByTestId("import-account-confirm-button").click();
664
+ await this.page.getByTestId("back-button").click();
665
+ await this.page.locator('[data-testid^="multichain-account-cell-keyring:"]').first().click();
666
+ }
667
+ async accountNameIs(accountName) {
668
+ await expect(this.page.getByTestId("account-menu-icon")).toContainText(
669
+ accountName,
670
+ { timeout: config.expectTimeout }
671
+ );
105
672
  }
106
673
  };
107
674
 
108
- // src/polkadotJS/polkadotJS.ts
675
+ // src/wallets/polkadot-js/polkadot-js.ts
109
676
  import { expect as expect2 } from "@playwright/test";
110
677
  var PolkadotJS = class extends Wallet {
111
678
  defaultPassword = "11111111";
@@ -113,9 +680,11 @@ var PolkadotJS = class extends Wallet {
113
680
  await this.page.goto(`chrome-extension://${this.extensionId}/index.html`);
114
681
  await expect2(
115
682
  this.page.getByText("Before we start, just a couple of notes")
116
- ).toBeVisible();
683
+ ).toBeVisible({ timeout: config.expectTimeout });
117
684
  }
118
685
  async onboard(seed, password, name) {
686
+ debug("polkadotJS.onboard: starting");
687
+ await this.gotoOnboardPage();
119
688
  await this.page.getByRole("button", { name: "Understood, let me continue" }).click();
120
689
  await this.page.getByRole("button", { name: "I Understand" }).click();
121
690
  await this.page.locator(".popupToggle").first().click();
@@ -132,12 +701,13 @@ var PolkadotJS = class extends Wallet {
132
701
  password ?? this.defaultPassword
133
702
  );
134
703
  await this.page.getByRole("button", { name: "Add the account with the supplied seed" }).click();
704
+ debug("polkadotJS.onboard: complete");
135
705
  }
136
706
  async selectAllAccounts() {
137
707
  await this.page.getByText("Select all").click();
138
708
  }
139
709
  async selectAccount(accountId) {
140
- await this.page.locator(".accountWichCheckbox").filter({ hasText: accountId }).locator(".accountTree-checkbox").locator("span").check();
710
+ await this.page.locator(".accountWichCheckbox").filter({ hasText: accountId }).locator(".accountTree-checkbox").click();
141
711
  }
142
712
  async enterPassword(password) {
143
713
  await this._getLabeledInput("Password for this account").fill(
@@ -145,6 +715,7 @@ var PolkadotJS = class extends Wallet {
145
715
  );
146
716
  }
147
717
  async approve() {
718
+ debug("polkadotJS.approve: starting");
148
719
  const connect = this.page.getByRole("button", { name: "Connect" });
149
720
  const signTransaction = this.page.getByRole("button", {
150
721
  name: "Sign the transaction"
@@ -152,6 +723,7 @@ var PolkadotJS = class extends Wallet {
152
723
  await connect.or(signTransaction).click();
153
724
  }
154
725
  async deny() {
726
+ debug("polkadotJS.deny: starting");
155
727
  const reject = this.page.getByRole("button", { name: "Reject" });
156
728
  const cancel = this.page.getByRole("link", { name: "Cancel" });
157
729
  await reject.or(cancel).click();
@@ -163,233 +735,37 @@ var PolkadotJS = class extends Wallet {
163
735
  }
164
736
  };
165
737
 
166
- // src/metamask/metamask.ts
167
- import { expect as expect3 } from "@playwright/test";
168
- var Metamask = class extends Wallet {
169
- defaultPassword = "11111111";
170
- async gotoOnboardPage() {
171
- await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
172
- }
173
- /**
174
- *
175
- * @param mnemonic 12-word mnemonic seed phrase
176
- */
177
- async onboard(mnemonic, password = this.defaultPassword) {
178
- await this.page.getByTestId("onboarding-import-wallet").click();
179
- await this.page.getByTestId("onboarding-import-with-srp-button").click();
180
- await this.page.getByTestId("srp-input-import__srp-note").pressSequentially(mnemonic, { delay: 5 });
181
- await this.page.getByRole("button", { name: "Continue" }).click();
182
- await this.page.getByTestId("create-password-new-input").fill(password);
183
- await this.page.getByTestId("create-password-confirm-input").fill(password);
184
- await this.page.getByTestId("create-password-terms").click();
185
- await this.page.getByTestId("create-password-submit").click();
186
- await this.page.getByTestId("metametrics-i-agree").click();
187
- await this.page.getByTestId("onboarding-complete-done").click();
188
- await this.clickTopRightCornerToCloseAllTheMarketingBullshit();
189
- }
190
- // async switchAccount(accountAddress: { address: string }): Promise<void>;
191
- async switchAccount(accountNameOrAddress) {
192
- await this.page.getByTestId("account-menu-icon").click();
193
- await this.page.getByText(accountNameOrAddress.name, { exact: true }).click();
194
- }
195
- async importAccount(privateKey) {
196
- await this.page.getByTestId("account-menu-icon").click();
197
- await this.page.getByTestId("account-list-add-wallet-button").click();
198
- await this.page.getByTestId("add-wallet-modal-import-account").click();
199
- await this.page.locator("#private-key-box").fill(privateKey);
200
- await this.page.getByTestId("import-account-confirm-button").click();
201
- await this.page.getByRole("button", { name: "Back" }).click();
202
- }
203
- async addAccount(accountName) {
204
- await this.page.getByTestId("account-menu-icon").click();
205
- await this.page.getByTestId("multichain-account-menu-popover-action-button").click();
206
- await this.page.getByTestId("multichain-account-menu-popover-add-account").click();
207
- if (accountName) {
208
- await this.page.locator("#account-name").fill(accountName);
209
- }
210
- await this.page.getByTestId("submit-add-account-with-name").click();
211
- }
212
- async getAccountName() {
213
- const accountSelect = this.page.getByTestId("account-menu-icon");
214
- await expect3(accountSelect).toBeVisible();
215
- const text = await accountSelect.textContent();
216
- if (!text) throw Error("Cannot get account name");
217
- return text;
218
- }
219
- async connectToNetwork(networkName, networkType = "Popular") {
220
- await this.page.getByTestId("sort-by-networks").click();
221
- await this.page.getByRole("tab", { name: networkType, exact: true }).click();
222
- const additionalNetwork = this.page.getByTestId("additional-network-item").getByText(networkName);
223
- await this.page.getByText(networkName).click();
224
- }
225
- async addCustomNetwork(settings) {
226
- await this.page.getByTestId("account-options-menu-button").click();
227
- await this.page.getByTestId("global-menu-networks").click();
228
- await this.page.getByRole("button", { name: "Add a custom network" }).click();
229
- await this.page.getByTestId("network-form-network-name").fill(settings.name);
230
- await this.page.getByTestId("network-form-chain-id").fill(settings.chainId.toString());
231
- await this.page.getByTestId("network-form-ticker-input").fill(settings.currencySymbol);
232
- await this.page.getByTestId("test-add-rpc-drop-down").click();
233
- await this.page.getByRole("button", { name: "Add RPC URL" }).click();
234
- await this.page.getByTestId("rpc-url-input-test").fill(settings.rpc);
235
- await this.page.getByRole("button", { name: "Add URL" }).click();
236
- await this.page.getByRole("button", { name: "Save" }).click();
237
- }
238
- async enableTestNetworks() {
239
- await this.page.getByTestId("account-options-menu-button").click();
240
- await this.page.getByTestId("global-menu-networks").click();
241
- await this.page.locator("text=Show test networks >> xpath=following-sibling::label").click();
242
- await this.page.keyboard.press("Escape");
243
- }
244
- async approve() {
245
- const p = await this.page.context().newPage();
246
- await p.goto(`chrome-extension://${this.extensionId}/notification.html`);
247
- await p.locator(
248
- '[data-testid="confirm-footer-button"], [data-testid="confirm-btn"], [data-testid="page-container-footer-next"], [data-testid="confirmation-submit-button"]'
249
- ).click();
250
- await p.waitForSelector(".multichain-app-header", {
251
- timeout: 1e4
252
- });
253
- await p.close();
254
- }
255
- async deny() {
256
- return this.usingNotificationPage(
257
- (p) => p.getByTestId("cancel-btn").click()
258
- );
259
- }
260
- async usingNotificationPage(action) {
261
- const p = await this.page.context().newPage();
262
- await p.goto(`chrome-extension://${this.extensionId}/notification.html`);
263
- await action(p);
264
- await p.close();
265
- }
266
- async clickTopRightCornerToCloseAllTheMarketingBullshit() {
267
- await this.page.waitForTimeout(500);
268
- await this.page.keyboard.press("Escape");
269
- await this.page.mouse.click(1e3, 10);
270
- }
271
- };
738
+ // src/wallets/index.ts
739
+ var metamask = createWallet({
740
+ name: "metamask",
741
+ extensionDir: "metamask",
742
+ WalletClass: Metamask,
743
+ homeUrl: "home.html"
744
+ });
745
+ var polkadotJS = createWallet({
746
+ name: "polkadotJS",
747
+ extensionDir: "polkadotjs",
748
+ WalletClass: PolkadotJS,
749
+ homeUrl: "index.html"
750
+ });
272
751
 
273
- // src/withWallets.ts
274
- var w3walletsDir = ".w3wallets";
275
- function withWallets(test, ...config) {
276
- const withBackpack = config.includes("backpack");
277
- const withPolkadotJS = config.includes("polkadotJS");
278
- const withMetamask = config.includes("metamask");
279
- const backpackPath = path.join(process.cwd(), w3walletsDir, "backpack");
280
- const polkadotJSPath = path.join(process.cwd(), w3walletsDir, "polkadotJS");
281
- const metamaskPath = path.join(process.cwd(), w3walletsDir, "metamask");
282
- return test.extend({
283
- /**
284
- * Sets up a persistent browser context with the requested extensions loaded.
285
- */
286
- context: async ({}, use, testInfo) => {
287
- const userDataDir = path.join(
288
- process.cwd(),
289
- ".w3wallets",
290
- ".context",
291
- testInfo.testId
292
- );
293
- cleanUserDataDir(userDataDir);
294
- const extensionPaths = [];
295
- if (withBackpack) {
296
- ensureWalletExtensionExists(backpackPath, "backpack");
297
- extensionPaths.push(backpackPath);
298
- }
299
- if (withPolkadotJS) {
300
- ensureWalletExtensionExists(polkadotJSPath, "polkadotJS");
301
- extensionPaths.push(polkadotJSPath);
302
- }
303
- if (withMetamask) {
304
- ensureWalletExtensionExists(metamaskPath, "metamask");
305
- extensionPaths.push(metamaskPath);
306
- }
307
- const context = await chromium.launchPersistentContext(userDataDir, {
308
- headless: testInfo.project.use.headless ?? true,
309
- channel: "chromium",
310
- args: [
311
- `--disable-extensions-except=${extensionPaths.join(",")}`,
312
- `--load-extension=${extensionPaths.join(",")}`
313
- ]
314
- });
315
- while (context.serviceWorkers().length < extensionPaths.length) {
316
- await sleep(1e3);
317
- }
318
- await use(context);
319
- await context.close();
320
- },
321
- backpack: async ({ context }, use) => {
322
- if (!withBackpack) {
323
- throw Error(
324
- "The Backpack wallet hasn't been loaded. Add it to the withWallets function."
325
- );
326
- }
327
- const backpack = await initializeExtension(
328
- context,
329
- Backpack,
330
- "Backpack is not initialized"
331
- );
332
- await use(backpack);
333
- },
334
- polkadotJS: async ({ context }, use) => {
335
- if (!withPolkadotJS) {
336
- throw Error(
337
- "The Polkadot{.js} wallet hasn't been loaded. Add it to the withWallets function."
338
- );
339
- }
340
- const polkadotJS = await initializeExtension(
341
- context,
342
- PolkadotJS,
343
- "Polkadot{.js} is not initialized"
344
- );
345
- await use(polkadotJS);
346
- },
347
- metamask: async ({ context }, use) => {
348
- if (!withMetamask) {
349
- throw Error(
350
- "The Metamask wallet hasn't been loaded. Add it to the withWallets function."
351
- );
352
- }
353
- const metamask = await initializeExtension(
354
- context,
355
- Metamask,
356
- "Metamask is not initialized"
357
- );
358
- await use(metamask);
359
- }
360
- });
361
- }
362
- function cleanUserDataDir(userDataDir) {
363
- if (fs.existsSync(userDataDir)) {
364
- fs.rmSync(userDataDir, { recursive: true });
365
- }
366
- }
367
- function ensureWalletExtensionExists(walletPath, walletName) {
368
- if (!fs.existsSync(path.join(walletPath, "manifest.json"))) {
369
- throw new Error(
370
- `Cannot find ${walletName}. Please download it via 'npx w3wallets ${walletName}'.`
371
- );
372
- }
373
- }
374
- async function initializeExtension(context, ExtensionClass, notInitializedErrorMessage) {
375
- const serviceWorkers = context.serviceWorkers();
376
- let page = await context.newPage();
377
- for (const worker of serviceWorkers) {
378
- const extensionId = worker.url().split("/")[2];
379
- if (!extensionId) {
380
- continue;
381
- }
382
- const extension = new ExtensionClass(page, extensionId);
383
- try {
384
- await extension.gotoOnboardPage();
385
- return extension;
386
- } catch {
387
- await page.close();
388
- page = await context.newPage();
389
- }
390
- }
391
- throw new Error(notInitializedErrorMessage);
752
+ // src/cache/prepareWallet.ts
753
+ function prepareWallet(walletConfig, setupFn) {
754
+ return {
755
+ ...walletConfig,
756
+ setupFn,
757
+ __cached: true
758
+ };
392
759
  }
393
760
  export {
761
+ Metamask,
762
+ PolkadotJS,
763
+ config,
764
+ createWallet,
765
+ debug,
766
+ isCachedConfig,
767
+ metamask,
768
+ polkadotJS,
769
+ prepareWallet,
394
770
  withWallets
395
771
  };