w3wallets 1.0.0-beta.8 → 1.0.0-beta.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +28 -4
- package/dist/index.d.ts +28 -4
- package/dist/index.js +117 -52
- package/dist/index.mjs +117 -52
- package/dist/scripts/cache.js +22 -8
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -2,10 +2,16 @@ import * as playwright_test from 'playwright/test';
|
|
|
2
2
|
import { Page, test, BrowserContext } from '@playwright/test';
|
|
3
3
|
|
|
4
4
|
interface IWallet {
|
|
5
|
-
|
|
5
|
+
page: Page;
|
|
6
6
|
gotoOnboardPage(): Promise<void>;
|
|
7
7
|
approve(): Promise<void>;
|
|
8
8
|
deny(): Promise<void>;
|
|
9
|
+
/**
|
|
10
|
+
* Called by the cache builder after setupFn completes, before the browser
|
|
11
|
+
* context is closed. Wallet implementations can override this to force
|
|
12
|
+
* persistence of in-memory state (e.g., locking the vault).
|
|
13
|
+
*/
|
|
14
|
+
beforeCacheClose?(): Promise<void>;
|
|
9
15
|
}
|
|
10
16
|
/**
|
|
11
17
|
* Configuration object for a wallet.
|
|
@@ -78,7 +84,7 @@ declare function withWallets<const T extends readonly WalletConfig[]>(test: type
|
|
|
78
84
|
}, playwright_test.PlaywrightWorkerArgs & playwright_test.PlaywrightWorkerOptions>;
|
|
79
85
|
|
|
80
86
|
declare abstract class Wallet implements IWallet {
|
|
81
|
-
|
|
87
|
+
page: Page;
|
|
82
88
|
readonly extensionId: string;
|
|
83
89
|
constructor(page: Page, extensionId: string);
|
|
84
90
|
abstract gotoOnboardPage(): Promise<void>;
|
|
@@ -121,10 +127,22 @@ declare class Metamask extends Wallet {
|
|
|
121
127
|
* confirmation-cancel-button is available.
|
|
122
128
|
*/
|
|
123
129
|
private dismissQueuedNotifications;
|
|
130
|
+
/**
|
|
131
|
+
* Close extra MetaMask extension pages that the service worker opened
|
|
132
|
+
* during onboarding (e.g. home.html#/onboarding/completion).
|
|
133
|
+
* Keeps only this.page alive.
|
|
134
|
+
*/
|
|
135
|
+
private closeExtraExtensionPages;
|
|
136
|
+
/**
|
|
137
|
+
* Poll the URL until it leaves a confirmation route. Uses polling instead
|
|
138
|
+
* of waitForURL because MetaMask uses HashRouter and hash-only changes
|
|
139
|
+
* are not reliably caught by Playwright's navigation events.
|
|
140
|
+
*/
|
|
141
|
+
private waitForConfirmRouteExit;
|
|
124
142
|
/**
|
|
125
143
|
* Wait for a target button while handling the Transaction Shield popup.
|
|
126
|
-
*
|
|
127
|
-
*
|
|
144
|
+
* Navigates to sidepanel.html so MetaMask's ConfirmationHandler routes
|
|
145
|
+
* to the pending approval, then waits for the button to become visible.
|
|
128
146
|
*/
|
|
129
147
|
private waitAndClickButton;
|
|
130
148
|
approve(): Promise<void>;
|
|
@@ -133,6 +151,12 @@ declare class Metamask extends Wallet {
|
|
|
133
151
|
* Lock the MetaMask wallet
|
|
134
152
|
*/
|
|
135
153
|
lock(): Promise<void>;
|
|
154
|
+
/**
|
|
155
|
+
* Force-persist the vault by locking the wallet. Called by the cache builder
|
|
156
|
+
* before closing the browser context. MetaMask MV3 stores the vault lazily —
|
|
157
|
+
* locking forces the encrypted vault to be written to chrome.storage.local.
|
|
158
|
+
*/
|
|
159
|
+
beforeCacheClose(): Promise<void>;
|
|
136
160
|
/**
|
|
137
161
|
* Unlock MetaMask with password.
|
|
138
162
|
* After unlocking, stabilizes the wallet UI by handling post-unlock
|
package/dist/index.d.ts
CHANGED
|
@@ -2,10 +2,16 @@ import * as playwright_test from 'playwright/test';
|
|
|
2
2
|
import { Page, test, BrowserContext } from '@playwright/test';
|
|
3
3
|
|
|
4
4
|
interface IWallet {
|
|
5
|
-
|
|
5
|
+
page: Page;
|
|
6
6
|
gotoOnboardPage(): Promise<void>;
|
|
7
7
|
approve(): Promise<void>;
|
|
8
8
|
deny(): Promise<void>;
|
|
9
|
+
/**
|
|
10
|
+
* Called by the cache builder after setupFn completes, before the browser
|
|
11
|
+
* context is closed. Wallet implementations can override this to force
|
|
12
|
+
* persistence of in-memory state (e.g., locking the vault).
|
|
13
|
+
*/
|
|
14
|
+
beforeCacheClose?(): Promise<void>;
|
|
9
15
|
}
|
|
10
16
|
/**
|
|
11
17
|
* Configuration object for a wallet.
|
|
@@ -78,7 +84,7 @@ declare function withWallets<const T extends readonly WalletConfig[]>(test: type
|
|
|
78
84
|
}, playwright_test.PlaywrightWorkerArgs & playwright_test.PlaywrightWorkerOptions>;
|
|
79
85
|
|
|
80
86
|
declare abstract class Wallet implements IWallet {
|
|
81
|
-
|
|
87
|
+
page: Page;
|
|
82
88
|
readonly extensionId: string;
|
|
83
89
|
constructor(page: Page, extensionId: string);
|
|
84
90
|
abstract gotoOnboardPage(): Promise<void>;
|
|
@@ -121,10 +127,22 @@ declare class Metamask extends Wallet {
|
|
|
121
127
|
* confirmation-cancel-button is available.
|
|
122
128
|
*/
|
|
123
129
|
private dismissQueuedNotifications;
|
|
130
|
+
/**
|
|
131
|
+
* Close extra MetaMask extension pages that the service worker opened
|
|
132
|
+
* during onboarding (e.g. home.html#/onboarding/completion).
|
|
133
|
+
* Keeps only this.page alive.
|
|
134
|
+
*/
|
|
135
|
+
private closeExtraExtensionPages;
|
|
136
|
+
/**
|
|
137
|
+
* Poll the URL until it leaves a confirmation route. Uses polling instead
|
|
138
|
+
* of waitForURL because MetaMask uses HashRouter and hash-only changes
|
|
139
|
+
* are not reliably caught by Playwright's navigation events.
|
|
140
|
+
*/
|
|
141
|
+
private waitForConfirmRouteExit;
|
|
124
142
|
/**
|
|
125
143
|
* Wait for a target button while handling the Transaction Shield popup.
|
|
126
|
-
*
|
|
127
|
-
*
|
|
144
|
+
* Navigates to sidepanel.html so MetaMask's ConfirmationHandler routes
|
|
145
|
+
* to the pending approval, then waits for the button to become visible.
|
|
128
146
|
*/
|
|
129
147
|
private waitAndClickButton;
|
|
130
148
|
approve(): Promise<void>;
|
|
@@ -133,6 +151,12 @@ declare class Metamask extends Wallet {
|
|
|
133
151
|
* Lock the MetaMask wallet
|
|
134
152
|
*/
|
|
135
153
|
lock(): Promise<void>;
|
|
154
|
+
/**
|
|
155
|
+
* Force-persist the vault by locking the wallet. Called by the cache builder
|
|
156
|
+
* before closing the browser context. MetaMask MV3 stores the vault lazily —
|
|
157
|
+
* locking forces the encrypted vault to be written to chrome.storage.local.
|
|
158
|
+
*/
|
|
159
|
+
beforeCacheClose(): Promise<void>;
|
|
136
160
|
/**
|
|
137
161
|
* Unlock MetaMask with password.
|
|
138
162
|
* After unlocking, stabilizes the wallet UI by handling post-unlock
|
package/dist/index.js
CHANGED
|
@@ -104,8 +104,6 @@ var LAST_RESORT_CLICK_TIMEOUT = 1e4;
|
|
|
104
104
|
var LOCK_SCREEN_TIMEOUT = 3e4;
|
|
105
105
|
var MENU_BUTTON_TIMEOUT = 3e4;
|
|
106
106
|
var ONBOARD_VISIBLE_TIMEOUT = 3e4;
|
|
107
|
-
var ROUTE_RETRY_TIMEOUT = 5e3;
|
|
108
|
-
var MAX_ROUTE_ATTEMPTS = 5;
|
|
109
107
|
var MNEMONIC_KEY_DELAY = 5;
|
|
110
108
|
var MNEMONIC_WORD_DELAY = 100;
|
|
111
109
|
|
|
@@ -399,9 +397,28 @@ var Metamask = class extends Wallet {
|
|
|
399
397
|
});
|
|
400
398
|
await openWalletBtn.click();
|
|
401
399
|
await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
|
|
400
|
+
const readyIndicator = this.page.getByTestId("account-options-menu-button");
|
|
401
|
+
const lockInput = this.page.getByTestId("unlock-password");
|
|
402
|
+
const state = await Promise.race([
|
|
403
|
+
readyIndicator.waitFor({ state: "visible", timeout: POST_UNLOCK_TIMEOUT }).then(() => "ready"),
|
|
404
|
+
lockInput.waitFor({ state: "visible", timeout: POST_UNLOCK_TIMEOUT }).then(() => "locked")
|
|
405
|
+
]).catch(() => "timeout");
|
|
406
|
+
debug(`metamask.onboard: post-navigate state=${state}`);
|
|
407
|
+
if (state === "locked") {
|
|
408
|
+
debug("metamask.onboard: vault locked after onboarding, auto-unlocking");
|
|
409
|
+
await lockInput.fill(password ?? this.defaultPassword);
|
|
410
|
+
await this.page.getByTestId("unlock-submit").click();
|
|
411
|
+
await this.page.waitForSelector('[data-testid="unlock-password"]', {
|
|
412
|
+
state: "hidden",
|
|
413
|
+
timeout: LOCK_SCREEN_TIMEOUT
|
|
414
|
+
});
|
|
415
|
+
await this.stabilizePostUnlock();
|
|
416
|
+
}
|
|
417
|
+
await this.closeExtraExtensionPages();
|
|
402
418
|
await this.page.goto(
|
|
403
419
|
`chrome-extension://${this.extensionId}/sidepanel.html`
|
|
404
420
|
);
|
|
421
|
+
await this.page.waitForLoadState("networkidle");
|
|
405
422
|
debug("metamask.onboard: complete");
|
|
406
423
|
}
|
|
407
424
|
/**
|
|
@@ -517,10 +534,37 @@ var Metamask = class extends Wallet {
|
|
|
517
534
|
}
|
|
518
535
|
await (0, import_test3.expect)(readyIndicator).toBeVisible({ timeout: POST_UNLOCK_TIMEOUT });
|
|
519
536
|
}
|
|
537
|
+
/**
|
|
538
|
+
* Close extra MetaMask extension pages that the service worker opened
|
|
539
|
+
* during onboarding (e.g. home.html#/onboarding/completion).
|
|
540
|
+
* Keeps only this.page alive.
|
|
541
|
+
*/
|
|
542
|
+
async closeExtraExtensionPages() {
|
|
543
|
+
const extOrigin = `chrome-extension://${this.extensionId}`;
|
|
544
|
+
const context = this.page.context();
|
|
545
|
+
const extras = context.pages().filter((p) => p !== this.page && p.url().startsWith(extOrigin));
|
|
546
|
+
if (extras.length > 0) {
|
|
547
|
+
debug(
|
|
548
|
+
`metamask.closeExtraExtensionPages: closing ${extras.length} extra page(s)`
|
|
549
|
+
);
|
|
550
|
+
await Promise.all(extras.map((p) => p.close()));
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Poll the URL until it leaves a confirmation route. Uses polling instead
|
|
555
|
+
* of waitForURL because MetaMask uses HashRouter and hash-only changes
|
|
556
|
+
* are not reliably caught by Playwright's navigation events.
|
|
557
|
+
*/
|
|
558
|
+
async waitForConfirmRouteExit(routePattern) {
|
|
559
|
+
const deadline = Date.now() + POST_CLICK_TIMEOUT;
|
|
560
|
+
while (routePattern.test(this.page.url()) && Date.now() < deadline) {
|
|
561
|
+
await this.page.waitForTimeout(500);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
520
564
|
/**
|
|
521
565
|
* Wait for a target button while handling the Transaction Shield popup.
|
|
522
|
-
*
|
|
523
|
-
*
|
|
566
|
+
* Navigates to sidepanel.html so MetaMask's ConfirmationHandler routes
|
|
567
|
+
* to the pending approval, then waits for the button to become visible.
|
|
524
568
|
*/
|
|
525
569
|
async waitAndClickButton(btnLocator) {
|
|
526
570
|
debug(`metamask.waitAndClickButton: navigating to sidepanel`);
|
|
@@ -531,63 +575,36 @@ var Metamask = class extends Wallet {
|
|
|
531
575
|
btnLocator.first().waitFor({ state: "visible", timeout }).then(() => "button"),
|
|
532
576
|
popup.first().waitFor({ state: "visible", timeout }).then(() => "popup")
|
|
533
577
|
]).catch(() => "timeout");
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
let routeFound = false;
|
|
540
|
-
for (let attempt = 0; attempt < MAX_ROUTE_ATTEMPTS; attempt++) {
|
|
541
|
-
await this.page.goto(sidepanelUrl);
|
|
542
|
-
try {
|
|
543
|
-
await this.page.waitForURL(confirmRoutePattern, {
|
|
544
|
-
timeout: ROUTE_RETRY_TIMEOUT
|
|
545
|
-
});
|
|
546
|
-
routeFound = true;
|
|
547
|
-
break;
|
|
548
|
-
} catch {
|
|
549
|
-
debug(
|
|
550
|
-
`metamask.waitAndClickButton: route attempt ${attempt + 1}/${MAX_ROUTE_ATTEMPTS} failed. URL: ${this.page.url()}`
|
|
551
|
-
);
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
if (!routeFound) {
|
|
555
|
-
console.warn(
|
|
556
|
-
`[w3wallets] confirmation route not found after ${MAX_ROUTE_ATTEMPTS} attempts. URL: ${this.page.url()}`
|
|
578
|
+
await this.page.goto(sidepanelUrl);
|
|
579
|
+
let result = await waitForButtonOrPopup(BUTTON_OR_POPUP_TIMEOUT);
|
|
580
|
+
if (result === "timeout") {
|
|
581
|
+
debug(
|
|
582
|
+
`metamask.waitAndClickButton: first wait timed out, reloading. URL: ${this.page.url()}`
|
|
557
583
|
);
|
|
584
|
+
await this.page.goto(sidepanelUrl);
|
|
585
|
+
result = await waitForButtonOrPopup(BUTTON_OR_POPUP_TIMEOUT);
|
|
558
586
|
}
|
|
559
|
-
const result = await waitForButtonOrPopup(BUTTON_OR_POPUP_TIMEOUT);
|
|
560
587
|
debug(
|
|
561
588
|
`metamask.waitAndClickButton: result=${result}, URL=${this.page.url()}`
|
|
562
589
|
);
|
|
563
590
|
if (result === "button") {
|
|
564
591
|
await btnLocator.first().click();
|
|
565
|
-
await this.
|
|
566
|
-
timeout: POST_CLICK_TIMEOUT
|
|
567
|
-
}).catch(() => {
|
|
568
|
-
console.warn(
|
|
569
|
-
`[w3wallets] still on confirmation route after click. URL: ${this.page.url()}`
|
|
570
|
-
);
|
|
571
|
-
});
|
|
592
|
+
await this.waitForConfirmRouteExit(confirmRoutePattern);
|
|
572
593
|
return;
|
|
573
594
|
}
|
|
574
595
|
if (result === "popup") {
|
|
575
|
-
await
|
|
576
|
-
await
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
console.warn(
|
|
580
|
-
`[w3wallets] still on confirmation route after popup dismiss. URL: ${this.page.url()}`
|
|
581
|
-
);
|
|
582
|
-
});
|
|
596
|
+
await this.dismissPopups();
|
|
597
|
+
await btnLocator.first().waitFor({ state: "visible", timeout: BUTTON_OR_POPUP_TIMEOUT });
|
|
598
|
+
await btnLocator.first().click();
|
|
599
|
+
await this.waitForConfirmRouteExit(confirmRoutePattern);
|
|
583
600
|
return;
|
|
584
601
|
}
|
|
585
602
|
const isOnConfirmRoute = confirmRoutePattern.test(this.page.url());
|
|
586
603
|
debug(
|
|
587
|
-
`metamask.waitAndClickButton: timeout
|
|
604
|
+
`metamask.waitAndClickButton: timeout. URL: ${this.page.url()}, onConfirmRoute: ${isOnConfirmRoute}`
|
|
588
605
|
);
|
|
589
606
|
console.warn(
|
|
590
|
-
`[w3wallets] no button or popup found after
|
|
607
|
+
`[w3wallets] no button or popup found after waiting. URL: ${this.page.url()}`
|
|
591
608
|
);
|
|
592
609
|
await btnLocator.first().click({ timeout: LAST_RESORT_CLICK_TIMEOUT });
|
|
593
610
|
}
|
|
@@ -612,6 +629,46 @@ var Metamask = class extends Wallet {
|
|
|
612
629
|
await menuBtn.click({ force: true });
|
|
613
630
|
await this.page.locator("text=Log out").click();
|
|
614
631
|
}
|
|
632
|
+
/**
|
|
633
|
+
* Force-persist the vault by locking the wallet. Called by the cache builder
|
|
634
|
+
* before closing the browser context. MetaMask MV3 stores the vault lazily —
|
|
635
|
+
* locking forces the encrypted vault to be written to chrome.storage.local.
|
|
636
|
+
*/
|
|
637
|
+
async beforeCacheClose() {
|
|
638
|
+
debug("metamask.beforeCacheClose: locking wallet to force vault persistence");
|
|
639
|
+
await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
|
|
640
|
+
const menuBtn = this.page.getByTestId("account-options-menu-button");
|
|
641
|
+
const lockInput = this.page.getByTestId("unlock-password");
|
|
642
|
+
const state = await Promise.race([
|
|
643
|
+
menuBtn.waitFor({ state: "visible", timeout: MENU_BUTTON_TIMEOUT }).then(() => "ready"),
|
|
644
|
+
lockInput.waitFor({ state: "visible", timeout: MENU_BUTTON_TIMEOUT }).then(() => "locked")
|
|
645
|
+
]).catch(() => "timeout");
|
|
646
|
+
debug(`metamask.beforeCacheClose: state=${state}`);
|
|
647
|
+
if (state === "ready") {
|
|
648
|
+
await this.lock();
|
|
649
|
+
await lockInput.waitFor({ state: "visible", timeout: LOCK_SCREEN_TIMEOUT }).catch(() => {
|
|
650
|
+
});
|
|
651
|
+
} else if (state === "timeout") {
|
|
652
|
+
debug("metamask.beforeCacheClose: forcing state sync via new UI connection");
|
|
653
|
+
const syncPage = await this.page.context().newPage();
|
|
654
|
+
await syncPage.goto(`chrome-extension://${this.extensionId}/home.html`);
|
|
655
|
+
await syncPage.waitForLoadState("networkidle");
|
|
656
|
+
const syncMenuBtn = syncPage.getByTestId("account-options-menu-button");
|
|
657
|
+
const syncLockInput = syncPage.getByTestId("unlock-password");
|
|
658
|
+
const syncState = await Promise.race([
|
|
659
|
+
syncMenuBtn.waitFor({ state: "visible", timeout: MENU_BUTTON_TIMEOUT }).then(() => "ready"),
|
|
660
|
+
syncLockInput.waitFor({ state: "visible", timeout: MENU_BUTTON_TIMEOUT }).then(() => "locked")
|
|
661
|
+
]).catch(() => "timeout");
|
|
662
|
+
debug(`metamask.beforeCacheClose: syncPage state=${syncState}`);
|
|
663
|
+
if (syncState === "ready") {
|
|
664
|
+
await syncMenuBtn.click({ force: true });
|
|
665
|
+
await syncPage.locator("text=Log out").click();
|
|
666
|
+
await syncLockInput.waitFor({ state: "visible", timeout: LOCK_SCREEN_TIMEOUT }).catch(() => {
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
await syncPage.close();
|
|
670
|
+
}
|
|
671
|
+
}
|
|
615
672
|
/**
|
|
616
673
|
* Unlock MetaMask with password.
|
|
617
674
|
* After unlocking, stabilizes the wallet UI by handling post-unlock
|
|
@@ -622,13 +679,21 @@ var Metamask = class extends Wallet {
|
|
|
622
679
|
debug("metamask.unlock: starting");
|
|
623
680
|
const pwd = password ?? this.defaultPassword;
|
|
624
681
|
await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
await
|
|
628
|
-
|
|
629
|
-
state: "
|
|
630
|
-
|
|
631
|
-
});
|
|
682
|
+
const lockInput = this.page.getByTestId("unlock-password");
|
|
683
|
+
const readyIndicator = this.page.getByTestId("account-options-menu-button");
|
|
684
|
+
const state = await Promise.race([
|
|
685
|
+
lockInput.waitFor({ state: "visible", timeout: POST_UNLOCK_TIMEOUT }).then(() => "locked"),
|
|
686
|
+
readyIndicator.waitFor({ state: "visible", timeout: POST_UNLOCK_TIMEOUT }).then(() => "ready")
|
|
687
|
+
]).catch(() => "timeout");
|
|
688
|
+
debug(`metamask.unlock: state=${state}`);
|
|
689
|
+
if (state === "locked") {
|
|
690
|
+
await lockInput.fill(pwd);
|
|
691
|
+
await this.page.getByTestId("unlock-submit").click();
|
|
692
|
+
await this.page.waitForSelector('[data-testid="unlock-password"]', {
|
|
693
|
+
state: "hidden",
|
|
694
|
+
timeout: LOCK_SCREEN_TIMEOUT
|
|
695
|
+
});
|
|
696
|
+
}
|
|
632
697
|
await this.stabilizePostUnlock();
|
|
633
698
|
debug("metamask.unlock: complete");
|
|
634
699
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -68,8 +68,6 @@ var LAST_RESORT_CLICK_TIMEOUT = 1e4;
|
|
|
68
68
|
var LOCK_SCREEN_TIMEOUT = 3e4;
|
|
69
69
|
var MENU_BUTTON_TIMEOUT = 3e4;
|
|
70
70
|
var ONBOARD_VISIBLE_TIMEOUT = 3e4;
|
|
71
|
-
var ROUTE_RETRY_TIMEOUT = 5e3;
|
|
72
|
-
var MAX_ROUTE_ATTEMPTS = 5;
|
|
73
71
|
var MNEMONIC_KEY_DELAY = 5;
|
|
74
72
|
var MNEMONIC_WORD_DELAY = 100;
|
|
75
73
|
|
|
@@ -363,9 +361,28 @@ var Metamask = class extends Wallet {
|
|
|
363
361
|
});
|
|
364
362
|
await openWalletBtn.click();
|
|
365
363
|
await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
|
|
364
|
+
const readyIndicator = this.page.getByTestId("account-options-menu-button");
|
|
365
|
+
const lockInput = this.page.getByTestId("unlock-password");
|
|
366
|
+
const state = await Promise.race([
|
|
367
|
+
readyIndicator.waitFor({ state: "visible", timeout: POST_UNLOCK_TIMEOUT }).then(() => "ready"),
|
|
368
|
+
lockInput.waitFor({ state: "visible", timeout: POST_UNLOCK_TIMEOUT }).then(() => "locked")
|
|
369
|
+
]).catch(() => "timeout");
|
|
370
|
+
debug(`metamask.onboard: post-navigate state=${state}`);
|
|
371
|
+
if (state === "locked") {
|
|
372
|
+
debug("metamask.onboard: vault locked after onboarding, auto-unlocking");
|
|
373
|
+
await lockInput.fill(password ?? this.defaultPassword);
|
|
374
|
+
await this.page.getByTestId("unlock-submit").click();
|
|
375
|
+
await this.page.waitForSelector('[data-testid="unlock-password"]', {
|
|
376
|
+
state: "hidden",
|
|
377
|
+
timeout: LOCK_SCREEN_TIMEOUT
|
|
378
|
+
});
|
|
379
|
+
await this.stabilizePostUnlock();
|
|
380
|
+
}
|
|
381
|
+
await this.closeExtraExtensionPages();
|
|
366
382
|
await this.page.goto(
|
|
367
383
|
`chrome-extension://${this.extensionId}/sidepanel.html`
|
|
368
384
|
);
|
|
385
|
+
await this.page.waitForLoadState("networkidle");
|
|
369
386
|
debug("metamask.onboard: complete");
|
|
370
387
|
}
|
|
371
388
|
/**
|
|
@@ -481,10 +498,37 @@ var Metamask = class extends Wallet {
|
|
|
481
498
|
}
|
|
482
499
|
await expect(readyIndicator).toBeVisible({ timeout: POST_UNLOCK_TIMEOUT });
|
|
483
500
|
}
|
|
501
|
+
/**
|
|
502
|
+
* Close extra MetaMask extension pages that the service worker opened
|
|
503
|
+
* during onboarding (e.g. home.html#/onboarding/completion).
|
|
504
|
+
* Keeps only this.page alive.
|
|
505
|
+
*/
|
|
506
|
+
async closeExtraExtensionPages() {
|
|
507
|
+
const extOrigin = `chrome-extension://${this.extensionId}`;
|
|
508
|
+
const context = this.page.context();
|
|
509
|
+
const extras = context.pages().filter((p) => p !== this.page && p.url().startsWith(extOrigin));
|
|
510
|
+
if (extras.length > 0) {
|
|
511
|
+
debug(
|
|
512
|
+
`metamask.closeExtraExtensionPages: closing ${extras.length} extra page(s)`
|
|
513
|
+
);
|
|
514
|
+
await Promise.all(extras.map((p) => p.close()));
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Poll the URL until it leaves a confirmation route. Uses polling instead
|
|
519
|
+
* of waitForURL because MetaMask uses HashRouter and hash-only changes
|
|
520
|
+
* are not reliably caught by Playwright's navigation events.
|
|
521
|
+
*/
|
|
522
|
+
async waitForConfirmRouteExit(routePattern) {
|
|
523
|
+
const deadline = Date.now() + POST_CLICK_TIMEOUT;
|
|
524
|
+
while (routePattern.test(this.page.url()) && Date.now() < deadline) {
|
|
525
|
+
await this.page.waitForTimeout(500);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
484
528
|
/**
|
|
485
529
|
* Wait for a target button while handling the Transaction Shield popup.
|
|
486
|
-
*
|
|
487
|
-
*
|
|
530
|
+
* Navigates to sidepanel.html so MetaMask's ConfirmationHandler routes
|
|
531
|
+
* to the pending approval, then waits for the button to become visible.
|
|
488
532
|
*/
|
|
489
533
|
async waitAndClickButton(btnLocator) {
|
|
490
534
|
debug(`metamask.waitAndClickButton: navigating to sidepanel`);
|
|
@@ -495,63 +539,36 @@ var Metamask = class extends Wallet {
|
|
|
495
539
|
btnLocator.first().waitFor({ state: "visible", timeout }).then(() => "button"),
|
|
496
540
|
popup.first().waitFor({ state: "visible", timeout }).then(() => "popup")
|
|
497
541
|
]).catch(() => "timeout");
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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()}`
|
|
542
|
+
await this.page.goto(sidepanelUrl);
|
|
543
|
+
let result = await waitForButtonOrPopup(BUTTON_OR_POPUP_TIMEOUT);
|
|
544
|
+
if (result === "timeout") {
|
|
545
|
+
debug(
|
|
546
|
+
`metamask.waitAndClickButton: first wait timed out, reloading. URL: ${this.page.url()}`
|
|
521
547
|
);
|
|
548
|
+
await this.page.goto(sidepanelUrl);
|
|
549
|
+
result = await waitForButtonOrPopup(BUTTON_OR_POPUP_TIMEOUT);
|
|
522
550
|
}
|
|
523
|
-
const result = await waitForButtonOrPopup(BUTTON_OR_POPUP_TIMEOUT);
|
|
524
551
|
debug(
|
|
525
552
|
`metamask.waitAndClickButton: result=${result}, URL=${this.page.url()}`
|
|
526
553
|
);
|
|
527
554
|
if (result === "button") {
|
|
528
555
|
await btnLocator.first().click();
|
|
529
|
-
await this.
|
|
530
|
-
timeout: POST_CLICK_TIMEOUT
|
|
531
|
-
}).catch(() => {
|
|
532
|
-
console.warn(
|
|
533
|
-
`[w3wallets] still on confirmation route after click. URL: ${this.page.url()}`
|
|
534
|
-
);
|
|
535
|
-
});
|
|
556
|
+
await this.waitForConfirmRouteExit(confirmRoutePattern);
|
|
536
557
|
return;
|
|
537
558
|
}
|
|
538
559
|
if (result === "popup") {
|
|
539
|
-
await
|
|
540
|
-
await
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
console.warn(
|
|
544
|
-
`[w3wallets] still on confirmation route after popup dismiss. URL: ${this.page.url()}`
|
|
545
|
-
);
|
|
546
|
-
});
|
|
560
|
+
await this.dismissPopups();
|
|
561
|
+
await btnLocator.first().waitFor({ state: "visible", timeout: BUTTON_OR_POPUP_TIMEOUT });
|
|
562
|
+
await btnLocator.first().click();
|
|
563
|
+
await this.waitForConfirmRouteExit(confirmRoutePattern);
|
|
547
564
|
return;
|
|
548
565
|
}
|
|
549
566
|
const isOnConfirmRoute = confirmRoutePattern.test(this.page.url());
|
|
550
567
|
debug(
|
|
551
|
-
`metamask.waitAndClickButton: timeout
|
|
568
|
+
`metamask.waitAndClickButton: timeout. URL: ${this.page.url()}, onConfirmRoute: ${isOnConfirmRoute}`
|
|
552
569
|
);
|
|
553
570
|
console.warn(
|
|
554
|
-
`[w3wallets] no button or popup found after
|
|
571
|
+
`[w3wallets] no button or popup found after waiting. URL: ${this.page.url()}`
|
|
555
572
|
);
|
|
556
573
|
await btnLocator.first().click({ timeout: LAST_RESORT_CLICK_TIMEOUT });
|
|
557
574
|
}
|
|
@@ -576,6 +593,46 @@ var Metamask = class extends Wallet {
|
|
|
576
593
|
await menuBtn.click({ force: true });
|
|
577
594
|
await this.page.locator("text=Log out").click();
|
|
578
595
|
}
|
|
596
|
+
/**
|
|
597
|
+
* Force-persist the vault by locking the wallet. Called by the cache builder
|
|
598
|
+
* before closing the browser context. MetaMask MV3 stores the vault lazily —
|
|
599
|
+
* locking forces the encrypted vault to be written to chrome.storage.local.
|
|
600
|
+
*/
|
|
601
|
+
async beforeCacheClose() {
|
|
602
|
+
debug("metamask.beforeCacheClose: locking wallet to force vault persistence");
|
|
603
|
+
await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
|
|
604
|
+
const menuBtn = this.page.getByTestId("account-options-menu-button");
|
|
605
|
+
const lockInput = this.page.getByTestId("unlock-password");
|
|
606
|
+
const state = await Promise.race([
|
|
607
|
+
menuBtn.waitFor({ state: "visible", timeout: MENU_BUTTON_TIMEOUT }).then(() => "ready"),
|
|
608
|
+
lockInput.waitFor({ state: "visible", timeout: MENU_BUTTON_TIMEOUT }).then(() => "locked")
|
|
609
|
+
]).catch(() => "timeout");
|
|
610
|
+
debug(`metamask.beforeCacheClose: state=${state}`);
|
|
611
|
+
if (state === "ready") {
|
|
612
|
+
await this.lock();
|
|
613
|
+
await lockInput.waitFor({ state: "visible", timeout: LOCK_SCREEN_TIMEOUT }).catch(() => {
|
|
614
|
+
});
|
|
615
|
+
} else if (state === "timeout") {
|
|
616
|
+
debug("metamask.beforeCacheClose: forcing state sync via new UI connection");
|
|
617
|
+
const syncPage = await this.page.context().newPage();
|
|
618
|
+
await syncPage.goto(`chrome-extension://${this.extensionId}/home.html`);
|
|
619
|
+
await syncPage.waitForLoadState("networkidle");
|
|
620
|
+
const syncMenuBtn = syncPage.getByTestId("account-options-menu-button");
|
|
621
|
+
const syncLockInput = syncPage.getByTestId("unlock-password");
|
|
622
|
+
const syncState = await Promise.race([
|
|
623
|
+
syncMenuBtn.waitFor({ state: "visible", timeout: MENU_BUTTON_TIMEOUT }).then(() => "ready"),
|
|
624
|
+
syncLockInput.waitFor({ state: "visible", timeout: MENU_BUTTON_TIMEOUT }).then(() => "locked")
|
|
625
|
+
]).catch(() => "timeout");
|
|
626
|
+
debug(`metamask.beforeCacheClose: syncPage state=${syncState}`);
|
|
627
|
+
if (syncState === "ready") {
|
|
628
|
+
await syncMenuBtn.click({ force: true });
|
|
629
|
+
await syncPage.locator("text=Log out").click();
|
|
630
|
+
await syncLockInput.waitFor({ state: "visible", timeout: LOCK_SCREEN_TIMEOUT }).catch(() => {
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
await syncPage.close();
|
|
634
|
+
}
|
|
635
|
+
}
|
|
579
636
|
/**
|
|
580
637
|
* Unlock MetaMask with password.
|
|
581
638
|
* After unlocking, stabilizes the wallet UI by handling post-unlock
|
|
@@ -586,13 +643,21 @@ var Metamask = class extends Wallet {
|
|
|
586
643
|
debug("metamask.unlock: starting");
|
|
587
644
|
const pwd = password ?? this.defaultPassword;
|
|
588
645
|
await this.page.goto(`chrome-extension://${this.extensionId}/home.html`);
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
await
|
|
592
|
-
|
|
593
|
-
state: "
|
|
594
|
-
|
|
595
|
-
});
|
|
646
|
+
const lockInput = this.page.getByTestId("unlock-password");
|
|
647
|
+
const readyIndicator = this.page.getByTestId("account-options-menu-button");
|
|
648
|
+
const state = await Promise.race([
|
|
649
|
+
lockInput.waitFor({ state: "visible", timeout: POST_UNLOCK_TIMEOUT }).then(() => "locked"),
|
|
650
|
+
readyIndicator.waitFor({ state: "visible", timeout: POST_UNLOCK_TIMEOUT }).then(() => "ready")
|
|
651
|
+
]).catch(() => "timeout");
|
|
652
|
+
debug(`metamask.unlock: state=${state}`);
|
|
653
|
+
if (state === "locked") {
|
|
654
|
+
await lockInput.fill(pwd);
|
|
655
|
+
await this.page.getByTestId("unlock-submit").click();
|
|
656
|
+
await this.page.waitForSelector('[data-testid="unlock-password"]', {
|
|
657
|
+
state: "hidden",
|
|
658
|
+
timeout: LOCK_SCREEN_TIMEOUT
|
|
659
|
+
});
|
|
660
|
+
}
|
|
596
661
|
await this.stabilizePostUnlock();
|
|
597
662
|
debug("metamask.unlock: complete");
|
|
598
663
|
}
|
package/dist/scripts/cache.js
CHANGED
|
@@ -89,7 +89,8 @@ async function waitForStorageStable(helperPage, helperUrl) {
|
|
|
89
89
|
const POLL_INTERVAL = STORAGE_POLL_INTERVAL;
|
|
90
90
|
const STABLE_CHECKS_REQUIRED = STORAGE_STABLE_CHECKS;
|
|
91
91
|
const start = Date.now();
|
|
92
|
-
let
|
|
92
|
+
let lastHash = "";
|
|
93
|
+
let lastKeyCount = 0;
|
|
93
94
|
let stableCount = 0;
|
|
94
95
|
while (Date.now() - start < TIMEOUT) {
|
|
95
96
|
await sleep(POLL_INTERVAL);
|
|
@@ -100,16 +101,19 @@ async function waitForStorageStable(helperPage, helperUrl) {
|
|
|
100
101
|
{ timeout: HELPER_PAGE_TIMEOUT }
|
|
101
102
|
);
|
|
102
103
|
const title = await helperPage.title();
|
|
103
|
-
const
|
|
104
|
+
const parts = title.split(":");
|
|
105
|
+
const keyCount = parseInt(parts[1], 10);
|
|
106
|
+
const hash = parts[2] ?? "";
|
|
104
107
|
if (keyCount === 0) continue;
|
|
105
|
-
if (
|
|
108
|
+
if (hash === lastHash) {
|
|
106
109
|
stableCount++;
|
|
107
110
|
} else {
|
|
108
111
|
stableCount = 1;
|
|
112
|
+
lastHash = hash;
|
|
109
113
|
lastKeyCount = keyCount;
|
|
110
114
|
}
|
|
111
115
|
debug(
|
|
112
|
-
`Storage poll: ${keyCount} keys (stable ${stableCount}/${STABLE_CHECKS_REQUIRED})`
|
|
116
|
+
`Storage poll: ${keyCount} keys, hash=${hash.slice(0, 8)} (stable ${stableCount}/${STABLE_CHECKS_REQUIRED})`
|
|
113
117
|
);
|
|
114
118
|
if (stableCount >= STABLE_CHECKS_REQUIRED) {
|
|
115
119
|
console.log(` Storage stabilized at ${keyCount} keys`);
|
|
@@ -176,16 +180,26 @@ async function buildCacheForSetup(compiledFilePath, originalFilePath, options2 =
|
|
|
176
180
|
await config.setupFn(wallet, page);
|
|
177
181
|
await page.goto(`chrome-extension://${extensionId}/home.html`);
|
|
178
182
|
await page.waitForLoadState("networkidle");
|
|
179
|
-
|
|
180
|
-
|
|
183
|
+
if (wallet.beforeCacheClose) {
|
|
184
|
+
debug("buildCache: calling wallet.beforeCacheClose()");
|
|
185
|
+
await wallet.beforeCacheClose();
|
|
186
|
+
await sleep(2e3);
|
|
187
|
+
}
|
|
181
188
|
try {
|
|
182
189
|
const extDir = import_path2.default.join(process.cwd(), W3WALLETS_DIR, config.extensionDir);
|
|
183
190
|
const helperJs = import_path2.default.join(extDir, "_w3wallets_helper.js");
|
|
184
191
|
const helperHtml = import_path2.default.join(extDir, "_w3wallets_helper.html");
|
|
185
192
|
import_fs2.default.writeFileSync(
|
|
186
193
|
helperJs,
|
|
187
|
-
`chrome.storage.local.get(null, (data) => {
|
|
188
|
-
|
|
194
|
+
`chrome.storage.local.get(null, async (data) => {
|
|
195
|
+
const keys = Object.keys(data);
|
|
196
|
+
const json = JSON.stringify(data);
|
|
197
|
+
// Compute a simple hash of all values via SubtleCrypto
|
|
198
|
+
const buf = new TextEncoder().encode(json);
|
|
199
|
+
const hashBuf = await crypto.subtle.digest("SHA-256", buf);
|
|
200
|
+
const hashArr = Array.from(new Uint8Array(hashBuf));
|
|
201
|
+
const hash = hashArr.map(b => b.toString(16).padStart(2, "0")).join("");
|
|
202
|
+
document.title = "done:" + keys.length + ":" + hash;
|
|
189
203
|
});`
|
|
190
204
|
);
|
|
191
205
|
import_fs2.default.writeFileSync(
|
package/package.json
CHANGED