diff --git a/apps/deploy-web/package.json b/apps/deploy-web/package.json index 54cb548efb..0b98b3f95c 100644 --- a/apps/deploy-web/package.json +++ b/apps/deploy-web/package.json @@ -126,7 +126,6 @@ "@akashnetwork/releaser": "*", "@chain-registry/types": "^0.50.12", "@cosmjs/amino": "~0.38.0", - "@cosmjs/crypto": "~0.38.0", "@faker-js/faker": "^9.4.0", "@keplr-wallet/types": "^0.12.111", "@next/bundle-analyzer": "^14.0.1", diff --git a/apps/deploy-web/tests/ui/actions/selectChainNetwork.ts b/apps/deploy-web/tests/ui/actions/selectChainNetwork.ts deleted file mode 100644 index 7789e17ee6..0000000000 --- a/apps/deploy-web/tests/ui/actions/selectChainNetwork.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { NetworkId } from "@akashnetwork/chain-sdk"; -import type { Page } from "@playwright/test"; - -import { isWalletConnected } from "../uiState/isWalletConnected"; - -export async function selectChainNetwork(page: Page, networkId: NetworkId = "sandbox") { - await page.getByRole("link", { name: "App Settings" }).click(); - const selectNetworkButton = page.getByLabel("Select Network"); - const selectedNetwork = await selectNetworkButton.locator("xpath=..").textContent(); - if (selectedNetwork?.toLowerCase().includes(networkId)) return; - - await selectNetworkButton.click({ timeout: 20_000 }); - - const networkRadioLocator = page.getByLabel(new RegExp(networkId, "i")); - await networkRadioLocator.click(); - const popupPromise = page - .context() - .waitForEvent("page", { timeout: 5_000 }) - .catch(() => null); - await page.getByRole("button", { name: "Save" }).click(); - - const popupPage = await Promise.race([ - popupPromise, - isWalletConnected(page).then( - () => null, - () => null - ) - ]); - await popupPage?.getByRole("button", { name: /Approve/i }).click(); -} diff --git a/apps/deploy-web/tests/ui/authorize-spending.spec.ts b/apps/deploy-web/tests/ui/authorize-spending.spec.ts deleted file mode 100644 index 27a1b697f2..0000000000 --- a/apps/deploy-web/tests/ui/authorize-spending.spec.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; -import type { BrowserContext, Page } from "@playwright/test"; - -import { createPage, expect, test } from "./fixture/context-with-extension"; -import { topUpWallet } from "./fixture/wallet-setup"; -import type { AuthorizationType } from "./pages/AuthorizationsPage"; -import { AuthorizationsPage, shortenAddress } from "./pages/AuthorizationsPage"; -import { WebWallet } from "./pages/WebWallet"; - -test.describe("Deployment Authorizations", () => { - includeAuthorizationTests({ authType: "deployment", denom: "ACT" }); -}); - -test.describe("Tx Fee Authorizations", () => { - includeAuthorizationTests({ authType: "tx_fee", denom: "AKT" }); -}); - -function includeAuthorizationTests(input: { authType: AuthorizationType; denom: "ACT" | "AKT" }) { - test.beforeAll(async ({ browser }) => { - const context = await browser.newContext(); - try { - const page = await createPage(context); - const authorizationsPage = new AuthorizationsPage(context, page); - await authorizationsPage.goto(); - await authorizationsPage.revokeAll(input.authType); - } finally { - await context.close(); - } - }); - - test.afterAll(async ({ browser }) => { - const context = await browser.newContext(); - try { - const page = await createPage(context); - const authorizationsPage = new AuthorizationsPage(context, page); - await authorizationsPage.goto(); - await authorizationsPage.revokeAll(input.authType); - } finally { - await context.close(); - } - }); - - test("can authorize spending", async ({ page, context }) => { - const { authorizationsPage, anotherWalletAddress: address } = await setup({ page, context, ...input }); - - const shortenedAddress = shortenAddress(address); - const grantList = authorizationsPage.getListLocator(input.authType); - - await expect(grantList.locator("td", { hasText: shortenedAddress })).toBeVisible({ timeout: 10_000 }); - }); - - test("can edit spending", async ({ page, context }) => { - const { authorizationsPage, anotherWalletAddress: address, extension } = await setup({ page, context, ...input }); - await Promise.all([extension.acceptTransaction("high"), authorizationsPage.editSpending(input.authType, address)]); - await extension.waitForTransaction("success"); - - const grantList = authorizationsPage.getListLocator(input.authType); - await expect(grantList.locator("tr", { hasText: new RegExp(`10(\\.0+?)\\s*${input.denom}`) })).toBeVisible({ timeout: 10_000 }); - }); - - test("can revoke spending", async ({ page, context }) => { - const { authorizationsPage, anotherWalletAddress: address, extension } = await setup({ page, context, ...input }); - - await Promise.all([extension.acceptTransaction("high"), authorizationsPage.revokeSpending(input.authType, address)]); - await extension.waitForTransaction("success"); - - const shortenedAddress = shortenAddress(address); - const grantList = authorizationsPage.getListLocator(input.authType); - await expect(grantList.locator("tr", { hasText: shortenedAddress })).not.toBeVisible({ timeout: 10_000 }); - }); -} - -async function setup({ page, context, authType }: { page: Page; context: BrowserContext; authType: AuthorizationType }) { - const extension = new WebWallet(context, page); - const anotherWallet = await DirectSecp256k1HdWallet.generate(12, { prefix: "akash" }); - const anotherWalletAccounts = await anotherWallet.getAccounts(); - const anotherWalletAddress = anotherWalletAccounts[0].address; - - await topUpWallet(anotherWalletAddress); - - const authorizationsPage = new AuthorizationsPage(context, page); - await authorizationsPage.goto(); - - await Promise.all([extension.acceptTransaction("high"), authorizationsPage.authorizeSpending(authType, anotherWalletAccounts[0].address)]); - const notification = await extension.getTransaction("success"); - await notification.close(); - - return { - authorizationsPage, - anotherWalletAddress, - extension - }; -} diff --git a/apps/deploy-web/tests/ui/change-wallets.spec.ts b/apps/deploy-web/tests/ui/change-wallets.spec.ts deleted file mode 100644 index 5826b6c882..0000000000 --- a/apps/deploy-web/tests/ui/change-wallets.spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { expect } from "@playwright/test"; - -import { test } from "./fixture/context-with-extension"; -import { WebWallet } from "./pages/WebWallet"; - -test.describe("Custodial wallet", () => { - test("switching to another wallet in the extension switches the wallet in Console", async ({ page, context }) => { - const extension = new WebWallet(context, page); - await extension.goto(); - const newWalletAddress = await extension.switchToNewWallet(); - - const container = page.getByLabel("Connected wallet name and balance"); - await container.waitFor({ state: "visible", timeout: 20_000 }); - await container.click({ timeout: 20_000 }); - await page.getByLabel("wallet address").click(); - const clipboardContent = await page.evaluate(() => navigator.clipboard.readText()); - - expect(clipboardContent).toEqual(newWalletAddress); - }); - - test("wallet stays disconnected after disconnecting and reloading", async ({ page, context }) => { - const extension = new WebWallet(context, page); - await extension.goto(); - await extension.disconnectWallet(); - - await expect(page.getByTestId("connect-wallet-btn")).toBeVisible({ timeout: 20_000 }); - }); -}); diff --git a/apps/deploy-web/tests/ui/custom-container-form.spec.ts b/apps/deploy-web/tests/ui/custom-container-form.spec.ts index 7daf05b838..764336476a 100644 --- a/apps/deploy-web/tests/ui/custom-container-form.spec.ts +++ b/apps/deploy-web/tests/ui/custom-container-form.spec.ts @@ -7,7 +7,7 @@ import { Sidebar } from "./pages/Sidebar"; test("custom container form shows connect wallet prompt", async ({ page, context }) => { const homePage = new HomePage(page); const sidebar = new Sidebar(page); - const deployPage = new DeployPage(context, page, { walletType: "extension" }); + const deployPage = new DeployPage(context, page); await homePage.goto(); await sidebar.openDeploy(); diff --git a/apps/deploy-web/tests/ui/deploy-self-custody-hello-world.spec.ts b/apps/deploy-web/tests/ui/deploy-self-custody-hello-world.spec.ts deleted file mode 100644 index ef1ee90e54..0000000000 --- a/apps/deploy-web/tests/ui/deploy-self-custody-hello-world.spec.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { test } from "./fixture/context-with-extension"; -import { DeployPage } from "./pages/DeployPage"; -import { HomePage } from "./pages/HomePage"; -import { Sidebar } from "./pages/Sidebar"; - -test("deploy hello world via self custody wallet", async ({ context, page }) => { - test.setTimeout(5 * 60 * 1000); - - const homePage = new HomePage(page); - const sidebar = new Sidebar(page); - const deployPage = new DeployPage(context, page, { walletType: "extension", feeType: "medium" }); - - await test.step("navigate to deploy page and select template", async () => { - await homePage.goto(); - await sidebar.openDeploy(); - await deployPage.selectTemplate("Hello World"); - }); - - await test.step("create deployment", async () => { - await deployPage.createDeployment(); - }); - - await test.step("create lease", async () => { - await deployPage.createLease(); - }); - - await test.step("validate lease and close", async () => { - await deployPage.validateLeaseAndClose(); - }); -}); diff --git a/apps/deploy-web/tests/ui/fixture/context-with-extension.ts b/apps/deploy-web/tests/ui/fixture/context-with-extension.ts deleted file mode 100644 index ac8dae3590..0000000000 --- a/apps/deploy-web/tests/ui/fixture/context-with-extension.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; -import type { BrowserContext, Page } from "@playwright/test"; - -import { selectChainNetwork } from "../actions/selectChainNetwork"; -import { injectWebWallet } from "./web-wallet/injectWebWallet"; -import { injectUIConfig, test as baseTest } from "./base-test"; -import { testEnvConfig } from "./test-env.config"; -import { connectWalletViaLeap, topUpWallet } from "./wallet-setup"; - -// @see https://playwright.dev/docs/chrome-extensions -export const test = baseTest.extend({ - page: [ - async ({ context }, use) => { - const page = await createPage(context); - - try { - await use(page); - } finally { - await page.close(); - } - }, - { scope: "test", timeout: 5 * 60 * 1000 } - ] -}); - -export const expect = test.expect; - -export interface ExtensionContext { - context: BrowserContext; - page: Page; -} - -export async function createPage(context: BrowserContext): Promise { - const w = await DirectSecp256k1HdWallet.fromMnemonic(testEnvConfig.TEST_WALLET_MNEMONIC, { prefix: "akash" }); - const accounts = await w.getAccounts(); - await topUpWallet(accounts[0].address); - - const page = await context.newPage(); - await injectUIConfig(page); - await injectWebWallet(page, testEnvConfig.TEST_WALLET_MNEMONIC); - - if (testEnvConfig.NETWORK_ID !== "mainnet") { - await page.goto(testEnvConfig.BASE_URL); - await connectWalletViaLeap(context, page); - await selectChainNetwork(page, testEnvConfig.NETWORK_ID); - } - - await page.goto(testEnvConfig.BASE_URL); - await connectWalletViaLeap(context, page); - return page; -} diff --git a/apps/deploy-web/tests/ui/fixture/test-env.config.ts b/apps/deploy-web/tests/ui/fixture/test-env.config.ts index 02f09cfc03..3c937d18dd 100644 --- a/apps/deploy-web/tests/ui/fixture/test-env.config.ts +++ b/apps/deploy-web/tests/ui/fixture/test-env.config.ts @@ -7,7 +7,6 @@ export const testEnvSchema = z.object({ .string() .default("http://localhost:3000") .transform(url => url.replace(/\/+$/, "")), - TEST_WALLET_MNEMONIC: z.string(), NETWORK_ID: z.enum(["mainnet", "sandbox", "testnet"]).default("sandbox"), USER_DATA_DIR: z.string().default(path.join(tmpdir(), "akash-console-web-ui-tests", crypto.randomUUID())), E2E_TESTING_CLIENT_TOKEN: z.string({ @@ -24,7 +23,7 @@ export const testEnvSchema = z.object({ export const testEnvConfig = testEnvSchema.parse({ BASE_URL: process.env.BASE_URL, - TEST_WALLET_MNEMONIC: process.env.TEST_WALLET_MNEMONIC, + NETWORK_ID: process.env.NETWORK_ID, USER_DATA_DIR: process.env.USER_DATA_DIR, E2E_TESTING_CLIENT_TOKEN: process.env.E2E_TESTING_CLIENT_TOKEN, AUTH0_M2M_DOMAIN: process.env.AUTH0_M2M_DOMAIN, diff --git a/apps/deploy-web/tests/ui/fixture/testing-helpers.ts b/apps/deploy-web/tests/ui/fixture/testing-helpers.ts deleted file mode 100644 index b69b048a0b..0000000000 --- a/apps/deploy-web/tests/ui/fixture/testing-helpers.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { type Page } from "@playwright/test"; - -export const clickWalletSelectorDropdown = async (page: Page) => { - return await page.getByLabel("wallet dropdown").click(); -}; - -export const clickConnectWalletButton = async (page: Page) => { - await page.getByRole("button", { name: /connect button/i }).click({ timeout: 20_000 }); -}; - -export const clickCopyAddressButton = async (page: Page) => { - await page.getByRole("button", { name: /akash\.\.\.[a-z0-9]{5}/ }).click(); - - const clipboardContents = await page.evaluate(async () => { - return await navigator.clipboard.readText(); - }); - - return clipboardContents; -}; diff --git a/apps/deploy-web/tests/ui/fixture/wallet-setup.ts b/apps/deploy-web/tests/ui/fixture/wallet-setup.ts deleted file mode 100644 index f91f23891a..0000000000 --- a/apps/deploy-web/tests/ui/fixture/wallet-setup.ts +++ /dev/null @@ -1,255 +0,0 @@ -import type { NetworkId } from "@akashnetwork/chain-sdk"; -import { netConfig } from "@akashnetwork/net"; -import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; -import type { BrowserContext, Page } from "@playwright/test"; -import fs from "fs"; -import path from "path"; -import { setTimeout as wait } from "timers/promises"; - -import { isWalletConnected } from "../uiState/isWalletConnected"; -import { testEnvConfig } from "./test-env.config"; -import { clickWalletSelectorDropdown } from "./testing-helpers"; - -const WALLET_PASSWORD = "12345678"; - -export async function getExtensionPage(context: BrowserContext): Promise { - const extensionId = await getExtensionId(context); - const extUrl = `chrome-extension://${extensionId}`; - const extPage = context.pages().find(page => page.url().startsWith(extUrl)); - - if (!extPage) { - const page = await context.newPage(); - await page.goto(`${extUrl}/index.html`, { waitUntil: "domcontentloaded" }); - return page; - } - - return extPage; -} - -let extensionId: string | undefined; -export async function getExtensionId(context: BrowserContext): Promise { - if (extensionId) return extensionId; - - let [background] = context.serviceWorkers(); - if (!background) { - background = await context.waitForEvent("serviceworker"); - } - - extensionId = background.url().split("/")[2]; - return extensionId; -} - -export async function setupWallet(page: Page) { - const address = await restoreExtensionStorage(page, testEnvConfig.NETWORK_ID); - await topUpWallet(address); - await unlockWallet(page); -} - -export async function createWallet(context: BrowserContext): Promise<{ - extPage: Page; - address: string; -}> { - const extPage = await getExtensionPage(context); - await extPage.waitForLoadState("load"); - - await clickWalletSelectorDropdown(extPage); - await extPage.getByRole("button", { name: /import wallet/i }).click(); - await extPage.getByRole("button", { name: /recovery phrase/i }).click(); - const tmpWallet = await DirectSecp256k1HdWallet.generate(12, { prefix: "akash" }); - await fillInMnemonic(extPage, tmpWallet.mnemonic); - await extPage.getByRole("button", { name: /import wallet/i }).click(); - - const accounts = await tmpWallet.getAccounts(); - - return { - extPage, - address: accounts[0].address - }; -} - -export async function connectWalletViaLeap(context: BrowserContext, page: Page) { - if (!(await isWalletConnected(page))) { - await page.getByRole("button", { name: /connect wallet/i }).click({ timeout: 30_000 }); - const popupPagePromise = context.waitForEvent("page").catch(() => null); - - await page.getByRole("button", { name: "Leap Leap" }).click(); - const popupPage = await Promise.race([popupPagePromise, isWalletConnected(page).then(() => null)]); - - if (popupPage) { - await connectOrUnlockWallet(popupPage); - await isWalletConnected(page); - } - } -} - -async function connectOrUnlockWallet(popupPage: Page) { - const buttonLocator = popupPage - .getByRole("button", { name: /Unlock wallet/i }) - .or(popupPage.getByRole("button", { name: /connect button in approve connection flow/i })) - .or(popupPage.getByRole("button", { name: /Approve/i })); - const buttonText = (await buttonLocator.textContent())?.trim(); - if (buttonText === "Unlock wallet") { - await unlockWallet(popupPage); - } else if (buttonText === "Connect" || buttonText === "Approve") { - await buttonLocator.click(); - } else { - throw new Error(`Unexpected state in wallet popup: ${buttonText}`); - } -} - -export async function awaitWalletAndApprove(context: BrowserContext, page: Page) { - const popupPage = await Promise.race([context.waitForEvent("page", { timeout: 5_000 }), getExtensionPage(context)]); - await approveWalletOperation(popupPage); - await isWalletConnected(page); -} - -export async function approveWalletOperation(popupPage: Page | null) { - if (!popupPage) return; - const buttonLocator = popupPage.locator("button", { hasText: /^\s*(Approve|Unlock wallet|Connect)\s*$/i }); - await buttonLocator.waitFor({ state: "visible", timeout: 5_000 }); - - const buttonText = await buttonLocator.textContent(); - - switch (buttonText?.trim()) { - case "Approve": { - // increase gas limit - await popupPage.getByText(/show additional settings/i).click(); - const gasInput = popupPage - .getByText("Enter gas limit manually") - .locator("xpath=..") // get parent - .locator("input"); - - const value = Number(await gasInput.inputValue()); - await gasInput.fill(Math.ceil(1.5 * value).toString()); - await buttonLocator.click(); - break; - } - case "Unlock wallet": - await unlockWallet(popupPage); - await popupPage - .getByRole("button", { name: /connect button in approve connection flow/i }) - .or(popupPage.getByLabel("wallet dropdown")) - .click(); - break; - case "Connect": - await buttonLocator.click(); - break; - default: - throw new Error("Unexpected state in wallet popup"); - } -} - -export async function unlockWallet(page: Page) { - await page.waitForEvent("load", { timeout: 2_000 }).catch(() => {}); - await page.locator("input").fill(WALLET_PASSWORD); - await page.getByRole("button", { name: /unlock wallet/i }).click(); -} - -export async function importWalletToLeap(page: Page, mnemonic: string) { - await page.getByText(/import an existing wallet/i).click(); - await page.getByText(/recovery phrase/i).click(); - await fillInMnemonic(page, mnemonic); - - await page.getByRole("button", { name: /Continue/i }).click(); - await page.waitForTimeout(2000); - await page.getByRole("checkbox", { name: "Wallet 1" }).setChecked(true); - await page.getByRole("button", { name: /Proceed/i }).click(); - - // Set password - await page.getByPlaceholder("Enter password").fill(WALLET_PASSWORD); - await page.getByPlaceholder("Confirm password").fill(WALLET_PASSWORD); - await page.locator("button", { hasText: /Set Password/i }).click(); - - await page.waitForLoadState("domcontentloaded"); - - return await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { - prefix: "akash" - }); -} - -async function fillInMnemonic(page: Page, mnemonic: string) { - const mnemonicArray = mnemonic.trim().split(" "); - - await page.locator('input[type="text"]:first-of-type').first().focus(); - - for (const word of mnemonicArray) { - await page.locator("input:focus").fill(word); - await page.keyboard.press("Tab"); - } -} - -export async function topUpWallet(address: string, attempt = 0) { - try { - const balance = await getBalance(address); - - if (balance > 50 * 1_000_000) { - // 50 AKT should be enough - return; - } - - let faucetUrl = netConfig.getFaucetUrl(testEnvConfig.NETWORK_ID); - if (!faucetUrl) { - console.error(`Faucet URL is not set for this network: ${testEnvConfig.NETWORK_ID}. Cannot auto top up wallet`); - return; - } - - if (faucetUrl.endsWith("/")) { - faucetUrl = faucetUrl.slice(0, -1); - } - - const response = await fetch(`${faucetUrl}/faucet`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded" - }, - body: `address=${encodeURIComponent(address)}` - }); - if (response.status >= 300) { - const error = await response.text(); - console.error(`Unexpected faucet response status: ${response.status}`); - console.error(error); - - if (error.includes("account sequence mismatch") && attempt < 10) { - console.log("retrying top up attempt...", attempt + 1); - await wait(2000); - await topUpWallet(address, attempt + 1); - return; - } - } - } catch (error) { - console.error("Unable to top up wallet"); - console.error(error); - } -} - -async function getBalance(address: string) { - const response = await fetch(`${netConfig.getBaseAPIUrl(testEnvConfig.NETWORK_ID)}/cosmos/bank/v1beta1/balances/${address}`); - const data = await response.json(); - if (!response.ok) return 0; - return data.balances.find((balance: Record) => balance.denom === "uakt")?.amount || 0; -} - -/** - * To get the extension storage, follow these steps: - * 1. Open Chrome with Leap extension installed - * 2. Open DevTools (F12) on Leap extension page - * 3. Run this in the script: - * ```js - * chrome.storage.local.get(null, (data) => { - * const json = JSON.stringify(data, null, 2); - * const blob = new Blob([json], {type: 'application/json'}); - * const url = URL.createObjectURL(blob); - * const a = document.createElement('a'); - * a.href = url; - * a.download = 'leapExtensionLocalStorage.json'; - * a.click(); - * }); - * ``` - * - * @see https://github.com/microsoft/playwright/issues/14949 - */ -export async function restoreExtensionStorage(page: Page, networkId: NetworkId): Promise { - const extensionStorage = JSON.parse(fs.readFileSync(path.join(__dirname, `leapExtensionLocalStorage.${networkId}.json`), "utf8")); - await page.evaluate(data => chrome.storage.local.set(data), extensionStorage); - return extensionStorage["active-wallet"].addresses.akash; -} diff --git a/apps/deploy-web/tests/ui/fixture/web-wallet/CosmjsWebWallet.ts b/apps/deploy-web/tests/ui/fixture/web-wallet/CosmjsWebWallet.ts deleted file mode 100644 index 24f43c78b1..0000000000 --- a/apps/deploy-web/tests/ui/fixture/web-wallet/CosmjsWebWallet.ts +++ /dev/null @@ -1,199 +0,0 @@ -import type { SignDoc } from "@akashnetwork/chain-sdk/private-types/cosmos.v1beta1"; -import { AuthInfo } from "@akashnetwork/chain-sdk/private-types/cosmos.v1beta1"; -import type { AminoSignResponse, StdSignature, StdSignDoc } from "@cosmjs/amino"; -import { Secp256k1HdWallet, serializeSignDoc } from "@cosmjs/amino"; -import { Secp256k1, Secp256k1Signature, sha256 } from "@cosmjs/crypto"; -import { fromBase64, fromBech32 } from "@cosmjs/encoding"; -import type { AccountData, DirectSignResponse } from "@cosmjs/proto-signing"; -import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; -import { SigningStargateClient } from "@cosmjs/stargate"; - -export type FeeType = "low" | "medium" | "high"; - -const GAS_MULTIPLIERS: Record = { - low: 1, - medium: 1.3, - high: 1.6 -}; - -export class CosmjsWebWallet { - #nextFeeType: FeeType = "low"; - #currentMnemonic = ""; - #wallets: Record = {}; - #suggestedChains: Record = {}; - #stargateClients: Record = {}; - - setFeeType(feeType: FeeType): void { - this.#nextFeeType = feeType; - } - - async switchWallet(mnemonic: string): Promise { - this.#currentMnemonic = mnemonic; - const chainIds = Object.keys(this.#suggestedChains); - for (const chainId of chainIds) { - delete this.#wallets[chainId]; - this.#stargateClients[chainId]?.disconnect(); - delete this.#stargateClients[chainId]; - } - for (const chainId of chainIds) { - const chainInfo = this.#suggestedChains[chainId]; - const prefix = chainInfo.bech32Config?.bech32PrefixAccAddr || "cosmos"; - await this.#getOrCreateWallet(mnemonic, chainId, prefix); - } - } - - async suggestChain(chainInfo: ChainInfo): Promise { - this.#suggestedChains[chainInfo.chainId] = chainInfo; - const prefix = chainInfo.bech32Config?.bech32PrefixAccAddr || "cosmos"; - await this.#getOrCreateWallet(this.#currentMnemonic, chainInfo.chainId, prefix); - } - - async getKey(chainId: string): Promise<{ - name: string; - algo: string; - pubKey: Uint8Array; - address: Uint8Array; - bech32Address: string; - ethereumHexAddress: string; - isNanoLedger: boolean; - isKeystone: boolean; - }> { - const w = this.#getWallet(chainId); - return { - name: "e2e-test-wallet", - algo: w.account.algo, - pubKey: w.account.pubkey, - address: fromBech32(w.account.address).data, - bech32Address: w.account.address, - ethereumHexAddress: "", - isNanoLedger: false, - isKeystone: false - }; - } - - async getAccounts(chainId: string): Promise { - const w = this.#getWallet(chainId); - return [{ address: w.account.address, algo: w.account.algo, pubkey: w.account.pubkey }]; - } - - async signDirect(chainId: string, signer: string, serializedDoc: SignDoc): Promise { - const w = this.#getWallet(chainId); - let { authInfoBytes } = serializedDoc; - const multiplier = GAS_MULTIPLIERS[this.#nextFeeType]; - if (multiplier !== 1) { - let authInfo = AuthInfo.decode(authInfoBytes); - if (authInfo.fee) { - authInfo = AuthInfo.fromPartial({ - ...authInfo, - fee: { - ...authInfo.fee, - gasLimit: BigInt(Math.ceil(Number(authInfo.fee.gasLimit) * multiplier)) - } - }); - } - authInfoBytes = AuthInfo.encode(authInfo).finish(); - this.#nextFeeType = "low"; - } - const signDoc: SignDoc = { - bodyBytes: serializedDoc.bodyBytes, - authInfoBytes, - chainId: serializedDoc.chainId, - accountNumber: serializedDoc.accountNumber - }; - const result = await w.direct.signDirect(signer, signDoc as any); - return result; - } - - async signAmino(chainId: string, signer: string, signDoc: StdSignDoc): Promise { - const w = this.#getWallet(chainId); - const result = await w.amino.signAmino(signer, signDoc); - return result; - } - - async signArbitrary(chainId: string, signer: string, data: string): Promise { - const w = this.#getWallet(chainId); - const b64Data = btoa(data); - const signDoc: StdSignDoc = { - chain_id: "", - account_number: "0", - sequence: "0", - fee: { gas: "0", amount: [] }, - msgs: [{ type: "sign/MsgSignData", value: { signer, data: b64Data } }], - memo: "" - }; - const result = await w.amino.signAmino(signer, signDoc); - return { pub_key: result.signature.pub_key, signature: result.signature.signature }; - } - - async verifyArbitrary(_: unknown, signer: string, data: string, signature: StdSignature): Promise { - const b64Data = btoa(data); - const signDoc: StdSignDoc = { - chain_id: "", - account_number: "0", - sequence: "0", - fee: { gas: "0", amount: [] }, - msgs: [{ type: "sign/MsgSignData", value: { signer, data: b64Data } }], - memo: "" - }; - const serialized = serializeSignDoc(signDoc); - const hash = sha256(serialized); - const pubkeyBytes = fromBase64(signature.pub_key.value); - const sigBytes = fromBase64(signature.signature); - const sig = Secp256k1Signature.fromFixedLength(sigBytes); - return await Secp256k1.verifySignature(sig, hash, pubkeyBytes); - } - - async sendTx(chainId: string, tx: Uint8Array): Promise { - const w = this.#getWallet(chainId); - const chainInfo = this.#suggestedChains[chainId]; - if (!chainInfo?.rpc) throw new Error(`No RPC for chain ${chainId}.`); - if (!this.#stargateClients[chainId]) { - this.#stargateClients[chainId] = await SigningStargateClient.connectWithSigner(chainInfo.rpc, w.direct); - } - const txBytes = tx instanceof Uint8Array ? tx : fromBase64(tx as unknown as string); - const result = await this.#stargateClients[chainId].broadcastTx(txBytes); - if (result.code !== 0) throw new Error(`Broadcast failed: code ${result.code}`); - const hashBytes = new Uint8Array(result.transactionHash.length / 2); - for (let i = 0; i < hashBytes.length; i++) { - hashBytes[i] = parseInt(result.transactionHash.substring(i * 2, i * 2 + 2), 16); - } - return hashBytes; - } - - #getWallet(chainId: string): WalletEntry { - const w = this.#wallets[chainId]; - if (!w) throw new Error(`Chain ${chainId} not registered. Call experimentalSuggestChain first.`); - return w; - } - - async #getOrCreateWallet(mnemonic: string, chainId: string, prefix: string): Promise { - if (this.#wallets[chainId]) return this.#wallets[chainId]; - - const direct = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { prefix }); - const amino = await Secp256k1HdWallet.fromMnemonic(mnemonic, { prefix }); - const [account] = await direct.getAccounts(); - - this.#wallets[chainId] = { direct, amino, account }; - return this.#wallets[chainId]; - } -} - -interface WalletEntry { - direct: DirectSecp256k1HdWallet; - amino: Secp256k1HdWallet; - account: AccountData; -} - -interface ChainInfo { - chainId: string; - rpc: string; - rest: string; - bech32Config?: { - bech32PrefixAccAddr: string; - bech32PrefixAccPub: string; - bech32PrefixValAddr: string; - bech32PrefixValPub: string; - bech32PrefixConsAddr: string; - bech32PrefixConsPub: string; - }; -} diff --git a/apps/deploy-web/tests/ui/fixture/web-wallet/initLeapWebWalletMock.ts b/apps/deploy-web/tests/ui/fixture/web-wallet/initLeapWebWalletMock.ts deleted file mode 100644 index 9be318a78c..0000000000 --- a/apps/deploy-web/tests/ui/fixture/web-wallet/initLeapWebWalletMock.ts +++ /dev/null @@ -1,65 +0,0 @@ -export function initLeapWebWalletMock(options: { rpcHandlerName: string }) { - async function rpc(method: string, ...args: unknown[]): Promise { - const result: string = await (window as any)[options.rpcHandlerName](method, args); - return result; - } - - function makeSigner(chainId: string) { - return { - async getAccounts() { - return rpc("getAccounts", chainId); - }, - async signDirect(signerAddress: string, signDoc: any) { - return rpc("signDirect", chainId, signerAddress, signDoc); - }, - async signAmino(signerAddress: string, signDoc: any) { - return rpc("signAmino", chainId, signerAddress, signDoc); - } - }; - } - - const leap = { - async enable() {}, - async disconnect() {}, - async experimentalSuggestChain(chainInfo: any) { - await rpc("suggestChain", chainInfo); - }, - async getKey(chainId: string) { - return rpc("getKey", chainId); - }, - getOfflineSigner(chainId: string) { - return makeSigner(chainId); - }, - async getOfflineSignerAuto(chainId: string) { - return makeSigner(chainId); - }, - getOfflineSignerOnlyAmino(chainId: string) { - const s = makeSigner(chainId); - return { getAccounts: s.getAccounts, signAmino: s.signAmino }; - }, - async signDirect(chainId: string, signer: string, signDoc: any) { - return rpc("signDirect", chainId, signer, signDoc); - }, - async signAmino(chainId: string, signer: string, signDoc: any) { - return rpc("signAmino", chainId, signer, signDoc); - }, - async signArbitrary(chainId: string, signer: string, data: string) { - return rpc("signArbitrary", chainId, signer, data); - }, - async verifyArbitrary(chainId: string, signer: string, data: string, signature: any) { - return rpc("verifyArbitrary", chainId, signer, data, signature); - }, - async sendTx(chainId: string, tx: Uint8Array, mode: string) { - return rpc("sendTx", chainId, tx, mode); - }, - defaultOptions: { - sign: { - preferNoSetFee: false, - preferNoSetMemo: true, - disableBalanceCheck: true - } - } - }; - - Object.defineProperty(window, "leap", { value: leap, writable: false, configurable: false }); -} diff --git a/apps/deploy-web/tests/ui/fixture/web-wallet/injectWebWallet.ts b/apps/deploy-web/tests/ui/fixture/web-wallet/injectWebWallet.ts deleted file mode 100644 index bc3eae4876..0000000000 --- a/apps/deploy-web/tests/ui/fixture/web-wallet/injectWebWallet.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { Page } from "@playwright/test"; - -import type { FeeType } from "./CosmjsWebWallet"; -import { CosmjsWebWallet } from "./CosmjsWebWallet"; -import { initLeapWebWalletMock } from "./initLeapWebWalletMock"; - -export type { FeeType } from "./CosmjsWebWallet"; - -const WALLETS = new Map(); -const getWallet = (page: Page): CosmjsWebWallet => { - let wallet = WALLETS.get(page); - if (!wallet) { - wallet = new CosmjsWebWallet(); - WALLETS.set(page, wallet); - page.once("close", () => WALLETS.delete(page)); - } - return wallet; -}; - -export function setFeeType(page: Page, feeType: FeeType) { - getWallet(page).setFeeType(feeType); -} - -export async function switchWebWallet(page: Page, mnemonic: string) { - await getWallet(page).switchWallet(mnemonic); - await page.evaluate(() => window.dispatchEvent(new Event("leap_keystorechange"))); -} - -const RPC_HANDLER_NAME = "__akashCosmjsWalletRpc"; - -export async function injectWebWallet(page: Page, mnemonic: string) { - const wallet = getWallet(page); - await wallet.switchWallet(mnemonic); - await page.exposeFunction(RPC_HANDLER_NAME, async (method: keyof typeof wallet, args: unknown[]) => { - if (!wallet[method]) throw new Error(`Unknown wallet RPC method: ${method}`); - const result = await (wallet[method] as (...args: unknown[]) => Promise)(...args); - return result; - }); - await page.addInitScript(initLeapWebWalletMock, { - rpcHandlerName: RPC_HANDLER_NAME - }); -} diff --git a/apps/deploy-web/tests/ui/managed-wallet-alerts.spec.ts b/apps/deploy-web/tests/ui/managed-wallet-alerts.spec.ts index 505872b159..03f6b148c6 100644 --- a/apps/deploy-web/tests/ui/managed-wallet-alerts.spec.ts +++ b/apps/deploy-web/tests/ui/managed-wallet-alerts.spec.ts @@ -18,7 +18,7 @@ test.describe("Managed wallet alerts", () => { const alertsPage = new AlertsPage(page); const alertsForm = new DeploymentAlertsForm(page); const billingPage = new BillingPage(page); - const deployPage = new DeployPage(context, page, { walletType: "api" }); + const deployPage = new DeployPage(context, page); await test.step("login", async () => { await homePage.goto(); diff --git a/apps/deploy-web/tests/ui/managed-wallet-deployment.spec.ts b/apps/deploy-web/tests/ui/managed-wallet-deployment.spec.ts index f7a6ac16ea..a375c0bff2 100644 --- a/apps/deploy-web/tests/ui/managed-wallet-deployment.spec.ts +++ b/apps/deploy-web/tests/ui/managed-wallet-deployment.spec.ts @@ -14,7 +14,7 @@ test.describe("Managed wallet deployment", () => { const authPage = new AuthPage(page); const billingPage = new BillingPage(page); const sidebar = new Sidebar(page); - const deployPage = new DeployPage(context, page, { walletType: "api" }); + const deployPage = new DeployPage(context, page); await test.step("login", async () => { await homePage.goto(); diff --git a/apps/deploy-web/tests/ui/pages/AuthorizationsPage.tsx b/apps/deploy-web/tests/ui/pages/AuthorizationsPage.tsx deleted file mode 100644 index bec1a4db0c..0000000000 --- a/apps/deploy-web/tests/ui/pages/AuthorizationsPage.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import type { BrowserContext as Context, Locator, Page } from "@playwright/test"; - -import { testEnvConfig } from "../fixture/test-env.config"; -import { WebWallet } from "./WebWallet"; - -export type AuthorizationType = "deployment" | "tx_fee"; - -const AUTHORIZE_BUTTON_LABELS = { - deployment: "Authorize Spend", - tx_fee: "Authorize Fee Spend" -}; -const AUTHORIZATION_LIST_LABELS = { - deployment: { - title: /Deployment Authorization/i, - emptyTitle: /No authorizations given/i - }, - tx_fee: { - title: /Tx Fee Authorization/i, - emptyTitle: /No allowances issued/i - } -}; - -export class AuthorizationsPage { - constructor( - readonly context: Context, - readonly page: Page - ) {} - - async goto(url = `${testEnvConfig.BASE_URL}/settings/authorizations`) { - await this.page.goto(url); - } - - async clickGrantButton() { - return await this.page.getByRole("button", { name: /^\s*Grant\s*$/ }).click(); - } - - async authorizeSpending(type: AuthorizationType, address: string) { - await this.page.getByRole("button", { name: AUTHORIZE_BUTTON_LABELS[type] }).click(); - await this.page.getByLabel("Spending Limit").fill("5"); - await this.page.getByLabel("Grantee Address").fill(address); - await this.clickGrantButton(); - } - - async editSpending(type: AuthorizationType, address: string) { - const shortenedAddress = shortenAddress(address); - await this.getListLocator(type).locator("tr", { hasText: shortenedAddress }).getByLabel("Edit Authorization").click(); - await this.page.getByLabel("Spending Limit").fill("10"); - await this.clickGrantButton(); - } - - async revokeSpending(type: AuthorizationType, address: string) { - const shortenedAddress = shortenAddress(address); - await this.page.getByLabel(AUTHORIZATION_LIST_LABELS[type].title).locator("tr", { hasText: shortenedAddress }).getByLabel("Revoke Authorization").click(); - await this.page.getByRole("button", { name: "Confirm" }).click(); - } - - async revokeAll(type: AuthorizationType): Promise { - const extension = new WebWallet(this.context, this.page); - const selectors = AUTHORIZATION_LIST_LABELS[type]; - const hasGrants = await Promise.race([ - this.getListLocator(type) - .getByRole("button", { name: /revoke all/i }) - .click() - .then( - () => true, - () => false - ), - this.getListLocator(type) - .getByText(selectors.emptyTitle) - .waitFor({ state: "visible" }) - .then( - () => false, - () => true - ) - ]); - if (hasGrants) { - await Promise.all([this.page.getByRole("button", { name: "Confirm" }).click(), extension.acceptTransaction("high")]); - await extension.waitForTransaction("success"); - } - } - - getListLocator(type: AuthorizationType): Locator { - return this.page.getByLabel(AUTHORIZATION_LIST_LABELS[type].title); - } -} - -export function shortenAddress(address: string) { - return `${address.slice(0, 8)}...${address.slice(-5)}`; -} diff --git a/apps/deploy-web/tests/ui/pages/DeployPage.tsx b/apps/deploy-web/tests/ui/pages/DeployPage.tsx index 14068fc613..70a74839c1 100644 --- a/apps/deploy-web/tests/ui/pages/DeployPage.tsx +++ b/apps/deploy-web/tests/ui/pages/DeployPage.tsx @@ -2,24 +2,12 @@ import type { BrowserContext as Context, Page } from "@playwright/test"; import { expect } from "@playwright/test"; import { PROVIDERS_WHITELIST, testEnvConfig } from "../fixture/test-env.config"; -import { WebWallet } from "./WebWallet"; - -export type FeeType = "low" | "medium" | "high"; -export type WalletType = "api" | "extension"; -export type SignOptions = { feeType?: FeeType; walletType: WalletType }; export class DeployPage { - protected feeType: FeeType = "low"; - protected walletType: WalletType; - constructor( readonly context: Context, - readonly page: Page, - private readonly options: SignOptions = { walletType: "api" } - ) { - this.feeType = options.feeType ?? this.feeType; - this.walletType = options.walletType; - } + readonly page: Page + ) {} async goto() { await this.page.goto(`${testEnvConfig.BASE_URL}/new-deployment`); @@ -50,41 +38,25 @@ export class DeployPage { return dialog; } - async createDeployment() { - await this.withTxAccepted(async () => { - await this.page.getByRole("button", { name: /create deployment/i }).click(); - await this.page.getByRole("button", { name: /^continue$/i }).click(); - }); - } - async createLease(providerName?: string) { - await this.withTxAccepted(async () => { - if (providerName) { - await this.page.getByLabel(providerName).click(); + if (providerName) { + await this.page.getByLabel(providerName).click(); + } else { + const providers = PROVIDERS_WHITELIST[testEnvConfig.NETWORK_ID]; + if (!providers.length) { + await this.page.getByRole("radio", { checked: false }).first().click(); } else { - const providers = PROVIDERS_WHITELIST[testEnvConfig.NETWORK_ID]; - if (!providers.length) { - await this.page.getByRole("radio", { checked: false }).first().click(); - } else { - const locator = providers - .slice(1) - .reduce( - (combined, owner) => combined.or(this.page.locator(`[role="radio"][aria-description="${owner}"]`)), - this.page.locator(`[role="radio"][aria-description="${providers[0]}"]`) - ); - await locator.first().click({ timeout: 60_000 }); - } + const locator = providers + .slice(1) + .reduce( + (combined, owner) => combined.or(this.page.locator(`[role="radio"][aria-description="${owner}"]`)), + this.page.locator(`[role="radio"][aria-description="${providers[0]}"]`) + ); + await locator.first().click({ timeout: 60_000 }); } + } - await this.page.getByRole("button", { name: /accept bid/i }).click(); - }); - } - - async validateLeaseAndClose() { - await this.validateLease(); - await this.withTxAccepted(async () => { - await this.closeDeployment(); - }); + await this.page.getByRole("button", { name: /accept bid/i }).click(); } async validateLease() { @@ -102,14 +74,4 @@ export class DeployPage { await this.page.getByRole("button", { name: /deployment actions/i }).click(); await this.page.getByRole("menuitem", { name: /close deployment/i }).click(); } - - private async withTxAccepted(fn: () => Promise) { - await Promise.all([this.walletType === "extension" ? this.signTransaction() : Promise.resolve(), fn()]); - } - - async signTransaction() { - const extension = new WebWallet(this.context, this.page); - await extension.acceptTransaction(this.feeType); - await extension.waitForTransaction("success"); - } } diff --git a/apps/deploy-web/tests/ui/pages/WebWallet.ts b/apps/deploy-web/tests/ui/pages/WebWallet.ts deleted file mode 100644 index a7fa35f54e..0000000000 --- a/apps/deploy-web/tests/ui/pages/WebWallet.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; -import type { BrowserContext, Page } from "@playwright/test"; - -import { testEnvConfig } from "../fixture/test-env.config"; -import { topUpWallet } from "../fixture/wallet-setup"; -import type { FeeType } from "../fixture/web-wallet/injectWebWallet"; -import { setFeeType, switchWebWallet } from "../fixture/web-wallet/injectWebWallet"; - -export class WebWallet { - constructor( - readonly context: BrowserContext, - readonly page: Page - ) {} - - async goto() {} - - async switchToNewWallet(): Promise { - const tmpWallet = await DirectSecp256k1HdWallet.generate(12, { prefix: "akash" }); - const [account] = await tmpWallet.getAccounts(); - await topUpWallet(account.address); - await switchWebWallet(this.page, tmpWallet.mnemonic); - return account.address; - } - - async switchToTestWallet(): Promise { - await switchWebWallet(this.page, testEnvConfig.TEST_WALLET_MNEMONIC); - } - - async disconnectWallet() { - await this.page.getByLabel("Connected wallet name and balance").click(); - await this.page.getByRole("button", { name: "Disconnect Wallet" }).click(); - await this.page.reload({ waitUntil: "networkidle" }); - } - - acceptTransaction(feeType: FeeType = "low") { - setFeeType(this.page, feeType); - } - - async waitForTransaction(type: "success" | "error"): Promise { - await this.getTransaction(type); - } - - async getTransaction(type: "success" | "error"): Promise<{ - type: "success" | "error"; - text: string; - close: () => Promise; - }> { - const MESSAGES = { - success: /Transaction success/i, - error: /Transaction has failed/i - }; - const resultLocator = this.page - .locator('[role="alert"]', { hasText: MESSAGES.success }) - .or(this.page.locator('[role="alert"]', { hasText: MESSAGES.error })); - const text = await resultLocator.textContent({ timeout: 25_000 }); - - if (!text || !MESSAGES[type].test(text)) { - throw new Error(`Expected transaction "${type}" but got "${text}"`); - } - - return { - type, - text, - close: () => resultLocator.getByRole("button").click() - }; - } -} diff --git a/apps/deploy-web/tests/ui/uiState/isWalletConnected.ts b/apps/deploy-web/tests/ui/uiState/isWalletConnected.ts deleted file mode 100644 index d148c38414..0000000000 --- a/apps/deploy-web/tests/ui/uiState/isWalletConnected.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { Page } from "@playwright/test"; - -export async function isWalletConnected(page: Page) { - const result = await Promise.race([ - page - .getByLabel("Connected wallet name and balance") - .waitFor({ state: "visible" }) - .then(() => true) - .catch(() => null), - page - .getByRole("button", { name: /connect wallet/i }) - .waitFor({ state: "visible" }) - .then(() => false) - .catch(() => null) - ]); - - if (result === null) { - throw new Error("Wallet is not connected and there is no button to connect it"); - } - - return result; -} diff --git a/package-lock.json b/package-lock.json index 20492bf7c8..23547942b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -626,7 +626,6 @@ "@akashnetwork/releaser": "*", "@chain-registry/types": "^0.50.12", "@cosmjs/amino": "~0.38.0", - "@cosmjs/crypto": "~0.38.0", "@faker-js/faker": "^9.4.0", "@keplr-wallet/types": "^0.12.111", "@next/bundle-analyzer": "^14.0.1",