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