diff --git a/wallets/keplr/src/KeplrWallet.ts b/wallets/keplr/src/KeplrWallet.ts new file mode 100644 index 000000000..b6e47aba1 --- /dev/null +++ b/wallets/keplr/src/KeplrWallet.ts @@ -0,0 +1,143 @@ +import type { Page } from '@playwright/test' +export type Keplr = 'keplr' + +export class KeplrWallet { + seedPhrase = '' + wallet: Keplr = 'keplr' + + constructor(readonly page: Page) { + this.page = page + } + + /** + * Imports wallet from secret phrase and password. + * + * @param secretWords. The secret words to import. + * @param password. The password to set. + * + * @returns true if the wallet was imported successfully. + */ + async importWallet(secretWords, password) { + await playwright.waitAndClickByText( + onboardingElements.createWalletButton, + await playwright.keplrWindow(), + ); + await playwright.waitAndClickByText( + onboardingElements.importRecoveryPhraseButton, + await playwright.keplrWindow(), + ); + await playwright.waitAndClickByText( + onboardingElements.useRecoveryPhraseButton, + await playwright.keplrWindow(), + ); + await playwright.waitAndClickByText( + onboardingElements.phraseCount24, + await playwright.keplrWindow(), + ); + + for (const [index, word] of secretWords.split(' ').entries()) { + await playwright.waitAndTypeByLocator( + onboardingElements.textAreaSelector, + word, + index, + ); + } + + await playwright.waitAndClick( + onboardingElements.submitPhraseButton, + await playwright.keplrWindow(), + ); + + await playwright.waitAndType( + onboardingElements.walletInput, + onboardingElements.walletName, + ); + await playwright.waitAndType(onboardingElements.passwordInput, password); + await playwright.waitAndType( + onboardingElements.confirmPasswordInput, + password, + ); + + await playwright.waitAndClick( + onboardingElements.submitWalletDataButton, + await playwright.keplrWindow(), + { number: 1 }, + ); + + await playwright.waitForByText( + onboardingElements.phraseSelectChain, + await playwright.keplrWindow(), + ); + + await playwright.waitAndClick( + onboardingElements.submitChainButton, + await playwright.keplrWindow(), + ); + + await playwright.waitForByText( + onboardingElements.phraseAccountCreated, + await playwright.keplrWindow(), + ); + + await playwright.waitAndClick( + onboardingElements.finishButton, + await playwright.keplrWindow(), + { dontWait: true }, + ); + + return true; + } + + /** + * Adds a new account. + * + * @returns true if the account was added successfully. + */ + async acceptAccess() { + const notificationPage = await playwright.switchToKeplrNotification(); + await playwright.waitAndClick( + notificationPageElements.approveButton, + notificationPage, + { waitForEvent: 'close' }, + ); + return true; + } + + /** + * Confirms a transaction. + * + * @returns true if the transaction was confirmed successfully. + */ + async confirmTransaction() { + const notificationPage = await playwright.switchToKeplrNotification(); + await playwright.waitAndClick( + notificationPageElements.approveButton, + notificationPage, + { waitForEvent: 'close' }, + ); + return true; + } + + /** + * Does initial setup for the wallet. + * + * @param playwrightInstance. The playwright instance to use. + * @param secretWordsOrPrivateKey. The secret words or private key to import. + * @param password. The password to set. + */ + async setupWallet( + playwrightInstance, + { secretWordsOrPrivateKey, password }, + ) { + if (playwrightInstance) { + await playwright.init(playwrightInstance); + } else { + await playwright.init(); + } + + await playwright.assignWindows(); + await playwright.assignActiveTabName('keplr'); + await module.exports.getExtensionDetails(); + await module.exports.importWallet(secretWordsOrPrivateKey, password); + } +} diff --git a/wallets/keplr/src/cypress/errors.ts b/wallets/keplr/src/cypress/errors.ts new file mode 100644 index 000000000..7d46bf925 --- /dev/null +++ b/wallets/keplr/src/cypress/errors.ts @@ -0,0 +1,3 @@ +export const NO_CONTEXT = 'No browser context found. Connect Playwright first - connectPlaywright()' +export const NO_PAGE = 'No page found. Use getPage()' +export const MISSING_INIT = 'Keplr not initialized. Use initKeplrWallet()' diff --git a/wallets/keplr/src/cypress/index.ts b/wallets/keplr/src/cypress/index.ts new file mode 100644 index 000000000..b06eeeeb6 --- /dev/null +++ b/wallets/keplr/src/cypress/index.ts @@ -0,0 +1 @@ +export { default as installSynpress } from './installSynpress' \ No newline at end of file diff --git a/wallets/keplr/src/cypress/initKeplrWallet.ts b/wallets/keplr/src/cypress/initKeplrWallet.ts new file mode 100644 index 000000000..0673ad282 --- /dev/null +++ b/wallets/keplr/src/cypress/initKeplrWallet.ts @@ -0,0 +1,77 @@ +import { readFileSync } from 'fs' +import { type BrowserContext, type Page, chromium } from '@playwright/test' + +import { KeplrWallet } from '../KeplrWallet' +import { SEED_PHRASE, mockEthereum, web3MockPath } from '../utils' +import { MISSING_INIT, NO_CONTEXT, NO_PAGE } from './errors' + +let context: BrowserContext | undefined +let cypressPage: Page | undefined +let keplrWallet: KeplrWallet | undefined + +let keplrLoaded = false + +const getCypressPage = async () => { + if (!context) { + console.error(NO_CONTEXT) + return + } + + cypressPage = context.pages()[0] + + return cypressPage +} + +export async function connectPlaywrightToChrome(port: number) { + const debuggerDetails = await fetch(`http://127.0.0.1:${port}/json/version`) + + const debuggerDetailsConfig = (await debuggerDetails.json()) as { + webSocketDebuggerUrl: string + } + + const browser = await chromium.connectOverCDP(debuggerDetailsConfig.webSocketDebuggerUrl) + + context = browser.contexts()[0] + + return browser.isConnected() +} + +export async function initKeplrWallet(port: number) { + await connectPlaywrightToChrome(port) + + if (!context) { + console.error(NO_CONTEXT) + return + } + + await getCypressPage() + + if (!cypressPage) { + console.error(NO_PAGE) + return + } + + await context.addInitScript({ + content: `${readFileSync(web3MockPath, 'utf-8')}\n(${mockEthereum.toString()})();` + }) + + // As we want to refresh the page after mocking the ethereum object + if (!keplrLoaded) { + await cypressPage.reload() + keplrLoaded = true + } + + keplrWallet = new KeplrWallet(cypressPage) + await keplrWallet.setupWallet(SEED_PHRASE, PASSWORD) +} + +export function getKeplrWallet() { + if (!context || !cypressPage || !keplrWallet) { + console.error(MISSING_INIT) + return + } + + if (keplrWallet) return keplrWallet + + return new KeplrWallet(cypressPage) +} diff --git a/wallets/keplr/src/cypress/installSynpress.ts b/wallets/keplr/src/cypress/installSynpress.ts new file mode 100644 index 000000000..e805a6017 --- /dev/null +++ b/wallets/keplr/src/cypress/installSynpress.ts @@ -0,0 +1,30 @@ +import { ensureRdpPort } from '@synthetixio/synpress-core' + +// import { initEthereumWalletMock } from './initEthereumWalletMock' +// import setupTasks from './setupTasks' + +let port: number + +export default function installSynpress(on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions) { + const browsers = config.browsers.filter((b) => b.name === 'chrome') + if (browsers.length === 0) { + throw new Error('No Chrome browser found in the configuration') + } + + on('before:browser:launch', async (_, launchOptions) => { + // Enable debug mode to establish playwright connection + const args = Array.isArray(launchOptions) ? launchOptions : launchOptions.args + port = ensureRdpPort(args) + }) + + on('before:spec', async () => { + await initEthereumWalletMock(port) + }) + + setupTasks(on) + + return { + ...config, + browsers + } +} diff --git a/wallets/keplr/src/cypress/setupTasks.ts b/wallets/keplr/src/cypress/setupTasks.ts new file mode 100644 index 000000000..6a801b90a --- /dev/null +++ b/wallets/keplr/src/cypress/setupTasks.ts @@ -0,0 +1,27 @@ +import { getEthereumWalletMock } from './initEthereumWalletMock' + +export default function setupTasks(on: Cypress.PluginEvents) { + on('task', { + importWallet: async function (seedPhrase: string) { + const ethereumWalletMock = getEthereumWalletMock() + if (ethereumWalletMock) { + await ethereumWalletMock.importWallet(seedPhrase) + } + return true + }, + addNewAccount: async function () { + const ethereumWalletMock = getEthereumWalletMock() + if (ethereumWalletMock) { + await ethereumWalletMock.addNewAccount() + } + return true + }, + getAllAccounts: async function () { + const ethereumWalletMock = getEthereumWalletMock() + if (ethereumWalletMock) { + return await ethereumWalletMock.getAllAccounts() + } + return [] + } + }) +} diff --git a/wallets/keplr/src/cypress/support/commands.ts b/wallets/keplr/src/cypress/support/commands.ts new file mode 100644 index 000000000..3a76ac258 --- /dev/null +++ b/wallets/keplr/src/cypress/support/commands.ts @@ -0,0 +1,23 @@ +/// +// *********************************************** +// This example commands.ts shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** + +import type { WalletMock } from '../../EthereumWalletMock' +import type { Network } from '../../network/Network' + +declare global { + namespace Cypress { + interface Chainable { + setupWallet(secretWordsOnPrivateKeys: string, password: string): Chainable + } + } +} + +Cypress.Commands.add('setupWallet', (seedPhrase, password) => cy.task('setupWallet', seedPhrase, password)) diff --git a/wallets/keplr/src/cypress/support/e2e.ts b/wallets/keplr/src/cypress/support/e2e.ts new file mode 100644 index 000000000..c90b6b6d3 --- /dev/null +++ b/wallets/keplr/src/cypress/support/e2e.ts @@ -0,0 +1,17 @@ +// *********************************************************** +// This example support/e2e.ts is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands'