diff --git a/.github/workflows/qa-playwright.yml b/.github/workflows/qa-playwright.yml new file mode 100644 index 00000000000..eace1374afa --- /dev/null +++ b/.github/workflows/qa-playwright.yml @@ -0,0 +1,89 @@ +name: Playwright QA + +on: + workflow_dispatch: + pull_request: + types: [opened, synchronize, reopened] + paths: + - apps/cowswap-frontend/** + - apps/cowswap-frontend-qa/** + - libs/** + - package.json + - pnpm-lock.yaml + - .github/workflows/qa-playwright.yml + +jobs: + qa-playwright: + name: Playwright QA + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + env: + CI: 'true' + FORK_BLOCK: '21000000' + HAS_MAINNET_RPC: ${{ secrets.MAINNET_RPC != '' }} + REACT_APP_NETWORK_URL_1: http://127.0.0.1:8545 + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Install pnpm + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + + - name: Set up node + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: 20 + cache: pnpm + + - name: Install dependencies + run: pnpm run install:ci --frozen-lockfile + + - name: Skip Playwright QA when MAINNET_RPC is unavailable + if: env.HAS_MAINNET_RPC != 'true' + run: echo "::notice::Skipping Playwright QA because MAINNET_RPC is not available for this workflow run." + + - name: Install Foundry + if: env.HAS_MAINNET_RPC == 'true' + uses: foundry-rs/foundry-toolchain@8789b3e21e6c11b2697f5eb56eddae542f746c10 # v1 + with: + version: stable + + - name: Cache Playwright browsers + if: env.HAS_MAINNET_RPC == 'true' + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml', 'apps/cowswap-frontend-qa/package.json') }} + restore-keys: | + ${{ runner.os }}-playwright- + + - name: Install Chromium + if: env.HAS_MAINNET_RPC == 'true' + run: pnpm --dir apps/cowswap-frontend-qa exec playwright install --with-deps chromium + + - name: Run Playwright QA + if: env.HAS_MAINNET_RPC == 'true' + env: + MAINNET_RPC: ${{ secrets.MAINNET_RPC }} + run: pnpm exec nx run cowswap-frontend-qa:e2e + + - name: Upload Playwright report + if: env.HAS_MAINNET_RPC == 'true' && failure() + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + with: + if-no-files-found: ignore + name: playwright-report + path: apps/cowswap-frontend-qa/playwright-report/ + + - name: Upload Playwright test results + if: env.HAS_MAINNET_RPC == 'true' && failure() + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + with: + if-no-files-found: ignore + name: playwright-test-results + path: apps/cowswap-frontend-qa/test-results/ diff --git a/apps/cowswap-frontend-qa/.gitignore b/apps/cowswap-frontend-qa/.gitignore new file mode 100644 index 00000000000..5a0b8a7f9e5 --- /dev/null +++ b/apps/cowswap-frontend-qa/.gitignore @@ -0,0 +1,3 @@ +playwright-report/ +test-results/ +.playwright/ diff --git a/apps/cowswap-frontend-qa/AGENTS.md b/apps/cowswap-frontend-qa/AGENTS.md new file mode 100644 index 00000000000..8b80f77157e --- /dev/null +++ b/apps/cowswap-frontend-qa/AGENTS.md @@ -0,0 +1,22 @@ +--- +author: agents +status: normative +last_reviewed: 2026-04-22 +--- + +# cowswap-frontend-qa AGENTS.md + +Root rules: [`../../AGENTS.md`](../../AGENTS.md) (global safety, workflow, and verification baseline). +This file: Playwright QA app-specific commands and caveats. + +## App commands + +- Run e2e: `pnpx nx run cowswap-frontend-qa:e2e` +- Run headed: `pnpx nx run cowswap-frontend-qa:e2e:headed` +- Lint: `pnpx nx run cowswap-frontend-qa:lint` +- Typecheck: `pnpx nx run cowswap-frontend-qa:typecheck` + +## Notes + +- Set `MAINNET_RPC` before running the QA lane locally. +- This project starts the `cowswap-frontend` preview server and an Anvil mainnet fork as part of the test run. diff --git a/apps/cowswap-frontend-qa/package.json b/apps/cowswap-frontend-qa/package.json new file mode 100644 index 00000000000..840a37570af --- /dev/null +++ b/apps/cowswap-frontend-qa/package.json @@ -0,0 +1,12 @@ +{ + "name": "@cowprotocol/cowswap-frontend-qa", + "version": "0.0.1", + "private": true, + "dependencies": { + "@cowprotocol/common-const": "workspace:*", + "viem": "^2.47.1" + }, + "devDependencies": { + "@playwright/test": "^1.59.1" + } +} diff --git a/apps/cowswap-frontend-qa/playwright.config.ts b/apps/cowswap-frontend-qa/playwright.config.ts new file mode 100644 index 00000000000..a71884e6e96 --- /dev/null +++ b/apps/cowswap-frontend-qa/playwright.config.ts @@ -0,0 +1,50 @@ +import { defineConfig, devices } from '@playwright/test' + +const ANVIL_RPC_URL = 'http://127.0.0.1:8545' + +export default defineConfig({ + testDir: './src/tests', + forbidOnly: Boolean(process.env.CI), + fullyParallel: false, + workers: 1, + timeout: 60_000, + expect: { + timeout: 20_000, + }, + reporter: [[process.env.CI ? 'line' : 'list'], ['html', { open: 'never' }]], + use: { + baseURL: 'http://127.0.0.1:3000', + trace: 'retain-on-failure', + screenshot: 'only-on-failure', + video: 'off', + viewport: { + width: 1440, + height: 900, + }, + }, + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + viewport: { + width: 1440, + height: 900, + }, + }, + }, + ], + webServer: { + command: 'pnpm exec nx run cowswap-frontend:preview:production', + env: { + ...process.env, + NX_DAEMON: 'false', + REACT_APP_ENABLE_QA_INJECTED_WALLET: 'true', + REACT_APP_NETWORK_URL_1: process.env.REACT_APP_NETWORK_URL_1 || ANVIL_RPC_URL, + REACT_APP_SERVICE_WORKER: 'false', + }, + url: 'http://127.0.0.1:3000', + timeout: 600_000, + reuseExistingServer: false, + }, +}) diff --git a/apps/cowswap-frontend-qa/project.json b/apps/cowswap-frontend-qa/project.json new file mode 100644 index 00000000000..02f5d7a0ad1 --- /dev/null +++ b/apps/cowswap-frontend-qa/project.json @@ -0,0 +1,37 @@ +{ + "name": "cowswap-frontend-qa", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "apps/cowswap-frontend-qa/src", + "projectType": "application", + "targets": { + "e2e": { + "executor": "nx:run-commands", + "options": { + "cwd": "apps/cowswap-frontend-qa", + "command": "playwright test" + } + }, + "e2e:headed": { + "executor": "nx:run-commands", + "options": { + "cwd": "apps/cowswap-frontend-qa", + "command": "playwright test --headed --project=chromium" + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["apps/cowswap-frontend-qa/**/*.{ts,js}"] + } + }, + "typecheck": { + "executor": "nx:run-commands", + "options": { + "command": "tsc --noEmit -p apps/cowswap-frontend-qa/tsconfig.json" + } + } + }, + "tags": [], + "implicitDependencies": ["cowswap-frontend"] +} diff --git a/apps/cowswap-frontend-qa/src/fixtures/anvil.ts b/apps/cowswap-frontend-qa/src/fixtures/anvil.ts new file mode 100644 index 00000000000..b3f3f205b7d --- /dev/null +++ b/apps/cowswap-frontend-qa/src/fixtures/anvil.ts @@ -0,0 +1,384 @@ +import { + createPublicClient, + encodeFunctionData, + erc20Abi, + formatEther, + http, + type Address, + type Hex, + toHex, +} from 'viem' +import { mainnet } from 'viem/chains' + +import { spawn, type ChildProcessByStdio } from 'node:child_process' +import { once } from 'node:events' + +import { MAINNET_COW_VAULT_RELAYER, MAINNET_WETH } from '../helpers/tokens' + +import type { Readable } from 'node:stream' + +const ANVIL_HOST = '127.0.0.1' +const ANVIL_PORT = 8545 +const ANVIL_CHAIN_ID = '0x1' +const DEFAULT_FORK_BLOCK = '21000000' +const START_TIMEOUT_MS = 30_000 +const STOP_TIMEOUT_MS = 5_000 + +type AnvilProcess = ChildProcessByStdio +type AnvilPublicClient = ReturnType + +interface JsonRpcResponse { + error?: { + code: number + message: string + } + result?: Result +} + +type SnapshotId = string + +const wethAbi = [ + { + inputs: [], + name: 'deposit', + outputs: [], + stateMutability: 'payable', + type: 'function', + }, +] as const + +export interface AnvilInstance { + dispose: () => Promise + url: string +} + +interface WrapEthToWethParams { + owner: Address + rpcUrl: string + amount: bigint +} + +interface ApproveWethParams { + amount: bigint + owner: Address + rpcUrl: string +} + +function logLocal(message: string): void { + if (!process.env.CI) { + process.stderr.write(`[cowswap-frontend-qa] ${message}\n`) + } +} + +function getMainnetRpc(): string { + const mainnetRpc = process.env.MAINNET_RPC + if (!mainnetRpc) { + throw new Error('MAINNET_RPC env missing') + } + + return mainnetRpc +} + +function getForkBlock(): string { + return process.env.FORK_BLOCK ?? DEFAULT_FORK_BLOCK +} + +function buildRpcUrl(): string { + return `http://${ANVIL_HOST}:${ANVIL_PORT}` +} + +function collectSecretsToRedact(mainnetRpc: string): string[] { + const secrets = new Set([mainnetRpc]) + + try { + const parsedUrl = new URL(mainnetRpc) + + if (parsedUrl.username) { + secrets.add(parsedUrl.username) + } + + if (parsedUrl.password) { + secrets.add(parsedUrl.password) + } + + const lastPathSegment = parsedUrl.pathname.split('/').filter(Boolean).at(-1) + if (lastPathSegment && lastPathSegment.length >= 8) { + secrets.add(lastPathSegment) + } + + for (const value of parsedUrl.searchParams.values()) { + if (value.length >= 8) { + secrets.add(value) + } + } + } catch { + // Keep the raw env value in the redact list even when URL parsing fails. + } + + return Array.from(secrets).sort((left, right) => right.length - left.length) +} + +function redactSecrets(value: string, secretsToRedact: string[]): string { + return secretsToRedact.reduce((result, secret) => { + return secret ? result.split(secret).join('[REDACTED]') : result + }, value) +} + +function formatBufferedOutput(outputBuffer: string[]): string { + const output = outputBuffer.join('').trim() + + return output ? `\nRecent anvil output:\n${output}` : '' +} + +function delay(timeoutMs: number): Promise { + return new Promise((resolve) => { + setTimeout(resolve, timeoutMs) + }) +} + +function getTransactionHash(result: unknown): Hex { + if (typeof result === 'string' && result.startsWith('0x')) { + return result as Hex + } + + throw new Error(`Expected a transaction hash from Anvil, received ${String(result)}`) +} + +function createRpcClient(rpcUrl: string): AnvilPublicClient { + return createPublicClient({ + chain: mainnet, + transport: http(rpcUrl), + }) +} + +async function waitForAnvil(url: string): Promise { + const deadline = Date.now() + START_TIMEOUT_MS + + while (Date.now() < deadline) { + try { + const response = await fetch(url, { + method: 'POST', + headers: { + 'content-type': 'application/json', + }, + body: JSON.stringify({ + id: 1, + jsonrpc: '2.0', + method: 'eth_chainId', + params: [], + }), + }) + + if (response.ok) { + const body = (await response.json()) as JsonRpcResponse + if (body.result === ANVIL_CHAIN_ID) { + return + } + } + } catch { + // Anvil is still booting. + } + + await delay(500) + } + + throw new Error(`Timed out waiting for Anvil at ${url}`) +} + +async function stopAnvilProcess(process: AnvilProcess): Promise { + if (process.exitCode !== null) return + + process.kill('SIGTERM') + + const exited = await Promise.race([once(process, 'exit').then(() => true), delay(STOP_TIMEOUT_MS).then(() => false)]) + + if (exited) return + + process.kill('SIGKILL') + await once(process, 'exit') +} + +export async function wrapEthToWeth({ owner, rpcUrl, amount }: WrapEthToWethParams): Promise { + const publicClient = createRpcClient(rpcUrl) + + logLocal(`Wrapping ${formatEther(amount)} ETH into WETH for ${owner}`) + + const hash = getTransactionHash( + await publicClient.request({ + method: 'eth_sendTransaction' as never, + params: [ + { + from: owner, + to: MAINNET_WETH, + value: toHex(amount), + data: encodeFunctionData({ + abi: wethAbi, + functionName: 'deposit', + }), + }, + ] as never, + }), + ) + + const receipt = await publicClient.waitForTransactionReceipt({ hash }) + if (receipt.status !== 'success') { + throw new Error(`WETH deposit transaction failed for ${owner}`) + } + + const wrappedBalance = await publicClient.readContract({ + address: MAINNET_WETH, + abi: erc20Abi, + functionName: 'balanceOf', + args: [owner], + }) + + if (wrappedBalance < amount) { + throw new Error( + `Expected wrapped WETH balance to be at least ${amount.toString()}, received ${wrappedBalance.toString()}`, + ) + } +} + +export async function approveWethForCowVaultRelayer({ owner, rpcUrl, amount }: ApproveWethParams): Promise { + const publicClient = createRpcClient(rpcUrl) + + const hash = getTransactionHash( + await publicClient.request({ + method: 'eth_sendTransaction' as never, + params: [ + { + from: owner, + to: MAINNET_WETH, + data: encodeFunctionData({ + abi: erc20Abi, + functionName: 'approve', + args: [MAINNET_COW_VAULT_RELAYER, amount], + }), + }, + ] as never, + }), + ) + + const receipt = await publicClient.waitForTransactionReceipt({ hash }) + if (receipt.status !== 'success') { + throw new Error(`WETH approval transaction failed for ${owner}`) + } + + const allowance = await publicClient.readContract({ + address: MAINNET_WETH, + abi: erc20Abi, + functionName: 'allowance', + args: [owner, MAINNET_COW_VAULT_RELAYER], + }) + + if (allowance < amount) { + throw new Error(`Expected WETH allowance to be at least ${amount.toString()}, received ${allowance.toString()}`) + } +} + +export async function createAnvilSnapshot(rpcUrl: string): Promise { + const publicClient = createRpcClient(rpcUrl) + const snapshotId = await publicClient.request({ + method: 'evm_snapshot' as never, + params: [] as never, + }) + + if (typeof snapshotId !== 'string' || !snapshotId) { + throw new Error(`Expected Anvil snapshot id to be a string, received ${String(snapshotId)}`) + } + + return snapshotId +} + +export async function revertAnvilSnapshot(rpcUrl: string, snapshotId: SnapshotId): Promise { + const publicClient = createRpcClient(rpcUrl) + const reverted = await publicClient.request({ + method: 'evm_revert' as never, + params: [snapshotId] as never, + }) + + if (reverted !== true) { + throw new Error(`Failed to revert Anvil snapshot ${snapshotId}`) + } +} + +export async function startAnvil(): Promise { + const url = buildRpcUrl() + const mainnetRpc = getMainnetRpc() + const secretsToRedact = collectSecretsToRedact(mainnetRpc) + const outputBuffer: string[] = [] + const forkBlock = getForkBlock() + + logLocal(`Starting Anvil mainnet fork on ${url} at block ${forkBlock}`) + + const process = spawn( + 'anvil', + [ + '--fork-url', + mainnetRpc, + '--fork-block-number', + forkBlock, + '--chain-id', + '1', + '--host', + ANVIL_HOST, + '--port', + String(ANVIL_PORT), + '--accounts', + '3', + '--balance', + '10000', + '--order', + 'fifo', + ], + { + stdio: ['ignore', 'pipe', 'pipe'], + }, + ) + + const captureOutput = (chunk: Buffer): void => { + outputBuffer.push(redactSecrets(chunk.toString(), secretsToRedact)) + if (outputBuffer.length > 20) { + outputBuffer.shift() + } + } + + process.stdout.on('data', captureOutput) + process.stderr.on('data', captureOutput) + + let started = false + + const earlyExit = new Promise((_, reject) => { + process.once('error', (error) => { + reject(new Error(redactSecrets(`Failed to spawn anvil: ${error.message}`, secretsToRedact))) + }) + process.once('exit', (code, signal) => { + if (!started) { + reject( + new Error( + `Anvil exited before becoming ready (code=${String(code)}, signal=${String(signal)})\n${outputBuffer.join('')}`, + ), + ) + } + }) + }) + + try { + await Promise.race([waitForAnvil(url), earlyExit]) + started = true + logLocal(`Anvil is ready on ${url}`) + } catch (error) { + await stopAnvilProcess(process) + + const reason = redactSecrets(error instanceof Error ? error.message : String(error), secretsToRedact) + throw new Error(`${reason}${formatBufferedOutput(outputBuffer)}`) + } + + return { + url, + dispose: async () => { + logLocal('Stopping Anvil') + await stopAnvilProcess(process) + }, + } +} diff --git a/apps/cowswap-frontend-qa/src/fixtures/browser.ts b/apps/cowswap-frontend-qa/src/fixtures/browser.ts new file mode 100644 index 00000000000..fa847af82c5 --- /dev/null +++ b/apps/cowswap-frontend-qa/src/fixtures/browser.ts @@ -0,0 +1,30 @@ +import type { Page } from '@playwright/test' + +const blockedUrlPatterns = [ + /googletagmanager\.com/i, + /google-analytics\.com/i, + /doubleclick\.net/i, + /ads-twitter\.com/i, + /clarity\.ms/i, + /telegram\.org/i, + /launchdarkly/i, +] as const + +export async function setupBrowser(page: Page): Promise { + await page.context().clearCookies() + + await page.route('**/*', (route) => { + const requestUrl = route.request().url() + + if (blockedUrlPatterns.some((pattern) => pattern.test(requestUrl))) { + return route.abort() + } + + return route.continue() + }) + + await page.addInitScript(() => { + window.localStorage.clear() + window.sessionStorage.clear() + }) +} diff --git a/apps/cowswap-frontend-qa/src/fixtures/index.ts b/apps/cowswap-frontend-qa/src/fixtures/index.ts new file mode 100644 index 00000000000..a8d031410ef --- /dev/null +++ b/apps/cowswap-frontend-qa/src/fixtures/index.ts @@ -0,0 +1,61 @@ +import { test as base, expect } from '@playwright/test' + +import { + approveWethForCowVaultRelayer, + createAnvilSnapshot, + revertAnvilSnapshot, + startAnvil, + wrapEthToWeth, +} from './anvil' +import { setupBrowser } from './browser' +import { installInjectedWallet, TEST_WALLET_ADDRESS } from './wallet' + +import type { Address } from 'viem' + +interface QaTestFixtures { + approveWethForCowVaultRelayer: (amount: bigint) => Promise + walletAddress: Address + wrapNativeToWeth: (amount: bigint) => Promise +} + +interface QaWorkerFixtures { + anvilUrl: string +} + +export const test = base.extend({ + anvilUrl: [ + async ({}, runFixture) => { + const anvil = await startAnvil() + + try { + await runFixture(anvil.url) + } finally { + await anvil.dispose() + } + }, + { scope: 'worker' }, + ], + walletAddress: async ({}, runFixture) => { + await runFixture(TEST_WALLET_ADDRESS) + }, + approveWethForCowVaultRelayer: async ({ anvilUrl, walletAddress }, runFixture) => { + await runFixture((amount) => approveWethForCowVaultRelayer({ amount, owner: walletAddress, rpcUrl: anvilUrl })) + }, + wrapNativeToWeth: async ({ anvilUrl, walletAddress }, runFixture) => { + await runFixture((amount) => wrapEthToWeth({ amount, owner: walletAddress, rpcUrl: anvilUrl })) + }, + page: async ({ anvilUrl, page }, runFixture) => { + const snapshotId = await createAnvilSnapshot(anvilUrl) + + await setupBrowser(page) + await installInjectedWallet(page, { rpcUrl: anvilUrl }) + + try { + await runFixture(page) + } finally { + await revertAnvilSnapshot(anvilUrl, snapshotId) + } + }, +}) + +export { expect } diff --git a/apps/cowswap-frontend-qa/src/fixtures/wallet.ts b/apps/cowswap-frontend-qa/src/fixtures/wallet.ts new file mode 100644 index 00000000000..d68558e54f7 --- /dev/null +++ b/apps/cowswap-frontend-qa/src/fixtures/wallet.ts @@ -0,0 +1,260 @@ +import { createPublicClient, http, type Hex } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' +import { mainnet } from 'viem/chains' + +import type { Page } from '@playwright/test' + +const REQUEST_BRIDGE_NAME = '__cowQaWalletRequest' +const TEST_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' as Hex +const TEST_ACCOUNT = privateKeyToAccount(TEST_PRIVATE_KEY) +const CHAIN_ID = 1 +const CHAIN_ID_HEX = '0x1' +const NOT_HANDLED = Symbol('NOT_HANDLED') + +export const TEST_WALLET_ADDRESS = TEST_ACCOUNT.address + +type WalletPublicClient = ReturnType +type WalletState = { + connected: boolean +} + +interface ProviderRpcError extends Error { + code: number +} + +interface WalletInstallOptions { + rpcUrl: string +} + +interface WalletRpcRequest { + method: string + params?: unknown[] +} + +function createProviderRpcError(code: number, message: string): ProviderRpcError { + const error = new Error(message) as ProviderRpcError + error.code = code + return error +} + +function toRecord(value: unknown): Record { + if (typeof value === 'object' && value !== null) { + return value as Record + } + + return {} +} + +function normalizeChainId(chainId: string): string { + try { + return `0x${BigInt(chainId).toString(16)}` + } catch { + return chainId.toLowerCase() + } +} + +function getRequestedChainId(value: unknown): string | null { + const record = toRecord(value) + return typeof record.chainId === 'string' ? normalizeChainId(record.chainId) : null +} + +function withDefaultFrom(value: unknown, address: string): Record { + const record = toRecord(value) + + return { + ...record, + from: typeof record.from === 'string' ? record.from : address, + } +} + +function handleWalletAuthorizationRequest( + accountAddress: string, + state: WalletState, + method: string, +): typeof NOT_HANDLED | string[] { + switch (method) { + case 'eth_requestAccounts': + state.connected = true + return [accountAddress] + case 'eth_accounts': + return state.connected ? [accountAddress] : [] + default: + return NOT_HANDLED + } +} + +function handleWalletNetworkRequest(method: string, params: unknown[]): typeof NOT_HANDLED | string | null { + switch (method) { + case 'eth_chainId': + return CHAIN_ID_HEX + case 'net_version': + return String(CHAIN_ID) + case 'wallet_switchEthereumChain': { + const requestedChainId = getRequestedChainId(params[0]) + + if (requestedChainId === CHAIN_ID_HEX) { + return null + } + + throw createProviderRpcError(4902, `Unsupported chain ${requestedChainId ?? 'unknown'}`) + } + case 'wallet_addEthereumChain': + return null + default: + return NOT_HANDLED + } +} + +async function handleWalletRpcRequest( + publicClient: WalletPublicClient, + accountAddress: string, + { method, params = [] }: WalletRpcRequest, +): Promise { + switch (method) { + case 'eth_sendTransaction': + return publicClient.request({ + method: 'eth_sendTransaction' as never, + params: [withDefaultFrom(params[0], accountAddress)] as never, + }) + case 'eth_call': { + const blockTag = typeof params[1] === 'string' ? params[1] : 'latest' + + return publicClient.request({ + method: 'eth_call' as never, + params: [withDefaultFrom(params[0], accountAddress), blockTag] as never, + }) + } + case 'eth_estimateGas': + return publicClient.request({ + method: 'eth_estimateGas' as never, + params: [withDefaultFrom(params[0], accountAddress)] as never, + }) + case 'eth_sign': + case 'personal_sign': + case 'eth_signTypedData': + case 'eth_signTypedData_v4': + return publicClient.request({ + method: method as never, + params: params as never, + }) + default: + return publicClient.request({ + method: method as never, + params: params as never, + }) + } +} + +function createWalletRequestHandler(publicClient: WalletPublicClient, accountAddress: string) { + const state: WalletState = { connected: false } + + return async (request: WalletRpcRequest): Promise => { + const authorizationResult = handleWalletAuthorizationRequest(accountAddress, state, request.method) + if (authorizationResult !== NOT_HANDLED) { + return authorizationResult + } + + const networkResult = handleWalletNetworkRequest(request.method, request.params ?? []) + if (networkResult !== NOT_HANDLED) { + return networkResult + } + + return handleWalletRpcRequest(publicClient, accountAddress, request) + } +} + +function initializeInjectedWalletProvider(initData: { + address: string + chainId: string + requestBridgeName: string +}): void { + type Listener = (...args: unknown[]) => void + type RpcRequest = { method: string; params?: unknown[] } + type BridgeWindow = Window & + typeof globalThis & { + [key: string]: ((request: RpcRequest) => Promise) | unknown + ethereum?: unknown + } + + const listeners = new Map>() + const emit = (eventName: string, ...args: unknown[]): void => { + listeners.get(eventName)?.forEach((listener) => listener(...args)) + } + const provider = { + autoRefreshOnNetworkChange: false, + chainId: initData.chainId, + isMetaMask: false, + providers: [] as unknown[], + selectedAddress: null as string | null, + connected: false, + addListener(eventName: string, listener: Listener) { + return this.on(eventName, listener) + }, + emit, + isConnected() { + return this.connected + }, + off(eventName: string, listener: Listener) { + return this.removeListener(eventName, listener) + }, + on(eventName: string, listener: Listener) { + const currentListeners = listeners.get(eventName) ?? new Set() + currentListeners.add(listener) + listeners.set(eventName, currentListeners) + return this + }, + removeListener(eventName: string, listener: Listener) { + listeners.get(eventName)?.delete(listener) + return this + }, + async request({ method, params = [] }: RpcRequest): Promise { + const bridge = (window as BridgeWindow)[initData.requestBridgeName] + if (typeof bridge !== 'function') + throw new Error(`Wallet bridge "${initData.requestBridgeName}" is not available`) + + const result = await bridge({ method, params }) + if (method === 'eth_requestAccounts') { + this.connected = true + this.selectedAddress = initData.address + emit('accountsChanged', result) + emit('connect', { chainId: initData.chainId }) + } + if (method === 'eth_accounts' && Array.isArray(result) && result.length > 0) { + this.connected = true + this.selectedAddress = initData.address + } + if (method === 'wallet_switchEthereumChain') { + emit('chainChanged', initData.chainId) + } + + return result + }, + } + + provider.providers = [provider] + Object.defineProperty(window as BridgeWindow, 'ethereum', { + configurable: true, + enumerable: true, + value: provider, + writable: true, + }) + window.dispatchEvent(new Event('ethereum#initialized')) +} + +async function installBrowserInjectedWallet(page: Page, accountAddress: string): Promise { + await page.addInitScript(initializeInjectedWalletProvider, { + address: accountAddress, + chainId: CHAIN_ID_HEX, + requestBridgeName: REQUEST_BRIDGE_NAME, + }) +} + +export async function installInjectedWallet(page: Page, { rpcUrl }: WalletInstallOptions): Promise { + const publicClient = createPublicClient({ + chain: mainnet, + transport: http(rpcUrl), + }) + + await page.exposeFunction(REQUEST_BRIDGE_NAME, createWalletRequestHandler(publicClient, TEST_WALLET_ADDRESS)) + await installBrowserInjectedWallet(page, TEST_WALLET_ADDRESS) +} diff --git a/apps/cowswap-frontend-qa/src/helpers/approvalProof.ts b/apps/cowswap-frontend-qa/src/helpers/approvalProof.ts new file mode 100644 index 00000000000..dbb532f544b --- /dev/null +++ b/apps/cowswap-frontend-qa/src/helpers/approvalProof.ts @@ -0,0 +1,131 @@ +import { expect, type TestInfo } from '@playwright/test' +import { formatUnits, parseEther, type Address } from 'viem' + +import { + getCurrentBlockNumber, + getErc20ApprovalLogsForOwner, + readErc20Allowance, + type Erc20ApprovalLog, +} from './onchain' +import { MAINNET_CHAIN_ID, MAINNET_WETH } from './tokens' + +const APPROVAL_SELL_AMOUNT = parseEther('1') + +interface WethApprovalProofParams { + anvilUrl: string + owner: Address +} + +interface AssertWethApprovalProofParams extends WethApprovalProofParams { + baseline: WethApprovalProofBaseline + testInfo: TestInfo +} + +export interface WethApprovalProofBaseline { + fromBlock: bigint + initialAllowance: bigint +} + +function formatWethAmount(value: bigint): string { + return `${formatUnits(value, 18)} WETH` +} + +function getLatestSufficientApprovalLog(logs: Erc20ApprovalLog[]): Erc20ApprovalLog | undefined { + for (let index = logs.length - 1; index >= 0; index -= 1) { + const log = logs[index] + + if (log.value >= APPROVAL_SELL_AMOUNT) { + return log + } + } + + return undefined +} + +export async function createWethApprovalProofBaseline({ + anvilUrl, + owner, +}: WethApprovalProofParams): Promise { + const fromBlock = await getCurrentBlockNumber(anvilUrl) + + const existingApprovalLogs = await getErc20ApprovalLogsForOwner({ + fromBlock, + owner, + rpcUrl: anvilUrl, + token: MAINNET_WETH, + }) + + expect(existingApprovalLogs).toHaveLength(0) + + return { fromBlock, initialAllowance: 0n } +} + +export async function assertWethApprovalProof({ + anvilUrl, + baseline, + owner, + testInfo, +}: AssertWethApprovalProofParams): Promise { + await expect + .poll( + async () => { + const approvalLogs = await getErc20ApprovalLogsForOwner({ + fromBlock: baseline.fromBlock, + owner, + rpcUrl: anvilUrl, + token: MAINNET_WETH, + }) + + return approvalLogs.some((log) => log.value >= APPROVAL_SELL_AMOUNT) + }, + { message: 'Expected a WETH Approval event on the Anvil fork', timeout: 30_000 }, + ) + .toBe(true) + + const approvalLogs = await getErc20ApprovalLogsForOwner({ + fromBlock: baseline.fromBlock, + owner, + rpcUrl: anvilUrl, + token: MAINNET_WETH, + }) + const latestApprovalLog = getLatestSufficientApprovalLog(approvalLogs) + + if (!latestApprovalLog) { + throw new Error('Expected a WETH Approval event on the Anvil fork') + } + + const approvedAllowance = await readErc20Allowance({ + owner, + rpcUrl: anvilUrl, + spender: latestApprovalLog.spender, + token: MAINNET_WETH, + }) + + expect(approvedAllowance).toBeGreaterThanOrEqual(APPROVAL_SELL_AMOUNT) + + await testInfo.attach('on-chain-approval-proof', { + contentType: 'application/json', + body: JSON.stringify( + { + chainId: MAINNET_CHAIN_ID, + forkRpc: anvilUrl, + owner, + spender: latestApprovalLog.spender, + token: MAINNET_WETH, + initialAllowance: baseline.initialAllowance.toString(), + approvedAllowance: approvedAllowance.toString(), + approvedAllowanceFormatted: formatWethAmount(approvedAllowance), + requiredAllowance: APPROVAL_SELL_AMOUNT.toString(), + requiredAllowanceFormatted: formatWethAmount(APPROVAL_SELL_AMOUNT), + approvalEvent: { + blockNumber: latestApprovalLog.blockNumber.toString(), + transactionHash: latestApprovalLog.transactionHash, + value: latestApprovalLog.value.toString(), + valueFormatted: formatWethAmount(latestApprovalLog.value), + }, + }, + null, + 2, + ), + }) +} diff --git a/apps/cowswap-frontend-qa/src/helpers/onchain.ts b/apps/cowswap-frontend-qa/src/helpers/onchain.ts new file mode 100644 index 00000000000..d7819afd49d --- /dev/null +++ b/apps/cowswap-frontend-qa/src/helpers/onchain.ts @@ -0,0 +1,107 @@ +import { createPublicClient, erc20Abi, http, parseAbiItem, type Address, type Hex, type PublicClient } from 'viem' +import { mainnet } from 'viem/chains' + +interface Erc20AllowanceParams { + owner: Address + rpcUrl: string + spender: Address + token: Address +} + +interface Erc20ApprovalLogsParams extends Erc20AllowanceParams { + fromBlock: bigint +} + +interface Erc20ApprovalLogsForOwnerParams { + fromBlock: bigint + owner: Address + rpcUrl: string + token: Address +} + +export interface Erc20ApprovalLog { + blockNumber: bigint + spender: Address + transactionHash: Hex + value: bigint +} + +const ERC20_APPROVAL_EVENT = parseAbiItem( + 'event Approval(address indexed owner, address indexed spender, uint256 value)', +) + +function createAnvilClient(rpcUrl: string): PublicClient { + return createPublicClient({ + chain: mainnet, + transport: http(rpcUrl), + }) +} + +export async function getCurrentBlockNumber(rpcUrl: string): Promise { + return createAnvilClient(rpcUrl).getBlockNumber() +} + +export async function readErc20Allowance({ owner, rpcUrl, spender, token }: Erc20AllowanceParams): Promise { + return createAnvilClient(rpcUrl).readContract({ + address: token, + abi: erc20Abi, + functionName: 'allowance', + args: [owner, spender], + }) +} + +export async function getErc20ApprovalLogsForOwner({ + fromBlock, + owner, + rpcUrl, + token, +}: Erc20ApprovalLogsForOwnerParams): Promise { + const logs = await createAnvilClient(rpcUrl).getLogs({ + address: token, + event: ERC20_APPROVAL_EVENT, + args: { owner }, + fromBlock, + toBlock: 'latest', + }) + + return logs.map((log) => { + const blockNumber = log.blockNumber + const spender = log.args.spender + const transactionHash = log.transactionHash + const value = log.args.value + + if (blockNumber === null || typeof spender !== 'string' || transactionHash === null || typeof value !== 'bigint') { + throw new Error('Received incomplete ERC20 Approval log from Anvil') + } + + return { blockNumber, spender, transactionHash, value } + }) +} + +export async function getErc20ApprovalLogs({ + fromBlock, + owner, + rpcUrl, + spender, + token, +}: Erc20ApprovalLogsParams): Promise { + const logs = await createAnvilClient(rpcUrl).getLogs({ + address: token, + event: ERC20_APPROVAL_EVENT, + args: { owner, spender }, + fromBlock, + toBlock: 'latest', + }) + + return logs.map((log) => { + const blockNumber = log.blockNumber + const transactionHash = log.transactionHash + const value = log.args.value + + if (blockNumber === null || transactionHash === null || typeof value !== 'bigint') { + throw new Error('Received incomplete ERC20 Approval log from Anvil') + } + + return { blockNumber, spender, transactionHash, value } + }) +} diff --git a/apps/cowswap-frontend-qa/src/helpers/selectors.ts b/apps/cowswap-frontend-qa/src/helpers/selectors.ts new file mode 100644 index 00000000000..b57fc9eedf0 --- /dev/null +++ b/apps/cowswap-frontend-qa/src/helpers/selectors.ts @@ -0,0 +1,22 @@ +export const walletSelectors = { + connectWalletButton: '#connect-wallet', + connectedWalletStatus: '#web3-status-connected', +} as const + +export const ethFlowSelectors = { + banner: '#classic-eth-flow-banner', + wrapNativeButton: '#wrap-native', +} as const + +export const swapSelectors = { + approveTradeButton: '#approve-trade-button', + buyAmountInput: '#output-currency-input .token-amount-input', + sellAmountInput: '#input-currency-input .token-amount-input', + swapActionButton: '#do-trade-button', + tradeConfirmationButton: '#trade-confirmation > button', + unlockCrossChainSwapButton: '#unlock-cross-chain-swap-btn', +} as const + +export const accountSelectors = { + accountIdentifierRow: '#web3-account-identifier-row', +} as const diff --git a/apps/cowswap-frontend-qa/src/helpers/tokens.ts b/apps/cowswap-frontend-qa/src/helpers/tokens.ts new file mode 100644 index 00000000000..f9d2ec61874 --- /dev/null +++ b/apps/cowswap-frontend-qa/src/helpers/tokens.ts @@ -0,0 +1,38 @@ +export const MAINNET_CHAIN_ID = 1 +export const MAINNET_ETH = 'ETH' +export const MAINNET_COW_VAULT_RELAYER = '0xC92E8bdf79f0507f65a392b0ab4667716BFE0110' +export const MAINNET_USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' +export const MAINNET_WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' + +export type MainnetCurrencyId = string + +export interface MainnetSwapRouteParams { + buyAmount?: string + orderKind?: 'buy' | 'sell' + sellAmount?: string +} + +export function buildMainnetSwapRoute( + sellToken: MainnetCurrencyId, + buyToken: MainnetCurrencyId, + params: MainnetSwapRouteParams = {}, +): string { + const searchParams = new URLSearchParams() + + if (params.sellAmount) { + searchParams.set('sellAmount', params.sellAmount) + } + + if (params.buyAmount) { + searchParams.set('buyAmount', params.buyAmount) + } + + if (params.orderKind) { + searchParams.set('orderKind', params.orderKind) + } + + const route = `/#/${MAINNET_CHAIN_ID}/swap/${sellToken}/${buyToken}` + const queryString = searchParams.toString() + + return queryString ? `${route}?${queryString}` : route +} diff --git a/apps/cowswap-frontend-qa/src/helpers/trade.ts b/apps/cowswap-frontend-qa/src/helpers/trade.ts new file mode 100644 index 00000000000..aaf4aeb3a61 --- /dev/null +++ b/apps/cowswap-frontend-qa/src/helpers/trade.ts @@ -0,0 +1,74 @@ +import { QA_CONNECT_INJECTED_WALLET_EVENT, QA_FORCE_ONCHAIN_APPROVAL_SESSION_KEY } from '@cowprotocol/common-const/qa' + +import { expect, type Page } from '@playwright/test' + +import { accountSelectors, ethFlowSelectors, swapSelectors, walletSelectors } from './selectors' + +export async function forceOnchainApproval(page: Page): Promise { + await page.addInitScript((sessionKey) => { + window.sessionStorage.setItem(sessionKey, 'true') + }, QA_FORCE_ONCHAIN_APPROVAL_SESSION_KEY) +} + +export async function connectInjectedWallet(page: Page): Promise { + const connectedWalletStatus = page.locator(walletSelectors.connectedWalletStatus) + + if (await connectedWalletStatus.isVisible()) { + return + } + + await expect(page.locator(walletSelectors.connectWalletButton)).toBeVisible() + await page.evaluate((eventName) => { + document.dispatchEvent(new CustomEvent(eventName)) + }, QA_CONNECT_INJECTED_WALLET_EVENT) + + await expect(connectedWalletStatus).toBeVisible() +} + +export async function unlockCrossChainSwapIfPresent(page: Page): Promise { + const unlockCrossChainSwapButton = page.locator(swapSelectors.unlockCrossChainSwapButton) + + if (await unlockCrossChainSwapButton.isVisible()) { + await unlockCrossChainSwapButton.click() + } +} + +export async function expectSellAmount(page: Page, amount: string): Promise { + await unlockCrossChainSwapIfPresent(page) + + const sellAmountInput = page.locator(swapSelectors.sellAmountInput) + await expect(sellAmountInput).toBeVisible() + await expect(sellAmountInput).toHaveValue(amount) +} + +export async function openNativeWrapModal(page: Page): Promise { + await unlockCrossChainSwapIfPresent(page) + + const wrapNativeButton = page.locator(ethFlowSelectors.wrapNativeButton) + if (await wrapNativeButton.isVisible()) { + await wrapNativeButton.click() + return + } + + const ethFlowBanner = page.locator(ethFlowSelectors.banner) + for (let attempt = 0; attempt < 3; attempt++) { + try { + await ethFlowBanner.waitFor({ state: 'visible', timeout: 5_000 }) + await ethFlowBanner.click() + await wrapNativeButton.waitFor({ state: 'visible', timeout: 5_000 }) + await wrapNativeButton.click() + return + } catch { + // The banner may render shortly after the quote; retry before failing the native-flow path. + } + } + + await expect(wrapNativeButton).toBeVisible() + await wrapNativeButton.click() +} + +export async function openAccountModal(page: Page): Promise { + await page.locator(walletSelectors.connectedWalletStatus).click() + await expect(page.locator(accountSelectors.accountIdentifierRow)).toBeVisible() + await expect(page.getByText('Your total surplus')).toBeVisible() +} diff --git a/apps/cowswap-frontend-qa/src/helpers/wait.ts b/apps/cowswap-frontend-qa/src/helpers/wait.ts new file mode 100644 index 00000000000..421eed11581 --- /dev/null +++ b/apps/cowswap-frontend-qa/src/helpers/wait.ts @@ -0,0 +1,24 @@ +import { expect, type Locator } from '@playwright/test' + +function parseNumericValue(value: string): number { + const normalized = value.replaceAll(',', '').trim() + + if (!normalized) { + return 0 + } + + return Number.parseFloat(normalized) +} + +export async function waitForNonZeroInputValue(locator: Locator): Promise { + await expect + .poll( + async () => { + return parseNumericValue(await locator.inputValue()) + }, + { message: 'Expected quote output to become a non-zero number' }, + ) + .toBeGreaterThan(0) + + return parseNumericValue(await locator.inputValue()) +} diff --git a/apps/cowswap-frontend-qa/src/tests/approval.smoke.spec.ts b/apps/cowswap-frontend-qa/src/tests/approval.smoke.spec.ts new file mode 100644 index 00000000000..d34867de04d --- /dev/null +++ b/apps/cowswap-frontend-qa/src/tests/approval.smoke.spec.ts @@ -0,0 +1,49 @@ +import { parseEther } from 'viem' + +import { expect, test } from '../fixtures' +import { assertWethApprovalProof, createWethApprovalProofBaseline } from '../helpers/approvalProof' +import { swapSelectors } from '../helpers/selectors' +import { buildMainnetSwapRoute, MAINNET_USDC, MAINNET_WETH } from '../helpers/tokens' +import { connectInjectedWallet, expectSellAmount, forceOnchainApproval } from '../helpers/trade' +import { waitForNonZeroInputValue } from '../helpers/wait' + +test('approval smoke: mainnet WETH approval persists and unlocks swap after reload', async ({ + anvilUrl, + page, + walletAddress, + wrapNativeToWeth, +}, testInfo) => { + test.setTimeout(120_000) + + await forceOnchainApproval(page) + await wrapNativeToWeth(parseEther('2')) + await page.goto(buildMainnetSwapRoute(MAINNET_WETH, MAINNET_USDC, { orderKind: 'sell', sellAmount: '1' })) + + await connectInjectedWallet(page) + await expectSellAmount(page, '1') + await waitForNonZeroInputValue(page.locator(swapSelectors.buyAmountInput)) + + const approvalProofBaseline = await createWethApprovalProofBaseline({ anvilUrl, owner: walletAddress }) + + const approveTradeButton = page.locator(swapSelectors.approveTradeButton) + await expect(approveTradeButton).toBeVisible() + await expect(approveTradeButton).toContainText('Approve and Swap') + await approveTradeButton.click() + + await assertWethApprovalProof({ anvilUrl, baseline: approvalProofBaseline, owner: walletAddress, testInfo }) + + const tradeConfirmationButton = page.locator(swapSelectors.tradeConfirmationButton) + await expect(tradeConfirmationButton).toBeVisible({ timeout: 30_000 }) + await expect(tradeConfirmationButton).toContainText('Confirm Swap') + + await page.reload() + + await connectInjectedWallet(page) + await expectSellAmount(page, '1') + await waitForNonZeroInputValue(page.locator(swapSelectors.buyAmountInput)) + + const swapActionButton = page.locator(swapSelectors.swapActionButton) + await expect(swapActionButton).toBeVisible({ timeout: 30_000 }) + await expect(swapActionButton).toContainText('Swap') + await expect(approveTradeButton).not.toBeVisible() +}) diff --git a/apps/cowswap-frontend-qa/src/tests/order-submission.smoke.spec.ts b/apps/cowswap-frontend-qa/src/tests/order-submission.smoke.spec.ts new file mode 100644 index 00000000000..2e95a2f7afe --- /dev/null +++ b/apps/cowswap-frontend-qa/src/tests/order-submission.smoke.spec.ts @@ -0,0 +1,276 @@ +import { getAddress, isAddressEqual, type Address } from 'viem' + +import { expect, test } from '../fixtures' +import { swapSelectors } from '../helpers/selectors' +import { MAINNET_USDC, MAINNET_WETH, buildMainnetSwapRoute } from '../helpers/tokens' +import { connectInjectedWallet, expectSellAmount, openAccountModal } from '../helpers/trade' +import { waitForNonZeroInputValue } from '../helpers/wait' + +import type { Page, TestInfo } from '@playwright/test' + +const MOCK_ORDER_UID = `0x${'ab'.repeat(56)}` +const ONE_WETH_IN_WEI = '1000000000000000000' +const MOCK_USDC_BUY_AMOUNT = '2300000000' + +interface SubmittedOrderBody { + appData?: unknown + appDataHash?: unknown + buyToken?: unknown + from?: unknown + kind?: unknown + receiver?: unknown + sellAmount?: unknown + sellToken?: unknown + signature?: unknown + signingScheme?: unknown +} + +interface QuoteRequestBody extends SubmittedOrderBody { + appData?: unknown + appDataHash?: unknown + priceQuality?: unknown + sellAmountBeforeFee?: unknown +} + +interface OrderSubmissionCapture { + submittedOrderBody: SubmittedOrderBody | null + uploadedAppDataDocument: Record | null + uploadedAppDataHash: string | null +} + +function toRecord(value: unknown): Record { + if (typeof value === 'object' && value !== null) { + return value as Record + } + + throw new Error(`Expected an object payload, received ${typeof value}`) +} + +function isSameAddress(value: unknown, address: string): boolean { + try { + return typeof value === 'string' && isAddressEqual(getAddress(value), getAddress(address)) + } catch { + return false + } +} + +function expectSameAddress(value: unknown, address: string, fieldName: string): void { + expect(isSameAddress(value, address), `${fieldName} should match ${address}`).toBe(true) +} + +function getStringOrFallback(value: unknown, fallback: string): string { + return typeof value === 'string' ? value : fallback +} + +function getFullAppData(value: Record): string | null { + const fullAppData = value.fullAppData + + return typeof fullAppData === 'string' ? fullAppData : null +} + +function getNormalizedAddress(value: string): Address { + return getAddress(value) +} + +async function clickSwapAction(page: Page): Promise { + const swapActionButton = page.locator(`${swapSelectors.swapActionButton}, ${swapSelectors.approveTradeButton}`) + + await expect(swapActionButton).toBeVisible({ timeout: 30_000 }) + await expect(swapActionButton).toContainText('Swap') + await expect(swapActionButton).toBeEnabled() + await swapActionButton.click() +} + +async function interceptDeterministicWrappedQuote(page: Page, walletAddress: string): Promise { + await page.route('**/mainnet/api/v1/quote', async (route) => { + const body = toRecord(route.request().postDataJSON()) as QuoteRequestBody + + if ( + body.kind !== 'sell' || + body.sellAmountBeforeFee !== ONE_WETH_IN_WEI || + !isSameAddress(body.sellToken, MAINNET_WETH) || + !isSameAddress(body.buyToken, MAINNET_USDC) + ) { + await route.continue() + return + } + + const appData = getStringOrFallback(body.appData, '{}') + const appDataHash = getStringOrFallback(body.appDataHash, `0x${'00'.repeat(32)}`) + const receiver = getStringOrFallback(body.receiver, walletAddress) + const signingScheme = getStringOrFallback(body.signingScheme, 'eip712') + const now = Math.floor(Date.now() / 1000) + + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + quote: { + sellToken: getNormalizedAddress(MAINNET_WETH), + buyToken: getNormalizedAddress(MAINNET_USDC), + receiver: getNormalizedAddress(receiver), + sellAmount: ONE_WETH_IN_WEI, + buyAmount: MOCK_USDC_BUY_AMOUNT, + validTo: now + 1800, + appData, + appDataHash, + feeAmount: '0', + gasAmount: '0', + gasPrice: '0', + sellTokenPrice: '1', + kind: 'sell', + partiallyFillable: false, + sellTokenBalance: 'erc20', + buyTokenBalance: 'erc20', + signingScheme, + }, + from: getNormalizedAddress(walletAddress), + expiration: new Date((now + 60) * 1000).toISOString(), + id: null, + verified: false, + protocolFeeBps: '0', + }), + }) + }) +} + +async function interceptWrappedOrderSubmission( + page: Page, + walletAddress: string, + capture: OrderSubmissionCapture, +): Promise { + await page.route('**/api/v1/app_data/*', async (route) => { + const request = route.request() + + if (request.method() !== 'PUT') { + await route.continue() + return + } + + expect(request.method()).toBe('PUT') + + capture.uploadedAppDataHash = request.url().split('/').at(-1) ?? null + capture.uploadedAppDataDocument = toRecord(request.postDataJSON()) + + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: '{}', + }) + }) + + await page.route('**/api/v1/orders', async (route) => { + const request = route.request() + + expect(request.method()).toBe('POST') + + const body = toRecord(request.postDataJSON()) + capture.submittedOrderBody = body + + expectSameAddress(body.sellToken, MAINNET_WETH, 'sellToken') + expectSameAddress(body.buyToken, MAINNET_USDC, 'buyToken') + expect(body.kind).toBe('sell') + expectSameAddress(body.from, walletAddress, 'from') + expectSameAddress(body.receiver, walletAddress, 'receiver') + expect(body.sellAmount).toBe(ONE_WETH_IN_WEI) + expect(body.signingScheme).toBe('eip712') + expect(typeof body.signature).toBe('string') + expect((body.signature as string).length).toBeGreaterThan(0) + + if (capture.uploadedAppDataHash) { + expect(body.appDataHash).toBe(capture.uploadedAppDataHash) + } + + const uploadedFullAppData = capture.uploadedAppDataDocument ? getFullAppData(capture.uploadedAppDataDocument) : null + if (uploadedFullAppData) { + expect(body.appData).toBe(uploadedFullAppData) + } + + await route.fulfill({ + status: 201, + contentType: 'application/json', + body: JSON.stringify(MOCK_ORDER_UID), + }) + }) +} + +async function attachOrderSubmissionProof( + testInfo: TestInfo, + { submittedOrderBody, uploadedAppDataDocument, uploadedAppDataHash }: OrderSubmissionCapture, +): Promise { + await testInfo.attach('order-submission-proof', { + body: Buffer.from( + JSON.stringify( + { + appDataHash: uploadedAppDataHash, + orderUid: MOCK_ORDER_UID, + submittedOrderBody, + uploadedAppDataDocument, + }, + null, + 2, + ), + ), + contentType: 'application/json', + }) +} + +test('order submission smoke: mainnet WETH -> USDC reaches submitted state and recent activity', async ({ + approveWethForCowVaultRelayer, + page, + walletAddress, + wrapNativeToWeth, +}, testInfo) => { + test.setTimeout(180_000) + + const submissionCapture: OrderSubmissionCapture = { + submittedOrderBody: null, + uploadedAppDataDocument: null, + uploadedAppDataHash: null, + } + + await interceptWrappedOrderSubmission(page, walletAddress, submissionCapture) + await interceptDeterministicWrappedQuote(page, walletAddress) + + await wrapNativeToWeth(BigInt(ONE_WETH_IN_WEI)) + await approveWethForCowVaultRelayer(BigInt(ONE_WETH_IN_WEI)) + + await page.goto(buildMainnetSwapRoute(MAINNET_WETH, MAINNET_USDC, { orderKind: 'sell', sellAmount: '1' })) + + await connectInjectedWallet(page) + await expectSellAmount(page, '1') + await waitForNonZeroInputValue(page.locator(swapSelectors.buyAmountInput)) + + await clickSwapAction(page) + + const tradeConfirmationButton = page.locator(swapSelectors.tradeConfirmationButton) + await expect(tradeConfirmationButton).toBeVisible({ timeout: 30_000 }) + await expect(tradeConfirmationButton).toContainText('Confirm Swap') + await tradeConfirmationButton.click() + + await expect(page.getByText('Batching orders')).toBeVisible({ timeout: 30_000 }) + await expect(page.getByText('batched together')).toBeVisible() + await expect(tradeConfirmationButton).not.toBeVisible() + + await expect + .poll(() => submissionCapture.uploadedAppDataHash, { + message: 'Expected app-data upload to happen before order submission', + }) + .toBeTruthy() + + await expect + .poll(() => Boolean(submissionCapture.submittedOrderBody), { + message: 'Expected order submission payload to be captured', + }) + .toBe(true) + + await attachOrderSubmissionProof(testInfo, submissionCapture) + + await openAccountModal(page) + + await expect(page.getByRole('heading', { name: /Recent Activity/ })).toBeVisible() + await expect(page.getByText(/sell order/i).first()).toBeVisible() + await expect(page.locator(':text-is("Open"):visible').first()).toBeVisible() + await expect(page.locator(':text-is("WETH"):visible').first()).toBeVisible() + await expect(page.locator(':text-is("USDC"):visible').first()).toBeVisible() +}) diff --git a/apps/cowswap-frontend-qa/src/tests/quote.smoke.spec.ts b/apps/cowswap-frontend-qa/src/tests/quote.smoke.spec.ts new file mode 100644 index 00000000000..fc8072252e4 --- /dev/null +++ b/apps/cowswap-frontend-qa/src/tests/quote.smoke.spec.ts @@ -0,0 +1,16 @@ +import { expect, test } from '../fixtures' +import { swapSelectors } from '../helpers/selectors' +import { buildMainnetSwapRoute, MAINNET_USDC, MAINNET_WETH } from '../helpers/tokens' +import { connectInjectedWallet, expectSellAmount } from '../helpers/trade' +import { waitForNonZeroInputValue } from '../helpers/wait' + +test('quote smoke: mainnet WETH -> USDC renders a non-zero buy amount', async ({ page }) => { + await page.goto(buildMainnetSwapRoute(MAINNET_WETH, MAINNET_USDC, { orderKind: 'sell', sellAmount: '1' })) + + await connectInjectedWallet(page) + await expectSellAmount(page, '1') + + const buyAmount = await waitForNonZeroInputValue(page.locator(swapSelectors.buyAmountInput)) + + expect(buyAmount).toBeGreaterThan(0) +}) diff --git a/apps/cowswap-frontend-qa/tsconfig.json b/apps/cowswap-frontend-qa/tsconfig.json new file mode 100644 index 00000000000..f8d8ae085ba --- /dev/null +++ b/apps/cowswap-frontend-qa/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "sourceMap": false, + "outDir": "../../dist/out-tsc", + "types": ["node"] + }, + "include": ["src/**/*.ts", "playwright.config.ts"] +} diff --git a/apps/cowswap-frontend/project.json b/apps/cowswap-frontend/project.json index df6a8282c2e..97ae41e329c 100644 --- a/apps/cowswap-frontend/project.json +++ b/apps/cowswap-frontend/project.json @@ -49,7 +49,8 @@ "executor": "@nx/vite:preview-server", "defaultConfiguration": "development", "options": { - "buildTarget": "cowswap-frontend:build" + "buildTarget": "cowswap-frontend:build", + "host": true }, "configurations": { "dev": { diff --git a/apps/cowswap-frontend/src/cow-react/index.tsx b/apps/cowswap-frontend/src/cow-react/index.tsx index 4d9903d18de..788c0f38b1c 100644 --- a/apps/cowswap-frontend/src/cow-react/index.tsx +++ b/apps/cowswap-frontend/src/cow-react/index.tsx @@ -30,6 +30,7 @@ import { WithLDProvider, } from 'modules/application' import { useInjectedWidgetParams } from 'modules/injectedWidget' +import { QaInjectedWalletConnector } from 'modules/wallet' import { loadActiveLocaleMessages } from 'lib/localeMessages' @@ -72,6 +73,7 @@ export function Main({ localeMessages }: MainProps): ReactNode { + diff --git a/apps/cowswap-frontend/src/locales/en-US.po b/apps/cowswap-frontend/src/locales/en-US.po index 29fed980af3..fc7009f7846 100644 --- a/apps/cowswap-frontend/src/locales/en-US.po +++ b/apps/cowswap-frontend/src/locales/en-US.po @@ -2802,8 +2802,8 @@ msgid "place a new order" msgstr "place a new order" #: apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTable/Content/NoWallet/OrdersTableNoWalletContent.tsx -msgid "To use {orderType} orders, please connect your wallet <0/>to one of our supported networks." -msgstr "To use {orderType} orders, please connect your wallet <0/>to one of our supported networks." +#~ msgid "To use {orderType} orders, please connect your wallet <0/>to one of our supported networks." +#~ msgstr "To use {orderType} orders, please connect your wallet <0/>to one of our supported networks." #: apps/cowswap-frontend/src/modules/limitOrders/pure/Settings/LimitOrdersSettings.pure.tsx msgid "When enabled, the limit price stays fixed when changing the BUY amount. When disabled, the limit price will update based on the BUY amount changes." diff --git a/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useApproveAndSwap.tsx b/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useApproveAndSwap.tsx index d365a0fa174..0573ea148a7 100644 --- a/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useApproveAndSwap.tsx +++ b/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useApproveAndSwap.tsx @@ -1,6 +1,7 @@ import { useCallback } from 'react' import { useTradeSpenderAddress } from '@cowprotocol/balances-and-allowances' +import { QA_FORCE_ONCHAIN_APPROVAL_SESSION_KEY } from '@cowprotocol/common-const/qa' import { Currency, CurrencyAmount } from '@cowprotocol/currency' import { useWalletInfo } from '@cowprotocol/wallet' @@ -41,7 +42,10 @@ export function useApproveAndSwap({ const handleApprove = useApproveCurrency(amountToApprove, useModals) const updateTradeApproveState = useUpdateApproveProgressModalState() - const isPermitSupported = useTokenSupportsPermit(amountToApprove.currency, TradeType.SWAP) && !ignorePermit + const isPermitSupported = + useTokenSupportsPermit(amountToApprove.currency, TradeType.SWAP) && + !ignorePermit && + !getShouldForceOnchainApproval() const generatePermitToTrade = useGeneratePermitInAdvanceToTrade(amountToApprove) const handlePermit = useCallback(async () => { @@ -138,3 +142,9 @@ function getApprovalTxHash(txResponse: object): string | null { return null } + +function getShouldForceOnchainApproval(): boolean { + if (typeof window === 'undefined') return false + + return window.sessionStorage.getItem(QA_FORCE_ONCHAIN_APPROVAL_SESSION_KEY) === 'true' +} diff --git a/apps/cowswap-frontend/src/modules/wallet/containers/QaInjectedWalletConnector/index.tsx b/apps/cowswap-frontend/src/modules/wallet/containers/QaInjectedWalletConnector/index.tsx new file mode 100644 index 00000000000..2310db2ea65 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/wallet/containers/QaInjectedWalletConnector/index.tsx @@ -0,0 +1,32 @@ +import { useEffect, type ReactNode } from 'react' + +import { QA_CONNECT_INJECTED_WALLET_EVENT } from '@cowprotocol/common-const/qa' + +import { useConnect, useConnectors } from 'wagmi' + +const ENABLE_QA_INJECTED_WALLET = process.env.REACT_APP_ENABLE_QA_INJECTED_WALLET === 'true' + +export function QaInjectedWalletConnector(): ReactNode { + const connectors = useConnectors() + const { mutateAsync: connect } = useConnect() + + useEffect(() => { + if (!ENABLE_QA_INJECTED_WALLET) return + + const handler = (): void => { + const injectedConnector = connectors.find((connector) => connector.id === 'injected') + + if (!injectedConnector) return + + void connect({ connector: injectedConnector }).catch((error: unknown) => { + console.error('[QaInjectedWalletConnector] failed', error) + }) + } + + document.addEventListener(QA_CONNECT_INJECTED_WALLET_EVENT, handler) + + return () => document.removeEventListener(QA_CONNECT_INJECTED_WALLET_EVENT, handler) + }, [connect, connectors]) + + return null +} diff --git a/apps/cowswap-frontend/src/modules/wallet/index.ts b/apps/cowswap-frontend/src/modules/wallet/index.ts index fc3361361b6..a2c0a976282 100644 --- a/apps/cowswap-frontend/src/modules/wallet/index.ts +++ b/apps/cowswap-frontend/src/modules/wallet/index.ts @@ -1,2 +1,3 @@ +export { QaInjectedWalletConnector } from './containers/QaInjectedWalletConnector' export { WatchAssetInWallet } from './containers/WatchAssetInWallet' export { Web3Status } from './containers/Web3Status' diff --git a/libs/common-const/package.json b/libs/common-const/package.json index 6a4084dc860..d28c2d74a7c 100644 --- a/libs/common-const/package.json +++ b/libs/common-const/package.json @@ -10,6 +10,12 @@ "import": "./src/index.ts", "require": "./src/index.ts", "default": "./src/index.ts" + }, + "./qa": { + "types": "./qa.ts", + "import": "./qa.ts", + "require": "./qa.ts", + "default": "./qa.ts" } }, "publishConfig": { @@ -20,6 +26,11 @@ "types": "./index.d.ts", "import": "./index.mjs", "require": "./index.js" + }, + "./qa": { + "types": "./qa.d.ts", + "import": "./qa.mjs", + "require": "./qa.js" } } }, diff --git a/libs/common-const/qa.ts b/libs/common-const/qa.ts new file mode 100644 index 00000000000..9f0d55a8f00 --- /dev/null +++ b/libs/common-const/qa.ts @@ -0,0 +1 @@ +export * from './src/qa' diff --git a/libs/common-const/src/qa.ts b/libs/common-const/src/qa.ts new file mode 100644 index 00000000000..666aa90a00e --- /dev/null +++ b/libs/common-const/src/qa.ts @@ -0,0 +1,2 @@ +export const QA_CONNECT_INJECTED_WALLET_EVENT = 'cowswap-connect-injected-wallet' +export const QA_FORCE_ONCHAIN_APPROVAL_SESSION_KEY = 'cowswap:qaForceOnchainApproval:v0' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4012750741a..1187be6a929 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,7 +46,7 @@ importers: version: 16.4.7 next: specifier: 15.2.8 - version: 15.2.8(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0) + version: 15.2.8(@babel/core@7.28.4)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0) styled-jsx: specifier: 5.1.2 version: 5.1.2(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react@19.1.2) @@ -71,7 +71,7 @@ importers: version: 9.39.2 '@lingui/swc-plugin': specifier: ^5.6.0 - version: 5.6.1(@lingui/core@5.5.1(@lingui/babel-plugin-lingui-macro@5.5.1(babel-plugin-macros@3.1.0))(babel-plugin-macros@3.1.0))(@swc/core@1.13.5(@swc/helpers@0.5.17))(next@15.2.8(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0)) + version: 5.6.1(@lingui/core@5.5.1(@lingui/babel-plugin-lingui-macro@5.5.1(babel-plugin-macros@3.1.0))(babel-plugin-macros@3.1.0))(@swc/core@1.13.5(@swc/helpers@0.5.17))(next@15.2.8(@babel/core@7.28.4)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0)) '@lingui/vite-plugin': specifier: ^5.4.1 version: 5.5.1(babel-plugin-macros@3.1.0)(vite@7.3.1(@types/node@24.7.1)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.88.0)(sass@1.88.0)(terser@5.46.0)(yaml@2.8.1)) @@ -95,10 +95,10 @@ importers: version: 22.4.2(@babel/traverse@7.28.6)(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))(nx@22.4.2(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))) '@nx/next': specifier: 22.4.2 - version: 22.4.2(@babel/core@7.28.4)(@babel/traverse@7.28.6)(@rspack/core@1.6.8(@swc/helpers@0.5.17))(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@zkochan/js-yaml@0.0.7)(bufferutil@4.0.8)(eslint@9.37.0(jiti@2.6.1))(html-webpack-plugin@5.5.3(webpack@5.102.1(@swc/core@1.13.5(@swc/helpers@0.5.17))))(next@15.2.8(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0))(nx@22.4.2(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17)))(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(vite@7.3.1(@types/node@24.7.1)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.88.0)(sass@1.88.0)(terser@5.46.0)(yaml@2.8.1))(vitest@4.0.9)(webpack@5.102.1(@swc/core@1.13.5(@swc/helpers@0.5.17))) + version: 22.4.2(@babel/core@7.28.4)(@babel/traverse@7.28.6)(@rspack/core@1.6.8(@swc/helpers@0.5.17))(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@zkochan/js-yaml@0.0.7)(bufferutil@4.0.8)(eslint@9.37.0(jiti@2.6.1))(html-webpack-plugin@5.5.3(webpack@5.102.1(@swc/core@1.13.5(@swc/helpers@0.5.17))))(next@15.2.8(@babel/core@7.28.4)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0))(nx@22.4.2(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17)))(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(vite@7.3.1(@types/node@24.7.1)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.88.0)(sass@1.88.0)(terser@5.46.0)(yaml@2.8.1))(vitest@4.0.9)(webpack@5.102.1(@swc/core@1.13.5(@swc/helpers@0.5.17))) '@nx/react': specifier: 22.4.2 - version: 22.4.2(@babel/core@7.28.4)(@babel/traverse@7.28.6)(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@zkochan/js-yaml@0.0.7)(bufferutil@4.0.8)(eslint@9.37.0(jiti@2.6.1))(next@15.2.8(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0))(nx@22.4.2(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17)))(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(vite@7.3.1(@types/node@24.7.1)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.88.0)(sass@1.88.0)(terser@5.46.0)(yaml@2.8.1))(vitest@4.0.9) + version: 22.4.2(@babel/core@7.28.4)(@babel/traverse@7.28.6)(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@zkochan/js-yaml@0.0.7)(bufferutil@4.0.8)(eslint@9.37.0(jiti@2.6.1))(next@15.2.8(@babel/core@7.28.4)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0))(nx@22.4.2(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17)))(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(vite@7.3.1(@types/node@24.7.1)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.88.0)(sass@1.88.0)(terser@5.46.0)(yaml@2.8.1))(vitest@4.0.9) '@nx/vite': specifier: 22.4.2 version: 22.4.2(@babel/traverse@7.28.6)(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))(nx@22.4.2(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17)))(typescript@5.9.3)(vite@7.3.1(@types/node@24.7.1)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.88.0)(sass@1.88.0)(terser@5.46.0)(yaml@2.8.1))(vitest@4.0.9) @@ -248,7 +248,7 @@ importers: version: 16.2.7 next-sitemap: specifier: ^4.2.3 - version: 4.2.3(next@15.2.8(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0)) + version: 4.2.3(next@15.2.8(@babel/core@7.28.4)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0)) node-stdlib-browser: specifier: ^1.2.0 version: 1.3.1 @@ -425,7 +425,7 @@ importers: version: 3.0.8(react-dom@19.1.2(react@19.1.2))(react@19.1.2) next: specifier: 15.2.8 - version: 15.2.8(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0) + version: 15.2.8(@babel/core@7.28.4)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0) polished: specifier: ^4.0.5 version: 4.2.2 @@ -889,6 +889,19 @@ importers: specifier: ^15.4.0 version: 15.4.0 + apps/cowswap-frontend-qa: + dependencies: + '@cowprotocol/common-const': + specifier: workspace:* + version: link:../../libs/common-const + viem: + specifier: 2.47.1 + version: 2.47.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12) + devDependencies: + '@playwright/test': + specifier: ^1.59.1 + version: 1.59.1 + apps/explorer: dependencies: '@apollo/client': @@ -1328,7 +1341,7 @@ importers: version: 2.47.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12) wagmi: specifier: 3.4.2 - version: 3.4.2(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@metamask/sdk@0.31.4(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(immer@10.0.2)(ox@0.14.0(typescript@5.9.3)(zod@4.1.12))(react@19.1.2)(typescript@5.9.3)(viem@2.47.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12)) + version: 3.4.2(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(ox@0.14.0(typescript@5.9.3)(zod@4.1.12))(react@19.1.2)(typescript@5.9.3)(viem@2.47.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12)) devDependencies: '@testing-library/react': specifier: 16.3.0 @@ -1423,7 +1436,7 @@ importers: version: 2.47.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12) wagmi: specifier: 3.4.2 - version: 3.4.2(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@metamask/sdk@0.31.4(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(immer@10.0.2)(ox@0.14.0(typescript@5.9.3)(zod@4.1.12))(react@19.1.2)(typescript@5.9.3)(viem@2.47.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12)) + version: 3.4.2(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(ox@0.14.0(typescript@5.9.3)(zod@4.1.12))(react@19.1.2)(typescript@5.9.3)(viem@2.47.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12)) devDependencies: '@testing-library/react': specifier: 16.3.0 @@ -1511,7 +1524,7 @@ importers: version: 2.47.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12) wagmi: specifier: 3.4.2 - version: 3.4.2(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@metamask/sdk@0.31.4(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(immer@10.0.2)(ox@0.14.0(typescript@5.9.3)(zod@4.1.12))(react@19.1.2)(typescript@5.9.3)(viem@2.47.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12)) + version: 3.4.2(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(ox@0.14.0(typescript@5.9.3)(zod@4.1.12))(react@19.1.2)(typescript@5.9.3)(viem@2.47.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12)) devDependencies: '@types/ms': specifier: ^2.1.0 @@ -1600,7 +1613,7 @@ importers: version: 2.47.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12) wagmi: specifier: 3.4.2 - version: 3.4.2(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@metamask/sdk@0.31.4(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(immer@10.0.2)(ox@0.14.0(typescript@5.9.3)(zod@4.1.12))(react@19.1.2)(typescript@5.9.3)(viem@2.47.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12)) + version: 3.4.2(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(ox@0.14.0(typescript@5.9.3)(zod@4.1.12))(react@19.1.2)(typescript@5.9.3)(viem@2.47.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12)) devDependencies: '@types/react': specifier: 19.1.3 @@ -1686,7 +1699,7 @@ importers: version: 2.47.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12) wagmi: specifier: 3.4.2 - version: 3.4.2(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@metamask/sdk@0.31.4(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(immer@10.0.2)(ox@0.14.0(typescript@5.9.3)(zod@4.1.12))(react@19.1.2)(typescript@5.9.3)(viem@2.47.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12)) + version: 3.4.2(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(ox@0.14.0(typescript@5.9.3)(zod@4.1.12))(react@19.1.2)(typescript@5.9.3)(viem@2.47.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12)) devDependencies: '@types/ms.macro': specifier: ^2.0.0 @@ -1796,7 +1809,7 @@ importers: version: 2.47.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12) wagmi: specifier: 3.4.2 - version: 3.4.2(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@metamask/sdk@0.31.4(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(immer@10.0.2)(ox@0.14.0(typescript@5.9.3)(zod@4.1.12))(react@19.1.2)(typescript@5.9.3)(viem@2.47.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12)) + version: 3.4.2(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(ox@0.14.0(typescript@5.9.3)(zod@4.1.12))(react@19.1.2)(typescript@5.9.3)(viem@2.47.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12)) devDependencies: '@testing-library/react': specifier: 16.3.0 @@ -1884,7 +1897,7 @@ importers: version: 2.16.2(@babel/core@7.28.4)(@babel/template@7.28.6)(@types/react@19.1.3)(react@19.1.2) next: specifier: 15.2.8 - version: 15.2.8(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0) + version: 15.2.8(@babel/core@7.28.4)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0) polished: specifier: ^4.0.5 version: 4.2.2 @@ -2066,7 +2079,7 @@ importers: version: 2.47.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12) wagmi: specifier: 3.4.2 - version: 3.4.2(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@metamask/sdk@0.31.4(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(immer@10.0.2)(ox@0.14.0(typescript@5.9.3)(zod@4.1.12))(react@19.1.2)(typescript@5.9.3)(viem@2.47.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12)) + version: 3.4.2(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(ox@0.14.0(typescript@5.9.3)(zod@4.1.12))(react@19.1.2)(typescript@5.9.3)(viem@2.47.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12)) devDependencies: '@types/react': specifier: 19.1.3 @@ -5369,6 +5382,11 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@playwright/test@1.59.1': + resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==} + engines: {node: '>=18'} + hasBin: true + '@polka/url@1.0.0-next.25': resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==} @@ -11028,6 +11046,11 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -13923,6 +13946,16 @@ packages: pkg-types@2.1.0: resolution: {integrity: sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==} + playwright-core@1.59.1: + resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.59.1: + resolution: {integrity: sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==} + engines: {node: '>=18'} + hasBin: true + pngjs@5.0.0: resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} engines: {node: '>=10.13.0'} @@ -20484,12 +20517,12 @@ snapshots: '@lingui/babel-plugin-lingui-macro': 5.5.1(babel-plugin-macros@3.1.0) babel-plugin-macros: 3.1.0 - '@lingui/swc-plugin@5.6.1(@lingui/core@5.5.1(@lingui/babel-plugin-lingui-macro@5.5.1(babel-plugin-macros@3.1.0))(babel-plugin-macros@3.1.0))(@swc/core@1.13.5(@swc/helpers@0.5.17))(next@15.2.8(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0))': + '@lingui/swc-plugin@5.6.1(@lingui/core@5.5.1(@lingui/babel-plugin-lingui-macro@5.5.1(babel-plugin-macros@3.1.0))(babel-plugin-macros@3.1.0))(@swc/core@1.13.5(@swc/helpers@0.5.17))(next@15.2.8(@babel/core@7.28.4)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0))': dependencies: '@lingui/core': 5.5.1(@lingui/babel-plugin-lingui-macro@5.5.1(babel-plugin-macros@3.1.0))(babel-plugin-macros@3.1.0) optionalDependencies: '@swc/core': 1.13.5(@swc/helpers@0.5.17) - next: 15.2.8(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0) + next: 15.2.8(@babel/core@7.28.4)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0) '@lingui/vite-plugin@5.5.1(babel-plugin-macros@3.1.0)(vite@7.3.1(@types/node@24.7.1)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.88.0)(sass@1.88.0)(terser@5.46.0)(yaml@2.8.1))': dependencies: @@ -21060,7 +21093,7 @@ snapshots: - utf-8-validate - vue-tsc - '@module-federation/node@2.7.28(@rspack/core@1.6.8(@swc/helpers@0.5.17))(bufferutil@4.0.8)(next@15.2.8(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0))(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.102.1(@swc/core@1.13.5(@swc/helpers@0.5.17)))': + '@module-federation/node@2.7.28(@rspack/core@1.6.8(@swc/helpers@0.5.17))(bufferutil@4.0.8)(next@15.2.8(@babel/core@7.28.4)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0))(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.102.1(@swc/core@1.13.5(@swc/helpers@0.5.17)))': dependencies: '@module-federation/enhanced': 0.23.0(@rspack/core@1.6.8(@swc/helpers@0.5.17))(bufferutil@4.0.8)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.102.1(@swc/core@1.13.5(@swc/helpers@0.5.17))) '@module-federation/runtime': 0.23.0 @@ -21070,7 +21103,7 @@ snapshots: node-fetch: 2.7.0(encoding@0.1.13) webpack: 5.102.1(@swc/core@1.13.5(@swc/helpers@0.5.17)) optionalDependencies: - next: 15.2.8(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0) + next: 15.2.8(@babel/core@7.28.4)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0) react: 19.1.2 react-dom: 19.1.2(react@19.1.2) transitivePeerDependencies: @@ -21643,10 +21676,10 @@ snapshots: - nx - supports-color - '@nx/module-federation@22.4.2(@babel/traverse@7.28.6)(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(bufferutil@4.0.8)(next@15.2.8(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0))(nx@22.4.2(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17)))(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)': + '@nx/module-federation@22.4.2(@babel/traverse@7.28.6)(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(bufferutil@4.0.8)(next@15.2.8(@babel/core@7.28.4)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0))(nx@22.4.2(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17)))(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)': dependencies: '@module-federation/enhanced': 0.21.6(@rspack/core@1.6.8(@swc/helpers@0.5.17))(bufferutil@4.0.8)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.102.1(@swc/core@1.13.5(@swc/helpers@0.5.17))) - '@module-federation/node': 2.7.28(@rspack/core@1.6.8(@swc/helpers@0.5.17))(bufferutil@4.0.8)(next@15.2.8(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0))(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.102.1(@swc/core@1.13.5(@swc/helpers@0.5.17))) + '@module-federation/node': 2.7.28(@rspack/core@1.6.8(@swc/helpers@0.5.17))(bufferutil@4.0.8)(next@15.2.8(@babel/core@7.28.4)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0))(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.102.1(@swc/core@1.13.5(@swc/helpers@0.5.17))) '@module-federation/sdk': 0.21.6 '@nx/devkit': 22.4.2(nx@22.4.2(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))) '@nx/js': 22.4.2(@babel/traverse@7.28.6)(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))(nx@22.4.2(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))) @@ -21677,20 +21710,20 @@ snapshots: - vue-tsc - webpack-cli - '@nx/next@22.4.2(@babel/core@7.28.4)(@babel/traverse@7.28.6)(@rspack/core@1.6.8(@swc/helpers@0.5.17))(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@zkochan/js-yaml@0.0.7)(bufferutil@4.0.8)(eslint@9.37.0(jiti@2.6.1))(html-webpack-plugin@5.5.3(webpack@5.102.1(@swc/core@1.13.5(@swc/helpers@0.5.17))))(next@15.2.8(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0))(nx@22.4.2(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17)))(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(vite@7.3.1(@types/node@24.7.1)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.88.0)(sass@1.88.0)(terser@5.46.0)(yaml@2.8.1))(vitest@4.0.9)(webpack@5.102.1(@swc/core@1.13.5(@swc/helpers@0.5.17)))': + '@nx/next@22.4.2(@babel/core@7.28.4)(@babel/traverse@7.28.6)(@rspack/core@1.6.8(@swc/helpers@0.5.17))(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@zkochan/js-yaml@0.0.7)(bufferutil@4.0.8)(eslint@9.37.0(jiti@2.6.1))(html-webpack-plugin@5.5.3(webpack@5.102.1(@swc/core@1.13.5(@swc/helpers@0.5.17))))(next@15.2.8(@babel/core@7.28.4)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0))(nx@22.4.2(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17)))(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(vite@7.3.1(@types/node@24.7.1)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.88.0)(sass@1.88.0)(terser@5.46.0)(yaml@2.8.1))(vitest@4.0.9)(webpack@5.102.1(@swc/core@1.13.5(@swc/helpers@0.5.17)))': dependencies: '@babel/plugin-proposal-decorators': 7.23.7(@babel/core@7.28.4) '@nx/devkit': 22.4.2(nx@22.4.2(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))) '@nx/eslint': 22.4.2(@babel/traverse@7.28.6)(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))(@zkochan/js-yaml@0.0.7)(eslint@9.37.0(jiti@2.6.1))(nx@22.4.2(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))) '@nx/js': 22.4.2(@babel/traverse@7.28.6)(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))(nx@22.4.2(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))) - '@nx/react': 22.4.2(@babel/core@7.28.4)(@babel/traverse@7.28.6)(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@zkochan/js-yaml@0.0.7)(bufferutil@4.0.8)(eslint@9.37.0(jiti@2.6.1))(next@15.2.8(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0))(nx@22.4.2(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17)))(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(vite@7.3.1(@types/node@24.7.1)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.88.0)(sass@1.88.0)(terser@5.46.0)(yaml@2.8.1))(vitest@4.0.9) + '@nx/react': 22.4.2(@babel/core@7.28.4)(@babel/traverse@7.28.6)(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@zkochan/js-yaml@0.0.7)(bufferutil@4.0.8)(eslint@9.37.0(jiti@2.6.1))(next@15.2.8(@babel/core@7.28.4)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0))(nx@22.4.2(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17)))(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(vite@7.3.1(@types/node@24.7.1)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.88.0)(sass@1.88.0)(terser@5.46.0)(yaml@2.8.1))(vitest@4.0.9) '@nx/web': 22.4.2(@babel/traverse@7.28.6)(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))(nx@22.4.2(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))) '@nx/webpack': 22.4.2(@babel/traverse@7.28.6)(@rspack/core@1.6.8(@swc/helpers@0.5.17))(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))(bufferutil@4.0.8)(html-webpack-plugin@5.5.3(webpack@5.102.1(@swc/core@1.13.5(@swc/helpers@0.5.17))))(nx@22.4.2(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17)))(typescript@5.9.3)(utf-8-validate@5.0.10) '@phenomnomnominal/tsquery': 6.1.4(typescript@5.9.3) '@svgr/webpack': 8.1.0 copy-webpack-plugin: 10.2.4(webpack@5.102.1(@swc/core@1.13.5(@swc/helpers@0.5.17))) ignore: 5.3.2 - next: 15.2.8(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0) + next: 15.2.8(@babel/core@7.28.4)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0) semver: 7.7.3 tslib: 2.8.1 webpack-merge: 5.10.0 @@ -21759,12 +21792,12 @@ snapshots: '@nx/nx-win32-x64-msvc@22.4.2': optional: true - '@nx/react@22.4.2(@babel/core@7.28.4)(@babel/traverse@7.28.6)(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@zkochan/js-yaml@0.0.7)(bufferutil@4.0.8)(eslint@9.37.0(jiti@2.6.1))(next@15.2.8(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0))(nx@22.4.2(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17)))(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(vite@7.3.1(@types/node@24.7.1)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.88.0)(sass@1.88.0)(terser@5.46.0)(yaml@2.8.1))(vitest@4.0.9)': + '@nx/react@22.4.2(@babel/core@7.28.4)(@babel/traverse@7.28.6)(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@zkochan/js-yaml@0.0.7)(bufferutil@4.0.8)(eslint@9.37.0(jiti@2.6.1))(next@15.2.8(@babel/core@7.28.4)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0))(nx@22.4.2(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17)))(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(vite@7.3.1(@types/node@24.7.1)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.88.0)(sass@1.88.0)(terser@5.46.0)(yaml@2.8.1))(vitest@4.0.9)': dependencies: '@nx/devkit': 22.4.2(nx@22.4.2(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))) '@nx/eslint': 22.4.2(@babel/traverse@7.28.6)(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))(@zkochan/js-yaml@0.0.7)(eslint@9.37.0(jiti@2.6.1))(nx@22.4.2(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))) '@nx/js': 22.4.2(@babel/traverse@7.28.6)(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))(nx@22.4.2(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))) - '@nx/module-federation': 22.4.2(@babel/traverse@7.28.6)(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(bufferutil@4.0.8)(next@15.2.8(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0))(nx@22.4.2(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17)))(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@nx/module-federation': 22.4.2(@babel/traverse@7.28.6)(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(bufferutil@4.0.8)(next@15.2.8(@babel/core@7.28.4)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0))(nx@22.4.2(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17)))(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(typescript@5.9.3)(utf-8-validate@5.0.10) '@nx/rollup': 22.4.2(@babel/core@7.28.4)(@babel/traverse@7.28.6)(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/babel__core@7.20.5)(nx@22.4.2(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17)))(typescript@5.9.3) '@nx/web': 22.4.2(@babel/traverse@7.28.6)(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))(nx@22.4.2(@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))) '@phenomnomnominal/tsquery': 6.1.4(typescript@5.9.3) @@ -22128,6 +22161,10 @@ snapshots: '@pkgr/core@0.2.9': {} + '@playwright/test@1.59.1': + dependencies: + playwright: 1.59.1 + '@polka/url@1.0.0-next.25': {} '@popperjs/core@2.11.8': {} @@ -25721,6 +25758,17 @@ snapshots: '@walletconnect/ethereum-provider': 2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10) typescript: 5.9.3 + '@wagmi/connectors@7.1.6(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@wagmi/core@3.3.2(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(ox@0.14.0(typescript@5.9.3)(zod@4.1.12))(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.2))(viem@2.47.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12)))(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(typescript@5.9.3)(viem@2.47.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))': + dependencies: + '@wagmi/core': 3.3.2(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(ox@0.14.0(typescript@5.9.3)(zod@4.1.12))(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.2))(viem@2.47.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12)) + viem: 2.47.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12) + optionalDependencies: + '@coinbase/wallet-sdk': 4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12) + '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12) + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12) + '@walletconnect/ethereum-provider': 2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10) + typescript: 5.9.3 + '@wagmi/core@3.3.2(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(ox@0.14.0(typescript@5.9.3)(zod@3.25.76))(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.2))(viem@2.47.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': dependencies: eventemitter3: 5.0.1 @@ -30328,6 +30376,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -33283,17 +33334,17 @@ snapshots: neo-async@2.6.2: {} - next-sitemap@4.2.3(next@15.2.8(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0)): + next-sitemap@4.2.3(next@15.2.8(@babel/core@7.28.4)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0)): dependencies: '@corex/deepmerge': 4.0.43 '@next/env': 13.5.7 fast-glob: 3.3.3 minimist: 1.2.8 - next: 15.2.8(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0) + next: 15.2.8(@babel/core@7.28.4)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0) next-tick@1.1.0: {} - next@15.2.8(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0): + next@15.2.8(@babel/core@7.28.4)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(sass@1.88.0): dependencies: '@next/env': 15.2.8 '@swc/counter': 0.1.3 @@ -33313,6 +33364,7 @@ snapshots: '@next/swc-linux-x64-musl': 15.2.5 '@next/swc-win32-arm64-msvc': 15.2.5 '@next/swc-win32-x64-msvc': 15.2.5 + '@playwright/test': 1.59.1 sass: 1.88.0 sharp: 0.33.5 transitivePeerDependencies: @@ -34075,6 +34127,14 @@ snapshots: exsolve: 1.0.5 pathe: 2.0.3 + playwright-core@1.59.1: {} + + playwright@1.59.1: + dependencies: + playwright-core: 1.59.1 + optionalDependencies: + fsevents: 2.3.2 + pngjs@5.0.0: {} pofile@1.1.4: {} @@ -37356,6 +37416,30 @@ snapshots: - ox - porto + wagmi@3.4.2(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.1.2))(@types/react@19.1.3)(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(ox@0.14.0(typescript@5.9.3)(zod@4.1.12))(react@19.1.2)(typescript@5.9.3)(viem@2.47.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12)): + dependencies: + '@tanstack/react-query': 5.90.20(react@19.1.2) + '@wagmi/connectors': 7.1.6(@coinbase/wallet-sdk@4.3.7(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(@wagmi/core@3.3.2(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(ox@0.14.0(typescript@5.9.3)(zod@4.1.12))(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.2))(viem@2.47.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12)))(@walletconnect/ethereum-provider@2.18.0(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(utf-8-validate@5.0.10))(typescript@5.9.3)(viem@2.47.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12)) + '@wagmi/core': 3.3.2(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(ox@0.14.0(typescript@5.9.3)(zod@4.1.12))(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.2))(viem@2.47.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12)) + react: 19.1.2 + use-sync-external-store: 1.4.0(react@19.1.2) + viem: 2.47.1(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@base-org/account' + - '@coinbase/wallet-sdk' + - '@gemini-wallet/core' + - '@metamask/sdk' + - '@safe-global/safe-apps-provider' + - '@safe-global/safe-apps-sdk' + - '@tanstack/query-core' + - '@types/react' + - '@walletconnect/ethereum-provider' + - immer + - ox + - porto + walker@1.0.8: dependencies: makeerror: 1.0.12