diff --git a/packages/curve-ui-kit/src/features/connect-wallet/lib/wagmi/useWagmiConfig.ts b/packages/curve-ui-kit/src/features/connect-wallet/lib/wagmi/useWagmiConfig.ts index b04e82f431..6697b210ae 100644 --- a/packages/curve-ui-kit/src/features/connect-wallet/lib/wagmi/useWagmiConfig.ts +++ b/packages/curve-ui-kit/src/features/connect-wallet/lib/wagmi/useWagmiConfig.ts @@ -1,22 +1,35 @@ import { useMemo } from 'react' import type { Chain } from 'viem' +import { generatePrivateKey } from 'viem/accounts' import { mapRecord, recordValues } from '@curvefi/prices-api/objects.util' import type { NetworkMapping } from '@ui/utils' +import { Chain as ChainEnum, isCypress, noCypressTestConnector } from '@ui-kit/utils' import { createChainFromNetwork } from './chains' import { defaultGetRpcUrls } from './rpc' import { createTransportFromNetwork } from './transports' import { createWagmiConfig } from './wagmi-config' +import { createTestConnector } from './wagmi-test' export const useWagmiConfig = (networks: T | undefined) => - useMemo( - () => - networks && - createWagmiConfig({ - chains: recordValues(networks).map((network) => createChainFromNetwork(network, defaultGetRpcUrls)) as [ - Chain, - ...Chain[], - ], - transports: mapRecord(networks, (_, network) => createTransportFromNetwork(network, defaultGetRpcUrls)), - }), - [networks], - ) + useMemo(() => { + if (networks == null) return + + const chains = recordValues(networks).map((network) => createChainFromNetwork(network, defaultGetRpcUrls)) as [ + Chain, + ...Chain[], + ] + + return createWagmiConfig({ + chains, + transports: mapRecord(networks, (_, network) => createTransportFromNetwork(network, defaultGetRpcUrls)), + ...(isCypress && + !noCypressTestConnector && { + connectors: [ + createTestConnector({ + privateKey: generatePrivateKey(), + chain: chains.find((chain) => chain.id === ChainEnum.Ethereum)!, + })!, + ], + }), + }) + }, [networks]) diff --git a/tests/cypress/support/helpers/test-connector.ts b/packages/curve-ui-kit/src/features/connect-wallet/lib/wagmi/wagmi-test.ts similarity index 57% rename from tests/cypress/support/helpers/test-connector.ts rename to packages/curve-ui-kit/src/features/connect-wallet/lib/wagmi/wagmi-test.ts index 97a1f3b2b9..f5a26043e8 100644 --- a/tests/cypress/support/helpers/test-connector.ts +++ b/packages/curve-ui-kit/src/features/connect-wallet/lib/wagmi/wagmi-test.ts @@ -1,69 +1,65 @@ import { - type Chain, createWalletClient, custom, fallback, - type Hex, http, - PrivateKeyAccount, - type RpcTransactionRequest, + type Address, + type Chain, + type CustomTransport, + type Hex, + type PrivateKeyAccount, } from 'viem' import { privateKeyToAccount } from 'viem/accounts' -import type { TenderlyConfig } from '@cy/support/helpers/tenderly/account' -import { Address } from '@ui-kit/utils' -import { createConnector, type CreateConnectorFn } from '@wagmi/core' -import { sendVnetTransaction } from './tenderly/vnet-transaction' - -type Options = { - /** A hexadecimal private key used to generate a test account */ - privateKey: Hex - /** The testnet chain configuration */ - chain: Chain - /** Tenderly configuration for the Virtual Testnet */ - tenderly: TenderlyConfig -} +import { type CreateConnectorFn, createConnector } from 'wagmi' type ConnectParams = { chainId?: number; isReconnecting?: boolean; withCapabilities: T } type ConnectResult = { accounts: readonly T[]; chainId: number } type Account = { address: Address; capabilities: Record } -/** - * Creates a custom transport that intercepts JSON-RPC requests to handle account retrieval and - * transaction sending via Tenderly Virtual Testnet Admin API. - * - * This is necessary because our code under test uses a BrowserProvider with http transport, - * which relies on RPC methods to retrieve accounts and send transactions. - */ -const customTransport = (account: PrivateKeyAccount, tenderly: TenderlyConfig) => +/** Default custom transport for Cypress E2E tests, read-only */ +const cypressTransport = (account: PrivateKeyAccount) => custom({ - request: async ({ method, params: [param] }): Promise => { + request: async ({ method }): Promise => { if (method === 'eth_accounts') { return [account.address] } - if (method === 'eth_sendTransaction') { - return await sendVnetTransaction({ tenderly, tx: param as RpcTransactionRequest }).catch((err) => { - console.error(`Tenderly failed for ${method}(${JSON.stringify(param)}). Error: ${err}`) - throw err - }) - } throw new Error(`Unsupported method: ${method}, http fallback is used`) }, }) +export type CreateTestConnectorOptions = { + /** A hexadecimal private key used to generate a test account */ + privateKey: Hex + /** The testnet chain configuration */ + chain: Chain + /** + * Creates a custom transport that intercepts JSON-RPC requests to handle account retrieval and such. + * + * This is necessary because our code under test uses a BrowserProvider with http transport, + * which relies on RPC methods not always available to retrieve accounts and send transactions. + * + * Defaults to a read-only custom transport for Cypress. + */ + transport?: (account: PrivateKeyAccount) => CustomTransport +} + /** - * Cypress test connector for Wagmi. + * Creates a wagmi test connector for Cypress. * - * This connector is designed for use in test environments (e.g., Cypress) with a testnet chain. - * It creates a wallet using a custom seed (private key) to allow testing contract write calls + * This connector is designed for use in test environments (e.g., Cypress) with optionally a testnet chain. + * It creates a wallet using a custom seed (private key) to allow testing contract read and write calls * without relying on third-party browser extensions like MetaMask. */ -export function createTestConnector({ privateKey, chain, tenderly }: Options): CreateConnectorFn { +export function createTestConnector({ privateKey, chain, transport }: CreateTestConnectorOptions): CreateConnectorFn { const account = privateKeyToAccount(privateKey) const client = createWalletClient({ account, chain, - transport: fallback([customTransport(account, tenderly), ...chain.rpcUrls.default.http.map((url) => http(url))]), + transport: fallback([ + (transport ?? cypressTransport)(account), + ...chain.rpcUrls.default.http.map((url) => http(url)), + ]), }) // A connect function with overloads to satisfy Wagmi's conditional return type diff --git a/packages/curve-ui-kit/src/utils/index.ts b/packages/curve-ui-kit/src/utils/index.ts index 462b0edd66..a58fc473a1 100644 --- a/packages/curve-ui-kit/src/utils/index.ts +++ b/packages/curve-ui-kit/src/utils/index.ts @@ -18,6 +18,7 @@ export enum ReleaseChannel { } export const isCypress = Boolean((window as { Cypress?: unknown }).Cypress) +export const noCypressTestConnector = Boolean((window as { CypressNoTestConnector?: unknown }).CypressNoTestConnector) const isDefaultBeta = process.env.NODE_ENV === 'development' || diff --git a/tests/cypress/component/llamalend/borrow-tab-contents.rpc.cy.tsx b/tests/cypress/component/llamalend/borrow-tab-contents.rpc.cy.tsx index 1200e08861..289f8b25b2 100644 --- a/tests/cypress/component/llamalend/borrow-tab-contents.rpc.cy.tsx +++ b/tests/cypress/component/llamalend/borrow-tab-contents.rpc.cy.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react' +import { useMemo } from 'react' import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts' import { prefetchMarkets } from '@/lend/entities/chain/chain-query' import { CreateLoanForm } from '@/llamalend/features/borrow/components/CreateLoanForm' @@ -7,7 +7,7 @@ import type { CreateLoanOptions } from '@/llamalend/mutations/create-loan.mutati import networks from '@/loan/networks' import { oneBool, oneValueOf } from '@cy/support/generators' import { ComponentTestWrapper } from '@cy/support/helpers/ComponentTestWrapper' -import { createTestWagmiConfigFromVNet, createVirtualTestnet } from '@cy/support/helpers/tenderly' +import { createTenderlyWagmiConfigFromVNet, createVirtualTestnet } from '@cy/support/helpers/tenderly' import { getRpcUrls } from '@cy/support/helpers/tenderly/vnet' import { fundErc20, fundEth } from '@cy/support/helpers/tenderly/vnet-fund' import { LOAD_TIMEOUT } from '@cy/support/ui' @@ -80,7 +80,10 @@ describe('BorrowTabContents Component Tests', () => { }) const BorrowTabTestWrapper = (props: BorrowTabTestProps) => ( - + { } const TestComponentWrapper = () => ( - + ({ slug: `pegkeepers-${uuid}`, @@ -25,7 +25,7 @@ describe('Peg stability reserve', () => { return } - const config = createTestWagmiConfigFromVNet({ vnet }) + const config = createTenderlyWagmiConfigFromVNet({ vnet }) cy.mount() // Initial data when not connected diff --git a/tests/cypress/component/root-layout.rpc.cy.tsx b/tests/cypress/component/root-layout.rpc.cy.tsx index 66ccec59d6..0ee34d2914 100644 --- a/tests/cypress/component/root-layout.rpc.cy.tsx +++ b/tests/cypress/component/root-layout.rpc.cy.tsx @@ -1,7 +1,6 @@ -import React from 'react' import { useNetworksQuery } from '@/dex/entities/networks' import { ComponentTestWrapper } from '@cy/support/helpers/ComponentTestWrapper' -import { createTestWagmiConfigFromVNet, createVirtualTestnet } from '@cy/support/helpers/tenderly' +import { createTenderlyWagmiConfigFromVNet, createVirtualTestnet } from '@cy/support/helpers/tenderly' import Box from '@mui/material/Box' import { ConnectionProvider } from '@ui-kit/features/connect-wallet/lib/ConnectionProvider' import { usePathname } from '@ui-kit/hooks/router' @@ -34,7 +33,7 @@ describe('RootLayout RPC Tests', () => { it(`redirects to arbitrum when the wallet is connected to it`, () => { cy.mount( - + , ) diff --git a/tests/cypress/e2e/all/header.cy.ts b/tests/cypress/e2e/all/header.cy.ts index 4bd5b9766c..7f2848d481 100644 --- a/tests/cypress/e2e/all/header.cy.ts +++ b/tests/cypress/e2e/all/header.cy.ts @@ -27,7 +27,7 @@ describe('Header', () => { viewport = oneDesktopViewport() cy.viewport(...viewport) route = oneAppRoute() - cy.visit(`/${route}`) + cy.visitWithoutTestConnector(route) waitIsLoaded(route) }) @@ -89,7 +89,7 @@ describe('Header', () => { viewport = oneMobileOrTabletViewport() cy.viewport(...viewport) route = oneAppRoute() - cy.visit(`/${route}`) + cy.visitWithoutTestConnector(route) waitIsLoaded(route) }) diff --git a/tests/cypress/e2e/lend/basic.cy.ts b/tests/cypress/e2e/lend/basic.cy.ts index 12f14aad79..740e0ea0a6 100644 --- a/tests/cypress/e2e/lend/basic.cy.ts +++ b/tests/cypress/e2e/lend/basic.cy.ts @@ -16,4 +16,9 @@ describe('Basic Access Test', () => { cy.url(LOAD_TIMEOUT).should('match', /http:\/\/localhost:\d+\/lend\/ethereum\/legal\/?\?tab=disclaimers$/) cy.title().should('equal', 'Legal - Curve Lend') }) + + it('should open a lend market page succesfully', () => { + cy.visit('/lend/ethereum/markets/0x23F5a668A9590130940eF55964ead9787976f2CC') // some WETH lend market on ethereum + cy.get('[data-testid^="detail-page-stack"]', LOAD_TIMEOUT).should('be.visible') + }) }) diff --git a/tests/cypress/e2e/loan/basic.cy.ts b/tests/cypress/e2e/loan/basic.cy.ts index d87f923de8..c9786bc94c 100644 --- a/tests/cypress/e2e/loan/basic.cy.ts +++ b/tests/cypress/e2e/loan/basic.cy.ts @@ -11,4 +11,9 @@ describe('Basic Access Test', () => { cy.title(LOAD_TIMEOUT).should('equal', 'Savings crvUSD - Curve') cy.url().should('match', /http:\/\/localhost:\d+\/crvusd\/ethereum\/scrvUSD\/?$/) }) + + it('should open a loan market page succesfully', () => { + cy.visit('/crvusd/ethereum/markets/WBTC') // some WBTC mint market on ethereum + cy.get('[data-testid^="detail-page-stack"]', LOAD_TIMEOUT).should('be.visible') + }) }) diff --git a/tests/cypress/e2e/main/basic.cy.ts b/tests/cypress/e2e/main/basic.cy.ts index 97e08cbdd4..1ddc7c613b 100644 --- a/tests/cypress/e2e/main/basic.cy.ts +++ b/tests/cypress/e2e/main/basic.cy.ts @@ -37,7 +37,7 @@ describe('Basic Access Test', () => { }) it('should load for lite networks', () => { - cy.visit('/dex/corn/pools') + cy.visitWithoutTestConnector('dex/corn/pools') cy.title(LOAD_TIMEOUT).should('equal', 'Pools - Curve') cy.url().should('include', '/dex/corn/pools') cy.contains(/LBTC\/wBTCN/i, LOAD_TIMEOUT).should('be.visible') diff --git a/tests/cypress/e2e/main/dex-markets.cy.ts b/tests/cypress/e2e/main/dex-markets.cy.ts index 6a7523b0df..aab6cb9c2e 100644 --- a/tests/cypress/e2e/main/dex-markets.cy.ts +++ b/tests/cypress/e2e/main/dex-markets.cy.ts @@ -4,7 +4,7 @@ import { setShowSmallPools } from '@cy/support/helpers/user-profile' import { API_LOAD_TIMEOUT, type Breakpoint, LOAD_TIMEOUT, oneViewport } from '@cy/support/ui' import { SMALL_POOL_TVL } from '@ui-kit/features/user-profile/store' -const PATH = '/dex/arbitrum/pools/' +const PATH = 'dex/arbitrum/pools/' // Parse compact USD strings like "$1.2M", "$950K", "$0", "-" function parseCompactUsd(value: string): number { @@ -30,7 +30,7 @@ function visitAndWait( ) { cy.viewport(width, height) const { query } = options ?? {} - cy.visit(`${PATH}${query ? `?${new URLSearchParams(query)}` : ''}`, options) + cy.visitWithoutTestConnector(`${PATH}${query ? `?${new URLSearchParams(query)}` : ''}`, options) cy.get('[data-testid^="data-table-row-"]', API_LOAD_TIMEOUT).should('have.length.greaterThan', 0) if (query?.['page']) { cy.get('[data-testid="table-pagination"]').should('be.visible') diff --git a/tests/cypress/e2e/main/pool-page.cy.ts b/tests/cypress/e2e/main/pool-page.cy.ts index daf0d3a74d..3b66da0d54 100644 --- a/tests/cypress/e2e/main/pool-page.cy.ts +++ b/tests/cypress/e2e/main/pool-page.cy.ts @@ -1,8 +1,9 @@ import { oneFloat, oneOf } from '@cy/support/generators' +import type { AppRoute } from '@cy/support/routes' import { LOAD_TIMEOUT } from '@cy/support/ui' describe('Pool page', () => { - const path = `/dex/${oneOf( + const path: AppRoute = `dex/${oneOf( 'ethereum/pools/factory-tricrypto-0', 'ethereum/pools/factory-stable-ng-561', 'arbitrum/pools/2pool', @@ -10,7 +11,7 @@ describe('Pool page', () => { )}/deposit` it('should update slippage settings', () => { - cy.visit(path) + cy.visitWithoutTestConnector(path) cy.get('[data-testid="tab-deposit"]', LOAD_TIMEOUT).should('have.class', 'Mui-selected') cy.get('[data-testid="borrow-slippage-value"]').contains(path.includes('crypto') ? '0.10%' : '0.03%') cy.get('[data-testid="slippage-settings-button"]').click() diff --git a/tests/cypress/e2e/main/swap.cy.ts b/tests/cypress/e2e/main/swap.cy.ts index 84c3a83299..2ad7eb3d09 100644 --- a/tests/cypress/e2e/main/swap.cy.ts +++ b/tests/cypress/e2e/main/swap.cy.ts @@ -5,7 +5,7 @@ describe('DEX Swap', () => { const TO_ETH = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE' it('shows quotes via router API when disconnected', () => { - cy.visit(`/dex/ethereum/swap?from=${FROM_USDT}&to=${TO_ETH}`) + cy.visitWithoutTestConnector(`dex/ethereum/swap?from=${FROM_USDT}&to=${TO_ETH}`) cy.get('[data-testid="btn-connect-wallet"]', LOAD_TIMEOUT).should('be.enabled') cy.get(`[data-testid="token-icon-${FROM_USDT}"]`, LOAD_TIMEOUT).should('be.visible') diff --git a/tests/cypress/support/e2e.ts b/tests/cypress/support/e2e.ts index 0ac96308ce..1f4e9e3e3e 100644 --- a/tests/cypress/support/e2e.ts +++ b/tests/cypress/support/e2e.ts @@ -1 +1,28 @@ -// This file is imported automatically before e2e tests run. Try to keep it empty if possible +import type { AppRoute } from './routes' + +declare global { + interface Window { + CypressNoTestConnector?: string + } + + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Cypress { + interface Chainable { + visitWithoutTestConnector(route: AppRoute, options?: Partial): Chainable + } + } +} + +/** + * For most of our e2e tests we have a wagmi test connect that auto-connects, so there's a wallet available. + * However, in some cases we want to test functionality without a wallet connected. + */ +Cypress.Commands.add('visitWithoutTestConnector', (route: AppRoute, options?: Partial) => + cy.visit(`/${route}`, { + ...options, + onBeforeLoad(win) { + win.CypressNoTestConnector = 'true' + options?.onBeforeLoad?.(win) + }, + }), +) diff --git a/tests/cypress/support/helpers/tenderly/connector.ts b/tests/cypress/support/helpers/tenderly/connector.ts new file mode 100644 index 0000000000..4556d8ccff --- /dev/null +++ b/tests/cypress/support/helpers/tenderly/connector.ts @@ -0,0 +1,33 @@ +import { custom, PrivateKeyAccount, type RpcTransactionRequest } from 'viem' +import type { TenderlyConfig } from '@cy/support/helpers/tenderly/account' +import { createTestConnector, CreateTestConnectorOptions } from '@ui-kit/features/connect-wallet/lib/wagmi/wagmi-test' +import { sendVnetTransaction } from './vnet-transaction' + +/** + * Creates a custom transport that intercepts JSON-RPC requests to handle account retrieval and + * transaction sending via Tenderly Virtual Testnet Admin API. + */ +const tenderlyTransport = (account: PrivateKeyAccount, tenderly: TenderlyConfig) => + custom({ + request: async ({ method, params: [param] }): Promise => { + if (method === 'eth_accounts') { + return [account.address] + } + if (method === 'eth_sendTransaction') { + return await sendVnetTransaction({ tenderly, tx: param as RpcTransactionRequest }).catch((err) => { + console.error(`Tenderly failed for ${method}(${JSON.stringify(param)}). Error: ${err}`) + throw err + }) + } + throw new Error(`Unsupported method: ${method}, http fallback is used`) + }, + }) + +/** Creates a wagmi test connector for Tenderly vnets in conjunction with component tests. */ +export const createTenderlyConnector = ({ + tenderly, + ...opts +}: Pick & { + /** Tenderly configuration for the Virtual Testnet */ + tenderly: TenderlyConfig +}) => createTestConnector({ ...opts, transport: (account) => tenderlyTransport(account, tenderly) }) diff --git a/tests/cypress/support/helpers/tenderly/index.ts b/tests/cypress/support/helpers/tenderly/index.ts index 0393ce68d6..e1515ea59f 100644 --- a/tests/cypress/support/helpers/tenderly/index.ts +++ b/tests/cypress/support/helpers/tenderly/index.ts @@ -1 +1 @@ -export { createVirtualTestnet, withVirtualTestnet, forkVirtualTestnet, createTestWagmiConfigFromVNet } from './vnet' +export { createVirtualTestnet, withVirtualTestnet, forkVirtualTestnet, createTenderlyWagmiConfigFromVNet } from './vnet' diff --git a/tests/cypress/support/helpers/tenderly/vnet.ts b/tests/cypress/support/helpers/tenderly/vnet.ts index 32ff517d87..38701ad437 100644 --- a/tests/cypress/support/helpers/tenderly/vnet.ts +++ b/tests/cypress/support/helpers/tenderly/vnet.ts @@ -1,7 +1,6 @@ import type { Hex } from 'viem' import { generatePrivateKey } from 'viem/accounts' import { DeepPartial } from '@ui-kit/types/util' -import { createTestWagmiConfig } from '../wagmi' import { tenderlyAccount } from './account' import { createVirtualTestnet as createVirtualTestnetRequest, @@ -19,6 +18,7 @@ import { type GetVirtualTestnetOptions, type GetVirtualTestnetResponse, } from './vnet-get' +import { createTenderlyWagmiConfig } from './wagmi' /** * Extracts the Admin and Public RPC URLs from a Tenderly virtual testnet response. @@ -31,7 +31,7 @@ export const getRpcUrls = ( publicRpcUrl: vnet.rpcs.find((rpc) => rpc.name === 'Public RPC')!.url, }) -export function createTestWagmiConfigFromVNet({ +export function createTenderlyWagmiConfigFromVNet({ vnet, privateKey = generatePrivateKey(), }: { @@ -39,7 +39,7 @@ export function createTestWagmiConfigFromVNet({ privateKey?: Hex }) { const { adminRpcUrl: rpcUrl, publicRpcUrl: explorerUrl } = getRpcUrls(vnet) - return createTestWagmiConfig({ + return createTenderlyWagmiConfig({ privateKey, rpcUrl, explorerUrl, diff --git a/tests/cypress/support/helpers/wagmi.ts b/tests/cypress/support/helpers/tenderly/wagmi.ts similarity index 75% rename from tests/cypress/support/helpers/wagmi.ts rename to tests/cypress/support/helpers/tenderly/wagmi.ts index 0f7fdbe25c..d60d079e2e 100644 --- a/tests/cypress/support/helpers/wagmi.ts +++ b/tests/cypress/support/helpers/tenderly/wagmi.ts @@ -2,7 +2,7 @@ import { defineChain, http, type Hex } from 'viem' import { mainnet, arbitrum } from 'viem/chains' import type { TenderlyConfig } from '@cy/support/helpers/tenderly/account' import { createWagmiConfig } from '@ui-kit/features/connect-wallet' -import { createTestConnector } from './test-connector' +import { createTenderlyConnector } from './connector' /** Configuration options for creating a test Wagmi config */ type Options = { @@ -18,13 +18,8 @@ type Options = { tenderly: TenderlyConfig } -/** - * Creates a Wagmi configuration for testing with a private key connector to a custom (testnet) RPC - * - * @param options - Configuration options - * @returns Configured Wagmi config instance for testing - */ -export function createTestWagmiConfig({ privateKey, rpcUrl, explorerUrl, chainId, tenderly }: Options) { +/** Creates a Wagmi configuration for testing with a private key connector to a custom Tenderly (testnet) RPC */ +export function createTenderlyWagmiConfig({ privateKey, rpcUrl, explorerUrl, chainId, tenderly }: Options) { // Tenderly docs recommends using chain ID 73571 to prevent replay attack, but a lot of our code relies on `if chain === Chain.Ethereum`. // Should be okay, since we're not using real life wallets. A browser wallet extension isn't even available. const chainDef = [mainnet, arbitrum].find((c) => c.id === +chainId) as typeof mainnet @@ -39,6 +34,6 @@ export function createTestWagmiConfig({ privateKey, rpcUrl, explorerUrl, chainId return createWagmiConfig({ chains: [chain], transports: { [chain.id]: http(rpcUrl) }, - connectors: [createTestConnector({ privateKey, chain, tenderly })], + connectors: [createTenderlyConnector({ privateKey, chain, tenderly })], }) }