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