w3wallets 1.0.0-beta.1 → 1.0.0-beta.11

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
- }
230
- }
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();
351
+ if (config.actionTimeout) {
352
+ page.setDefaultTimeout(config.actionTimeout);
243
353
  }
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,328 @@ 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();
395
+ const passkeyMaybeLater = this.page.getByTestId(
396
+ "passkey-maybe-later-button"
397
+ );
280
398
  const metametricsBtn = this.page.getByTestId("metametrics-i-agree");
281
- await metametricsBtn.waitFor({ state: "visible", timeout: 3e4 });
282
- await metametricsBtn.click();
283
- const openWalletBtn = this.page.getByRole("button", {
284
- name: /open wallet/i
399
+ const postPasswordState = await Promise.race([
400
+ passkeyMaybeLater.waitFor({ state: "visible", timeout: ONBOARD_VISIBLE_TIMEOUT }).then(() => "passkey"),
401
+ metametricsBtn.waitFor({ state: "visible", timeout: ONBOARD_VISIBLE_TIMEOUT }).then(() => "metametrics")
402
+ ]);
403
+ if (postPasswordState === "passkey") {
404
+ await passkeyMaybeLater.click();
405
+ }
406
+ await metametricsBtn.waitFor({
407
+ state: "visible",
408
+ timeout: ONBOARD_VISIBLE_TIMEOUT
285
409
  });
286
- await openWalletBtn.waitFor({ state: "visible", timeout: 3e4 });
410
+ await metametricsBtn.click();
411
+ const openWalletBtn = this.page.getByTestId("onboarding-complete-done");
287
412
  await openWalletBtn.click();
413
+ await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
288
414
  await this.page.goto(
289
415
  `chrome-extension://${this.extensionId}/sidepanel.html`
290
416
  );
291
- await this.page.getByTestId("account-options-menu-button").waitFor({ state: "visible", timeout: 3e4 });
417
+ debug("metamask.onboard: complete");
418
+ }
419
+ /**
420
+ * Dismiss MetaMask promotional popups (e.g., "Transaction Shield")
421
+ * that may overlay the confirmation UI.
422
+ */
423
+ async dismissPopups() {
424
+ debug("metamask.dismissPopups: checking for popups");
425
+ const popup = this.page.getByText(/Transaction Shield|free trial/i);
426
+ if (!await popup.first().isVisible({ timeout: POPUP_VISIBILITY_TIMEOUT }).catch(() => false)) {
427
+ return;
428
+ }
429
+ const shieldClose = this.page.getByTestId(
430
+ "shield-entry-modal-close-button"
431
+ );
432
+ if (await shieldClose.isVisible({ timeout: SHIELD_CLOSE_TIMEOUT }).catch(() => false)) {
433
+ await shieldClose.click();
434
+ if (await this.waitForPopupHidden(popup)) return;
435
+ }
436
+ const closeByAria = this.page.locator('button[aria-label="close"]').first();
437
+ if (await closeByAria.isVisible({ timeout: ARIA_CLOSE_TIMEOUT }).catch(() => false)) {
438
+ await closeByAria.click();
439
+ if (await this.waitForPopupHidden(popup)) return;
440
+ }
441
+ await this.page.keyboard.press("Escape");
442
+ await this.waitForPopupHidden(popup);
443
+ }
444
+ async waitForPopupHidden(popup) {
445
+ try {
446
+ await popup.first().waitFor({ state: "hidden", timeout: POPUP_HIDDEN_TIMEOUT });
447
+ return true;
448
+ } catch {
449
+ return false;
450
+ }
451
+ }
452
+ /**
453
+ * MetaMask's MV3 service worker can fail to come up on cold-start
454
+ * (especially in fresh-extension CI runs), showing an error screen
455
+ * with a "Restart MetaMask" button. Detect and click it so the
456
+ * normal post-unlock flow can proceed.
457
+ */
458
+ async recoverFromStartupError() {
459
+ const restartBtn = this.page.getByRole("button", {
460
+ name: "Restart MetaMask"
461
+ });
462
+ if (!await restartBtn.isVisible({ timeout: POPUP_VISIBILITY_TIMEOUT }).catch(() => false)) {
463
+ return;
464
+ }
465
+ debug("metamask.recoverFromStartupError: clicking Restart MetaMask");
466
+ await restartBtn.click();
467
+ await restartBtn.waitFor({
468
+ state: "hidden",
469
+ timeout: POST_UNLOCK_TIMEOUT
470
+ });
471
+ }
472
+ /**
473
+ * After unlock, MetaMask may show onboarding screens, queued
474
+ * notifications, or go straight to the wallet UI. Race all possible
475
+ * states in a single wait to avoid sequential timeout penalties.
476
+ */
477
+ async stabilizePostUnlock() {
478
+ debug("metamask.stabilizePostUnlock: racing post-unlock states");
479
+ await this.recoverFromStartupError();
480
+ const passkeyMaybeLater = this.page.getByTestId(
481
+ "passkey-maybe-later-button"
482
+ );
483
+ const metametricsBtn = this.page.getByTestId("metametrics-i-agree");
484
+ const openWalletBtn = this.page.getByTestId("onboarding-complete-done");
485
+ const readyIndicator = this.page.getByTestId("account-options-menu-button");
486
+ const rejectAllBtn = this.page.getByText("Reject all");
487
+ const notificationCancelBtn = this.page.getByTestId(
488
+ "confirmation-cancel-button"
489
+ );
490
+ const state = await Promise.race([
491
+ passkeyMaybeLater.waitFor({ state: "visible", timeout: POST_UNLOCK_TIMEOUT }).then(() => "passkey"),
492
+ metametricsBtn.waitFor({ state: "visible", timeout: POST_UNLOCK_TIMEOUT }).then(() => "metametrics"),
493
+ openWalletBtn.waitFor({ state: "visible", timeout: POST_UNLOCK_TIMEOUT }).then(() => "openWallet"),
494
+ rejectAllBtn.waitFor({ state: "visible", timeout: POST_UNLOCK_TIMEOUT }).then(() => "rejectAll"),
495
+ notificationCancelBtn.waitFor({ state: "visible", timeout: POST_UNLOCK_TIMEOUT }).then(() => "notification"),
496
+ readyIndicator.waitFor({ state: "visible", timeout: POST_UNLOCK_TIMEOUT }).then(() => "ready")
497
+ ]).catch(() => "timeout");
498
+ debug(`metamask.stabilizePostUnlock: state=${state}`);
499
+ if (state === "timeout") {
500
+ debug(
501
+ `metamask.stabilizePostUnlock: timeout after ${POST_UNLOCK_TIMEOUT}ms. URL: ${this.page.url()}. Checked: passkey-maybe-later-button, metametrics-i-agree, onboarding-complete-done, Reject all, confirmation-cancel-button, account-options-menu-button`
502
+ );
503
+ }
504
+ if (state === "passkey") {
505
+ await passkeyMaybeLater.click();
506
+ if (await metametricsBtn.isVisible({ timeout: POST_UNLOCK_TIMEOUT }).catch(() => false)) {
507
+ await metametricsBtn.click();
508
+ }
509
+ if (await openWalletBtn.isVisible({ timeout: POPUP_HIDDEN_TIMEOUT }).catch(() => false)) {
510
+ await openWalletBtn.click();
511
+ }
512
+ }
513
+ if (state === "metametrics") {
514
+ await metametricsBtn.click();
515
+ if (await openWalletBtn.isVisible({ timeout: POPUP_HIDDEN_TIMEOUT }).catch(() => false)) {
516
+ await openWalletBtn.click();
517
+ }
518
+ }
519
+ if (state === "openWallet") {
520
+ await openWalletBtn.click();
521
+ }
522
+ if (state !== "ready") {
523
+ await this.dismissQueuedNotifications();
524
+ }
525
+ }
526
+ /**
527
+ * Dismiss all queued MetaMask notifications (e.g., Solana/Tron account
528
+ * removal). These use the templated confirmation flow at #/confirmation/...
529
+ * If multiple are queued, "Reject all" appears; if only one, just
530
+ * confirmation-cancel-button is available.
531
+ */
532
+ async dismissQueuedNotifications() {
533
+ const homeUrl = `chrome-extension://${this.extensionId}/home.html`;
534
+ const readyIndicator = this.page.getByTestId("account-options-menu-button");
535
+ const rejectAllBtn = this.page.getByText("Reject all");
536
+ const notificationCancelBtn = this.page.getByTestId(
537
+ "confirmation-cancel-button"
538
+ );
539
+ await this.page.goto(homeUrl);
540
+ await this.recoverFromStartupError();
541
+ for (let i = 0; i < 10; i++) {
542
+ const state = await Promise.race([
543
+ readyIndicator.waitFor({ state: "visible", timeout: NOTIFICATION_CHECK_TIMEOUT }).then(() => "ready"),
544
+ rejectAllBtn.waitFor({ state: "visible", timeout: NOTIFICATION_CHECK_TIMEOUT }).then(() => "rejectAll"),
545
+ notificationCancelBtn.waitFor({ state: "visible", timeout: NOTIFICATION_CHECK_TIMEOUT }).then(() => "notification")
546
+ ]).catch(() => "timeout");
547
+ if (state === "ready") return;
548
+ if (state === "rejectAll") {
549
+ await rejectAllBtn.click();
550
+ await this.page.goto(homeUrl);
551
+ continue;
552
+ }
553
+ if (state === "notification") {
554
+ await notificationCancelBtn.click();
555
+ await this.page.goto(homeUrl);
556
+ continue;
557
+ }
558
+ debug(
559
+ `metamask.dismissQueuedNotifications: timeout at iteration ${i}. URL: ${this.page.url()}`
560
+ );
561
+ break;
562
+ }
563
+ await (0, import_test3.expect)(readyIndicator).toBeVisible({ timeout: POST_UNLOCK_TIMEOUT });
564
+ }
565
+ /**
566
+ * Wait for a target button while handling the Transaction Shield popup.
567
+ * Always navigates to sidepanel.html fresh so MetaMask's
568
+ * ConfirmationHandler can route to the pending approval.
569
+ */
570
+ async waitAndClickButton(btnLocator) {
571
+ debug(`metamask.waitAndClickButton: navigating to sidepanel`);
572
+ const popup = this.page.getByText(/Transaction Shield|free trial/i);
573
+ const sidepanelUrl = `chrome-extension://${this.extensionId}/sidepanel.html`;
574
+ const confirmRoutePattern = /#\/(confirm-transaction|connect|confirmation)\b/;
575
+ const waitForButtonOrPopup = (timeout) => Promise.race([
576
+ btnLocator.first().waitFor({ state: "visible", timeout }).then(() => "button"),
577
+ popup.first().waitFor({ state: "visible", timeout }).then(() => "popup")
578
+ ]).catch(() => "timeout");
579
+ const handlePopupAndClick = async () => {
580
+ await this.dismissPopups();
581
+ await btnLocator.first().waitFor({ state: "visible", timeout: BUTTON_OR_POPUP_TIMEOUT });
582
+ await btnLocator.first().click();
583
+ };
584
+ let routeFound = false;
585
+ for (let attempt = 0; attempt < MAX_ROUTE_ATTEMPTS; attempt++) {
586
+ try {
587
+ await this.page.goto(sidepanelUrl, { timeout: ROUTE_RETRY_TIMEOUT });
588
+ await this.page.waitForURL(confirmRoutePattern, {
589
+ timeout: ROUTE_RETRY_TIMEOUT
590
+ });
591
+ routeFound = true;
592
+ break;
593
+ } catch {
594
+ debug(
595
+ `metamask.waitAndClickButton: route attempt ${attempt + 1}/${MAX_ROUTE_ATTEMPTS} failed. URL: ${this.page.url()}`
596
+ );
597
+ }
598
+ }
599
+ if (!routeFound) {
600
+ console.warn(
601
+ `[w3wallets] confirmation route not found after ${MAX_ROUTE_ATTEMPTS} attempts. URL: ${this.page.url()}`
602
+ );
603
+ }
604
+ const result = await waitForButtonOrPopup(BUTTON_OR_POPUP_TIMEOUT);
605
+ debug(
606
+ `metamask.waitAndClickButton: result=${result}, URL=${this.page.url()}`
607
+ );
608
+ if (result === "button") {
609
+ await btnLocator.first().click();
610
+ await this.page.waitForURL((url) => !confirmRoutePattern.test(url.toString()), {
611
+ timeout: POST_CLICK_TIMEOUT
612
+ }).catch(() => {
613
+ console.warn(
614
+ `[w3wallets] still on confirmation route after click. URL: ${this.page.url()}`
615
+ );
616
+ });
617
+ return;
618
+ }
619
+ if (result === "popup") {
620
+ await handlePopupAndClick();
621
+ await this.page.waitForURL((url) => !confirmRoutePattern.test(url.toString()), {
622
+ timeout: POST_CLICK_TIMEOUT
623
+ }).catch(() => {
624
+ console.warn(
625
+ `[w3wallets] still on confirmation route after popup dismiss. URL: ${this.page.url()}`
626
+ );
627
+ });
628
+ return;
629
+ }
630
+ const isOnConfirmRoute = confirmRoutePattern.test(this.page.url());
631
+ debug(
632
+ `metamask.waitAndClickButton: timeout after ${BUTTON_OR_POPUP_TIMEOUT}ms. URL: ${this.page.url()}, onConfirmRoute: ${isOnConfirmRoute}`
633
+ );
634
+ console.warn(
635
+ `[w3wallets] no button or popup found after ${BUTTON_OR_POPUP_TIMEOUT / 1e3}s. URL: ${this.page.url()}`
636
+ );
637
+ await btnLocator.first().click({ timeout: LAST_RESORT_CLICK_TIMEOUT });
292
638
  }
293
639
  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 });
