diff --git a/.env.example b/.env.example index de84436f6..3a4af6f8e 100644 --- a/.env.example +++ b/.env.example @@ -1,23 +1,23 @@ -# Client-exposed env vars must be prefixed with VITE_. Server-only vars must NOT be prefixed. -VITE_RPC_URI_FOR_1= -VITE_RPC_URI_FOR_10= -VITE_RPC_URI_FOR_100= -VITE_RPC_URI_FOR_137= -VITE_RPC_URI_FOR_146= -VITE_RPC_URI_FOR_250= -VITE_RPC_URI_FOR_8453= -VITE_RPC_URI_FOR_42161= -VITE_RPC_URI_FOR_747474= +# Client-exposed env vars must be prefixed with NEXT_PUBLIC_. Server-only vars must NOT be prefixed. +NEXT_PUBLIC_RPC_URI_FOR_1= +NEXT_PUBLIC_RPC_URI_FOR_10= +NEXT_PUBLIC_RPC_URI_FOR_100= +NEXT_PUBLIC_RPC_URI_FOR_137= +NEXT_PUBLIC_RPC_URI_FOR_146= +NEXT_PUBLIC_RPC_URI_FOR_250= +NEXT_PUBLIC_RPC_URI_FOR_8453= +NEXT_PUBLIC_RPC_URI_FOR_42161= +NEXT_PUBLIC_RPC_URI_FOR_747474= # Tenderly Virtual TestNet mode: opt-in execution-chain override layer. # Keep the app canonical on chain IDs like 1/10/8453 and map execution only through these vars. # Repeat the *_FOR_ pattern for each canonical chain you enable. -VITE_TENDERLY_MODE= -VITE_TENDERLY_CHAIN_ID_FOR_1= -VITE_TENDERLY_RPC_URI_FOR_1= -# Server-only Admin RPC for local scripts and cheatcodes. Keep this private and out of client-exposed VITE_ vars. +NEXT_PUBLIC_TENDERLY_MODE= +NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_1= +NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_1= +# Server-only Admin RPC for local scripts and cheatcodes. Keep this private and out of client-exposed NEXT_PUBLIC_ vars. TENDERLY_ADMIN_RPC_URI_FOR_1= -VITE_TENDERLY_EXPLORER_URI_FOR_1= +NEXT_PUBLIC_TENDERLY_EXPLORER_URI_FOR_1= PERSONAL_TENDERLY_API_KEY= PERSONAL_ACCOUNT_SLUG= @@ -25,35 +25,45 @@ PERSONAL_PROJECT_SLUG= PERSONAL_TENDERLY_RPC_NAME= TENDERLY_TEST_TX_FROM_ADDRESS= -VITE_ALCHEMY_KEY= -VITE_INFURA_PROJECT_ID= -VITE_PARTNER_ID_ADDRESS= -VITE_SHOULD_USE_PARTNER_CONTRACT=true -VITE_WALLETCONNECT_PROJECT_ID= -VITE_BASE_YEARN_ASSETS_URI=https://cdn.jsdelivr.net/gh/yearn/tokenassets@main -VITE_YDAEMON_BASE_URI=https://ydaemon.yearn.fi -VITE_KONG_REST_URL=https://kong.yearn.fi/api/rest -VITE_SENTRY_DSN= +NEXT_PUBLIC_ALCHEMY_KEY= +NEXT_PUBLIC_INFURA_PROJECT_ID= +NEXT_PUBLIC_PARTNER_ID_ADDRESS= +NEXT_PUBLIC_SHOULD_USE_PARTNER_CONTRACT=true +NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID= -VITE_CLUSTERS_API_URL=https://api.clusters.xyz/v1 -VITE_CLUSTERS_API_KEY= +# Optional local-only mock wallet for extensionless browser automation. +# Also supports ?agentWallet=true in dev, persisted in localStorage. +NEXT_PUBLIC_AGENT_WALLET= +NEXT_PUBLIC_AGENT_WALLET_ADDRESS= + +NEXT_PUBLIC_BASE_YEARN_ASSETS_URI=https://cdn.jsdelivr.net/gh/yearn/tokenassets@main +NEXT_PUBLIC_YDAEMON_BASE_URI=https://ydaemon.yearn.fi +NEXT_PUBLIC_KONG_REST_URL=https://kong.yearn.fi/api/rest +NEXT_PUBLIC_SENTRY_DSN= + +NEXT_PUBLIC_CLUSTERS_API_URL=https://api.clusters.xyz/v1 +NEXT_PUBLIC_CLUSTERS_API_KEY= # Balance fetching strategy: 'enso' (default) | 'multicall' # Enso reduces RPC load by fetching all balances via their API in a single call -VITE_BALANCE_SOURCE= +NEXT_PUBLIC_BALANCE_SOURCE= ENSO_API_KEY= -VITE_PLAUSIBLE_TRACK_LOCALHOST= +NEXT_PUBLIC_PLAUSIBLE_TRACK_LOCALHOST= # Comma-separated extra Vite dev/preview hosts. Keep personal hostnames in local .env only. ALLOWED_HOSTS= # Enso routing: set to 'true' to disable Enso zaps, forcing direct deposit/withdraw only -VITE_ENSO_DISABLED= +NEXT_PUBLIC_ENSO_DISABLED= # Holdings History API (server-only) ENVIO_GRAPHQL_URL=http://localhost:8080/v1/graphql ENVIO_PASSWORD=testing HOLDINGS_TEST_WALLET_ADDRESS= +# Optional server-only Kong REST override. Falls back to NEXT_PUBLIC_KONG_REST_URL. +KONG_REST_URL= +# Optional private RPC for server-side holdings activity enrichment. Falls back to NEXT_PUBLIC_RPC_URI_FOR_. +RPC_URI_FOR_1= # Holdings storage UPSTASH_REDIS_REST_URL_PORTFOLIO= UPSTASH_REDIS_REST_TOKEN_PORTFOLIO= diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 478dba207..0a42b891c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,28 +1,16 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" +name: CodeQL on: push: - branches: [ "main" ] + branches: ['main'] pull_request: - # The branches below must be a subset of the branches above - branches: [ "main" ] + branches: ['main'] schedule: - cron: '24 20 * * 5' jobs: analyze: - name: Analyze + name: Analyze (${{ matrix.language }}) runs-on: ubuntu-latest permissions: actions: read @@ -32,45 +20,22 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'javascript' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Use only 'java' to analyze code written in Java, Kotlin or both - # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + include: + - language: javascript-typescript + category: /language:javascript + - language: actions + category: /language:actions steps: - - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v4 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - - # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v4 - - # â„šī¸ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 - with: - category: "/language:${{matrix.language}}" + - name: Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: ${{ matrix.category }} diff --git a/.gitignore b/.gitignore index 129798d8b..c266ad64b 100755 --- a/.gitignore +++ b/.gitignore @@ -42,9 +42,12 @@ _app # docs /docs/plans /docs/temp +/docs/performance # playwright .playwright-mcp +.playwright-cli/ +output/playwright/ # local worktree clones yearn.fi-worktree-* @@ -58,3 +61,5 @@ yearn.fi-worktree-* .codex .codex/* .gstack/ + +.deepsec/ \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit index ea91fa262..85fcfde7e 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -3,5 +3,8 @@ set -e echo 'husky: running lint-staged checks' bunx lint-staged +echo 'husky: running client/server boundary check' +bun run check:client-server-boundary + echo 'husky: running type check' bun run tslint diff --git a/CLAUDE.md b/CLAUDE.md index a3d7005d3..1802a7821 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,14 +1,14 @@ # yearn.fi -Yearn Finance vaults interface — React 19 + TypeScript SPA (Vite, TanStack Query, Tailwind CSS 4, Wagmi/Viem). +Yearn Finance vaults interface — Next.js 16 App Router + React 19 + TypeScript, TanStack Query, Tailwind CSS 4, Wagmi/Viem. ## Commands ```bash bun install # Install dependencies -bun run dev # Vite dev server + Bun API server; prompts for an API port when needed -bun run preview # Vite preview + Bun API server; prompts for an API port when needed -bun run build # TypeScript check + Vite build +bun run dev # Next dev server on 127.0.0.1:3000 +bun run preview # Next production server on 127.0.0.1:3000 after a build +bun run build # Next production build bun run test # Full Vitest suite bunx vitest run src/path/to/test.ts # Single test file bun run lint:fix # Biome format and fix @@ -60,23 +60,26 @@ When writing a new `useEffect`, add a brief comment explaining why an alternativ ## Architecture -**Tech stack:** React 19, Vite, React Router (lazy-loaded), Tailwind CSS 4, TanStack Query, Wagmi/Viem/RainbowKit +**Tech stack:** Next.js 16 App Router, React 19, Tailwind CSS 4, TanStack Query, Wagmi/Viem/RainbowKit -**Path aliases** (defined in vite.config.ts): +**Path aliases** (defined in tsconfig.json and next.config.ts): - `@/*` → `src/*` - `@shared/*` → `src/components/shared/*` - `@pages/*` → `src/components/pages/*` - `@components/*` → `src/components/*` **Key directories:** +- `app/` — Next App Router pages, route handlers, metadata, redirects, and root layout - `src/components/shared/` — shared library (contexts, hooks, utils, types, contracts) - `src/components/pages/` — route pages (landing, portfolio, vaults) -- `api/` — Vercel serverless functions (prod) / Bun dev server (local) +- `src/server/` — focused API endpoint implementations and shared server-side helpers used by `app/api/**/route.ts` **Key patterns:** - Context provider chain defined in `App.tsx` — read that file for the full order +- Next route wrappers in `app/**/page.tsx` own route-level metadata and render client page components from `src/components/pages/` +- `src/navigation/` provides small client helpers backed by Next navigation context +- `/api/*` is served by explicit Next route handlers under `app/api/**/route.ts`; there is no catch-all API dispatcher - Vault data flows through `useYearn` context → filtered/sorted via hooks in `@shared/hooks/` -- Dual server: `bun run dev` starts Vite plus a Bun API server and keeps `/api/*` proxied to the selected API port. In prod, `api/` runs as Vercel serverless functions. ## Multi-Chain diff --git a/README.md b/README.md index 988561c0b..ac9c99b19 100755 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ git remote add upstream https://github.com/yearn/yearn.fi.git ``` -- Optional: Create `.env` file in root directory of repo then copy contents of `.env.example` to `.env` and replace values with your own keys. If you do not do this the default values from `next.config.js` will be used. +- Optional: Create a `.env` file in the repo root, copy `.env.example`, and replace values with your own keys. Client-readable values must use the `NEXT_PUBLIC_` prefix; server-only secrets must stay unprefixed. ``` bash cp .env.example .env diff --git a/api/README.md b/api/README.md deleted file mode 100644 index a36fe69f2..000000000 --- a/api/README.md +++ /dev/null @@ -1,127 +0,0 @@ -# API Routes - -This directory contains Vercel API functions plus `api/server.ts`, a local Bun server that mirrors the app-facing API routes on `localhost:3001`. - -## Local Server - -Run the local API with: - -```bash -bun api/server.ts -``` - -`API_PORT` overrides the default `3001`. `API_SERVER_PORT` is still accepted as a backwards-compatible fallback. - -`bun run dev:server` runs the same server with file watching. The Vite dev and preview scripts proxy `/api/*` to this server, using `API_PORT`, `VITE_API_PORT`, or `API_SERVER_PORT` when a non-default port is configured. - -The local server adds CORS to all handled routes and includes dev-only Tenderly admin routes. Vercel production routes are implemented as individual files under `api/`. - -## Route Inventory - -| Route | Method | Runtime | Purpose | -|-------|--------|---------|---------| -| `/api/holdings/history` | `GET` | Vercel + local | Daily holdings chart, USD or ETH-denominated | -| `/api/holdings/progress` | `GET` | Vercel + local | Durable progress state for long holdings requests | -| `/api/holdings/breakdown` | `GET` | Vercel + local | Per-vault breakdown for a settled UTC day | -| `/api/holdings/activity` | `GET` | Vercel + local | Recent classified vault activity | -| `/api/holdings/activity-facets` | `GET` | Vercel + local | Chain facets for holdings activity filters | -| `/api/holdings/protocol-return/history` | `GET` | Vercel + local | Protocol-return history for vault exposure | -| `/api/holdings/pnl/simple-history` | `GET` | Vercel + local | Compatibility alias for protocol-return history | -| `/api/enso/status` | `GET` | Vercel + local | Returns whether `ENSO_API_KEY` is configured | -| `/api/enso/balances` | `GET` | Vercel + local | Proxies Enso wallet balances | -| `/api/enso/route` | `GET` | Vercel + local | Proxies Enso route quotes/transactions | -| `/api/optimization/change` | `GET` | Vercel + local | Latest or historical optimization payloads from Redis | -| `/api/optimization/alignment` | `GET` | Vercel + local | Envio keeper-event alignment for a selected optimization | -| `/api/optimization/vault-state` | `POST` | Vercel + local | Live vault and strategy debt state from chain RPCs | -| `/api/yvusd/aprs` | `GET` | Vercel + local | Proxies the yvUSD APR service | -| `/api/vault/meta` | `GET` | Vercel | Serves SPA HTML with vault-specific SEO and OG tags | -| `/api/tenderly/status` | `GET` | local only | Returns configured local Tenderly chains | -| `/api/tenderly/snapshot` | `POST` | local only | Creates a Tenderly EVM snapshot | -| `/api/tenderly/revert` | `POST` | local only | Reverts a Tenderly EVM snapshot | -| `/api/tenderly/increase-time` | `POST` | local only | Advances Tenderly chain time and optionally mines | -| `/api/tenderly/fund` | `POST` | local only | Funds native or ERC-20 balances on Tenderly | - -## Holdings APIs - -The holdings implementation is the largest API surface here. See [`lib/holdings/README.md`](./lib/holdings/README.md) for: - -- Endpoint query params and response shapes. -- Envio, Kong, yearn-prices, and DefiLlama data flow. -- `timeframe=all` support from `2024-01-01`. -- Cache schema, hashed user cache keys, and invalidation behavior. -- Historical price provider switching and yearn-prices range requests. - -## Enso Proxies - -`/api/enso/*` routes keep `ENSO_API_KEY` server-side and forward requests to `https://api.enso.finance`. - -- `/api/enso/status` returns `{ "configured": boolean }`; the Vercel handler does not currently enforce the HTTP method. -- `/api/enso/balances` requires `eoaAddress`; Vercel always requests `chainId=all`, while the local server also accepts an optional `chainId`. -- `/api/enso/route` requires `fromAddress`, `chainId`, `tokenIn`, `tokenOut`, and `amountIn`. Optional params are `slippage`, `routingStrategy`, `destinationChainId`, and `receiver`. -- `/api/enso/balances` sets `Cache-Control: private, no-store, max-age=0, must-revalidate`. - -## Optimization APIs - -The optimization routes expose the current DOA optimization payloads and the local verification data used by optimization UI flows. - -- `/api/optimization/change` reads optimization records from Upstash Redis keys under `doa:optimizations:*`. `vault=0x...` selects one vault, and `history=1` or `history=true` returns all records for that vault instead of the latest one. -- `/api/optimization/alignment` requires `vault=0x...`, selects the matching optimization, resolves its source chain, and fetches aligned keeper `DebtUpdated` events from Envio. It needs `ENVIO_GRAPHQL_URL`; `ENVIO_PASSWORD` is sent as a bearer token when configured. -- `/api/optimization/vault-state` accepts `POST` JSON shaped as `{ "vault": "0x...", "chainId": 1, "strategies": ["0x..."] }`, then reads live vault and strategy debt state from configured public RPC endpoints. -- CDN cache headers: `change` uses `Vercel-CDN-Cache-Control: public, s-maxage=600, stale-while-revalidate=60`; `alignment` and `vault-state` use `Vercel-CDN-Cache-Control: public, s-maxage=60, stale-while-revalidate=30`. Browser-facing `Cache-Control` stays `public, max-age=0, must-revalidate`. - -## yvUSD APR Proxy - -`/api/yvusd/aprs` forwards query params to `YVUSD_APR_SERVICE_API`. - -- Default upstream: `https://yearn-yvusd-apr-service.vercel.app/api/aprs`. -- CDN cache header: `Vercel-CDN-Cache-Control: public, s-maxage=30, stale-while-revalidate=120`. Browser-facing `Cache-Control` stays `public, max-age=0, must-revalidate`. - -## Vault Metadata HTML - -`/api/vault/meta?chainId=1&address=0x...` validates both params, loads `dist/index.html`, injects vault-specific SEO and Open Graph tags, and returns HTML. - -- `chainId` must be digits only. -- `address` must be a 20-byte EVM address. -- CDN cache header: `Vercel-CDN-Cache-Control: public, s-maxage=86400, stale-while-revalidate=604800`. - -## Tenderly Local Routes - -Tenderly admin routes only exist in `api/server.ts` and are blocked unless the request comes from localhost. - -Required env for a configured chain: - -- `VITE_TENDERLY_MODE=true`. -- `VITE_TENDERLY_CHAIN_ID_FOR_`. -- `VITE_TENDERLY_RPC_URI_FOR_`. -- `TENDERLY_ADMIN_RPC_URI_FOR_` for snapshot, revert, time travel, and funding actions. - -## Environment Variables - -| Variable | Used by | Description | -|----------|---------|-------------| -| `API_PORT` | local server, Vite proxy | Local Bun API port, default `3001` | -| `API_SERVER_PORT` | local server, Vite proxy | Backwards-compatible local API port fallback | -| `VITE_API_PORT` | Vite proxy | Client-dev API proxy port fallback | -| `API_PROXY_TARGET` / `VITE_API_PROXY_TARGET` | Vite proxy | Explicit `/api` proxy target, overrides host/port resolution | -| `API_PROXY_HOST` | Vite proxy | Host used when Vite builds the default `/api` proxy target | -| `ENSO_API_KEY` | Enso routes | Bearer token for Enso upstream requests | -| `YVUSD_APR_SERVICE_API` | yvUSD route | Upstream APR service URL | -| `ENVIO_GRAPHQL_URL` | holdings, optimization alignment | Envio GraphQL endpoint | -| `ENVIO_PASSWORD` | holdings, optimization alignment | Optional Envio secret or bearer token | -| `VITE_RPC_URI_FOR_` | holdings activity | Optional chain RPC URL for receipt enrichment | -| `HOLDINGS_PRICE_PROVIDER` | holdings | `auto`, `yearn-prices`, or `defillama` | -| `YEARN_PRICES_BASE_URL` | holdings | yearn-prices base URL | -| `YEARN_PRICES_API_URL` | holdings | Legacy alias for `YEARN_PRICES_BASE_URL` | -| `YEARN_PRICES_API_KEY` | holdings | Bearer token for yearn-prices | -| `API_KEY_PORTFOLIO` | holdings | Fallback bearer token for yearn-prices | -| `DEFILLAMA_API_KEY` | holdings | Enables DefiLlama Pro | -| `VERCEL_HOLDINGS_RATE_LIMIT_ID` | holdings | Optional Vercel Firewall rate limit ID, defaults to `holdings-public-api` | -| `UPSTASH_REDIS_REST_URL_PORTFOLIO` | holdings | Upstash Redis REST URL for holdings cache/progress/invalidation | -| `UPSTASH_REDIS_REST_TOKEN_PORTFOLIO` | holdings | Upstash Redis REST token for holdings storage | -| `UPSTASH_REDIS_REST_URL` | optimization | Upstash Redis REST URL for optimization payloads | -| `UPSTASH_REDIS_REST_TOKEN` | optimization | Upstash Redis REST token for optimization payloads | -| `HOLDINGS_DEBUG` | local holdings | Enables holdings debug logs in `api/server.ts` | -| `VITE_TENDERLY_MODE` | local Tenderly | Enables Tenderly config parsing | -| `VITE_TENDERLY_CHAIN_ID_FOR_` | local Tenderly | Tenderly execution chain ID for a canonical chain | -| `VITE_TENDERLY_RPC_URI_FOR_` | local Tenderly | Public Tenderly RPC URI | -| `TENDERLY_ADMIN_RPC_URI_FOR_` | local Tenderly | Admin Tenderly RPC URI | diff --git a/api/enso/balances.ts b/api/enso/balances.ts deleted file mode 100644 index 2d41848ec..000000000 --- a/api/enso/balances.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { VercelRequest, VercelResponse } from '@vercel/node' -import { ENSO_BALANCES_CACHE_CONTROL } from './cache' - -const ENSO_API_BASE = 'https://api.enso.finance' - -export default async function handler(req: VercelRequest, res: VercelResponse) { - if (req.method !== 'GET') { - return res.status(405).json({ error: 'Method not allowed' }) - } - - const { eoaAddress } = req.query - - if (!eoaAddress || typeof eoaAddress !== 'string') { - return res.status(400).json({ error: 'Missing or invalid eoaAddress' }) - } - - const apiKey = process.env.ENSO_API_KEY - if (!apiKey) { - console.error('ENSO_API_KEY not configured') - return res.status(500).json({ error: 'Enso API not configured' }) - } - - try { - const params = new URLSearchParams({ - eoaAddress, - useEoa: 'true', - chainId: 'all' - }) - - const url = `${ENSO_API_BASE}/api/v1/wallet/balances?${params}` - - const response = await fetch(url, { - headers: { - Authorization: `Bearer ${apiKey}` - } - }) - - if (!response.ok) { - const errorText = await response.text() - console.error(`Enso API error: ${response.status}`, errorText) - return res.status(response.status).json({ - error: 'Enso API error', - status: response.status, - details: errorText - }) - } - - const data = await response.json() - - res.setHeader('Cache-Control', ENSO_BALANCES_CACHE_CONTROL) - return res.status(200).json(data) - } catch (error) { - console.error('Error proxying Enso request:', error) - return res.status(500).json({ error: 'Internal server error' }) - } -} diff --git a/api/enso/route.ts b/api/enso/route.ts deleted file mode 100644 index 07f3de244..000000000 --- a/api/enso/route.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { VercelRequest, VercelResponse } from '@vercel/node' - -const ENSO_API_BASE = 'https://api.enso.finance' - -export default async function handler(req: VercelRequest, res: VercelResponse) { - if (req.method !== 'GET') { - return res.status(405).json({ error: 'Method not allowed' }) - } - - const { fromAddress, chainId, tokenIn, tokenOut, amountIn, slippage, routingStrategy, destinationChainId, receiver } = - req.query - - if (!fromAddress || typeof fromAddress !== 'string') { - return res.status(400).json({ error: 'Missing or invalid fromAddress' }) - } - if (!chainId || typeof chainId !== 'string') { - return res.status(400).json({ error: 'Missing or invalid chainId' }) - } - if (!tokenIn || typeof tokenIn !== 'string') { - return res.status(400).json({ error: 'Missing or invalid tokenIn' }) - } - if (!tokenOut || typeof tokenOut !== 'string') { - return res.status(400).json({ error: 'Missing or invalid tokenOut' }) - } - if (!amountIn || typeof amountIn !== 'string') { - return res.status(400).json({ error: 'Missing or invalid amountIn' }) - } - - const apiKey = process.env.ENSO_API_KEY - if (!apiKey) { - console.error('ENSO_API_KEY not configured') - return res.status(500).json({ error: 'Enso API not configured' }) - } - - try { - const params = new URLSearchParams({ - fromAddress, - chainId, - tokenIn, - tokenOut, - amountIn, - slippage: (slippage as string) || '100' - }) - - if (destinationChainId && typeof destinationChainId === 'string') { - params.set('destinationChainId', destinationChainId) - } - if (receiver && typeof receiver === 'string') { - params.set('receiver', receiver) - } - if (routingStrategy && typeof routingStrategy === 'string') { - params.set('routingStrategy', routingStrategy) - } - - const url = `${ENSO_API_BASE}/api/v1/shortcuts/route?${params}` - - const response = await fetch(url, { - headers: { - Authorization: `Bearer ${apiKey}`, - 'Content-Type': 'application/json' - } - }) - - const data = await response.json() - - if (!response.ok) { - return res.status(response.status).json(data) - } - - return res.status(200).json(data) - } catch (error) { - console.error('Error proxying Enso route request:', error) - return res.status(500).json({ error: 'Internal server error' }) - } -} diff --git a/api/enso/status.ts b/api/enso/status.ts deleted file mode 100644 index bf640cc6c..000000000 --- a/api/enso/status.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { VercelRequest, VercelResponse } from '@vercel/node' - -export default function handler(_req: VercelRequest, res: VercelResponse) { - const apiKey = process.env.ENSO_API_KEY - const isConfigured = !!apiKey - - return res.status(200).json({ configured: isConfigured }) -} diff --git a/api/holdings/activity-facets.ts b/api/holdings/activity-facets.ts deleted file mode 100644 index 8270cbd29..000000000 --- a/api/holdings/activity-facets.ts +++ /dev/null @@ -1,103 +0,0 @@ -import type { VercelRequest, VercelResponse } from '@vercel/node' -import { setVercelCdnCacheHeaders } from '../lib/cacheHeaders' -import type { VaultVersion } from '../lib/holdings' -import { checkRateLimit, ensureHoldingsStorageInitialized } from '../lib/holdings' - -const HOLDINGS_ACTIVITY_FACETS_CDN_CACHE_CONTROL = 'public, s-maxage=300, stale-while-revalidate=900' - -function simpleHash(str: string): string { - const hash = Array.from(str).reduce((currentHash, char) => { - const nextHash = (currentHash << 5) - currentHash + char.charCodeAt(0) - return nextHash & nextHash - }, 0) - return Math.abs(hash).toString(36) -} - -function getClientIdentifier(req: VercelRequest): string { - const forwarded = req.headers['x-forwarded-for'] - if (forwarded) { - return (Array.isArray(forwarded) ? forwarded[0] : forwarded).split(',')[0].trim() - } - - const ua = req.headers['user-agent'] || '' - const lang = req.headers['accept-language'] || '' - const encoding = req.headers['accept-encoding'] || '' - return `fp-${simpleHash(ua + lang + encoding)}` -} - -function isValidAddress(address: string): boolean { - return /^0x[a-fA-F0-9]{40}$/.test(address) -} - -function parseVersion(value: string | string[] | undefined): VaultVersion { - return value === 'v2' || value === 'v3' ? value : 'all' -} - -export default async function handler(req: VercelRequest, res: VercelResponse) { - res.setHeader('Access-Control-Allow-Origin', '*') - res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS') - res.setHeader('Access-Control-Allow-Headers', 'Content-Type') - - if (req.method === 'OPTIONS') { - return res.status(204).end() - } - - if (req.method !== 'GET') { - return res.status(405).json({ error: 'Method not allowed' }) - } - - try { - await ensureHoldingsStorageInitialized() - } catch (error) { - console.error('Holdings activity facets storage initialization error:', error) - return res.status(500).json({ error: 'Failed to initialize holdings storage' }) - } - - const clientId = getClientIdentifier(req) - const rateCheck = await checkRateLimit(clientId, req.headers) - if (!rateCheck.allowed) { - res.setHeader('Retry-After', String(rateCheck.retryAfter)) - return res.status(429).json({ error: 'Too many requests', retryAfter: rateCheck.retryAfter }) - } - - const envioUrl = process.env.ENVIO_GRAPHQL_URL - if (!envioUrl) { - return res.status(503).json({ - error: 'Holdings activity API not configured', - details: 'ENVIO_GRAPHQL_URL environment variable is not set. This feature requires a running Envio indexer.' - }) - } - - const { address, version: versionParam } = req.query - - if (!address || typeof address !== 'string') { - return res.status(400).json({ error: 'Missing required parameter: address' }) - } - - if (!isValidAddress(address)) { - return res.status(400).json({ error: 'Invalid Ethereum address' }) - } - - try { - const { getHoldingsActivityFacetResponse } = await import('../lib/holdings') - const version = parseVersion(versionParam) - const facetsResponse = await getHoldingsActivityFacetResponse(address, version) - - setVercelCdnCacheHeaders(res, HOLDINGS_ACTIVITY_FACETS_CDN_CACHE_CONTROL) - return res.status(200).json(facetsResponse) - } catch (error) { - console.error('Holdings activity facets error:', error) - - if (process.env.NODE_ENV === 'development') { - const message = error instanceof Error ? error.message : String(error) - const stack = error instanceof Error ? error.stack : undefined - return res.status(502).json({ - error: 'Failed to fetch holdings activity facets', - message, - stack - }) - } - - return res.status(502).json({ error: 'Failed to fetch holdings activity facets' }) - } -} diff --git a/api/holdings/breakdown.test.ts b/api/holdings/breakdown.test.ts deleted file mode 100644 index 39b313f84..000000000 --- a/api/holdings/breakdown.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { parseUtcDateParam } from './breakdown' - -describe('parseUtcDateParam', () => { - it('parses valid UTC dates', () => { - expect(parseUtcDateParam('2026-02-28')).toBe(Math.floor(Date.UTC(2026, 1, 28) / 1000)) - }) - - it('rejects impossible calendar dates instead of normalizing them', () => { - expect(parseUtcDateParam('2026-02-31')).toBeNull() - expect(parseUtcDateParam('2026-13-01')).toBeNull() - expect(parseUtcDateParam('2026-00-10')).toBeNull() - }) -}) diff --git a/api/holdings/breakdown.ts b/api/holdings/breakdown.ts deleted file mode 100644 index 816a58c27..000000000 --- a/api/holdings/breakdown.ts +++ /dev/null @@ -1,157 +0,0 @@ -import type { VercelRequest, VercelResponse } from '@vercel/node' -import { setVercelCdnCacheHeaders } from '../lib/cacheHeaders' -import type { HoldingsEventFetchType, HoldingsEventPaginationMode, VaultVersion } from '../lib/holdings' -import { checkRateLimit, ensureHoldingsStorageInitialized } from '../lib/holdings' - -const HOLDINGS_BREAKDOWN_CDN_CACHE_CONTROL = 'public, s-maxage=300, stale-while-revalidate=600' - -function simpleHash(str: string): string { - let hash = 0 - for (let i = 0; i < str.length; i++) { - const char = str.charCodeAt(i) - hash = (hash << 5) - hash + char - hash = hash & hash - } - return Math.abs(hash).toString(36) -} - -function getClientIdentifier(req: VercelRequest): string { - const forwarded = req.headers['x-forwarded-for'] - if (forwarded) { - return (Array.isArray(forwarded) ? forwarded[0] : forwarded).split(',')[0].trim() - } - - const ua = req.headers['user-agent'] || '' - const lang = req.headers['accept-language'] || '' - const encoding = req.headers['accept-encoding'] || '' - return `fp-${simpleHash(ua + lang + encoding)}` -} - -function isValidAddress(address: string): boolean { - return /^0x[a-fA-F0-9]{40}$/.test(address) -} - -function parseHoldingsEventFetchType(value: string | string[] | undefined): HoldingsEventFetchType { - return value === 'parallel' ? 'parallel' : 'seq' -} - -function parseHoldingsEventPaginationMode(value: string | string[] | undefined): HoldingsEventPaginationMode { - return value === 'all' ? 'all' : 'paged' -} - -export function parseUtcDateParam(value: string | string[] | undefined): number | null { - if (!value || Array.isArray(value)) { - return null - } - - const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value) - if (!match) { - return null - } - - const [, year, month, day] = match - const yearNumber = Number(year) - const monthNumber = Number(month) - const dayNumber = Number(day) - const utcDate = new Date(Date.UTC(yearNumber, monthNumber - 1, dayNumber)) - - if ( - utcDate.getUTCFullYear() !== yearNumber || - utcDate.getUTCMonth() !== monthNumber - 1 || - utcDate.getUTCDate() !== dayNumber - ) { - return null - } - - const timestamp = Math.floor(utcDate.getTime() / 1000) - return Number.isFinite(timestamp) ? timestamp : null -} - -export default async function handler(req: VercelRequest, res: VercelResponse) { - res.setHeader('Access-Control-Allow-Origin', '*') - res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS') - res.setHeader('Access-Control-Allow-Headers', 'Content-Type') - - if (req.method === 'OPTIONS') { - return res.status(204).end() - } - - if (req.method !== 'GET') { - return res.status(405).json({ error: 'Method not allowed' }) - } - - try { - await ensureHoldingsStorageInitialized() - } catch (error) { - console.error('Holdings breakdown storage initialization error:', error) - return res.status(500).json({ error: 'Failed to initialize holdings storage' }) - } - - const clientId = getClientIdentifier(req) - const rateCheck = await checkRateLimit(clientId, req.headers) - if (!rateCheck.allowed) { - res.setHeader('Retry-After', String(rateCheck.retryAfter)) - return res.status(429).json({ error: 'Too many requests', retryAfter: rateCheck.retryAfter }) - } - - const envioUrl = process.env.ENVIO_GRAPHQL_URL - if (!envioUrl) { - return res.status(503).json({ - error: 'Holdings breakdown API not configured', - details: 'ENVIO_GRAPHQL_URL environment variable is not set. This feature requires a running Envio indexer.' - }) - } - - const { - address, - date: dateParam, - version: versionParam, - fetchType: fetchTypeParam, - paginationMode: paginationModeParam - } = req.query - - if (!address || typeof address !== 'string') { - return res.status(400).json({ error: 'Missing required parameter: address' }) - } - - if (!isValidAddress(address)) { - return res.status(400).json({ error: 'Invalid Ethereum address' }) - } - - const breakdownTimestamp = parseUtcDateParam(dateParam) - if (dateParam && breakdownTimestamp === null) { - return res.status(400).json({ error: 'Invalid date format, expected YYYY-MM-DD' }) - } - - const version: VaultVersion = versionParam === 'v2' || versionParam === 'v3' ? versionParam : 'all' - const fetchType = parseHoldingsEventFetchType(fetchTypeParam) - const paginationMode = parseHoldingsEventPaginationMode(paginationModeParam) - - try { - const { getHoldingsBreakdown } = await import('../lib/holdings') - const breakdown = await getHoldingsBreakdown( - address, - version, - fetchType, - paginationMode, - breakdownTimestamp ?? undefined - ) - - setVercelCdnCacheHeaders(res, HOLDINGS_BREAKDOWN_CDN_CACHE_CONTROL) - return res.status(200).json(breakdown) - } catch (error) { - console.error('Holdings breakdown error:', error) - - if (process.env.NODE_ENV === 'development') { - const message = error instanceof Error ? error.message : String(error) - const stack = error instanceof Error ? error.stack : undefined - return res.status(502).json({ - error: 'Failed to fetch holdings breakdown', - message, - stack - }) - } - - return res.status(502).json({ error: 'Failed to fetch holdings breakdown' }) - } -} diff --git a/api/holdings/pnl/simple-history.ts b/api/holdings/pnl/simple-history.ts deleted file mode 100644 index b1be31ab4..000000000 --- a/api/holdings/pnl/simple-history.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from '../protocol-return/history' diff --git a/api/holdings/progress.test.ts b/api/holdings/progress.test.ts deleted file mode 100644 index d715b8313..000000000 --- a/api/holdings/progress.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' - -const getHoldingsProgressMock = vi.fn() - -vi.mock('../lib/holdings/services/progress', () => ({ - getHoldingsProgress: getHoldingsProgressMock -})) - -type TMockResponse = { - statusCode: number - headers: Record - body: unknown - setHeader: (name: string, value: string) => void - status: (code: number) => TMockResponse - json: (payload: unknown) => TMockResponse - end: () => TMockResponse -} - -function createMockResponse(): TMockResponse { - return { - statusCode: 200, - headers: {}, - body: null, - setHeader(name: string, value: string) { - this.headers[name] = value - }, - status(code: number) { - this.statusCode = code - return this - }, - json(payload: unknown) { - this.body = payload - return this - }, - end() { - return this - } - } -} - -describe('holdings progress route', () => { - beforeEach(() => { - vi.resetModules() - vi.clearAllMocks() - }) - - it('returns no content when a progress record is not available', async () => { - getHoldingsProgressMock.mockResolvedValue(null) - - const { default: handler } = await import('./progress') - const req = { - method: 'GET', - query: { - id: 'portfolio-history:usd:1y:test' - } - } as any - const res = createMockResponse() - - await handler(req, res as any) - - expect(res.statusCode).toBe(204) - expect(res.body).toBeNull() - expect(res.headers['Cache-Control']).toBe('no-store') - }) - - it('returns the persisted progress record', async () => { - const progress = { - id: 'portfolio-history:usd:1y:test', - route: 'history', - addressHash: 'hash', - status: 'running', - progress: 40, - message: 'Fetched prices', - detail: null, - startedAt: 1, - updatedAt: 2, - logs: [] - } - getHoldingsProgressMock.mockResolvedValue(progress) - - const { default: handler } = await import('./progress') - const req = { - method: 'GET', - query: { - id: progress.id - } - } as any - const res = createMockResponse() - - await handler(req, res as any) - - expect(res.statusCode).toBe(200) - expect(res.body).toEqual(progress) - expect(res.headers['Cache-Control']).toBe('no-store') - }) -}) diff --git a/api/holdings/progress.ts b/api/holdings/progress.ts deleted file mode 100644 index e45837164..000000000 --- a/api/holdings/progress.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { VercelRequest, VercelResponse } from '@vercel/node' -import { getHoldingsProgress } from '../lib/holdings/services/progress' - -export default async function handler(req: VercelRequest, res: VercelResponse) { - res.setHeader('Access-Control-Allow-Origin', '*') - res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS') - res.setHeader('Access-Control-Allow-Headers', 'Content-Type') - - if (req.method === 'OPTIONS') { - return res.status(204).end() - } - - if (req.method !== 'GET') { - return res.status(405).json({ error: 'Method not allowed' }) - } - - const id = req.query.id - const progress = await getHoldingsProgress(typeof id === 'string' ? id : null) - - res.setHeader('Cache-Control', 'no-store') - - if (!progress) { - return res.status(204).end() - } - - return res.status(200).json(progress) -} diff --git a/api/lib/holdings/services/ratelimit.test.ts b/api/lib/holdings/services/ratelimit.test.ts deleted file mode 100644 index c652e1af7..000000000 --- a/api/lib/holdings/services/ratelimit.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' - -const checkVercelRateLimitMock = vi.fn() - -vi.mock('@vercel/firewall', () => ({ - checkRateLimit: checkVercelRateLimitMock -})) - -describe('Vercel Firewall rate limiting', () => { - beforeEach(() => { - vi.resetModules() - vi.clearAllMocks() - vi.unstubAllEnvs() - }) - - it('allows requests without checking Vercel Firewall outside Vercel', async () => { - const { checkRateLimit } = await import('./ratelimit') - const result = await checkRateLimit('127.0.0.1', { host: 'localhost:3001' }) - - expect(result).toEqual({ allowed: true }) - expect(checkVercelRateLimitMock).not.toHaveBeenCalled() - }) - - it('checks Vercel Firewall with the client identifier as the rate limit key', async () => { - vi.stubEnv('VERCEL', '1') - vi.stubEnv('VERCEL_HOLDINGS_RATE_LIMIT_ID', 'test-holdings-limit') - checkVercelRateLimitMock.mockResolvedValue({ rateLimited: false }) - - const { checkRateLimit } = await import('./ratelimit') - const result = await checkRateLimit('127.0.0.1', { host: 'example.vercel.app', 'x-test': undefined }) - - expect(result).toEqual({ allowed: true }) - expect(checkVercelRateLimitMock).toHaveBeenCalledWith('test-holdings-limit', { - headers: { host: 'example.vercel.app' }, - rateLimitKey: '127.0.0.1' - }) - }) - - it('returns a fixed retry-after when Vercel rate limits the request', async () => { - vi.stubEnv('VERCEL', '1') - checkVercelRateLimitMock.mockResolvedValue({ rateLimited: true }) - - const { checkRateLimit } = await import('./ratelimit') - const result = await checkRateLimit('127.0.0.1', { host: 'example.vercel.app' }) - - expect(result).toEqual({ allowed: false, retryAfter: 60 }) - }) -}) diff --git a/api/lib/holdings/services/ratelimit.ts b/api/lib/holdings/services/ratelimit.ts deleted file mode 100644 index ee2a8a1e9..000000000 --- a/api/lib/holdings/services/ratelimit.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { checkRateLimit as checkVercelRateLimit } from '@vercel/firewall' - -const WINDOW_SECONDS = 60 -const DEFAULT_RATE_LIMIT_ID = 'holdings-public-api' - -export interface RateLimitResult { - allowed: boolean - retryAfter?: number -} - -type RateLimitHeaders = Headers | Record - -function getRateLimitId(): string { - return process.env.VERCEL_HOLDINGS_RATE_LIMIT_ID?.trim() || DEFAULT_RATE_LIMIT_ID -} - -function normalizeHeaders(headers?: RateLimitHeaders): Headers | Record | undefined { - if (!headers || headers instanceof Headers) { - return headers - } - - const normalizedHeaders: Record = {} - for (const [key, value] of Object.entries(headers)) { - if (typeof value !== 'undefined') { - normalizedHeaders[key] = value - } - } - return normalizedHeaders -} - -export async function checkRateLimit(clientIdentifier: string, headers?: RateLimitHeaders): Promise { - if (process.env.VERCEL !== '1') { - return { allowed: true } - } - - const rateLimitId = getRateLimitId() - - try { - const result = await checkVercelRateLimit(rateLimitId, { - headers: normalizeHeaders(headers), - rateLimitKey: clientIdentifier - }) - - if (result.error) { - console.warn(`[Holdings] Vercel rate limit check returned ${result.error} for ${rateLimitId}`) - } - - return result.rateLimited ? { allowed: false, retryAfter: WINDOW_SECONDS } : { allowed: true } - } catch (error) { - console.warn('[Holdings] Vercel rate limit check failed', error) - return { allowed: true } - } -} diff --git a/api/optimization/_lib/cors.ts b/api/optimization/_lib/cors.ts deleted file mode 100644 index b06da3b2a..000000000 --- a/api/optimization/_lib/cors.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { VercelResponse } from '@vercel/node' - -export const OPTIMIZATION_GET_CORS_HEADERS = { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type' -} as const - -export const OPTIMIZATION_POST_CORS_HEADERS = { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'POST, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type' -} as const - -export function setCorsHeaders(res: VercelResponse, headers: Readonly>): VercelResponse { - res.setHeader('Access-Control-Allow-Origin', headers['Access-Control-Allow-Origin']) - res.setHeader('Access-Control-Allow-Methods', headers['Access-Control-Allow-Methods']) - res.setHeader('Access-Control-Allow-Headers', headers['Access-Control-Allow-Headers']) - return res -} diff --git a/api/optimization/change.ts b/api/optimization/change.ts deleted file mode 100644 index 9f5fd4167..000000000 --- a/api/optimization/change.ts +++ /dev/null @@ -1,78 +0,0 @@ -import type { VercelRequest, VercelResponse } from '@vercel/node' -import { setVercelCdnCacheHeaders } from '../lib/cacheHeaders' -import { OPTIMIZATION_GET_CORS_HEADERS, setCorsHeaders } from './_lib/cors' -import { - findVaultOptimization, - isRedisAuthenticationError, - isRedisConnectivityError, - REDIS_AUTHENTICATION_ERROR_MESSAGE, - REDIS_CONNECTIVITY_ERROR_MESSAGE, - readOptimizations -} from './_lib/redis' - -const CACHE_CONTROL = 'public, s-maxage=600, stale-while-revalidate=60' - -function isHistoryQueryEnabled(historyParam: string | string[] | undefined): boolean { - const value = Array.isArray(historyParam) ? historyParam[0] : historyParam - return value === '1' || value === 'true' -} - -export default async function handler(req: VercelRequest, res: VercelResponse) { - setCorsHeaders(res, OPTIMIZATION_GET_CORS_HEADERS) - - if (req.method === 'OPTIONS') { - return res.status(204).send(null) - } - - if (req.method !== 'GET') { - return res.status(405).json({ error: 'Method not allowed' }) - } - - try { - const optimizations = await readOptimizations() - if (!optimizations || optimizations.length === 0) { - return res.status(404).json({ error: 'No optimization data available' }) - } - - const requestedVault = req.query.vault as string | undefined - if (requestedVault) { - if (isHistoryQueryEnabled(req.query.history)) { - const selectedHistory = optimizations.filter((optimization) => { - return optimization.vault.toLowerCase() === requestedVault.toLowerCase() - }) - if (selectedHistory.length === 0) { - return res.status(404).json({ error: `Vault not found in optimization payload: ${requestedVault}` }) - } - - setVercelCdnCacheHeaders(res, CACHE_CONTROL) - return res.status(200).json(selectedHistory) - } - - const selected = findVaultOptimization(optimizations, requestedVault) - if (!selected) { - return res.status(404).json({ error: `Vault not found in optimization payload: ${requestedVault}` }) - } - - setVercelCdnCacheHeaders(res, CACHE_CONTROL) - return res.status(200).json(selected) - } - - setVercelCdnCacheHeaders(res, CACHE_CONTROL) - return res.status(200).json(optimizations) - } catch (error) { - if (isRedisAuthenticationError(error)) { - return res.status(500).json({ - error: REDIS_AUTHENTICATION_ERROR_MESSAGE - }) - } - - if (isRedisConnectivityError(error)) { - return res.status(503).json({ - error: REDIS_CONNECTIVITY_ERROR_MESSAGE - }) - } - - const message = error instanceof Error ? error.message : String(error) - return res.status(500).json({ error: message }) - } -} diff --git a/api/optimization/vault-state.ts b/api/optimization/vault-state.ts deleted file mode 100644 index e3d35e101..000000000 --- a/api/optimization/vault-state.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { VercelRequest, VercelResponse } from '@vercel/node' -import { setVercelCdnCacheHeaders } from '../lib/cacheHeaders' -import { OPTIMIZATION_POST_CORS_HEADERS, setCorsHeaders } from './_lib/cors' -import { fetchVaultOnChainState } from './_lib/rpc' - -const CACHE_CONTROL = 'public, s-maxage=60, stale-while-revalidate=30' - -export default async function handler(req: VercelRequest, res: VercelResponse) { - setCorsHeaders(res, OPTIMIZATION_POST_CORS_HEADERS) - - if (req.method === 'OPTIONS') { - return res.status(204).send(null) - } - - if (req.method !== 'POST') { - return res.status(405).json({ error: 'Method not allowed' }) - } - - const payload = req.body && typeof req.body === 'object' ? (req.body as Record) : {} - const vault = typeof payload.vault === 'string' ? payload.vault : null - const chainId = typeof payload.chainId === 'number' ? payload.chainId : null - const strategies = Array.isArray(payload.strategies) - ? payload.strategies.filter((s: unknown): s is string => typeof s === 'string') - : [] - - if (!vault || !/^0x[a-fA-F0-9]{40}$/.test(vault)) { - return res.status(400).json({ error: 'Invalid vault address' }) - } - if (chainId === null || !Number.isFinite(chainId)) { - return res.status(400).json({ error: 'Invalid chainId' }) - } - if (strategies.length === 0) { - return res.status(400).json({ error: 'No strategy addresses provided' }) - } - - try { - const state = await fetchVaultOnChainState(chainId, vault, strategies) - - const strategyDebts: Record = {} - for (const [addr, debt] of state.strategyDebts) { - strategyDebts[addr] = debt.toString() - } - - setVercelCdnCacheHeaders(res, CACHE_CONTROL) - return res.status(200).json({ - totalAssets: state.totalAssets.toString(), - strategyDebts, - unallocatedBps: state.unallocatedBps - }) - } catch (error) { - const message = error instanceof Error ? error.message : String(error) - return res.status(503).json({ error: message }) - } -} diff --git a/api/server.ts b/api/server.ts deleted file mode 100644 index 2aa51c79f..000000000 --- a/api/server.ts +++ /dev/null @@ -1,1433 +0,0 @@ -import { serve } from 'bun' -import type { - TTenderlyFundRequest, - TTenderlyIncreaseTimeRequest, - TTenderlyRevertRequest, - TTenderlySnapshotRequest -} from '../src/components/shared/types/tenderly' -import { ENSO_BALANCES_CACHE_CONTROL } from './enso/cache' -import type { TVaultListEntry, TVaultSnapshot } from './lib/aio' -import { buildSitemap, buildVaultMarkdown, buildVaultsMarkdown, KONG_REST_BASE, KONG_VAULT_LIST_URL } from './lib/aio' -import { getVercelCdnCacheHeaders } from './lib/cacheHeaders' -import { - clearUserCache, - getHistoricalHoldingsChart, - getHoldingsActivity, - getHoldingsActivityFacetResponse, - getHoldingsBreakdown, - getHoldingsProtocolReturnHistory, - getHoldingsTotalsCacheVersion, - type HoldingsActivityTypeFilter, - type HoldingsEventFetchType, - type HoldingsEventPaginationMode, - type HoldingsHistoryDenomination, - type HoldingsHistoryTimeframe, - initializeHoldingsStorage, - type VaultVersion, - validateConfig -} from './lib/holdings' -import { - createHoldingsDebugContext, - debugError, - debugLog, - isHoldingsDebugRequested, - withHoldingsDebugContext -} from './lib/holdings/services/debug' -import { getHoldingsProgress, startHoldingsProgress, updateHoldingsProgress } from './lib/holdings/services/progress' -import { getVaultDecimals } from './optimization/_lib/assetLogos' -import { fetchAlignedEvents } from './optimization/_lib/envio' -import { parseExplainMetadata } from './optimization/_lib/explain-parse' -import { - findVaultOptimization, - isRedisAuthenticationError, - isRedisConnectivityError, - REDIS_AUTHENTICATION_ERROR_MESSAGE, - REDIS_CONNECTIVITY_ERROR_MESSAGE, - readOptimizations -} from './optimization/_lib/redis' -import { fetchVaultOnChainState } from './optimization/_lib/rpc' -import { - buildTenderlyPanelStatus, - buildTenderlyRevertResponse, - buildTenderlySnapshotRecord, - requireTenderlyServerChain, - resolveTenderlyFundRpcRequest -} from './tenderly.helpers' -import { buildTenderlyAdminAccessDeniedResponse } from './tenderlyAccess' - -const ENSO_API_BASE = 'https://api.enso.finance' -const DEFAULT_API_PORT = 3001 -const YVUSD_APR_SERVICE_API = ( - process.env.YVUSD_APR_SERVICE_API || 'https://yearn-yvusd-apr-service.vercel.app/api/aprs' -).replace(/\/$/, '') -const YVUSD_APR_CDN_CACHE_CONTROL = 'public, s-maxage=30, stale-while-revalidate=120' - -function isHistoryQueryEnabled(historyParam: string | null): boolean { - return historyParam === '1' || historyParam === 'true' -} - -function resolveApiPort(env: NodeJS.ProcessEnv): number { - const rawPort = env.API_PORT?.trim() || env.API_SERVER_PORT?.trim() || String(DEFAULT_API_PORT) - const port = Number(rawPort) - - if (!Number.isInteger(port) || port <= 0) { - throw new Error(`Invalid API port value: ${rawPort}`) - } - - return port -} - -const API_PORT = resolveApiPort(process.env) - -type TTenderlyJsonRpcSuccess = { - id: string | number | null - jsonrpc: '2.0' - result: unknown -} - -type TTenderlyJsonRpcError = { - id: string | number | null - jsonrpc: '2.0' - error: { - code: number - message: string - data?: unknown - } -} - -async function handleYvUsdAprs(req: Request): Promise { - if (req.method !== 'GET') { - return Response.json({ error: 'Method not allowed' }, { status: 405 }) - } - - const requestUrl = new URL(req.url) - const upstreamUrl = new URL(YVUSD_APR_SERVICE_API) - requestUrl.searchParams.forEach((value, key) => { - upstreamUrl.searchParams.set(key, value) - }) - - try { - const response = await fetch(upstreamUrl.toString(), { - headers: { - Accept: 'application/json' - } - }) - - if (!response.ok) { - const details = await response.text() - return Response.json( - { error: 'yvUSD APR upstream error', status: response.status, details }, - { status: response.status } - ) - } - - const data = await response.json() - return Response.json(data, { - headers: getVercelCdnCacheHeaders(YVUSD_APR_CDN_CACHE_CONTROL) - }) - } catch (error) { - console.error('Error proxying yvUSD APR request:', error) - return Response.json({ error: 'Internal server error' }, { status: 500 }) - } -} - -const CORS_HEADERS = { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type, Authorization' -} - -function withCors(response: Response): Response { - const newHeaders = new Headers(response.headers) - for (const [key, value] of Object.entries(CORS_HEADERS)) { - newHeaders.set(key, value) - } - return new Response(response.body, { - status: response.status, - statusText: response.statusText, - headers: newHeaders - }) -} - -function handleCorsPreFlight(): Response { - return new Response(null, { - status: 204, - headers: CORS_HEADERS - }) -} - -async function handleHoldingsProgress(req: Request): Promise { - const url = new URL(req.url) - const progress = await getHoldingsProgress(url.searchParams.get('id')) - - if (!progress) { - return new Response(null, { - status: 204, - headers: { - 'Cache-Control': 'no-store' - } - }) - } - - return Response.json(progress, { - headers: { - 'Cache-Control': 'no-store' - } - }) -} - -function isValidAddress(address: string): boolean { - return /^0x[a-fA-F0-9]{40}$/.test(address) -} - -function parseVaultFilters(url: URL): Array<{ chainId: number; vaultAddress: string }> | null | undefined { - const vaults = url.searchParams.get('vaults') - - if (vaults !== null) { - const entries = vaults - .split(',') - .map((entry) => entry.trim()) - .filter(Boolean) - const parsedEntries = entries.map((entry) => { - const [entryChainId, entryVaultAddress] = entry.split(':') - const parsedChainId = Number(entryChainId) - - if ( - !entryChainId || - !entryVaultAddress || - !Number.isInteger(parsedChainId) || - !isValidAddress(entryVaultAddress) - ) { - return null - } - - return { chainId: parsedChainId, vaultAddress: entryVaultAddress } - }) - - if (parsedEntries.some((entry) => entry === null)) { - return null - } - - return parsedEntries.filter((entry): entry is { chainId: number; vaultAddress: string } => entry !== null) - } - - const vault = url.searchParams.get('vault') - if (vault === null) { - return undefined - } - - const chainId = url.searchParams.get('chainId') - if (!isValidAddress(vault) || !chainId || !Number.isInteger(Number(chainId))) { - return null - } - - return [{ chainId: Number(chainId), vaultAddress: vault }] -} - -function parseHoldingsEventFetchType(value: string | null): HoldingsEventFetchType { - return value === 'parallel' ? 'parallel' : 'seq' -} - -function parseHoldingsEventPaginationMode(value: string | null): HoldingsEventPaginationMode { - return value === 'all' ? 'all' : 'paged' -} - -function parseHoldingsHistoryDenomination(value: string | null): HoldingsHistoryDenomination { - return value === 'eth' ? 'eth' : 'usd' -} - -function parseHoldingsHistoryTimeframe(value: string | null): HoldingsHistoryTimeframe { - return value === 'all' ? 'all' : '1y' -} - -function parseHoldingsActivityLimit(value: string | null): number { - const parsed = Number(value) - - if (!Number.isFinite(parsed) || !Number.isInteger(parsed)) { - return 10 - } - - return Math.min(Math.max(parsed, 1), 500) -} - -function parseHoldingsActivityOffset(value: string | null): number { - const parsed = Number(value) - - if (!Number.isFinite(parsed) || !Number.isInteger(parsed)) { - return 0 - } - - return Math.max(parsed, 0) -} - -function parseHoldingsActivityType(value: string | null): HoldingsActivityTypeFilter { - return value === 'deposit' || - value === 'withdraw' || - value === 'stake' || - value === 'unstake' || - value === 'transfer' || - value === 'swap' - ? value - : 'all' -} - -function parseHoldingsActivityChainId(value: string | null): number | null { - const parsed = Number(value) - - return Number.isInteger(parsed) && parsed > 0 ? parsed : null -} - -function parseHoldingsActivityTimestamp(value: string | null): number | null { - if (!value) { - return null - } - - const parsed = Number(value) - - return Number.isInteger(parsed) && parsed >= 0 ? parsed : null -} - -function parseUtcDateParam(value: string | null): number | null { - if (!value) { - return null - } - - const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value) - if (!match) { - return null - } - - const [, year, month, day] = match - const yearNumber = Number(year) - const monthNumber = Number(month) - const dayNumber = Number(day) - const utcDate = new Date(Date.UTC(yearNumber, monthNumber - 1, dayNumber)) - - if ( - utcDate.getUTCFullYear() !== yearNumber || - utcDate.getUTCMonth() !== monthNumber - 1 || - utcDate.getUTCDate() !== dayNumber - ) { - return null - } - - const timestamp = Math.floor(utcDate.getTime() / 1000) - return Number.isFinite(timestamp) ? timestamp : null -} - -async function parseJsonBody(req: Request): Promise { - try { - return (await req.json()) as T - } catch (_error) { - throw new Error('Invalid JSON body') - } -} - -async function callTenderlyAdminRpc(canonicalChainId: number, method: string, params: unknown[]): Promise { - const configuredChain = requireTenderlyServerChain(process.env, canonicalChainId) - const response = await fetch(configuredChain.adminRpcUri as string, { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ - id: 1, - jsonrpc: '2.0', - method, - params - }) - }) - - if (!response.ok) { - const details = await response.text() - throw new Error(`Tenderly RPC request failed with status ${response.status}: ${details}`) - } - - const payload = (await response.json()) as TTenderlyJsonRpcSuccess | TTenderlyJsonRpcError - if ('error' in payload) { - throw new Error(`${payload.error.message} (code ${payload.error.code})`) - } - - return payload.result -} - -function handleTenderlyStatus(req: Request): Response { - if (req.method !== 'GET') { - return Response.json({ error: 'Method not allowed' }, { status: 405 }) - } - - try { - return Response.json(buildTenderlyPanelStatus(process.env)) - } catch (error) { - console.error('Error building Tenderly status:', error) - return Response.json( - { error: error instanceof Error ? error.message : 'Failed to build Tenderly status' }, - { status: 500 } - ) - } -} - -async function handleTenderlySnapshot(req: Request): Promise { - if (req.method !== 'POST') { - return Response.json({ error: 'Method not allowed' }, { status: 405 }) - } - - try { - const body = await parseJsonBody(req) - const configuredChain = requireTenderlyServerChain(process.env, body.canonicalChainId) - const snapshotId = await callTenderlyAdminRpc(body.canonicalChainId, 'evm_snapshot', []) - const snapshotRecord = buildTenderlySnapshotRecord({ - canonicalChainId: body.canonicalChainId, - executionChainId: configuredChain.executionChainId, - snapshotId: String(snapshotId), - label: body.label, - isBaseline: body.isBaseline - }) - - return Response.json(snapshotRecord) - } catch (error) { - console.error('Error creating Tenderly snapshot:', error) - return Response.json( - { error: error instanceof Error ? error.message : 'Failed to create Tenderly snapshot' }, - { status: 400 } - ) - } -} - -async function handleTenderlyRevert(req: Request): Promise { - if (req.method !== 'POST') { - return Response.json({ error: 'Method not allowed' }, { status: 405 }) - } - - try { - const body = await parseJsonBody(req) - const result = await callTenderlyAdminRpc(body.canonicalChainId, 'evm_revert', [body.snapshotId]) - - return Response.json(buildTenderlyRevertResponse(result, body.snapshotId)) - } catch (error) { - console.error('Error reverting Tenderly snapshot:', error) - return Response.json( - { error: error instanceof Error ? error.message : 'Failed to revert Tenderly snapshot' }, - { status: 400 } - ) - } -} - -async function handleTenderlyIncreaseTime(req: Request): Promise { - if (req.method !== 'POST') { - return Response.json({ error: 'Method not allowed' }, { status: 405 }) - } - - try { - const body = await parseJsonBody(req) - if (!Number.isInteger(body.seconds) || body.seconds <= 0) { - throw new Error('seconds must be a positive integer') - } - - const timeResult = await callTenderlyAdminRpc(body.canonicalChainId, 'evm_increaseTime', [ - `0x${BigInt(body.seconds).toString(16)}` - ]) - const mineResult = body.mineBlock ? await callTenderlyAdminRpc(body.canonicalChainId, 'evm_mine', []) : undefined - - return Response.json({ - timeResult, - mineResult - }) - } catch (error) { - console.error('Error increasing Tenderly time:', error) - return Response.json( - { error: error instanceof Error ? error.message : 'Failed to increase Tenderly time' }, - { status: 400 } - ) - } -} - -async function handleTenderlyFund(req: Request): Promise { - if (req.method !== 'POST') { - return Response.json({ error: 'Method not allowed' }, { status: 405 }) - } - - try { - const body = await parseJsonBody(req) - const { method, params } = resolveTenderlyFundRpcRequest(body) - const result = await callTenderlyAdminRpc(body.canonicalChainId, method, params) - - return Response.json({ - method, - result - }) - } catch (error) { - console.error('Error funding Tenderly wallet:', error) - return Response.json( - { error: error instanceof Error ? error.message : 'Failed to fund wallet on Tenderly' }, - { status: 400 } - ) - } -} - -async function handleSitemap(req: Request): Promise { - if (req.method !== 'GET' && req.method !== 'HEAD') { - return Response.json({ error: 'Method not allowed' }, { status: 405 }) - } - try { - const upstream = await fetch(KONG_VAULT_LIST_URL, { headers: { Accept: 'application/json' } }) - const vaults: TVaultListEntry[] = upstream.ok ? ((await upstream.json()) as TVaultListEntry[]) : [] - return new Response(req.method === 'HEAD' ? null : buildSitemap(vaults), { - headers: { - 'Content-Type': 'application/xml; charset=utf-8', - 'Cache-Control': 'public, s-maxage=86400, stale-while-revalidate=3600' - } - }) - } catch (error) { - console.error('Error generating sitemap:', error) - return new Response( - '', - { - status: 500, - headers: { 'Content-Type': 'application/xml; charset=utf-8' } - } - ) - } -} - -async function handleVaultsMarkdown(req: Request): Promise { - if (req.method !== 'GET' && req.method !== 'HEAD') { - return Response.json({ error: 'Method not allowed' }, { status: 405 }) - } - const chainIdParam = new URL(req.url).searchParams.get('chainId') - const chainId = chainIdParam && /^\d+$/.test(chainIdParam) ? Number(chainIdParam) : undefined - try { - const upstream = await fetch(KONG_VAULT_LIST_URL, { headers: { Accept: 'application/json' } }) - if (!upstream.ok) return Response.json({ error: 'Failed to fetch vault list from upstream' }, { status: 502 }) - const vaults = (await upstream.json()) as TVaultListEntry[] - return new Response(req.method === 'HEAD' ? null : buildVaultsMarkdown(vaults, chainId), { - headers: { - 'Content-Type': 'text/markdown; charset=utf-8', - 'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=600' - } - }) - } catch (error) { - console.error('Error generating vaults markdown:', error) - return Response.json({ error: 'Internal server error' }, { status: 500 }) - } -} - -async function handleVaultMarkdown(req: Request): Promise { - if (req.method !== 'GET' && req.method !== 'HEAD') { - return Response.json({ error: 'Method not allowed' }, { status: 405 }) - } - const params = new URL(req.url).searchParams - const chainId = params.get('chainId') - const address = params.get('address') - if (!chainId || !address) return Response.json({ error: 'Missing chainId or address' }, { status: 400 }) - if (!/^\d+$/.test(chainId)) return Response.json({ error: 'Invalid chainId' }, { status: 400 }) - if (!/^0x[a-fA-F0-9]{40}$/.test(address)) return Response.json({ error: 'Invalid address' }, { status: 400 }) - try { - const upstream = await fetch(`${KONG_REST_BASE}/snapshot/${chainId}/${address}`, { - headers: { Accept: 'application/json' } - }) - if (!upstream.ok) { - return Response.json( - { error: upstream.status === 404 ? 'Vault not found' : 'Upstream error' }, - { status: upstream.status === 404 ? 404 : 502 } - ) - } - const snapshot = (await upstream.json()) as TVaultSnapshot - return new Response(req.method === 'HEAD' ? null : buildVaultMarkdown(snapshot, Number(chainId), address), { - headers: { - 'Content-Type': 'text/markdown; charset=utf-8', - 'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=600' - } - }) - } catch (error) { - console.error('Error generating vault markdown:', error) - return Response.json({ error: 'Internal server error' }, { status: 500 }) - } -} - -function handleEnsoStatus(): Response { - const apiKey = process.env.ENSO_API_KEY - return Response.json({ configured: !!apiKey }) -} - -async function handleEnsoRoute(req: Request): Promise { - const url = new URL(req.url) - const fromAddress = url.searchParams.get('fromAddress') - const chainId = url.searchParams.get('chainId') - const tokenIn = url.searchParams.get('tokenIn') - const tokenOut = url.searchParams.get('tokenOut') - const amountIn = url.searchParams.get('amountIn') - const slippage = url.searchParams.get('slippage') || '100' - const routingStrategy = url.searchParams.get('routingStrategy') - const destinationChainId = url.searchParams.get('destinationChainId') - const receiver = url.searchParams.get('receiver') - - if (!fromAddress || !chainId || !tokenIn || !tokenOut || !amountIn) { - return Response.json({ error: 'Missing required parameters' }, { status: 400 }) - } - - const apiKey = process.env.ENSO_API_KEY - if (!apiKey) { - console.error('ENSO_API_KEY not configured') - return Response.json({ error: 'Enso API not configured' }, { status: 500 }) - } - - const params = new URLSearchParams({ - fromAddress, - chainId, - tokenIn, - tokenOut, - amountIn, - slippage - }) - - if (destinationChainId) { - params.set('destinationChainId', destinationChainId) - } - if (receiver) { - params.set('receiver', receiver) - } - if (routingStrategy) { - params.set('routingStrategy', routingStrategy) - } - - const ensoUrl = `${ENSO_API_BASE}/api/v1/shortcuts/route?${params}` - - try { - const response = await fetch(ensoUrl, { - headers: { - Authorization: `Bearer ${apiKey}`, - 'Content-Type': 'application/json' - } - }) - - const data = await response.json() - - if (!response.ok) { - return Response.json(data, { status: response.status }) - } - - return Response.json(data) - } catch (error) { - console.error('Error proxying Enso route request:', error) - return Response.json({ error: 'Internal server error' }, { status: 500 }) - } -} - -async function handleEnsoBalances(req: Request): Promise { - const url = new URL(req.url) - const eoaAddress = url.searchParams.get('eoaAddress') - const chainId = url.searchParams.get('chainId') - - if (!eoaAddress) { - return Response.json({ error: 'Missing eoaAddress' }, { status: 400 }) - } - - const apiKey = process.env.ENSO_API_KEY - if (!apiKey) { - console.error('ENSO_API_KEY not configured') - return Response.json({ error: 'Enso API not configured' }, { status: 500 }) - } - - const params = new URLSearchParams({ - eoaAddress, - useEoa: 'true', - chainId: chainId || 'all' - }) - - const ensoUrl = `${ENSO_API_BASE}/api/v1/wallet/balances?${params}` - - try { - const response = await fetch(ensoUrl, { - headers: { - Authorization: `Bearer ${apiKey}` - } - }) - - if (!response.ok) { - const errorText = await response.text() - console.error(`Enso API error: ${response.status}`, errorText) - return Response.json( - { error: 'Enso API error', status: response.status, details: errorText }, - { status: response.status } - ) - } - - const data = await response.json() - return Response.json(data, { - headers: { - 'Cache-Control': ENSO_BALANCES_CACHE_CONTROL - } - }) - } catch (error) { - console.error('Error proxying Enso request:', error) - return Response.json({ error: 'Internal server error' }, { status: 500 }) - } -} - -const CHANGE_CACHE_CONTROL = 'public, s-maxage=600, stale-while-revalidate=60' -const ALIGNMENT_CACHE_CONTROL = 'public, s-maxage=60, stale-while-revalidate=30' -const VAULT_STATE_CACHE_CONTROL = 'public, s-maxage=60, stale-while-revalidate=30' -const HOLDINGS_HISTORY_CACHE_CONTROL = 'public, s-maxage=300, stale-while-revalidate=600' -const HOLDINGS_ACTIVITY_CACHE_CONTROL = 'public, s-maxage=60, stale-while-revalidate=300' -const HOLDINGS_ACTIVITY_FACETS_CACHE_CONTROL = 'public, s-maxage=300, stale-while-revalidate=900' - -async function handleOptimizationChange(req: Request): Promise { - if (req.method !== 'GET') { - return Response.json({ error: 'Method not allowed' }, { status: 405 }) - } - - try { - const optimizations = await readOptimizations() - if (!optimizations || optimizations.length === 0) { - return Response.json({ error: 'No optimization data available' }, { status: 404 }) - } - - const url = new URL(req.url) - const requestedVault = url.searchParams.get('vault') - if (requestedVault) { - if (isHistoryQueryEnabled(url.searchParams.get('history'))) { - const selectedHistory = optimizations.filter((optimization) => { - return optimization.vault.toLowerCase() === requestedVault.toLowerCase() - }) - if (selectedHistory.length === 0) { - return Response.json({ error: `Vault not found in optimization payload: ${requestedVault}` }, { status: 404 }) - } - - return Response.json(selectedHistory, { - headers: getVercelCdnCacheHeaders(CHANGE_CACHE_CONTROL) - }) - } - - const selected = findVaultOptimization(optimizations, requestedVault) - if (!selected) { - return Response.json({ error: `Vault not found in optimization payload: ${requestedVault}` }, { status: 404 }) - } - - return Response.json(selected, { - headers: getVercelCdnCacheHeaders(CHANGE_CACHE_CONTROL) - }) - } - - return Response.json(optimizations, { - headers: getVercelCdnCacheHeaders(CHANGE_CACHE_CONTROL) - }) - } catch (error) { - if (isRedisAuthenticationError(error)) { - return Response.json({ error: REDIS_AUTHENTICATION_ERROR_MESSAGE }, { status: 500 }) - } - - if (isRedisConnectivityError(error)) { - return Response.json({ error: REDIS_CONNECTIVITY_ERROR_MESSAGE }, { status: 503 }) - } - - const message = error instanceof Error ? error.message : String(error) - return Response.json({ error: message }, { status: 500 }) - } -} - -async function handleOptimizationAlignment(req: Request): Promise { - if (req.method !== 'GET') { - return Response.json({ error: 'Method not allowed' }, { status: 405 }) - } - - const url = new URL(req.url) - const vault = url.searchParams.get('vault') - if (!vault) { - return Response.json({ error: 'vault parameter required' }, { status: 400 }) - } - - const envioUrl = process.env.ENVIO_GRAPHQL_URL - if (!envioUrl) { - return Response.json({ error: 'ENVIO_GRAPHQL_URL not configured' }, { status: 503 }) - } - - try { - const optimizations = await readOptimizations() - if (!optimizations || optimizations.length === 0) { - return Response.json({ error: 'No optimization data available' }, { status: 404 }) - } - - const optimization = findVaultOptimization(optimizations, vault) - if (!optimization) { - return Response.json({ error: `Vault not found: ${vault}` }, { status: 404 }) - } - - const metadataChainId = optimization.source.chainId ? undefined : parseExplainMetadata(optimization.explain).chainId - const chainId = optimization.source.chainId ?? metadataChainId - if (!chainId) { - return Response.json({ error: 'Could not determine chain ID for vault' }, { status: 400 }) - } - - const timestampStr = optimization.source.latestMatchedTimestampUtc ?? optimization.source.timestampUtc - if (!timestampStr) { - return Response.json({ error: 'No timestamp available for vault snapshot' }, { status: 400 }) - } - - const fromTs = Math.floor(new Date(timestampStr.replace(' UTC', 'Z').replace(' ', 'T')).getTime() / 1000) - const numStrategies = optimization.strategyDebtRatios.length - const toTs = fromTs + numStrategies * 10 * 60 * 2 - const decimals = getVaultDecimals(vault) - const events = await fetchAlignedEvents( - envioUrl, - vault, - chainId, - optimization.strategyDebtRatios, - fromTs, - toTs, - decimals - ) - - return Response.json(events, { - headers: getVercelCdnCacheHeaders(ALIGNMENT_CACHE_CONTROL) - }) - } catch (error) { - if (isRedisAuthenticationError(error)) { - return Response.json({ error: REDIS_AUTHENTICATION_ERROR_MESSAGE }, { status: 500 }) - } - - if (isRedisConnectivityError(error)) { - return Response.json({ error: REDIS_CONNECTIVITY_ERROR_MESSAGE }, { status: 503 }) - } - - const message = error instanceof Error ? error.message : String(error) - return Response.json({ error: message }, { status: 500 }) - } -} - -async function handleOptimizationVaultState(req: Request): Promise { - if (req.method !== 'POST') { - return Response.json({ error: 'Method not allowed' }, { status: 405 }) - } - - let body: unknown - try { - body = await req.json() - } catch { - return Response.json({ error: 'Invalid JSON body' }, { status: 400 }) - } - - const payload = body && typeof body === 'object' ? (body as Record) : {} - const vault = typeof payload.vault === 'string' ? payload.vault : null - const chainId = typeof payload.chainId === 'number' ? payload.chainId : null - const strategies = Array.isArray(payload.strategies) - ? payload.strategies.filter((strategy: unknown): strategy is string => typeof strategy === 'string') - : [] - - if (!vault || !isValidAddress(vault)) { - return Response.json({ error: 'Invalid vault address' }, { status: 400 }) - } - - if (chainId === null || !Number.isFinite(chainId)) { - return Response.json({ error: 'Invalid chainId' }, { status: 400 }) - } - - if (strategies.length === 0) { - return Response.json({ error: 'No strategy addresses provided' }, { status: 400 }) - } - - try { - const state = await fetchVaultOnChainState(chainId, vault, strategies) - const strategyDebts = Object.fromEntries( - [...state.strategyDebts].map(([strategyAddress, debt]) => [strategyAddress, debt.toString()]) - ) - - return Response.json( - { - totalAssets: state.totalAssets.toString(), - strategyDebts, - unallocatedBps: state.unallocatedBps - }, - { - headers: getVercelCdnCacheHeaders(VAULT_STATE_CACHE_CONTROL) - } - ) - } catch (error) { - const message = error instanceof Error ? error.message : String(error) - return Response.json({ error: message }, { status: 503 }) - } -} - -async function handleHoldingsHistory(req: Request): Promise { - const url = new URL(req.url) - const address = url.searchParams.get('address') - const versionParam = url.searchParams.get('version') - const fetchType = parseHoldingsEventFetchType(url.searchParams.get('fetchType')) - const paginationMode = parseHoldingsEventPaginationMode(url.searchParams.get('paginationMode')) - const denomination = parseHoldingsHistoryDenomination(url.searchParams.get('denomination')) - const timeframe = parseHoldingsHistoryTimeframe(url.searchParams.get('timeframe')) - const vaultFilters = parseVaultFilters(url) - const debugEnabled = - isHoldingsDebugRequested(url.searchParams.get('debug')) || isHoldingsDebugRequested(process.env.HOLDINGS_DEBUG) - const debugLotsEnabled = isHoldingsDebugRequested(url.searchParams.get('debugLots')) - const debugVault = url.searchParams.get('debugVault') - const debugTx = url.searchParams.get('debugTx') - const refreshParam = url.searchParams.get('refresh') - const progressId = url.searchParams.get('progressId') - const refresh = refreshParam === 'true' || refreshParam === '1' - - if (!address) { - return Response.json({ error: 'Missing required parameter: address', status: 400 }, { status: 400 }) - } - - if (!isValidAddress(address)) { - return Response.json({ error: 'Invalid Ethereum address', status: 400 }, { status: 400 }) - } - - if (vaultFilters === null) { - return Response.json({ error: 'Invalid vault filter', status: 400 }, { status: 400 }) - } - - const version: VaultVersion = versionParam === 'v2' || versionParam === 'v3' ? versionParam : 'all' - - try { - const activeProgressId = await startHoldingsProgress({ - id: progressId, - route: 'history', - address, - message: 'Fetching historical user data' - }) - await updateHoldingsProgress(activeProgressId, { - progress: 8, - message: 'Fetching historical user data', - detail: null - }) - - if (refresh) { - const cleared = await clearUserCache(address, getHoldingsTotalsCacheVersion(version)) - console.log(`[Server] Cleared ${cleared} cached entries for ${address}`) - } - - const holdings = await withHoldingsDebugContext( - createHoldingsDebugContext('history', address, debugEnabled, { - lotsEnabled: debugLotsEnabled, - vaultFilter: debugVault, - txFilter: debugTx, - progressId: activeProgressId - }), - async () => { - debugLog('route', 'started holdings history request', { - version, - fetchType, - paginationMode, - refresh, - debugLotsEnabled, - debugVault: debugVault?.toLowerCase() ?? null, - debugTx: debugTx?.toLowerCase() ?? null - }) - - try { - const response = await getHistoricalHoldingsChart( - address, - version, - fetchType, - paginationMode, - denomination, - timeframe, - vaultFilters - ) - debugLog('route', 'completed holdings history request', { - version, - fetchType, - paginationMode, - denomination, - timeframe, - refresh, - points: response.dataPoints.length, - nonZeroPoints: response.dataPoints.filter((point) => point.value > 0).length - }) - return response - } catch (error) { - debugError('route', 'holdings history request failed', error, { version, fetchType, paginationMode }) - throw error - } - } - ) - - if (!holdings.hasActivity) { - await updateHoldingsProgress(activeProgressId, { - status: 'complete', - progress: 100, - message: 'No historical holdings found', - detail: null - }) - return Response.json({ error: 'No holdings found for address', status: 404 }, { status: 404 }) - } - - await updateHoldingsProgress(activeProgressId, { - status: 'complete', - progress: 100, - message: 'Historical user data ready', - detail: `${holdings.dataPoints.length} chart points` - }) - - return Response.json( - { - address: holdings.address, - version, - denomination, - timeframe, - dataPoints: holdings.dataPoints.map((dp) => ({ - date: dp.date, - value: dp.value - })) - }, - { - headers: getVercelCdnCacheHeaders(HOLDINGS_HISTORY_CACHE_CONTROL) - } - ) - } catch (error) { - await updateHoldingsProgress(progressId, { - status: 'error', - message: 'Failed to fetch historical user data', - detail: error instanceof Error ? error.message : String(error) - }) - console.error('Error fetching holdings history:', error) - const message = error instanceof Error ? error.message : String(error) - const stack = error instanceof Error ? error.stack : undefined - return Response.json({ error: 'Failed to fetch historical holdings', message, stack, status: 502 }, { status: 502 }) - } -} - -async function handleHoldingsActivity(req: Request): Promise { - const url = new URL(req.url) - const address = url.searchParams.get('address') - const versionParam = url.searchParams.get('version') - const limit = parseHoldingsActivityLimit(url.searchParams.get('limit')) - const offset = parseHoldingsActivityOffset(url.searchParams.get('offset')) - const type = parseHoldingsActivityType(url.searchParams.get('type')) - const chainId = parseHoldingsActivityChainId(url.searchParams.get('chainId')) - const startTimestamp = parseHoldingsActivityTimestamp(url.searchParams.get('startTimestamp')) - const endTimestamp = parseHoldingsActivityTimestamp(url.searchParams.get('endTimestamp')) - - if (!address) { - return Response.json({ error: 'Missing required parameter: address', status: 400 }, { status: 400 }) - } - - if (!isValidAddress(address)) { - return Response.json({ error: 'Invalid Ethereum address', status: 400 }, { status: 400 }) - } - - const version: VaultVersion = versionParam === 'v2' || versionParam === 'v3' ? versionParam : 'all' - - try { - const activity = await getHoldingsActivity(address, version, limit, offset, { - type, - chainId, - startTimestamp, - endTimestamp - }) - - return Response.json(activity, { - headers: getVercelCdnCacheHeaders(HOLDINGS_ACTIVITY_CACHE_CONTROL) - }) - } catch (error) { - console.error('Error fetching holdings activity:', error) - const message = error instanceof Error ? error.message : String(error) - const stack = error instanceof Error ? error.stack : undefined - return Response.json({ error: 'Failed to fetch holdings activity', message, stack, status: 502 }, { status: 502 }) - } -} - -async function handleHoldingsActivityFacets(req: Request): Promise { - const url = new URL(req.url) - const address = url.searchParams.get('address') - const versionParam = url.searchParams.get('version') - - if (!address) { - return Response.json({ error: 'Missing required parameter: address', status: 400 }, { status: 400 }) - } - - if (!isValidAddress(address)) { - return Response.json({ error: 'Invalid Ethereum address', status: 400 }, { status: 400 }) - } - - const version: VaultVersion = versionParam === 'v2' || versionParam === 'v3' ? versionParam : 'all' - - try { - const facetsResponse = await getHoldingsActivityFacetResponse(address, version) - - return Response.json(facetsResponse, { - headers: getVercelCdnCacheHeaders(HOLDINGS_ACTIVITY_FACETS_CACHE_CONTROL) - }) - } catch (error) { - console.error('Error fetching holdings activity facets:', error) - const message = error instanceof Error ? error.message : String(error) - const stack = error instanceof Error ? error.stack : undefined - return Response.json( - { error: 'Failed to fetch holdings activity facets', message, stack, status: 502 }, - { status: 502 } - ) - } -} - -async function handleHoldingsBreakdown(req: Request): Promise { - const url = new URL(req.url) - const address = url.searchParams.get('address') - const dateParam = url.searchParams.get('date') - const versionParam = url.searchParams.get('version') - const fetchType = parseHoldingsEventFetchType(url.searchParams.get('fetchType')) - const paginationMode = parseHoldingsEventPaginationMode(url.searchParams.get('paginationMode')) - const debugEnabled = - isHoldingsDebugRequested(url.searchParams.get('debug')) || isHoldingsDebugRequested(process.env.HOLDINGS_DEBUG) - const debugLotsEnabled = isHoldingsDebugRequested(url.searchParams.get('debugLots')) - const debugVault = url.searchParams.get('debugVault') - const debugTx = url.searchParams.get('debugTx') - - if (!address) { - return Response.json({ error: 'Missing required parameter: address', status: 400 }, { status: 400 }) - } - - if (!isValidAddress(address)) { - return Response.json({ error: 'Invalid Ethereum address', status: 400 }, { status: 400 }) - } - - const breakdownTimestamp = parseUtcDateParam(dateParam) - if (dateParam && breakdownTimestamp === null) { - return Response.json({ error: 'Invalid date format, expected YYYY-MM-DD', status: 400 }, { status: 400 }) - } - - const version: VaultVersion = versionParam === 'v2' || versionParam === 'v3' ? versionParam : 'all' - - try { - const breakdown = await withHoldingsDebugContext( - createHoldingsDebugContext('breakdown', address, debugEnabled, { - lotsEnabled: debugLotsEnabled, - vaultFilter: debugVault, - txFilter: debugTx - }), - async () => { - debugLog('route', 'started holdings breakdown request', { - version, - date: dateParam, - fetchType, - paginationMode, - debugLotsEnabled, - debugVault: debugVault?.toLowerCase() ?? null, - debugTx: debugTx?.toLowerCase() ?? null - }) - - try { - const response = await getHoldingsBreakdown( - address, - version, - fetchType, - paginationMode, - breakdownTimestamp ?? undefined - ) - debugLog('route', 'completed holdings breakdown request', { - version, - date: response.date, - fetchType, - paginationMode, - timestamp: response.timestamp, - totalVaults: response.summary.totalVaults, - vaultsWithShares: response.summary.vaultsWithShares, - totalUsdValue: response.summary.totalUsdValue - }) - return response - } catch (error) { - debugError('route', 'holdings breakdown request failed', error, { - version, - date: dateParam, - fetchType, - paginationMode - }) - throw error - } - } - ) - - return Response.json(breakdown, { - headers: getVercelCdnCacheHeaders(HOLDINGS_HISTORY_CACHE_CONTROL) - }) - } catch (error) { - console.error('Error fetching holdings breakdown:', error) - const message = error instanceof Error ? error.message : String(error) - const stack = error instanceof Error ? error.stack : undefined - return Response.json({ error: 'Failed to fetch holdings breakdown', message, stack, status: 502 }, { status: 502 }) - } -} - -async function handleHoldingsProtocolReturnHistory(req: Request): Promise { - const url = new URL(req.url) - const address = url.searchParams.get('address') - const versionParam = url.searchParams.get('version') - const vaultFilters = parseVaultFilters(url) - const timeframe = parseHoldingsHistoryTimeframe(url.searchParams.get('timeframe')) - const debugEnabled = - isHoldingsDebugRequested(url.searchParams.get('debug')) || isHoldingsDebugRequested(process.env.HOLDINGS_DEBUG) - const debugLotsEnabled = isHoldingsDebugRequested(url.searchParams.get('debugLots')) - const debugVault = url.searchParams.get('debugVault') - const debugTx = url.searchParams.get('debugTx') - const fetchType = parseHoldingsEventFetchType(url.searchParams.get('fetchType')) - const paginationMode = parseHoldingsEventPaginationMode(url.searchParams.get('paginationMode')) - const progressId = url.searchParams.get('progressId') - - if (!address) { - return Response.json({ error: 'Missing required parameter: address', status: 400 }, { status: 400 }) - } - - if (!isValidAddress(address)) { - return Response.json({ error: 'Invalid Ethereum address', status: 400 }, { status: 400 }) - } - - if (vaultFilters === null) { - return Response.json({ error: 'Invalid vault filter', status: 400 }, { status: 400 }) - } - - const version: VaultVersion = versionParam === 'v2' || versionParam === 'v3' ? versionParam : 'all' - - try { - const activeProgressId = await startHoldingsProgress({ - id: progressId, - route: 'pnl-simple-history', - address, - message: 'Fetching historical user data' - }) - await updateHoldingsProgress(activeProgressId, { - progress: 8, - message: 'Fetching historical user data', - detail: null - }) - - const history = await withHoldingsDebugContext( - createHoldingsDebugContext('protocol-return-history', address, debugEnabled, { - lotsEnabled: debugLotsEnabled, - vaultFilter: debugVault, - txFilter: debugTx, - progressId: activeProgressId - }), - async () => { - debugLog('route', 'started holdings protocol return history request', { - version, - timeframe, - vaults: vaultFilters?.map((vault) => `${vault.chainId}:${vault.vaultAddress.toLowerCase()}`) ?? null, - fetchType, - paginationMode, - debugLotsEnabled, - debugVault: debugVault?.toLowerCase() ?? null, - debugTx: debugTx?.toLowerCase() ?? null - }) - - try { - const response = await getHoldingsProtocolReturnHistory( - address, - version, - fetchType, - paginationMode, - timeframe, - vaultFilters - ) - debugLog('route', 'completed holdings protocol return history request', { - version, - timeframe, - vaults: vaultFilters?.map((vault) => `${vault.chainId}:${vault.vaultAddress.toLowerCase()}`) ?? null, - fetchType, - paginationMode, - totalVaults: response.summary.totalVaults, - points: response.dataPoints.length - }) - return response - } catch (error) { - debugError('route', 'holdings protocol return history request failed', error, { - version, - timeframe, - vaults: vaultFilters?.map((vault) => `${vault.chainId}:${vault.vaultAddress.toLowerCase()}`) ?? null, - fetchType, - paginationMode - }) - throw error - } - } - ) - - if (history.summary.totalVaults === 0) { - await updateHoldingsProgress(activeProgressId, { - status: 'complete', - progress: 100, - message: 'No historical holdings found', - detail: null - }) - return Response.json({ error: 'No holdings found for address', status: 404 }, { status: 404 }) - } - - await updateHoldingsProgress(activeProgressId, { - status: 'complete', - progress: 100, - message: 'Historical user data ready', - detail: `${history.dataPoints.length} chart points` - }) - - return Response.json(history, { - headers: getVercelCdnCacheHeaders(HOLDINGS_HISTORY_CACHE_CONTROL) - }) - } catch (error) { - await updateHoldingsProgress(progressId, { - status: 'error', - message: 'Failed to fetch historical user data', - detail: error instanceof Error ? error.message : String(error) - }) - console.error('Error fetching holdings protocol return history:', error) - const message = error instanceof Error ? error.message : String(error) - const stack = error instanceof Error ? error.stack : undefined - return Response.json( - { error: 'Failed to fetch holdings protocol return history', message, stack, status: 502 }, - { status: 502 } - ) - } -} - -async function main() { - process.on('uncaughtException', (error) => { - console.error('đŸ’Ĩ Uncaught Exception:', error) - }) - - process.on('unhandledRejection', (reason, promise) => { - console.error('đŸ’Ĩ Unhandled Rejection at:', promise, 'reason:', reason) - }) - - validateConfig() - - await initializeHoldingsStorage() - - serve({ - async fetch(req, server) { - const url = new URL(req.url) - console.log(`[Server] ${req.method} ${url.pathname}`) - - try { - if (req.method === 'OPTIONS') { - return handleCorsPreFlight() - } - - if (url.pathname === '/api/sitemap') { - return withCors(await handleSitemap(req)) - } - - if (url.pathname === '/api/vaults/markdown') { - return withCors(await handleVaultsMarkdown(req)) - } - - if (url.pathname === '/api/vault/markdown') { - return withCors(await handleVaultMarkdown(req)) - } - - if (url.pathname === '/api/enso/status') { - return withCors(handleEnsoStatus()) - } - - if (url.pathname === '/api/enso/balances') { - return withCors(await handleEnsoBalances(req)) - } - - if (url.pathname === '/api/enso/route') { - return withCors(await handleEnsoRoute(req)) - } - - if (url.pathname === '/api/holdings/history') { - return withCors(await handleHoldingsHistory(req)) - } - - if (url.pathname === '/api/holdings/progress') { - return withCors(await handleHoldingsProgress(req)) - } - - if (url.pathname === '/api/holdings/activity') { - return withCors(await handleHoldingsActivity(req)) - } - - if (url.pathname === '/api/holdings/activity-facets') { - return withCors(await handleHoldingsActivityFacets(req)) - } - - if (url.pathname === '/api/holdings/breakdown') { - return withCors(await handleHoldingsBreakdown(req)) - } - - if ( - url.pathname === '/api/holdings/protocol-return/history' || - url.pathname === '/api/holdings/pnl/simple-history' - ) { - return withCors(await handleHoldingsProtocolReturnHistory(req)) - } - - if (url.pathname === '/api/yvusd/aprs') { - return withCors(await handleYvUsdAprs(req)) - } - - if (url.pathname === '/api/optimization/change') { - return withCors(await handleOptimizationChange(req)) - } - - if (url.pathname === '/api/optimization/alignment') { - return withCors(await handleOptimizationAlignment(req)) - } - - if (url.pathname === '/api/optimization/vault-state') { - return withCors(await handleOptimizationVaultState(req)) - } - - if (url.pathname === '/api/tenderly/status') { - return withCors(handleTenderlyStatus(req)) - } - - if (url.pathname === '/api/tenderly/snapshot') { - const accessDeniedResponse = buildTenderlyAdminAccessDeniedResponse(server.requestIP(req)?.address) - if (accessDeniedResponse) { - return withCors(accessDeniedResponse) - } - return withCors(await handleTenderlySnapshot(req)) - } - - if (url.pathname === '/api/tenderly/revert') { - const accessDeniedResponse = buildTenderlyAdminAccessDeniedResponse(server.requestIP(req)?.address) - if (accessDeniedResponse) { - return withCors(accessDeniedResponse) - } - return withCors(await handleTenderlyRevert(req)) - } - - if (url.pathname === '/api/tenderly/increase-time') { - const accessDeniedResponse = buildTenderlyAdminAccessDeniedResponse(server.requestIP(req)?.address) - if (accessDeniedResponse) { - return withCors(accessDeniedResponse) - } - return withCors(await handleTenderlyIncreaseTime(req)) - } - - if (url.pathname === '/api/tenderly/fund') { - const accessDeniedResponse = buildTenderlyAdminAccessDeniedResponse(server.requestIP(req)?.address) - if (accessDeniedResponse) { - return withCors(accessDeniedResponse) - } - return withCors(await handleTenderlyFund(req)) - } - - return withCors(new Response('Not found', { status: 404 })) - } catch (error) { - console.error('đŸ’Ĩ Request handler error:', error) - return withCors( - Response.json( - { error: 'Internal server error', message: error instanceof Error ? error.message : String(error) }, - { status: 500 } - ) - ) - } - }, - port: API_PORT, - idleTimeout: 120 - }) - - console.log(`🚀 API server running on http://localhost:${API_PORT}`) - console.log(`📊 Holdings API: http://localhost:${API_PORT}/api/holdings/history?address=0x...`) - console.log(`đŸ—‚ī¸ Holdings Activity API: http://localhost:${API_PORT}/api/holdings/activity?address=0x...`) - console.log(`🧩 Holdings Breakdown API: http://localhost:${API_PORT}/api/holdings/breakdown?address=0x...`) - console.log(`💹 PnL API: http://localhost:${API_PORT}/api/holdings/pnl?address=0x...`) - console.log(`📈 Simple PnL API: http://localhost:${API_PORT}/api/holdings/pnl/simple?address=0x...`) - console.log(`📊 Simple PnL History API: http://localhost:${API_PORT}/api/holdings/pnl/simple-history?address=0x...`) - console.log(`🧾 PnL Drilldown API: http://localhost:${API_PORT}/api/holdings/pnl/drilldown?address=0x...`) -} - -main().catch((error) => { - console.error('Failed to start server:', error) - process.exit(1) -}) diff --git a/api/sitemap.ts b/api/sitemap.ts deleted file mode 100644 index cb3af2caa..000000000 --- a/api/sitemap.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { VercelRequest, VercelResponse } from '@vercel/node' -import type { TVaultListEntry } from './lib/aio' -import { buildSitemap, KONG_VAULT_LIST_URL } from './lib/aio' - -export default async function handler(req: VercelRequest, res: VercelResponse) { - if (req.method !== 'GET') { - return res.status(405).json({ error: 'Method not allowed' }) - } - - try { - const response = await fetch(KONG_VAULT_LIST_URL, { - headers: { Accept: 'application/json' } - }) - const vaults: TVaultListEntry[] = response.ok ? ((await response.json()) as TVaultListEntry[]) : [] - - res.setHeader('Content-Type', 'application/xml; charset=utf-8') - res.setHeader('Cache-Control', 'public, s-maxage=86400, stale-while-revalidate=3600') - return res.status(200).send(buildSitemap(vaults)) - } catch (error) { - console.error('Error generating sitemap:', error) - return res - .status(500) - .send( - '' - ) - } -} diff --git a/api/tsconfig.json b/api/tsconfig.json deleted file mode 100644 index a2e462520..000000000 --- a/api/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "commonjs", - "moduleResolution": "node", - "lib": ["ES2022"], - "allowJs": true, - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "allowSyntheticDefaultImports": true - }, - "include": ["**/*.ts"], - "exclude": ["node_modules"] -} diff --git a/api/vault/markdown.ts b/api/vault/markdown.ts deleted file mode 100644 index f6bf48c6c..000000000 --- a/api/vault/markdown.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './vault-markdown' diff --git a/api/vault/meta.ts b/api/vault/meta.ts deleted file mode 100644 index 82bce222d..000000000 --- a/api/vault/meta.ts +++ /dev/null @@ -1,114 +0,0 @@ -import type { VercelRequest, VercelResponse } from '@vercel/node' -import { readFileSync } from 'fs' -import { join } from 'path' - -// Load HTML at module level (happens once per container cold start) -const indexPath = join(process.cwd(), 'dist', 'index.html') -const baseHtml = readFileSync(indexPath, 'utf-8') - -export default async function handler(req: VercelRequest, res: VercelResponse) { - const { chainId, address } = req.query - - if (!chainId || !address || typeof chainId !== 'string' || typeof address !== 'string') { - return res.status(400).json({ error: 'Missing or invalid chainId or address' }) - } - - // Strict allowlist validation to prevent XSS via parameter injection - if (!/^\d+$/.test(chainId)) { - return res.status(400).json({ error: 'Invalid chainId' }) - } - if (!/^0x[a-fA-F0-9]{40}$/.test(address)) { - return res.status(400).json({ error: 'Invalid address' }) - } - - try { - let html = baseHtml - - // Generate dynamic meta tags - const ogBaseUrl = 'https://og.yearn.fi' - const ogImageUrl = `${ogBaseUrl}/api/og/yearn/vault/${chainId}/${address}` - const canonicalUrl = `https://yearn.fi/vaults/${chainId}/${address}` - const markdownUrl = `https://yearn.fi/api/vault/markdown?chainId=${chainId}&address=${address}` - const snapshotUrl = `https://kong.yearn.fi/api/rest/snapshot/${chainId}/${address}` - - const title = 'Yearn Vault' - const description = "Earn yield on your crypto with Yearn's automated vault strategies" - - const safeJson = (obj: unknown): string => JSON.stringify(obj).replace(/<\/script>/gi, '<\\/script>') - - const organizationSchema = safeJson({ - '@context': 'https://schema.org', - '@type': 'Organization', - name: 'Yearn', - url: 'https://yearn.fi', - logo: 'https://yearn.fi/logo.svg', - description: 'DeFi yield aggregator — automatically maximize returns on deposited digital assets.', - sameAs: ['https://x.com/yearnfi', 'https://github.com/yearn', 'https://discord.gg/yearn/'] - }) - - const vaultSchema = safeJson({ - '@context': 'https://schema.org', - '@type': 'FinancialProduct', - name: title, - description, - url: canonicalUrl, - sameAs: [snapshotUrl, markdownUrl], - provider: { '@type': 'Organization', name: 'Yearn', url: 'https://yearn.fi' } - }) - - // Inject meta tags - const metaTags = ` - ${title} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ` - - // Remove existing meta tags that we're replacing - html = html.replace(/.*?<\/title>/gi, '') - html = html.replace(/<meta property="og:.*?".*?>/gi, '') - html = html.replace(/<meta name="twitter:.*?".*?>/gi, '') - html = html.replace(/<meta name="description".*?>/gi, '') - - // Inject new meta tags - html = html.replace('</head>', `${metaTags}\n </head>`) - - res.setHeader('Content-Type', 'text/html') - res.setHeader('Vercel-CDN-Cache-Control', 'public, s-maxage=86400, stale-while-revalidate=604800') - res.setHeader('Cache-Control', 'public, max-age=0, must-revalidate') - - return res.status(200).send(html) - } catch (error) { - console.error('Error generating meta tags:', error) - // Fallback to regular SPA with cached HTML - res.setHeader('Content-Type', 'text/html') - res.setHeader('Vercel-CDN-Cache-Control', 'public, s-maxage=86400, stale-while-revalidate=604800') - res.setHeader('Cache-Control', 'public, max-age=0, must-revalidate') - return res.status(200).send(baseHtml) - } -} diff --git a/api/vault/vault-markdown.ts b/api/vault/vault-markdown.ts deleted file mode 100644 index 7c1b51bb1..000000000 --- a/api/vault/vault-markdown.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { VercelRequest, VercelResponse } from '@vercel/node' -import type { TVaultSnapshot } from '../lib/aio' -import { buildVaultMarkdown, KONG_REST_BASE } from '../lib/aio' - -export default async function handler(req: VercelRequest, res: VercelResponse) { - if (req.method !== 'GET') { - return res.status(405).json({ error: 'Method not allowed' }) - } - - const { chainId, address } = req.query - - if (!chainId || !address || typeof chainId !== 'string' || typeof address !== 'string') { - return res.status(400).json({ error: 'Missing or invalid chainId or address' }) - } - if (!/^\d+$/.test(chainId)) { - return res.status(400).json({ error: 'Invalid chainId' }) - } - if (!/^0x[a-fA-F0-9]{40}$/.test(address)) { - return res.status(400).json({ error: 'Invalid address' }) - } - - const chainIdNum = Number(chainId) - - try { - const response = await fetch(`${KONG_REST_BASE}/snapshot/${chainId}/${address}`, { - headers: { Accept: 'application/json' } - }) - - if (!response.ok) { - return res.status(response.status === 404 ? 404 : 502).json({ - error: response.status === 404 ? 'Vault not found' : 'Failed to fetch vault data from upstream' - }) - } - - const snapshot = (await response.json()) as TVaultSnapshot - - res.setHeader('Content-Type', 'text/markdown; charset=utf-8') - res.setHeader('Cache-Control', 'public, s-maxage=300, stale-while-revalidate=600') - return res.status(200).send(buildVaultMarkdown(snapshot, chainIdNum, address)) - } catch (error) { - console.error('Error generating vault markdown:', error) - return res.status(500).json({ error: 'Internal server error' }) - } -} diff --git a/api/vaults/markdown.ts b/api/vaults/markdown.ts deleted file mode 100644 index 7f18a8c18..000000000 --- a/api/vaults/markdown.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { VercelRequest, VercelResponse } from '@vercel/node' -import type { TVaultListEntry } from '../lib/aio' -import { buildVaultsMarkdown, KONG_VAULT_LIST_URL } from '../lib/aio' - -export default async function handler(req: VercelRequest, res: VercelResponse) { - if (req.method !== 'GET') { - return res.status(405).json({ error: 'Method not allowed' }) - } - - const { chainId: chainIdParam } = req.query - const chainId = - chainIdParam && typeof chainIdParam === 'string' && /^\d+$/.test(chainIdParam) ? Number(chainIdParam) : undefined - - try { - const response = await fetch(KONG_VAULT_LIST_URL, { - headers: { Accept: 'application/json' } - }) - - if (!response.ok) { - return res.status(502).json({ error: 'Failed to fetch vault list from upstream' }) - } - - const vaults = (await response.json()) as TVaultListEntry[] - - res.setHeader('Content-Type', 'text/markdown; charset=utf-8') - res.setHeader('Cache-Control', 'public, s-maxage=300, stale-while-revalidate=600') - return res.status(200).send(buildVaultsMarkdown(vaults, chainId)) - } catch (error) { - console.error('Error generating vaults markdown:', error) - return res.status(500).json({ error: 'Internal server error' }) - } -} diff --git a/api/yvusd/aprs.ts b/api/yvusd/aprs.ts deleted file mode 100644 index e86076a99..000000000 --- a/api/yvusd/aprs.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { VercelRequest, VercelResponse } from '@vercel/node' -import { setVercelCdnCacheHeaders } from '../lib/cacheHeaders' - -const YVUSD_APR_SERVICE_API = ( - process.env.YVUSD_APR_SERVICE_API || 'https://yearn-yvusd-apr-service.vercel.app/api/aprs' -).replace(/\/$/, '') -const YVUSD_APR_CDN_CACHE_CONTROL = 'public, s-maxage=30, stale-while-revalidate=120' - -export default async function handler(req: VercelRequest, res: VercelResponse) { - if (req.method !== 'GET') { - return res.status(405).json({ error: 'Method not allowed' }) - } - - try { - const upstreamUrl = new URL(YVUSD_APR_SERVICE_API) - Object.entries(req.query).forEach(([key, value]) => { - if (typeof value === 'string') { - upstreamUrl.searchParams.set(key, value) - } - }) - - const response = await fetch(upstreamUrl.toString(), { - headers: { - Accept: 'application/json' - } - }) - - if (!response.ok) { - const details = await response.text() - return res.status(response.status).json({ - error: 'yvUSD APR upstream error', - status: response.status, - details - }) - } - - const data = await response.json() - setVercelCdnCacheHeaders(res, YVUSD_APR_CDN_CACHE_CONTROL) - return res.status(200).json(data) - } catch (error) { - console.error('Error proxying yvUSD APR request:', error) - return res.status(500).json({ error: 'Internal server error' }) - } -} diff --git a/app/[...notFound]/page.tsx b/app/[...notFound]/page.tsx new file mode 100644 index 000000000..8c88a55fa --- /dev/null +++ b/app/[...notFound]/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from 'next/navigation' + +export default function Page(): never { + redirect('/') +} diff --git a/app/api/admin/invalidate-cache/route.ts b/app/api/admin/invalidate-cache/route.ts new file mode 100644 index 000000000..7a96c906b --- /dev/null +++ b/app/api/admin/invalidate-cache/route.ts @@ -0,0 +1,4 @@ +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export { OPTIONS, POST } from '@/server/admin/invalidate-cache' diff --git a/app/api/enso/balances/route.ts b/app/api/enso/balances/route.ts new file mode 100644 index 000000000..1aeedc8e4 --- /dev/null +++ b/app/api/enso/balances/route.ts @@ -0,0 +1,4 @@ +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export { GET, OPTIONS } from '@/server/enso/balances' diff --git a/app/api/enso/route/route.ts b/app/api/enso/route/route.ts new file mode 100644 index 000000000..83c0b2ccc --- /dev/null +++ b/app/api/enso/route/route.ts @@ -0,0 +1,4 @@ +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export { GET, OPTIONS } from '@/server/enso/route' diff --git a/app/api/enso/status/route.ts b/app/api/enso/status/route.ts new file mode 100644 index 000000000..d710b23d7 --- /dev/null +++ b/app/api/enso/status/route.ts @@ -0,0 +1,4 @@ +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export { GET, OPTIONS } from '@/server/enso/status' diff --git a/app/api/holdings/activity-facets/route.ts b/app/api/holdings/activity-facets/route.ts new file mode 100644 index 000000000..a9cd0f71d --- /dev/null +++ b/app/api/holdings/activity-facets/route.ts @@ -0,0 +1,4 @@ +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export { GET, OPTIONS } from '@/server/holdings/activity-facets' diff --git a/app/api/holdings/activity/route.ts b/app/api/holdings/activity/route.ts new file mode 100644 index 000000000..32b8c3a74 --- /dev/null +++ b/app/api/holdings/activity/route.ts @@ -0,0 +1,4 @@ +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export { GET, OPTIONS } from '@/server/holdings/activity' diff --git a/app/api/holdings/breakdown/route.ts b/app/api/holdings/breakdown/route.ts new file mode 100644 index 000000000..7b0c3c65b --- /dev/null +++ b/app/api/holdings/breakdown/route.ts @@ -0,0 +1,4 @@ +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export { GET, OPTIONS } from '@/server/holdings/breakdown' diff --git a/app/api/holdings/history/route.ts b/app/api/holdings/history/route.ts new file mode 100644 index 000000000..aadb035c0 --- /dev/null +++ b/app/api/holdings/history/route.ts @@ -0,0 +1,4 @@ +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export { GET, OPTIONS } from '@/server/holdings/history' diff --git a/app/api/holdings/pnl/simple-history/route.ts b/app/api/holdings/pnl/simple-history/route.ts new file mode 100644 index 000000000..e052149d5 --- /dev/null +++ b/app/api/holdings/pnl/simple-history/route.ts @@ -0,0 +1,4 @@ +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export { GET, OPTIONS } from '@/server/holdings/pnl/simple-history' diff --git a/app/api/holdings/progress/route.ts b/app/api/holdings/progress/route.ts new file mode 100644 index 000000000..9fd66adad --- /dev/null +++ b/app/api/holdings/progress/route.ts @@ -0,0 +1,4 @@ +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export { GET, OPTIONS } from '@/server/holdings/progress' diff --git a/app/api/holdings/protocol-return/history/route.ts b/app/api/holdings/protocol-return/history/route.ts new file mode 100644 index 000000000..740bbd171 --- /dev/null +++ b/app/api/holdings/protocol-return/history/route.ts @@ -0,0 +1,4 @@ +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export { GET, OPTIONS } from '@/server/holdings/protocol-return/history' diff --git a/app/api/optimization/alignment/route.ts b/app/api/optimization/alignment/route.ts new file mode 100644 index 000000000..59ebbd007 --- /dev/null +++ b/app/api/optimization/alignment/route.ts @@ -0,0 +1,4 @@ +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export { GET, OPTIONS } from '@/server/optimization/alignment' diff --git a/app/api/optimization/change/route.ts b/app/api/optimization/change/route.ts new file mode 100644 index 000000000..183e2c70b --- /dev/null +++ b/app/api/optimization/change/route.ts @@ -0,0 +1,4 @@ +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export { GET, OPTIONS } from '@/server/optimization/change' diff --git a/app/api/optimization/vault-state/route.ts b/app/api/optimization/vault-state/route.ts new file mode 100644 index 000000000..b37d06314 --- /dev/null +++ b/app/api/optimization/vault-state/route.ts @@ -0,0 +1,4 @@ +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export { OPTIONS, POST } from '@/server/optimization/vault-state' diff --git a/app/api/prices/spot/route.ts b/app/api/prices/spot/route.ts new file mode 100644 index 000000000..ac47f8838 --- /dev/null +++ b/app/api/prices/spot/route.ts @@ -0,0 +1,4 @@ +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export { GET, OPTIONS } from '@/server/prices/spot' diff --git a/app/api/sitemap/route.ts b/app/api/sitemap/route.ts new file mode 100644 index 000000000..879aaa0a8 --- /dev/null +++ b/app/api/sitemap/route.ts @@ -0,0 +1,4 @@ +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export { GET, HEAD, OPTIONS } from '@/server/sitemap' diff --git a/app/api/tenderly/fund/route.ts b/app/api/tenderly/fund/route.ts new file mode 100644 index 000000000..f2fd21ef7 --- /dev/null +++ b/app/api/tenderly/fund/route.ts @@ -0,0 +1,4 @@ +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export { OPTIONS, POST } from '@/server/tenderly/fund' diff --git a/app/api/tenderly/increase-time/route.ts b/app/api/tenderly/increase-time/route.ts new file mode 100644 index 000000000..609fbcf4e --- /dev/null +++ b/app/api/tenderly/increase-time/route.ts @@ -0,0 +1,4 @@ +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export { OPTIONS, POST } from '@/server/tenderly/increase-time' diff --git a/app/api/tenderly/revert/route.ts b/app/api/tenderly/revert/route.ts new file mode 100644 index 000000000..ffc0725fa --- /dev/null +++ b/app/api/tenderly/revert/route.ts @@ -0,0 +1,4 @@ +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export { OPTIONS, POST } from '@/server/tenderly/revert' diff --git a/app/api/tenderly/snapshot/route.ts b/app/api/tenderly/snapshot/route.ts new file mode 100644 index 000000000..1e8f1d8b2 --- /dev/null +++ b/app/api/tenderly/snapshot/route.ts @@ -0,0 +1,4 @@ +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export { OPTIONS, POST } from '@/server/tenderly/snapshot' diff --git a/app/api/tenderly/status/route.ts b/app/api/tenderly/status/route.ts new file mode 100644 index 000000000..c66441acf --- /dev/null +++ b/app/api/tenderly/status/route.ts @@ -0,0 +1,4 @@ +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export { GET, OPTIONS } from '@/server/tenderly/status' diff --git a/app/api/vault/markdown/route.ts b/app/api/vault/markdown/route.ts new file mode 100644 index 000000000..ec1af020e --- /dev/null +++ b/app/api/vault/markdown/route.ts @@ -0,0 +1,4 @@ +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export { GET, HEAD, OPTIONS } from '@/server/vault/markdown' diff --git a/app/api/vaults/markdown/route.ts b/app/api/vaults/markdown/route.ts new file mode 100644 index 000000000..5550f264a --- /dev/null +++ b/app/api/vaults/markdown/route.ts @@ -0,0 +1,4 @@ +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export { GET, HEAD, OPTIONS } from '@/server/vaults/markdown' diff --git a/app/api/yvusd/aprs/route.ts b/app/api/yvusd/aprs/route.ts new file mode 100644 index 000000000..4696eb2c7 --- /dev/null +++ b/app/api/yvusd/aprs/route.ts @@ -0,0 +1,4 @@ +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export { GET, OPTIONS } from '@/server/yvusd/aprs' diff --git a/app/icon-list/page-client.tsx b/app/icon-list/page-client.tsx new file mode 100644 index 000000000..8e547cdb6 --- /dev/null +++ b/app/icon-list/page-client.tsx @@ -0,0 +1,9 @@ +'use client' + +import type { TUsageMap } from '@pages/icon-list/data' +import IconListPage from '@pages/icon-list/index' +import type { ReactElement } from 'react' + +export default function IconListPageClient({ usageMap }: { usageMap: TUsageMap }): ReactElement { + return <IconListPage usageMap={usageMap} /> +} diff --git a/app/icon-list/page.tsx b/app/icon-list/page.tsx new file mode 100644 index 000000000..ddd5fd544 --- /dev/null +++ b/app/icon-list/page.tsx @@ -0,0 +1,19 @@ +import { buildIconUsageMap } from '@pages/icon-list/sourceUsage' +import { redirect } from 'next/navigation' +import type { ReactElement } from 'react' +import PublicApp from '@/PublicApp' +import IconListPageClient from './page-client' + +export default async function Page(): Promise<ReactElement> { + if (process.env.NODE_ENV === 'production') { + redirect('/') + } + + const usageMap = await buildIconUsageMap() + + return ( + <PublicApp> + <IconListPageClient usageMap={usageMap} /> + </PublicApp> + ) +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 000000000..25a75f4e2 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,167 @@ +import type { Metadata, Viewport } from 'next' +import localFont from 'next/font/local' +import Script from 'next/script' +import type { ReactNode } from 'react' +import { Suspense } from 'react' +import '../style.css' + +const aeonik = localFont({ + src: [ + { path: '../public/fonts/Aeonik-Light.ttf', weight: '300', style: 'normal' }, + { path: '../public/fonts/Aeonik-Regular.woff2', weight: '400', style: 'normal' }, + { path: '../public/fonts/Aeonik-Bold.woff2', weight: '700', style: 'normal' }, + { path: '../public/fonts/Aeonik-Black.ttf', weight: '900', style: 'normal' } + ], + variable: '--font-aeonik', + display: 'swap' +}) + +const aeonikMono = localFont({ + src: [ + { path: '../public/fonts/AeonikMono-Regular.woff2', weight: '400', style: 'normal' }, + { path: '../public/fonts/AeonikMono-Bold.woff2', weight: '700', style: 'normal' } + ], + variable: '--font-aeonik-mono', + display: 'swap' +}) + +const defaultDescription = 'The yield protocol for digital assets' +const defaultOgImage = '/apps/vaults-og.png' + +export const metadata: Metadata = { + metadataBase: new URL('https://yearn.fi'), + title: { + default: 'Yearn Finance', + template: '%s | Yearn' + }, + description: defaultDescription, + manifest: '/manifest.json', + applicationName: 'Yearn Finance', + appleWebApp: { + capable: true, + title: 'Yearn Finance', + statusBarStyle: 'default' + }, + formatDetection: { + telephone: false + }, + robots: { + index: true, + follow: true, + googleBot: { + index: true, + follow: true, + 'max-video-preview': -1, + 'max-image-preview': 'large', + 'max-snippet': -1 + } + }, + openGraph: { + title: 'Yearn Finance', + description: defaultDescription, + url: 'https://yearn.fi', + siteName: 'Yearn Finance', + type: 'website', + images: [defaultOgImage] + }, + twitter: { + card: 'summary_large_image', + title: 'Yearn Finance', + description: defaultDescription, + images: [defaultOgImage] + }, + icons: { + icon: [ + { url: '/favicons/favicon-32x32.png', sizes: '32x32', type: 'image/png' }, + { url: '/favicons/favicon-16x16.png', sizes: '16x16', type: 'image/png' }, + { url: '/favicons/favicon-512x512.png', sizes: '512x512', type: 'image/png' }, + { url: '/favicons/android-icon-192x192.png', sizes: '192x192', type: 'image/png' }, + { url: '/favicons/android-icon-144x144.png', sizes: '144x144', type: 'image/png' } + ], + shortcut: '/favicons/favicon.ico', + apple: [ + { url: '/favicons/apple-icon.png' }, + { url: '/favicons/apple-icon-152x152.png', sizes: '152x152' }, + { url: '/favicons/apple-icon-180x180.png', sizes: '180x180' }, + { url: '/favicons/apple-icon-167x167.png', sizes: '167x167' } + ], + other: [{ rel: 'mask-icon', url: '/favicons/safari-pinned-tab.svg', color: '#000000' }] + }, + other: { + 'msapplication-TileColor': '#ffffff', + 'msapplication-config': '/favicons/browserconfig.xml', + 'msapplication-tap-highlight': 'no' + } +} + +export const viewport: Viewport = { + width: 'device-width', + initialScale: 1, + minimumScale: 1, + viewportFit: 'cover', + themeColor: '#000000' +} + +const themeScript = ` +const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); +const NEW_THEME_STORAGE_KEY = 'yearn-theme'; +const OLD_THEME_STORAGE_KEY = 'isDarkMode'; +const THEME_EVENT_NAME = 'yearn-theme-change'; + +function resolveTheme() { + const storedValue = window.localStorage.getItem(NEW_THEME_STORAGE_KEY); + if (storedValue === 'light' || storedValue === 'soft-dark' || storedValue === 'blue-dark' || storedValue === 'midnight') { + return storedValue; + } + if (storedValue === 'dark') { + window.localStorage.setItem(NEW_THEME_STORAGE_KEY, 'midnight'); + return 'midnight'; + } + + const oldStoredValue = window.localStorage.getItem(OLD_THEME_STORAGE_KEY); + if (oldStoredValue === 'true') { + window.localStorage.setItem(NEW_THEME_STORAGE_KEY, 'soft-dark'); + return 'soft-dark'; + } + if (oldStoredValue === 'false') { + window.localStorage.setItem(NEW_THEME_STORAGE_KEY, 'light'); + return 'light'; + } + + return darkModeMediaQuery.matches ? 'soft-dark' : 'light'; +} + +function applyTheme(theme) { + const root = document.documentElement; + root.setAttribute('data-theme', theme); + root.classList.remove('dark', 'light', 'v3'); + if (theme === 'soft-dark' || theme === 'blue-dark' || theme === 'midnight') { + root.classList.add('dark'); + root.classList.add('v3'); + } else { + root.classList.add('light'); + } +} + +function updateMode() { + applyTheme(resolveTheme()); +} + +updateMode(); +darkModeMediaQuery.addEventListener('change', updateMode); +window.addEventListener('storage', updateMode); +window.addEventListener(THEME_EVENT_NAME, updateMode); +` + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + <html lang="en" className="duration-150 transition-colors" suppressHydrationWarning> + <body className={`${aeonik.variable} ${aeonikMono.variable}`}> + <Script id="yearn-theme-init" strategy="beforeInteractive"> + {themeScript} + </Script> + <Suspense>{children}</Suspense> + </body> + </html> + ) +} diff --git a/app/metadata.ts b/app/metadata.ts new file mode 100644 index 000000000..158d93294 --- /dev/null +++ b/app/metadata.ts @@ -0,0 +1,305 @@ +import landingManifest from '@shared/data/landing-manifest.json' +import { buildVaultSnapshotEndpoint } from '@shared/data/publicQueryEndpoints' +import vaultsManifest from '@shared/data/vaults-manifest.json' +import { formatApyDisplay, formatTvlDisplay, toAddress } from '@shared/utils' +import { fetchWithSchema } from '@shared/utils/fetchQuery' +import { kongVaultSnapshotSchema, type TKongVaultSnapshot } from '@shared/utils/schemas/kongVaultSnapshotSchema' +import { getNetwork } from '@shared/utils/wagmi' +import type { Metadata } from 'next' +import { cache } from 'react' +import { isAddress } from 'viem' + +export function buildManifestMetadata(manifest: typeof landingManifest | typeof vaultsManifest): Metadata { + return { + title: manifest.name, + description: manifest.description, + alternates: { + canonical: manifest.uri + }, + openGraph: { + title: manifest.name, + description: manifest.description, + url: manifest.uri, + images: manifest.og ? [manifest.og] : undefined, + type: 'website' + }, + twitter: { + card: 'summary_large_image', + title: manifest.name, + description: manifest.description, + images: manifest.og ? [manifest.og] : undefined + } + } +} + +type TVaultMetadataInput = { + chainID: string + address: string + snapshot?: TKongVaultSnapshot | null +} + +const genericVaultTitle = 'Yearn Vault' +const genericVaultDescription = "Earn yield on your crypto with Yearn's automated vault strategies" + +export const yearnOrganizationJsonLd = { + '@context': 'https://schema.org', + '@type': 'Organization', + '@id': 'https://yearn.fi/#organization', + name: 'Yearn Finance', + alternateName: 'Yearn', + url: 'https://yearn.fi', + logo: 'https://yearn.fi/favicons/android-icon-192x192.png', + description: 'DeFi yield aggregator — automatically maximize returns on deposited digital assets.', + sameAs: ['https://github.com/yearn', 'https://x.com/yearnfi', 'https://docs.yearn.fi', 'https://discord.gg/yearn/'] +} + +function isValidVaultMetadataParams(chainID: string, address: string): boolean { + const isValidChainID = /^\d+$/.test(chainID) + const isValidAddress = isAddress(address) + return isValidChainID && isValidAddress +} + +const VAULT_METADATA_TIMEOUT_MS = 7000 + +const fetchVaultMetadataSnapshot = cache(async function fetchVaultMetadataSnapshot( + chainID: string, + address: string +): Promise<TKongVaultSnapshot | null> { + const endpoint = buildVaultSnapshotEndpoint(chainID, address) + if (!endpoint) { + return null + } + + try { + return await fetchWithSchema(endpoint, kongVaultSnapshotSchema, { timeout: VAULT_METADATA_TIMEOUT_MS }) + } catch (error) { + console.warn(`[Metadata] Failed to fetch vault snapshot ${chainID}/${address}`, error) + return null + } +}) + +function pickFirstText(...values: Array<string | null | undefined>): string { + return values.map((value) => value?.trim() ?? '').find(Boolean) ?? '' +} + +function pickFirstNumber(...values: Array<number | null | undefined>): number | null { + return values.find((value) => typeof value === 'number' && Number.isFinite(value)) ?? null +} + +function buildVaultDescription({ + assetSymbol, + chainName, + name, + apy, + tvl +}: { + assetSymbol: string + chainName: string + name: string + apy: number | null + tvl: number | null +}): string { + const assetCopy = assetSymbol ? `${assetSymbol} ` : '' + const apyCopy = apy === null ? null : `Est. APY ${formatApyDisplay(apy)}` + const tvlCopy = tvl === null ? null : `TVL ${formatTvlDisplay(tvl)}` + const metricsCopy = [apyCopy, tvlCopy].filter(Boolean).join(' and ') + const suffix = metricsCopy ? ` ${metricsCopy}.` : '' + return `Earn yield with ${name}, a Yearn ${assetCopy}vault on ${chainName}.${suffix}` +} + +function buildVaultCanonicalUrl(chainID: string, address: string): string { + return `https://yearn.fi/vaults/${chainID}/${toAddress(address)}` +} + +function buildVaultMarkdownUrl(chainID: string, address: string): string { + return `https://yearn.fi/api/vault/markdown?chainId=${chainID}&address=${toAddress(address)}` +} + +function buildVaultDiscoveryUrls( + chainID: string, + address: string +): { + canonicalUrl: string + markdownUrl: string + normalizedAddress: string + snapshotUrl: string +} | null { + if (!isValidVaultMetadataParams(chainID, address)) { + return null + } + + const normalizedAddress = toAddress(address) + const snapshotUrl = buildVaultSnapshotEndpoint(chainID, normalizedAddress) + if (!snapshotUrl) { + return null + } + + return { + canonicalUrl: buildVaultCanonicalUrl(chainID, normalizedAddress), + markdownUrl: buildVaultMarkdownUrl(chainID, normalizedAddress), + normalizedAddress, + snapshotUrl + } +} + +export function buildVaultMetadataFromInput({ chainID, address, snapshot }: TVaultMetadataInput): Metadata { + const discoveryUrls = buildVaultDiscoveryUrls(chainID, address) + const canonicalPath = discoveryUrls ? `/vaults/${chainID}/${discoveryUrls.normalizedAddress}` : '/vaults' + const ogImage = discoveryUrls + ? `https://og.yearn.fi/api/og/yearn/vault/${chainID}/${discoveryUrls.normalizedAddress}` + : vaultsManifest.og + const chainIdNumber = Number(chainID) + const chainName = Number.isInteger(chainIdNumber) ? getNetwork(chainIdNumber).name : 'Yearn' + const name = pickFirstText(snapshot?.meta?.displayName, snapshot?.meta?.name, snapshot?.name, genericVaultTitle) + const symbol = pickFirstText(snapshot?.meta?.displaySymbol, snapshot?.symbol) + const assetSymbol = pickFirstText(snapshot?.meta?.token?.symbol, snapshot?.asset?.symbol) + const title = symbol ? `${name} (${symbol})` : name + const apy = pickFirstNumber( + snapshot?.apy?.net, + snapshot?.performance?.estimated?.apy, + snapshot?.performance?.estimated?.apr, + snapshot?.performance?.oracle?.netAPY, + snapshot?.performance?.oracle?.apy, + snapshot?.performance?.oracle?.netAPR, + snapshot?.performance?.oracle?.apr + ) + const tvl = pickFirstNumber(snapshot?.tvl?.close) + const description = snapshot + ? buildVaultDescription({ assetSymbol, chainName, name, apy, tvl }) + : genericVaultDescription + + return { + title, + description, + alternates: { + canonical: canonicalPath, + ...(discoveryUrls + ? { + types: { + 'text/markdown': [{ title: 'Vault markdown', url: discoveryUrls.markdownUrl }], + 'application/json': [{ title: 'Canonical Kong vault snapshot', url: discoveryUrls.snapshotUrl }] + } + } + : {}) + }, + openGraph: { + title, + description, + url: canonicalPath, + images: ogImage ? [ogImage] : undefined, + type: 'website' + }, + twitter: { + card: 'summary_large_image', + title, + description, + images: ogImage ? [ogImage] : undefined + }, + ...(discoveryUrls + ? { + other: { + 'yearn:chainId': chainID, + 'yearn:vaultAddress': discoveryUrls.normalizedAddress, + 'yearn:markdown': discoveryUrls.markdownUrl, + 'yearn:canonicalData': discoveryUrls.snapshotUrl + } + } + : {}) + } +} + +export function buildVaultStructuredDataFromInput({ + chainID, + address, + snapshot +}: TVaultMetadataInput): Record<string, unknown> | null { + if (!isValidVaultMetadataParams(chainID, address)) { + return null + } + + const discoveryUrls = buildVaultDiscoveryUrls(chainID, address) + if (!discoveryUrls) { + return null + } + + const chainIdNumber = Number(chainID) + const chainName = getNetwork(chainIdNumber).name + const name = pickFirstText(snapshot?.meta?.displayName, snapshot?.meta?.name, snapshot?.name, genericVaultTitle) + const symbol = pickFirstText(snapshot?.meta?.displaySymbol, snapshot?.symbol) + const assetSymbol = pickFirstText(snapshot?.meta?.token?.symbol, snapshot?.asset?.symbol) + const apy = pickFirstNumber( + snapshot?.apy?.net, + snapshot?.performance?.estimated?.apy, + snapshot?.performance?.estimated?.apr, + snapshot?.performance?.oracle?.netAPY, + snapshot?.performance?.oracle?.apy, + snapshot?.performance?.oracle?.netAPR, + snapshot?.performance?.oracle?.apr + ) + const title = symbol ? `${name} (${symbol})` : name + const description = snapshot + ? buildVaultDescription({ + assetSymbol, + chainName, + name, + apy, + tvl: pickFirstNumber(snapshot.tvl?.close) + }) + : genericVaultDescription + const annualPercentageRate = + apy === null + ? undefined + : { + '@type': 'QuantitativeValue', + value: Number((apy * 100).toFixed(2)), + unitText: 'PERCENT' + } + + return { + '@context': 'https://schema.org', + '@type': 'FinancialProduct', + name: title, + description, + url: discoveryUrls.canonicalUrl, + sameAs: [discoveryUrls.snapshotUrl, discoveryUrls.markdownUrl], + ...(annualPercentageRate ? { annualPercentageRate } : {}), + provider: { + '@id': yearnOrganizationJsonLd['@id'], + '@type': 'Organization', + name: yearnOrganizationJsonLd.name, + url: yearnOrganizationJsonLd.url + }, + offers: { + '@type': 'Offer', + category: 'Yield Vault', + description: `Automated yield vault for ${assetSymbol || symbol || 'crypto'} on ${chainName}` + } + } +} + +export async function buildVaultMetadata(chainID: string, address: string): Promise<Metadata> { + const snapshot = isValidVaultMetadataParams(chainID, address) + ? await fetchVaultMetadataSnapshot(chainID, address) + : null + return buildVaultMetadataFromInput({ chainID, address, snapshot }) +} + +export async function buildVaultStructuredData( + chainID: string, + address: string +): Promise<Record<string, unknown> | null> { + const snapshot = isValidVaultMetadataParams(chainID, address) + ? await fetchVaultMetadataSnapshot(chainID, address) + : null + return buildVaultStructuredDataFromInput({ chainID, address, snapshot }) +} + +export const landingMetadata = buildManifestMetadata(landingManifest) +export const vaultsMetadata = buildManifestMetadata(vaultsManifest) +export const portfolioMetadata: Metadata = { + ...vaultsMetadata, + title: 'Yearn Portfolio', + alternates: { + canonical: '/portfolio' + } +} diff --git a/app/page-client.tsx b/app/page-client.tsx new file mode 100644 index 000000000..e948b0b6c --- /dev/null +++ b/app/page-client.tsx @@ -0,0 +1,8 @@ +'use client' + +import HomePage from '@pages/landing' +import type { ReactElement } from 'react' + +export default function HomePageClient(): ReactElement { + return <HomePage /> +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 000000000..e5bdd6303 --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,78 @@ +import { JsonLd } from '@shared/components/JsonLd' +import { HydrationBoundary } from '@tanstack/react-query' +import type { ReactElement } from 'react' +import PublicApp from '@/PublicApp' +import { getLandingPageDehydratedState } from '@/server/ssr/publicDataHydration' +import { landingMetadata, yearnOrganizationJsonLd } from './metadata' +import HomePageClient from './page-client' + +export const metadata = landingMetadata +export const revalidate = 21600 + +const websiteJsonLd = { + '@context': 'https://schema.org', + '@type': 'WebSite', + name: 'Yearn Finance', + url: 'https://yearn.fi', + potentialAction: { + '@type': 'SearchAction', + target: 'https://yearn.fi/vaults?search={search_term_string}', + 'query-input': 'required name=search_term_string' + } +} + +const faqJsonLd = { + '@context': 'https://schema.org', + '@type': 'FAQPage', + mainEntity: [ + { + '@type': 'Question', + name: 'What is a Yearn Vault?', + acceptedAnswer: { + '@type': 'Answer', + text: "Yearn Vaults are DeFi's yield-optimizing asset management platform. You deposit tokens, and strategies automatically maximize yields across protocols." + } + }, + { + '@type': 'Question', + name: 'What are the risks?', + acceptedAnswer: { + '@type': 'Answer', + text: 'As with any DeFi protocol, there are smart contract risks. Yearn reduces these risks through auditing and testing before deployment.' + } + }, + { + '@type': 'Question', + name: 'What is YFI?', + acceptedAnswer: { + '@type': 'Answer', + text: "YFI is Yearn's governance token. YFI holders can vote on proposals and shape the future of the protocol." + } + }, + { + '@type': 'Question', + name: 'Are there Developer Docs?', + acceptedAnswer: { + '@type': 'Answer', + text: 'Yes. Yearn has documentation for developers building on top of the protocol at https://docs.yearn.fi.' + } + } + ] +} + +export default async function Page(): Promise<ReactElement> { + const dehydratedState = await getLandingPageDehydratedState() + + return ( + <> + <JsonLd schema={yearnOrganizationJsonLd} /> + <JsonLd schema={websiteJsonLd} /> + <JsonLd schema={faqJsonLd} /> + <PublicApp> + <HydrationBoundary state={dehydratedState}> + <HomePageClient /> + </HydrationBoundary> + </PublicApp> + </> + ) +} diff --git a/app/portfolio/layout.tsx b/app/portfolio/layout.tsx new file mode 100644 index 000000000..999625796 --- /dev/null +++ b/app/portfolio/layout.tsx @@ -0,0 +1,6 @@ +import type { ReactElement, ReactNode } from 'react' +import App from '@/App' + +export default function PortfolioLayout({ children }: { children: ReactNode }): ReactElement { + return <App>{children}</App> +} diff --git a/app/portfolio/page-client.tsx b/app/portfolio/page-client.tsx new file mode 100644 index 000000000..051b152a7 --- /dev/null +++ b/app/portfolio/page-client.tsx @@ -0,0 +1,8 @@ +'use client' + +import PortfolioPage from '@pages/portfolio/index' +import type { ReactElement } from 'react' + +export default function PortfolioPageClient(): ReactElement { + return <PortfolioPage /> +} diff --git a/app/portfolio/page.tsx b/app/portfolio/page.tsx new file mode 100644 index 000000000..3fadcca2c --- /dev/null +++ b/app/portfolio/page.tsx @@ -0,0 +1,9 @@ +import type { ReactElement } from 'react' +import { portfolioMetadata } from '../metadata' +import PortfolioPageClient from './page-client' + +export const metadata = portfolioMetadata + +export default function Page(): ReactElement { + return <PortfolioPageClient /> +} diff --git a/app/robots.ts b/app/robots.ts new file mode 100644 index 000000000..2b69502a5 --- /dev/null +++ b/app/robots.ts @@ -0,0 +1,24 @@ +import type { MetadataRoute } from 'next' + +const SITE_URL = 'https://yearn.fi' +const isBlockedDeployment = process.env.VERCEL_ENV === 'preview' || process.env.VERCEL_ENV === 'development' + +export default function robots(): MetadataRoute.Robots { + if (isBlockedDeployment) { + return { + rules: { + userAgent: '*', + disallow: '/' + } + } + } + + return { + rules: { + userAgent: '*', + allow: ['/', '/llms.txt', '/robots.txt', '/sitemap.xml', '/api/vaults/markdown', '/api/vault/markdown'], + disallow: ['/api/enso/', '/api/holdings/', '/api/optimization/', '/api/tenderly/'] + }, + sitemap: `${SITE_URL}/sitemap.xml` + } +} diff --git a/app/sitemap.xml/route.ts b/app/sitemap.xml/route.ts new file mode 100644 index 000000000..879aaa0a8 --- /dev/null +++ b/app/sitemap.xml/route.ts @@ -0,0 +1,4 @@ +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export { GET, HEAD, OPTIONS } from '@/server/sitemap' diff --git a/app/v2/[...slug]/page.tsx b/app/v2/[...slug]/page.tsx new file mode 100644 index 000000000..115a07218 --- /dev/null +++ b/app/v2/[...slug]/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from 'next/navigation' + +export default function Page(): never { + redirect('/vaults?type=lp') +} diff --git a/app/v2/page.tsx b/app/v2/page.tsx new file mode 100644 index 000000000..115a07218 --- /dev/null +++ b/app/v2/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from 'next/navigation' + +export default function Page(): never { + redirect('/vaults?type=lp') +} diff --git a/app/v3/[...slug]/page.tsx b/app/v3/[...slug]/page.tsx new file mode 100644 index 000000000..8aa27f84e --- /dev/null +++ b/app/v3/[...slug]/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from 'next/navigation' + +export default function Page(): never { + redirect('/vaults') +} diff --git a/app/v3/[chainID]/[address]/layout.tsx b/app/v3/[chainID]/[address]/layout.tsx new file mode 100644 index 000000000..939422cd3 --- /dev/null +++ b/app/v3/[chainID]/[address]/layout.tsx @@ -0,0 +1,6 @@ +import type { ReactElement, ReactNode } from 'react' +import App from '@/App' + +export default function V3VaultDetailsLayout({ children }: { children: ReactNode }): ReactElement { + return <App>{children}</App> +} diff --git a/app/v3/[chainID]/[address]/loading.tsx b/app/v3/[chainID]/[address]/loading.tsx new file mode 100644 index 000000000..db5e4969c --- /dev/null +++ b/app/v3/[chainID]/[address]/loading.tsx @@ -0,0 +1,6 @@ +import { VaultDetailLoading } from '@pages/vaults/components/detail/VaultDetailLoading' +import type { ReactElement } from 'react' + +export default function Loading(): ReactElement { + return <VaultDetailLoading /> +} diff --git a/app/v3/[chainID]/[address]/page-client.tsx b/app/v3/[chainID]/[address]/page-client.tsx new file mode 100644 index 000000000..c1c992b23 --- /dev/null +++ b/app/v3/[chainID]/[address]/page-client.tsx @@ -0,0 +1,8 @@ +'use client' + +import VaultsDetailPage from '@pages/vaults/[chainID]/[address]' +import type { ReactElement } from 'react' + +export default function V3VaultsDetailPageClient(): ReactElement { + return <VaultsDetailPage /> +} diff --git a/app/v3/[chainID]/[address]/page.tsx b/app/v3/[chainID]/[address]/page.tsx new file mode 100644 index 000000000..bec3bd971 --- /dev/null +++ b/app/v3/[chainID]/[address]/page.tsx @@ -0,0 +1,41 @@ +import { JsonLd } from '@shared/components/JsonLd' +import { HydrationBoundary } from '@tanstack/react-query' +import type { Metadata } from 'next' +import type { ReactElement } from 'react' +import { getVaultDetailPageDehydratedState } from '@/server/ssr/publicDataHydration' +import { buildVaultMetadata, buildVaultStructuredData, yearnOrganizationJsonLd } from '../../../metadata' +import V3VaultsDetailPageClient from './page-client' + +export const revalidate = 21600 + +type TVaultPageProps = { + params: Promise<{ + chainID: string + address: string + }> +} + +export async function generateMetadata({ params }: TVaultPageProps): Promise<Metadata> { + const { chainID, address } = await params + return await buildVaultMetadata(chainID, address) +} + +export default async function Page({ params }: TVaultPageProps): Promise<ReactElement> { + const { chainID, address } = await params + const structuredData = await buildVaultStructuredData(chainID, address) + const dehydratedState = await getVaultDetailPageDehydratedState(chainID, address) + + return ( + <> + {structuredData ? ( + <> + <JsonLd schema={yearnOrganizationJsonLd} /> + <JsonLd schema={structuredData} /> + </> + ) : null} + <HydrationBoundary state={dehydratedState}> + <V3VaultsDetailPageClient /> + </HydrationBoundary> + </> + ) +} diff --git a/app/v3/page.tsx b/app/v3/page.tsx new file mode 100644 index 000000000..8aa27f84e --- /dev/null +++ b/app/v3/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from 'next/navigation' + +export default function Page(): never { + redirect('/vaults') +} diff --git a/app/vaults/(list)/loading.tsx b/app/vaults/(list)/loading.tsx new file mode 100644 index 000000000..4c0f37159 --- /dev/null +++ b/app/vaults/(list)/loading.tsx @@ -0,0 +1,317 @@ +import { VaultsListRowSkeleton } from '@pages/vaults/components/list/VaultsListRowSkeleton' +import { getVaultTypeLabel, type TVaultType } from '@pages/vaults/utils/vaultTypeCopy' +import { Breadcrumbs } from '@shared/components/Breadcrumbs' +import { IconChevron } from '@shared/icons/IconChevron' +import { IconFilter } from '@shared/icons/IconFilter' +import { IconGitCompare } from '@shared/icons/IconGitCompare' +import { IconSearch } from '@shared/icons/IconSearch' +import { LogoYearn } from '@shared/icons/LogoYearn' +import { cl, SUPPORTED_NETWORKS } from '@shared/utils' +import type { ReactElement } from 'react' +import { env } from '@/env' + +const SKELETON_ROWS = Array.from({ length: 16 }, (_, index) => index) +const VAULT_TYPE_FILTERS: TVaultType[] = ['all', 'v3', 'factory'] +const VAULT_LIST_HEAD_ITEMS = [ + { label: 'Est. APY', className: 'col-span-6' }, + { label: 'TVL', className: 'col-span-5' } +] as const +const LOADING_CHAIN_IDS = [1, 747474, 8453, 10] as const +const CHAIN_FILTERS = [ + { label: 'All Chains', chainId: null }, + ...LOADING_CHAIN_IDS.map((chainId) => ({ + label: SUPPORTED_NETWORKS.find((network) => network.id === chainId)?.name ?? `Chain ${chainId}`, + chainId + })) +] as const + +function DisabledVaultTypeToggle({ stretch = false }: { stretch?: boolean }): ReactElement { + return ( + <div + aria-disabled={true} + className={ + 'flex h-10 shrink-0 items-stretch overflow-hidden rounded-lg border border-border bg-surface-secondary text-sm text-text-primary divide-x divide-border opacity-70' + } + > + {VAULT_TYPE_FILTERS.map((vaultType) => ( + <button + key={vaultType} + type={'button'} + disabled={true} + className={cl( + 'flex h-full items-center justify-center gap-1 px-4 font-medium', + vaultType === 'all' + ? 'bg-surface font-semibold text-text-primary' + : 'cursor-not-allowed text-text-secondary', + stretch ? 'flex-1' : '' + )} + aria-pressed={vaultType === 'all'} + > + <span className={'whitespace-nowrap'}>{getVaultTypeLabel(vaultType)}</span> + </button> + ))} + </div> + ) +} + +function DisabledChainIcon({ chainId, label }: { chainId: number | null; label: string }): ReactElement { + if (chainId === null) { + return ( + <span className={'size-5 overflow-hidden rounded-full'}> + <LogoYearn className={'size-full'} back={'text-text-primary'} front={'text-surface'} /> + </span> + ) + } + + return ( + <span aria-hidden={true} className={'size-5 overflow-hidden rounded-full bg-surface/80'}> + {/* biome-ignore lint/performance/noImgElement: loading fallback mirrors the chain selector before app hydration. */} + <img + src={`${env.NEXT_PUBLIC_BASE_YEARN_ASSETS_URI}/chains/${chainId}/logo-128.png`} + alt={''} + className={'size-full object-cover'} + width={20} + height={20} + loading={'eager'} + decoding={'async'} + title={label} + /> + </span> + ) +} + +function DisabledChainSelector(): ReactElement { + return ( + <div + aria-disabled={true} + className={ + 'flex h-10 w-full items-stretch overflow-x-auto scrollbar-themed rounded-lg border border-border bg-surface-secondary text-sm text-text-primary divide-x divide-border opacity-70' + } + > + {CHAIN_FILTERS.map((chain) => ( + <button + key={chain.label} + type={'button'} + disabled={true} + className={cl( + 'flex h-full flex-1 cursor-not-allowed items-center justify-center gap-1 px-2 font-medium', + chain.label === 'All Chains' ? 'bg-surface font-semibold text-text-primary' : 'text-text-secondary' + )} + aria-pressed={chain.label === 'All Chains'} + aria-label={chain.label === 'All Chains' ? undefined : chain.label} + > + <DisabledChainIcon chainId={chain.chainId} label={chain.label} /> + {chain.label === 'All Chains' ? <span className={'whitespace-nowrap'}>{chain.label}</span> : null} + </button> + ))} + </div> + ) +} + +function DisabledFiltersButton(): ReactElement { + return ( + <button + type={'button'} + disabled={true} + className={ + 'relative flex h-10 w-[34px] shrink-0 cursor-not-allowed items-center justify-center gap-1 overflow-visible rounded-lg border border-border bg-surface py-2 text-sm font-medium text-text-secondary opacity-70 min-[1075px]:w-auto min-[1075px]:px-4' + } + aria-label={'Open filters'} + > + <IconFilter className={'size-4 shrink-0'} /> + <span className={'hidden min-[1075px]:inline'}>{'Filters'}</span> + </button> + ) +} + +function DisabledCompareButton(): ReactElement { + return ( + <button + type={'button'} + disabled={true} + className={ + 'flex h-10 shrink-0 cursor-not-allowed items-center justify-center gap-1 rounded-lg border border-border bg-surface px-4 text-sm font-medium text-text-secondary opacity-70' + } + > + <IconGitCompare className={'size-4'} /> + {'Compare'} + </button> + ) +} + +function DisabledSearchBar(): ReactElement { + return ( + <div + aria-disabled={true} + className={ + 'flex h-10 w-full items-center gap-2 rounded-lg border border-border bg-surface px-2 text-text-primary opacity-70' + } + > + <div className={'flex h-full w-full items-center gap-2 overflow-hidden'}> + <input + id={'vaults-loading-search'} + disabled={true} + className={ + 'h-full min-w-0 flex-1 cursor-not-allowed bg-transparent py-2 text-base text-text-primary placeholder:text-text-tertiary focus:outline-none' + } + type={'text'} + placeholder={'Find a Vault'} + value={''} + readOnly={true} + /> + <div + className={'flex min-h-[44px] min-w-[44px] shrink-0 items-center justify-center rounded text-text-primary'} + > + <IconSearch className={'size-4'} /> + </div> + </div> + </div> + ) +} + +function DisabledMobileFilters(): ReactElement { + return ( + <div className={'md:hidden'}> + <div className={'mb-2 w-full'}> + <DisabledVaultTypeToggle stretch={true} /> + </div> + <div className={'mb-2 w-full'}> + <button + type={'button'} + disabled={true} + className={ + 'flex h-10 w-full cursor-not-allowed items-center justify-between gap-2 rounded-lg border border-border bg-surface px-3 text-sm font-medium text-text-primary opacity-70' + } + > + <div className={'flex items-center gap-2'}> + <DisabledChainIcon chainId={null} label={'All Chains'} /> + <span>{'All Chains'}</span> + </div> + </button> + </div> + <div className={'flex w-full items-center gap-1'}> + <button + type={'button'} + disabled={true} + className={ + 'flex h-10 flex-1 cursor-not-allowed items-center justify-center rounded-lg border border-border bg-surface text-sm font-medium text-text-secondary opacity-70' + } + > + {'Filter Vaults'} + </button> + <DisabledCompareButton /> + <button + type={'button'} + disabled={true} + className={ + 'flex size-10 shrink-0 cursor-not-allowed items-center justify-center rounded-lg border border-border bg-surface text-text-secondary opacity-70' + } + aria-label={'Search vaults'} + > + <IconSearch className={'size-4'} /> + </button> + </div> + </div> + ) +} + +function DisabledDesktopFilters(): ReactElement { + return ( + <div className={'hidden md:block'}> + <div className={'flex flex-col gap-4'}> + <div> + <div className={'flex flex-col gap-2'}> + <div className={'flex w-full flex-wrap items-center gap-3'}> + <div className={'shrink-0'}> + <DisabledVaultTypeToggle /> + </div> + <div className={'min-w-0 max-w-[580px] shrink'}> + <DisabledChainSelector /> + </div> + <div className={'flex min-w-0 flex-1 flex-row items-center gap-1.5'}> + <DisabledFiltersButton /> + <DisabledCompareButton /> + <div className={'min-w-0 flex-1'}> + <DisabledSearchBar /> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + ) +} + +function VaultsLoadingFilters(): ReactElement { + return ( + <div className={'relative col-span-24 w-full md:col-span-19'} data-tour="vaults-filters"> + <DisabledMobileFilters /> + <DisabledDesktopFilters /> + </div> + ) +} + +function VaultsLoadingListHead(): ReactElement { + return ( + <div className={'relative z-10 hidden w-full grid-cols-1 rounded-t-lg border border-border bg-transparent md:grid'}> + <div className={'grid w-full grid-cols-1 rounded-t-xl bg-surface py-2 pl-6 pr-20 md:grid-cols-24'}> + <div className={'col-span-12 flex flex-row items-center justify-between py-4 md:py-0'}> + <p className={'yearn--table-head-label text-text-primary/60'}>{'Vault'}</p> + </div> + <div className={'col-span-12 z-10 mt-4 grid grid-cols-2 gap-4 md:mt-0 md:grid-cols-12'}> + {VAULT_LIST_HEAD_ITEMS.map((item) => ( + <div + key={item.label} + className={cl('yearn--table-head-label-wrapper gap-1 justify-end text-right', item.className)} + datatype={'number'} + > + <p className={'yearn--table-head-label text-right text-text-primary/60'}> {item.label}</p> + <span className={'flex items-center justify-center'}> + <IconChevron className={'size-4 min-w-[16px] text-text-primary/40'} /> + </span> + </div> + ))} + <div className={'col-span-1'} /> + </div> + </div> + </div> + ) +} + +export default function Loading(): ReactElement { + return ( + <main className={'min-h-[calc(100vh-var(--header-height))] w-full bg-app'}> + <div className={'mx-auto w-full max-w-[1232px] px-4 pb-4'}> + <div className={'flex flex-col'}> + <div className={'sticky z-40 w-full shrink-0 bg-app pb-2'} style={{ top: 'var(--header-height)' }}> + <Breadcrumbs + className={'mb-3 px-1'} + items={[ + { label: 'Home', href: '/' }, + { label: 'Vaults', href: '/vaults', isCurrent: true } + ]} + /> + <VaultsLoadingFilters /> + </div> + + <div className={'relative w-full rounded-lg bg-surface'}> + <VaultsLoadingListHead /> + <div + className={ + 'flex flex-col overflow-hidden rounded-lg border border-border md:rounded-t-none md:border-t-0' + } + > + {SKELETON_ROWS.map((rowIndex) => ( + <VaultsListRowSkeleton + key={`vaults-loading-row-${rowIndex}`} + className={rowIndex === SKELETON_ROWS.length - 1 ? undefined : 'border-b border-border'} + /> + ))} + </div> + </div> + </div> + </div> + </main> + ) +} diff --git a/app/vaults/(list)/page-client.tsx b/app/vaults/(list)/page-client.tsx new file mode 100644 index 000000000..924148a7e --- /dev/null +++ b/app/vaults/(list)/page-client.tsx @@ -0,0 +1,15 @@ +'use client' + +import VaultsPage from '@pages/vaults/index' +import type { TVaultsInitialPayload } from '@pages/vaults/utils/vaultsInitialPayload' +import type { TVaultsQuerySnapshot } from '@pages/vaults/utils/vaultsQueryState' +import type { ReactElement } from 'react' + +type TVaultsPageClientProps = { + initialQueryState?: TVaultsQuerySnapshot + initialVaults?: TVaultsInitialPayload +} + +export default function VaultsPageClient({ initialQueryState, initialVaults }: TVaultsPageClientProps): ReactElement { + return <VaultsPage initialQueryState={initialQueryState} initialVaults={initialVaults} /> +} diff --git a/app/vaults/(list)/page.tsx b/app/vaults/(list)/page.tsx new file mode 100644 index 000000000..58e84e2ee --- /dev/null +++ b/app/vaults/(list)/page.tsx @@ -0,0 +1,15 @@ +import { buildInitialVaultsQuerySnapshot, DEFAULT_VAULT_QUERY_DEFAULTS } from '@pages/vaults/utils/vaultsQueryState' +import type { ReactElement } from 'react' +import { getVaultsPageInitialPayload } from '@/server/ssr/publicDataHydration' +import { vaultsMetadata } from '../../metadata' +import VaultsPageClient from './page-client' + +export const metadata = vaultsMetadata +export const revalidate = 3600 + +export default async function Page(): Promise<ReactElement> { + const initialQueryState = buildInitialVaultsQuerySnapshot(undefined, DEFAULT_VAULT_QUERY_DEFAULTS) + const initialVaults = await getVaultsPageInitialPayload() + + return <VaultsPageClient initialQueryState={initialQueryState} initialVaults={initialVaults} /> +} diff --git a/app/vaults/[chainID]/[address]/loading.tsx b/app/vaults/[chainID]/[address]/loading.tsx new file mode 100644 index 000000000..db5e4969c --- /dev/null +++ b/app/vaults/[chainID]/[address]/loading.tsx @@ -0,0 +1,6 @@ +import { VaultDetailLoading } from '@pages/vaults/components/detail/VaultDetailLoading' +import type { ReactElement } from 'react' + +export default function Loading(): ReactElement { + return <VaultDetailLoading /> +} diff --git a/app/vaults/[chainID]/[address]/page-client.tsx b/app/vaults/[chainID]/[address]/page-client.tsx new file mode 100644 index 000000000..6f380ab95 --- /dev/null +++ b/app/vaults/[chainID]/[address]/page-client.tsx @@ -0,0 +1,8 @@ +'use client' + +import VaultsDetailPage from '@pages/vaults/[chainID]/[address]' +import type { ReactElement } from 'react' + +export default function VaultsDetailPageClient(): ReactElement { + return <VaultsDetailPage /> +} diff --git a/app/vaults/[chainID]/[address]/page.tsx b/app/vaults/[chainID]/[address]/page.tsx new file mode 100644 index 000000000..2f9d8b008 --- /dev/null +++ b/app/vaults/[chainID]/[address]/page.tsx @@ -0,0 +1,41 @@ +import { JsonLd } from '@shared/components/JsonLd' +import { HydrationBoundary } from '@tanstack/react-query' +import type { Metadata } from 'next' +import type { ReactElement } from 'react' +import { getVaultDetailPageDehydratedState } from '@/server/ssr/publicDataHydration' +import { buildVaultMetadata, buildVaultStructuredData, yearnOrganizationJsonLd } from '../../../metadata' +import VaultsDetailPageClient from './page-client' + +export const revalidate = 21600 + +type TVaultPageProps = { + params: Promise<{ + chainID: string + address: string + }> +} + +export async function generateMetadata({ params }: TVaultPageProps): Promise<Metadata> { + const { chainID, address } = await params + return await buildVaultMetadata(chainID, address) +} + +export default async function Page({ params }: TVaultPageProps): Promise<ReactElement> { + const { chainID, address } = await params + const structuredData = await buildVaultStructuredData(chainID, address) + const dehydratedState = await getVaultDetailPageDehydratedState(chainID, address) + + return ( + <> + {structuredData ? ( + <> + <JsonLd schema={yearnOrganizationJsonLd} /> + <JsonLd schema={structuredData} /> + </> + ) : null} + <HydrationBoundary state={dehydratedState}> + <VaultsDetailPageClient /> + </HydrationBoundary> + </> + ) +} diff --git a/app/vaults/layout.tsx b/app/vaults/layout.tsx new file mode 100644 index 000000000..bded6ed13 --- /dev/null +++ b/app/vaults/layout.tsx @@ -0,0 +1,6 @@ +import type { ReactElement, ReactNode } from 'react' +import App from '@/App' + +export default function VaultsLayout({ children }: { children: ReactNode }): ReactElement { + return <App>{children}</App> +} diff --git a/biome.jsonc b/biome.jsonc index 7bbdcd9ed..ae46125f8 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -69,10 +69,12 @@ "!**/build", "!**/node_modules", "!**/.next", + "!**/.codex", "!**/.git", "!**/generated", "!**/wagmi.ts", "!**/wagmi.d.ts", + "!next-env.d.ts", "!**/bun.lock", "!style.css", "!**/yearn.fi-worktree-*", diff --git a/bun.lock b/bun.lock index 4a14d4294..0eac13c92 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 1, "workspaces": { "": { "name": "yearnfi", @@ -12,17 +11,16 @@ "@tanstack/react-query": "5.90.21", "@tanstack/react-virtual": "3.13.26", "@upstash/redis": "1.37.0", - "@vercel/firewall": "1.2.1", "cross-fetch": "4.1.0", "ethers": "5.7.2", "framer-motion": "12.34.0", "graphql": "16.12.0", + "next": "16.2.6", "react": "19.2.6", "react-dom": "19.2.6", "react-hot-toast": "2.6.0", "react-markdown": "10.1.0", "react-rewards": "2.1.0", - "react-router": "7.15.0", "recharts": "2.15.4", "use-indexeddb": "2.0.2", "vaul": "1.1.2", @@ -38,15 +36,14 @@ "@types/node": "25.3.0", "@types/react": "19.2.15", "@types/react-dom": "19.2.3", - "@vercel/node": "5.6.3", - "@vitejs/plugin-react": "5.1.4", "bump": "0.2.5", "husky": "9.1.7", + "jsdom": "29.1.1", "lint-staged": "16.2.7", + "playwright": "1.61.0", "postcss": "8.5.15", "tailwindcss": "4.1.18", "typescript": "5.9.3", - "vite": "7.3.5", "vitest": "4.1.8", }, }, @@ -59,46 +56,20 @@ "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], - "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - - "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], - - "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="], - - "@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], + "@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.1.11", "", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@csstools/css-calc": "^3.2.0", "@csstools/css-color-parser": "^4.1.0", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg=="], - "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], + "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@7.1.1", "", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1" } }, "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ=="], - "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], + "@asamuzakjp/generational-cache": ["@asamuzakjp/generational-cache@1.0.1", "", {}, "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg=="], - "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], + "@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="], - "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], - - "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="], - - "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], - - "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - - "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw=="], - - "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q=="], - "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], - "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - - "@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - - "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - "@base-org/account": ["@base-org/account@1.1.1", "", { "dependencies": { "@noble/hashes": "1.4.0", "clsx": "1.2.1", "eventemitter3": "5.0.1", "idb-keyval": "6.2.1", "ox": "0.6.9", "preact": "10.24.2", "viem": "^2.31.7", "zustand": "5.0.3" } }, "sha512-IfVJPrDPhHfqXRDb89472hXkpvJuQQR7FDI9isLPHEqSYt/45whIoBxSPgZ0ssTt379VhQo4+87PWI1DoLSfAQ=="], "@biomejs/biome": ["@biomejs/biome@2.4.16", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.16", "@biomejs/cli-darwin-x64": "2.4.16", "@biomejs/cli-linux-arm64": "2.4.16", "@biomejs/cli-linux-arm64-musl": "2.4.16", "@biomejs/cli-linux-x64": "2.4.16", "@biomejs/cli-linux-x64-musl": "2.4.16", "@biomejs/cli-win32-arm64": "2.4.16", "@biomejs/cli-win32-x64": "2.4.16" }, "bin": { "biome": "bin/biome" } }, "sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA=="], @@ -119,75 +90,31 @@ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.16", "", { "os": "win32", "cpu": "x64" }, "sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw=="], - "@bytecodealliance/preview2-shim": ["@bytecodealliance/preview2-shim@0.17.6", "", {}, "sha512-n3cM88gTen5980UOBAD6xDcNNL3ocTK8keab21bpx1ONdA+ARj7uD1qoFxOWCyKlkpSi195FH+GeAut7Oc6zZw=="], + "@bramus/specificity": ["@bramus/specificity@2.4.2", "", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="], "@coinbase/wallet-sdk": ["@coinbase/wallet-sdk@4.3.6", "", { "dependencies": { "@noble/hashes": "1.4.0", "clsx": "1.2.1", "eventemitter3": "5.0.1", "idb-keyval": "6.2.1", "ox": "0.6.9", "preact": "10.24.2", "viem": "^2.27.2", "zustand": "5.0.3" } }, "sha512-4q8BNG1ViL4mSAAvPAtpwlOs1gpC+67eQtgIwNvT3xyeyFFd+guwkc8bcX5rTmQhXpqnhzC4f0obACbP9CqMSA=="], - "@ecies/ciphers": ["@ecies/ciphers@0.2.6", "", { "peerDependencies": { "@noble/ciphers": "^1.0.0" } }, "sha512-patgsRPKGkhhoBjETV4XxD0En4ui5fbX0hzayqI3M8tvNMGUoUvmyYAIWwlxBc1KX5cturfqByYdj5bYGRpN9g=="], - - "@edge-runtime/format": ["@edge-runtime/format@2.2.1", "", {}, "sha512-JQTRVuiusQLNNLe2W9tnzBlV/GvSVcozLl4XZHk5swnRZ/v6jp8TqR8P7sqmJsQqblDZ3EztcWmLDbhRje/+8g=="], - - "@edge-runtime/node-utils": ["@edge-runtime/node-utils@2.3.0", "", {}, "sha512-uUtx8BFoO1hNxtHjp3eqVPC/mWImGb2exOfGjMLUoipuWgjej+f4o/VP4bUI8U40gu7Teogd5VTeZUkGvJSPOQ=="], - - "@edge-runtime/ponyfill": ["@edge-runtime/ponyfill@2.4.2", "", {}, "sha512-oN17GjFr69chu6sDLvXxdhg0Qe8EZviGSuqzR9qOiKh4MhFYGdBBcqRNzdmYeAdeRzOW2mM9yil4RftUQ7sUOA=="], - - "@edge-runtime/primitives": ["@edge-runtime/primitives@4.1.0", "", {}, "sha512-Vw0lbJ2lvRUqc7/soqygUX216Xb8T3WBZ987oywz6aJqRxcwSVWwr9e+Nqo2m9bxobA9mdbWNNoRY6S9eko1EQ=="], - - "@edge-runtime/vm": ["@edge-runtime/vm@3.2.0", "", { "dependencies": { "@edge-runtime/primitives": "4.1.0" } }, "sha512-0dEVyRLM/lG4gp1R/Ik5bfPl/1wX00xFwd5KcNH602tzBa09oF7pbTKETEhR1GjZ75K6OJnYFu8II2dyMhONMw=="], - - "@emotion/hash": ["@emotion/hash@0.9.2", "", {}, "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="], - - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.27.0", "", { "os": "android", "cpu": "arm" }, "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.0", "", { "os": "android", "cpu": "arm64" }, "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.27.0", "", { "os": "android", "cpu": "x64" }, "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g=="], + "@csstools/color-helpers": ["@csstools/color-helpers@6.0.2", "", {}, "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ=="], + "@csstools/css-calc": ["@csstools/css-calc@3.2.1", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ=="], + "@csstools/css-color-parser": ["@csstools/css-color-parser@4.1.8", "", { "dependencies": { "@csstools/color-helpers": "^6.0.2", "@csstools/css-calc": "^3.2.1" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-3chWb7PRLijpJpPIKkDxdu6IBeO5MrFACND57On0j8OPpc0wZibcGc3xAHrSEbOx/KDRyMHoIxGn0w1PhXMYHw=="], - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw=="], + "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="], - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg=="], + "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.5", "", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg=="], + "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.0", "", { "os": "linux", "cpu": "x64" }, "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.0", "", { "os": "none", "cpu": "arm64" }, "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.0", "", { "os": "none", "cpu": "x64" }, "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A=="], - - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.0", "", { "os": "none", "cpu": "arm64" }, "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA=="], + "@ecies/ciphers": ["@ecies/ciphers@0.2.6", "", { "peerDependencies": { "@noble/ciphers": "^1.0.0" } }, "sha512-patgsRPKGkhhoBjETV4XxD0En4ui5fbX0hzayqI3M8tvNMGUoUvmyYAIWwlxBc1KX5cturfqByYdj5bYGRpN9g=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA=="], + "@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg=="], + "@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ=="], + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.0", "", { "os": "win32", "cpu": "x64" }, "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg=="], + "@emotion/hash": ["@emotion/hash@0.9.2", "", {}, "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="], "@ethereumjs/common": ["@ethereumjs/common@3.2.0", "", { "dependencies": { "@ethereumjs/util": "^8.1.0", "crc-32": "^1.2.0" } }, "sha512-pksvzI0VyLgmuEF2FA/JR/4/y6hcPq8OUail3/AvycBaW1d5VSauOZzqGvJ3RTmR4MU35lWE8KseKOsEhrFRBA=="], @@ -257,7 +184,7 @@ "@ethersproject/wordlists": ["@ethersproject/wordlists@5.7.0", "", { "dependencies": { "@ethersproject/bytes": "^5.7.0", "@ethersproject/hash": "^5.7.0", "@ethersproject/logger": "^5.7.0", "@ethersproject/properties": "^5.7.0", "@ethersproject/strings": "^5.7.0" } }, "sha512-S2TFNJNfHWVHNE6cNDjbVlZ6MgE17MIxMbMg2zv3wn+3XSJGosL1m9ZVv3GXCf/2ymSsQ+hRI5IzoMJTG6aoVA=="], - "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], + "@exodus/bytes": ["@exodus/bytes@1.15.1", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q=="], "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], @@ -273,17 +200,61 @@ "@headlessui/react": ["@headlessui/react@2.2.10", "", { "dependencies": { "@floating-ui/react": "^0.26.16", "@react-aria/focus": "^3.20.2", "@react-aria/interactions": "^3.25.0", "@tanstack/react-virtual": "^3.13.9", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-5pVLNK9wlpxTUTy9GpgbX/SdcRh+HBnPktjM2wbiLTH4p+2EPHBO1aoSryUCuKUIItdDWO9ITlhUL8UnUN/oIA=="], - "@internationalized/date": ["@internationalized/date@3.12.2", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw=="], + "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], - "@internationalized/number": ["@internationalized/number@3.6.7", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-3ji1fcrT+FPAK86UqEhB/psHixYo6niWPJtt7+qRaYFynt/BaJG8GhAPimtWUpEiVSTq8ZM8L5psMxGquiB/Vg=="], + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], - "@internationalized/string": ["@internationalized/string@3.2.9", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-kzP/M/mbQxODlmOt4bIQZ2SBVUWUSqMLXooXixnX7noche8WHaQcA+nwFN1K2KCF/cp+LDUhcJsCicwkvhD1pg=="], + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], + + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], + + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], + + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], + + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], - "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], - "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.1", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ=="], + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], - "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + + "@internationalized/date": ["@internationalized/date@3.12.2", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw=="], + + "@internationalized/number": ["@internationalized/number@3.6.7", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-3ji1fcrT+FPAK86UqEhB/psHixYo6niWPJtt7+qRaYFynt/BaJG8GhAPimtWUpEiVSTq8ZM8L5psMxGquiB/Vg=="], + + "@internationalized/string": ["@internationalized/string@3.2.9", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-kzP/M/mbQxODlmOt4bIQZ2SBVUWUSqMLXooXixnX7noche8WHaQcA+nwFN1K2KCF/cp+LDUhcJsCicwkvhD1pg=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], @@ -299,8 +270,6 @@ "@lit/reactive-element": ["@lit/reactive-element@2.1.2", "", { "dependencies": { "@lit-labs/ssr-dom-shim": "^1.5.0" } }, "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A=="], - "@mapbox/node-pre-gyp": ["@mapbox/node-pre-gyp@2.0.3", "", { "dependencies": { "consola": "^3.2.3", "detect-libc": "^2.0.0", "https-proxy-agent": "^7.0.5", "node-fetch": "^2.6.7", "nopt": "^8.0.0", "semver": "^7.5.3", "tar": "^7.4.0" }, "bin": { "node-pre-gyp": "bin/node-pre-gyp" } }, "sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg=="], - "@metamask/eth-json-rpc-provider": ["@metamask/eth-json-rpc-provider@1.0.1", "", { "dependencies": { "@metamask/json-rpc-engine": "^7.0.0", "@metamask/safe-event-emitter": "^3.0.0", "@metamask/utils": "^5.0.1" } }, "sha512-whiUMPlAOrVGmX8aKYVPvlKyG4CpQXiNNyt74vE1xb5sPvmx5oA7B/kOi/JdBvhGQq97U1/AVdXEdk2zkP8qyA=="], "@metamask/json-rpc-engine": ["@metamask/json-rpc-engine@8.0.2", "", { "dependencies": { "@metamask/rpc-errors": "^6.2.1", "@metamask/safe-event-emitter": "^3.0.0", "@metamask/utils": "^8.3.0" } }, "sha512-IoQPmql8q7ABLruW7i4EYVHWUbF74yrp63bRuXV5Zf9BQwcn5H9Ww1eLtROYvI1bUXwOiHZ6qT5CWTrDc/t/AA=="], @@ -329,55 +298,71 @@ "@metamask/utils": ["@metamask/utils@11.11.0", "", { "dependencies": { "@ethereumjs/tx": "^4.2.0", "@metamask/superstruct": "^3.1.0", "@noble/hashes": "^1.3.1", "@scure/base": "^1.1.3", "@types/debug": "^4.1.7", "@types/lodash": "^4.17.20", "debug": "^4.3.4", "lodash": "^4.17.21", "pony-cause": "^2.1.10", "semver": "^7.5.4", "uuid": "^9.0.1" } }, "sha512-0nF2CWjWQr/m0Y2t2lJnBTU1/CZPPTvKvcESLplyWe/tyeb8zFOi/FeneDmaFnML6LYRIGZU6f+xR0jKAIUZfw=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.5", "", { "dependencies": { "@tybys/wasm-util": "^0.10.2" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q=="], + + "@next/env": ["@next/env@16.2.6", "", {}, "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw=="], + + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg=="], + + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ=="], + + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w=="], + + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA=="], + + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw=="], + + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g=="], + + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg=="], + + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.6", "", { "os": "win32", "cpu": "x64" }, "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA=="], + "@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], "@noble/curves": ["@noble/curves@1.9.1", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA=="], "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], - - "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], - - "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + "@oxc-project/types": ["@oxc-project/types@0.137.0", "", {}, "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA=="], "@paulmillr/qr": ["@paulmillr/qr@0.2.1", "", {}, "sha512-IHnV6A+zxU7XwmKFinmYjUcwlyK9+xkG3/s9KcQhI9BjQKycrJ1JRO+FbNYPwZiPKW3je/DR0k7w8/gLa5eaxQ=="], "@plausible-analytics/tracker": ["@plausible-analytics/tracker@0.4.5", "", {}, "sha512-6BfAGejXY+YA3Cw6LYT2Zpn4hTxDtPQAawFsYUsQCOg78wIS5C4deAGXTfJffa5VleMWITv5lpJ/EYuQBl1tPA=="], - "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.4", "", {}, "sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ=="], - "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], + "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="], - "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + "@radix-ui/react-context": ["@radix-ui/react-context@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg=="], - "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="], + "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.17", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-dismissable-layer": "1.1.13", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.10", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-portal": "1.1.12", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-slot": "1.3.0", "@radix-ui/react-use-controllable-state": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TDTYmpdq8dI2+Xgvgj9AJ8Ghqq+Eph/TRVEdaFQPDItIY+6QSkU7MJMeevw1568Yw/2Ijz8BTphPSP2XejKphw=="], - "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-escape-keydown": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-2v+zNAWWe0ySxgC0D0yeXMPQ23xZVgXZTerTz+JKlmdRj6gfTqmCcR29jb6d290DezXPGgruHWDX/vYUebtErg=="], - "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q=="], - "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.10", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-callback-ref": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fas/lXQqhVvqwAb64s5RFeHiHYElZ6SUQbZaNd6EkfhP/Al7wTIQ9WIR4QVX475tlu5yFCEdDcJH6/UwsZjMWw=="], - "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="], - "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.12", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m309havGzsjLHHaIX50G5PlvRs3xkgPCsGk/5PTvYm8D5q33yG0J7w/712PTOhid7NTaFETtnSXjngHQavvhVw=="], - "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.6", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ=="], - "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.6", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g=="], - "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], - "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw=="], - "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.3", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.3", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA=="], - "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.3", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA=="], - "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], + "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.2", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2uVLvLjgO7NZCWw01/FdqRwmA42J0BcjPMUCA+koFEOAb+zjqIP7SiFz/7zWPrKnVmSqr76Omq2ALyCuX4dhLw=="], - "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], "@rainbow-me/rainbowkit": ["@rainbow-me/rainbowkit@2.2.11", "", { "dependencies": { "@vanilla-extract/css": "1.20.1", "@vanilla-extract/dynamic": "2.1.5", "@vanilla-extract/sprinkles": "1.6.5", "clsx": "2.1.1", "cuer": "0.0.3", "react-remove-scroll": "2.7.2", "ua-parser-js": "^2.0.9" }, "peerDependencies": { "@tanstack/react-query": ">=5.0.0", "react": ">=18", "react-dom": ">=18", "viem": "2.x", "wagmi": "^2.9.0" } }, "sha512-FHPsRHMBpuHHhuyKktAR13O9agmsUUunDnVEP4hG1dSZ2JojXLUSWyLG28VbGIJakHYylkNguiLFnqM/BM8ERA=="], @@ -389,9 +374,7 @@ "@react-hookz/web": ["@react-hookz/web@24.0.4", "", { "dependencies": { "@react-hookz/deep-equal": "^1.0.4" }, "peerDependencies": { "js-cookie": "^3.0.5", "react": "^16.8 || ^17 || ^18", "react-dom": "^16.8 || ^17 || ^18" }, "optionalPeers": ["js-cookie"] }, "sha512-DcIM6JiZklDyHF6CRD1FTXzuggAkQ+3Ncq2Wln7Kdih8GV6ZIeN9JfS6ZaQxpQUxan8/4n0J2V/R7nMeiSrb2Q=="], - "@react-types/shared": ["@react-types/shared@3.35.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-iNWvuzEwANttpQpdlu8nPBtdHb0mcCMj1ZTH//iRB5E/14IAnyRlR25rxH7pNLyzHINsPGEKnWvpwDMCT6vziQ=="], - - "@renovatebot/pep440": ["@renovatebot/pep440@4.2.1", "", {}, "sha512-2FK1hF93Fuf1laSdfiEmJvSJPVIDHEUTz68D3Fi9s0IZrrpaEcj6pTFBTbYvsgC5du4ogrtf5re7yMMvrKNgkw=="], + "@react-types/shared": ["@react-types/shared@3.36.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-DkP/H0C2YjjS7gZWKNqOmU8a16qHPjQNdzMwmTq9SzplM6Iw0kVMTZ0OIoe6FOgGqa+FwMsE2QbPjh/n3g/jXQ=="], "@reown/appkit": ["@reown/appkit@1.7.8", "", { "dependencies": { "@reown/appkit-common": "1.7.8", "@reown/appkit-controllers": "1.7.8", "@reown/appkit-pay": "1.7.8", "@reown/appkit-polyfills": "1.7.8", "@reown/appkit-scaffold-ui": "1.7.8", "@reown/appkit-ui": "1.7.8", "@reown/appkit-utils": "1.7.8", "@reown/appkit-wallet": "1.7.8", "@walletconnect/types": "2.21.0", "@walletconnect/universal-provider": "2.21.0", "bs58": "6.0.0", "valtio": "1.13.2", "viem": ">=2.29.0" } }, "sha512-51kTleozhA618T1UvMghkhKfaPcc9JlKwLJ5uV+riHyvSoWPKPRIa5A6M1Wano5puNyW0s3fwywhyqTHSilkaA=="], @@ -411,59 +394,37 @@ "@reown/appkit-wallet": ["@reown/appkit-wallet@1.7.8", "", { "dependencies": { "@reown/appkit-common": "1.7.8", "@reown/appkit-polyfills": "1.7.8", "@walletconnect/logger": "2.1.2", "zod": "3.22.4" } }, "sha512-kspz32EwHIOT/eg/ZQbFPxgXq0B/olDOj3YMu7gvLEFz4xyOFd/wgzxxAXkp5LbG4Cp++s/elh79rVNmVFdB9A=="], - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], - - "@rollup/pluginutils": ["@rollup/pluginutils@5.4.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg=="], - - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.61.0", "", { "os": "android", "cpu": "arm" }, "sha512-dnxczajOqt0gesZlN5pGQ1s1imQVrsmCw5G2Ci4oM+0WvNz3pyRnlWrT7McoZIb8VlFwCawdmbWRmxRn7HI+VQ=="], - - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.61.0", "", { "os": "android", "cpu": "arm64" }, "sha512-Bp3JpGP00Vu3f238ivRrjf7z3xSzVPXqCmaJYA9t2c+c8vKYvOzmXF7LkkeUalTEGd6cZcSWe+PFIP3Vy48fRg=="], - - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.61.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zaYIpr670mUmmZ1tVzUFplbQbG7h3Gugx3L5FoqhsC2m/YnLlR1a7zVLmXNPy+iY1tFPEbNG+HHBXZGyId0G5w=="], - - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.61.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-+P49fvkv2dSoeevUW+lgZ/I2JHSsJCK1Lyjj7Cu6E4UHG4tS9XIefzIjo5qhgELjAclnen1rLzK2PMKJdo+Dyg=="], - - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.61.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-l3FAAOyKJXH2ea6KNFN+MMgC/rnE94YGLXs2ehYqDcCoHt1DpvgWX75BhUJxN38XojP7Ul+4H8PRn7EdyqSDrw=="], - - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.61.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-VokPN3TSctKj65cyCNPaUh4vMFA8awxOot/0sp+4J7ZlNRKQEhXhawqPwajoi8H5ZFt61i0ugZJuTKXBjGJ17Q=="], - - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.61.0", "", { "os": "linux", "cpu": "arm" }, "sha512-DxH0P3wxm+Yzs/p3zrk9dw1rURu8p0Nv5+MRK/L7OtnLNg5rLZraSBFZ8iUXOd9f2BlhJyEpIZUH/emjq4UJ4g=="], - - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.61.0", "", { "os": "linux", "cpu": "arm" }, "sha512-T6ZvMNe84kAz6TBWHC7hGAoEtzP1LWYw/AqayGWEF6uISt3Abk/st06LqRD9THd7Xz3NxzurUpzAuEAUbZf+nw=="], - - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.61.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-q/4hzvQkDs8b4jIBab1pnLiiM0ayTZsN2amBFPDzuyZxjEd4wDwx0UJFYM3cOZzSf5Kw8fnWSprJzIBMkcR44Q=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.2", "", { "os": "android", "cpu": "arm64" }, "sha512-2cZ+7xRS+DBcuJBJKnfzsbleumJhBqSlJVpuzHC0nTqfd3QQ7Vx2/x5YR/D7cBamKSeWplwo82Fn9lqYUDEMfA=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.61.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-vvYWX3akdEAY6km+9wAqFDnk6pQsbJKVnj7xawcvs/+fdlYBGp+U+Qq/lLfpIxYIZvZLHMAKD9HLdacSx/r3dw=="], + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RkPMJnygxsgOYdkfqgpwY0/Fzm8d0VQe6HGU2/B00Xa9eqdLbrII+DOKAodbJAn3ZL1AJxGHkZRPYazgGY6Ljw=="], - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.61.0", "", { "os": "linux", "cpu": "none" }, "sha512-DePa5cqOxDP/Zp0VOXpeWaGew5iIv5DXp9NYbzkX5PFQyWVX9184WCTh3hvr/7lhXo8ZVlbFLkz8+o/q1dU6gA=="], + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-Uiczh6vFhwyfd7WNe7Q7mCA4KxAiLdz7jPE/WGizfRpIieoyFuNVMmM8HqZ9HwudTkY6/AeMQwlNJ9NJijguWw=="], - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.61.0", "", { "os": "linux", "cpu": "none" }, "sha512-LV8aWMB8UChglMCEzs7RkN0GsH29RJaLLqwm9fCIjlqwxQTiWAqNcc7wjBkH31hV0PU/yVxGYvrYsgfea2qw6g=="], + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-+TpdtTRgHiJFjCVFbw311SuLk3KfytPOQQn+VlAEv+gBxYPtL7E6JS9e/tk+8CwxhIZvemJKo4rTKgfWNsKkkA=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.61.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-QoNSnwQtaeNu5grdBbsL0tt1uyl5EnS8DA8Mr3nluMXbhdQNyhN+G4tBax7VCdxLKj8YJ0/4OO9Ho84jMnJtKA=="], + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.2", "", { "os": "linux", "cpu": "arm" }, "sha512-4lv1/tkmi7ueIVHnyreaOeUpiZP26BH9rRy6hoYfR9310A2B9nUEVRDvBx69vx64Nr3eTPPRkyciqJJs+j9Jmw=="], - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.61.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-/zZp5MKapIIApE8trN8qLGNSiRN9TUoaUZ1cmVu4XnVdd5LQLOXTtyi+vtfUbNnT3iyjzpPqYeKXmvJ+gJGYWw=="], + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-gBSUVO0eaWgw1JMjK3gB8BMlX2Mk148s2lTiVT3e9vjVxbl7UDfMWWY8CfIaaqiXuM9fVTMxIpUz6CAo/B6Vlw=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.61.0", "", { "os": "linux", "cpu": "none" }, "sha512-RbrzcD3aJ1k3UbtMRRBNwojdVVyXjuVAFTfn/xPa6EEl6GE9Sm/akPgFTb9aAC9pMKGJ6CtWxaGrqWcabH+ySg=="], + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-LjQP/iZLBu8o8PjIfk4x3At0/mT6h282pvz8Z5LAyhGbu/kDezyO7ea62rF5uoqmgnIYqbN/MqJ3Si3Aymi7xQ=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.61.0", "", { "os": "linux", "cpu": "none" }, "sha512-ZF+onDsBso8PJf1XaG9lB+O9RnBpKGnY6OrzC4CSHrtC1jb6jWLTKK4bRqdoCXHd22gyr2hiYmEAm8Wns/BOCw=="], + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-X/7bVLWelEsbyWDUSXt7zVsTniLLPIY2n1rH58qr78l9i7MNbbxBWD8gI2vRfBWf4NUXJCUuQnfZDsp32LqsfQ=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.61.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-Atk0aSIk5Zx2Wuh9dgRQgLP0Koc8hOeYpbWryMXyk8G8/HmPkwPPkMqIIDhrXHHYqfUzSJA/I7IWSBv8xSmRBA=="], + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-gb6dYKW/1KDorGXyy48glEBJs/sxVSC5pcVrox/pFGV4mvwSFeg2sK5L2tRkVsVlh7kueqOgg4GEcuipJcGuKg=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.61.0", "", { "os": "linux", "cpu": "x64" }, "sha512-0uMOcf3eZ5K+K4cYHkdxShFMPlPXCOdfDFEFn9dNYAEEd2cVvmOfH7zFgRVoDgmtQ1m9k5q7qfrHzyMAubKYUA=="], + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.2", "", { "os": "linux", "cpu": "x64" }, "sha512-JY4w85pU3iAiJVMh5nuk4/Mh9GjMsupe8MrIN53rwxAZW64GKrWeJBuN6SxQg9QTU5uB1cxyhDzW8jqRn1EABw=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.61.0", "", { "os": "linux", "cpu": "x64" }, "sha512-mvFtE4A/t/7hRJ7X8Ozmu8FsIkAUat2nzl12pgU337BRmq87AQUJztwHz2Zv5/tjo9/C95E66CK03SI/ToEDJw=="], + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.2", "", { "os": "linux", "cpu": "x64" }, "sha512-xvpA7o5KCYLB0Rwscmuylb1/zHHSUx4g4xilm4prC5jP76pEUlzBmMbgpbh7bVDbId4NcfT96gN5i6mE6UDaiw=="], - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.61.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-z9b9+aTxvt8n2rNltMPvyaUfB8NJ+CVyOrGK/MdIKHx7B+lXmZpm/XbRsU7Rpf3fRqJ2uS6mBJiJveCtq8LHDg=="], + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.2", "", { "os": "none", "cpu": "arm64" }, "sha512-p/ts6KBLjuk49Bp21XH77poQGt02iNz7ChgHep7tudPOaLinR/De/RHdxF8w8Yj4r/bF/bqXwH6PZrB2sA+Nvw=="], - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.61.0", "", { "os": "none", "cpu": "arm64" }, "sha512-jXaXFqKMehsOc+g8R6oo33RRC6w07G9jDBxAE5eAKX7mOcCbZloYIPNhfG9Wl+P9O9IWHFO4OJgPi1Ml2qkt7w=="], + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.2", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.5" }, "cpu": "none" }, "sha512-VMu/wmrZ9hJzYlRhbw7jK5PODlugyKZ5mOdX78+lS8OvuFkWNQdz1pFLrI2p3P0pjXOmUZ7B48o5VnMH9QOGtg=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.61.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-OXNWVFocS2IA4+QplhTZZ2a+8hPZR7T8KuozsNmJKK8y7cp83StHvGksfHzPG3wczWTczyWHVQuqeiTUbjiyBg=="], + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-xtUJqs8qEkuSviS0n1tsohaPuz3a1SPhZywOji4Oo+sgrJs8daEDMZ0QtqL0OS7dx8PoVpg2J/ZZycPY5I2+Zg=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.61.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-AlAbNtBO637LxSldqV43z0FfXoGfl2TW1DgAg/bs7aQswFbDewz2SJm3BUhiGfbOVtW571xbc9p+REdxhyN/Eg=="], + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.2", "", { "os": "win32", "cpu": "x64" }, "sha512-85YiLQqjUKgSO/Zjnf9e0XIn5Ymrh1fLDWBeAkZqpuBR/3R8TpfoHXuyblqyQrftSSgWO9qpcHN8mkyKsLraoA=="], - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.61.0", "", { "os": "win32", "cpu": "x64" }, "sha512-QRSrQXyJ1M4tjNXdR0/G/IgV6lzfQQJYBjlWIEYkY2Xs86DRl/iEpQ4blMDjJxSl7n19eDKKXMg0AmuBVYy8pQ=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.61.0", "", { "os": "win32", "cpu": "x64" }, "sha512-tkuFxhvKO/HlGd0VsINF6vHSYH8AF8W0TcNxKDK6JZmrehngFj78pToc8iemtnvwilDjs2G/qSzYFhe9U8q+fw=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], "@safe-global/safe-apps-provider": ["@safe-global/safe-apps-provider@0.18.6", "", { "dependencies": { "@safe-global/safe-apps-sdk": "^9.1.0", "events": "^3.3.0" } }, "sha512-4LhMmjPWlIO8TTDC2AwLk44XKXaK6hfBTWyljDm0HQ6TWlOEijVWNrt2s3OCVMSxlXAcEzYfqyu1daHZooTC2Q=="], @@ -481,7 +442,7 @@ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@swc/helpers": ["@swc/helpers@0.5.23", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw=="], + "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], "@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="], @@ -525,18 +486,10 @@ "@testing-library/react": ["@testing-library/react@16.3.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g=="], - "@ts-morph/common": ["@ts-morph/common@0.11.1", "", { "dependencies": { "fast-glob": "^3.2.7", "minimatch": "^3.0.4", "mkdirp": "^1.0.4", "path-browserify": "^1.0.1" } }, "sha512-7hWZS0NRpEsNV8vWJzg7FEz6V8MaLNeJOmwmghqUXTpzk16V1LLZhdo+4QvE/+zv4cVci0OviuJFnqhEfoV3+g=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], - "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], - - "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], - - "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], - - "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], - "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], @@ -567,8 +520,6 @@ "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], - "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - "@types/lodash": ["@types/lodash@4.17.24", "", {}, "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ=="], "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], @@ -587,7 +538,7 @@ "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - "@ungap/structured-clone": ["@ungap/structured-clone@1.3.1", "", {}, "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ=="], + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.2", "", {}, "sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA=="], "@upstash/redis": ["@upstash/redis@1.37.0", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-LqOJ3+XWPLSZ2rGSed5DYG3ixybxb8EhZu3yQqF7MdZX1wLBG/FRcI6xcUZXHy/SS7mmXWyadrud0HJHkOc+uw=="], @@ -599,22 +550,6 @@ "@vanilla-extract/sprinkles": ["@vanilla-extract/sprinkles@1.6.5", "", { "peerDependencies": { "@vanilla-extract/css": "^1.0.0" } }, "sha512-HOYidLONR/SeGk8NBAeI64I4gYdsMX9vJmniL13ZcLVwawyK0s2GUENEAcGA+GYLIoeyQB61UqmhqPodJry7zA=="], - "@vercel/build-utils": ["@vercel/build-utils@13.4.0", "", { "dependencies": { "@vercel/python-analysis": "0.4.1" } }, "sha512-hGBYt+olxUtDZu0W8DKl4NjY8tQC1eJUxDpkJdOhcugap4NnHeHZQ33vEKs0Z72bYlqotgu0AoEOv+ri5rlh4w=="], - - "@vercel/error-utils": ["@vercel/error-utils@2.0.3", "", {}, "sha512-CqC01WZxbLUxoiVdh9B/poPbNpY9U+tO1N9oWHwTl5YAZxcqXmmWJ8KNMFItJCUUWdY3J3xv8LvAuQv2KZ5YdQ=="], - - "@vercel/firewall": ["@vercel/firewall@1.2.1", "", {}, "sha512-WlxjEPpf+GWYMontNNZqZjvLL40ziGwy9swwI9da/L1fB/syAO1S2fMtlBXkp4EGVF6nlXSKmwvkUd6YPWJbbg=="], - - "@vercel/nft": ["@vercel/nft@1.1.1", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^13.0.0", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-mKMGa7CEUcXU75474kOeqHbtvK1kAcu4wiahhmlUenB5JbTQB8wVlDI8CyHR3rpGo0qlzoRWqcDzI41FUoBJCA=="], - - "@vercel/node": ["@vercel/node@5.6.3", "", { "dependencies": { "@edge-runtime/node-utils": "2.3.0", "@edge-runtime/primitives": "4.1.0", "@edge-runtime/vm": "3.2.0", "@types/node": "20.11.0", "@vercel/build-utils": "13.4.0", "@vercel/error-utils": "2.0.3", "@vercel/nft": "1.1.1", "@vercel/static-config": "3.1.2", "async-listen": "3.0.0", "cjs-module-lexer": "1.2.3", "edge-runtime": "2.5.9", "es-module-lexer": "1.4.1", "esbuild": "0.27.0", "etag": "1.8.1", "mime-types": "2.1.35", "node-fetch": "2.6.9", "path-to-regexp": "6.1.0", "path-to-regexp-updated": "npm:path-to-regexp@6.3.0", "ts-morph": "12.0.0", "tsx": "4.21.0", "typescript": "npm:typescript@5.9.3", "undici": "5.28.4" } }, "sha512-sordxYoSnYquTxQ2sSIubukhZtAThSAjT+A+ACFac9nkOWyPfW83eQINPWBD/xotdNm3w+1v/afkt427GbXfNg=="], - - "@vercel/python-analysis": ["@vercel/python-analysis@0.4.1", "", { "dependencies": { "@bytecodealliance/preview2-shim": "0.17.6", "@renovatebot/pep440": "4.2.1", "fs-extra": "11.1.1", "js-yaml": "4.1.1", "minimatch": "10.1.1", "pip-requirements-js": "1.0.2", "smol-toml": "1.5.2", "zod": "3.22.4" } }, "sha512-Ae6zayRNAO//z9ULOV+T8sjJFJLfA0OU540/BDkaVWHWhlqElIA7uyi71WuD7Ls06CDVpKYNRysv3keJaVbwow=="], - - "@vercel/static-config": ["@vercel/static-config@3.1.2", "", { "dependencies": { "ajv": "8.6.3", "json-schema-to-ts": "1.6.4", "ts-morph": "12.0.0" } }, "sha512-2d+TXr6K30w86a+WbMbGm2W91O0UzO5VeemZYBBUJbCjk/5FLLGIi8aV6RS2+WmaRvtcqNTn2pUA7nCOK3bGcQ=="], - - "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.4", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA=="], - "@vitest/expect": ["@vitest/expect@4.1.8", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.8", "@vitest/utils": "4.1.8", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ=="], "@vitest/mocker": ["@vitest/mocker@4.1.8", "", { "dependencies": { "@vitest/spy": "4.1.8", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw=="], @@ -677,20 +612,10 @@ "@walletconnect/window-metadata": ["@walletconnect/window-metadata@1.0.1", "", { "dependencies": { "@walletconnect/window-getters": "^1.0.1", "tslib": "1.14.1" } }, "sha512-9koTqyGrM2cqFRW517BPY/iEtUDx2r1+Pwwu5m7sJ7ka79wi3EyqhqcICk/yDmv6jAS1rjKgTKXlEhanYjijcA=="], - "abbrev": ["abbrev@3.0.1", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="], - "abitype": ["abitype@1.2.3", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg=="], - "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], - - "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], - "aes-js": ["aes-js@3.0.0", "", {}, "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw=="], - "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - - "ajv": ["ajv@8.6.3", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw=="], - "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -699,20 +624,14 @@ "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], "aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], - "async-listen": ["async-listen@3.0.0", "", {}, "sha512-V+SsTpDqkrWTimiotsyl33ePSjA5/KrithwupuvJ6ztsqPvGv6ge4OredFhPffVXiLN/QUWvE0XcqJaYgt6fOg=="], - "async-mutex": ["async-mutex@0.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-Hs4R+4SPgamu6rSGW8C7cV9gaWUKEHykfzCCvIRuaVv636Ju10ZdeUbvb4TBEW0INuq2DHZqXbK4Nd3yG4RaRw=="], - "async-sema": ["async-sema@3.1.1", "", {}, "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg=="], - "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], @@ -725,13 +644,13 @@ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.33", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.38", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw=="], "bech32": ["bech32@1.1.4", "", {}, "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ=="], - "big.js": ["big.js@6.2.2", "", {}, "sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ=="], + "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], - "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], + "big.js": ["big.js@6.2.2", "", {}, "sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ=="], "bn.js": ["bn.js@5.2.3", "", {}, "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w=="], @@ -743,8 +662,6 @@ "brorand": ["brorand@1.1.0", "", {}, "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w=="], - "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], - "bs58": ["bs58@6.0.0", "", { "dependencies": { "base-x": "^5.0.0" } }, "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw=="], "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], @@ -761,7 +678,7 @@ "camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], - "caniuse-lite": ["caniuse-lite@1.0.30001793", "", {}, "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA=="], + "caniuse-lite": ["caniuse-lite@1.0.30001799", "", {}, "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw=="], "cbw-sdk": ["@coinbase/wallet-sdk@3.9.3", "", { "dependencies": { "bn.js": "^5.2.1", "buffer": "^6.0.3", "clsx": "^1.2.1", "eth-block-tracker": "^7.1.0", "eth-json-rpc-filters": "^6.0.0", "eventemitter3": "^5.0.1", "keccak": "^3.0.3", "preact": "^10.16.0", "sha.js": "^2.4.11" } }, "sha512-N/A2DRIf0Y3PHc1XAMvbBUu4zisna6qAdqABMZwBMNEfWrXpAwx16pZGkYCLGE+Rvv1edbcB2LYDRnACNcmCiw=="], @@ -779,20 +696,16 @@ "chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], - "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], - - "cjs-module-lexer": ["cjs-module-lexer@1.2.3", "", {}, "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ=="], - "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], "cli-truncate": ["cli-truncate@5.2.0", "", { "dependencies": { "slice-ansi": "^8.0.0", "string-width": "^8.2.0" } }, "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw=="], + "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], + "cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], - "code-block-writer": ["code-block-writer@10.1.1", "", {}, "sha512-67ueh2IRGst/51p0n6FvPrnRjAGHY5F8xdjkgrYE7DDzpJe6qA07RYQ9VcoUeo5ATOjSOiWpSL3SWBRRbempMw=="], - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], @@ -803,16 +716,8 @@ "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - - "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], - - "convert-hrtime": ["convert-hrtime@3.0.0", "", {}, "sha512-7V+KqSvMiHp8yWDuwfww06XleMWVVB9b9tURBx+G7UTADuo5hYPuowKloz4OzOqbPezxgo+fdQ1522WzPG4OeA=="], - "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], - "cookie-es": ["cookie-es@1.2.3", "", {}, "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw=="], "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], @@ -823,6 +728,8 @@ "crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="], + "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], + "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], @@ -851,6 +758,8 @@ "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + "data-urls": ["data-urls@7.0.0", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0" } }, "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA=="], + "date-fns": ["date-fns@2.30.0", "", { "dependencies": { "@babel/runtime": "^7.21.0" } }, "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw=="], "dayjs": ["dayjs@1.11.13", "", {}, "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="], @@ -859,6 +768,8 @@ "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], + "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], + "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], @@ -903,10 +814,6 @@ "eciesjs": ["eciesjs@0.4.18", "", { "dependencies": { "@ecies/ciphers": "^0.2.5", "@noble/ciphers": "^1.3.0", "@noble/curves": "^1.9.7", "@noble/hashes": "^1.8.0" } }, "sha512-wG99Zcfcys9fZux7Cft8BAX/YrOJLJSZ3jyYPfhZHqN2E+Ffx+QXBDsv3gubEgPtV6dTzJMSQUwk1H98/t/0wQ=="], - "edge-runtime": ["edge-runtime@2.5.9", "", { "dependencies": { "@edge-runtime/format": "2.2.1", "@edge-runtime/ponyfill": "2.4.2", "@edge-runtime/vm": "3.2.0", "async-listen": "3.0.1", "mri": "1.2.0", "picocolors": "1.0.0", "pretty-ms": "7.0.1", "signal-exit": "4.0.2", "time-span": "4.0.0" }, "bin": { "edge-runtime": "dist/cli/index.js" } }, "sha512-pk+k0oK0PVXdlT4oRp4lwh+unuKB7Ng4iZ2HB+EZ7QCEQizX360Rp/F4aRpgpRgdP2ufB35N+1KppHmYjqIGSg=="], - - "electron-to-chromium": ["electron-to-chromium@1.5.364", "", {}, "sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw=="], - "elliptic": ["elliptic@6.5.4", "", { "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", "hash.js": "^1.0.0", "hmac-drbg": "^1.0.1", "inherits": "^2.0.4", "minimalistic-assert": "^1.0.1", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ=="], "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], @@ -915,11 +822,13 @@ "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], - "engine.io-client": ["engine.io-client@6.6.5", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.20.1", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-QCwxUDULPlXv8F6tqMMKx5dNkTe6OaBYRMPYeXKBlyOoKvAmE0ac6pW7fFhSscJ/5SI7666/U/B+MElbsrJlIg=="], + "engine.io-client": ["engine.io-client@6.6.6", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.21.0", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q=="], "engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="], - "enhanced-resolve": ["enhanced-resolve@5.22.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww=="], + "enhanced-resolve": ["enhanced-resolve@5.24.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-SkE2t82KlkkxQRVMVLAGKxLfORGQfrkx5dkj+vlgXRVNEdPc4eZcR+J/Fvj8C+yKSFH5L0q3NFlyufOVQnCcYQ=="], + + "entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="], "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], @@ -927,21 +836,15 @@ "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - "es-module-lexer": ["es-module-lexer@1.4.1", "", {}, "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w=="], + "es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="], "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], "es-toolkit": ["es-toolkit@1.33.0", "", {}, "sha512-X13Q/ZSc+vsO1q600bvNK4bxgXMkHcf//RxCmYDaRY5DAcT+eoXjY5hoAPGMdRnWQjvyLEcyauG3b6hz76LNqg=="], - "esbuild": ["esbuild@0.27.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.0", "@esbuild/android-arm": "0.27.0", "@esbuild/android-arm64": "0.27.0", "@esbuild/android-x64": "0.27.0", "@esbuild/darwin-arm64": "0.27.0", "@esbuild/darwin-x64": "0.27.0", "@esbuild/freebsd-arm64": "0.27.0", "@esbuild/freebsd-x64": "0.27.0", "@esbuild/linux-arm": "0.27.0", "@esbuild/linux-arm64": "0.27.0", "@esbuild/linux-ia32": "0.27.0", "@esbuild/linux-loong64": "0.27.0", "@esbuild/linux-mips64el": "0.27.0", "@esbuild/linux-ppc64": "0.27.0", "@esbuild/linux-riscv64": "0.27.0", "@esbuild/linux-s390x": "0.27.0", "@esbuild/linux-x64": "0.27.0", "@esbuild/netbsd-arm64": "0.27.0", "@esbuild/netbsd-x64": "0.27.0", "@esbuild/openbsd-arm64": "0.27.0", "@esbuild/openbsd-x64": "0.27.0", "@esbuild/openharmony-arm64": "0.27.0", "@esbuild/sunos-x64": "0.27.0", "@esbuild/win32-arm64": "0.27.0", "@esbuild/win32-ia32": "0.27.0", "@esbuild/win32-x64": "0.27.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA=="], - - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], - "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], - - "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], "eth-block-tracker": ["eth-block-tracker@7.1.0", "", { "dependencies": { "@metamask/eth-json-rpc-provider": "^1.0.0", "@metamask/safe-event-emitter": "^3.0.0", "@metamask/utils": "^5.0.1", "json-rpc-random-id": "^1.0.1", "pify": "^3.0.0" } }, "sha512-8YdplnuE1IK4xfqpf4iU7oBxnOYAc35934o083G8ao+8WM8QQtt/mVlAY6yIAdY1eMeLqg4Z//PZjJGmWGPMRg=="], @@ -971,18 +874,12 @@ "fast-equals": ["fast-equals@5.4.0", "", {}, "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw=="], - "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], - "fast-redact": ["fast-redact@3.5.0", "", {}, "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A=="], "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], - "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "filter-obj": ["filter-obj@1.1.0", "", {}, "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ=="], @@ -993,16 +890,12 @@ "framer-motion": ["framer-motion@12.34.0", "", { "dependencies": { "motion-dom": "^12.34.0", "motion-utils": "^12.29.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-+/H49owhzkzQyxtn7nZeF4kdH++I2FWrESQ184Zbcw5cEqNHYkE5yxWxcTLSj5lNx3NWdbIRy5FHqUvetD8FWg=="], - "fs-extra": ["fs-extra@11.1.1", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ=="], - - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], - "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], - "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], @@ -1013,12 +906,6 @@ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], - - "glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], - - "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - "goober": ["goober@2.1.19", "", { "peerDependencies": { "csstype": "^3.0.10" } }, "sha512-U7veizMqxyKlM58+Z5j2ngJBH/r9siDmxpvNxSw0PylF6WQvrASJEZrxh1hidRBJc2jqoBVSyOban5u8m+6Rxg=="], "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], @@ -1045,11 +932,11 @@ "hmac-drbg": ["hmac-drbg@1.0.1", "", { "dependencies": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg=="], - "hono": ["hono@4.12.23", "", {}, "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA=="], + "hono": ["hono@4.12.27", "", {}, "sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q=="], - "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], + "html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="], - "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], @@ -1075,20 +962,18 @@ "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - "is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], + "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], "is-standalone-pwa": ["is-standalone-pwa@0.1.1", "", {}, "sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g=="], @@ -1107,22 +992,12 @@ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - - "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + "jsdom": ["jsdom@29.1.1", "", { "dependencies": { "@asamuzakjp/css-color": "^5.1.11", "@asamuzakjp/dom-selector": "^7.1.1", "@bramus/specificity": "^2.4.2", "@csstools/css-syntax-patches-for-csstree": "^1.1.3", "@exodus/bytes": "^1.15.0", "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.3.5", "parse5": "^8.0.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.1", "undici": "^7.25.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.1", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q=="], "json-rpc-engine": ["json-rpc-engine@6.1.0", "", { "dependencies": { "@metamask/safe-event-emitter": "^2.0.0", "eth-rpc-errors": "^4.0.2" } }, "sha512-NEdLrtrq1jUZyfjkr9OCz9EzCNhnRyWtt1PAnvnhwy6e8XETS0Dtc+ZNCO2gvuAoKsIn2+vCSowXTYE4CkgnAQ=="], "json-rpc-random-id": ["json-rpc-random-id@1.0.1", "", {}, "sha512-RJ9YYNCkhVDBuP4zN5BBtYAzEl03yq/jIIsyif0JY9qyJuQQZNeDK7anAPKKlyEtLSj2s8h6hNh2F8zO5q7ScA=="], - "json-schema-to-ts": ["json-schema-to-ts@1.6.4", "", { "dependencies": { "@types/json-schema": "^7.0.6", "ts-toolbelt": "^6.15.5" } }, "sha512-pR4yQ9DHz6itqswtHCm26mw45FSNfQ9rEQjosaZErhn5J3J2sIViQiz8rDaezjKAhFGpmsoczYVBgGHzFw/stA=="], - - "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - - "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], - - "jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], - "keccak": ["keccak@3.0.4", "", { "dependencies": { "node-addon-api": "^2.0.0", "node-gyp-build": "^4.2.0", "readable-stream": "^3.6.0" } }, "sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q=="], "keyvaluestorage-interface": ["keyvaluestorage-interface@1.0.0", "", {}, "sha512-8t6Q3TclQ4uZynJY9IGr2+SsIGwK9JHcO6ootkHCGA0CrQCRy+VkouYNO2xicET6b9al7QKzpebNow+gkpCL8g=="], @@ -1171,7 +1046,7 @@ "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], - "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="], "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], @@ -1195,9 +1070,9 @@ "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], - "media-query-parser": ["media-query-parser@2.0.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" } }, "sha512-1N4qp+jE0pL5Xv4uEcwVUhIkwdUO3S/9gML90nqKA7v7FcOS5vUtatfzok9S9U1EJU8dHWlcv95WLnKmmxZI9w=="], + "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], - "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + "media-query-parser": ["media-query-parser@2.0.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" } }, "sha512-1N4qp+jE0pL5Xv4uEcwVUhIkwdUO3S/9gML90nqKA7v7FcOS5vUtatfzok9S9U1EJU8dHWlcv95WLnKmmxZI9w=="], "micro-ftch": ["micro-ftch@0.3.1", "", {}, "sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg=="], @@ -1245,10 +1120,6 @@ "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - - "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], "minimalistic-assert": ["minimalistic-assert@1.0.1", "", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="], @@ -1257,33 +1128,27 @@ "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], - "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], - - "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], - "mipd": ["mipd@0.0.7", "", { "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-aAPZPNDQ3uMTdKbuO2YmAw2TxLHO0moa4YKAyETM/DTj5FloZo+a+8tU+iv4GmW+sOxKLSRwcSFuczk+Cpt6fg=="], - "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], - "modern-ahocorasick": ["modern-ahocorasick@1.1.0", "", {}, "sha512-sEKPVl2rM+MNVkGQt3ChdmD8YsigmXdn5NifZn6jiwn9LRJpWm8F3guhaqrJT/JOat6pwpbXEk6kv+b9DMIjsQ=="], - "motion-dom": ["motion-dom@12.40.0", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg=="], + "motion-dom": ["motion-dom@12.41.0", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-Lk3J39fOGg6xNr1KRZsN6usDyBf8aP7MEbUPez1VCughHt79OrP7VGqNrPyFL0riaT7WS8t9DRw1M3BHtM/xKw=="], "motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="], - "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], - "ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], "multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], "nano-spawn": ["nano-spawn@2.1.0", "", {}, "sha512-yTW+2okrElHiH4fsiz/+/zc0EDo9BDDoC3iKk8dpv1GeRc9nUWzUZHx6TofMWErchhUQR8hY9/Eu1Uja9x1nqA=="], - "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + "nanoid": ["nanoid@3.3.15", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="], + + "next": ["next@16.2.6", "", { "dependencies": { "@next/env": "16.2.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.6", "@next/swc-darwin-x64": "16.2.6", "@next/swc-linux-arm64-gnu": "16.2.6", "@next/swc-linux-arm64-musl": "16.2.6", "@next/swc-linux-x64-gnu": "16.2.6", "@next/swc-linux-x64-musl": "16.2.6", "@next/swc-win32-arm64-msvc": "16.2.6", "@next/swc-win32-x64-msvc": "16.2.6", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw=="], "node-addon-api": ["node-addon-api@2.0.2", "", {}, "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA=="], - "node-fetch": ["node-fetch@2.6.9", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg=="], + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], @@ -1291,22 +1156,16 @@ "node-mock-http": ["node-mock-http@1.0.4", "", {}, "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ=="], - "node-releases": ["node-releases@2.0.46", "", {}, "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ=="], - - "nopt": ["nopt@8.1.0", "", { "dependencies": { "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A=="], - "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], "obj-multiplex": ["obj-multiplex@1.0.0", "", { "dependencies": { "end-of-stream": "^1.4.0", "once": "^1.4.0", "readable-stream": "^2.3.3" } }, "sha512-0GNJAOsHoBHeNTvl5Vt6IWnpUEcc3uSRxzBri7EDyIcMgYvnY2JL2qdeV5zTMjWQX5OHcD5amcW2HFfDh0gjIA=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + "obug": ["obug@2.1.3", "", {}, "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg=="], "ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], - "ohm-js": ["ohm-js@17.5.0", "", {}, "sha512-l4Sa7026+6jsvYbt0PXKmL+f+ML32fD++IznLgxDhx2t9Cx6NC7zwRqblCujPHGGmkQerHoeBzRutdxaw/S72g=="], - "on-exit-leak-free": ["on-exit-leak-free@0.2.0", "", {}, "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg=="], "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], @@ -1327,25 +1186,17 @@ "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], - "parse-ms": ["parse-ms@2.1.0", "", {}, "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA=="], - - "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], + "parse5": ["parse5@8.0.1", "", { "dependencies": { "entities": "^8.0.0" } }, "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw=="], "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], - "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], - - "path-to-regexp": ["path-to-regexp@6.1.0", "", {}, "sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw=="], - - "path-to-regexp-updated": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], - "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="], + "pidtree": ["pidtree@0.6.1", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-e0F9AOF1JMrCfBsyJOwU9lNvQ0WtXTq0j/4jk0BQ5JSI9VAybPXmDpPRw/2FQ3e5d3ZFN1mLh7jW99m/jjaptw=="], "pify": ["pify@3.0.0", "", {}, "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg=="], @@ -1355,7 +1206,9 @@ "pino-std-serializers": ["pino-std-serializers@4.0.0", "", {}, "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q=="], - "pip-requirements-js": ["pip-requirements-js@1.0.2", "", { "dependencies": { "ohm-js": "^17.1.0" } }, "sha512-awqoNOSOl4Blu4E4Hzp7jL0g8WKEhCwO+s7C2ibtIW3CAJMwspgoTXd4vnHd21UmhdrsI44Pn8FFSuA8QKrzvg=="], + "playwright": ["playwright@1.61.0", "", { "dependencies": { "playwright-core": "1.61.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ=="], + + "playwright-core": ["playwright-core@1.61.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA=="], "pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="], @@ -1371,8 +1224,6 @@ "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], - "pretty-ms": ["pretty-ms@7.0.1", "", { "dependencies": { "parse-ms": "^2.1.0" } }, "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q=="], - "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], "process-warning": ["process-warning@1.0.0", "", {}, "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q=="], @@ -1393,15 +1244,13 @@ "query-string": ["query-string@7.1.3", "", { "dependencies": { "decode-uri-component": "^0.2.2", "filter-obj": "^1.1.0", "split-on-first": "^1.0.0", "strict-uri-encode": "^2.0.0" } }, "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg=="], - "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], - "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], "radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="], "react": ["react@19.2.6", "", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="], - "react-aria": ["react-aria@3.49.0", "", { "dependencies": { "@internationalized/date": "^3.12.2", "@internationalized/number": "^3.6.7", "@internationalized/string": "^3.2.9", "@react-types/shared": "^3.35.0", "@swc/helpers": "^0.5.0", "aria-hidden": "^1.2.3", "clsx": "^2.0.0", "react-stately": "3.47.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-4+oK9FwJQWYhyA5zLfj/feOGY0zZbkE1muoF4gyxMroHVypjcYaRSTlJwvxph2zIlxt757KX6xIK2wJ5Aw1Kog=="], + "react-aria": ["react-aria@3.50.0", "", { "dependencies": { "@internationalized/date": "^3.12.2", "@internationalized/number": "^3.6.7", "@internationalized/string": "^3.2.9", "@react-types/shared": "^3.36.0", "@swc/helpers": "^0.5.0", "aria-hidden": "^1.2.3", "clsx": "^2.0.0", "react-stately": "3.48.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-S0Os6QZk33fzUAKu1QLT9afoUaCBt1ZNdoiq0n2YMVgKIdNIQS8zxiZ8O9hYE6QyDkHKjD6q39LQZ+qaSAIgjw=="], "react-dom": ["react-dom@19.2.6", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="], @@ -1411,19 +1260,15 @@ "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="], - "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], - "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], "react-rewards": ["react-rewards@2.1.0", "", { "peerDependencies": { "react": ">=15", "react-dom": ">=15" } }, "sha512-99EgPVXWWA7qphBnFAGxEh74HJIkskChdxUuaiCuYjdsOSUmsIzB3xwaaXCHr6dr9l7locYFPFiqelZ5gCLpew=="], - "react-router": ["react-router@7.15.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-HW9vYwuM8f4yx66Izy8xfrzCM+SBJluoZcCbww9A1TySax11S5Vgw6fi3ZjMONw9J4gQwngL7PzkyIpJJpJ7RQ=="], - "react-smooth": ["react-smooth@4.0.4", "", { "dependencies": { "fast-equals": "^5.0.1", "prop-types": "^15.8.1", "react-transition-group": "^4.4.5" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q=="], - "react-stately": ["react-stately@3.47.0", "", { "dependencies": { "@internationalized/date": "^3.12.2", "@internationalized/number": "^3.6.7", "@internationalized/string": "^3.2.9", "@react-types/shared": "^3.35.0", "@swc/helpers": "^0.5.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-H3ar+SOWP920EbVg7qWfP3fZjZiwhlEJAEJQqjt+w8oKijCwFgr0+R4941PIHscOXRNRvEOjvWilitImC0DdBg=="], + "react-stately": ["react-stately@3.48.0", "", { "dependencies": { "@internationalized/date": "^3.12.2", "@internationalized/number": "^3.6.7", "@internationalized/string": "^3.2.9", "@react-types/shared": "^3.36.0", "@swc/helpers": "^0.5.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-ImicSAG+lTotAe5izcs1fz49Zk48w7pDusqYg04WaPhCoej8BJ24soMu3iLXIrsi273s4P1gZrYGrqReMfgEEA=="], "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], @@ -1449,19 +1294,11 @@ "require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="], - "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], - - "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], - "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], - "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], - "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], - "rollup": ["rollup@4.61.0", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.61.0", "@rollup/rollup-android-arm64": "4.61.0", "@rollup/rollup-darwin-arm64": "4.61.0", "@rollup/rollup-darwin-x64": "4.61.0", "@rollup/rollup-freebsd-arm64": "4.61.0", "@rollup/rollup-freebsd-x64": "4.61.0", "@rollup/rollup-linux-arm-gnueabihf": "4.61.0", "@rollup/rollup-linux-arm-musleabihf": "4.61.0", "@rollup/rollup-linux-arm64-gnu": "4.61.0", "@rollup/rollup-linux-arm64-musl": "4.61.0", "@rollup/rollup-linux-loong64-gnu": "4.61.0", "@rollup/rollup-linux-loong64-musl": "4.61.0", "@rollup/rollup-linux-ppc64-gnu": "4.61.0", "@rollup/rollup-linux-ppc64-musl": "4.61.0", "@rollup/rollup-linux-riscv64-gnu": "4.61.0", "@rollup/rollup-linux-riscv64-musl": "4.61.0", "@rollup/rollup-linux-s390x-gnu": "4.61.0", "@rollup/rollup-linux-x64-gnu": "4.61.0", "@rollup/rollup-linux-x64-musl": "4.61.0", "@rollup/rollup-openbsd-x64": "4.61.0", "@rollup/rollup-openharmony-arm64": "4.61.0", "@rollup/rollup-win32-arm64-msvc": "4.61.0", "@rollup/rollup-win32-ia32-msvc": "4.61.0", "@rollup/rollup-win32-x64-gnu": "4.61.0", "@rollup/rollup-win32-x64-msvc": "4.61.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-T9mWdbWfQtp0B5lv/HX+wrhYsmXRlcWnXXmJbXqKJhlRaoS6KMhq0gpyzW4UJfclcxrEdLnTgjT2NjruLONu0g=="], - - "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + "rolldown": ["rolldown@1.1.2", "", { "dependencies": { "@oxc-project/types": "=0.137.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.2", "@rolldown/binding-darwin-arm64": "1.1.2", "@rolldown/binding-darwin-x64": "1.1.2", "@rolldown/binding-freebsd-x64": "1.1.2", "@rolldown/binding-linux-arm-gnueabihf": "1.1.2", "@rolldown/binding-linux-arm64-gnu": "1.1.2", "@rolldown/binding-linux-arm64-musl": "1.1.2", "@rolldown/binding-linux-ppc64-gnu": "1.1.2", "@rolldown/binding-linux-s390x-gnu": "1.1.2", "@rolldown/binding-linux-x64-gnu": "1.1.2", "@rolldown/binding-linux-x64-musl": "1.1.2", "@rolldown/binding-openharmony-arm64": "1.1.2", "@rolldown/binding-wasm32-wasi": "1.1.2", "@rolldown/binding-win32-arm64-msvc": "1.1.2", "@rolldown/binding-win32-x64-msvc": "1.1.2" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-x0CrQQqCXWGeI8dTvFfN/Dnv3yMKT9hv5jFjlOreKAx9wqLq9wz7VvLLHyaAXC90/CpggTu9SisSbsJJTPSjNQ=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], @@ -1469,28 +1306,28 @@ "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], + "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "scrypt-js": ["scrypt-js@3.0.1", "", {}, "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA=="], - "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], - "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], - "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], "sha.js": ["sha.js@2.4.12", "", { "dependencies": { "inherits": "^2.0.4", "safe-buffer": "^5.2.1", "to-buffer": "^1.2.0" }, "bin": { "sha.js": "bin.js" } }, "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w=="], + "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - "signal-exit": ["signal-exit@4.0.2", "", {}, "sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q=="], + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="], - "smol-toml": ["smol-toml@1.5.2", "", {}, "sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ=="], - "socket.io-client": ["socket.io-client@4.8.3", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g=="], "socket.io-parser": ["socket.io-parser@4.2.6", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" } }, "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg=="], @@ -1527,20 +1364,20 @@ "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], + "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], + "superstruct": ["superstruct@1.0.4", "", {}, "sha512-7JpaAoX2NGyoFlI9NBh66BQXGONc+uE+MRS5i2iOBKuS4e+ccgMDjATgZldkah+33DakBxDHiss9kvUcGAO8UQ=="], - "tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="], + "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], + + "tabbable": ["tabbable@6.5.0", "", {}, "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA=="], "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], - "tar": ["tar@7.5.16", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w=="], - "thread-stream": ["thread-stream@0.15.2", "", { "dependencies": { "real-require": "^0.1.0" } }, "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA=="], - "time-span": ["time-span@4.0.0", "", { "dependencies": { "convert-hrtime": "^3.0.0" } }, "sha512-MyqZCTGLDZ77u4k+jqg4UlrzPTPZ49NDlaekU6uuFaJLzPIN1woaRXCbGeqOfxwc3Y37ZROGAJ614Rdv7Olt+g=="], - "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], @@ -1551,24 +1388,24 @@ "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + "tldts": ["tldts@7.4.4", "", { "dependencies": { "tldts-core": "^7.4.4" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-kFXFK7O4WPextIUAOk8qtnw9dxR9UIXP9CjuH1cTBVBZMDeQcUPgr/IazGiw1B0Yiw5L75gHLWeW4iD793r90g=="], + + "tldts-core": ["tldts-core@7.4.4", "", {}, "sha512-vwVLJVvvpslm7vqAH7+XNj/neA/Ynq7DT2EEcMuwc5YzN5XaMyRAqxwU+uX3azZ1FQtB2gvrvnLnAEkvYlVdfg=="], + "to-buffer": ["to-buffer@1.2.2", "", { "dependencies": { "isarray": "^2.0.5", "safe-buffer": "^5.2.1", "typed-array-buffer": "^1.0.3" } }, "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + "tough-cookie": ["tough-cookie@6.0.1", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw=="], + + "tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="], "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], - "ts-morph": ["ts-morph@12.0.0", "", { "dependencies": { "@ts-morph/common": "~0.11.0", "code-block-writer": "^10.1.1" } }, "sha512-VHC8XgU2fFW7yO1f/b3mxKDje1vmyzFXHWzOYmKEkCEwcLjDtbdLgBQviqj4ZwP4MJkQtRo6Ha2I29lq/B+VxA=="], - - "ts-toolbelt": ["ts-toolbelt@6.15.5", "", {}, "sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A=="], - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], - "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], @@ -1583,7 +1420,7 @@ "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="], - "undici": ["undici@5.28.4", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g=="], + "undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="], "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], @@ -1599,14 +1436,8 @@ "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], - "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - "unstorage": ["unstorage@1.17.5", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.10", "lru-cache": "^11.2.7", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg=="], - "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], - - "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], - "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], "use-indexeddb": ["use-indexeddb@2.0.2", "", { "peerDependencies": { "react": ">=17" } }, "sha512-pF1Gnrdg7wYvSKAXQvITP0KDVFnecjfckxLAtne6WYDJfbGaeOG7038sZjpR9SrcS9xrjKLJSjeakkYzHyhhEw=="], @@ -1635,21 +1466,25 @@ "viem": ["viem@2.46.1", "", { "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.2.3", "isows": "1.0.7", "ox": "0.12.1", "ws": "8.18.3" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-c5YPQR/VueqoPG09Tp1JBw2iItKVRGVI0YkWekquRDZw0ciNBhO3muu2QjO9xFelOXh18q3d/kLbW83B2Oxf0g=="], - "vite": ["vite@7.3.5", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww=="], + "vite": ["vite@8.1.0", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "~1.1.2", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q=="], "vitest": ["vitest@4.1.8", "", { "dependencies": { "@vitest/expect": "4.1.8", "@vitest/mocker": "4.1.8", "@vitest/pretty-format": "4.1.8", "@vitest/runner": "4.1.8", "@vitest/snapshot": "4.1.8", "@vitest/spy": "4.1.8", "@vitest/utils": "4.1.8", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.8", "@vitest/browser-preview": "4.1.8", "@vitest/browser-webdriverio": "4.1.8", "@vitest/coverage-istanbul": "4.1.8", "@vitest/coverage-v8": "4.1.8", "@vitest/ui": "4.1.8", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig=="], + "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], + "wagmi": ["wagmi@2.18.2", "", { "dependencies": { "@wagmi/connectors": "6.1.0", "@wagmi/core": "2.22.1", "use-sync-external-store": "1.4.0" }, "peerDependencies": { "@tanstack/react-query": ">=5.0.0", "react": ">=18", "typescript": ">=5.0.4", "viem": "2.x" }, "optionalPeers": ["typescript"] }, "sha512-9jFip+0ZfjMBxT72m02MZD2+VmQQ/UmqZhHl+98N9HEqXLn765fIu45QPV85DAnQqIHD81gvY3vTvfWt16A5yQ=="], "webextension-polyfill": ["webextension-polyfill@0.10.0", "", {}, "sha512-c5s35LgVa5tFaHhrZDnr3FpQpjj1BB+RXhLTYUxGqBVN460HkbM8TBtEqdXWbpTKfzwCcjAZVF7zXCYSKtcp9g=="], - "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + "webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="], - "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + "whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="], + + "whatwg-url": ["whatwg-url@16.0.1", "", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="], "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], - "which-typed-array": ["which-typed-array@1.1.21", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-zbRA8cVm6io/d5W8uIe2hblzN76/Wm3v/yiythQvr+dpBWeqhPSWIDNj4zOyHi4zKbMK6DN34Xsr9jPHJERAEw=="], + "which-typed-array": ["which-typed-array@1.1.22", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw=="], "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], @@ -1659,14 +1494,16 @@ "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], + + "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], + "xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="], "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], "y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="], - "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], - "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], "yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], @@ -1679,10 +1516,6 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], - "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - - "@babel/traverse/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "@base-org/account/@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="], "@base-org/account/clsx": ["clsx@1.2.1", "", {}, "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="], @@ -1707,10 +1540,6 @@ "@gemini-wallet/core/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], - "@mapbox/node-pre-gyp/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - - "@mapbox/node-pre-gyp/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], - "@metamask/eth-json-rpc-provider/@metamask/json-rpc-engine": ["@metamask/json-rpc-engine@7.3.3", "", { "dependencies": { "@metamask/rpc-errors": "^6.2.1", "@metamask/safe-event-emitter": "^3.0.0", "@metamask/utils": "^8.3.0" } }, "sha512-dwZPq8wx9yV3IX2caLi9q9xZBw2XeIoYqdyihDDDpuHVCEiqadJLwqM3zy+uwf6F1QYQ65A8aOMQg1Uw7LMLNg=="], "@metamask/eth-json-rpc-provider/@metamask/utils": ["@metamask/utils@5.0.2", "", { "dependencies": { "@ethereumjs/tx": "^4.1.2", "@types/debug": "^4.1.7", "debug": "^4.3.4", "semver": "^7.3.8", "superstruct": "^1.0.3" } }, "sha512-yfmE79bRQtnMzarnKfX7AEJBwFTxvTyw3nBQlu/5rmGXrjAeAMltoGxO62TFurxrQAFMNa/fEjIHNvungZp0+g=="], @@ -1727,8 +1556,6 @@ "@metamask/utils/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "@metamask/utils/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], - "@metamask/utils/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], "@reown/appkit/@walletconnect/types": ["@walletconnect/types@2.21.0", "", { "dependencies": { "@walletconnect/events": "1.0.1", "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "2.1.2", "events": "3.3.0" } }, "sha512-ll+9upzqt95ZBWcfkOszXZkfnpbJJ2CmxMfGgE5GmhdxxxCcO5bGhXkI+x8OpiS555RJ/v/sXJYMSOLkmu4fFw=="], @@ -1741,27 +1568,19 @@ "@reown/appkit-wallet/zod": ["zod@3.22.4", "", {}, "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], - "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.5", "", { "dependencies": { "@tybys/wasm-util": "^0.10.2" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q=="], - "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@ts-morph/common/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - - "@vercel/node/@types/node": ["@types/node@20.11.0", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ=="], - - "@vercel/python-analysis/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], - - "@vercel/python-analysis/zod": ["zod@3.22.4", "", {}, "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg=="], - - "@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + "@vanilla-extract/css/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "@wagmi/core/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], @@ -1775,7 +1594,7 @@ "@walletconnect/jsonrpc-ws-connection/ws": ["ws@7.5.11", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA=="], - "@walletconnect/keyvaluestorage/idb-keyval": ["idb-keyval@6.2.4", "", {}, "sha512-D/NzHWUmYJGXi++z67aMSrnisb9A3621CyRK5G89JyTlN13C8xf0g04DLxUKMufPem3e3L2JAXR6Z00OWy183Q=="], + "@walletconnect/keyvaluestorage/idb-keyval": ["idb-keyval@6.2.5", "", {}, "sha512-eKQkTnS0relYsSOYomx8ozIbmdsQCKUdhyuIaQ2DZgKuaxtyQQMkyD/wlnQN32pO3yutN1b1L8uqwcDKaJd7/Q=="], "@walletconnect/relay-auth/@noble/curves": ["@noble/curves@1.8.0", "", { "dependencies": { "@noble/hashes": "1.7.0" } }, "sha512-j84kjAbzEnQHaSIhRPUmB3/eVXu2k3dKPl2LOrR8fSOIL+89U+7lV117EWHtq/GHM3ReGHM46iRBdZfpc4HRUQ=="], @@ -1801,7 +1620,7 @@ "cbw-sdk/clsx": ["clsx@1.2.1", "", {}, "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="], - "cbw-sdk/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + "cbw-sdk/eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], "cbw-sdk/preact": ["preact@10.29.2", "", {}, "sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ=="], @@ -1811,19 +1630,13 @@ "cliui/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], - "cross-fetch/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - "eciesjs/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], - "edge-runtime/async-listen": ["async-listen@3.0.1", "", {}, "sha512-cWMaNwUJnf37C/S5TfCkk/15MwbPRwVYALA2jtjkbHjCmAPiDXyNJy2q3p1KAZzDLHAWyarUWSujUoHR4pEgrA=="], - - "edge-runtime/picocolors": ["picocolors@1.0.0", "", {}, "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="], - "elliptic/bn.js": ["bn.js@4.12.3", "", {}, "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g=="], "engine.io-client/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "engine.io-client/ws": ["ws@8.20.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w=="], + "engine.io-client/ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], "eth-block-tracker/@metamask/utils": ["@metamask/utils@5.0.2", "", { "dependencies": { "@ethereumjs/tx": "^4.1.2", "@types/debug": "^4.1.7", "debug": "^4.3.4", "semver": "^7.3.8", "superstruct": "^1.0.3" } }, "sha512-yfmE79bRQtnMzarnKfX7AEJBwFTxvTyw3nBQlu/5rmGXrjAeAMltoGxO62TFurxrQAFMNa/fEjIHNvungZp0+g=="], @@ -1837,27 +1650,25 @@ "ethereum-cryptography/@scure/bip39": ["@scure/bip39@1.3.0", "", { "dependencies": { "@noble/hashes": "~1.4.0", "@scure/base": "~1.1.6" } }, "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ=="], - "https-proxy-agent/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "json-rpc-engine/@metamask/safe-event-emitter": ["@metamask/safe-event-emitter@2.0.0", "", {}, "sha512-/kSXhY692qiV1MXu6EeOZvg5nECLclxNXcKCxJ3cXQgYuRymRHpdx/t7JXfsK+JLjwA1e1c1/SBrlQYpusC29Q=="], - "listr2/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + "listr2/eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], "log-update/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], - "micromark/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], + + "node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + "obj-multiplex/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "ox/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], - "path-scurry/lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="], - - "porto/idb-keyval": ["idb-keyval@6.2.4", "", {}, "sha512-D/NzHWUmYJGXi++z67aMSrnisb9A3621CyRK5G89JyTlN13C8xf0g04DLxUKMufPem3e3L2JAXR6Z00OWy183Q=="], + "porto/idb-keyval": ["idb-keyval@6.2.5", "", {}, "sha512-eKQkTnS0relYsSOYomx8ozIbmdsQCKUdhyuIaQ2DZgKuaxtyQQMkyD/wlnQN32pO3yutN1b1L8uqwcDKaJd7/Q=="], "porto/ox": ["ox@0.9.17", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.0.9", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-rKAnhzhRU3Xh3hiko+i1ZxywZ55eWQzeS/Q4HRKLx2PqfHOolisZHErSsJVipGlmQKHW5qwOED/GighEw9dbLg=="], @@ -1867,8 +1678,6 @@ "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], - "restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - "slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], "socket.io-client/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], @@ -1879,13 +1688,11 @@ "to-buffer/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], - "unstorage/lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="], - "valtio/use-sync-external-store": ["use-sync-external-store@1.2.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA=="], - "vitest/es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="], + "vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - "vitest/vite": ["vite@7.3.2", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg=="], + "vite/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], "wagmi/use-sync-external-store": ["use-sync-external-store@1.4.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw=="], @@ -1895,10 +1702,6 @@ "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - - "@babel/traverse/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "@base-org/account/ox/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], "@base-org/account/ox/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], @@ -1913,28 +1716,20 @@ "@metamask/eth-json-rpc-provider/@metamask/utils/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "@metamask/eth-json-rpc-provider/@metamask/utils/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], - "@metamask/json-rpc-engine/@metamask/rpc-errors/@metamask/utils": ["@metamask/utils@9.3.0", "", { "dependencies": { "@ethereumjs/tx": "^4.2.0", "@metamask/superstruct": "^3.1.0", "@noble/hashes": "^1.3.1", "@scure/base": "^1.1.3", "@types/debug": "^4.1.7", "debug": "^4.3.4", "pony-cause": "^2.1.10", "semver": "^7.5.4", "uuid": "^9.0.1" } }, "sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g=="], "@metamask/json-rpc-engine/@metamask/utils/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "@metamask/json-rpc-engine/@metamask/utils/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], - "@metamask/json-rpc-engine/@metamask/utils/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], "@metamask/json-rpc-middleware-stream/@metamask/utils/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "@metamask/json-rpc-middleware-stream/@metamask/utils/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], - "@metamask/json-rpc-middleware-stream/@metamask/utils/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], "@metamask/providers/@metamask/rpc-errors/@metamask/utils": ["@metamask/utils@9.3.0", "", { "dependencies": { "@ethereumjs/tx": "^4.2.0", "@metamask/superstruct": "^3.1.0", "@noble/hashes": "^1.3.1", "@scure/base": "^1.1.3", "@types/debug": "^4.1.7", "debug": "^4.3.4", "pony-cause": "^2.1.10", "semver": "^7.5.4", "uuid": "^9.0.1" } }, "sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g=="], "@metamask/providers/@metamask/utils/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "@metamask/providers/@metamask/utils/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], - "@metamask/providers/@metamask/utils/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], "@metamask/utils/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -1955,12 +1750,6 @@ "@reown/appkit/@walletconnect/universal-provider/@walletconnect/utils": ["@walletconnect/utils@2.21.0", "", { "dependencies": { "@noble/ciphers": "1.2.1", "@noble/curves": "1.8.1", "@noble/hashes": "1.7.1", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/relay-api": "1.0.11", "@walletconnect/relay-auth": "1.1.0", "@walletconnect/safe-json": "1.0.2", "@walletconnect/time": "1.0.2", "@walletconnect/types": "2.21.0", "@walletconnect/window-getters": "1.0.1", "@walletconnect/window-metadata": "1.0.1", "bs58": "6.0.0", "detect-browser": "5.3.0", "query-string": "7.1.3", "uint8arrays": "3.1.0", "viem": "2.23.2" } }, "sha512-zfHLiUoBrQ8rP57HTPXW7rQMnYxYI4gT9yTACxVW6LhIFROTF6/ytm5SKNoIvi4a5nX5dfXG4D9XwQUCu8Ilig=="], - "@ts-morph/common/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="], - - "@vercel/node/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], - - "@walletconnect/jsonrpc-http-connection/cross-fetch/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - "@walletconnect/utils/viem/@scure/bip32": ["@scure/bip32@1.6.2", "", { "dependencies": { "@noble/curves": "~1.8.1", "@noble/hashes": "~1.7.1", "@scure/base": "~1.2.2" } }, "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw=="], "@walletconnect/utils/viem/@scure/bip39": ["@scure/bip39@1.5.4", "", { "dependencies": { "@noble/hashes": "~1.7.1", "@scure/base": "~1.2.4" } }, "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA=="], @@ -1983,17 +1772,15 @@ "eth-block-tracker/@metamask/utils/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "eth-block-tracker/@metamask/utils/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], - "ethereum-cryptography/@scure/bip32/@scure/base": ["@scure/base@1.1.9", "", {}, "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg=="], "ethereum-cryptography/@scure/bip39/@scure/base": ["@scure/base@1.1.9", "", {}, "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg=="], - "https-proxy-agent/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "log-update/slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - "micromark/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], "obj-multiplex/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], @@ -2005,6 +1792,28 @@ "socket.io-parser/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "vite/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "vite/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "vite/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "vite/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "vite/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "vite/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "vite/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "vite/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "vite/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "vite/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "vite/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], @@ -2015,16 +1824,12 @@ "@metamask/eth-json-rpc-provider/@metamask/json-rpc-engine/@metamask/utils/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "@metamask/eth-json-rpc-provider/@metamask/json-rpc-engine/@metamask/utils/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], - "@metamask/eth-json-rpc-provider/@metamask/json-rpc-engine/@metamask/utils/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], "@metamask/eth-json-rpc-provider/@metamask/utils/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "@metamask/json-rpc-engine/@metamask/rpc-errors/@metamask/utils/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "@metamask/json-rpc-engine/@metamask/rpc-errors/@metamask/utils/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], - "@metamask/json-rpc-engine/@metamask/rpc-errors/@metamask/utils/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], "@metamask/json-rpc-engine/@metamask/utils/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -2033,8 +1838,6 @@ "@metamask/providers/@metamask/rpc-errors/@metamask/utils/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "@metamask/providers/@metamask/rpc-errors/@metamask/utils/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], - "@metamask/providers/@metamask/rpc-errors/@metamask/utils/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], "@metamask/providers/@metamask/utils/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -2069,8 +1872,6 @@ "@reown/appkit/@walletconnect/universal-provider/@walletconnect/utils/viem": ["viem@2.23.2", "", { "dependencies": { "@noble/curves": "1.8.1", "@noble/hashes": "1.7.1", "@scure/bip32": "1.6.2", "@scure/bip39": "1.5.4", "abitype": "1.0.8", "isows": "1.0.6", "ox": "0.6.7", "ws": "8.18.0" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-NVmW/E0c5crMOtbEAqMF0e3NmvQykFXhLOc/CkLIXOlzHSA6KXVz3CYVmaKqBF8/xtjsjHAGjdJN3Ru1kFJLaA=="], - "@ts-morph/common/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "@walletconnect/utils/viem/ox/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], "@walletconnect/utils/viem/ox/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], @@ -2087,8 +1888,6 @@ "@metamask/eth-json-rpc-provider/@metamask/json-rpc-engine/@metamask/rpc-errors/@metamask/utils/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "@metamask/eth-json-rpc-provider/@metamask/json-rpc-engine/@metamask/rpc-errors/@metamask/utils/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], - "@metamask/eth-json-rpc-provider/@metamask/json-rpc-engine/@metamask/rpc-errors/@metamask/utils/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], "@metamask/eth-json-rpc-provider/@metamask/json-rpc-engine/@metamask/utils/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], diff --git a/docs/ssr-measurement-baseline.md b/docs/ssr-measurement-baseline.md new file mode 100644 index 000000000..517ae4d40 --- /dev/null +++ b/docs/ssr-measurement-baseline.md @@ -0,0 +1,43 @@ +# SSR Measurement Baseline + +Use this before and after each SSR improvement commit so the result is comparable. + +## Workflow + +```bash +bun run build +bun run preview +``` + +In another terminal: + +```bash +bun run measure:ssr +``` + +If preview is running on another host or port: + +```bash +BASE_URL=http://127.0.0.1:3001 bun run measure:ssr +``` + +To measure only build artifacts and client chunks: + +```bash +SKIP_LIVE=1 bun run measure:ssr +``` + +## What It Measures + +- Static HTML artifacts for `/` and `/vaults`. +- Whether expected route text is present in generated HTML or live responses. +- Live preview response timings for `/`, `/vaults`, and one vault detail route. +- Aggregate `.next/static/chunks/*.js` size and the largest chunks. + +The live timings are local smoke numbers, not lab-grade performance scores. Use them to catch regressions and compare directionally between adjacent SSR commits. + +## `/vaults` Route Strategy + +`/vaults` is prerendered as static content with `revalidate = 3600`. The route hydrates the main public vault list query on the server so first paint uses the same vault list components as the hydrated app instead of a separate static approximation. Secondary yvUSD/yvBTC snapshot queries are client-side enrichment and should not hold the page behind the route loading skeleton. + +This improves visual parity, but it increases the HTML/RSC payload because the cached response includes dehydrated vault query data. Re-check `.next/server/app/vaults.html` gzip size when changing the hydrated query set. diff --git a/index.html b/index.html deleted file mode 100644 index 36f05393c..000000000 --- a/index.html +++ /dev/null @@ -1,100 +0,0 @@ -<!DOCTYPE html> -<html lang="en" class="duration-150 transition-colors"> - <head> - <meta charset="UTF-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <meta name="theme-color" content="#000000" /> - <meta name="description" content="The yield protocol for digital assets" /> - - <!-- Open Graph --> - <meta property="og:title" content="Yearn" /> - <meta - property="og:description" - content="The yield protocol for digital assets" - /> - <meta property="og:image" content="https://yearn.fi/apps/vaults-og.png" /> - <meta property="og:url" content="https://yearn.fi" /> - <meta property="og:type" content="website" /> - - <!-- Twitter --> - <meta name="twitter:card" content="summary_large_image" /> - <meta name="twitter:title" content="Yearn" /> - <meta - name="twitter:description" - content="The yield protocol for digital assets" - /> - <meta name="twitter:image" content="https://yearn.fi/apps/vaults-og.png" /> - - <link rel="icon" href="/favicon.ico" /> - <link rel="sitemap" type="application/xml" title="Sitemap" href="/sitemap.xml" /> - <link rel="alternate" type="text/plain" title="LLMs" href="/llms.txt" /> - <link rel="alternate" type="text/markdown" title="Yearn vault catalog markdown" href="/api/vaults/markdown" /> - <title>Yearn - - - -
- - - diff --git a/next-env.d.ts b/next-env.d.ts new file mode 100644 index 000000000..c4b7818fb --- /dev/null +++ b/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +import "./.next/dev/types/routes.d.ts"; + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/next.config.ts b/next.config.ts new file mode 100644 index 000000000..6f89f22e7 --- /dev/null +++ b/next.config.ts @@ -0,0 +1,74 @@ +import type { NextConfig } from 'next' + +const securityHeaders = [ + { + key: 'Strict-Transport-Security', + value: 'max-age=63072000; includeSubDomains; preload' + }, + { + key: 'X-Content-Type-Options', + value: 'nosniff' + }, + { + key: 'Referrer-Policy', + value: 'strict-origin-when-cross-origin' + }, + { + key: 'Content-Security-Policy-Report-Only', + value: + "frame-ancestors 'self'; report-uri https://o4510960324837376.ingest.us.sentry.io/api/4510960614375424/security/?sentry_key=6b1b2932f1532eff2227d01a122adbb4;" + } +] + +const nextConfig: NextConfig = { + reactStrictMode: true, + poweredByHeader: false, + turbopack: { + resolveAlias: { + '@safe-global/safe-apps-sdk': './node_modules/@safe-global/safe-apps-sdk/dist/esm' + } + }, + images: { + remotePatterns: [ + { protocol: 'https', hostname: 'assets.yearn.fi' }, + { protocol: 'https', hostname: 'cdn.jsdelivr.net' }, + { protocol: 'https', hostname: 'raw.githubusercontent.com' }, + { protocol: 'https', hostname: 'yearn.fi' }, + { protocol: 'https', hostname: 'og.yearn.fi' } + ] + }, + async headers() { + return [ + { + source: '/:path*', + headers: securityHeaders + } + ] + }, + async redirects() { + return [ + { source: '/v2', destination: '/vaults?type=lp', permanent: false }, + { source: '/v2/:path*', destination: '/vaults?type=lp', permanent: false }, + { source: '/v3', destination: '/vaults', permanent: false }, + { source: '/ybribe/:path*', destination: 'https://ybribe.yearn.fi/:path*', permanent: false }, + { source: '/ycrv/:path*', destination: 'https://ycrv.yearn.fi/:path*', permanent: false }, + { source: '/veyfi/:path*', destination: 'https://veyfi.yearn.fi/:path*', permanent: false }, + { source: '/twitter', destination: 'https://twitter.com/yearnfi', permanent: false }, + { source: '/telegram', destination: 'https://t.me/yearnfinance/', permanent: false }, + { source: '/medium', destination: 'https://medium.com/iearn', permanent: false }, + { source: '/governance', destination: 'https://gov.yearn.fi/', permanent: false }, + { source: '/snapshot', destination: 'https://snapshot.org/#/veyfi.eth', permanent: false }, + { source: '/github', destination: 'https://github.com/yearn/yearn.fi', permanent: false } + ] + }, + async rewrites() { + return [ + { + source: '/proxy/plausible/:path*', + destination: 'https://plausible.io/:path*' + } + ] + } +} + +export default nextConfig diff --git a/package.json b/package.json index 87b6ef504..32f615da1 100644 --- a/package.json +++ b/package.json @@ -2,17 +2,17 @@ "name": "yearnfi", "version": "0.3.7", "scripts": { - "dev": "bun scripts/dev.mjs", - "dev:client": "vite", - "dev:server": "bun --watch api/server.ts", + "dev": "next dev --hostname 127.0.0.1 --port 3000", + "dev:client": "next dev --hostname 127.0.0.1 --port 3000", "dev:ts": "tsc --watch", "tenderly": "bun scripts/tenderly.ts", - "build": "tsc && vite build", - "preview": "bun scripts/preview.mjs", - "preview:client": "vite preview", - "preview:server": "bun scripts/ensure-api-server.js", + "build": "next build", + "preview": "next start --hostname 127.0.0.1 --port 3000", + "preview:client": "next start --hostname 127.0.0.1 --port 3000", "lint": "biome check .", "lint:fix": "biome check --write .", + "measure:ssr": "bun scripts/measure-ssr-baseline.ts", + "check:client-server-boundary": "bun scripts/check-client-server-boundary.ts", "tslint": "tsc -p tsconfig.json --noEmit", "bump": "bump", "test": "vitest run", @@ -27,17 +27,16 @@ "@tanstack/react-query": "5.90.21", "@tanstack/react-virtual": "3.13.26", "@upstash/redis": "1.37.0", - "@vercel/firewall": "1.2.1", "cross-fetch": "4.1.0", "ethers": "5.7.2", "framer-motion": "12.34.0", "graphql": "16.12.0", + "next": "16.2.6", "react": "19.2.6", "react-dom": "19.2.6", "react-hot-toast": "2.6.0", "react-markdown": "10.1.0", "react-rewards": "2.1.0", - "react-router": "7.15.0", "recharts": "2.15.4", "use-indexeddb": "2.0.2", "vaul": "1.1.2", @@ -56,15 +55,14 @@ "@types/node": "25.3.0", "@types/react": "19.2.15", "@types/react-dom": "19.2.3", - "@vercel/node": "5.6.3", - "@vitejs/plugin-react": "5.1.4", "bump": "0.2.5", "husky": "9.1.7", + "jsdom": "29.1.1", "lint-staged": "16.2.7", + "playwright": "1.61.0", "postcss": "8.5.15", "tailwindcss": "4.1.18", "typescript": "5.9.3", - "vite": "7.3.5", "vitest": "4.1.8" } } diff --git a/plans/2026-04-06-withdraw-zap-usd-amounts.md b/plans/2026-04-06-withdraw-zap-usd-amounts.md index 1de0b2b9e..9738b5141 100644 --- a/plans/2026-04-06-withdraw-zap-usd-amounts.md +++ b/plans/2026-04-06-withdraw-zap-usd-amounts.md @@ -6,7 +6,7 @@ **Architecture:** Keep the quote math in the withdraw flow, where raw `bigint` quote values and token decimals already exist. Pass either raw amount metadata or preformatted USD strings into presentational components so `InputTokenAmount` and `WithdrawDetails` do not have to reverse-engineer values from compact strings like `16.1K`. Reuse existing shared formatting utilities (`formatCounterValue` / `formatAmount`) instead of introducing a new currency formatter. -**Tech Stack:** React 19, TypeScript, Vitest, Vite, Wagmi/Viem, shared formatting helpers in `src/components/shared/utils/format.ts` +**Tech Stack:** React 19, TypeScript, Vitest, Next.js, Wagmi/Viem, shared formatting helpers in `src/components/shared/utils/format.ts` --- diff --git a/proxy.ts b/proxy.ts new file mode 100644 index 000000000..ea4c5f3d8 --- /dev/null +++ b/proxy.ts @@ -0,0 +1,32 @@ +import { type NextRequest, NextResponse } from 'next/server' + +const TENDERLY_API_PREFIX = '/api/tenderly' + +export function shouldBlockTenderlyApiRequestInProduction({ + pathname, + vercelEnv +}: { + pathname: string + vercelEnv: string | undefined +}): boolean { + return ( + vercelEnv === 'production' && (pathname === TENDERLY_API_PREFIX || pathname.startsWith(`${TENDERLY_API_PREFIX}/`)) + ) +} + +export function proxy(request: NextRequest): Response { + if ( + shouldBlockTenderlyApiRequestInProduction({ + pathname: request.nextUrl.pathname, + vercelEnv: process.env.VERCEL_ENV + }) + ) { + return new Response(null, { status: 404 }) + } + + return NextResponse.next() +} + +export const config = { + matcher: '/api/tenderly/:path*' +} diff --git a/public/robots.txt b/public/robots.txt deleted file mode 100644 index 263ada8bd..000000000 --- a/public/robots.txt +++ /dev/null @@ -1,13 +0,0 @@ -User-agent: * -Allow: / -Allow: /llms.txt -Allow: /robots.txt -Allow: /sitemap.xml -Allow: /api/vaults/markdown -Allow: /api/vault/markdown -Disallow: /api/enso/ -Disallow: /api/holdings/ -Disallow: /api/optimization/ -Disallow: /api/tenderly/ - -Sitemap: https://yearn.fi/sitemap.xml diff --git a/scripts/api-runtime.mjs b/scripts/api-runtime.mjs deleted file mode 100644 index bde447947..000000000 --- a/scripts/api-runtime.mjs +++ /dev/null @@ -1,771 +0,0 @@ -import { spawnSync } from 'node:child_process' -import { createHash } from 'node:crypto' -import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs' -import { createServer } from 'node:net' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { cwd, env, exit, stdin as input, stdout as output } from 'node:process' -import { createInterface } from 'node:readline/promises' -import { loadEnv } from 'vite' - -const DEFAULT_API_PORT = 3001 -const API_HEALTHCHECK_PATH = '/api/enso/balances' -const API_HEALTHCHECK_EXPECTED_ERROR = 'Missing eoaAddress' -const API_HEALTHCHECK_TIMEOUT_MS = 500 -const LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '0.0.0.0', '::1', '[::1]']) -const API_RUNTIME_STATE_DIR = join(tmpdir(), 'yearn-api-runtime') -export const DEFAULT_API_ENV_CHANGE_PATHS = ['.env', '.env.local'] - -export const DEFAULT_API_CHANGE_PATHS = [ - 'api', - 'vite.config.ts', - 'scripts/ensure-api-server.js', - 'scripts/api-runtime.mjs', - 'package.json' -] - -function resolveRealPath(path) { - try { - return realpathSync(path) - } catch { - return path - } -} - -function readWorkspacePathForPid(pid) { - const procCwdPath = `/proc/${pid}/cwd` - if (existsSync(procCwdPath)) { - return resolveRealPath(procCwdPath) - } - - return runCommand('lsof', ['-a', '-p', String(pid), '-d', 'cwd', '-Fn']) - ?.stdout.split('\n') - .map((line) => line.trim()) - .find((line) => line.startsWith('n')) - ?.slice(1) -} - -export function resolveLauncherEnv( - mode, - { - envDir = cwd(), - shellEnv = Object.fromEntries(Object.entries(env).filter((entry) => typeof entry[1] === 'string')) - } = {} -) { - return { - ...loadEnv(mode, envDir, ''), - ...shellEnv - } -} - -function getModeEnvChangePaths(mode) { - return Array.from(new Set([...DEFAULT_API_ENV_CHANGE_PATHS, `.env.${mode}`, `.env.${mode}.local`])) -} - -export function getApiChangePathsForMode(mode) { - return [...DEFAULT_API_CHANGE_PATHS, ...getModeEnvChangePaths(mode)] -} - -function resolveApiRuntimeStatePath(workspacePath, port) { - const workspaceKey = createHash('sha1').update(workspacePath).digest('hex') - return join(API_RUNTIME_STATE_DIR, `${workspaceKey}-${port}.json`) -} - -function readApiRuntimeState(workspacePath, port) { - const statePath = resolveApiRuntimeStatePath(workspacePath, port) - - if (!existsSync(statePath)) { - return undefined - } - - try { - return JSON.parse(readFileSync(statePath, 'utf8')) - } catch { - return undefined - } -} - -function writeApiRuntimeState({ workspacePath, port, pid, mode, head, startedAtMs }) { - mkdirSync(API_RUNTIME_STATE_DIR, { recursive: true }) - writeFileSync( - resolveApiRuntimeStatePath(workspacePath, port), - JSON.stringify({ pid, mode, head, startedAtMs }), - 'utf8' - ) -} - -function getCurrentGitHead() { - const gitHeadResult = runCommand('git', ['rev-parse', 'HEAD']) - - if (!gitHeadResult || gitHeadResult.status !== 0) { - return undefined - } - - const gitHead = gitHeadResult.stdout.trim() - return gitHead || undefined -} - -function readProcessStartedAtMs(pid) { - const startedAtResult = runCommand('ps', ['-p', String(pid), '-o', 'lstart=']) - - if (!startedAtResult || startedAtResult.status !== 0) { - return undefined - } - - const startedAtParts = startedAtResult.stdout.trim().split(/\s+/) - if (startedAtParts.length < 5) { - return undefined - } - - const [, month, day, time, year] = startedAtParts - const startedAtMs = Date.parse(`${month} ${day} ${year} ${time}`) - - return Number.isNaN(startedAtMs) ? undefined : startedAtMs -} - -export function getEnvChangeEntriesSince(mode, startedAtMs, envDir = cwd()) { - const envPaths = getModeEnvChangePaths(mode) - - return envPaths - .map((relativePath) => ({ - relativePath, - absolutePath: join(envDir, relativePath) - })) - .filter(({ absolutePath }) => existsSync(absolutePath)) - .flatMap(({ relativePath, absolutePath }) => { - if (startedAtMs === undefined) { - return [`M ${relativePath} (could not verify against the running API process start time)`] - } - - return statSync(absolutePath).mtimeMs > startedAtMs - ? [`M ${relativePath} (newer than the running API process)`] - : [] - }) -} - -export function getRecordedApiRuntimeMismatchEntries({ workspacePath, port, pid, mode, processStartedAtMs }) { - const recordedRuntimeState = readApiRuntimeState(workspacePath, port) - - if (!recordedRuntimeState) { - return ['M could not verify the running API launch context for this workspace'] - } - - if (recordedRuntimeState.pid !== pid) { - return ['M the running API pid does not match the last launcher-managed API process for this workspace'] - } - - if (processStartedAtMs === undefined || recordedRuntimeState.startedAtMs !== processStartedAtMs) { - return ['M could not verify that the running API process matches the last launcher-managed start'] - } - - if (recordedRuntimeState.mode !== mode) { - return [`M the running API was started in ${recordedRuntimeState.mode} mode, not ${mode}`] - } - - return [] -} - -export function getRecordedApiCommittedChangeEntries({ - workspacePath, - port, - currentHead, - changePaths, - runCommandImpl = runCommand -}) { - const recordedRuntimeState = readApiRuntimeState(workspacePath, port) - - if (!recordedRuntimeState?.head || !currentHead) { - return ['M could not verify API-related commits since the running API started'] - } - - if (recordedRuntimeState.head === currentHead) { - return [] - } - - const diffResult = runCommandImpl('git', [ - 'diff', - '--name-status', - `${recordedRuntimeState.head}..${currentHead}`, - '--', - ...changePaths - ]) - - if (!diffResult || diffResult.status !== 0) { - return ['M could not verify API-related commits since the running API started'] - } - - return diffResult.stdout - .split('\n') - .map((line) => line.trim()) - .filter(Boolean) -} - -export async function resolveLocalApiRuntimeOwner({ - apiPort, - apiProxyTarget, - checkApiHealthImpl = checkApiHealth, - inspectPortOwnerImpl = inspectPortOwner, - readProcessStartedAtMsImpl = readProcessStartedAtMs, - timeoutMs = 5_000, - pollIntervalMs = 100 -}) { - const deadline = Date.now() + timeoutMs - - while (Date.now() <= deadline) { - const health = await checkApiHealthImpl(apiProxyTarget) - - if (health.ok) { - const portOwner = inspectPortOwnerImpl(apiPort) - if (portOwner?.pid) { - return { - pid: portOwner.pid, - startedAtMs: readProcessStartedAtMsImpl(portOwner.pid) - } - } - } - - await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)) - } - - return undefined -} - -function normalizePort(portValue, fallbackPort = DEFAULT_API_PORT) { - const port = Number(portValue) - return Number.isInteger(port) && port > 0 ? port : fallbackPort -} - -function isLoopbackHostname(hostname) { - return LOOPBACK_HOSTNAMES.has(hostname.trim().toLowerCase()) -} - -function getLocalApiProxyTarget(apiPort, apiProxyTarget) { - try { - const parsedTarget = new URL(apiProxyTarget) - if (parsedTarget.protocol !== 'http:' || !isLoopbackHostname(parsedTarget.hostname)) { - return `http://localhost:${apiPort}` - } - - parsedTarget.port = String(apiPort) - return parsedTarget.toString().replace(/\/$/, '') - } catch { - return `http://localhost:${apiPort}` - } -} - -export function resolveConfiguredApiRuntime(launcherEnv) { - const explicitTarget = launcherEnv.API_PROXY_TARGET?.trim() || launcherEnv.VITE_API_PROXY_TARGET?.trim() - - if (explicitTarget) { - try { - const parsedTarget = new URL(explicitTarget) - const inferredPort = normalizePort( - parsedTarget.port || (parsedTarget.protocol === 'https:' ? '443' : '80'), - DEFAULT_API_PORT - ) - - return { - apiPort: inferredPort, - apiProxyTarget: explicitTarget.replace(/\/$/, ''), - isLocalApiTarget: parsedTarget.protocol === 'http:' && isLoopbackHostname(parsedTarget.hostname) - } - } catch { - return { - apiPort: DEFAULT_API_PORT, - apiProxyTarget: `http://localhost:${DEFAULT_API_PORT}`, - isLocalApiTarget: true - } - } - } - - const configuredPort = normalizePort( - launcherEnv.API_PORT?.trim() || launcherEnv.VITE_API_PORT?.trim(), - DEFAULT_API_PORT - ) - - return { - apiPort: configuredPort, - apiProxyTarget: `http://localhost:${configuredPort}`, - isLocalApiTarget: true - } -} - -function describeCommandOwner(commandOwner) { - if (!commandOwner) { - return 'unknown process' - } - - const ownerWorkspace = commandOwner.workspacePath ? ` in ${commandOwner.workspacePath}` : '' - return `${commandOwner.command} (pid ${commandOwner.pid})${ownerWorkspace}` -} - -async function checkApiHealth(apiProxyTarget) { - const controller = new AbortController() - const timeoutId = setTimeout(() => controller.abort(), API_HEALTHCHECK_TIMEOUT_MS) - - try { - const response = await fetch(`${apiProxyTarget}${API_HEALTHCHECK_PATH}`, { signal: controller.signal }) - const payload = await response.json().catch(() => undefined) - return { - ok: response.status === 400 && payload?.error === API_HEALTHCHECK_EXPECTED_ERROR, - status: response.status - } - } catch (error) { - return { - ok: false, - error - } - } finally { - clearTimeout(timeoutId) - } -} - -function runCommand(command, args) { - const result = spawnSync(command, args, { - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'pipe'] - }) - - if (result.error) { - return undefined - } - - return result -} - -function inspectPortOwnerWithLsof(port) { - const inspectionResult = runCommand('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-Fpct']) - - if (!inspectionResult || inspectionResult.status !== 0) { - return undefined - } - - const lines = inspectionResult.stdout - .split('\n') - .map((line) => line.trim()) - .filter(Boolean) - const pid = Number(lines.find((line) => line.startsWith('p'))?.slice(1)) - const command = lines.find((line) => line.startsWith('c'))?.slice(1) - - if (!pid || !command) { - return undefined - } - - return { - pid, - command, - workspacePath: readWorkspacePathForPid(pid) - } -} - -function inspectPortOwnerWithSs(port) { - const inspectionResult = runCommand('bash', ['-lc', `ss -ltnp '( sport = :${port} )' 2>/dev/null`]) - - if (!inspectionResult || inspectionResult.status !== 0) { - return undefined - } - - const inspectionText = inspectionResult.stdout.trim() - const match = inspectionText.match(/users:\(\("([^"]+)",pid=(\d+)/) - - if (!match) { - return undefined - } - - const pid = Number(match[2]) - return { - pid, - command: match[1], - workspacePath: readWorkspacePathForPid(pid) - } -} - -function inspectPortOwner(port) { - return inspectPortOwnerWithLsof(port) || inspectPortOwnerWithSs(port) -} - -function getApiChangeEntries(changePaths) { - const gitStatus = runCommand('git', ['status', '--short', '--untracked-files=all', '--', ...changePaths]) - - if (!gitStatus || gitStatus.status !== 0) { - return [] - } - - return gitStatus.stdout - .split('\n') - .map((line) => line.trim()) - .filter(Boolean) -} - -export async function isPortAvailable(port) { - return new Promise((resolve) => { - const probe = createServer() - - probe.once('error', (error) => { - if (error && typeof error === 'object' && 'code' in error) { - resolve(!['EADDRINUSE', 'EACCES'].includes(error.code)) - return - } - - resolve(false) - }) - - probe.once('listening', () => { - probe.close(() => resolve(true)) - }) - - probe.listen({ port, exclusive: true }) - }) -} - -async function findNextAvailablePort(startPort) { - return (await isPortAvailable(startPort)) ? startPort : findNextAvailablePort(startPort + 1) -} - -export function resolveLauncherStrategy({ - defaultApiPort, - portAvailable, - healthyExistingApi, - ownerWorkspaceMatches, - hasApiChanges, - nextAvailablePort -}) { - if (portAvailable) { - return { - kind: 'start-new', - apiPort: defaultApiPort, - reuseExistingApi: false, - shouldPrompt: false - } - } - - if (healthyExistingApi && ownerWorkspaceMatches && !hasApiChanges) { - return { - kind: 'reuse-existing', - apiPort: defaultApiPort, - reuseExistingApi: true, - shouldPrompt: false - } - } - - return { - kind: 'prompt', - apiPort: nextAvailablePort, - recommendedPort: nextAvailablePort, - reuseExistingApi: false, - canReuseExistingApi: healthyExistingApi, - shouldPrompt: true - } -} - -async function promptForCustomPort(defaultPort) { - const rl = createInterface({ input, output }) - const answer = (await rl.question(`Custom API port [${defaultPort}]: `)).trim() - rl.close() - - const selectedPort = normalizePort(answer || String(defaultPort), 0) - - if (!selectedPort) { - output.write('Enter a valid positive port number.\n') - return promptForCustomPort(defaultPort) - } - - if (!(await isPortAvailable(selectedPort))) { - output.write(`Port ${selectedPort} is already in use.\n`) - return promptForCustomPort(defaultPort) - } - - return selectedPort -} - -async function promptForApiRuntime({ - defaultApiPort, - commandOwner, - healthyExistingApi, - apiChangeEntries, - recommendedPort, - canReuseExistingApi -}) { - output.write(`\nDefault API port ${defaultApiPort} is already in use.\n`) - output.write(`Owner: ${describeCommandOwner(commandOwner)}\n`) - output.write(`Health: ${healthyExistingApi ? 'Yearn API server detected' : 'Not the expected Yearn API response'}\n`) - - if (apiChangeEntries.length > 0) { - output.write(`API-related changes in this worktree:\n${apiChangeEntries.map((entry) => ` ${entry}`).join('\n')}\n`) - } - - const options = [ - { - key: '1', - label: `Start this workspace on ${recommendedPort} (Recommended)`, - value: { apiPort: recommendedPort, reuseExistingApi: false } - }, - canReuseExistingApi - ? { - key: '2', - label: `Reuse the existing API on ${defaultApiPort} and start only the client`, - value: { apiPort: defaultApiPort, reuseExistingApi: true } - } - : undefined, - { - key: canReuseExistingApi ? '3' : '2', - label: 'Choose a custom API port', - value: 'custom' - }, - { - key: canReuseExistingApi ? '4' : '3', - label: 'Cancel', - value: 'cancel' - } - ].filter(Boolean) - - output.write(`${options.map((option) => `${option.key}. ${option.label}`).join('\n')}\n`) - - const rl = createInterface({ input, output }) - const answer = (await rl.question('Choice [1]: ')).trim() || '1' - rl.close() - - const selectedOption = options.find((option) => option.key === answer) - - if (!selectedOption) { - output.write('Invalid choice.\n') - return promptForApiRuntime({ - defaultApiPort, - commandOwner, - healthyExistingApi, - apiChangeEntries, - recommendedPort, - canReuseExistingApi - }) - } - - if (selectedOption.value === 'cancel') { - exit(1) - } - - if (selectedOption.value === 'custom') { - const customPort = await promptForCustomPort(recommendedPort) - return { apiPort: customPort, reuseExistingApi: false } - } - - return selectedOption.value -} - -export function buildSessionEnv({ apiPort, apiProxyTarget, isLocalApiTarget, launcherEnv }) { - const sessionEnv = { - ...launcherEnv, - API_PROXY_TARGET: apiProxyTarget - } - - if (isLocalApiTarget) { - sessionEnv.API_PORT = String(apiPort) - } else { - delete sessionEnv.API_PORT - } - - return sessionEnv -} - -function killChild(child) { - try { - child.kill('SIGTERM') - } catch { - return undefined - } - - return undefined -} - -export async function runLauncherProcesses({ - apiPort, - apiProxyTarget, - isLocalApiTarget, - reuseExistingApi, - launcherEnv, - runtimeMetadata, - serverCommand, - clientCommand -}) { - const sessionEnv = buildSessionEnv({ - apiPort, - apiProxyTarget, - isLocalApiTarget, - launcherEnv - }) - const children = [ - reuseExistingApi - ? undefined - : (() => { - const serverChild = Bun.spawn(serverCommand, { - env: sessionEnv, - stdin: 'inherit', - stdout: 'inherit', - stderr: 'inherit' - }) - - if (runtimeMetadata) { - void resolveLocalApiRuntimeOwner({ apiPort, apiProxyTarget }).then((runtimeOwner) => { - if (!runtimeOwner) { - return - } - - writeApiRuntimeState({ - workspacePath: runtimeMetadata.workspacePath, - port: apiPort, - pid: runtimeOwner.pid, - mode: runtimeMetadata.mode, - head: runtimeMetadata.head, - startedAtMs: runtimeOwner.startedAtMs - }) - }) - } - - return serverChild - })(), - Bun.spawn(clientCommand, { - env: sessionEnv, - stdin: 'inherit', - stdout: 'inherit', - stderr: 'inherit' - }) - ].filter(Boolean) - - const stopChildren = () => children.map(killChild) - const handleInterrupt = () => { - stopChildren() - exit(0) - } - - process.once('SIGINT', handleInterrupt) - process.once('SIGTERM', handleInterrupt) - - const firstExit = await Promise.race(children.map((child) => child.exited.then((code) => ({ child, code })))) - children.filter((child) => child !== firstExit.child).map(killChild) - - exit(firstExit.code ?? 0) -} - -export async function chooseApiRuntime({ changePaths, mode = 'development' } = {}) { - const launcherEnv = resolveLauncherEnv(mode) - const configuredApiRuntime = resolveConfiguredApiRuntime(launcherEnv) - const { apiPort: defaultApiPort, apiProxyTarget, isLocalApiTarget } = configuredApiRuntime - const currentHead = getCurrentGitHead() - const trackedChangePaths = changePaths || getApiChangePathsForMode(mode) - - if (!isLocalApiTarget) { - return { - apiPort: defaultApiPort, - apiProxyTarget, - isLocalApiTarget, - reuseExistingApi: true, - launcherEnv - } - } - - const currentWorkspacePath = resolveRealPath(cwd()) - const portAvailable = await isPortAvailable(defaultApiPort) - const commandOwner = portAvailable ? undefined : inspectPortOwner(defaultApiPort) - const processStartedAtMs = commandOwner ? readProcessStartedAtMs(commandOwner.pid) : undefined - const ownerWorkspaceMatches = commandOwner?.workspacePath === currentWorkspacePath - const healthyExistingApi = portAvailable ? false : (await checkApiHealth(apiProxyTarget)).ok - const trackedApiChangeEntries = getApiChangeEntries(trackedChangePaths) - const runtimeEnvChangeEntries = ownerWorkspaceMatches ? getEnvChangeEntriesSince(mode, processStartedAtMs) : [] - const runtimeMismatchEntries = - ownerWorkspaceMatches && healthyExistingApi - ? getRecordedApiRuntimeMismatchEntries({ - workspacePath: currentWorkspacePath, - port: defaultApiPort, - pid: commandOwner.pid, - mode, - processStartedAtMs - }) - : [] - const committedApiChangeEntries = - ownerWorkspaceMatches && healthyExistingApi && runtimeMismatchEntries.length === 0 - ? getRecordedApiCommittedChangeEntries({ - workspacePath: currentWorkspacePath, - port: defaultApiPort, - currentHead, - changePaths: trackedChangePaths - }) - : [] - const apiChangeEntries = Array.from( - new Set([ - ...trackedApiChangeEntries, - ...runtimeEnvChangeEntries, - ...runtimeMismatchEntries, - ...committedApiChangeEntries - ]) - ) - const strategy = resolveLauncherStrategy({ - defaultApiPort, - portAvailable, - healthyExistingApi, - ownerWorkspaceMatches, - hasApiChanges: apiChangeEntries.length > 0, - nextAvailablePort: portAvailable ? defaultApiPort : await findNextAvailablePort(defaultApiPort + 1) - }) - - if (!strategy.shouldPrompt) { - return { - apiPort: strategy.apiPort, - apiProxyTarget: strategy.reuseExistingApi - ? apiProxyTarget - : getLocalApiProxyTarget(strategy.apiPort, apiProxyTarget), - isLocalApiTarget, - reuseExistingApi: strategy.reuseExistingApi, - launcherEnv, - runtimeMetadata: strategy.reuseExistingApi - ? undefined - : { - workspacePath: currentWorkspacePath, - mode, - head: currentHead - } - } - } - - if (!input.isTTY || !output.isTTY) { - output.write( - `API port ${defaultApiPort} is busy${apiChangeEntries.length > 0 ? ' and this worktree has API-related changes' : ''}. Using ${strategy.recommendedPort} instead.\n` - ) - - return { - apiPort: strategy.recommendedPort, - apiProxyTarget: getLocalApiProxyTarget(strategy.recommendedPort, apiProxyTarget), - isLocalApiTarget, - reuseExistingApi: false, - launcherEnv, - runtimeMetadata: { - workspacePath: currentWorkspacePath, - mode, - head: currentHead - } - } - } - - const selection = await promptForApiRuntime({ - defaultApiPort, - commandOwner, - healthyExistingApi, - apiChangeEntries, - recommendedPort: strategy.recommendedPort, - canReuseExistingApi: strategy.canReuseExistingApi - }) - - return { - ...selection, - apiProxyTarget: selection.reuseExistingApi - ? apiProxyTarget - : getLocalApiProxyTarget(selection.apiPort, apiProxyTarget), - isLocalApiTarget, - launcherEnv, - runtimeMetadata: selection.reuseExistingApi - ? undefined - : { - workspacePath: currentWorkspacePath, - mode, - head: currentHead - } - } -} diff --git a/scripts/api-runtime.test.ts b/scripts/api-runtime.test.ts deleted file mode 100644 index 9964ed9d7..000000000 --- a/scripts/api-runtime.test.ts +++ /dev/null @@ -1,332 +0,0 @@ -import { createHash } from 'node:crypto' -import { mkdirSync, mkdtempSync, rmSync, utimesSync, writeFileSync } from 'node:fs' -import { createServer } from 'node:net' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { afterEach, describe, expect, it } from 'vitest' -import { - buildSessionEnv, - DEFAULT_API_CHANGE_PATHS, - DEFAULT_API_ENV_CHANGE_PATHS, - getApiChangePathsForMode, - getEnvChangeEntriesSince, - getRecordedApiCommittedChangeEntries, - getRecordedApiRuntimeMismatchEntries, - isPortAvailable, - resolveConfiguredApiRuntime, - resolveLauncherEnv, - resolveLocalApiRuntimeOwner -} from './api-runtime.mjs' - -const openServers = new Set>() -const tempDirs = new Set() -const tempFiles = new Set() - -afterEach(async () => { - await Promise.all( - [...openServers].map( - (server) => - new Promise((resolve) => { - server.close(() => resolve(undefined)) - }) - ) - ) - openServers.clear() - tempDirs.forEach((dir) => { - rmSync(dir, { recursive: true, force: true }) - }) - tempDirs.clear() - tempFiles.forEach((file) => { - rmSync(file, { force: true }) - }) - tempFiles.clear() -}) - -describe('resolveConfiguredApiRuntime', () => { - it('treats non-loopback API proxy targets as external APIs', () => { - expect(resolveConfiguredApiRuntime({ API_PROXY_TARGET: 'https://staging.example.internal' })).toEqual({ - apiPort: 443, - apiProxyTarget: 'https://staging.example.internal', - isLocalApiTarget: false - }) - }) -}) - -describe('buildSessionEnv', () => { - it('preserves explicit external API proxy targets without forcing a local API port', () => { - expect( - buildSessionEnv({ - apiPort: 443, - apiProxyTarget: 'https://staging.example.internal', - isLocalApiTarget: false, - launcherEnv: { - API_PORT: '3001', - API_PROXY_TARGET: 'https://staging.example.internal' - } - }) - ).toEqual({ - API_PROXY_TARGET: 'https://staging.example.internal' - }) - }) -}) - -describe('resolveLauncherEnv', () => { - it('uses Vite env semantics, including mode-specific files and variable expansion', () => { - const envDir = mkdtempSync(join(tmpdir(), 'yearn-api-runtime-')) - tempDirs.add(envDir) - const apiPortPlaceholder = '${' + 'API_PORT}' - - writeFileSync(join(envDir, '.env'), `API_PORT=3001\nVITE_API_PROXY_TARGET=http://localhost:${apiPortPlaceholder}\n`) - writeFileSync(join(envDir, '.env.local'), 'VITE_API_PROXY_TARGET=http://localhost:3002\n') - writeFileSync(join(envDir, '.env.production'), 'API_PORT=3003\n') - writeFileSync( - join(envDir, '.env.production.local'), - `API_PORT=3007\nVITE_API_PROXY_TARGET=http://localhost:${apiPortPlaceholder}\n` - ) - - expect(resolveLauncherEnv('production', { envDir, shellEnv: {} }).VITE_API_PROXY_TARGET).toBe( - 'http://localhost:3007' - ) - }) -}) - -describe('getApiChangePathsForMode', () => { - it('includes only development env files for development launcher reuse detection', () => { - expect(getApiChangePathsForMode('development')).toEqual( - expect.arrayContaining([ - ...DEFAULT_API_CHANGE_PATHS, - ...DEFAULT_API_ENV_CHANGE_PATHS, - '.env.development', - '.env.development.local' - ]) - ) - expect(getApiChangePathsForMode('development')).not.toEqual( - expect.arrayContaining(['.env.production', '.env.production.local']) - ) - }) - - it('includes only production env files for production launcher reuse detection', () => { - expect(getApiChangePathsForMode('production')).toEqual( - expect.arrayContaining([ - ...DEFAULT_API_CHANGE_PATHS, - ...DEFAULT_API_ENV_CHANGE_PATHS, - '.env.production', - '.env.production.local' - ]) - ) - expect(getApiChangePathsForMode('production')).not.toEqual( - expect.arrayContaining(['.env.development', '.env.development.local']) - ) - }) -}) - -describe('getEnvChangeEntriesSince', () => { - it('flags loaded env files that are newer than the running API process', () => { - const envDir = mkdtempSync(join(tmpdir(), 'yearn-api-runtime-')) - tempDirs.add(envDir) - const processStartedAtMs = new Date('2026-04-10T17:00:00.000Z').getTime() - const olderEnvPath = join(envDir, '.env') - const newerEnvPath = join(envDir, '.env.development.local') - - writeFileSync(olderEnvPath, 'API_PORT=3001\n') - writeFileSync(newerEnvPath, 'API_PORT=3007\n') - utimesSync(olderEnvPath, new Date(processStartedAtMs - 60_000), new Date(processStartedAtMs - 60_000)) - utimesSync(newerEnvPath, new Date(processStartedAtMs + 60_000), new Date(processStartedAtMs + 60_000)) - - expect(getEnvChangeEntriesSince('development', processStartedAtMs, envDir)).toEqual([ - 'M .env.development.local (newer than the running API process)' - ]) - }) -}) - -describe('getRecordedApiRuntimeMismatchEntries', () => { - it('flags running APIs that were started in a different launcher mode', () => { - const workspacePath = mkdtempSync(join(tmpdir(), 'yearn-api-runtime-workspace-')) - tempDirs.add(workspacePath) - const port = 3001 - const statePath = join( - tmpdir(), - 'yearn-api-runtime', - `${createHash('sha1').update(workspacePath).digest('hex')}-${port}.json` - ) - tempFiles.add(statePath) - mkdirSync(join(tmpdir(), 'yearn-api-runtime'), { recursive: true }) - - writeFileSync( - statePath, - JSON.stringify({ - pid: 1234, - mode: 'production', - head: 'abc123', - startedAtMs: 1_710_000_000_000 - }) - ) - - expect( - getRecordedApiRuntimeMismatchEntries({ - workspacePath, - port, - pid: 1234, - mode: 'development', - processStartedAtMs: 1_710_000_000_000 - }) - ).toEqual(['M the running API was started in production mode, not development']) - }) - - it('does not treat a different git checkout as a runtime mismatch by itself', () => { - const workspacePath = mkdtempSync(join(tmpdir(), 'yearn-api-runtime-workspace-')) - tempDirs.add(workspacePath) - const port = 3001 - const statePath = join( - tmpdir(), - 'yearn-api-runtime', - `${createHash('sha1').update(workspacePath).digest('hex')}-${port}.json` - ) - tempFiles.add(statePath) - mkdirSync(join(tmpdir(), 'yearn-api-runtime'), { recursive: true }) - - writeFileSync( - statePath, - JSON.stringify({ - pid: 1234, - mode: 'development', - head: 'old-head', - startedAtMs: 1_710_000_000_000 - }) - ) - - expect( - getRecordedApiRuntimeMismatchEntries({ - workspacePath, - port, - pid: 1234, - mode: 'development', - processStartedAtMs: 1_710_000_000_000 - }) - ).toEqual([]) - }) -}) - -describe('getRecordedApiCommittedChangeEntries', () => { - it('ignores HEAD changes when API-tracked paths are unchanged', () => { - const workspacePath = mkdtempSync(join(tmpdir(), 'yearn-api-runtime-workspace-')) - tempDirs.add(workspacePath) - const port = 3001 - const statePath = join( - tmpdir(), - 'yearn-api-runtime', - `${createHash('sha1').update(workspacePath).digest('hex')}-${port}.json` - ) - tempFiles.add(statePath) - mkdirSync(join(tmpdir(), 'yearn-api-runtime'), { recursive: true }) - - writeFileSync( - statePath, - JSON.stringify({ - pid: 1234, - mode: 'development', - head: 'old-head', - startedAtMs: 1_710_000_000_000 - }) - ) - - expect( - getRecordedApiCommittedChangeEntries({ - workspacePath, - port, - currentHead: 'new-head', - changePaths: ['api', '.env.development'], - runCommandImpl: () => ({ - pid: 1, - status: 0, - stdout: '', - stderr: '', - output: ['', '', ''], - signal: null - }) - }) - ).toEqual([]) - }) - - it('flags committed API-path changes since the running API started', () => { - const workspacePath = mkdtempSync(join(tmpdir(), 'yearn-api-runtime-workspace-')) - tempDirs.add(workspacePath) - const port = 3001 - const statePath = join( - tmpdir(), - 'yearn-api-runtime', - `${createHash('sha1').update(workspacePath).digest('hex')}-${port}.json` - ) - tempFiles.add(statePath) - mkdirSync(join(tmpdir(), 'yearn-api-runtime'), { recursive: true }) - - writeFileSync( - statePath, - JSON.stringify({ - pid: 1234, - mode: 'development', - head: 'old-head', - startedAtMs: 1_710_000_000_000 - }) - ) - - expect( - getRecordedApiCommittedChangeEntries({ - workspacePath, - port, - currentHead: 'new-head', - changePaths: ['api', '.env.development'], - runCommandImpl: () => ({ - pid: 1, - status: 0, - stdout: 'M\tapi/server.ts\n', - stderr: '', - output: ['', '', ''], - signal: null - }) - }) - ).toEqual(['M\tapi/server.ts']) - }) -}) - -describe('resolveLocalApiRuntimeOwner', () => { - it('records the actual API listener pid instead of the wrapper pid', async () => { - const healthChecks = [ - { ok: false, error: new Error('not ready') }, - { ok: true, status: 400 } - ] - - await expect( - resolveLocalApiRuntimeOwner({ - apiPort: 3001, - apiProxyTarget: 'http://localhost:3001', - checkApiHealthImpl: async () => healthChecks.shift() || { ok: true, status: 400 }, - inspectPortOwnerImpl: () => ({ pid: 4321, command: 'bun', workspacePath: '/tmp/yearn-fi' }), - readProcessStartedAtMsImpl: () => 1_710_000_000_000, - pollIntervalMs: 0 - }) - ).resolves.toEqual({ - pid: 4321, - startedAtMs: 1_710_000_000_000 - }) - }) -}) - -describe('isPortAvailable', () => { - it('detects ports already claimed by another listener without relying on ss', async () => { - const server = createServer() - openServers.add(server) - - await new Promise((resolve, reject) => { - server.once('error', reject) - server.listen(0, () => resolve(undefined)) - }) - - const address = server.address() - if (!address || typeof address === 'string') { - throw new Error('Expected a TCP server address') - } - - expect(await isPortAvailable(address.port)).toBe(false) - }) -}) diff --git a/scripts/check-client-server-boundary.ts b/scripts/check-client-server-boundary.ts new file mode 100644 index 000000000..b1c57c7a7 --- /dev/null +++ b/scripts/check-client-server-boundary.ts @@ -0,0 +1,234 @@ +import fs from 'node:fs' +import path from 'node:path' +import * as ts from 'typescript' + +const PROJECT_ROOT = process.cwd() +const SOURCE_ROOTS = ['app', 'src', 'pages'] +const SOURCE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'] +const SERVER_ONLY_ROOTS = ['src/server', 'app/api'].map((root) => path.join(PROJECT_ROOT, root)) + +type TImportEdge = { + importer: string + specifier: string + resolvedPath: string +} + +type TViolation = { + clientRoot: string + serverPath: string + importSpecifier: string + chain: string[] +} + +const aliasPrefixes = [ + ['@/', 'src/'], + ['@shared/', 'src/components/shared/'], + ['@pages/', 'src/components/pages/'], + ['@components/', 'src/components/'], + ['@hooks/', 'src/hooks/'], + ['/src/', 'src/'], + ['src/', 'src/'] +] as const + +function toDisplayPath(filePath: string): string { + return path.relative(PROJECT_ROOT, filePath).split(path.sep).join('/') +} + +function isSourceFile(filePath: string): boolean { + return SOURCE_EXTENSIONS.some((extension) => filePath.endsWith(extension)) +} + +function isServerOnlyPath(filePath: string): boolean { + return SERVER_ONLY_ROOTS.some((root) => filePath === root || filePath.startsWith(`${root}${path.sep}`)) +} + +function collectSourceFiles(directory: string): string[] { + if (!fs.existsSync(directory)) return [] + + return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { + const entryPath = path.join(directory, entry.name) + + if (entry.isDirectory()) { + return collectSourceFiles(entryPath) + } + if (entry.isFile() && isSourceFile(entryPath)) { + return [entryPath] + } + return [] + }) +} + +function resolveCandidate(candidatePath: string): string | undefined { + if (fs.existsSync(candidatePath) && fs.statSync(candidatePath).isFile() && isSourceFile(candidatePath)) { + return candidatePath + } + + for (const extension of SOURCE_EXTENSIONS) { + const filePath = `${candidatePath}${extension}` + if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) { + return filePath + } + } + + if (fs.existsSync(candidatePath) && fs.statSync(candidatePath).isDirectory()) { + for (const extension of SOURCE_EXTENSIONS) { + const filePath = path.join(candidatePath, `index${extension}`) + if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) { + return filePath + } + } + } + + return undefined +} + +function resolveImport(importer: string, specifier: string): string | undefined { + if (specifier.startsWith('.')) { + return resolveCandidate(path.resolve(path.dirname(importer), specifier)) + } + + const matchingAlias = aliasPrefixes.find(([prefix]) => specifier.startsWith(prefix)) + if (!matchingAlias) return undefined + + const [prefix, target] = matchingAlias + return resolveCandidate(path.join(PROJECT_ROOT, target, specifier.slice(prefix.length))) +} + +function hasUseClientDirective(sourceFile: ts.SourceFile): boolean { + for (const statement of sourceFile.statements) { + if (ts.isExpressionStatement(statement) && ts.isStringLiteral(statement.expression)) { + if (statement.expression.text === 'use client') return true + continue + } + + return false + } + + return false +} + +function isTypeOnlyImport(node: ts.ImportDeclaration): boolean { + const importClause = node.importClause + if (!importClause) return false + if (importClause.isTypeOnly) return true + if (importClause.name) return false + if (!importClause.namedBindings || ts.isNamespaceImport(importClause.namedBindings)) return false + + return importClause.namedBindings.elements.every((element) => element.isTypeOnly) +} + +function isTypeOnlyExport(node: ts.ExportDeclaration): boolean { + if (node.isTypeOnly) return true + if (!node.exportClause || ts.isNamespaceExport(node.exportClause)) return false + + return node.exportClause.elements.every((element) => element.isTypeOnly) +} + +function collectImports(filePath: string, sourceFile: ts.SourceFile): TImportEdge[] { + const edges: TImportEdge[] = [] + + function addEdge(specifier: string): void { + const resolvedPath = resolveImport(filePath, specifier) + if (!resolvedPath) return + edges.push({ importer: filePath, specifier, resolvedPath }) + } + + function visit(node: ts.Node): void { + if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && !isTypeOnlyImport(node)) { + addEdge(node.moduleSpecifier.text) + return + } + + if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) { + if (!isTypeOnlyExport(node)) { + addEdge(node.moduleSpecifier.text) + } + return + } + + if (ts.isCallExpression(node)) { + const firstArgument = node.arguments[0] + const isDynamicImport = node.expression.kind === ts.SyntaxKind.ImportKeyword + const isRequire = ts.isIdentifier(node.expression) && node.expression.text === 'require' + + if ((isDynamicImport || isRequire) && firstArgument && ts.isStringLiteral(firstArgument)) { + addEdge(firstArgument.text) + } + } + + ts.forEachChild(node, visit) + } + + visit(sourceFile) + return edges +} + +const sourceFiles = SOURCE_ROOTS.flatMap((root) => collectSourceFiles(path.join(PROJECT_ROOT, root))).sort((a, b) => + a.localeCompare(b) +) + +const sourceFileByPath = new Map( + sourceFiles.map((filePath) => { + const content = fs.readFileSync(filePath, 'utf8') + return [filePath, ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true)] as const + }) +) + +const importsByPath = new Map( + Array.from(sourceFileByPath.entries()).map(([filePath, sourceFile]) => [ + filePath, + collectImports(filePath, sourceFile) + ]) +) + +const clientRoots = Array.from(sourceFileByPath.entries()) + .filter(([, sourceFile]) => hasUseClientDirective(sourceFile)) + .map(([filePath]) => filePath) + +const violations: TViolation[] = [] + +function walkClientGraph(clientRoot: string, filePath: string, chain: string[], visited: Set): void { + if (visited.has(filePath)) return + visited.add(filePath) + + for (const edge of importsByPath.get(filePath) ?? []) { + if (isServerOnlyPath(edge.resolvedPath)) { + violations.push({ + clientRoot, + serverPath: edge.resolvedPath, + importSpecifier: edge.specifier, + chain: [...chain, edge.resolvedPath] + }) + continue + } + + if (sourceFileByPath.has(edge.resolvedPath)) { + walkClientGraph(clientRoot, edge.resolvedPath, [...chain, edge.resolvedPath], visited) + } + } +} + +for (const clientRoot of clientRoots) { + walkClientGraph(clientRoot, clientRoot, [clientRoot], new Set()) +} + +if (violations.length > 0) { + console.error('Client/server boundary check failed.') + console.error( + 'A client component import graph reaches server-only code. Move the import behind an API route or a server component.' + ) + console.error('') + + violations.forEach((violation, index) => { + console.error(`${index + 1}. ${toDisplayPath(violation.clientRoot)} reaches ${toDisplayPath(violation.serverPath)}`) + console.error(` via import "${violation.importSpecifier}"`) + violation.chain.forEach((filePath, chainIndex) => { + console.error(` ${chainIndex === 0 ? 'root' : '->'} ${toDisplayPath(filePath)}`) + }) + console.error('') + }) + + process.exit(1) +} + +console.log(`client/server boundary check passed (${clientRoots.length} client roots scanned)`) diff --git a/scripts/dev.mjs b/scripts/dev.mjs deleted file mode 100644 index a71210aba..000000000 --- a/scripts/dev.mjs +++ /dev/null @@ -1,34 +0,0 @@ -import { exit, stdout as output } from 'node:process' -import { - buildSessionEnv, - chooseApiRuntime, - getApiChangePathsForMode, - resolveLauncherStrategy, - runLauncherProcesses -} from './api-runtime.mjs' - -const DEV_API_CHANGE_PATHS = [...getApiChangePathsForMode('development'), 'scripts/dev.mjs'] - -export const resolveDevLauncherStrategy = resolveLauncherStrategy - -async function main() { - const selection = await chooseApiRuntime({ changePaths: DEV_API_CHANGE_PATHS, mode: 'development' }) - const sessionEnv = buildSessionEnv(selection) - - output.write( - `${selection.reuseExistingApi ? 'Reusing' : 'Starting'} API ${selection.reuseExistingApi ? 'at' : 'on'} ${sessionEnv.API_PROXY_TARGET}\n` - ) - - await runLauncherProcesses({ - ...selection, - serverCommand: ['bun', 'run', 'dev:server'], - clientCommand: ['bun', 'run', 'dev:client'] - }) -} - -if (typeof Bun !== 'undefined' && import.meta.path === Bun.main) { - void main().catch((error) => { - console.error('Failed to start dev environment', error) - exit(1) - }) -} diff --git a/scripts/dev.test.ts b/scripts/dev.test.ts deleted file mode 100644 index d5d0e5a72..000000000 --- a/scripts/dev.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { resolveDevLauncherStrategy } from './dev.mjs' - -describe('resolveDevLauncherStrategy', () => { - it('starts a new API server on the default port when the port is free', () => { - expect( - resolveDevLauncherStrategy({ - defaultApiPort: 3001, - portAvailable: true, - healthyExistingApi: false, - ownerWorkspaceMatches: false, - hasApiChanges: false, - nextAvailablePort: 3002 - }) - ).toEqual({ - kind: 'start-new', - apiPort: 3001, - reuseExistingApi: false, - shouldPrompt: false - }) - }) - - it('reuses the existing API server when it belongs to this workspace and there are no API changes', () => { - expect( - resolveDevLauncherStrategy({ - defaultApiPort: 3001, - portAvailable: false, - healthyExistingApi: true, - ownerWorkspaceMatches: true, - hasApiChanges: false, - nextAvailablePort: 3002 - }) - ).toEqual({ - kind: 'reuse-existing', - apiPort: 3001, - reuseExistingApi: true, - shouldPrompt: false - }) - }) - - it('prompts for a new port when the default port is occupied by another workspace', () => { - expect( - resolveDevLauncherStrategy({ - defaultApiPort: 3001, - portAvailable: false, - healthyExistingApi: true, - ownerWorkspaceMatches: false, - hasApiChanges: false, - nextAvailablePort: 3002 - }) - ).toEqual({ - kind: 'prompt', - apiPort: 3002, - recommendedPort: 3002, - reuseExistingApi: false, - canReuseExistingApi: true, - shouldPrompt: true - }) - }) - - it('prompts for a new port when this workspace has API-related changes', () => { - expect( - resolveDevLauncherStrategy({ - defaultApiPort: 3001, - portAvailable: false, - healthyExistingApi: true, - ownerWorkspaceMatches: true, - hasApiChanges: true, - nextAvailablePort: 3002 - }) - ).toEqual({ - kind: 'prompt', - apiPort: 3002, - recommendedPort: 3002, - reuseExistingApi: false, - canReuseExistingApi: true, - shouldPrompt: true - }) - }) -}) diff --git a/scripts/ensure-api-server.js b/scripts/ensure-api-server.js deleted file mode 100644 index 94082de04..000000000 --- a/scripts/ensure-api-server.js +++ /dev/null @@ -1,74 +0,0 @@ -const DEFAULT_API_PORT = '3001' -const API_PROXY_TARGET = - process.env.API_PROXY_TARGET || - `http://localhost:${process.env.API_PORT || process.env.API_SERVER_PORT || DEFAULT_API_PORT}` -const API_PROXY_TARGET_DESCRIPTION = process.env.API_PROXY_TARGET?.trim() - ? 'the configured API proxy target' - : 'the default local API address' - -const API_HEALTHCHECK_PATH = process.env.API_HEALTHCHECK_PATH || '/api/enso/balances' -const API_HEALTHCHECK_EXPECTED_ERROR = 'Missing eoaAddress' -const API_HEALTHCHECK_TIMEOUT_MS = Number(process.env.API_HEALTHCHECK_TIMEOUT_MS || '500') - -function resolveApiPort() { - if (process.env.API_PORT) { - return process.env.API_PORT - } - - try { - const parsedTarget = new URL(API_PROXY_TARGET) - return parsedTarget.port || (parsedTarget.protocol === 'https:' ? '443' : '80') - } catch { - return '3001' - } -} - -async function checkApi() { - const controller = new AbortController() - const timeout = setTimeout(() => controller.abort(), API_HEALTHCHECK_TIMEOUT_MS) - - try { - const response = await fetch(`${API_PROXY_TARGET}${API_HEALTHCHECK_PATH}`, { signal: controller.signal }) - const data = await response.json().catch(() => null) - if (response.status === 400 && data?.error === API_HEALTHCHECK_EXPECTED_ERROR) { - return { ok: true } - } - - return { ok: false, reason: 'unexpected-response', status: response.status } - } catch (error) { - return { ok: false, reason: 'no-response', error } - } finally { - clearTimeout(timeout) - } -} - -;(async () => { - const health = await checkApi() - if (health.ok) { - console.log(`API already running at ${API_PROXY_TARGET_DESCRIPTION}`) - process.exit(0) - } - - if (health.reason === 'unexpected-response') { - console.error( - `${API_PROXY_TARGET_DESCRIPTION} is already in use, but it doesn't look like the Yearn API server. Stop that process or set API_PROXY_TARGET to a different value.` - ) - process.exit(1) - } - - const child = Bun.spawn(['bun', 'api/server.ts'], { - env: { - ...process.env, - API_PORT: resolveApiPort() - }, - stdin: 'inherit', - stdout: 'inherit', - stderr: 'inherit' - }) - - const exitCode = await child.exited - process.exit(exitCode) -})().catch((error) => { - console.error('Failed to start or detect API server', error) - process.exit(1) -}) diff --git a/scripts/measure-ssr-baseline.ts b/scripts/measure-ssr-baseline.ts new file mode 100644 index 000000000..8e1922290 --- /dev/null +++ b/scripts/measure-ssr-baseline.ts @@ -0,0 +1,308 @@ +import fs from 'node:fs' +import path from 'node:path' +import { gzipSync } from 'node:zlib' + +const PROJECT_ROOT = process.cwd() +const BUILD_DIR = path.join(PROJECT_ROOT, '.next') +const STATIC_CHUNKS_DIR = path.join(BUILD_DIR, 'static', 'chunks') +const BASE_URL = process.env.BASE_URL ?? 'http://127.0.0.1:3000' +const REPEAT_COUNT = Number(process.env.REPEAT ?? '3') +const REQUEST_TIMEOUT_MS = Number(process.env.REQUEST_TIMEOUT_MS ?? '15000') +const SKIP_LIVE = process.env.SKIP_LIVE === '1' + +type TRouteBaseline = { + label: string + path: string + artifact?: string + expectedText: string[] +} + +type TStaticArtifactResult = { + route: TRouteBaseline + exists: boolean + bytes: number + gzipBytes: number + scriptTags: number + expectedTextFound: boolean +} + +type TLiveAttempt = { + status: number + headersMs: number + bodyMs: number + bytes: number + cacheControl: string + nextCache: string + expectedTextFound: boolean +} + +type TLiveRouteResult = { + route: TRouteBaseline + attempts: TLiveAttempt[] + error?: string +} + +type TChunkMetric = { + file: string + bytes: number + gzipBytes: number +} + +const routes: TRouteBaseline[] = [ + { + label: 'landing', + path: '/', + artifact: '.next/server/app/index.html', + expectedText: ['Earn on your Crypto', 'Explore Vaults'] + }, + { + label: 'vaults', + path: '/vaults', + artifact: '.next/server/app/vaults.html', + expectedText: ['Vaults', 'yvUSD'] + }, + { + label: 'vault detail', + path: '/vaults/1/0x696d02Db93291651ED510704c9b286841d506987', + expectedText: ['yvUSD'] + } +] + +function formatBytes(bytes: number): string { + if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(2)} MB` + if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB` + return `${bytes} B` +} + +function formatMs(ms: number): string { + return `${ms.toFixed(1)} ms` +} + +function average(values: number[]): number { + if (values.length === 0) return 0 + return values.reduce((total, value) => total + value, 0) / values.length +} + +function max(values: number[]): number { + if (values.length === 0) return 0 + return Math.max(...values) +} + +function countMatches(content: string, pattern: RegExp): number { + return content.match(pattern)?.length ?? 0 +} + +function includesExpectedText(content: string, expectedText: string[]): boolean { + return expectedText.every((text) => content.includes(text)) +} + +function formatTable(rows: string[][]): string { + const columnWidths = + rows[0]?.map((_, columnIndex) => Math.max(...rows.map((row) => row[columnIndex]?.length ?? 0))) ?? [] + + return rows + .map((row) => row.map((cell, columnIndex) => cell.padEnd(columnWidths[columnIndex] ?? 0)).join(' ')) + .join('\n') +} + +function collectFiles(directory: string): string[] { + if (!fs.existsSync(directory)) return [] + + return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { + const entryPath = path.join(directory, entry.name) + + if (entry.isDirectory()) return collectFiles(entryPath) + if (entry.isFile()) return [entryPath] + return [] + }) +} + +function measureStaticArtifact(route: TRouteBaseline): TStaticArtifactResult { + const artifactPath = route.artifact ? path.join(PROJECT_ROOT, route.artifact) : undefined + const content = artifactPath && fs.existsSync(artifactPath) ? fs.readFileSync(artifactPath, 'utf8') : '' + const bytes = Buffer.byteLength(content) + + return { + route, + exists: Boolean(artifactPath && fs.existsSync(artifactPath)), + bytes, + gzipBytes: content ? gzipSync(content).byteLength : 0, + scriptTags: countMatches(content, / filePath.endsWith('.js')) + .map((filePath) => { + const buffer = fs.readFileSync(filePath) + + return { + file: path.relative(PROJECT_ROOT, filePath).split(path.sep).join('/'), + bytes: buffer.byteLength, + gzipBytes: gzipSync(buffer).byteLength + } + }) + .sort((left, right) => right.bytes - left.bytes) +} + +async function runSequential(tasks: Array<() => Promise>): Promise { + return tasks.reduce>(async (previousPromise, task) => { + const previous = await previousPromise + const next = await task() + return [...previous, next] + }, Promise.resolve([])) +} + +async function fetchRouteAttempt(route: TRouteBaseline): Promise { + const controller = new AbortController() + const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS) + const url = new URL(route.path, BASE_URL).toString() + const startedAt = performance.now() + + try { + const response = await fetch(url, { + headers: { + Accept: 'text/html' + }, + signal: controller.signal + }) + const headersAt = performance.now() + const html = await response.text() + const finishedAt = performance.now() + + return { + status: response.status, + headersMs: headersAt - startedAt, + bodyMs: finishedAt - startedAt, + bytes: Buffer.byteLength(html), + cacheControl: response.headers.get('cache-control') ?? '', + nextCache: response.headers.get('x-nextjs-cache') ?? '', + expectedTextFound: includesExpectedText(html, route.expectedText) + } + } finally { + clearTimeout(timeout) + } +} + +async function measureLiveRoute(route: TRouteBaseline): Promise { + try { + const attempts = await runSequential(Array.from({ length: REPEAT_COUNT }, () => () => fetchRouteAttempt(route))) + + return { route, attempts } + } catch (error) { + return { + route, + attempts: [], + error: error instanceof Error ? error.message : String(error) + } + } +} + +function printStaticArtifactResults(results: TStaticArtifactResult[]): void { + console.log('\nStatic HTML artifacts') + console.log( + formatTable([ + ['route', 'artifact', 'size', 'gzip', 'scripts', 'expected text'], + ...results.map((result) => [ + result.route.label, + result.route.artifact ?? '-', + result.exists ? formatBytes(result.bytes) : 'missing', + result.exists ? formatBytes(result.gzipBytes) : '-', + result.exists ? String(result.scriptTags) : '-', + result.exists ? (result.expectedTextFound ? 'yes' : 'no') : '-' + ]) + ]) + ) +} + +function printChunkResults(metrics: TChunkMetric[]): void { + const totalBytes = metrics.reduce((total, metric) => total + metric.bytes, 0) + const totalGzipBytes = metrics.reduce((total, metric) => total + metric.gzipBytes, 0) + + console.log('\nClient JS chunks') + console.log(`files: ${metrics.length}`) + console.log(`total: ${formatBytes(totalBytes)} (${formatBytes(totalGzipBytes)} gzip)`) + console.log( + formatTable([ + ['largest chunks', 'size', 'gzip'], + ...metrics.slice(0, 8).map((metric) => [metric.file, formatBytes(metric.bytes), formatBytes(metric.gzipBytes)]) + ]) + ) +} + +function summarizeAttempts(attempts: TLiveAttempt[]): { + status: string + headersMs: string + bodyMs: string + maxBodyMs: string + bytes: string + cache: string + expectedText: string +} { + const bodyMs = attempts.map((attempt) => attempt.bodyMs) + + return { + status: Array.from(new Set(attempts.map((attempt) => attempt.status))).join(', '), + headersMs: formatMs(average(attempts.map((attempt) => attempt.headersMs))), + bodyMs: formatMs(average(bodyMs)), + maxBodyMs: formatMs(max(bodyMs)), + bytes: formatBytes(Math.round(average(attempts.map((attempt) => attempt.bytes)))), + cache: + attempts[0]?.nextCache || attempts[0]?.cacheControl + ? `${attempts[0]?.nextCache || '-'} ${attempts[0]?.cacheControl || ''}`.trim() + : '-', + expectedText: attempts.every((attempt) => attempt.expectedTextFound) ? 'yes' : 'no' + } +} + +function printLiveResults(results: TLiveRouteResult[]): void { + console.log('\nLive preview responses') + console.log(`base URL: ${BASE_URL}`) + console.log(`repeat count: ${REPEAT_COUNT}`) + console.log( + formatTable([ + ['route', 'status', 'headers avg', 'body avg', 'body max', 'size', 'cache', 'expected text'], + ...results.map((result) => { + if (result.error) { + return [result.route.label, 'error', '-', '-', '-', '-', result.error, 'no'] + } + + const summary = summarizeAttempts(result.attempts) + return [ + result.route.label, + summary.status, + summary.headersMs, + summary.bodyMs, + summary.maxBodyMs, + summary.bytes, + summary.cache, + summary.expectedText + ] + }) + ]) + ) +} + +async function main(): Promise { + console.log('SSR baseline measurement') + console.log(`build dir: ${path.relative(PROJECT_ROOT, BUILD_DIR)}`) + + printStaticArtifactResults(routes.map(measureStaticArtifact)) + printChunkResults(getChunkMetrics()) + + if (SKIP_LIVE) { + console.log('\nLive preview responses skipped because SKIP_LIVE=1.') + return + } + + const liveResults = await runSequential(routes.map((route) => () => measureLiveRoute(route))) + printLiveResults(liveResults) + + if (liveResults.some((result) => result.error)) { + process.exitCode = 1 + } +} + +void main() diff --git a/scripts/preview.mjs b/scripts/preview.mjs deleted file mode 100644 index 37e14f378..000000000 --- a/scripts/preview.mjs +++ /dev/null @@ -1,34 +0,0 @@ -import { exit, stdout as output } from 'node:process' -import { - buildSessionEnv, - chooseApiRuntime, - getApiChangePathsForMode, - resolveLauncherStrategy, - runLauncherProcesses -} from './api-runtime.mjs' - -const PREVIEW_API_CHANGE_PATHS = [...getApiChangePathsForMode('production'), 'scripts/preview.mjs'] - -export const resolvePreviewLauncherStrategy = resolveLauncherStrategy - -async function main() { - const selection = await chooseApiRuntime({ changePaths: PREVIEW_API_CHANGE_PATHS, mode: 'production' }) - const sessionEnv = buildSessionEnv(selection) - - output.write( - `${selection.reuseExistingApi ? 'Reusing' : 'Starting'} API ${selection.reuseExistingApi ? 'at' : 'on'} ${sessionEnv.API_PROXY_TARGET}\n` - ) - - await runLauncherProcesses({ - ...selection, - serverCommand: ['bun', 'run', 'preview:server'], - clientCommand: ['bun', 'run', 'preview:client'] - }) -} - -if (typeof Bun !== 'undefined' && import.meta.path === Bun.main) { - void main().catch((error) => { - console.error('Failed to start preview environment', error) - exit(1) - }) -} diff --git a/scripts/preview.test.ts b/scripts/preview.test.ts deleted file mode 100644 index c33e61f04..000000000 --- a/scripts/preview.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { resolvePreviewLauncherStrategy } from './preview.mjs' - -describe('resolvePreviewLauncherStrategy', () => { - it('reuses the existing API server when it belongs to this workspace and there are no API changes', () => { - expect( - resolvePreviewLauncherStrategy({ - defaultApiPort: 3001, - portAvailable: false, - healthyExistingApi: true, - ownerWorkspaceMatches: true, - hasApiChanges: false, - nextAvailablePort: 3002 - }) - ).toEqual({ - kind: 'reuse-existing', - apiPort: 3001, - reuseExistingApi: true, - shouldPrompt: false - }) - }) - - it('prompts for a new port when this workspace has API-related changes', () => { - expect( - resolvePreviewLauncherStrategy({ - defaultApiPort: 3001, - portAvailable: false, - healthyExistingApi: true, - ownerWorkspaceMatches: true, - hasApiChanges: true, - nextAvailablePort: 3002 - }) - ).toEqual({ - kind: 'prompt', - apiPort: 3002, - recommendedPort: 3002, - reuseExistingApi: false, - canReuseExistingApi: true, - shouldPrompt: true - }) - }) -}) diff --git a/scripts/run-local.js b/scripts/run-local.js deleted file mode 100644 index 98cba8883..000000000 --- a/scripts/run-local.js +++ /dev/null @@ -1,195 +0,0 @@ -const net = require('net') - -const DEFAULT_CLIENT_PORT = 3000 -const DEFAULT_API_PORT = 3001 -const DEFAULT_HOST = '127.0.0.1' -const DEFAULT_API_PROXY_HOST = '127.0.0.1' -const MAX_PORT_ATTEMPTS = 100 - -function isPositiveInteger(value) { - const parsed = Number(value) - return Number.isInteger(parsed) && parsed > 0 -} - -function resolvePort(value, fallback) { - return isPositiveInteger(value) ? Number(value) : fallback -} - -function checkPortAvailability(port, host) { - return new Promise((resolve) => { - const server = net.createServer() - - server.once('error', () => { - resolve(false) - }) - - server.once('listening', () => { - server.close(() => resolve(true)) - }) - - server.listen(port, host) - }) -} - -function requestEphemeralPort(host, reservedPorts = new Set()) { - return new Promise((resolve, reject) => { - const server = net.createServer() - - server.once('error', reject) - - server.once('listening', () => { - const address = server.address() - const port = typeof address === 'object' && address ? address.port : undefined - - if (!port) { - server.close(() => reject(new Error('Failed to resolve an ephemeral port'))) - return - } - - server.close(async () => { - if (reservedPorts.has(port)) { - try { - const nextPort = await requestEphemeralPort(host, reservedPorts) - resolve(nextPort) - } catch (error) { - reject(error) - } - return - } - - resolve(port) - }) - }) - - server.listen(0, host) - }) -} - -async function findAvailablePort(startPort, host, attemptsLeft = MAX_PORT_ATTEMPTS, reservedPorts = new Set()) { - if (attemptsLeft <= 0) { - throw new Error(`Unable to find an available port starting at ${startPort}`) - } - - if (reservedPorts.has(startPort)) { - return findAvailablePort(startPort + 1, host, attemptsLeft - 1, reservedPorts) - } - - const isAvailable = await checkPortAvailability(startPort, host) - if (isAvailable) { - return startPort - } - - return findAvailablePort(startPort + 1, host, attemptsLeft - 1, reservedPorts) -} - -async function resolveOpenPort(startPort, host, reservedPorts = new Set()) { - try { - return await findAvailablePort(startPort, host, MAX_PORT_ATTEMPTS, reservedPorts) - } catch (_error) { - return requestEphemeralPort(host, reservedPorts) - } -} - -function createEnv({ apiPort, apiProxyHost, apiProxyTarget }) { - return { - ...process.env, - API_PROXY_HOST: apiProxyHost, - API_PROXY_TARGET: apiProxyTarget, - ...(apiPort ? { API_SERVER_PORT: String(apiPort) } : {}) - } -} - -function spawnChild(command, env) { - return Bun.spawn(command, { - env, - stdin: 'inherit', - stdout: 'inherit', - stderr: 'inherit' - }) -} - -function terminateChild(child) { - if (child.exitCode !== null) { - return - } - - child.kill('SIGTERM') -} - -async function waitForChildren(children) { - const exitCode = await Promise.race(children.map((child) => child.exited)) - children.forEach(terminateChild) - process.exit(exitCode) -} - -async function resolveStartupConfig() { - const host = process.env.HOST || DEFAULT_HOST - const apiProxyHost = process.env.API_PROXY_HOST || DEFAULT_API_PROXY_HOST - const explicitClientPort = process.env.PORT - const explicitApiPort = process.env.API_SERVER_PORT - const explicitApiProxyTarget = process.env.API_PROXY_TARGET?.trim() - - const clientPort = explicitClientPort ? resolvePort(explicitClientPort, DEFAULT_CLIENT_PORT) : undefined - const apiPort = explicitApiPort - ? resolvePort(explicitApiPort, DEFAULT_API_PORT) - : explicitApiProxyTarget - ? undefined - : await resolveOpenPort(DEFAULT_API_PORT, host, clientPort ? new Set([clientPort]) : new Set()) - - if (clientPort && apiPort && apiPort === clientPort) { - throw new Error(`Client and API ports must be different. Both resolved to ${clientPort}`) - } - - return { - apiPort, - apiProxyHost, - apiProxyTarget: explicitApiProxyTarget || `http://${apiProxyHost}:${apiPort}`, - clientPort, - host, - shouldStartLocalApi: Boolean(apiPort), - usesConfiguredApiProxyTarget: Boolean(explicitApiProxyTarget) - } -} - -async function main() { - const mode = process.argv[2] - if (!mode || !['dev', 'preview'].includes(mode)) { - throw new Error('Usage: bun scripts/run-local.js ') - } - - const { apiPort, apiProxyHost, apiProxyTarget, clientPort, host, shouldStartLocalApi, usesConfiguredApiProxyTarget } = - await resolveStartupConfig() - const env = { - ...createEnv({ apiPort, apiProxyHost, apiProxyTarget }), - HOST: host, - ...(clientPort ? { PORT: String(clientPort) } : {}) - } - - console.log( - `${mode} startup using ${usesConfiguredApiProxyTarget ? 'the configured API proxy target' : 'the local API server'}` - ) - - const clientPortArgs = clientPort ? ['--port', String(clientPort)] : [] - const clientCommand = - mode === 'dev' - ? ['bunx', 'vite', '--host', host, ...clientPortArgs] - : ['bunx', 'vite', 'preview', '--host', host, ...clientPortArgs] - - const apiChild = shouldStartLocalApi - ? spawnChild(mode === 'dev' ? ['bun', '--watch', 'api/server.ts'] : ['bun', 'api/server.ts'], env) - : undefined - const clientChild = spawnChild(clientCommand, env) - - ;['SIGINT', 'SIGTERM'].forEach((signal) => { - process.on(signal, () => { - ;[apiChild, clientChild].filter(Boolean).forEach(terminateChild) - }) - }) - - await waitForChildren([apiChild, clientChild].filter(Boolean)) -} - -main().catch((error) => { - console.error(error instanceof Error ? error.message : error) - process.exit(1) -}) diff --git a/scripts/tenderly-vnet-status.ts b/scripts/tenderly-vnet-status.ts index 332480921..a5660bc31 100644 --- a/scripts/tenderly-vnet-status.ts +++ b/scripts/tenderly-vnet-status.ts @@ -1,8 +1,7 @@ /// import { resolve as resolvePath } from 'node:path' -import { fileURLToPath } from 'node:url' -import { parseTenderlyServerChains, type TTenderlyServerChainConfig } from '../api/tenderly.helpers' +import { parseTenderlyServerChains, type TTenderlyServerChainConfig } from '../src/config/tenderlyServer' import { buildPredictablePublicRpcUrl, readEnvFile, sanitizeConsoleText } from './tenderly-vnet' type TParsedCliArgs = { @@ -662,7 +661,8 @@ function buildChainReport(params: { } env: Record }): TTenderlyStatusChainReport { - const explorerUri = params.env[`VITE_TENDERLY_EXPLORER_URI_FOR_${params.chain.canonicalChainId}`]?.trim() || undefined + const explorerUri = + params.env[`NEXT_PUBLIC_TENDERLY_EXPLORER_URI_FOR_${params.chain.canonicalChainId}`]?.trim() || undefined const forkNetworkId = parseNumericString(params.matchedVnet?.record.fork_config?.network_id) const forkBlockNumber = parseNumericString(params.matchedVnet?.record.fork_config?.block_number) @@ -911,8 +911,7 @@ async function main(): Promise { return } - const scriptDir = resolvePath(fileURLToPath(import.meta.url), '..') - const envFromFile = readEnvFile(resolvePath(scriptDir, '../.env')) + const envFromFile = readEnvFile(resolvePath(process.cwd(), '.env')) const env = { ...envFromFile, ...process.env } const report = await buildTenderlyStatusReport(flags, env) diff --git a/scripts/tenderly-vnet.test.ts b/scripts/tenderly-vnet.test.ts index faad488f3..938bacbd4 100644 --- a/scripts/tenderly-vnet.test.ts +++ b/scripts/tenderly-vnet.test.ts @@ -42,11 +42,13 @@ describe('tenderly-vnet console safety', () => { details: connectionDetails }) - expect(envFragment).toContain('VITE_TENDERLY_MODE=true') - expect(envFragment).toContain('VITE_TENDERLY_CHAIN_ID_FOR_1=694201') - expect(envFragment).toContain('VITE_TENDERLY_RPC_URI_FOR_1=https://predictable.public.rpc/stable-path') + expect(envFragment).toContain('NEXT_PUBLIC_TENDERLY_MODE=true') + expect(envFragment).toContain('NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_1=694201') + expect(envFragment).toContain('NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_1=https://predictable.public.rpc/stable-path') expect(envFragment).toContain('TENDERLY_ADMIN_RPC_URI_FOR_1=https://admin.rpc/secret-path') - expect(envFragment).toContain('VITE_TENDERLY_EXPLORER_URI_FOR_1=https://explorer.tenderly.public/virtual-mainnet') + expect(envFragment).toContain( + 'NEXT_PUBLIC_TENDERLY_EXPLORER_URI_FOR_1=https://explorer.tenderly.public/virtual-mainnet' + ) }) it('extracts explorer urls from explorer-specific response fields without mistaking public rpc urls for explorers', () => { diff --git a/scripts/tenderly-vnet.ts b/scripts/tenderly-vnet.ts index cb1b2132c..9734ebe2c 100644 --- a/scripts/tenderly-vnet.ts +++ b/scripts/tenderly-vnet.ts @@ -2,7 +2,6 @@ import { accessSync, chmodSync, constants, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs' import { dirname, resolve } from 'node:path' -import { fileURLToPath } from 'node:url' type TParsedCliArgs = { flags: Record @@ -406,11 +405,11 @@ export function buildTenderlyEnvFragment(params: { return [ '# Generated by scripts/tenderly-vnet.ts', - 'VITE_TENDERLY_MODE=true', - `VITE_TENDERLY_CHAIN_ID_FOR_${params.canonicalChainId}=${params.executionChainId}`, - `VITE_TENDERLY_RPC_URI_FOR_${params.canonicalChainId}=${publicRpc}`, + 'NEXT_PUBLIC_TENDERLY_MODE=true', + `NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_${params.canonicalChainId}=${params.executionChainId}`, + `NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_${params.canonicalChainId}=${publicRpc}`, `TENDERLY_ADMIN_RPC_URI_FOR_${params.canonicalChainId}=${params.details.adminRpc || ''}`, - `VITE_TENDERLY_EXPLORER_URI_FOR_${params.canonicalChainId}=${params.details.explorerUri || ''}` + `NEXT_PUBLIC_TENDERLY_EXPLORER_URI_FOR_${params.canonicalChainId}=${params.details.explorerUri || ''}` ].join('\n') } @@ -538,8 +537,7 @@ async function main(): Promise { return } - const scriptDir = resolve(fileURLToPath(import.meta.url), '..') - const envFromFile = readEnvFile(resolve(scriptDir, '../.env')) + const envFromFile = readEnvFile(resolve(process.cwd(), '.env')) const env = { ...envFromFile, ...process.env } const { apiKey, accountSlug, projectSlug, profile } = resolveTenderlyCredentials(flags, env) const rpcName = resolveRpcName(flags, env, profile) @@ -629,7 +627,10 @@ async function main(): Promise { }) } -if (fileURLToPath(import.meta.url) === process.argv[1]) { +if ( + process.argv[1]?.endsWith('/scripts/tenderly-vnet.ts') || + process.argv[1]?.endsWith('\\scripts\\tenderly-vnet.ts') +) { void main().catch((error: unknown) => { const message = error instanceof Error ? sanitizeConsoleText(error.message) : 'Tenderly VNet bootstrap failed' console.error(message) diff --git a/scripts/tenderly.ts b/scripts/tenderly.ts index 750c8c50e..e2ecc02db 100644 --- a/scripts/tenderly.ts +++ b/scripts/tenderly.ts @@ -1,6 +1,5 @@ /// -import { fileURLToPath } from 'node:url' import { getAddress } from 'viem' type TParsedCliArgs = { @@ -343,7 +342,7 @@ async function main(): Promise { await runCommand(parsedArgs) } -if (process.argv[1] === fileURLToPath(import.meta.url)) { +if (process.argv[1]?.endsWith('/scripts/tenderly.ts') || process.argv[1]?.endsWith('\\scripts\\tenderly.ts')) { void main().catch((error: unknown) => { const message = error instanceof Error ? error.message : String(error) console.error(message) diff --git a/src/App.tsx b/src/App.tsx index 7777caa36..49fad684c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,187 +1,15 @@ -import { IframeAutoConnect } from '@components/IframeAutoConnect' -import { ScrollToTopButton } from '@pages/vaults/components/detail/ScrollToTopButton' -import { AppSettingsContextApp } from '@pages/vaults/contexts/useAppSettings' -import { EnsoStatusProvider } from '@pages/vaults/contexts/useEnsoStatus' -import { RainbowKitProvider } from '@rainbow-me/rainbowkit' import AppHeader from '@shared/components/Header' -import { JsonLd } from '@shared/components/JsonLd' -import { Meta } from '@shared/components/Meta' -import { WithFonts } from '@shared/components/WithFonts' -import { ChartStyleContextApp } from '@shared/contexts/useChartStyle' -import { IndexedDB } from '@shared/contexts/useIndexedDB' -import { WithNotifications } from '@shared/contexts/useNotifications' -import { WithNotificationsActions } from '@shared/contexts/useNotificationsActions' -import { TenderlyPanelProvider } from '@shared/contexts/useTenderlyPanel' -import { WalletContextApp } from '@shared/contexts/useWallet' -import { Web3ContextApp } from '@shared/contexts/useWeb3' -import { YearnContextApp } from '@shared/contexts/useYearn' -import { WithTokenList } from '@shared/contexts/WithTokenList' -import { useCurrentApp } from '@shared/hooks/useCurrentApp' -import { IconAlertCritical } from '@shared/icons/IconAlertCritical' -import { IconAlertError } from '@shared/icons/IconAlertError' -import { IconCheckmark } from '@shared/icons/IconCheckmark' -import { cl } from '@shared/utils' -import { isIframe } from '@shared/utils/helpers' -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import type { ReactElement } from 'react' -import { useLayoutEffect } from 'react' -import { Toaster } from 'react-hot-toast' -import { useLocation } from 'react-router' -import { WagmiProvider } from 'wagmi' -import { TenderlyControlPanel } from '@/components/shared/components/TenderlyControlPanel' -import { wagmiConfig } from '@/config/wagmi' -import { ChainsProvider } from '@/context/ChainsProvider' -import '@hooks/usePlausible' -import { Routes } from './routes' - -const queryClient = new QueryClient() - -function WithLayout(): ReactElement { - return ( - <> -
- -
-
-
- -
-
- - - ) -} - -function App(): ReactElement { - const location = useLocation() - const { manifest } = useCurrentApp() - - // Scroll to top on route change - useLayoutEffect(() => { - window.scrollTo(0, 0) - }, [location.pathname]) - - // Determine dynamic meta for vault detail pages - const asPath = location.pathname - - // Get most basic og and uri info - const ogBaseUrl = 'https://og.yearn.fi' - const isVaultDetailPage = asPath.startsWith('/vaults/') && asPath.split('/').length === 4 - const [, , chainID, address] = isVaultDetailPage ? asPath.split('/') : [] - const ogUrl = isVaultDetailPage - ? `${ogBaseUrl}/api/og/yearn/vault/${chainID}/${address}` - : manifest.og || 'https://yearn.fi/apps/vaults-og.png' - const pageUri = isVaultDetailPage ? `https://yearn.fi${asPath}` : manifest.uri || 'https://yearn.fi' +import type { ReactElement, ReactNode } from 'react' +import { AppFrame } from '@/AppFrame' +import { AppProviders } from '@/AppProviders' +function App({ children }: { children: ReactNode }): ReactElement { return ( - <> - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - , - style: { - backgroundColor: '#C73203', - color: 'white' - } - }, - success: { - icon: , - style: { - backgroundColor: '#00796D', - color: 'white' - } - }, - icon: , - style: { - backgroundColor: '#0657F9', - color: 'white' - } - }} - position={'bottom-right'} - containerStyle={{ maxWidth: 'calc(100vw - 32px)', width: '100%' }} - /> -
-
- +
+ + }>{children} + +
) } diff --git a/src/AppClientEffects.tsx b/src/AppClientEffects.tsx new file mode 100644 index 000000000..e087c64a1 --- /dev/null +++ b/src/AppClientEffects.tsx @@ -0,0 +1,24 @@ +'use client' + +import { usePathname } from 'next/navigation' +import { useEffect, useLayoutEffect } from 'react' +import { initializePlausible } from '@/hooks/usePlausible' +import { disableServiceWorkerDev } from '@/utils/disableServiceWorkerDev' + +export function AppClientEffects(): null { + const pathname = usePathname() + + useLayoutEffect(() => { + window.scrollTo(0, 0) + }, [pathname]) + + useEffect(() => { + initializePlausible() + + if (process.env.NODE_ENV === 'development') { + void disableServiceWorkerDev() + } + }, []) + + return null +} diff --git a/src/AppFrame.tsx b/src/AppFrame.tsx new file mode 100644 index 000000000..36f42051b --- /dev/null +++ b/src/AppFrame.tsx @@ -0,0 +1,20 @@ +import { ScrollToTopButton } from '@pages/vaults/components/detail/ScrollToTopButton' +import { cl } from '@shared/utils/cl' +import type { ReactElement, ReactNode } from 'react' + +type TAppFrameProps = { + children: ReactNode + header: ReactElement +} + +export function AppFrame({ children, header }: TAppFrameProps): ReactElement { + return ( + <> +
{header}
+
+
{children}
+
+ + + ) +} diff --git a/src/AppProviders.tsx b/src/AppProviders.tsx new file mode 100644 index 000000000..8c7b9f5b6 --- /dev/null +++ b/src/AppProviders.tsx @@ -0,0 +1,122 @@ +'use client' + +import { IframeAutoConnect } from '@components/IframeAutoConnect' +import { AppSettingsContextApp } from '@pages/vaults/contexts/useAppSettings' +import { EnsoStatusProvider } from '@pages/vaults/contexts/useEnsoStatus' +import { RainbowKitProvider } from '@rainbow-me/rainbowkit' +import { ChartStyleContextApp } from '@shared/contexts/useChartStyle' +import { IndexedDB } from '@shared/contexts/useIndexedDB' +import { WithNotifications } from '@shared/contexts/useNotifications' +import { WithNotificationsActions } from '@shared/contexts/useNotificationsActions' +import { TenderlyPanelProvider } from '@shared/contexts/useTenderlyPanel' +import { WalletContextApp } from '@shared/contexts/useWallet' +import { Web3ContextApp } from '@shared/contexts/useWeb3' +import { YearnContextApp } from '@shared/contexts/useYearn' +import { WithTokenList } from '@shared/contexts/WithTokenList' +import { IconAlertCritical } from '@shared/icons/IconAlertCritical' +import { IconAlertError } from '@shared/icons/IconAlertError' +import { IconCheckmark } from '@shared/icons/IconCheckmark' +import { isIframe } from '@shared/utils/helpers' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { usePathname } from 'next/navigation' +import type { ReactElement, ReactNode } from 'react' +import { useState } from 'react' +import { Toaster } from 'react-hot-toast' +import { WagmiProvider } from 'wagmi' +import { AppClientEffects } from '@/AppClientEffects' +import { shouldLoadAppTokenLists } from '@/appRouteDataLoading' +import { TenderlyControlPanel } from '@/components/shared/components/TenderlyControlPanel' +import { wagmiConfig } from '@/config/wagmi' +import { ChainsProvider } from '@/context/ChainsProvider' + +const bigintPrototype = BigInt.prototype as unknown as { toJSON?: () => string } +if (!bigintPrototype.toJSON) { + bigintPrototype.toJSON = function toJSON() { + return this.toString() + } +} + +const appTokenLists = [ + 'https://cdn.jsdelivr.net/gh/yearn/tokenLists@main/lists/yearn.json', + 'https://cdn.jsdelivr.net/gh/yearn/tokenLists@main/lists/popular.json' +] + +function TokenListGate({ children }: { children: ReactElement }): ReactElement { + const pathname = usePathname() || '/' + const shouldLoadTokenLists = shouldLoadAppTokenLists(pathname) + + return ( + + {children} + + ) +} + +export function AppProviders({ children }: { children: ReactNode }): ReactElement { + const [queryClient] = useState(() => new QueryClient()) + + return ( + + + + + + + + + + + + + + + + + + {children} + + + + + + + + + + + + + + + + + , + style: { + backgroundColor: '#C73203', + color: 'white' + } + }, + success: { + icon: , + style: { + backgroundColor: '#00796D', + color: 'white' + } + }, + icon: , + style: { + backgroundColor: '#0657F9', + color: 'white' + } + }} + position={'bottom-right'} + containerStyle={{ maxWidth: 'calc(100vw - 32px)', width: '100%' }} + /> + + ) +} diff --git a/src/AppPublicProviders.tsx b/src/AppPublicProviders.tsx new file mode 100644 index 000000000..e82a5396d --- /dev/null +++ b/src/AppPublicProviders.tsx @@ -0,0 +1,28 @@ +'use client' + +import { YearnContextApp } from '@shared/contexts/useYearn' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import type { ReactElement, ReactNode } from 'react' +import { useState } from 'react' +import { AppClientEffects } from '@/AppClientEffects' + +function AppPublicProviderContent({ children }: { children: ReactNode }): ReactElement { + return ( + <> + + {children} + + ) +} + +export function AppPublicProviders({ children }: { children: ReactNode }): ReactElement { + const [queryClient] = useState(() => new QueryClient()) + + return ( + + + {children} + + + ) +} diff --git a/src/PublicApp.tsx b/src/PublicApp.tsx new file mode 100644 index 000000000..7c96d0307 --- /dev/null +++ b/src/PublicApp.tsx @@ -0,0 +1,16 @@ +import { PublicHeader } from '@shared/components/PublicHeader' +import type { ReactElement, ReactNode } from 'react' +import { AppFrame } from '@/AppFrame' +import { AppPublicProviders } from '@/AppPublicProviders' + +function PublicApp({ children }: { children: ReactNode }): ReactElement { + return ( +
+ + }>{children} + +
+ ) +} + +export default PublicApp diff --git a/src/appRouteDataLoading.test.ts b/src/appRouteDataLoading.test.ts new file mode 100644 index 000000000..215a9595a --- /dev/null +++ b/src/appRouteDataLoading.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from 'vitest' +import { isVaultDetailPathname, isVaultsListPathname, shouldLoadAppTokenLists } from '@/appRouteDataLoading' + +describe('app route data loading', () => { + it('identifies vault list routes', () => { + expect(isVaultsListPathname('/vaults')).toBe(true) + expect(isVaultsListPathname('/vaults?type=v3')).toBe(true) + expect(isVaultsListPathname('/v3')).toBe(true) + expect(isVaultsListPathname('/portfolio')).toBe(false) + }) + + it('identifies vault detail routes', () => { + expect(isVaultDetailPathname('/vaults/1/0x0000000000000000000000000000000000000001')).toBe(true) + expect(isVaultDetailPathname('/v3/1/0x0000000000000000000000000000000000000001')).toBe(true) + expect(isVaultDetailPathname('/vaults/1/not-an-address')).toBe(false) + expect(isVaultDetailPathname('/vaults')).toBe(false) + }) + + it('keeps token lists lazy on vault list and detail routes', () => { + expect(shouldLoadAppTokenLists('/vaults')).toBe(false) + expect(shouldLoadAppTokenLists('/vaults/1/0x0000000000000000000000000000000000000001')).toBe(false) + expect(shouldLoadAppTokenLists('/portfolio')).toBe(true) + }) +}) diff --git a/src/appRouteDataLoading.ts b/src/appRouteDataLoading.ts new file mode 100644 index 000000000..f025a5f15 --- /dev/null +++ b/src/appRouteDataLoading.ts @@ -0,0 +1,23 @@ +function getPathSegments(pathname: string): string[] { + const [pathOnly] = pathname.split(/[?#]/) + return (pathOnly || '/').split('/').filter(Boolean) +} + +export function isVaultDetailPathname(pathname: string): boolean { + const [section, chainID, address, ...rest] = getPathSegments(pathname) + return ( + rest.length === 0 && + (section === 'vaults' || section === 'v3') && + /^\d+$/.test(chainID ?? '') && + /^0x[0-9a-fA-F]{40}$/.test(address ?? '') + ) +} + +export function isVaultsListPathname(pathname: string): boolean { + const [section, ...rest] = getPathSegments(pathname) + return rest.length === 0 && (section === 'vaults' || section === 'v3') +} + +export function shouldLoadAppTokenLists(pathname: string): boolean { + return !isVaultDetailPathname(pathname) && !isVaultsListPathname(pathname) +} diff --git a/src/components/Image.tsx b/src/components/Image.tsx index bfab6794c..332d3e996 100644 --- a/src/components/Image.tsx +++ b/src/components/Image.tsx @@ -139,6 +139,7 @@ function Image(props: CustomImageProps): ReactElement {
{errorPlaceholder} {isVisible && ( + // biome-ignore lint/performance/noImgElement: this compatibility image primitive owns fallback and lazy-loading behavior. { - it('renders external links as with safe defaults', () => { - const originalWindow = globalThis.window - ;(globalThis as unknown as { window: unknown }).window = { - location: { href: 'https://yearn.fi/', hostname: 'yearn.fi' } - } - - try { - const html = renderToStaticMarkup(Example) - - expect(html).toContain('href="https://example.com"') - expect(html).toContain('target="_blank"') - expect(html).toContain('rel="noopener noreferrer"') - } finally { - ;(globalThis as unknown as { window: unknown }).window = originalWindow - } - }) - - it('renders internal links through react-router', () => { - const originalWindow = globalThis.window - ;(globalThis as unknown as { window: unknown }).window = { - location: { href: 'https://yearn.fi/', hostname: 'yearn.fi' } - } - - try { - const html = renderToStaticMarkup( - - Vaults - - ) - - expect(html).toContain('href="/vaults"') - expect(html).not.toContain('target="_blank"') - } finally { - ;(globalThis as unknown as { window: unknown }).window = originalWindow - } - }) -}) diff --git a/src/components/Link.tsx b/src/components/Link.tsx deleted file mode 100644 index e74e537e3..000000000 --- a/src/components/Link.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import type { ReactNode } from 'react' -import type { LinkProps as RouterLinkProps } from 'react-router' -import { Link as RouterLink } from 'react-router' - -export type LinkProps = { - href?: string - to?: string - children: ReactNode - className?: string - target?: string - rel?: string - onClick?: (e: React.MouseEvent) => void -} & Omit - -function isExternalLink(url: string): boolean { - try { - const urlObj = new URL(url, window.location.href) - return urlObj.hostname !== window.location.hostname - } catch { - return false - } -} - -export default function Link(props: LinkProps): React.ReactElement { - const { href, to, children, className, target, rel, onClick, ...rest } = props - - // Use href or to, with href taking precedence - const url = href || to || '' - - // Check if it's an external link - if (isExternalLink(url)) { - return ( - - {children} - - ) - } - - // Internal link using React Router - return ( - - {children} - - ) -} diff --git a/src/components/pages/icon-list/data.ts b/src/components/pages/icon-list/data.ts new file mode 100644 index 000000000..d52251c1e --- /dev/null +++ b/src/components/pages/icon-list/data.ts @@ -0,0 +1,306 @@ +import type { ReactElement, SVGProps } from 'react' +import { ICON_MODULES } from './iconRegistry' + +export type TIconProps = SVGProps & { + color?: string + back?: string + front?: string + gradient?: { start: string; end: string } +} + +export type TIconEntry = { + key: string + name: string + path: string + displayPath: string + Component: (props: TIconProps) => ReactElement +} + +export type TPublicIconAsset = { + name: string + path: string + src: string +} + +export type TUsageMap = Record + +export const BROKEN_ASSET_NOTES: Record = { + 'public/yearn-logo.svg': 'Broken: file is empty.', + 'public/v3Mark.svg': 'Broken: SVG contains JSX syntax.' +} + +export const ICON_OVERRIDES: Record; forceFill?: boolean }> = { + IconExpand: { note: 'Override: forced fill for visibility.', forceFill: true }, + IconMinimize: { note: 'Override: forced fill for visibility.', forceFill: true }, + LogoYearn: { + note: 'Override: front/back colors set for contrast.', + props: { back: 'text-neutral-900', front: 'text-neutral-0' } + } +} + +export const DUPLICATE_GROUPS = [ + { + id: 'close', + label: 'Duplicate: close icon', + tone: 'primary', + members: new Set(['CloseIcon', 'IconClose', 'IconCross']) + } +] + +export const DUPLICATE_TONE_CLASS: Record = { + primary: 'border-amber-300/70 bg-amber-50/60', + secondary: 'border-fuchsia-300/70 bg-fuchsia-50/60' +} + +export const PUBLIC_ICON_ASSETS: TPublicIconAsset[] = [ + { + name: 'GitBook - Icon - Dark.svg', + path: 'public/GitBook - Icon - Dark.svg', + src: '/GitBook%20-%20Icon%20-%20Dark.svg' + }, + { + name: 'discourse-icon.svg', + path: 'public/discourse-icon.svg', + src: '/discourse-icon.svg' + }, + { + name: '3d-logo-bw.png', + path: 'public/3d-logo-bw.png', + src: '/3d-logo-bw.png' + }, + { + name: '3d-logo.png', + path: 'public/3d-logo.png', + src: '/3d-logo.png' + }, + { + name: 'bearn-logo.png', + path: 'public/bearn-logo.png', + src: '/bearn-logo.png' + }, + { + name: 'factory-icon.svg', + path: 'public/factory-icon.svg', + src: '/factory-icon.svg' + }, + { + name: 'logo.svg', + path: 'public/logo.svg', + src: '/logo.svg' + }, + { + name: 'naughty-yearn-typemark-1.svg', + path: 'public/naughty-yearn-typemark-1.svg', + src: '/naughty-yearn-typemark-1.svg' + }, + { + name: 'resupply-logo.svg', + path: 'public/resupply-logo.svg', + src: '/resupply-logo.svg' + }, + { + name: 'v2.png', + path: 'public/v2.png', + src: '/v2.png' + }, + { + name: 'v3.png', + path: 'public/v3.png', + src: '/v3.png' + }, + { + name: 'v3Mark.svg', + path: 'public/v3Mark.svg', + src: '/v3Mark.svg' + }, + { + name: 'yearn-logo-text.svg', + path: 'public/yearn-logo-text.svg', + src: '/yearn-logo-text.svg' + }, + { + name: 'yearn-logo.svg', + path: 'public/yearn-logo.svg', + src: '/yearn-logo.svg' + }, + { + name: 'yearn-text.svg', + path: 'public/yearn-text.svg', + src: '/yearn-text.svg' + }, + { + name: 'yearn-typemark.svg', + path: 'public/yearn-typemark.svg', + src: '/yearn-typemark.svg' + }, + { + name: 'favicon.ico', + path: 'public/favicon.ico', + src: '/favicon.ico' + }, + { + name: 'android-icon-36x36.png', + path: 'public/favicons/android-icon-36x36.png', + src: '/favicons/android-icon-36x36.png' + }, + { + name: 'android-icon-48x48.png', + path: 'public/favicons/android-icon-48x48.png', + src: '/favicons/android-icon-48x48.png' + }, + { + name: 'android-icon-72x72.png', + path: 'public/favicons/android-icon-72x72.png', + src: '/favicons/android-icon-72x72.png' + }, + { + name: 'android-icon-96x96.png', + path: 'public/favicons/android-icon-96x96.png', + src: '/favicons/android-icon-96x96.png' + }, + { + name: 'android-icon-144x144.png', + path: 'public/favicons/android-icon-144x144.png', + src: '/favicons/android-icon-144x144.png' + }, + { + name: 'android-icon-192x192.png', + path: 'public/favicons/android-icon-192x192.png', + src: '/favicons/android-icon-192x192.png' + }, + { + name: 'android-icon-512x512.png', + path: 'public/favicons/android-icon-512x512.png', + src: '/favicons/android-icon-512x512.png' + }, + { + name: 'apple-icon.png', + path: 'public/favicons/apple-icon.png', + src: '/favicons/apple-icon.png' + }, + { + name: 'apple-icon-57x57.png', + path: 'public/favicons/apple-icon-57x57.png', + src: '/favicons/apple-icon-57x57.png' + }, + { + name: 'apple-icon-60x60.png', + path: 'public/favicons/apple-icon-60x60.png', + src: '/favicons/apple-icon-60x60.png' + }, + { + name: 'apple-icon-72x72.png', + path: 'public/favicons/apple-icon-72x72.png', + src: '/favicons/apple-icon-72x72.png' + }, + { + name: 'apple-icon-76x76.png', + path: 'public/favicons/apple-icon-76x76.png', + src: '/favicons/apple-icon-76x76.png' + }, + { + name: 'apple-icon-114x114.png', + path: 'public/favicons/apple-icon-114x114.png', + src: '/favicons/apple-icon-114x114.png' + }, + { + name: 'apple-icon-120x120.png', + path: 'public/favicons/apple-icon-120x120.png', + src: '/favicons/apple-icon-120x120.png' + }, + { + name: 'apple-icon-144x144.png', + path: 'public/favicons/apple-icon-144x144.png', + src: '/favicons/apple-icon-144x144.png' + }, + { + name: 'apple-icon-152x152.png', + path: 'public/favicons/apple-icon-152x152.png', + src: '/favicons/apple-icon-152x152.png' + }, + { + name: 'apple-icon-180x180.png', + path: 'public/favicons/apple-icon-180x180.png', + src: '/favicons/apple-icon-180x180.png' + }, + { + name: 'apple-icon-precomposed.png', + path: 'public/favicons/apple-icon-precomposed.png', + src: '/favicons/apple-icon-precomposed.png' + }, + { + name: 'favicon-16x16.png', + path: 'public/favicons/favicon-16x16.png', + src: '/favicons/favicon-16x16.png' + }, + { + name: 'favicon-32x32.png', + path: 'public/favicons/favicon-32x32.png', + src: '/favicons/favicon-32x32.png' + }, + { + name: 'favicon-96x96.png', + path: 'public/favicons/favicon-96x96.png', + src: '/favicons/favicon-96x96.png' + }, + { + name: 'favicon.ico', + path: 'public/favicons/favicon.ico', + src: '/favicons/favicon.ico' + }, + { + name: 'favicon.svg', + path: 'public/favicons/favicon.svg', + src: '/favicons/favicon.svg' + }, + { + name: 'ms-icon-70x70.png', + path: 'public/favicons/ms-icon-70x70.png', + src: '/favicons/ms-icon-70x70.png' + }, + { + name: 'ms-icon-144x144.png', + path: 'public/favicons/ms-icon-144x144.png', + src: '/favicons/ms-icon-144x144.png' + }, + { + name: 'ms-icon-150x150.png', + path: 'public/favicons/ms-icon-150x150.png', + src: '/favicons/ms-icon-150x150.png' + }, + { + name: 'ms-icon-310x310.png', + path: 'public/favicons/ms-icon-310x310.png', + src: '/favicons/ms-icon-310x310.png' + } +] + +export const normalizePath = (path: string): string => (path.startsWith('/src/') ? `src/${path.slice(5)}` : path) + +export const ICON_ENTRIES: TIconEntry[] = Object.entries(ICON_MODULES) + .flatMap(([path, module]) => + Object.entries(module) + .filter(([, value]) => typeof value === 'function') + .map(([name, Component]) => ({ + key: `${path}:${name}`, + name, + path, + displayPath: normalizePath(path), + Component: Component as TIconEntry['Component'] + })) + ) + .sort((a, b) => a.path.localeCompare(b.path) || a.name.localeCompare(b.name)) + +export function getIconProps(name: string, override?: Partial, forceFill?: boolean): TIconProps { + const lower = name.toLowerCase() + const baseClass = lower.includes('typemark') || lower.includes('logo') ? 'h-6 w-auto' : 'size-6' + const className = forceFill ? `${baseClass} icon-force-fill` : baseClass + + if (lower.includes('typemark')) { + return { className, ...override } + } + if (lower.includes('logo')) { + return { className, ...override } + } + return { className, ...override } +} diff --git a/src/components/pages/icon-list/iconRegistry.ts b/src/components/pages/icon-list/iconRegistry.ts new file mode 100644 index 000000000..b7bb50fdb --- /dev/null +++ b/src/components/pages/icon-list/iconRegistry.ts @@ -0,0 +1,159 @@ +import * as IconsModule from '@pages/vaults/components/widget/shared/Icons' +import * as IconAddToMetamaskModule from '@shared/icons/IconAddToMetamask' +import * as IconAlertCriticalModule from '@shared/icons/IconAlertCritical' +import * as IconAlertErrorModule from '@shared/icons/IconAlertError' +import * as IconAlertWarningModule from '@shared/icons/IconAlertWarning' +import * as IconArrowModule from '@shared/icons/IconArrow' +import * as IconArrowLeftModule from '@shared/icons/IconArrowLeft' +import * as IconArrowRightModule from '@shared/icons/IconArrowRight' +import * as IconBellModule from '@shared/icons/IconBell' +import * as IconBurgerPlainModule from '@shared/icons/IconBurgerPlain' +import * as IconCalendarDaysModule from '@shared/icons/IconCalendarDays' +import * as IconChainModule from '@shared/icons/IconChain' +import * as IconCheckModule from '@shared/icons/IconCheck' +import * as IconCheckmarkModule from '@shared/icons/IconCheckmark' +import * as IconChevronModule from '@shared/icons/IconChevron' +import * as IconCircleModule from '@shared/icons/IconCircle' +import * as IconCirclePileModule from '@shared/icons/IconCirclePile' +import * as IconCloseModule from '@shared/icons/IconClose' +import * as IconCopyModule from '@shared/icons/IconCopy' +import * as IconCrossModule from '@shared/icons/IconCross' +import * as IconDepositModule from '@shared/icons/IconDeposit' +import * as IconDiscordModule from '@shared/icons/IconDiscord' +import * as IconEnterModule from '@shared/icons/IconEnter' +import * as IconExpandModule from '@shared/icons/IconExpand' +import * as IconEyeOffModule from '@shared/icons/IconEyeOff' +import * as IconFilterModule from '@shared/icons/IconFilter' +import * as IconFixedRateModule from '@shared/icons/IconFixedRate' +import * as IconGitCompareModule from '@shared/icons/IconGitCompare' +import * as IconHandCoinsModule from '@shared/icons/IconHandCoins' +import * as IconInfinifiPointsModule from '@shared/icons/IconInfinifiPoints' +import * as IconInfoModule from '@shared/icons/IconInfo' +import * as IconLinkOutModule from '@shared/icons/IconLinkOut' +import * as IconLoaderModule from '@shared/icons/IconLoader' +import * as IconLockModule from '@shared/icons/IconLock' +import * as IconLockOpenModule from '@shared/icons/IconLockOpen' +import * as IconMigratableModule from '@shared/icons/IconMigratable' +import * as IconMinimizeModule from '@shared/icons/IconMinimize' +import * as IconMinusModule from '@shared/icons/IconMinus' +import * as IconMoonModule from '@shared/icons/IconMoon' +import * as IconPaginationArrowModule from '@shared/icons/IconPaginationArrow' +import * as IconParagraphModule from '@shared/icons/IconParagraph' +import * as IconPendleModule from '@shared/icons/IconPendle' +import * as IconPercentModule from '@shared/icons/IconPercent' +import * as IconPlusModule from '@shared/icons/IconPlus' +import * as IconPowerModule from '@shared/icons/IconPower' +import * as IconQuestionModule from '@shared/icons/IconQuestion' +import * as IconRetiredModule from '@shared/icons/IconRetired' +import * as IconRewindModule from '@shared/icons/IconRewind' +import * as IconScissorsModule from '@shared/icons/IconScissors' +import * as IconSearchModule from '@shared/icons/IconSearch' +import * as IconSettingsModule from '@shared/icons/IconSettings' +import * as IconShareModule from '@shared/icons/IconShare' +import * as IconSlidersModule from '@shared/icons/IconSliders' +import * as IconSpectraModule from '@shared/icons/IconSpectra' +import * as IconSpinnerModule from '@shared/icons/IconSpinner' +import * as IconStablecoinModule from '@shared/icons/IconStablecoin' +import * as IconStackModule from '@shared/icons/IconStack' +import * as IconStakeModule from '@shared/icons/IconStake' +import * as IconSunModule from '@shared/icons/IconSun' +import * as IconTelegramModule from '@shared/icons/IconTelegram' +import * as IconTwitterModule from '@shared/icons/IconTwitter' +import * as IconUnstakeModule from '@shared/icons/IconUnstake' +import * as IconVaultsModule from '@shared/icons/IconVaults' +import * as IconVolatileModule from '@shared/icons/IconVolatile' +import * as IconWalletModule from '@shared/icons/IconWallet' +import * as IconWithdrawModule from '@shared/icons/IconWithdraw' +import * as LogoCurationModule from '@shared/icons/LogoCuration' +import * as LogoDiscordModule from '@shared/icons/LogoDiscord' +import * as LogoGimmeModule from '@shared/icons/LogoGimme' +import * as LogoGithubModule from '@shared/icons/LogoGithub' +import * as LogoTwitterModule from '@shared/icons/LogoTwitter' +import * as LogoYearnModule from '@shared/icons/LogoYearn' +import * as LogoYearnMarkModule from '@shared/icons/LogoYearnMark' +import * as LogoYearnMarkOutlineModule from '@shared/icons/LogoYearnMarkOutline' +import * as LogoYearnOutlineModule from '@shared/icons/LogoYearnOutline' +import * as TypeMarkYearnModule from '@shared/icons/TypeMarkYearn' +import * as TypeMarkYearnNaughtyModule from '@shared/icons/TypeMarkYearn-naughty' +import * as TypeMarkYearnTextOnlyModule from '@shared/icons/TypeMarkYearn-text-only' + +export const ICON_MODULES: Record> = { + '/src/components/pages/vaults/components/widget/shared/Icons.tsx': IconsModule, + '/src/components/shared/icons/IconAddToMetamask.tsx': IconAddToMetamaskModule, + '/src/components/shared/icons/IconAlertCritical.tsx': IconAlertCriticalModule, + '/src/components/shared/icons/IconAlertError.tsx': IconAlertErrorModule, + '/src/components/shared/icons/IconAlertWarning.tsx': IconAlertWarningModule, + '/src/components/shared/icons/IconArrow.tsx': IconArrowModule, + '/src/components/shared/icons/IconArrowLeft.tsx': IconArrowLeftModule, + '/src/components/shared/icons/IconArrowRight.tsx': IconArrowRightModule, + '/src/components/shared/icons/IconBell.tsx': IconBellModule, + '/src/components/shared/icons/IconBurgerPlain.tsx': IconBurgerPlainModule, + '/src/components/shared/icons/IconCalendarDays.tsx': IconCalendarDaysModule, + '/src/components/shared/icons/IconChain.tsx': IconChainModule, + '/src/components/shared/icons/IconCheck.tsx': IconCheckModule, + '/src/components/shared/icons/IconCheckmark.tsx': IconCheckmarkModule, + '/src/components/shared/icons/IconChevron.tsx': IconChevronModule, + '/src/components/shared/icons/IconCircle.tsx': IconCircleModule, + '/src/components/shared/icons/IconCirclePile.tsx': IconCirclePileModule, + '/src/components/shared/icons/IconClose.tsx': IconCloseModule, + '/src/components/shared/icons/IconCopy.tsx': IconCopyModule, + '/src/components/shared/icons/IconCross.tsx': IconCrossModule, + '/src/components/shared/icons/IconDeposit.tsx': IconDepositModule, + '/src/components/shared/icons/IconDiscord.tsx': IconDiscordModule, + '/src/components/shared/icons/IconEnter.tsx': IconEnterModule, + '/src/components/shared/icons/IconExpand.tsx': IconExpandModule, + '/src/components/shared/icons/IconEyeOff.tsx': IconEyeOffModule, + '/src/components/shared/icons/IconFilter.tsx': IconFilterModule, + '/src/components/shared/icons/IconFixedRate.tsx': IconFixedRateModule, + '/src/components/shared/icons/IconGitCompare.tsx': IconGitCompareModule, + '/src/components/shared/icons/IconHandCoins.tsx': IconHandCoinsModule, + '/src/components/shared/icons/IconInfinifiPoints.tsx': IconInfinifiPointsModule, + '/src/components/shared/icons/IconInfo.tsx': IconInfoModule, + '/src/components/shared/icons/IconLinkOut.tsx': IconLinkOutModule, + '/src/components/shared/icons/IconLoader.tsx': IconLoaderModule, + '/src/components/shared/icons/IconLock.tsx': IconLockModule, + '/src/components/shared/icons/IconLockOpen.tsx': IconLockOpenModule, + '/src/components/shared/icons/IconMigratable.tsx': IconMigratableModule, + '/src/components/shared/icons/IconMinimize.tsx': IconMinimizeModule, + '/src/components/shared/icons/IconMinus.tsx': IconMinusModule, + '/src/components/shared/icons/IconMoon.tsx': IconMoonModule, + '/src/components/shared/icons/IconPaginationArrow.tsx': IconPaginationArrowModule, + '/src/components/shared/icons/IconParagraph.tsx': IconParagraphModule, + '/src/components/shared/icons/IconPendle.tsx': IconPendleModule, + '/src/components/shared/icons/IconPercent.tsx': IconPercentModule, + '/src/components/shared/icons/IconPlus.tsx': IconPlusModule, + '/src/components/shared/icons/IconPower.tsx': IconPowerModule, + '/src/components/shared/icons/IconQuestion.tsx': IconQuestionModule, + '/src/components/shared/icons/IconRetired.tsx': IconRetiredModule, + '/src/components/shared/icons/IconRewind.tsx': IconRewindModule, + '/src/components/shared/icons/IconScissors.tsx': IconScissorsModule, + '/src/components/shared/icons/IconSearch.tsx': IconSearchModule, + '/src/components/shared/icons/IconSettings.tsx': IconSettingsModule, + '/src/components/shared/icons/IconShare.tsx': IconShareModule, + '/src/components/shared/icons/IconSliders.tsx': IconSlidersModule, + '/src/components/shared/icons/IconSpectra.tsx': IconSpectraModule, + '/src/components/shared/icons/IconSpinner.tsx': IconSpinnerModule, + '/src/components/shared/icons/IconStablecoin.tsx': IconStablecoinModule, + '/src/components/shared/icons/IconStack.tsx': IconStackModule, + '/src/components/shared/icons/IconStake.tsx': IconStakeModule, + '/src/components/shared/icons/IconSun.tsx': IconSunModule, + '/src/components/shared/icons/IconTelegram.tsx': IconTelegramModule, + '/src/components/shared/icons/IconTwitter.tsx': IconTwitterModule, + '/src/components/shared/icons/IconUnstake.tsx': IconUnstakeModule, + '/src/components/shared/icons/IconVaults.tsx': IconVaultsModule, + '/src/components/shared/icons/IconVolatile.tsx': IconVolatileModule, + '/src/components/shared/icons/IconWallet.tsx': IconWalletModule, + '/src/components/shared/icons/IconWithdraw.tsx': IconWithdrawModule, + '/src/components/shared/icons/LogoCuration.tsx': LogoCurationModule, + '/src/components/shared/icons/LogoDiscord.tsx': LogoDiscordModule, + '/src/components/shared/icons/LogoGimme.tsx': LogoGimmeModule, + '/src/components/shared/icons/LogoGithub.tsx': LogoGithubModule, + '/src/components/shared/icons/LogoTwitter.tsx': LogoTwitterModule, + '/src/components/shared/icons/LogoYearn.tsx': LogoYearnModule, + '/src/components/shared/icons/LogoYearnMark.tsx': LogoYearnMarkModule, + '/src/components/shared/icons/LogoYearnMarkOutline.tsx': LogoYearnMarkOutlineModule, + '/src/components/shared/icons/LogoYearnOutline.tsx': LogoYearnOutlineModule, + '/src/components/shared/icons/TypeMarkYearn.tsx': TypeMarkYearnModule, + '/src/components/shared/icons/TypeMarkYearn-naughty.tsx': TypeMarkYearnNaughtyModule, + '/src/components/shared/icons/TypeMarkYearn-text-only.tsx': TypeMarkYearnTextOnlyModule +} diff --git a/src/components/pages/icon-list/index.tsx b/src/components/pages/icon-list/index.tsx index 243a3d927..c8e94fc32 100644 --- a/src/components/pages/icon-list/index.tsx +++ b/src/components/pages/icon-list/index.tsx @@ -1,382 +1,19 @@ import { cl } from '@shared/utils' -import type { ReactElement, SVGProps } from 'react' -import { createElement, useCallback, useEffect, useState } from 'react' +import type { ReactElement } from 'react' +import { createElement } from 'react' import Image from '/src/components/Image' - -type TIconProps = SVGProps & { - color?: string - back?: string - front?: string - gradient?: { start: string; end: string } -} - -type TIconEntry = { - key: string - name: string - path: string - displayPath: string - Component: (props: TIconProps) => ReactElement -} - -type TPublicIconAsset = { - name: string - path: string - src: string -} - -type TUsageMap = Record - -const ICON_MODULES = { - ...import.meta.glob('/src/components/shared/icons/*.tsx', { eager: true }), - ...import.meta.glob('/src/components/pages/**/Icons.tsx', { eager: true }) -} - -const SOURCE_MODULES = import.meta.glob('/src/**/*.{ts,tsx,css}', { query: '?raw', import: 'default' }) - -const BROKEN_ASSET_NOTES: Record = { - 'public/yearn-logo.svg': 'Broken: file is empty.', - 'public/v3Mark.svg': 'Broken: SVG contains JSX syntax.' -} - -const ICON_OVERRIDES: Record; forceFill?: boolean }> = { - IconExpand: { note: 'Override: forced fill for visibility.', forceFill: true }, - IconMinimize: { note: 'Override: forced fill for visibility.', forceFill: true }, - LogoYearn: { - note: 'Override: front/back colors set for contrast.', - props: { back: 'text-neutral-900', front: 'text-neutral-0' } - } -} - -const DUPLICATE_GROUPS = [ - { - id: 'close', - label: 'Duplicate: close icon', - tone: 'primary', - members: new Set(['CloseIcon', 'IconClose', 'IconCross']) - } -] - -const DUPLICATE_TONE_CLASS: Record = { - primary: 'border-amber-300/70 bg-amber-50/60', - secondary: 'border-fuchsia-300/70 bg-fuchsia-50/60' -} - -const PUBLIC_ICON_ASSETS: TPublicIconAsset[] = [ - { - name: 'GitBook - Icon - Dark.svg', - path: 'public/GitBook - Icon - Dark.svg', - src: '/GitBook%20-%20Icon%20-%20Dark.svg' - }, - { - name: 'discourse-icon.svg', - path: 'public/discourse-icon.svg', - src: '/discourse-icon.svg' - }, - { - name: '3d-logo-bw.png', - path: 'public/3d-logo-bw.png', - src: '/3d-logo-bw.png' - }, - { - name: '3d-logo.png', - path: 'public/3d-logo.png', - src: '/3d-logo.png' - }, - { - name: 'bearn-logo.png', - path: 'public/bearn-logo.png', - src: '/bearn-logo.png' - }, - { - name: 'factory-icon.svg', - path: 'public/factory-icon.svg', - src: '/factory-icon.svg' - }, - { - name: 'logo.svg', - path: 'public/logo.svg', - src: '/logo.svg' - }, - { - name: 'naughty-yearn-typemark-1.svg', - path: 'public/naughty-yearn-typemark-1.svg', - src: '/naughty-yearn-typemark-1.svg' - }, - { - name: 'resupply-logo.svg', - path: 'public/resupply-logo.svg', - src: '/resupply-logo.svg' - }, - { - name: 'v2.png', - path: 'public/v2.png', - src: '/v2.png' - }, - { - name: 'v3.png', - path: 'public/v3.png', - src: '/v3.png' - }, - { - name: 'v3Mark.svg', - path: 'public/v3Mark.svg', - src: '/v3Mark.svg' - }, - { - name: 'yearn-logo-text.svg', - path: 'public/yearn-logo-text.svg', - src: '/yearn-logo-text.svg' - }, - { - name: 'yearn-logo.svg', - path: 'public/yearn-logo.svg', - src: '/yearn-logo.svg' - }, - { - name: 'yearn-text.svg', - path: 'public/yearn-text.svg', - src: '/yearn-text.svg' - }, - { - name: 'yearn-typemark.svg', - path: 'public/yearn-typemark.svg', - src: '/yearn-typemark.svg' - }, - { - name: 'favicon.ico', - path: 'public/favicon.ico', - src: '/favicon.ico' - }, - { - name: 'android-icon-36x36.png', - path: 'public/favicons/android-icon-36x36.png', - src: '/favicons/android-icon-36x36.png' - }, - { - name: 'android-icon-48x48.png', - path: 'public/favicons/android-icon-48x48.png', - src: '/favicons/android-icon-48x48.png' - }, - { - name: 'android-icon-72x72.png', - path: 'public/favicons/android-icon-72x72.png', - src: '/favicons/android-icon-72x72.png' - }, - { - name: 'android-icon-96x96.png', - path: 'public/favicons/android-icon-96x96.png', - src: '/favicons/android-icon-96x96.png' - }, - { - name: 'android-icon-144x144.png', - path: 'public/favicons/android-icon-144x144.png', - src: '/favicons/android-icon-144x144.png' - }, - { - name: 'android-icon-192x192.png', - path: 'public/favicons/android-icon-192x192.png', - src: '/favicons/android-icon-192x192.png' - }, - { - name: 'android-icon-512x512.png', - path: 'public/favicons/android-icon-512x512.png', - src: '/favicons/android-icon-512x512.png' - }, - { - name: 'apple-icon.png', - path: 'public/favicons/apple-icon.png', - src: '/favicons/apple-icon.png' - }, - { - name: 'apple-icon-57x57.png', - path: 'public/favicons/apple-icon-57x57.png', - src: '/favicons/apple-icon-57x57.png' - }, - { - name: 'apple-icon-60x60.png', - path: 'public/favicons/apple-icon-60x60.png', - src: '/favicons/apple-icon-60x60.png' - }, - { - name: 'apple-icon-72x72.png', - path: 'public/favicons/apple-icon-72x72.png', - src: '/favicons/apple-icon-72x72.png' - }, - { - name: 'apple-icon-76x76.png', - path: 'public/favicons/apple-icon-76x76.png', - src: '/favicons/apple-icon-76x76.png' - }, - { - name: 'apple-icon-114x114.png', - path: 'public/favicons/apple-icon-114x114.png', - src: '/favicons/apple-icon-114x114.png' - }, - { - name: 'apple-icon-120x120.png', - path: 'public/favicons/apple-icon-120x120.png', - src: '/favicons/apple-icon-120x120.png' - }, - { - name: 'apple-icon-144x144.png', - path: 'public/favicons/apple-icon-144x144.png', - src: '/favicons/apple-icon-144x144.png' - }, - { - name: 'apple-icon-152x152.png', - path: 'public/favicons/apple-icon-152x152.png', - src: '/favicons/apple-icon-152x152.png' - }, - { - name: 'apple-icon-180x180.png', - path: 'public/favicons/apple-icon-180x180.png', - src: '/favicons/apple-icon-180x180.png' - }, - { - name: 'apple-icon-precomposed.png', - path: 'public/favicons/apple-icon-precomposed.png', - src: '/favicons/apple-icon-precomposed.png' - }, - { - name: 'favicon-16x16.png', - path: 'public/favicons/favicon-16x16.png', - src: '/favicons/favicon-16x16.png' - }, - { - name: 'favicon-32x32.png', - path: 'public/favicons/favicon-32x32.png', - src: '/favicons/favicon-32x32.png' - }, - { - name: 'favicon-96x96.png', - path: 'public/favicons/favicon-96x96.png', - src: '/favicons/favicon-96x96.png' - }, - { - name: 'favicon.ico', - path: 'public/favicons/favicon.ico', - src: '/favicons/favicon.ico' - }, - { - name: 'favicon.svg', - path: 'public/favicons/favicon.svg', - src: '/favicons/favicon.svg' - }, - { - name: 'ms-icon-70x70.png', - path: 'public/favicons/ms-icon-70x70.png', - src: '/favicons/ms-icon-70x70.png' - }, - { - name: 'ms-icon-144x144.png', - path: 'public/favicons/ms-icon-144x144.png', - src: '/favicons/ms-icon-144x144.png' - }, - { - name: 'ms-icon-150x150.png', - path: 'public/favicons/ms-icon-150x150.png', - src: '/favicons/ms-icon-150x150.png' - }, - { - name: 'ms-icon-310x310.png', - path: 'public/favicons/ms-icon-310x310.png', - src: '/favicons/ms-icon-310x310.png' - } -] - -const normalizePath = (path: string): string => (path.startsWith('/src/') ? `src/${path.slice(5)}` : path) - -const ICON_ENTRIES: TIconEntry[] = Object.entries(ICON_MODULES) - .flatMap(([path, module]) => { - const exports = module as Record - return Object.entries(exports) - .filter(([, value]) => typeof value === 'function') - .map(([name, Component]) => ({ - key: `${path}:${name}`, - name, - path, - displayPath: normalizePath(path), - Component: Component as TIconEntry['Component'] - })) - }) - .sort((a, b) => a.path.localeCompare(b.path) || a.name.localeCompare(b.name)) - -function getIconProps(name: string, override?: Partial, forceFill?: boolean): TIconProps { - const lower = name.toLowerCase() - const baseClass = lower.includes('typemark') || lower.includes('logo') ? 'h-6 w-auto' : 'size-6' - const className = forceFill ? `${baseClass} icon-force-fill` : baseClass - - if (lower.includes('typemark')) { - return { className, ...override } - } - if (lower.includes('logo')) { - return { className, ...override } - } - return { className, ...override } -} - -function findMatches(sources: Record, matchers: string[], excludePaths: string[]): string[] { - return Object.entries(sources) - .filter(([path]) => !excludePaths.includes(path)) - .filter(([, content]) => matchers.some((matcher) => content.includes(matcher))) - .map(([path]) => normalizePath(path)) - .sort((a, b) => a.localeCompare(b)) -} - -export default function IconListPage(): ReactElement { - const [usageMap, setUsageMap] = useState({}) - const [isLoadingUsage, setIsLoadingUsage] = useState(true) - const [sourceCache, setSourceCache] = useState | null>(null) - - const loadSourceCache = useCallback(async (): Promise> => { - if (sourceCache) return sourceCache - const entries = await Promise.all( - Object.entries(SOURCE_MODULES).map(async ([path, loader]) => [path, await loader()]) - ) - const nextCache = Object.fromEntries(entries) as Record - setSourceCache(nextCache) - return nextCache - }, [sourceCache]) - - useEffect(() => { - let isMounted = true - - const buildUsageMap = async (): Promise => { - setIsLoadingUsage(true) - const sources = await loadSourceCache() - if (!isMounted) return - - const nextMap: TUsageMap = {} - - ICON_ENTRIES.forEach((icon) => { - const usageKey = `icon:${icon.key}` - nextMap[usageKey] = findMatches( - sources, - [icon.name, `@shared/icons/${icon.name}`, `icons/${icon.name}`], - ['/src/components/pages/icon-list/index.tsx', icon.path] - ) - }) - - PUBLIC_ICON_ASSETS.forEach((asset) => { - const usageKey = `asset:${asset.path}` - nextMap[usageKey] = findMatches( - sources, - [asset.name, asset.src, asset.src.replace(/^\//, '')], - ['/src/components/pages/icon-list/index.tsx'] - ) - }) - - setUsageMap(nextMap) - setIsLoadingUsage(false) - } - - void buildUsageMap() - - return () => { - isMounted = false - } - }, [loadSourceCache]) - +import { + BROKEN_ASSET_NOTES, + DUPLICATE_GROUPS, + DUPLICATE_TONE_CLASS, + getIconProps, + ICON_ENTRIES, + ICON_OVERRIDES, + PUBLIC_ICON_ASSETS, + type TUsageMap +} from './data' + +export default function IconListPage({ usageMap }: { usageMap: TUsageMap }): ReactElement { return (
@@ -424,11 +61,10 @@ export default function IconListPage(): ReactElement {
- {isLoadingUsage &&

{'Loading imports...'}

} - {!isLoadingUsage && usage && usage.length === 0 && ( + {(!usage || usage.length === 0) && (

{'No imports found in src.'}

)} - {!isLoadingUsage && usage && usage.length > 0 && ( + {usage && usage.length > 0 && (
{usage.map((path) => (

@@ -477,11 +113,10 @@ export default function IconListPage(): ReactElement {

- {isLoadingUsage &&

{'Loading imports...'}

} - {!isLoadingUsage && usage && usage.length === 0 && ( + {(!usage || usage.length === 0) && (

{'No imports found in src.'}

)} - {!isLoadingUsage && usage && usage.length > 0 && ( + {usage && usage.length > 0 && (
{usage.map((path) => (

diff --git a/src/components/pages/icon-list/sourceUsage.ts b/src/components/pages/icon-list/sourceUsage.ts new file mode 100644 index 000000000..97749fa78 --- /dev/null +++ b/src/components/pages/icon-list/sourceUsage.ts @@ -0,0 +1,68 @@ +import { readdir, readFile } from 'node:fs/promises' +import path from 'node:path' +import { ICON_ENTRIES, normalizePath, PUBLIC_ICON_ASSETS, type TUsageMap } from './data' + +const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.css']) +const SOURCE_ROOT = path.join(process.cwd(), 'src') +const ICON_LIST_SOURCE_PREFIX = '/src/components/pages/icon-list/' + +async function collectSourceFiles(directory: string): Promise { + const entries = await readdir(directory, { withFileTypes: true }) + const nestedFiles = await Promise.all( + entries.map(async (entry) => { + const entryPath = path.join(directory, entry.name) + + if (entry.isDirectory()) { + return collectSourceFiles(entryPath) + } + if (entry.isFile() && SOURCE_EXTENSIONS.has(path.extname(entry.name))) { + return [entryPath] + } + return [] + }) + ) + + return nestedFiles.flat() +} + +function toSourcePath(filePath: string): string { + return `/${path.relative(process.cwd(), filePath).split(path.sep).join('/')}` +} + +async function loadSources(): Promise> { + const files = await collectSourceFiles(SOURCE_ROOT) + const entries = await Promise.all( + files.map(async (filePath) => [toSourcePath(filePath), await readFile(filePath, 'utf8')] as const) + ) + + return Object.fromEntries(entries.filter(([sourcePath]) => !sourcePath.startsWith(ICON_LIST_SOURCE_PREFIX))) +} + +function findMatches(sources: Record, matchers: string[], excludePaths: string[]): string[] { + const excludedPaths = new Set(excludePaths) + + return Object.entries(sources) + .filter(([sourcePath]) => !excludedPaths.has(sourcePath)) + .filter(([, content]) => matchers.some((matcher) => content.includes(matcher))) + .map(([sourcePath]) => normalizePath(sourcePath)) + .sort((a, b) => a.localeCompare(b)) +} + +export async function buildIconUsageMap(): Promise { + const sources = await loadSources() + const usageMap: TUsageMap = {} + + ICON_ENTRIES.forEach((icon) => { + usageMap[`icon:${icon.key}`] = findMatches( + sources, + [icon.name, `@shared/icons/${icon.name}`, `icons/${icon.name}`], + [icon.path] + ) + }) + + PUBLIC_ICON_ASSETS.forEach((asset) => { + usageMap[`asset:${asset.path}`] = findMatches(sources, [asset.name, asset.src, asset.src.replace(/^\//, '')], []) + }) + + return usageMap +} diff --git a/src/components/pages/landing/components/sections/Footer.tsx b/src/components/pages/landing/components/sections/Footer.tsx index 4d45b0a58..eac08fec2 100644 --- a/src/components/pages/landing/components/sections/Footer.tsx +++ b/src/components/pages/landing/components/sections/Footer.tsx @@ -1,5 +1,5 @@ +import Link from 'next/link' import type { ReactElement } from 'react' -import Link from '/src/components/Link' const footerLinks = [ { path: 'https://docs.yearn.fi/', label: 'Docs' }, @@ -25,6 +25,7 @@ export function Footer(): ReactElement { key={link.path} href={link.path} target={'_blank'} + rel={'noopener noreferrer'} className={ 'flex min-h-[44px] items-center gap-1 px-1 text-neutral-900 transition-colors hover:text-white sm:min-h-0 sm:px-0' } diff --git a/src/components/pages/landing/components/sections/Hero.tsx b/src/components/pages/landing/components/sections/Hero.tsx index 46bf53a90..02d1b171d 100644 --- a/src/components/pages/landing/components/sections/Hero.tsx +++ b/src/components/pages/landing/components/sections/Hero.tsx @@ -2,12 +2,15 @@ import { usePlausible } from '@hooks/usePlausible' import { Button } from '@shared/components/Button' import { SectionHeader } from '@shared/components/SectionHeader' import { TvlStat } from '@shared/components/TvlStat' +import { YEARN_TVL_ENDPOINT } from '@shared/data/publicQueryEndpoints' import { useFetch } from '@shared/hooks/useFetch' import { PLAUSIBLE_EVENTS } from '@shared/utils/plausible' +import Link from 'next/link' +import { useRouter } from 'next/navigation' import type { ReactElement } from 'react' +import { useEffect } from 'react' import * as z from 'zod' import Image from '/src/components/Image' -import Link from '/src/components/Link' function AnimatedLogos(): ReactElement { return ( @@ -74,11 +77,20 @@ function AnimatedLogos(): ReactElement { export function Hero(): ReactElement { const trackEvent = usePlausible() + const router = useRouter() const { data: tvl } = useFetch({ - endpoint: 'https://api.llama.fi/tvl/yearn', + endpoint: YEARN_TVL_ENDPOINT, schema: z.number() }) + useEffect(() => { + const prefetchTimer = window.setTimeout(() => { + router.prefetch('/vaults') + }, 500) + + return () => window.clearTimeout(prefetchTimer) + }, [router]) + return ( <> {/* Desktop Hero Section */} @@ -110,7 +122,11 @@ export function Hero(): ReactElement { description={"Yearn is DeFi's Yield Aggregator"} />

- trackEvent(PLAUSIBLE_EVENTS.LANDER_CTA_EXPLORE_VAULTS, {})}> + trackEvent(PLAUSIBLE_EVENTS.LANDER_CTA_EXPLORE_VAULTS, {})} + > @@ -154,6 +170,7 @@ export function Hero(): ReactElement {
trackEvent(PLAUSIBLE_EVENTS.LANDER_CTA_EXPLORE_VAULTS, {})} > diff --git a/src/components/pages/landing/components/sections/Integrations.tsx b/src/components/pages/landing/components/sections/Integrations.tsx index b97f5650f..b1ad210a2 100644 --- a/src/components/pages/landing/components/sections/Integrations.tsx +++ b/src/components/pages/landing/components/sections/Integrations.tsx @@ -1,7 +1,7 @@ import { SectionHeader } from '@shared/components/SectionHeader' +import Link from 'next/link' import type { FC } from 'react' import Image from '/src/components/Image' -import Link from '/src/components/Link' type TIntegration = { name: string @@ -57,7 +57,7 @@ const integrations: TIntegration[] = [ const IntegrationItem: FC = ({ name, imageSrc, description, href, index }) => { return ( - +
diff --git a/src/components/pages/landing/components/sections/Partners.tsx b/src/components/pages/landing/components/sections/Partners.tsx index 7080b94f1..7593777be 100644 --- a/src/components/pages/landing/components/sections/Partners.tsx +++ b/src/components/pages/landing/components/sections/Partners.tsx @@ -1,8 +1,8 @@ import { SectionHeader } from '@shared/components/SectionHeader' +import Link from 'next/link' import type { FC } from 'react' import { useState } from 'react' import Image from '/src/components/Image' -import Link from '/src/components/Link' type TPartner = { image: string @@ -47,7 +47,7 @@ const partners: TPartner[] = [ const PartnerLogo: FC = ({ image, alt, href, size = 60 }) => { const [isHovered, setIsHovered] = useState(false) return ( - +
+
diff --git a/src/components/pages/portfolio/components/EmptySectionCard.tsx b/src/components/pages/portfolio/components/EmptySectionCard.tsx index d390e9b16..104dae51a 100644 --- a/src/components/pages/portfolio/components/EmptySectionCard.tsx +++ b/src/components/pages/portfolio/components/EmptySectionCard.tsx @@ -1,7 +1,7 @@ import { Button } from '@shared/components/Button' import { EmptyState } from '@shared/components/EmptyState' +import Link from 'next/link' import type { ReactElement } from 'react' -import { Link } from 'react-router' type TEmptySectionCardProps = { title: string @@ -25,7 +25,7 @@ export function EmptySectionCard({ href }: TEmptySectionCardProps): ReactElement { const actionButton = href ? ( - + {ctaLabel} ) : ( @@ -36,7 +36,7 @@ export function EmptySectionCard({ const secondaryActionButton = secondaryCtaHref && secondaryCtaLabel ? (
- + {'Explore Vaults'}
diff --git a/src/components/pages/portfolio/hooks/usePortfolioModel.ts b/src/components/pages/portfolio/hooks/usePortfolioModel.ts index b2768a76d..2360e1472 100644 --- a/src/components/pages/portfolio/hooks/usePortfolioModel.ts +++ b/src/components/pages/portfolio/hooks/usePortfolioModel.ts @@ -28,10 +28,12 @@ import { YVUSD_LOCKED_ADDRESS, YVUSD_UNLOCKED_ADDRESS } from '@pages/vaults/utils/yvUsd' -import { useWallet } from '@shared/contexts/useWallet' +import { useWalletHoldings, useWalletStatus, useWalletTokens } from '@shared/contexts/useWallet' +import { useWalletVaultTotals } from '@shared/contexts/useWalletVaultTotals' import { useWeb3 } from '@shared/contexts/useWeb3' import { useYearn } from '@shared/contexts/useYearn' import { getVaultKey, isV3Vault, type TVaultFlags } from '@shared/hooks/useVaultFilterUtils' +import { useYearnSpotPrices } from '@shared/hooks/useYearnSpotPrices' import type { TSortDirection } from '@shared/types' import { isZeroAddress, toAddress } from '@shared/utils' import { ETH_TOKEN_ADDRESS } from '@shared/utils/constants' @@ -147,7 +149,7 @@ function getPortfolioRowHref(vault: TKongVaultInput): string | undefined { return `/vaults/${getVaultChainID(vault)}/${toAddress(getVaultAddress(vault))}` } -function getStablecoinHoldingMatch(balances: ReturnType['balances']): TStablecoinHoldingMatch { +function getStablecoinHoldingMatch(balances: ReturnType['balances']): TStablecoinHoldingMatch { return ( Object.entries(balances ?? {}) .flatMap(([chainIDKey, perChain]) => @@ -170,17 +172,13 @@ function getStablecoinHoldingMatch(balances: ReturnType['balan } export function usePortfolioModel(): TPortfolioModel { - const { - cumulatedValueInV2Vaults, - cumulatedValueInV3Vaults, - isLoading: isWalletLoading, - hasCompletedBalanceLoad, - getBalance, - getVaultHoldingsUsd, - balances - } = useWallet() + const { getBalance, balances } = useWalletTokens() + const { getVaultHoldingsUsd } = useWalletHoldings() + const { isLoading: isWalletLoading, hasCompletedBalanceLoad } = useWalletStatus() + const { totalValue: totalPortfolioValue } = useWalletVaultTotals() const { isActive, openLoginModal, isUserConnecting, isIdentityLoading } = useWeb3() - const { vaults, allVaults, isLoadingVaultList, getPrice } = useYearn() + const { vaults, allVaults, isLoadingVaultList } = useYearn() + const { getPrice } = useYearnSpotPrices([{ address: ETH_TOKEN_ADDRESS, chainID: 1 }]) const { listVault: yvUsdVault, unlockedVault: yvUsdUnlockedVault, lockedVault: yvUsdLockedVault } = useYvUsdVaults() const { apyData: yvUsdHistoricalApyData } = useYvUsdCharts() const { shouldHideDust } = useAppSettings() @@ -477,7 +475,6 @@ export function usePortfolioModel(): TPortfolioModel { () => sortedHoldings.some((vault) => getVaultChainID(vault) === KATANA_CHAIN_ID), [sortedHoldings] ) - const totalPortfolioValue = (cumulatedValueInV2Vaults || 0) + (cumulatedValueInV3Vaults || 0) const ethPrice = getPrice({ address: ETH_TOKEN_ADDRESS, chainID: 1 }).normalized const liveBalanceSnapshot = useMemo(() => { diff --git a/src/components/pages/portfolio/hooks/useTokenSuggestions.ts b/src/components/pages/portfolio/hooks/useTokenSuggestions.ts index eb9164a45..4f6e70aa5 100644 --- a/src/components/pages/portfolio/hooks/useTokenSuggestions.ts +++ b/src/components/pages/portfolio/hooks/useTokenSuggestions.ts @@ -1,6 +1,6 @@ import { getEligibleVaults, normalizeSymbol, selectPreferredVault } from '@pages/portfolio/hooks/getEligibleVaults' import { getVaultToken, type TKongVault } from '@pages/vaults/domain/kongVaultSelectors' -import { useWallet } from '@shared/contexts/useWallet' +import { useWalletTokens } from '@shared/contexts/useWallet' import { useYearn } from '@shared/contexts/useYearn' import { getVaultKey } from '@shared/hooks/useVaultFilterUtils' import { useMemo } from 'react' @@ -12,7 +12,7 @@ export type TTokenSuggestion = { } export function useTokenSuggestions(holdingsKeySet: Set): TTokenSuggestion[] { - const { balances } = useWallet() + const { balances } = useWalletTokens() const { vaults } = useYearn() return useMemo(() => { diff --git a/src/components/pages/portfolio/index.tsx b/src/components/pages/portfolio/index.tsx index 0321f337a..4857fa719 100644 --- a/src/components/pages/portfolio/index.tsx +++ b/src/components/pages/portfolio/index.tsx @@ -32,13 +32,11 @@ import { getVaultToken, type TKongVault } from '@pages/vaults/domain/kongVaultSelectors' -import { useClaimMerkleRewards } from '@pages/vaults/hooks/rewards/useClaimMerkleRewards' -import { buildMerkleRewardKey, useMerkleRewards } from '@pages/vaults/hooks/rewards/useMerkleRewards' +import { useMerkleRewards } from '@pages/vaults/hooks/rewards/useMerkleRewards' import { useStakingRewards } from '@pages/vaults/hooks/rewards/useStakingRewards' import type { TPossibleSortBy } from '@pages/vaults/hooks/useSortVaults' import { resolveNextSingleChainSelection } from '@pages/vaults/utils/chainSelection' import { Breadcrumbs } from '@shared/components/Breadcrumbs' -import { Button } from '@shared/components/Button' import { METRIC_VALUE_CLASS, MetricHeader, type TMetricBlock } from '@shared/components/MetricsCard' import { SearchBar } from '@shared/components/SearchBar' import { SwitchChainPrompt } from '@shared/components/SwitchChainPrompt' @@ -46,7 +44,7 @@ import { TokenLogo } from '@shared/components/TokenLogo' import { Tooltip } from '@shared/components/Tooltip' import { YearnLogoSpinner } from '@shared/components/YearnLogoSpinner' import { useNotifications } from '@shared/contexts/useNotifications' -import { useWallet } from '@shared/contexts/useWallet' +import { useWalletActions } from '@shared/contexts/useWallet' import { useWeb3 } from '@shared/contexts/useWeb3' import { useYearn } from '@shared/contexts/useYearn' import { useTokenList } from '@shared/contexts/WithTokenList' @@ -74,20 +72,17 @@ import { formatUSD } from '@shared/utils/format' import { copyToClipboard } from '@shared/utils/helpers' import { PLAUSIBLE_EVENTS } from '@shared/utils/plausible' import { getNetwork } from '@shared/utils/wagmi' +import Link from 'next/link' +import { usePathname, useRouter, useSearchParams } from 'next/navigation' import type { CSSProperties, ReactElement } from 'react' import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { useSearchParams } from 'react-router' -import { useWriteContract } from 'wagmi' -import Link from '/src/components/Link' +import { env } from '@/env' +import Image from '/src/components/Image' import { doesActivityEntryMatchSearch, doesLocalActivityMatchFilters, - formatIndexedActivityDate, - getActivityChainName, - getActivityEntryTitle, isRecentLocalActivityEntry, isZapNotification, - type TActivityModalFilters, toLocalActivityEntry } from './activity.helpers' import type { @@ -122,7 +117,6 @@ const headingTooltipClassName = 'rounded-lg border border-border bg-surface-secondary px-2 py-1 text-xs text-text-primary' const metricTooltipContentClassName = 'flex max-w-[280px] flex-col gap-1 leading-relaxed' const metricCardClassName = 'bg-surface px-5 py-3 md:px-5 md:py-2.5' -const portfolioRewardClaimButtonClassName = 'w-full md:w-[112px]' const PORTFOLIO_TABS = [ { key: 'positions', label: 'Account Overview' }, { key: 'activity', label: 'Activity' }, @@ -159,6 +153,14 @@ type TPortfolioActivityProps = Pick +const ACTIVITY_ACTION_LABELS: Record = { + deposit: 'Deposit', + withdraw: 'Withdraw', + stake: 'Stake', + unstake: 'Unstake', + transfer: 'Transfer', + swap: 'Swap' +} const ACTIVITY_TYPE_FILTERS: Array<{ key: TPortfolioActivityTypeFilter; label: string }> = [ { key: 'all', label: 'All' }, { key: 'deposit', label: 'Deposit' }, @@ -168,7 +170,6 @@ const ACTIVITY_TYPE_FILTERS: Array<{ key: TPortfolioActivityTypeFilter; label: s { key: 'transfer', label: 'Transfer' }, { key: 'swap', label: 'Swap' } ] -const PORTFOLIO_ACTIVITY_PAGE_SIZE = 500 const ACTIVITY_CALENDAR_DAY_LABELS = [ { key: 'sunday', label: 'S' }, { key: 'monday', label: 'M' }, @@ -179,6 +180,11 @@ const ACTIVITY_CALENDAR_DAY_LABELS = [ { key: 'saturday', label: 'S' } ] as const +type TActivityModalFilters = { + types: TPortfolioActivityEntry['action'][] + startDate: string + endDate: string +} type TActivityDateField = 'startDate' | 'endDate' function getTodayDateInputValue(): string { @@ -263,8 +269,20 @@ function getActivityExplorerUrl(chainId: number, txHash: string): string | null return explorerBaseUrl ? `${explorerBaseUrl}/tx/${txHash}` : null } +function getActivityChainName(chainId: number): string { + return SUPPORTED_NETWORKS.find((item) => item.id === chainId)?.name ?? `Chain ${chainId}` +} + function getActivityChainLogoUrl(chainId: number): string { - return `${import.meta.env.VITE_BASE_YEARN_ASSETS_URI}/chains/${chainId}/logo.svg` + return `${env.NEXT_PUBLIC_BASE_YEARN_ASSETS_URI}/chains/${chainId}/logo.svg` +} + +function formatIndexedActivityDate(timestamp: number): string { + return new Date(timestamp * 1000).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric' + }) } function formatIndexedActivityDateTime(timestamp: number): string { @@ -380,6 +398,26 @@ function getEarlierActivityDate(firstDate: string, secondDate: string): string { return firstDate < secondDate ? firstDate : secondDate } +function getActivityEntryTitle(entry: TPortfolioActivityEntry): string { + if (entry.displayType === 'reward_claim') { + return 'Reward Claim' + } + + if (entry.action === 'transfer' && entry.inputTokenAddress && entry.outputTokenAddress) { + return 'Zap' + } + + if (entry.action === 'transfer' && entry.transferDirection === 'in') { + return 'Transfer in' + } + + if (entry.action === 'transfer' && entry.transferDirection === 'out') { + return 'Transfer out' + } + + return ACTIVITY_ACTION_LABELS[entry.action] +} + function getActivityEntryKey(entry: TPortfolioActivityEntry, index: number): string { return [ entry.chainId, @@ -1230,7 +1268,7 @@ function IndexedActivityRow({
- {chainName} {tokenAddress ? ( - {chainName} - groupedReward.rewards.map((reward) => buildMerkleRewardKey(reward.root, reward.token.address)) - ) -} - -function getMerkleRewardTokenCount(groupedRewards: TGroupedMerkleReward[]): number { - return groupedRewards.filter((groupedReward) => groupedReward.totalUnclaimed > 0n).length -} - -function MerkleClaimAllButton({ - chainData, - userAddress, - onStartClaim, - onSwitchChain -}: { - chainData: TChainRewardData - userAddress: `0x${string}` - onStartClaim: (step: TransactionStep, merkleRewardKeys: string[], chainId: number) => void - onSwitchChain: () => void -}): ReactElement | null { - const currentChainId = useChainId() - const { isPending } = useWriteContract() - const merkleRewardTokenCount = getMerkleRewardTokenCount(chainData.merkleRewards) - const isWrongChain = currentChainId !== chainData.chainId - - const { prepare } = useClaimMerkleRewards({ - groupedRewards: chainData.merkleRewards, - userAddress, - chainId: chainData.chainId, - enabled: merkleRewardTokenCount > 1 - }) - - const step = useMemo((): TransactionStep | undefined => { - if (!prepare.isSuccess || !prepare.data?.request) { - return undefined - } - - return { - prepare, - label: 'Claim', - confirmMessage: 'Claim all Merkl rewards', - successTitle: 'Rewards Claimed', - successMessage: 'You claimed all Merkl rewards', - showConfetti: true - } - }, [prepare]) - - const handleClaimAll = useCallback(() => { - if (isWrongChain) { - onSwitchChain() - return - } - if (!step) return - - onStartClaim(step, getMerkleRewardKeys(chainData.merkleRewards), chainData.chainId) - }, [chainData.chainId, chainData.merkleRewards, isWrongChain, onStartClaim, onSwitchChain, step]) - - if (merkleRewardTokenCount < 2) { - return null - } - - return ( -
-
-
- -
-
- ) -} - function PortfolioClaimRewardsSection({ isActive, openLoginModal }: TPortfolioClaimRewardsProps): ReactElement { const { address: userAddress } = useWeb3() const { vaults } = useYearn() @@ -2640,7 +2598,7 @@ function PortfolioClaimRewardsSection({ isActive, openLoginModal }: TPortfolioCl }, [userAddress]) function getChainLogoUrl(chainId: number): string { - return `${import.meta.env.VITE_BASE_YEARN_ASSETS_URI}/chains/${chainId}/logo.svg` + return `${env.NEXT_PUBLIC_BASE_YEARN_ASSETS_URI}/chains/${chainId}/logo.svg` } function renderRewardsContent(): ReactElement { @@ -2666,7 +2624,7 @@ function PortfolioClaimRewardsSection({ isActive, openLoginModal }: TPortfolioCl
{showChainHeader && (
- {chainData.chainName} switchChainAsync({ chainId: chainData.chainId })} - claimButtonClassName={portfolioRewardClaimButtonClassName} /> )) )} @@ -2702,15 +2659,8 @@ function PortfolioClaimRewardsSection({ isActive, openLoginModal }: TPortfolioCl isFirst={idx === 0 && chainData.stakingRewards.length === 0} isAllChainsView={isAllChainsView} onSwitchChain={() => switchChainAsync({ chainId: chainData.chainId })} - claimButtonClassName={portfolioRewardClaimButtonClassName} /> ))} - switchChainAsync({ chainId: chainData.chainId })} - /> {needsSwitchChain(chainData) && (
- {chainData.chainName} + {chainData.chainName} {chainData.chainName} {isSelected && }
@@ -3071,15 +3021,25 @@ function PortfolioPage(): ReactElement { null ) const [historyVaultGrowthMode, setHistoryVaultGrowthMode] = useState('position') - const [searchParams, setSearchParams] = useSearchParams() + const searchParams = useSearchParams() + const pathname = usePathname() || '/portfolio' + const router = useRouter() const varsRef = useRef(null) const breadcrumbsRef = useRef(null) const tabsRef = useRef(null) const historyFetchTimeframe: TPortfolioHistoryTimeframe = historyTimeframe === 'all' ? 'all' : '1y' - const { onRefresh } = useWallet() + const { onRefresh } = useWalletActions() usePortfolioEntryRefresh({ isActive: model.isActive, onRefresh }) + const replaceSearchParams = useCallback( + (nextParams: URLSearchParams): void => { + const query = nextParams.toString() + router.replace(`${pathname}${query ? `?${query}` : ''}`, { scroll: false }) + }, + [pathname, router] + ) + const activeTab = useMemo((): TPortfolioTabKey => { const tabParam = searchParams.get('tab') if (tabParam === 'activity' || tabParam === 'claim-rewards' || tabParam === 'positions') { @@ -3154,15 +3114,15 @@ function PortfolioPage(): ReactElement { } }) } - const nextParams = new URLSearchParams(searchParams) + const nextParams = new URLSearchParams(searchParams.toString()) if (tab === 'positions') { nextParams.delete('tab') } else { nextParams.set('tab', tab) } - setSearchParams(nextParams, { replace: true }) + replaceSearchParams(nextParams) }, - [activeTab, model.hasHoldings, model.isActive, searchParams, setSearchParams, trackEvent] + [activeTab, model.hasHoldings, model.isActive, replaceSearchParams, searchParams, trackEvent] ) useEffect(() => { diff --git a/src/components/pages/vaults/[chainID]/[address].tsx b/src/components/pages/vaults/[chainID]/[address].tsx index 9fd2181fa..0550903a1 100644 --- a/src/components/pages/vaults/[chainID]/[address].tsx +++ b/src/components/pages/vaults/[chainID]/[address].tsx @@ -1,4 +1,3 @@ -import Link from '@components/Link' import { useScrollSpy } from '@hooks/useScrollSpy' import { BottomDrawer } from '@pages/vaults/components/detail/BottomDrawer' import { @@ -47,27 +46,29 @@ import { useYvBtcVaults } from '@pages/vaults/hooks/useYvBtcVaults' import { useYvUsdVaults } from '@pages/vaults/hooks/useYvUsdVaults' import { WidgetActionType } from '@pages/vaults/types' import { getVaultUserHistoryVaults } from '@pages/vaults/utils/vaultUserHistoryVaults' -import { isYvBtcAddress, YVBTC_LOCKED_ADDRESS, YVBTC_UNLOCKED_ADDRESS } from '@pages/vaults/utils/yvBtc' +import { YVBTC_CHAIN_ID, YVBTC_LOCKED_ADDRESS, YVBTC_UNLOCKED_ADDRESS } from '@pages/vaults/utils/yvBtc' import { getYvUsdInfinifiPointsNote, getYvUsdSharePrice, - isYvUsdAddress, type TYvUsdVariant, YVUSD_CHAIN_ID, YVUSD_LOCKED_ADDRESS, YVUSD_UNLOCKED_ADDRESS } from '@pages/vaults/utils/yvUsd' +import { useMediaQuery } from '@react-hookz/web' import { Breadcrumbs } from '@shared/components/Breadcrumbs' -import { JsonLd } from '@shared/components/JsonLd' import { TokenLogo } from '@shared/components/TokenLogo' import { Tooltip } from '@shared/components/Tooltip' -import { useWallet } from '@shared/contexts/useWallet' +import { useWalletActions, useWalletStatus, useWalletTokens } from '@shared/contexts/useWallet' import { useYearn } from '@shared/contexts/useYearn' +import { useYearnSpotPrices } from '@shared/hooks/useYearnSpotPrices' import { IconChevron } from '@shared/icons/IconChevron' import { IconInfo } from '@shared/icons/IconInfo' import { cl, isZeroAddress, toAddress, toNormalizedBN } from '@shared/utils' import { getVaultName } from '@shared/utils/helpers' import type { TKongVaultSnapshot } from '@shared/utils/schemas/kongVaultSnapshotSchema' +import Link from 'next/link' +import { useParams, useRouter, useSearchParams } from 'next/navigation' import type { ReactElement, ReactNode } from 'react' import { cloneElement, @@ -80,9 +81,9 @@ import { useRef, useState } from 'react' -import { useLocation, useNavigate, useParams } from 'react-router' import { formatUnits, isAddressEqual } from 'viem' import { VaultsListChip } from '@/components/pages/vaults/components/list/VaultsListChip' +import { isRouteChainAddressMatch, resolveRouteVaultFromMap } from '@/components/pages/vaults/utils/routeVault' import { deriveListKind } from '@/components/pages/vaults/utils/vaultListFacets' import { getVaultPrimaryLogoSrc } from '@/components/pages/vaults/utils/vaultLogo' import { getCategoryDescription, getProductTypeDescription } from '@/components/pages/vaults/utils/vaultTagCopy' @@ -251,35 +252,6 @@ const splitFirstSentence = (message: string): { title: string; body?: string } = return body ? { title, body } : { title } } -const buildVaultStructuredData = (currentVault: TKongVaultView): Record => { - const annualPercentageRate = Number.isFinite(currentVault.apr.netAPR) - ? { - '@type': 'QuantitativeValue', - value: Number((currentVault.apr.netAPR * 100).toFixed(2)), - unitText: 'PERCENT' - } - : undefined - - return { - '@context': 'https://schema.org', - '@type': 'FinancialProduct', - name: currentVault.name, - description: currentVault.description || `Deposit ${currentVault.token.symbol} to earn automated yield on Yearn.`, - url: `https://yearn.fi/vaults/${currentVault.chainID}/${currentVault.address}`, - ...(annualPercentageRate ? { annualPercentageRate } : {}), - provider: { - '@type': 'Organization', - name: 'Yearn', - url: 'https://yearn.fi' - }, - offers: { - '@type': 'Offer', - category: 'Yield Vault', - description: `Automated yield vault for ${currentVault.token.symbol} on chain ${currentVault.chainID}` - } - } -} - const isSnapshotLikelyV3Vault = (snapshot: TKongVaultSnapshot): boolean => { const apiVersion = snapshot.apiVersion ?? '' if (apiVersion.startsWith('3') || apiVersion.startsWith('~3')) { @@ -437,10 +409,10 @@ function YvUsdMobileKeyMetrics({ onApyVariantChange: (variant: TYvUsdVariant) => void }): ReactElement { const { address } = useWeb3() - const { getPrice } = useYearn() const { metrics, unlockedVault, lockedVault } = useYvUsdVaults() const account = address ? toAddress(address) : undefined const unlockedAssetAddress = toAddress(unlockedVault?.token.address ?? YVUSD_UNLOCKED_ADDRESS) + const { getPrice } = useYearnSpotPrices([{ address: unlockedAssetAddress, chainID: YVUSD_CHAIN_ID }]) const unlockedUserData = useVaultUserData({ vaultAddress: toAddress(unlockedVault?.address ?? YVUSD_UNLOCKED_ADDRESS), @@ -495,11 +467,13 @@ function Index(): ReactElement | null { const { headerDisplayMode } = useDevFlags() const mobileDetailsSectionId = useId() - const params = useParams() - const location = useLocation() - const navigate = useNavigate() + const params = useParams<{ chainID?: string; address?: string }>() + const searchParams = useSearchParams() + const router = useRouter() const chainId = Number(params.chainID) - const { getBalance, onRefresh, isLoading: isWalletLoading } = useWallet() + const { getBalance } = useWalletTokens() + const { onRefresh } = useWalletActions() + const { isLoading: isWalletLoading } = useWalletStatus() const { address } = useWeb3() const { vaults, allVaults, isLoadingVaultList, enableVaultListFetch } = useYearn() const { @@ -515,12 +489,34 @@ function Index(): ReactElement | null { metrics: yvBtcMetrics, isLoading: isLoadingYvBtc } = useYvBtcVaults() - const isYvUsd = isYvUsdAddress(params.address) - const isYvBtc = isYvBtcAddress(params.address) + const isYvUsd = + isRouteChainAddressMatch({ + routeChainId: chainId, + routeAddress: params.address, + expectedChainId: YVUSD_CHAIN_ID, + expectedAddress: YVUSD_UNLOCKED_ADDRESS + }) || + isRouteChainAddressMatch({ + routeChainId: chainId, + routeAddress: params.address, + expectedChainId: YVUSD_CHAIN_ID, + expectedAddress: YVUSD_LOCKED_ADDRESS + }) + const isYvBtc = isRouteChainAddressMatch({ + routeChainId: chainId, + routeAddress: params.address, + expectedChainId: YVBTC_CHAIN_ID, + expectedAddress: YVBTC_UNLOCKED_ADDRESS + }) const isDualVariantVault = isYvUsd || isYvBtc + const isDesktopViewport = useMediaQuery('(min-width: 768px)', { initializeWithValue: false }) + const shouldRenderDesktopCharts = isDesktopViewport === true + const shouldRenderMobileCharts = isDesktopViewport === false const isLockedYvUsdRoute = chainId === YVUSD_CHAIN_ID && params.address ? toAddress(params.address) === YVUSD_LOCKED_ADDRESS : false - const unlockedYvUsdPath = `/vaults/${YVUSD_CHAIN_ID}/${YVUSD_UNLOCKED_ADDRESS}${location.search}${location.hash}` + const currentSearch = searchParams.toString() + const currentHash = typeof window === 'undefined' ? '' : window.location.hash + const unlockedYvUsdPath = `/vaults/${YVUSD_CHAIN_ID}/${YVUSD_UNLOCKED_ADDRESS}${currentSearch ? `?${currentSearch}` : ''}${currentHash}` const vaultKey = `${params.chainID}-${params.address}` const [isMobileDrawerOpen, setIsMobileDrawerOpen] = useState(false) const [mobileDrawerAction, setMobileDrawerAction] = useState(WidgetActionType.Deposit) @@ -590,19 +586,16 @@ function Index(): ReactElement | null { if (!isLockedYvUsdRoute) { return } - navigate(unlockedYvUsdPath, { replace: true }) - }, [isLockedYvUsdRoute, navigate, unlockedYvUsdPath]) + router.replace(unlockedYvUsdPath) + }, [isLockedYvUsdRoute, router, unlockedYvUsdPath]) const baseVault = useMemo(() => { - if (!params.address) return undefined - const resolvedAddress = toAddress(params.address) - return vaults[resolvedAddress] - }, [params.address, vaults]) + return resolveRouteVaultFromMap(vaults, { routeChainId: chainId, routeAddress: params.address }) + }, [chainId, params.address, vaults]) const metadataVault = useMemo(() => { - if (!params.address) return undefined - return allVaults[toAddress(params.address)] - }, [allVaults, params.address]) + return resolveRouteVaultFromMap(allVaults, { routeChainId: chainId, routeAddress: params.address }) + }, [allVaults, chainId, params.address]) const hasVaultList = Object.keys(vaults).length > 0 @@ -621,8 +614,13 @@ function Index(): ReactElement | null { if (isDualVariantVault) return false if (!baseVault?.address && !params.address) return false const resolvedAddress = toAddress(baseVault?.address ?? params.address ?? '0x') - return isAddressEqual(resolvedAddress, YBOLD_VAULT_ADDRESS) - }, [isDualVariantVault, baseVault?.address, params.address]) + return isRouteChainAddressMatch({ + routeChainId: chainId, + routeAddress: resolvedAddress, + expectedChainId: 1, + expectedAddress: YBOLD_VAULT_ADDRESS + }) + }, [isDualVariantVault, baseVault?.address, params.address, chainId]) const yBoldStakingVault = useMemo(() => { if (!isYBold) return undefined @@ -710,6 +708,14 @@ function Index(): ReactElement | null { ? (isYvUsd && (isLoadingYvUsd || shouldBootstrapYvUsdVaultList)) || (isYvBtc && (isLoadingYvBtc || shouldBootstrapYvBtcVaultList)) : !currentVault && (isLoadingSnapshotVault || (isLoadingVaultList && !isSnapshotNotFound)) + const { getPrice: getCurrentVaultAssetPrice } = useYearnSpotPrices([ + { address: currentVault?.token.address, chainID: currentVault?.chainID } + ]) + const currentVaultTokenPrice = currentVault + ? getCurrentVaultAssetPrice({ address: currentVault.token.address, chainID: currentVault.chainID }).normalized || + currentVault.tvl.price || + 0 + : 0 const vaultApyData = useVaultApyData(currentVault ?? EMPTY_VAULT_FOR_APY_DATA) const stakingAddress = !isZeroAddress(currentVault?.staking?.address) ? toAddress(currentVault?.staking?.address) @@ -1127,20 +1133,22 @@ function Index(): ReactElement | null { key: 'charts' as const, shouldRender: Number.isInteger(chainId), ref: sectionRefs.charts, - content: isYvUsd ? ( - - ) : ( - - ) + content: shouldRenderDesktopCharts ? ( + isYvUsd ? ( + + ) : ( + + ) + ) : null }, { key: 'about' as const, @@ -1180,6 +1188,7 @@ function Index(): ReactElement | null { currentVault, isYvUsd, sectionRefs, + shouldRenderDesktopCharts, snapshotVault?.inceptTime, detailUserHistoryVaults, vaultAddresses @@ -1707,168 +1716,298 @@ function Index(): ReactElement | null { } return ( - <> - {currentVault && } -
-
- {isCollapsibleMode ? ( -