UNPKG

w3wallets

Version:
390 lines (380 loc) 15.1 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { Metamask: () => Metamask, PolkadotJS: () => PolkadotJS, config: () => config, createWallet: () => createWallet, metamask: () => metamask, polkadotJS: () => polkadotJS, withWallets: () => withWallets }); module.exports = __toCommonJS(index_exports); // src/withWallets.ts var import_path = __toESM(require("path")); var import_fs = __toESM(require("fs")); var import_crypto = __toESM(require("crypto")); var import_test = require("@playwright/test"); var W3WALLETS_DIR = ".w3wallets"; function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function withWallets(test, ...wallets) { const extensionInfo = wallets.map((w) => { const extPath = import_path.default.join(process.cwd(), W3WALLETS_DIR, w.extensionDir); ensureWalletExtensionExists(extPath, w.name); const extensionId = w.extensionId ?? getExtensionId(extPath); return { path: extPath, id: extensionId, name: w.name }; }); const extensionPaths = extensionInfo.map((e) => e.path); const fixtures = { context: async ({}, use, testInfo) => { const userDataDir = import_path.default.join( process.cwd(), W3WALLETS_DIR, ".context", testInfo.testId ); cleanUserDataDir(userDataDir); const context = await import_test.chromium.launchPersistentContext(userDataDir, { headless: testInfo.project.use.headless ?? true, channel: "chromium", args: [ `--disable-extensions-except=${extensionPaths.join(",")}`, `--load-extension=${extensionPaths.join(",")}` ] }); while (context.serviceWorkers().length < extensionPaths.length) { await sleep(1e3); } await use(context); await context.close(); } }; for (let i = 0; i < wallets.length; i++) { const wallet = wallets[i]; const info = extensionInfo[i]; fixtures[wallet.name] = async ({ context }, use) => { const instance = await initializeExtension( context, wallet.WalletClass, info.id, wallet.name ); await use(instance); }; } return test.extend(fixtures); } function cleanUserDataDir(userDataDir) { if (import_fs.default.existsSync(userDataDir)) { import_fs.default.rmSync(userDataDir, { recursive: true }); } } function ensureWalletExtensionExists(walletPath, walletName) { if (!import_fs.default.existsSync(import_path.default.join(walletPath, "manifest.json"))) { const cliAlias = walletName.toLowerCase(); throw new Error( `Cannot find ${walletName}. Please download it via 'npx w3wallets ${cliAlias}'.` ); } } function getExtensionId(extensionPath) { const absolutePath = import_path.default.resolve(extensionPath); const manifestPath = import_path.default.join(absolutePath, "manifest.json"); const manifest = JSON.parse(import_fs.default.readFileSync(manifestPath, "utf-8")); let dataToHash; if (manifest.key) { dataToHash = Buffer.from(manifest.key, "base64"); } else { dataToHash = Buffer.from(absolutePath); } const hash = import_crypto.default.createHash("sha256").update(dataToHash).digest(); const ALPHABET = "abcdefghijklmnop"; let extensionId = ""; for (let i = 0; i < 16; i++) { const byte = hash[i]; extensionId += ALPHABET[byte >> 4 & 15]; extensionId += ALPHABET[byte & 15]; } return extensionId; } async function initializeExtension(context, ExtensionClass, expectedExtensionId, walletName) { const expectedUrl = `chrome-extension://${expectedExtensionId}/`; const worker = context.serviceWorkers().find((w) => w.url().startsWith(expectedUrl)); if (!worker) { const availableIds = context.serviceWorkers().map((w) => w.url().split("/")[2]).filter(Boolean); throw new Error( `Service worker for ${walletName} (ID: ${expectedExtensionId}) not found. Available extension IDs: [${availableIds.join(", ")}]` ); } const page = await context.newPage(); const extension = new ExtensionClass(page, expectedExtensionId); return extension; } // src/core/types.ts function createWallet(config2) { return config2; } // src/wallets/metamask/metamask.ts var import_test2 = require("@playwright/test"); // src/config.ts var config = { /** * Timeout for actions like click, fill, waitFor, goto. * Set via W3WALLETS_ACTION_TIMEOUT env variable. * @default 30000 (30 seconds) */ get actionTimeout() { const value = process.env.W3WALLETS_ACTION_TIMEOUT; return value ? parseInt(value, 10) : void 0; } }; // src/core/wallet.ts var Wallet = class { constructor(page, extensionId) { this.page = page; this.extensionId = extensionId; if (config.actionTimeout) { page.setDefaultTimeout(config.actionTimeout); } } }; // src/wallets/metamask/metamask.ts var Metamask = class extends Wallet { defaultPassword = "TestPassword123!"; async gotoOnboardPage() { await this.page.goto(`chrome-extension://${this.extensionId}/home.html`); await (0, import_test2.expect)( this.page.getByRole("button", { name: "I have an existing wallet" }) ).toBeVisible(); } /** * Onboard MetaMask with a mnemonic phrase * @param mnemonic - 12 or 24 word recovery phrase * @param password - Optional password (defaults to TestPassword123!) */ async onboard(mnemonic, password) { const pwd = password ?? this.defaultPassword; await this.gotoOnboardPage(); await this.page.getByRole("button", { name: "I have an existing wallet" }).click(); await this.page.getByRole("button", { name: "Import using Secret Recovery Phrase" }).click(); const textbox = this.page.getByRole("textbox"); await textbox.click(); for (const word of mnemonic.split(" ")) { await this.page.keyboard.type(word); await this.page.keyboard.type(" "); await this.page.waitForTimeout(30); } const continueBtn = this.page.getByTestId("import-srp-confirm"); await continueBtn.click(); const passwordInputs = this.page.locator('input[type="password"]'); await passwordInputs.nth(0).fill(pwd); await passwordInputs.nth(1).fill(pwd); await this.page.getByRole("checkbox").click(); await this.page.getByRole("button", { name: "Create password" }).click(); const metametricsBtn = this.page.getByTestId("metametrics-i-agree"); await metametricsBtn.click(); const openWalletBtn = this.page.getByRole("button", { name: /open wallet/i }); await openWalletBtn.click(); await this.page.goto( `chrome-extension://${this.extensionId}/sidepanel.html` ); } async approve() { await this.page.getByTestId("confirm-btn").or(this.page.getByTestId("confirm-footer-button")).or(this.page.getByTestId("page-container-footer-next")).or(this.page.getByRole("button", { name: /confirm/i })).click(); } async deny() { const cancelBtn = this.page.getByTestId("cancel-btn").or(this.page.getByTestId("confirm-footer-cancel-button")).or(this.page.getByTestId("page-container-footer-cancel")).or(this.page.getByRole("button", { name: /cancel|reject/i })); await cancelBtn.first().click(); } /** * Lock the MetaMask wallet */ async lock() { await this.page.getByTestId("account-options-menu-button").click(); await this.page.locator("text=Lock MetaMask").click(); } /** * Unlock MetaMask with password */ async unlock(password) { const pwd = password ?? this.defaultPassword; const passwordInput = this.page.getByTestId("unlock-password"); await passwordInput.fill(pwd); await this.page.getByTestId("unlock-submit").click(); } /** * Switch to an existing network in MetaMask * @param networkName - Name of the network to switch to (e.g., "Ethereum Mainnet", "Sepolia") */ async switchNetwork(networkName, networkType = "Popular") { await this.page.getByTestId("sort-by-networks").click(); if (networkType === "Custom") { await this.page.getByRole("tab", { name: "Custom" }).click(); } await this.page.getByText(networkName).click(); await (0, import_test2.expect)(this.page.getByTestId("sort-by-networks")).toHaveText( networkName ); } async switchAccount(accountName) { await this.page.getByTestId("account-menu-icon").click(); await this.page.getByText(accountName, { exact: true }).click(); } /** * Add a custom network to MetaMask */ async addNetwork(network) { await this.page.goto( `chrome-extension://${this.extensionId}/home.html#settings/networks/add-network` ); await this.page.getByTestId("network-form-network-name").fill(network.name); await this.page.getByTestId("network-form-rpc-url").fill(network.rpc); await this.page.getByTestId("network-form-chain-id").fill(network.chainId.toString()); await this.page.getByTestId("network-form-ticker-input").fill(network.currencySymbol); await this.page.getByRole("button", { name: /save/i }).click(); } async addCustomNetwork(settings) { await this.page.getByTestId("account-options-menu-button").click(); await this.page.getByTestId("global-menu-networks").click(); await this.page.getByRole("button", { name: "Add a custom network" }).click(); await this.page.getByTestId("network-form-network-name").fill(settings.name); await this.page.getByTestId("network-form-chain-id").fill(settings.chainId.toString()); await this.page.getByTestId("network-form-ticker-input").fill(settings.currencySymbol); await this.page.getByTestId("test-add-rpc-drop-down").click(); await this.page.getByRole("button", { name: "Add RPC URL" }).click(); await this.page.getByTestId("rpc-url-input-test").fill(settings.rpc); await this.page.getByRole("button", { name: "Add URL" }).click(); await this.page.getByRole("button", { name: "Save" }).click(); } async enableTestNetworks() { await this.page.getByTestId("account-options-menu-button").click(); await this.page.getByTestId("global-menu-networks").click(); await this.page.locator("text=Show test networks >> xpath=following-sibling::label").click(); await this.page.keyboard.press("Escape"); } async importAccount(privateKey) { await this.page.getByTestId("account-menu-icon").click(); await this.page.getByTestId("account-list-add-wallet-button").click(); await this.page.getByTestId("add-wallet-modal-import-account").click(); await this.page.locator("#private-key-box").fill(privateKey); await this.page.getByTestId("import-account-confirm-button").click(); await this.page.getByRole("button", { name: "Back" }).click(); } async accountNameIs(accountName) { await (0, import_test2.expect)(this.page.getByTestId("account-menu-icon")).toContainText( accountName ); } }; // src/wallets/polkadot-js/polkadot-js.ts var import_test3 = require("@playwright/test"); var PolkadotJS = class extends Wallet { defaultPassword = "11111111"; async gotoOnboardPage() { await this.page.goto(`chrome-extension://${this.extensionId}/index.html`); await (0, import_test3.expect)( this.page.getByText("Before we start, just a couple of notes") ).toBeVisible(); } async onboard(seed, password, name) { await this.gotoOnboardPage(); await this.page.getByRole("button", { name: "Understood, let me continue" }).click(); await this.page.getByRole("button", { name: "I Understand" }).click(); await this.page.locator(".popupToggle").first().click(); await this.page.getByText("Import account from pre-existing seed").click(); await this.page.locator(".seedInput").getByRole("textbox").fill(seed); await this.page.getByRole("button", { name: "Next" }).click(); await this._getLabeledInput("A descriptive name for your account").fill( name ?? "Test" ); await this._getLabeledInput("A new password for this account").fill( password ?? this.defaultPassword ); await this._getLabeledInput("Repeat password for verification").fill( password ?? this.defaultPassword ); await this.page.getByRole("button", { name: "Add the account with the supplied seed" }).click(); } async selectAllAccounts() { await this.page.getByText("Select all").click(); } async selectAccount(accountId) { const cb = this.page.locator(".accountWichCheckbox").filter({ hasText: accountId }).locator(".accountTree-checkbox").locator("span"); await cb.check().catch(() => cb.check()); } async enterPassword(password) { await this._getLabeledInput("Password for this account").fill( password ?? this.defaultPassword ); } async approve() { const connect = this.page.getByRole("button", { name: "Connect" }); const signTransaction = this.page.getByRole("button", { name: "Sign the transaction" }); await connect.or(signTransaction).click(); } async deny() { const reject = this.page.getByRole("button", { name: "Reject" }); const cancel = this.page.getByRole("link", { name: "Cancel" }); await reject.or(cancel).click(); } _getLabeledInput(label) { return this.page.locator( `//label[text()="${label}"]/following-sibling::input` ); } }; // src/wallets/index.ts var metamask = createWallet({ name: "metamask", extensionDir: "metamask", WalletClass: Metamask }); var polkadotJS = createWallet({ name: "polkadotJS", extensionDir: "polkadotjs", WalletClass: PolkadotJS }); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Metamask, PolkadotJS, config, createWallet, metamask, polkadotJS, withWallets });