640
+ debug("metamask.approve: starting");
641
+ 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 }));
642
+ await this.waitAndClickButton(confirmBtn);
296
643
  }
297
644
  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();
645
+ debug("metamask.deny: starting");
646
+ 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 }));
647
+ await this.waitAndClickButton(cancelBtn);
300
648
  }
301
649
  /**
302
650
  * Lock the MetaMask wallet
303
651
  */
304
652
  async lock() {
305
- await this.page.getByTestId("account-options-menu-button").click();
306
- await this.page.locator("text=Lock MetaMask").click();
653
+ debug("metamask.lock: starting");
654
+ await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
655
+ const menuBtn = this.page.getByTestId("account-options-menu-button");
656
+ await menuBtn.waitFor({ state: "visible", timeout: MENU_BUTTON_TIMEOUT });
657
+ await menuBtn.click({ force: true });
658
+ await this.page.getByTestId("global-menu-lock").click();
307
659
  }
308
660
  /**
309
- * Unlock MetaMask with password
661
+ * Unlock MetaMask with password.
662
+ * After unlocking, stabilizes the wallet UI by handling post-unlock
663
+ * screens (metametrics, onboarding completion) and dismissing queued
664
+ * notifications. Ends on home.html with the wallet UI ready.
310
665
  */
311
666
  async unlock(password) {
667
+ debug("metamask.unlock: starting");
312
668
  const pwd = password ?? this.defaultPassword;
669
+ await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
313
670
  const passwordInput = this.page.getByTestId("unlock-password");
314
671
  await passwordInput.fill(pwd);
315
672
  await this.page.getByTestId("unlock-submit").click();
673
+ await this.page.waitForSelector('[data-testid="unlock-password"]', {
674
+ state: "hidden",
675
+ timeout: LOCK_SCREEN_TIMEOUT
676
+ });
677
+ await this.stabilizePostUnlock();
678
+ debug("metamask.unlock: complete");
316
679
  }
