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 +44 -9
- package/dist/index.d.mts +33 -10
- package/dist/index.d.ts +33 -10
- package/dist/index.js +157 -28
- package/dist/index.mjs +160 -29
- package/package.json +17 -5
- package/src/scripts/download.js +298 -70
package/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
[](https://www.npmjs.com/package/w3wallets)
|
|
5
|
+

|
|
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
|
-
|
|
17
|
+
The `Backpack` and the `Polkadot{.js}` wallets are currently supported.
|
|
17
18
|
|
|
18
|
-
#### 1. Download
|
|
19
|
+
#### 1. Download wallets
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
```sh
|
|
22
|
+
npx w3wallets backpack polkadotJS
|
|
23
|
+
```
|
|
23
24
|
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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=${
|
|
122
|
-
`--load-extension=${
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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=${
|
|
86
|
-
`--load-extension=${
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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.
|
|
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
|
-
"
|
|
37
|
-
"
|
|
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.
|
|
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.
|
|
62
|
+
"@playwright/test": "^1.50.1"
|
|
51
63
|
}
|
|
52
64
|
}
|
package/src/scripts/download.js
CHANGED
|
@@ -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
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
//
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
+
}
|