w3wallets 0.1.2 → 0.2.0

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/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  ![License](https://img.shields.io/badge/License-MIT-yellow.svg)
4
4
  [![npm version](https://img.shields.io/npm/v/w3wallets.svg)](https://www.npmjs.com/package/w3wallets)
5
+ ![CodeQL](https://github.com/Maksandre/w3wallets/actions/workflows/github-code-scanning/codeql/badge.svg?branch=main)
5
6
 
6
7
  Web3 wallets for Playwright.
7
8
 
@@ -13,17 +14,15 @@ npm install -D w3wallets
13
14
 
14
15
  ## Getting Started
15
16
 
16
- Only the `Backpack` wallet is supported at this point.
17
+ The `Backpack` and the `Polkadot{.js}` wallets are currently supported.
17
18
 
18
- #### 1. Download Backpack
19
+ #### 1. Download wallets
19
20
 
20
- Currently, you need to download the extension manually. You can use [Chrome extension source viewer](https://chromewebstore.google.com/detail/chrome-extension-source-v/jifpbeccnghkjeaalbbjmodiffmgedin).
21
-
22
- Put the unzipped files to the root of your Playwright project `wallets/backpack`.
21
+ ```sh
22
+ npx w3wallets backpack polkadotJS
23
+ ```
23
24
 
24
- <!-- ```sh
25
- npx w3wallets
26
- ``` -->
25
+ The unzipped files should be stored in the `.w3wallets/<wallet-name>` directory. Add them to `.gitignore`.
27
26
 
28
27
  #### 2. Wrap your fixture `withWallets`
29
28
 
@@ -31,7 +30,8 @@ npx w3wallets
31
30
  import { test as base } from "@playwright/test";
32
31
  import { withWallets } from "../src/withWallets";
33
32
 
34
- const test = withWallets(base, { backpack: true });
33
+ // Specify one or many wallets that should be installed in the browser
34
+ const test = withWallets(base, "backpack", "polkadotJS");
35
35
 
36
36
  test("has title", async ({ page, backpack }) => {
37
37
  await page.goto("https://playwright.dev/");
@@ -42,3 +42,38 @@ test("has title", async ({ page, backpack }) => {
42
42
  await backpack.onboard("Eclipse", privateKey);
43
43
  });
44
44
  ```
45
+
46
+ ## Run tests
47
+
48
+ To work on this project in VS Code, make sure you open the project's root directory.
49
+
50
+ 0. Create the `.env` using `.env.example` as a reference.
51
+ 1. Install dependencies
52
+
53
+ ```sh
54
+ yarn
55
+ ```
56
+
57
+ 2. Install Chrome browser
58
+
59
+ ```sh
60
+ npx playwright install chromium
61
+ ```
62
+
63
+ 3. Download wallet extensions
64
+
65
+ ```sh
66
+ npx w3wallets backpack polkadotJS
67
+ ```
68
+
69
+ 4. Start UI
70
+
71
+ ```sh
72
+ yarn start:ui
73
+ ```
74
+
75
+ 5. Run Tests with Playwright
76
+
77
+ ```sh
78
+ yarn test
79
+ ```
package/dist/index.d.mts CHANGED
@@ -1,13 +1,27 @@
1
1
  import * as playwright_test from 'playwright/test';
2
2
  import { Page, test, BrowserContext } from '@playwright/test';
3
3
 
4
- type BackPackNetwork = "Eclipse" | "Ethereum";
4
+ /**
5
+ * Represents the supported network types for the BackPack application.
6
+ */
7
+ type BackPackNetwork = "Solana" | "Eclipse" | "Ethereum" | "Polygon" | "Base" | "Arbitrum" | "Optimism";
5
8
 
6
- declare class Backpack {
7
- private page;
8
- private extensionId;
9
- private defaultPassword;
9
+ type WalletName = "backpack" | "polkadotJS";
10
+ type NoDuplicates<T extends readonly unknown[], Acc extends readonly unknown[] = []> = T extends [infer Head, ...infer Tail] ? Head extends Acc[number] ? never : [Head, ...NoDuplicates<Tail, [...Acc, Head]>] : T;
11
+ interface IWallet {
12
+ gotoOnboardPage(): Promise<void>;
13
+ }
14
+
15
+ declare abstract class Wallet implements IWallet {
16
+ protected page: Page;
17
+ protected extensionId: string;
10
18
  constructor(page: Page, extensionId: string);
19
+ abstract gotoOnboardPage(): Promise<void>;
20
+ }
21
+
22
+ declare class Backpack extends Wallet {
23
+ private defaultPassword;
24
+ gotoOnboardPage(): Promise<void>;
11
25
  onboard(network: BackPackNetwork, privateKey: string): Promise<void>;
12
26
  unlock(): Promise<void>;
13
27
  goToSettings(accountName?: string): Promise<void>;
@@ -17,13 +31,22 @@ declare class Backpack {
17
31
  deny(): Promise<void>;
18
32
  }
19
33
 
20
- type Config = {
21
- backpack: boolean;
22
- };
23
- declare function withWallets(test: typeof test, config: Config): playwright_test.TestType<playwright_test.PlaywrightTestArgs & playwright_test.PlaywrightTestOptions & {
34
+ declare class PolkadotJS extends Wallet {
35
+ private defaultPassword;
36
+ gotoOnboardPage(): Promise<void>;
37
+ onboard(seed: string, password?: string, name?: string): Promise<void>;
38
+ selectAllAccounts(): Promise<void>;
39
+ selectAccount(accountId: string): Promise<void>;
40
+ enterPassword(password?: string): Promise<void>;
41
+ approve(): Promise<void>;
42
+ deny(): Promise<void>;
43
+ private _getLabeledInput;
44
+ }
45
+
46
+ declare function withWallets<T extends readonly WalletName[]>(test: typeof test, ...config: NoDuplicates<T>): playwright_test.TestType<playwright_test.PlaywrightTestArgs & playwright_test.PlaywrightTestOptions & {
24
47
  context: BrowserContext;
25
48
  backpack: Backpack;
26
- extensionId: string;
49
+ polkadotJS: PolkadotJS;
27
50
  }, playwright_test.PlaywrightWorkerArgs & playwright_test.PlaywrightWorkerOptions>;
28
51
 
29
52
  export { withWallets };
package/dist/index.d.ts CHANGED
@@ -1,13 +1,27 @@
1
1
  import * as playwright_test from 'playwright/test';
2
2
  import { Page, test, BrowserContext } from '@playwright/test';
3
3
 
4
- type BackPackNetwork = "Eclipse" | "Ethereum";
4
+ /**
5
+ * Represents the supported network types for the BackPack application.
6
+ */
7
+ type BackPackNetwork = "Solana" | "Eclipse" | "Ethereum" | "Polygon" | "Base" | "Arbitrum" | "Optimism";
5
8
 
6
- declare class Backpack {
7
- private page;
8
- private extensionId;
9
- private defaultPassword;
9
+ type WalletName = "backpack" | "polkadotJS";
10
+ type NoDuplicates<T extends readonly unknown[], Acc extends readonly unknown[] = []> = T extends [infer Head, ...infer Tail] ? Head extends Acc[number] ? never : [Head, ...NoDuplicates<Tail, [...Acc, Head]>] : T;
11
+ interface IWallet {
12
+ gotoOnboardPage(): Promise<void>;
13
+ }
14
+
15
+ declare abstract class Wallet implements IWallet {
16
+ protected page: Page;
17
+ protected extensionId: string;
10
18
  constructor(page: Page, extensionId: string);
19
+ abstract gotoOnboardPage(): Promise<void>;
20
+ }
21
+
22
+ declare class Backpack extends Wallet {
23
+ private defaultPassword;
24
+ gotoOnboardPage(): Promise<void>;
11
25
  onboard(network: BackPackNetwork, privateKey: string): Promise<void>;
12
26
  unlock(): Promise<void>;
13
27
  goToSettings(accountName?: string): Promise<void>;
@@ -17,13 +31,22 @@ declare class Backpack {
17
31
  deny(): Promise<void>;
18
32
  }
19
33
 
20
- type Config = {
21
- backpack: boolean;
22
- };
23
- declare function withWallets(test: typeof test, config: Config): playwright_test.TestType<playwright_test.PlaywrightTestArgs & playwright_test.PlaywrightTestOptions & {
34
+ declare class PolkadotJS extends Wallet {
35
+ private defaultPassword;
36
+ gotoOnboardPage(): Promise<void>;
37
+ onboard(seed: string, password?: string, name?: string): Promise<void>;
38
+ selectAllAccounts(): Promise<void>;
39
+ selectAccount(accountId: string): Promise<void>;
40
+ enterPassword(password?: string): Promise<void>;
41
+ approve(): Promise<void>;
42
+ deny(): Promise<void>;
43
+ private _getLabeledInput;
44
+ }
45
+
46
+ declare function withWallets<T extends readonly WalletName[]>(test: typeof test, ...config: NoDuplicates<T>): playwright_test.TestType<playwright_test.PlaywrightTestArgs & playwright_test.PlaywrightTestOptions & {
24
47
  context: BrowserContext;
25
48
  backpack: Backpack;
26
- extensionId: string;
49
+ polkadotJS: PolkadotJS;
27
50
  }, playwright_test.PlaywrightWorkerArgs & playwright_test.PlaywrightWorkerOptions>;
28
51
 
29
52
  export { withWallets };
package/dist/index.js CHANGED
@@ -37,17 +37,33 @@ module.exports = __toCommonJS(index_exports);
37
37
  // src/withWallets.ts
38
38
  var import_path = __toESM(require("path"));
39
39
  var import_fs = __toESM(require("fs"));
40
- var import_test2 = require("@playwright/test");
40
+
41
+ // tests/utils/sleep.ts
42
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
43
+
44
+ // src/withWallets.ts
41
45
  var import_test3 = require("@playwright/test");
42
46
 
43
47
  // src/backpack/backpack.ts
44
48
  var import_test = require("@playwright/test");
45
- var Backpack = class {
49
+
50
+ // src/wallet.ts
51
+ var Wallet = class {
46
52
  constructor(page, extensionId) {
47
53
  this.page = page;
48
54
  this.extensionId = extensionId;
49
55
  }
56
+ };
57
+
58
+ // src/backpack/backpack.ts
59
+ var Backpack = class extends Wallet {
50
60
  defaultPassword = "11111111";
61
+ async gotoOnboardPage() {
62
+ await this.page.goto(
63
+ `chrome-extension://${this.extensionId}/options.html?onboarding=true`
64
+ );
65
+ await (0, import_test.expect)(this.page.getByText("Welcome to Backpack")).toBeVisible();
66
+ }
51
67
  async onboard(network, privateKey) {
52
68
  await this.page.getByRole("button", { name: "Import wallet" }).click();
53
69
  await this.page.getByRole("button", { name: network }).click();
@@ -89,51 +105,164 @@ var Backpack = class {
89
105
  }
90
106
  };
91
107
 
108
+ // src/polkadotJS/polkadotJS.ts
109
+ var import_test2 = require("@playwright/test");
110
+ var PolkadotJS = class extends Wallet {
111
+ defaultPassword = "11111111";
112
+ async gotoOnboardPage() {
113
+ await this.page.goto(`chrome-extension://${this.extensionId}/index.html`);
114
+ await (0, import_test2.expect)(
115
+ this.page.getByText("Before we start, just a couple of notes")
116
+ ).toBeVisible();
117
+ }
118
+ async onboard(seed, password, name) {
119
+ await this.page.getByRole("button", { name: "Understood, let me continue" }).click();
120
+ await this.page.locator(".popupToggle").first().click();
121
+ await this.page.getByText("Import account from pre-existing seed").click();
122
+ await this.page.locator(".seedInput").getByRole("textbox").fill(seed);
123
+ await this.page.getByRole("button", { name: "Next" }).click();
124
+ await this._getLabeledInput("A descriptive name for your account").fill(
125
+ name ?? "Test"
126
+ );
127
+ await this._getLabeledInput("A new password for this account").fill(
128
+ password ?? this.defaultPassword
129
+ );
130
+ await this._getLabeledInput("Repeat password for verification").fill(
131
+ password ?? this.defaultPassword
132
+ );
133
+ await this.page.getByRole("button", { name: "Add the account with the supplied seed" }).click();
134
+ }
135
+ async selectAllAccounts() {
136
+ await this.page.getByText("Select all").click();
137
+ }
138
+ async selectAccount(accountId) {
139
+ await this.page.locator(".accountWichCheckbox").filter({ hasText: accountId }).locator(".accountTree-checkbox").locator("span").check();
140
+ }
141
+ async enterPassword(password) {
142
+ await this._getLabeledInput("Password for this account").fill(
143
+ password ?? this.defaultPassword
144
+ );
145
+ }
146
+ async approve() {
147
+ const connect = this.page.getByRole("button", { name: "Connect" });
148
+ const signTransaction = this.page.getByRole("button", {
149
+ name: "Sign the transaction"
150
+ });
151
+ await connect.or(signTransaction).click();
152
+ }
153
+ async deny() {
154
+ const reject = this.page.getByRole("button", { name: "Reject" });
155
+ const cancel = this.page.getByRole("link", { name: "Cancel" });
156
+ await reject.or(cancel).click();
157
+ }
158
+ _getLabeledInput(label) {
159
+ return this.page.locator(
160
+ `//label[text()="${label}"]/following-sibling::input`
161
+ );
162
+ }
163
+ };
164
+
92
165
  // src/withWallets.ts
93
- function withWallets(test, config) {
94
- const backpack = import_path.default.join(process.cwd(), "wallets", "backpack");
166
+ var w3walletsDir = ".w3wallets";
167
+ function withWallets(test, ...config) {
168
+ const withBackpack = config.includes("backpack");
169
+ const withPolkadotJS = config.includes("polkadotJS");
170
+ const backpackPath = import_path.default.join(process.cwd(), w3walletsDir, "backpack");
171
+ const polkadotJSPath = import_path.default.join(process.cwd(), w3walletsDir, "polkadotJS");
95
172
  return test.extend({
96
- backpack: async ({ context, extensionId }, use) => {
97
- const page = context.pages()[0];
98
- if (!page) throw Error("No pages in context");
99
- const backpack2 = new Backpack(page, extensionId);
100
- await page.goto(
101
- `chrome-extension://${extensionId}/options.html?onboarding=true`
102
- );
103
- await use(backpack2);
104
- },
173
+ /**
174
+ * Sets up a persistent browser context with the requested extensions loaded.
175
+ */
105
176
  context: async ({}, use, testInfo) => {
106
177
  const userDataDir = import_path.default.join(
107
178
  process.cwd(),
108
179
  ".w3wallets",
180
+ ".context",
109
181
  testInfo.testId
110
182
  );
111
- if (import_fs.default.existsSync(userDataDir))
112
- import_fs.default.rmSync(userDataDir, { recursive: true });
113
- const backpackDownloaded = import_fs.default.existsSync(
114
- import_path.default.join(backpack, "manifest.json")
115
- );
116
- if (!backpackDownloaded)
117
- throw Error("Cannot find Backpack. download it `npx w3wallets`");
183
+ cleanUserDataDir(userDataDir);
184
+ const extensionPaths = [];
185
+ if (withBackpack) {
186
+ ensureWalletExtensionExists(backpackPath, "backpack");
187
+ extensionPaths.push(backpackPath);
188
+ }
189
+ if (withPolkadotJS) {
190
+ ensureWalletExtensionExists(polkadotJSPath, "polkadotJS");
191
+ extensionPaths.push(polkadotJSPath);
192
+ }
118
193
  const context = await import_test3.chromium.launchPersistentContext(userDataDir, {
119
194
  headless: false,
120
195
  args: [
121
- `--disable-extensions-except=${backpack}`,
122
- `--load-extension=${backpack}`
196
+ `--disable-extensions-except=${extensionPaths.join(",")}`,
197
+ `--load-extension=${extensionPaths.join(",")}`
123
198
  ]
124
199
  });
200
+ await context.waitForEvent("serviceworker");
201
+ while (context.serviceWorkers().length < extensionPaths.length) {
202
+ await sleep(1e3);
203
+ }
125
204
  await use(context);
126
205
  await context.close();
127
206
  },
128
- extensionId: async ({ context }, use) => {
129
- let [background] = context.serviceWorkers();
130
- if (!background) background = await context.waitForEvent("serviceworker");
131
- const extensionId = background.url().split("/")[2];
132
- if (!extensionId) throw Error("No extension id");
133
- await use(extensionId);
207
+ backpack: async ({ context }, use) => {
208
+ if (!withBackpack) {
209
+ throw Error(
210
+ "The Backpack wallet hasn't been loaded. Add it to the withWallets function."
211
+ );
212
+ }
213
+ const backpack = await initializeExtension(
214
+ context,
215
+ Backpack,
216
+ "Backpack is not initialized"
217
+ );
218
+ await use(backpack);
219
+ },
220
+ polkadotJS: async ({ context }, use) => {
221
+ if (!withPolkadotJS) {
222
+ throw Error(
223
+ "The Polkadot{.js} wallet hasn't been loaded. Add it to the withWallets function."
224
+ );
225
+ }
226
+ const polkadotJS = await initializeExtension(
227
+ context,
228
+ PolkadotJS,
229
+ "Polkadot{.js} is not initialized"
230
+ );
231
+ await use(polkadotJS);
134
232
  }
135
233
  });
136
234
  }
235
+ function cleanUserDataDir(userDataDir) {
236
+ if (import_fs.default.existsSync(userDataDir)) {
237
+ import_fs.default.rmSync(userDataDir, { recursive: true });
238
+ }
239
+ }
240
+ function ensureWalletExtensionExists(walletPath, walletName) {
241
+ if (!import_fs.default.existsSync(import_path.default.join(walletPath, "manifest.json"))) {
242
+ throw new Error(
243
+ `Cannot find ${walletName}. Please download it via 'npx w3wallets ${walletName}'.`
244
+ );
245
+ }
246
+ }
247
+ async function initializeExtension(context, ExtensionClass, notInitializedErrorMessage) {
248
+ const serviceWorkers = context.serviceWorkers();
249
+ let page = await context.newPage();
250
+ for (const worker of serviceWorkers) {
251
+ const extensionId = worker.url().split("/")[2];
252
+ if (!extensionId) {
253
+ continue;
254
+ }
255
+ const extension = new ExtensionClass(page, extensionId);
256
+ try {
257
+ await extension.gotoOnboardPage();
258
+ return extension;
259
+ } catch {
260
+ await page.close();
261
+ page = await context.newPage();
262
+ }
263
+ }
264
+ throw new Error(notInitializedErrorMessage);
265
+ }
137
266
  // Annotate the CommonJS export names for ESM import in node:
138
267
  0 && (module.exports = {
139
268
  withWallets
package/dist/index.mjs CHANGED
@@ -1,17 +1,35 @@
1
1
  // src/withWallets.ts
2
2
  import path from "path";
3
3
  import fs from "fs";
4
- import "@playwright/test";
5
- import { chromium } from "@playwright/test";
4
+
5
+ // tests/utils/sleep.ts
6
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
7
+
8
+ // src/withWallets.ts
9
+ import {
10
+ chromium
11
+ } from "@playwright/test";
6
12
 
7
13
  // src/backpack/backpack.ts
8
14
  import { expect } from "@playwright/test";
9
- var Backpack = class {
15
+
16
+ // src/wallet.ts
17
+ var Wallet = class {
10
18
  constructor(page, extensionId) {
11
19
  this.page = page;
12
20
  this.extensionId = extensionId;
13
21
  }
22
+ };
23
+
24
+ // src/backpack/backpack.ts
25
+ var Backpack = class extends Wallet {
14
26
  defaultPassword = "11111111";
27
+ async gotoOnboardPage() {
28
+ await this.page.goto(
29
+ `chrome-extension://${this.extensionId}/options.html?onboarding=true`
30
+ );
31
+ await expect(this.page.getByText("Welcome to Backpack")).toBeVisible();
32
+ }
15
33
  async onboard(network, privateKey) {
16
34
  await this.page.getByRole("button", { name: "Import wallet" }).click();
17
35
  await this.page.getByRole("button", { name: network }).click();
@@ -53,51 +71,164 @@ var Backpack = class {
53
71
  }
54
72
  };
55
73
 
74
+ // src/polkadotJS/polkadotJS.ts
75
+ import { expect as expect2 } from "@playwright/test";
76
+ var PolkadotJS = class extends Wallet {
77
+ defaultPassword = "11111111";
78
+ async gotoOnboardPage() {
79
+ await this.page.goto(`chrome-extension://${this.extensionId}/index.html`);
80
+ await expect2(
81
+ this.page.getByText("Before we start, just a couple of notes")
82
+ ).toBeVisible();
83
+ }
84
+ async onboard(seed, password, name) {
85
+ await this.page.getByRole("button", { name: "Understood, let me continue" }).click();
86
+ await this.page.locator(".popupToggle").first().click();
87
+ await this.page.getByText("Import account from pre-existing seed").click();
88
+ await this.page.locator(".seedInput").getByRole("textbox").fill(seed);
89
+ await this.page.getByRole("button", { name: "Next" }).click();
90
+ await this._getLabeledInput("A descriptive name for your account").fill(
91
+ name ?? "Test"
92
+ );
93
+ await this._getLabeledInput("A new password for this account").fill(
94
+ password ?? this.defaultPassword
95
+ );
96
+ await this._getLabeledInput("Repeat password for verification").fill(
97
+ password ?? this.defaultPassword
98
+ );
99
+ await this.page.getByRole("button", { name: "Add the account with the supplied seed" }).click();
100
+ }
101
+ async selectAllAccounts() {
102
+ await this.page.getByText("Select all").click();
103
+ }
104
+ async selectAccount(accountId) {
105
+ await this.page.locator(".accountWichCheckbox").filter({ hasText: accountId }).locator(".accountTree-checkbox").locator("span").check();
106
+ }
107
+ async enterPassword(password) {
108
+ await this._getLabeledInput("Password for this account").fill(
109
+ password ?? this.defaultPassword
110
+ );
111
+ }
112
+ async approve() {
113
+ const connect = this.page.getByRole("button", { name: "Connect" });
114
+ const signTransaction = this.page.getByRole("button", {
115
+ name: "Sign the transaction"
116
+ });
117
+ await connect.or(signTransaction).click();
118
+ }
119
+ async deny() {
120
+ const reject = this.page.getByRole("button", { name: "Reject" });
121
+ const cancel = this.page.getByRole("link", { name: "Cancel" });
122
+ await reject.or(cancel).click();
123
+ }
124
+ _getLabeledInput(label) {
125
+ return this.page.locator(
126
+ `//label[text()="${label}"]/following-sibling::input`
127
+ );
128
+ }
129
+ };
130
+
56
131
  // src/withWallets.ts
57
- function withWallets(test, config) {
58
- const backpack = path.join(process.cwd(), "wallets", "backpack");
132
+ var w3walletsDir = ".w3wallets";
133
+ function withWallets(test, ...config) {
134
+ const withBackpack = config.includes("backpack");
135
+ const withPolkadotJS = config.includes("polkadotJS");
136
+ const backpackPath = path.join(process.cwd(), w3walletsDir, "backpack");
137
+ const polkadotJSPath = path.join(process.cwd(), w3walletsDir, "polkadotJS");
59
138
  return test.extend({
60
- backpack: async ({ context, extensionId }, use) => {
61
- const page = context.pages()[0];
62
- if (!page) throw Error("No pages in context");
63
- const backpack2 = new Backpack(page, extensionId);
64
- await page.goto(
65
- `chrome-extension://${extensionId}/options.html?onboarding=true`
66
- );
67
- await use(backpack2);
68
- },
139
+ /**
140
+ * Sets up a persistent browser context with the requested extensions loaded.
141
+ */
69
142
  context: async ({}, use, testInfo) => {
70
143
  const userDataDir = path.join(
71
144
  process.cwd(),
72
145
  ".w3wallets",
146
+ ".context",
73
147
  testInfo.testId
74
148
  );
75
- if (fs.existsSync(userDataDir))
76
- fs.rmSync(userDataDir, { recursive: true });
77
- const backpackDownloaded = fs.existsSync(
78
- path.join(backpack, "manifest.json")
79
- );
80
- if (!backpackDownloaded)
81
- throw Error("Cannot find Backpack. download it `npx w3wallets`");
149
+ cleanUserDataDir(userDataDir);
150
+ const extensionPaths = [];
151
+ if (withBackpack) {
152
+ ensureWalletExtensionExists(backpackPath, "backpack");
153
+ extensionPaths.push(backpackPath);
154
+ }
155
+ if (withPolkadotJS) {
156
+ ensureWalletExtensionExists(polkadotJSPath, "polkadotJS");
157
+ extensionPaths.push(polkadotJSPath);
158
+ }
82
159
  const context = await chromium.launchPersistentContext(userDataDir, {
83
160
  headless: false,
84
161
  args: [
85
- `--disable-extensions-except=${backpack}`,
86
- `--load-extension=${backpack}`
162
+ `--disable-extensions-except=${extensionPaths.join(",")}`,
163
+ `--load-extension=${extensionPaths.join(",")}`
87
164
  ]
88
165
  });
166
+ await context.waitForEvent("serviceworker");
167
+ while (context.serviceWorkers().length < extensionPaths.length) {
168
+ await sleep(1e3);
169
+ }
89
170
  await use(context);
90
171
  await context.close();
91
172
  },
92
- extensionId: async ({ context }, use) => {
93
- let [background] = context.serviceWorkers();
94
- if (!background) background = await context.waitForEvent("serviceworker");
95
- const extensionId = background.url().split("/")[2];
96
- if (!extensionId) throw Error("No extension id");
97
- await use(extensionId);
173
+ backpack: async ({ context }, use) => {
174
+ if (!withBackpack) {
175
+ throw Error(
176
+ "The Backpack wallet hasn't been loaded. Add it to the withWallets function."
177
+ );
178
+ }
179
+ const backpack = await initializeExtension(
180
+ context,
181
+ Backpack,
182
+ "Backpack is not initialized"
183
+ );
184
+ await use(backpack);
185
+ },
186
+ polkadotJS: async ({ context }, use) => {
187
+ if (!withPolkadotJS) {
188
+ throw Error(
189
+ "The Polkadot{.js} wallet hasn't been loaded. Add it to the withWallets function."
190
+ );
191
+ }
192
+ const polkadotJS = await initializeExtension(
193
+ context,
194
+ PolkadotJS,
195
+ "Polkadot{.js} is not initialized"
196
+ );
197
+ await use(polkadotJS);
98
198
  }
99
199
  });
100
200
  }
201
+ function cleanUserDataDir(userDataDir) {
202
+ if (fs.existsSync(userDataDir)) {
203
+ fs.rmSync(userDataDir, { recursive: true });
204
+ }
205
+ }
206
+ function ensureWalletExtensionExists(walletPath, walletName) {
207
+ if (!fs.existsSync(path.join(walletPath, "manifest.json"))) {
208
+ throw new Error(
209
+ `Cannot find ${walletName}. Please download it via 'npx w3wallets ${walletName}'.`
210
+ );
211
+ }
212
+ }
213
+ async function initializeExtension(context, ExtensionClass, notInitializedErrorMessage) {
214
+ const serviceWorkers = context.serviceWorkers();
215
+ let page = await context.newPage();
216
+ for (const worker of serviceWorkers) {
217
+ const extensionId = worker.url().split("/")[2];
218
+ if (!extensionId) {
219
+ continue;
220
+ }
221
+ const extension = new ExtensionClass(page, extensionId);
222
+ try {
223
+ await extension.gotoOnboardPage();
224
+ return extension;
225
+ } catch {
226
+ await page.close();
227
+ page = await context.newPage();
228
+ }
229
+ }
230
+ throw new Error(notInitializedErrorMessage);
231
+ }
101
232
  export {
102
233
  withWallets
103
234
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "w3wallets",
3
3
  "description": "browser wallets for playwright",
4
- "version": "0.1.2",
4
+ "version": "0.2.0",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "homepage": "https://github.com/Maksandre/w3wallets",
@@ -13,6 +13,15 @@
13
13
  "type": "git",
14
14
  "url": "git+https://github.com/Maksandre/w3wallets.git"
15
15
  },
16
+ "keywords": [
17
+ "e2e",
18
+ "playwright",
19
+ "backpack",
20
+ "testing",
21
+ "ethereum",
22
+ "polkadot",
23
+ "eclipse"
24
+ ],
16
25
  "license": "MIT",
17
26
  "publishConfig": {
18
27
  "access": "public"
@@ -22,6 +31,8 @@
22
31
  ],
23
32
  "bin": "./src/scripts/download.js",
24
33
  "scripts": {
34
+ "start:ui": "yarn workspace @w3wallets/test-app dev",
35
+ "test": "npx playwright test",
25
36
  "build": "tsup",
26
37
  "clean": "rm -rf dist",
27
38
  "check-format": "prettier --check .",
@@ -33,20 +44,21 @@
33
44
  "local-release": "changeset version && changeset publish"
34
45
  },
35
46
  "dependencies": {
36
- "adm-zip": "^0.5.16",
37
- "follow-redirects": "^1.15.9"
47
+ "follow-redirects": "^1.15.9",
48
+ "unzip-crx": "^0.2.0"
38
49
  },
39
50
  "devDependencies": {
40
51
  "@arethetypeswrong/cli": "^0.17.2",
41
52
  "@changesets/cli": "^2.27.11",
42
- "@playwright/test": "^1.49.1",
53
+ "@playwright/test": "^1.50.1",
43
54
  "@types/node": "^22.10.5",
55
+ "dotenv": "^16.4.7",
44
56
  "prettier": "^3.4.2",
45
57
  "standard-version": "^9.5.0",
46
58
  "tsup": "^8.3.5",
47
59
  "typescript": "^5.7.2"
48
60
  },
49
61
  "peerDependencies": {
50
- "@playwright/test": "^1.49.1"
62
+ "@playwright/test": "^1.50.1"
51
63
  }
52
64
  }
@@ -1,79 +1,307 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ /**
4
+ *
5
+ * Downloads and extracts Chrome extensions by alias ("backpack" and "metamask")
6
+ *
7
+ * Usage:
8
+ * npx w3wallets backpack
9
+ * npx w3wallets metamask
10
+ * npx w3wallets backpack metamask
11
+ */
12
+
3
13
  const fs = require("fs");
14
+ const https = require("https");
4
15
  const path = require("path");
5
- const { https } = require("follow-redirects");
6
- const AdmZip = require("adm-zip");
7
-
8
- const args = process.argv.slice(2);
9
-
10
- // Default configurations
11
- const DEFAULT_WALLET = "backpack";
12
- const DEFAULT_DOWNLOAD_LINK =
13
- "https://github.com/coral-xyz/backpack/releases/download/0.10.1-latest-4/build-beta-4.zip";
14
-
15
- // Parse arguments
16
- const walletArg = args.find((arg) => arg.startsWith("--wallet="));
17
- const downloadArg = args.find((arg) => arg.startsWith("--version="));
18
-
19
- const wallet = walletArg ? walletArg.split("=")[1] : DEFAULT_WALLET;
20
- const downloadLink = downloadArg
21
- ? downloadArg.split("=")[1]
22
- : DEFAULT_DOWNLOAD_LINK;
23
- const outputDir = path.resolve(`extensions/${wallet}`);
24
- const zipPath = path.resolve(outputDir, `${wallet}.zip`);
25
-
26
- console.log(`Fetching ${wallet}...`);
27
-
28
- // Ensure the output directory exists
29
- fs.mkdirSync(outputDir, { recursive: true });
30
-
31
- // Download the zip file
32
- https
33
- .get(downloadLink, (response) => {
34
- if (response.statusCode !== 200) {
35
- console.error(
36
- `Failed to download file. Status Code: ${response.statusCode}`,
37
- );
38
- return;
16
+ const url = require("url");
17
+ const zlib = require("zlib");
18
+
19
+ // ---------------------------------------------------------------------
20
+ // 1. Known aliases -> extension IDs
21
+ // ---------------------------------------------------------------------
22
+ const ALIASES = {
23
+ backpack: "aflkmfhebedbjioipglgcbcmnbpgliof",
24
+ metamask: "nkbihfbeogaeaoehlefnkodbefgpgknn",
25
+ polkadotJS: "mopnmbcafieddcagagdcbnhejhlodfdd",
26
+ };
27
+
28
+ // ---------------------------------------------------------------------
29
+ // 2. Read aliases from CLI
30
+ // ---------------------------------------------------------------------
31
+ const inputAliases = process.argv.slice(2);
32
+
33
+ if (!inputAliases.length) {
34
+ console.error("Usage: npx w3wallets <aliases...>");
35
+ console.error("Available aliases:", Object.keys(ALIASES).join(", "));
36
+ process.exit(1);
37
+ }
38
+
39
+ for (const alias of inputAliases) {
40
+ if (!ALIASES[alias]) {
41
+ console.error(
42
+ `Unknown alias "${alias}". Must be one of: ${Object.keys(ALIASES).join(", ")}`,
43
+ );
44
+ process.exit(1);
45
+ }
46
+ }
47
+
48
+ // ---------------------------------------------------------------------
49
+ // 3. Main: download and extract each requested alias
50
+ // ---------------------------------------------------------------------
51
+ (async function main() {
52
+ for (const alias of inputAliases) {
53
+ const extensionId = ALIASES[alias];
54
+ console.log(`\n=== Processing alias: "${alias}" (ID: ${extensionId}) ===`);
55
+
56
+ try {
57
+ // 1) Download CRX
58
+ const crxBuffer = await downloadCrx(extensionId);
59
+ console.log(`Got CRX data for "${alias}"! ${crxBuffer.length} bytes`);
60
+
61
+ // 2) Save raw CRX to disk
62
+ const outDir = path.join(".w3wallets", alias);
63
+ fs.mkdirSync(outDir, { recursive: true });
64
+
65
+ const debugPath = path.join(outDir, `debug-${alias}.crx`);
66
+ fs.writeFileSync(debugPath, crxBuffer);
67
+ console.log(`Saved ${debugPath}`);
68
+
69
+ // 3) Extract CRX into "wallets/<alias>"
70
+ extractCrxToFolder(crxBuffer, outDir);
71
+ console.log(`Extraction complete! See folder: ${outDir}`);
72
+ } catch (err) {
73
+ console.error(`Failed to process "${alias}":`, err.message);
74
+ process.exit(1);
75
+ }
76
+ }
77
+ })();
78
+
79
+ // ---------------------------------------------------------------------
80
+ // downloadCrx: Build CRX URL and fetch it
81
+ // ---------------------------------------------------------------------
82
+ async function downloadCrx(extensionId) {
83
+ const downloadUrl =
84
+ "https://clients2.google.com/service/update2/crx" +
85
+ "?response=redirect" +
86
+ "&prod=chrome" +
87
+ "&prodversion=9999" +
88
+ "&acceptformat=crx2,crx3" +
89
+ `&x=id%3D${extensionId}%26uc`;
90
+
91
+ console.log("Requesting:", downloadUrl);
92
+
93
+ const crxBuffer = await fetchUrl(downloadUrl);
94
+ return crxBuffer;
95
+ }
96
+
97
+ // ---------------------------------------------------------------------
98
+ // fetchUrl: minimal GET + redirect handling
99
+ // ---------------------------------------------------------------------
100
+ function fetchUrl(
101
+ targetUrl,
102
+ options = {},
103
+ redirectCount = 0,
104
+ maxRedirects = 10,
105
+ ) {
106
+ return new Promise((resolve, reject) => {
107
+ if (redirectCount > maxRedirects) {
108
+ return reject(new Error("Too many redirects"));
39
109
  }
40
110
 
41
- const fileStream = fs.createWriteStream(zipPath);
42
- response.pipe(fileStream);
43
-
44
- fileStream.on("finish", () => {
45
- fileStream.close();
46
- console.log(`Downloaded to ${zipPath}`);
47
-
48
- // Unzip the file
49
- console.log("Unzipping...");
50
- const zip = new AdmZip(zipPath);
51
- zip.extractAllTo(outputDir, true);
52
- console.log(`Extracted to ${outputDir}`);
53
-
54
- fs.unlinkSync(zipPath);
55
-
56
- // Check if the result is a single directory with manifest.json inside
57
- const files = fs.readdirSync(outputDir);
58
- if (files.length === 1) {
59
- const singleDirPath = path.join(outputDir, files[0]);
60
- if (
61
- fs.lstatSync(singleDirPath).isDirectory() &&
62
- fs.existsSync(path.join(singleDirPath, "manifest.json"))
63
- ) {
64
- // Move all files from the directory to the outputDir
65
- const nestedFiles = fs.readdirSync(singleDirPath);
66
- nestedFiles.forEach((file) => {
67
- const srcPath = path.join(singleDirPath, file);
68
- const destPath = path.join(outputDir, file);
69
- fs.renameSync(srcPath, destPath);
70
- });
71
- } else {
72
- throw Error("Cannot find the manifest.json file");
73
- }
111
+ const req = https.get(targetUrl, options, (res) => {
112
+ const { statusCode, headers } = res;
113
+
114
+ // Follow redirects
115
+ if ([301, 302, 303, 307, 308].includes(statusCode) && headers.location) {
116
+ const newUrl = url.resolve(targetUrl, headers.location);
117
+ res.resume(); // discard body
118
+ return resolve(
119
+ fetchUrl(newUrl, options, redirectCount + 1, maxRedirects),
120
+ );
121
+ }
122
+
123
+ if (statusCode !== 200) {
124
+ res.resume();
125
+ return reject(
126
+ new Error(`Request failed with status code ${statusCode}`),
127
+ );
74
128
  }
129
+
130
+ const dataChunks = [];
131
+ res.on("data", (chunk) => dataChunks.push(chunk));
132
+ res.on("end", () => resolve(Buffer.concat(dataChunks)));
75
133
  });
76
- })
77
- .on("error", (err) => {
78
- console.error(`Error downloading the file: ${err.message}`);
134
+
135
+ req.on("error", (err) => reject(err));
79
136
  });
137
+ }
138
+
139
+ // ---------------------------------------------------------------------
140
+ // extractCrxToFolder
141
+ // 1) Checks "Cr24" magic
142
+ // 2) Reads version (2 or 3/4) to find the ZIP start
143
+ // 3) Uses parseZipCentralDirectory() to extract files properly
144
+ // ---------------------------------------------------------------------
145
+ function extractCrxToFolder(crxBuffer, outFolder) {
146
+ if (crxBuffer.toString("utf8", 0, 4) !== "Cr24") {
147
+ throw new Error("Not a valid CRX file (missing Cr24 magic).");
148
+ }
149
+
150
+ const version = crxBuffer.readUInt32LE(4);
151
+ let zipStartOffset = 0;
152
+ if (version === 2) {
153
+ const pkLen = crxBuffer.readUInt32LE(8);
154
+ const sigLen = crxBuffer.readUInt32LE(12);
155
+ zipStartOffset = 16 + pkLen + sigLen;
156
+ } else if (version === 3 || version === 4) {
157
+ const headerSize = crxBuffer.readUInt32LE(8);
158
+ zipStartOffset = 12 + headerSize;
159
+ } else {
160
+ throw new Error(
161
+ `Unsupported CRX version (${version}). Only v2, v3, or v4 are supported.`,
162
+ );
163
+ }
164
+
165
+ if (zipStartOffset >= crxBuffer.length) {
166
+ throw new Error("Malformed CRX: header size exceeds file length.");
167
+ }
168
+
169
+ const zipBuffer = crxBuffer.slice(zipStartOffset);
170
+
171
+ // Parse that ZIP via the central directory approach
172
+ parseZipCentralDirectory(zipBuffer, outFolder);
173
+ }
174
+
175
+ // ---------------------------------------------------------------------
176
+ // parseZipCentralDirectory(buffer, outFolder)
177
+ // 1) Finds End of Central Directory (EOCD) record (0x06054b50).
178
+ // 2) Reads central directory for file metadata
179
+ // 3) For each file, decompress into outFolder
180
+ // ---------------------------------------------------------------------
181
+ function parseZipCentralDirectory(zipBuffer, outFolder) {
182
+ const eocdSig = 0x06054b50;
183
+ let eocdPos = -1;
184
+ const minPos = Math.max(0, zipBuffer.length - 65557);
185
+ for (let i = zipBuffer.length - 4; i >= minPos; i--) {
186
+ if (zipBuffer.readUInt32LE(i) === eocdSig) {
187
+ eocdPos = i;
188
+ break;
189
+ }
190
+ }
191
+ if (eocdPos < 0) {
192
+ throw new Error("Could not find End of Central Directory (EOCD) in ZIP.");
193
+ }
194
+
195
+ const totalCD = zipBuffer.readUInt16LE(eocdPos + 10);
196
+ const cdSize = zipBuffer.readUInt32LE(eocdPos + 12);
197
+ const cdOffset = zipBuffer.readUInt32LE(eocdPos + 16);
198
+
199
+ if (cdOffset + cdSize > zipBuffer.length) {
200
+ throw new Error("Central directory offset/size out of range.");
201
+ }
202
+
203
+ let ptr = cdOffset;
204
+ const files = [];
205
+ for (let i = 0; i < totalCD; i++) {
206
+ const sig = zipBuffer.readUInt32LE(ptr);
207
+ if (sig !== 0x02014b50) {
208
+ throw new Error(`Central directory signature mismatch at ${ptr}`);
209
+ }
210
+ ptr += 4;
211
+
212
+ /* const verMade = */ zipBuffer.readUInt16LE(ptr);
213
+ ptr += 2;
214
+ const verNeed = zipBuffer.readUInt16LE(ptr);
215
+ ptr += 2;
216
+ const flags = zipBuffer.readUInt16LE(ptr);
217
+ ptr += 2;
218
+ const method = zipBuffer.readUInt16LE(ptr);
219
+ ptr += 2;
220
+ /* const modTime = */ zipBuffer.readUInt16LE(ptr);
221
+ ptr += 2;
222
+ /* const modDate = */ zipBuffer.readUInt16LE(ptr);
223
+ ptr += 2;
224
+ const crc32 = zipBuffer.readUInt32LE(ptr);
225
+ ptr += 4;
226
+ const compSize = zipBuffer.readUInt32LE(ptr);
227
+ ptr += 4;
228
+ const unCompSize = zipBuffer.readUInt32LE(ptr);
229
+ ptr += 4;
230
+ const fLen = zipBuffer.readUInt16LE(ptr);
231
+ ptr += 2;
232
+ const xLen = zipBuffer.readUInt16LE(ptr);
233
+ ptr += 2;
234
+ const cLen = zipBuffer.readUInt16LE(ptr);
235
+ ptr += 2;
236
+ /* const diskNo = */ zipBuffer.readUInt16LE(ptr);
237
+ ptr += 2;
238
+ /* const intAttr = */ zipBuffer.readUInt16LE(ptr);
239
+ ptr += 2;
240
+ /* const extAttr = */ zipBuffer.readUInt32LE(ptr);
241
+ ptr += 4;
242
+ const localHeaderOffset = zipBuffer.readUInt32LE(ptr);
243
+ ptr += 4;
244
+
245
+ const filename = zipBuffer.toString("utf8", ptr, ptr + fLen);
246
+ ptr += fLen + xLen + cLen; // skip the extra + comment
247
+
248
+ files.push({
249
+ filename,
250
+ method,
251
+ compSize,
252
+ unCompSize,
253
+ flags,
254
+ localHeaderOffset,
255
+ crc32,
256
+ verNeed,
257
+ });
258
+ }
259
+
260
+ fs.mkdirSync(outFolder, { recursive: true });
261
+
262
+ for (const file of files) {
263
+ const { filename, method, compSize, localHeaderOffset } = file;
264
+
265
+ if (filename.endsWith("/")) {
266
+ fs.mkdirSync(path.join(outFolder, filename), { recursive: true });
267
+ continue;
268
+ }
269
+
270
+ let lhPtr = localHeaderOffset;
271
+ const localSig = zipBuffer.readUInt32LE(lhPtr);
272
+ if (localSig !== 0x04034b50) {
273
+ throw new Error(`Local file header mismatch at ${lhPtr} for ${filename}`);
274
+ }
275
+ lhPtr += 4;
276
+
277
+ lhPtr += 2; // version needed
278
+ lhPtr += 2; // flags
279
+ lhPtr += 2; // method
280
+ lhPtr += 2; // mod time
281
+ lhPtr += 2; // mod date
282
+ lhPtr += 4; // crc32
283
+ lhPtr += 4; // comp size
284
+ lhPtr += 4; // uncomp size
285
+ const lhFNameLen = zipBuffer.readUInt16LE(lhPtr);
286
+ lhPtr += 2;
287
+ const lhXLen = zipBuffer.readUInt16LE(lhPtr);
288
+ lhPtr += 2;
289
+
290
+ lhPtr += lhFNameLen + lhXLen;
291
+ const fileData = zipBuffer.slice(lhPtr, lhPtr + compSize);
292
+
293
+ const outPath = path.join(outFolder, filename);
294
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
295
+
296
+ if (method === 0) {
297
+ fs.writeFileSync(outPath, fileData);
298
+ } else if (method === 8) {
299
+ const unzipped = zlib.inflateRawSync(fileData);
300
+ fs.writeFileSync(outPath, unzipped);
301
+ } else {
302
+ throw new Error(
303
+ `Unsupported compression method (${method}) for file ${filename}`,
304
+ );
305
+ }
306
+ }
307
+ }