317
680
  /**
318
681
  * Switch to an existing network in MetaMask
319
682
  * @param networkName - Name of the network to switch to (e.g., "Ethereum Mainnet", "Sepolia")
320
683
  */
321
684
  async switchNetwork(networkName, networkType = "Popular") {
685
+ debug(`metamask.switchNetwork: ${networkName} (${networkType})`);
322
686
  await this.page.getByTestId("sort-by-networks").click();
323
687
  if (networkType === "Custom") {
324
- await this.page.getByRole("tab", { name: "Custom" }).click();
688
+ await this.page.getByRole("tab", { name: "Custom" }).click({ force: true });
325
689
  }
326
690
  await this.page.getByText(networkName).click();
327
691
  await (0, import_test3.expect)(this.page.getByTestId("sort-by-networks")).toHaveText(
328
- networkName
692
+ networkName,
693
+ { timeout: config.expectTimeout }
329
694
  );
330
695
  }
331
696
  async switchAccount(accountName) {
@@ -346,7 +711,7 @@ var Metamask = class extends Wallet {
346
711
  await this.page.getByRole("button", { name: /save/i }).click();
347
712
  }
348
713
  async addCustomNetwork(settings) {
349
- await this.page.getByTestId("account-options-menu-button").click();
714
+ await this.page.getByTestId("account-options-menu-button").click({ force: true });
350
715
  await this.page.getByTestId("global-menu-networks").click();
351
716
  await this.page.getByRole("button", { name: "Add a custom network" }).click();
352
717
  await this.page.getByTestId("network-form-network-name").fill(settings.name);
@@ -357,24 +722,33 @@ var Metamask = class extends Wallet {
357
722
  await this.page.getByTestId("rpc-url-input-test").fill(settings.rpc);
358
723
  await this.page.getByRole("button", { name: "Add URL" }).click();
359
724
  await this.page.getByRole("button", { name: "Save" }).click();
725
+ await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
726
+ await this.page.waitForLoadState("domcontentloaded");
360
727
  }
361
728
  async enableTestNetworks() {
362
- await this.page.getByTestId("account-options-menu-button").click();
729
+ await this.page.getByTestId("account-options-menu-button").click({ force: true });
363
730
  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");
731
+ const toggle = this.page.locator(
732
+ "text=Show test networks >> xpath=following-sibling::label"
733
+ );
734
+ await (0, import_test3.expect)(toggle).toBeVisible({ timeout: config.expectTimeout });
735
+ await toggle.click();
736
+ await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
366
737
  }
367
738
  async importAccount(privateKey) {
739
+ debug("metamask.importAccount: starting");
368
740
  await this.page.getByTestId("account-menu-icon").click();
369
741
  await this.page.getByTestId("account-list-add-wallet-button").click();
370
- await this.page.getByTestId("add-wallet-modal-import-account").click();
742
+ await this.page.getByTestId("choose-wallet-type-import-account").click();
371
743
  await this.page.locator("#private-key-box").fill(privateKey);
372
744
  await this.page.getByTestId("import-account-confirm-button").click();
373
- await this.page.getByRole("button", { name: "Back" }).click();
745
+ await this.page.getByTestId("back-button").click();
746
+ await this.page.locator('[data-testid^="multichain-account-cell-keyring:"]').first().click();
374
747
  }
375
748
  async accountNameIs(accountName) {
376
749
  await (0, import_test3.expect)(this.page.getByTestId("account-menu-icon")).toContainText(
377
- accountName
750
+ accountName,
751
+ { timeout: config.expectTimeout }
378
752
  );
379
753
  }
380
754
  };
@@ -387,9 +761,11 @@ var PolkadotJS = class extends Wallet {
387
761
  await this.page.goto(`chrome-extension://${this.extensionId}/index.html`);
388
762
  await (0, import_test4.expect)(
389
763
  this.page.getByText("Before we start, just a couple of notes")
390
- ).toBeVisible();
764
+ ).toBeVisible({ timeout: config.expectTimeout });
391
765
  }
392
766
  async onboard(seed, password, name) {
767
+ debug("polkadotJS.onboard: starting");
768
+ await this.gotoOnboardPage();
393
769
  await this.page.getByRole("button", { name: "Understood, let me continue" }).click();
394
770
  await this.page.getByRole("button", { name: "I Understand" }).click();
395
771
  await this.page.locator(".popupToggle").first().click();
@@ -406,12 +782,13 @@ var PolkadotJS = class extends Wallet {
406
782
  password ?? this.defaultPassword
407
783
  );
408
784
  await this.page.getByRole("button", { name: "Add the account with the supplied seed" }).click();
785
+ debug("polkadotJS.onboard: complete");
409
786
  }
410
787
  async selectAllAccounts() {
411
788
  await this.page.getByText("Select all").click();
412
789
  }
413
790
  async selectAccount(accountId) {
414
- await this.page.locator(".accountWichCheckbox").filter({ hasText: accountId }).locator(".accountTree-checkbox").locator("span").check();
791
+ await this.page.locator(".accountWichCheckbox").filter({ hasText: accountId }).locator(".accountTree-checkbox").click();
415
792
  }
416
793
  async enterPassword(password) {
417
794
  await this._getLabeledInput("Password for this account").fill(
@@ -419,6 +796,7 @@ var PolkadotJS = class extends Wallet {
419
796
  );
420
797
  }
421
798
  async approve() {
799
+ debug("polkadotJS.approve: starting");
422
800
  const connect = this.page.getByRole("button", { name: "Connect" });
423
801
  const signTransaction = this.page.getByRole("button", {
424
802
  name: "Sign the transaction"
@@ -426,6 +804,7 @@ var PolkadotJS = class extends Wallet {
426
804
  await connect.or(signTransaction).click();
427
805
  }
428
806
  async deny() {
807
+ debug("polkadotJS.deny: starting");
429
808
  const reject = this.page.getByRole("button", { name: "Reject" });
430
809
  const cancel = this.page.getByRole("link", { name: "Cancel" });
431
810
  await reject.or(cancel).click();
@@ -438,29 +817,37 @@ var PolkadotJS = class extends Wallet {
438
817
  };
439
818
 
440
819
  // src/wallets/index.ts
441
- var backpack = createWallet({
442
- name: "backpack",
443
- extensionDir: "backpack",
444
- WalletClass: Backpack
445
- });
446
820
  var metamask = createWallet({
447
821
  name: "metamask",
448
822
  extensionDir: "metamask",
449
- WalletClass: Metamask
823
+ WalletClass: Metamask,
824
+ homeUrl: "home.html"
450
825
  });
451
826
  var polkadotJS = createWallet({
452
827
  name: "polkadotJS",
453
828
  extensionDir: "polkadotjs",
454
- WalletClass: PolkadotJS
829
+ WalletClass: PolkadotJS,
830
+ homeUrl: "index.html"
455
831
  });
832
+
833
+ // src/cache/prepareWallet.ts
834
+ function prepareWallet(walletConfig, setupFn) {
835
+ return {
836
+ ...walletConfig,
837
+ setupFn,
838
+ __cached: true
839
+ };
840
+ }
456
841
  // Annotate the CommonJS export names for ESM import in node:
457
842
  0 && (module.exports = {
458
- Backpack,
459
843
  Metamask,
460
844
  PolkadotJS,
461
- backpack,
845
+ config,
462
846
  createWallet,
847
+ debug,
848
+ isCachedConfig,
463
849
  metamask,
464
850
  polkadotJS,
851
+ prepareWallet,
465
852
  withWallets
466
853
  });