From efa0b32c33f203b7de5842519c70b45920394c8c Mon Sep 17 00:00:00 2001 From: w84april Date: Fri, 22 May 2026 13:31:33 +0300 Subject: [PATCH 01/59] feat: init next --- .env.example | 60 +- .husky/pre-commit | 3 + CLAUDE.md | 19 +- README.md | 2 +- api/README.md | 127 -- api/enso/balances.ts | 56 - api/enso/route.ts | 75 - api/enso/status.ts | 8 - api/holdings/activity-facets.ts | 103 -- api/holdings/breakdown.ts | 157 -- api/holdings/pnl/simple-history.ts | 1 - api/holdings/progress.ts | 27 - api/optimization/_lib/cors.ts | 20 - api/optimization/change.ts | 78 - api/optimization/vault-state.ts | 54 - api/server.ts | 1433 ----------------- api/tsconfig.json | 19 - api/vault/meta.ts | 114 -- api/yvusd/aprs.ts | 44 - app/[...notFound]/page.tsx | 5 + app/api/admin/invalidate-cache/route.ts | 4 + app/api/enso/balances/route.ts | 4 + app/api/enso/route/route.ts | 4 + app/api/enso/status/route.ts | 4 + app/api/holdings/activity-facets/route.ts | 4 + app/api/holdings/activity/route.ts | 4 + app/api/holdings/breakdown/route.ts | 4 + app/api/holdings/history/route.ts | 4 + app/api/holdings/pnl/simple-history/route.ts | 4 + app/api/holdings/progress/route.ts | 4 + .../holdings/protocol-return/history/route.ts | 4 + app/api/optimization/alignment/route.ts | 4 + app/api/optimization/change/route.ts | 4 + app/api/optimization/vault-state/route.ts | 4 + app/api/tenderly/fund/route.ts | 4 + app/api/tenderly/increase-time/route.ts | 4 + app/api/tenderly/revert/route.ts | 4 + app/api/tenderly/snapshot/route.ts | 4 + app/api/tenderly/status/route.ts | 4 + app/api/yvusd/aprs/route.ts | 4 + app/icon-list/page-client.tsx | 9 + app/icon-list/page.tsx | 14 + app/layout.tsx | 174 ++ app/metadata.ts | 69 + app/page-client.tsx | 8 + app/page.tsx | 9 + app/portfolio/page-client.tsx | 8 + app/portfolio/page.tsx | 9 + app/v2/[...slug]/page.tsx | 5 + app/v2/page.tsx | 5 + app/v3/[...slug]/page.tsx | 5 + app/v3/[chainID]/[address]/page-client.tsx | 8 + app/v3/[chainID]/[address]/page.tsx | 20 + app/v3/page.tsx | 5 + .../[chainID]/[address]/page-client.tsx | 8 + app/vaults/[chainID]/[address]/page.tsx | 20 + app/vaults/page-client.tsx | 8 + app/vaults/page.tsx | 9 + bun.lock | 702 +++----- index.html | 100 -- next-env.d.ts | 6 + next.config.ts | 75 + package.json | 19 +- scripts/api-runtime.mjs | 771 --------- scripts/api-runtime.test.ts | 332 ---- scripts/check-client-server-boundary.ts | 242 +++ scripts/dev.mjs | 34 - scripts/dev.test.ts | 80 - scripts/ensure-api-server.js | 74 - scripts/preview.mjs | 34 - scripts/preview.test.ts | 42 - scripts/run-local.js | 195 --- scripts/tenderly-vnet-status.ts | 5 +- scripts/tenderly-vnet.test.ts | 10 +- scripts/tenderly-vnet.ts | 8 +- src/App.tsx | 232 ++- src/components/Image.tsx | 1 + src/components/Link.test.tsx | 49 +- src/components/Link.tsx | 14 +- src/components/pages/icon-list/data.ts | 306 ++++ .../pages/icon-list/iconRegistry.ts | 159 ++ src/components/pages/icon-list/index.tsx | 401 +---- src/components/pages/icon-list/sourceUsage.ts | 68 + .../PortfolioHistoryBreakdownModal.tsx | 7 +- src/components/pages/portfolio/index.tsx | 395 ++--- .../vaults/components/SuggestedVaultCard.tsx | 5 +- .../vaults/components/TrendingVaults.tsx | 5 +- .../components/compare/CompareVaultCard.tsx | 5 +- .../components/compare/VaultsCompareModal.tsx | 5 +- .../components/detail/VaultAboutSection.tsx | 3 +- .../components/detail/VaultDetailsHeader.tsx | 3 +- .../components/detail/VaultsListStrategy.tsx | 9 +- .../detail/charts/ChartErrorBoundary.tsx | 3 +- .../vaults/components/list/VaultsListRow.tsx | 3 +- .../components/notifications/Notification.tsx | 33 +- .../components/table/KatanaApyTooltip.tsx | 3 +- .../components/table/VaultForwardAPY.test.tsx | 2 +- .../widget/InputTokenAmount.test.tsx | 2 +- .../components/widget/InputTokenAmount.tsx | 3 +- .../components/widget/TokenSelector.tsx | 3 +- .../components/widget/deposit/index.tsx | 5 +- .../components/widget/migrate/index.tsx | 5 +- .../components/widget/rewards/RewardRow.tsx | 3 +- .../components/widget/tokenLogo.utils.ts | 4 +- .../components/widget/withdraw/index.tsx | 5 +- .../widget/yvUSD/YvUsdWithdraw.helpers.ts | 8 +- .../components/yvUSD/YvUsdHeaderBanner.tsx | 3 +- .../vaults/hooks/solvers/useSolverEnso.ts | 3 +- .../pages/vaults/hooks/useEnsoEnabled.ts | 3 +- .../pages/vaults/hooks/useVaultsPageModel.ts | 3 +- src/components/pages/vaults/utils/kongRest.ts | 8 +- .../pages/vaults/utils/vaultLogo.ts | 5 +- src/components/pages/vaults/utils/yvUsd.ts | 3 +- .../shared/components/HeaderNavMenu.tsx | 16 +- .../shared/components/MetricsCard.tsx | 4 +- .../shared/components/MobileNavMenu.tsx | 16 +- .../shared/components/TokenLogo.tsx | 5 +- .../shared/components/Tooltip.test.tsx | 8 +- .../shared/components/YearnApps.tsx | 17 +- .../shared/components/YearnLogoSpinner.tsx | 3 +- .../shared/contexts/WithTokenList.tsx | 9 +- src/components/shared/contexts/useWallet.tsx | 3 +- src/components/shared/contexts/useYearn.tsx | 5 +- src/components/shared/hooks/useChains.tsx | 4 +- .../shared/hooks/useYDaemonBaseURI.ts | 4 +- src/components/shared/icons/IconLock.tsx | 5 +- src/components/shared/icons/IconLockOpen.tsx | 1 + src/components/shared/utils/clusters.ts | 6 +- src/components/shared/utils/tools.address.ts | 3 +- src/components/shared/utils/wagmi/actions.ts | 5 +- src/components/shared/utils/wagmi/config.ts | 25 +- src/components/shared/utils/wagmi/utils.ts | 13 +- src/config/tenderly.test.ts | 46 +- src/config/tenderly.ts | 15 +- .../config/tenderlyServer.ts | 14 +- src/config/wagmi.ts | 5 +- src/config/wagmiTransports.ts | 3 +- src/contexts/useDevFlags.tsx | 4 +- src/env.ts | 69 + src/hooks/usePlausible.ts | 46 +- src/main.tsx | 55 - src/navigation/NextNavigationProvider.tsx | 33 + src/navigation/context.tsx | 23 + src/navigation/react-router.tsx | 170 ++ src/navigation/url.ts | 25 + src/routes.tsx | 71 - src/server/README.md | 112 ++ src/server/admin/invalidate-cache.ts | 99 ++ src/server/enso/balances.ts | 65 + {api => src/server}/enso/cache.test.ts | 0 {api => src/server}/enso/cache.ts | 0 src/server/enso/route.ts | 84 + src/server/enso/status.ts | 14 + .../server}/holdings/activity-facets.test.ts | 0 src/server/holdings/activity-facets.ts | 131 ++ {api => src/server}/holdings/activity.test.ts | 222 +-- {api => src/server}/holdings/activity.ts | 113 +- .../server}/holdings/breakdown.test.ts | 0 src/server/holdings/breakdown.ts | 136 ++ {api => src/server}/holdings/history.test.ts | 64 +- {api => src/server}/holdings/history.ts | 146 +- .../holdings/pnl/simple-history.test.ts | 47 +- src/server/holdings/pnl/simple-history.ts | 1 + {api => src/server}/holdings/progress.test.ts | 0 src/server/holdings/progress.ts | 23 + .../holdings/protocol-return/history.ts | 122 +- src/server/http.ts | 96 ++ {api => src/server}/lib/aio.test.ts | 0 {api => src/server}/lib/aio.ts | 0 {api => src/server}/lib/cacheHeaders.ts | 0 {api => src/server}/lib/holdings/README.md | 137 +- {api => src/server}/lib/holdings/config.ts | 0 {api => src/server}/lib/holdings/index.ts | 0 .../lib/holdings/services/activity.test.ts | 16 +- .../server}/lib/holdings/services/activity.ts | 0 .../lib/holdings/services/activityFacets.ts | 0 .../services/activityReceiptEnrichment.ts | 2 +- .../lib/holdings/services/aggregator.test.ts | 0 .../lib/holdings/services/aggregator.ts | 0 .../lib/holdings/services/cache.test.ts | 0 .../server}/lib/holdings/services/cache.ts | 30 + .../server}/lib/holdings/services/debug.ts | 0 .../lib/holdings/services/defillama.test.ts | 0 .../lib/holdings/services/defillama.ts | 0 .../lib/holdings/services/graphql.test.ts | 0 .../server}/lib/holdings/services/graphql.ts | 0 .../lib/holdings/services/holdings.test.ts | 0 .../server}/lib/holdings/services/holdings.ts | 0 .../lib/holdings/services/kong.test.ts | 0 .../server}/lib/holdings/services/kong.ts | 0 .../services/nestedVaultPrices.test.ts | 0 .../holdings/services/nestedVaultPrices.ts | 0 .../lib/holdings/services/pnlEvents.ts | 0 .../lib/holdings/services/pnlShared.ts | 0 .../lib/holdings/services/pnlSimple.test.ts | 0 .../lib/holdings/services/pnlSimple.ts | 0 .../services/pnlSimpleHistory.test.ts | 0 .../server}/lib/holdings/services/pnlTypes.ts | 0 .../lib/holdings/services/progress.test.ts | 0 .../server}/lib/holdings/services/progress.ts | 0 .../lib/holdings/services/ratelimit.test.ts | 0 .../lib/holdings/services/ratelimit.ts | 0 .../services/settledHoldingsContext.ts | 0 .../server}/lib/holdings/services/staking.ts | 0 .../lib/holdings/services/vaults.test.ts | 0 .../server}/lib/holdings/services/vaults.ts | 0 .../server}/lib/holdings/storage/redis.ts | 0 {api => src/server}/lib/holdings/types.ts | 0 .../server}/optimization/_lib/assetLogos.ts | 14 - .../server}/optimization/_lib/colors.ts | 0 .../server}/optimization/_lib/envio.ts | 0 .../optimization/_lib/explain-parse.ts | 0 .../server}/optimization/_lib/formatters.ts | 0 .../server}/optimization/_lib/normalize.ts | 0 .../server}/optimization/_lib/redis.test.ts | 0 .../server}/optimization/_lib/redis.ts | 0 .../server}/optimization/_lib/rpc.test.ts | 0 {api => src/server}/optimization/_lib/rpc.ts | 0 .../server}/optimization/_lib/schema.ts | 0 {api => src/server}/optimization/alignment.ts | 43 +- src/server/optimization/change.ts | 75 + .../server}/optimization/handlers.test.ts | 124 +- src/server/optimization/vault-state.ts | 49 + {api => src/server}/sitemap.ts | 0 {api => src/server}/tenderly.helpers.test.ts | 14 +- src/server/tenderly.helpers.ts | 1 + src/server/tenderly/fund.ts | 38 + src/server/tenderly/increase-time.ts | 43 + src/server/tenderly/revert.ts | 31 + src/server/tenderly/rpc.ts | 47 + src/server/tenderly/snapshot.ts | 39 + src/server/tenderly/status.ts | 20 + {api => src/server}/tenderlyAccess.test.ts | 0 {api => src/server}/tenderlyAccess.ts | 7 +- {api => src/server}/vault/markdown.ts | 0 {api => src/server}/vault/vault-markdown.ts | 0 {api => src/server}/vaults/markdown.ts | 0 src/server/yvusd/aprs.ts | 49 + style.css | 10 +- tsconfig.json | 25 +- vercel.json | 67 +- vite.config.ts | 193 --- vitest.config.ts | 7 +- 243 files changed, 4151 insertions(+), 6520 deletions(-) delete mode 100644 api/README.md delete mode 100644 api/enso/balances.ts delete mode 100644 api/enso/route.ts delete mode 100644 api/enso/status.ts delete mode 100644 api/holdings/activity-facets.ts delete mode 100644 api/holdings/breakdown.ts delete mode 100644 api/holdings/pnl/simple-history.ts delete mode 100644 api/holdings/progress.ts delete mode 100644 api/optimization/_lib/cors.ts delete mode 100644 api/optimization/change.ts delete mode 100644 api/optimization/vault-state.ts delete mode 100644 api/server.ts delete mode 100644 api/tsconfig.json delete mode 100644 api/vault/meta.ts delete mode 100644 api/yvusd/aprs.ts create mode 100644 app/[...notFound]/page.tsx create mode 100644 app/api/admin/invalidate-cache/route.ts create mode 100644 app/api/enso/balances/route.ts create mode 100644 app/api/enso/route/route.ts create mode 100644 app/api/enso/status/route.ts create mode 100644 app/api/holdings/activity-facets/route.ts create mode 100644 app/api/holdings/activity/route.ts create mode 100644 app/api/holdings/breakdown/route.ts create mode 100644 app/api/holdings/history/route.ts create mode 100644 app/api/holdings/pnl/simple-history/route.ts create mode 100644 app/api/holdings/progress/route.ts create mode 100644 app/api/holdings/protocol-return/history/route.ts create mode 100644 app/api/optimization/alignment/route.ts create mode 100644 app/api/optimization/change/route.ts create mode 100644 app/api/optimization/vault-state/route.ts create mode 100644 app/api/tenderly/fund/route.ts create mode 100644 app/api/tenderly/increase-time/route.ts create mode 100644 app/api/tenderly/revert/route.ts create mode 100644 app/api/tenderly/snapshot/route.ts create mode 100644 app/api/tenderly/status/route.ts create mode 100644 app/api/yvusd/aprs/route.ts create mode 100644 app/icon-list/page-client.tsx create mode 100644 app/icon-list/page.tsx create mode 100644 app/layout.tsx create mode 100644 app/metadata.ts create mode 100644 app/page-client.tsx create mode 100644 app/page.tsx create mode 100644 app/portfolio/page-client.tsx create mode 100644 app/portfolio/page.tsx create mode 100644 app/v2/[...slug]/page.tsx create mode 100644 app/v2/page.tsx create mode 100644 app/v3/[...slug]/page.tsx create mode 100644 app/v3/[chainID]/[address]/page-client.tsx create mode 100644 app/v3/[chainID]/[address]/page.tsx create mode 100644 app/v3/page.tsx create mode 100644 app/vaults/[chainID]/[address]/page-client.tsx create mode 100644 app/vaults/[chainID]/[address]/page.tsx create mode 100644 app/vaults/page-client.tsx create mode 100644 app/vaults/page.tsx delete mode 100644 index.html create mode 100644 next-env.d.ts create mode 100644 next.config.ts delete mode 100644 scripts/api-runtime.mjs delete mode 100644 scripts/api-runtime.test.ts create mode 100644 scripts/check-client-server-boundary.ts delete mode 100644 scripts/dev.mjs delete mode 100644 scripts/dev.test.ts delete mode 100644 scripts/ensure-api-server.js delete mode 100644 scripts/preview.mjs delete mode 100644 scripts/preview.test.ts delete mode 100644 scripts/run-local.js create mode 100644 src/components/pages/icon-list/data.ts create mode 100644 src/components/pages/icon-list/iconRegistry.ts create mode 100644 src/components/pages/icon-list/sourceUsage.ts rename api/tenderly.helpers.ts => src/config/tenderlyServer.ts (90%) create mode 100644 src/env.ts delete mode 100644 src/main.tsx create mode 100644 src/navigation/NextNavigationProvider.tsx create mode 100644 src/navigation/context.tsx create mode 100644 src/navigation/react-router.tsx create mode 100644 src/navigation/url.ts delete mode 100644 src/routes.tsx create mode 100644 src/server/README.md create mode 100644 src/server/admin/invalidate-cache.ts create mode 100644 src/server/enso/balances.ts rename {api => src/server}/enso/cache.test.ts (100%) rename {api => src/server}/enso/cache.ts (100%) create mode 100644 src/server/enso/route.ts create mode 100644 src/server/enso/status.ts rename {api => src/server}/holdings/activity-facets.test.ts (100%) create mode 100644 src/server/holdings/activity-facets.ts rename {api => src/server}/holdings/activity.test.ts (57%) rename {api => src/server}/holdings/activity.ts (52%) rename {api => src/server}/holdings/breakdown.test.ts (100%) create mode 100644 src/server/holdings/breakdown.ts rename {api => src/server}/holdings/history.test.ts (70%) rename {api => src/server}/holdings/history.ts (66%) rename {api => src/server}/holdings/pnl/simple-history.test.ts (71%) create mode 100644 src/server/holdings/pnl/simple-history.ts rename {api => src/server}/holdings/progress.test.ts (100%) create mode 100644 src/server/holdings/progress.ts rename {api => src/server}/holdings/protocol-return/history.ts (67%) create mode 100644 src/server/http.ts rename {api => src/server}/lib/aio.test.ts (100%) rename {api => src/server}/lib/aio.ts (100%) rename {api => src/server}/lib/cacheHeaders.ts (100%) rename {api => src/server}/lib/holdings/README.md (77%) rename {api => src/server}/lib/holdings/config.ts (100%) rename {api => src/server}/lib/holdings/index.ts (100%) rename {api => src/server}/lib/holdings/services/activity.test.ts (99%) rename {api => src/server}/lib/holdings/services/activity.ts (100%) rename {api => src/server}/lib/holdings/services/activityFacets.ts (100%) rename {api => src/server}/lib/holdings/services/activityReceiptEnrichment.ts (99%) rename {api => src/server}/lib/holdings/services/aggregator.test.ts (100%) rename {api => src/server}/lib/holdings/services/aggregator.ts (100%) rename {api => src/server}/lib/holdings/services/cache.test.ts (100%) rename {api => src/server}/lib/holdings/services/cache.ts (90%) rename {api => src/server}/lib/holdings/services/debug.ts (100%) rename {api => src/server}/lib/holdings/services/defillama.test.ts (100%) rename {api => src/server}/lib/holdings/services/defillama.ts (100%) rename {api => src/server}/lib/holdings/services/graphql.test.ts (100%) rename {api => src/server}/lib/holdings/services/graphql.ts (100%) rename {api => src/server}/lib/holdings/services/holdings.test.ts (100%) rename {api => src/server}/lib/holdings/services/holdings.ts (100%) rename {api => src/server}/lib/holdings/services/kong.test.ts (100%) rename {api => src/server}/lib/holdings/services/kong.ts (100%) rename {api => src/server}/lib/holdings/services/nestedVaultPrices.test.ts (100%) rename {api => src/server}/lib/holdings/services/nestedVaultPrices.ts (100%) rename {api => src/server}/lib/holdings/services/pnlEvents.ts (100%) rename {api => src/server}/lib/holdings/services/pnlShared.ts (100%) rename {api => src/server}/lib/holdings/services/pnlSimple.test.ts (100%) rename {api => src/server}/lib/holdings/services/pnlSimple.ts (100%) rename {api => src/server}/lib/holdings/services/pnlSimpleHistory.test.ts (100%) rename {api => src/server}/lib/holdings/services/pnlTypes.ts (100%) rename {api => src/server}/lib/holdings/services/progress.test.ts (100%) rename {api => src/server}/lib/holdings/services/progress.ts (100%) rename {api => src/server}/lib/holdings/services/ratelimit.test.ts (100%) rename {api => src/server}/lib/holdings/services/ratelimit.ts (100%) rename {api => src/server}/lib/holdings/services/settledHoldingsContext.ts (100%) rename {api => src/server}/lib/holdings/services/staking.ts (100%) rename {api => src/server}/lib/holdings/services/vaults.test.ts (100%) rename {api => src/server}/lib/holdings/services/vaults.ts (100%) rename {api => src/server}/lib/holdings/storage/redis.ts (100%) rename {api => src/server}/lib/holdings/types.ts (100%) rename {api => src/server}/optimization/_lib/assetLogos.ts (96%) rename {api => src/server}/optimization/_lib/colors.ts (100%) rename {api => src/server}/optimization/_lib/envio.ts (100%) rename {api => src/server}/optimization/_lib/explain-parse.ts (100%) rename {api => src/server}/optimization/_lib/formatters.ts (100%) rename {api => src/server}/optimization/_lib/normalize.ts (100%) rename {api => src/server}/optimization/_lib/redis.test.ts (100%) rename {api => src/server}/optimization/_lib/redis.ts (100%) rename {api => src/server}/optimization/_lib/rpc.test.ts (100%) rename {api => src/server}/optimization/_lib/rpc.ts (100%) rename {api => src/server}/optimization/_lib/schema.ts (100%) rename {api => src/server}/optimization/alignment.ts (56%) create mode 100644 src/server/optimization/change.ts rename {api => src/server}/optimization/handlers.test.ts (61%) create mode 100644 src/server/optimization/vault-state.ts rename {api => src/server}/sitemap.ts (100%) rename {api => src/server}/tenderly.helpers.test.ts (88%) create mode 100644 src/server/tenderly.helpers.ts create mode 100644 src/server/tenderly/fund.ts create mode 100644 src/server/tenderly/increase-time.ts create mode 100644 src/server/tenderly/revert.ts create mode 100644 src/server/tenderly/rpc.ts create mode 100644 src/server/tenderly/snapshot.ts create mode 100644 src/server/tenderly/status.ts rename {api => src/server}/tenderlyAccess.test.ts (100%) rename {api => src/server}/tenderlyAccess.ts (84%) rename {api => src/server}/vault/markdown.ts (100%) rename {api => src/server}/vault/vault-markdown.ts (100%) rename {api => src/server}/vaults/markdown.ts (100%) create mode 100644 src/server/yvusd/aprs.ts delete mode 100644 vite.config.ts diff --git a/.env.example b/.env.example index de84436f6..b62028314 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,37 @@ 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= +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= -VITE_CLUSTERS_API_URL=https://api.clusters.xyz/v1 -VITE_CLUSTERS_API_KEY= +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 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/.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..ce610f7de 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 the compatibility layer for existing React Router-style hooks/components while the app runs on Next navigation +- `/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.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.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/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/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/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/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/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/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..2f8de77d2 --- /dev/null +++ b/app/icon-list/page.tsx @@ -0,0 +1,14 @@ +import { buildIconUsageMap } from '@pages/icon-list/sourceUsage' +import { redirect } from 'next/navigation' +import type { ReactElement } from 'react' +import IconListPageClient from './page-client' + +export default async function Page(): Promise<ReactElement> { + if (process.env.NODE_ENV === 'production') { + redirect('/') + } + + const usageMap = await buildIconUsageMap() + + return <IconListPageClient usageMap={usageMap} /> +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 000000000..dff01f323 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,174 @@ +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 App from '@/App' +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 aeonikFono = localFont({ + src: [ + { path: '../public/fonts/AeonikFono-Light.otf', weight: '300', style: 'normal' }, + { path: '../public/fonts/AeonikFono-Regular.otf', weight: '400', style: 'normal' } + ], + variable: '--font-aeonik-fono', + 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 = '/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: { + googleBot: { + index: true, + follow: false + } + }, + 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} ${aeonikFono.variable} ${aeonikMono.variable}`}> + <Script id="yearn-theme-init" strategy="beforeInteractive"> + {themeScript} + </Script> + <Suspense> + <App>{children}</App> + </Suspense> + </body> + </html> + ) +} diff --git a/app/metadata.ts b/app/metadata.ts new file mode 100644 index 000000000..cdc06b82f --- /dev/null +++ b/app/metadata.ts @@ -0,0 +1,69 @@ +import landingManifest from '@shared/data/landing-manifest.json' +import vaultsManifest from '@shared/data/vaults-manifest.json' +import type { Metadata } from 'next' + +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 + } + } +} + +export function buildVaultMetadata(chainID: string, address: string): Metadata { + const isValidChainID = /^\d+$/.test(chainID) + const isValidAddress = /^0x[a-fA-F0-9]{40}$/.test(address) + const canonicalPath = isValidChainID && isValidAddress ? `/vaults/${chainID}/${address}` : '/vaults' + const ogImage = + isValidChainID && isValidAddress + ? `https://og.yearn.fi/api/og/yearn/vault/${chainID}/${address}` + : vaultsManifest.og + const title = 'Yearn Vault' + const description = "Earn yield on your crypto with Yearn's automated vault strategies" + + return { + title, + description, + alternates: { + canonical: canonicalPath + }, + openGraph: { + title, + description, + url: canonicalPath, + images: ogImage ? [ogImage] : undefined, + type: 'website' + }, + twitter: { + card: 'summary_large_image', + title, + description, + images: ogImage ? [ogImage] : undefined + } + } +} + +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..94b39b013 --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,9 @@ +import type { ReactElement } from 'react' +import { landingMetadata } from './metadata' +import HomePageClient from './page-client' + +export const metadata = landingMetadata + +export default function Page(): ReactElement { + return <HomePageClient /> +} 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/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]/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..a64f52f8a --- /dev/null +++ b/app/v3/[chainID]/[address]/page.tsx @@ -0,0 +1,20 @@ +import type { Metadata } from 'next' +import type { ReactElement } from 'react' +import { buildVaultMetadata } from '../../../metadata' +import V3VaultsDetailPageClient from './page-client' + +type TVaultPageProps = { + params: Promise<{ + chainID: string + address: string + }> +} + +export async function generateMetadata({ params }: TVaultPageProps): Promise<Metadata> { + const { chainID, address } = await params + return buildVaultMetadata(chainID, address) +} + +export default function Page(): ReactElement { + return <V3VaultsDetailPageClient /> +} 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/[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..65c0b98de --- /dev/null +++ b/app/vaults/[chainID]/[address]/page.tsx @@ -0,0 +1,20 @@ +import type { Metadata } from 'next' +import type { ReactElement } from 'react' +import { buildVaultMetadata } from '../../../metadata' +import VaultsDetailPageClient from './page-client' + +type TVaultPageProps = { + params: Promise<{ + chainID: string + address: string + }> +} + +export async function generateMetadata({ params }: TVaultPageProps): Promise<Metadata> { + const { chainID, address } = await params + return buildVaultMetadata(chainID, address) +} + +export default function Page(): ReactElement { + return <VaultsDetailPageClient /> +} diff --git a/app/vaults/page-client.tsx b/app/vaults/page-client.tsx new file mode 100644 index 000000000..e93abf8b7 --- /dev/null +++ b/app/vaults/page-client.tsx @@ -0,0 +1,8 @@ +'use client' + +import VaultsPage from '@pages/vaults/index' +import type { ReactElement } from 'react' + +export default function VaultsPageClient(): ReactElement { + return <VaultsPage /> +} diff --git a/app/vaults/page.tsx b/app/vaults/page.tsx new file mode 100644 index 000000000..e71067842 --- /dev/null +++ b/app/vaults/page.tsx @@ -0,0 +1,9 @@ +import type { ReactElement } from 'react' +import { vaultsMetadata } from '../metadata' +import VaultsPageClient from './page-client' + +export const metadata = vaultsMetadata + +export default function Page(): ReactElement { + return <VaultsPageClient /> +} diff --git a/bun.lock b/bun.lock index 354e4f604..182009945 100644 --- a/bun.lock +++ b/bun.lock @@ -11,17 +11,16 @@ "@tanstack/react-query": "5.90.21", "@tanstack/react-virtual": "3.13.24", "@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.13.0", "recharts": "2.15.4", "use-indexeddb": "2.0.2", "vaul": "1.1.2", @@ -37,15 +36,14 @@ "@types/node": "25.3.0", "@types/react": "19.2.14", "@types/react-dom": "19.2.3", - "@vercel/node": "5.6.3", - "@vitejs/plugin-react": "5.1.4", "bump": "0.2.5", + "concurrently": "9.2.1", "husky": "9.1.7", + "jsdom": "29.1.1", "lint-staged": "16.2.7", "postcss": "8.5.14", "tailwindcss": "4.1.18", "typescript": "5.9.3", - "vite": "7.3.2", "vitest": "1.6.1", }, }, @@ -55,46 +53,20 @@ "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], - "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], - - "@babel/compat-data": ["@babel/compat-data@7.29.3", "", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="], - - "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@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-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], - - "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], - - "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + "@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-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + "@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-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + "@asamuzakjp/generational-cache": ["@asamuzakjp/generational-cache@1.0.1", "", {}, "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg=="], - "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + "@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="], - "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], - - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], - - "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="], - - "@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="], - - "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], - - "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], - "@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], - "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], - - "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], - - "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], - "@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.14", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.14", "@biomejs/cli-darwin-x64": "2.4.14", "@biomejs/cli-linux-arm64": "2.4.14", "@biomejs/cli-linux-arm64-musl": "2.4.14", "@biomejs/cli-linux-x64": "2.4.14", "@biomejs/cli-linux-x64-musl": "2.4.14", "@biomejs/cli-win32-arm64": "2.4.14", "@biomejs/cli-win32-x64": "2.4.14" }, "bin": { "biome": "bin/biome" } }, "sha512-TmAvxOEgrpLypzVGJ8FulIZnlyA9TxrO1hyqYrCz9r+bwma9xXxuLA5IuYnj55XQneFx460KjRbx6SWGLkg3bQ=="], @@ -115,75 +87,77 @@ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.14", "", { "os": "win32", "cpu": "x64" }, "sha512-WL0EG5qE+EAKomGXbf2g6VnSKJhTL3tXC0QRzWRwA5VpjxNYa6H4P7ZWfymbGE4IhZZQi1KXQ2R0YjwInmz2fA=="], - "@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=="], + "@csstools/color-helpers": ["@csstools/color-helpers@6.0.2", "", {}, "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q=="], - "@edge-runtime/format": ["@edge-runtime/format@2.2.1", "", {}, "sha512-JQTRVuiusQLNNLe2W9tnzBlV/GvSVcozLl4XZHk5swnRZ/v6jp8TqR8P7sqmJsQqblDZ3EztcWmLDbhRje/+8g=="], + "@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=="], - "@edge-runtime/node-utils": ["@edge-runtime/node-utils@2.3.0", "", {}, "sha512-uUtx8BFoO1hNxtHjp3eqVPC/mWImGb2exOfGjMLUoipuWgjej+f4o/VP4bUI8U40gu7Teogd5VTeZUkGvJSPOQ=="], + "@csstools/css-color-parser": ["@csstools/css-color-parser@4.1.1", "", { "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-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g=="], - "@edge-runtime/ponyfill": ["@edge-runtime/ponyfill@2.4.2", "", {}, "sha512-oN17GjFr69chu6sDLvXxdhg0Qe8EZviGSuqzR9qOiKh4MhFYGdBBcqRNzdmYeAdeRzOW2mM9yil4RftUQ7sUOA=="], + "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="], - "@edge-runtime/primitives": ["@edge-runtime/primitives@4.1.0", "", {}, "sha512-Vw0lbJ2lvRUqc7/soqygUX216Xb8T3WBZ987oywz6aJqRxcwSVWwr9e+Nqo2m9bxobA9mdbWNNoRY6S9eko1EQ=="], + "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.4", "", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw=="], - "@edge-runtime/vm": ["@edge-runtime/vm@3.2.0", "", { "dependencies": { "@edge-runtime/primitives": "4.1.0" } }, "sha512-0dEVyRLM/lG4gp1R/Ik5bfPl/1wX00xFwd5KcNH602tzBa09oF7pbTKETEhR1GjZ75K6OJnYFu8II2dyMhONMw=="], + "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="], - "@emotion/hash": ["@emotion/hash@0.9.2", "", {}, "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="], + "@ecies/ciphers": ["@ecies/ciphers@0.2.6", "", { "peerDependencies": { "@noble/ciphers": "^1.0.0" } }, "sha512-patgsRPKGkhhoBjETV4XxD0En4ui5fbX0hzayqI3M8tvNMGUoUvmyYAIWwlxBc1KX5cturfqByYdj5bYGRpN9g=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A=="], + "@edge-runtime/primitives": ["@edge-runtime/primitives@4.1.0", "", {}, "sha512-Vw0lbJ2lvRUqc7/soqygUX216Xb8T3WBZ987oywz6aJqRxcwSVWwr9e+Nqo2m9bxobA9mdbWNNoRY6S9eko1EQ=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.27.0", "", { "os": "android", "cpu": "arm" }, "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ=="], + "@edge-runtime/vm": ["@edge-runtime/vm@3.2.0", "", { "dependencies": { "@edge-runtime/primitives": "4.1.0" } }, "sha512-0dEVyRLM/lG4gp1R/Ik5bfPl/1wX00xFwd5KcNH602tzBa09oF7pbTKETEhR1GjZ75K6OJnYFu8II2dyMhONMw=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.0", "", { "os": "android", "cpu": "arm64" }, "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ=="], + "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.27.0", "", { "os": "android", "cpu": "x64" }, "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q=="], + "@emotion/hash": ["@emotion/hash@0.9.2", "", {}, "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg=="], + "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g=="], + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw=="], + "@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g=="], + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ=="], + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.0", "", { "os": "linux", "cpu": "x64" }, "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.0", "", { "os": "none", "cpu": "arm64" }, "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.0", "", { "os": "none", "cpu": "x64" }, "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.0", "", { "os": "none", "cpu": "arm64" }, "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA=="], + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg=="], + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ=="], + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.0", "", { "os": "win32", "cpu": "x64" }, "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg=="], + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], "@ethereumjs/common": ["@ethereumjs/common@3.2.0", "", { "dependencies": { "@ethereumjs/util": "^8.1.0", "crc-32": "^1.2.0" } }, "sha512-pksvzI0VyLgmuEF2FA/JR/4/y6hcPq8OUail3/AvycBaW1d5VSauOZzqGvJ3RTmR4MU35lWE8KseKOsEhrFRBA=="], @@ -253,7 +227,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=="], @@ -269,17 +243,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.1", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ=="], + "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], - "@internationalized/number": ["@internationalized/number@3.6.6", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-iFgmQaXHE0vytNfpLZWOC2mEJCBRzcUxt53Xf/yCXG93lRvqas237i3r7X4RKMwO3txiyZD4mQjKAByFv6UGSQ=="], + "@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.8", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-NdbMQUSfXLYIQol5VyMtinm9pZDciiMfN7RtmSuSB78io1hqwJ0naYfxyW6vgxWBkzWymQa/3uLDlbfmshtCaA=="], + "@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=="], - "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], - "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.1", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ=="], + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], - "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + "@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=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], + + "@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.1", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ=="], + + "@internationalized/number": ["@internationalized/number@3.6.6", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-iFgmQaXHE0vytNfpLZWOC2mEJCBRzcUxt53Xf/yCXG93lRvqas237i3r7X4RKMwO3txiyZD4mQjKAByFv6UGSQ=="], + + "@internationalized/string": ["@internationalized/string@3.2.8", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-NdbMQUSfXLYIQol5VyMtinm9pZDciiMfN7RtmSuSB78io1hqwJ0naYfxyW6vgxWBkzWymQa/3uLDlbfmshtCaA=="], "@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], @@ -297,8 +315,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=="], @@ -327,17 +343,29 @@ "@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=="], - "@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], + "@next/env": ["@next/env@16.2.6", "", {}, "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw=="], - "@noble/curves": ["@noble/curves@1.9.1", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA=="], + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg=="], - "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ=="], - "@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=="], + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w=="], - "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA=="], - "@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=="], + "@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=="], "@paulmillr/qr": ["@paulmillr/qr@0.2.1", "", {}, "sha512-IHnV6A+zxU7XwmKFinmYjUcwlyK9+xkG3/s9KcQhI9BjQKycrJ1JRO+FbNYPwZiPKW3je/DR0k7w8/gLa5eaxQ=="], @@ -389,8 +417,6 @@ "@react-types/shared": ["@react-types/shared@3.34.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-gp6xo/s2lX54AlTjOiqwDnxA7UW79BNvI9dB9pr3LZTzRKCd1ZA+ZbgKw/ReIiWuvvVw/8QFJpnqeeFyLocMcQ=="], - "@renovatebot/pep440": ["@renovatebot/pep440@4.2.1", "", {}, "sha512-2FK1hF93Fuf1laSdfiEmJvSJPVIDHEUTz68D3Fi9s0IZrrpaEcj6pTFBTbYvsgC5du4ogrtf5re7yMMvrKNgkw=="], - "@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=="], "@reown/appkit-common": ["@reown/appkit-common@1.7.8", "", { "dependencies": { "big.js": "6.2.2", "dayjs": "1.11.13", "viem": ">=2.29.0" } }, "sha512-ridIhc/x6JOp7KbDdwGKY4zwf8/iK8EYBl+HtWrruutSLwZyVi5P8WaZa+8iajL6LcDcDF7LoyLwMTym7SRuwQ=="], @@ -409,10 +435,6 @@ "@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.3.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-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.4", "", { "os": "android", "cpu": "arm" }, "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ=="], "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.4", "", { "os": "android", "cpu": "arm64" }, "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw=="], @@ -479,7 +501,7 @@ "@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="], - "@swc/helpers": ["@swc/helpers@0.5.21", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg=="], + "@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=="], @@ -523,18 +545,8 @@ "@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=="], - "@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/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], @@ -555,14 +567,12 @@ "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], "@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=="], @@ -593,22 +603,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@1.6.1", "", { "dependencies": { "@vitest/spy": "1.6.1", "@vitest/utils": "1.6.1", "chai": "^4.3.10" } }, "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog=="], "@vitest/runner": ["@vitest/runner@1.6.1", "", { "dependencies": { "@vitest/utils": "1.6.1", "p-limit": "^5.0.0", "pathe": "^1.1.1" } }, "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA=="], @@ -667,44 +661,30 @@ "@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=="], - "acorn-walk": ["acorn-walk@8.3.5", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw=="], "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=="], - "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "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@1.1.0", "", {}, "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw=="], - "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=="], @@ -721,9 +701,9 @@ "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=="], @@ -735,8 +715,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=="], @@ -763,6 +741,8 @@ "chai": ["chai@4.5.0", "", { "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", "deep-eql": "^4.1.3", "get-func-name": "^2.0.2", "loupe": "^2.3.6", "pathval": "^1.1.1", "type-detect": "^4.1.0" } }, "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw=="], + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], @@ -775,19 +755,15 @@ "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=="], - "cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], + "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], - "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - "code-block-writer": ["code-block-writer@10.1.1", "", {}, "sha512-67ueh2IRGst/51p0n6FvPrnRjAGHY5F8xdjkgrYE7DDzpJe6qA07RYQ9VcoUeo5ATOjSOiWpSL3SWBRRbempMw=="], + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], @@ -799,18 +775,10 @@ "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "concurrently": ["concurrently@9.2.1", "", { "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", "shell-quote": "1.8.3", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng=="], "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], - "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 +791,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 +821,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 +831,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=="], @@ -907,13 +881,9 @@ "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.357", "", {}, "sha512-NHlTIQDK8fmVwHwuIzmXYEJ1Ewq3D9wDNc0cWXxDGysP6Pb21giwGNkxiTifyKy/4SoPuN5l6GLP1W9Sv7zB2g=="], - "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=="], + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "encode-utf8": ["encode-utf8@1.0.3", "", {}, "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw=="], @@ -925,27 +895,25 @@ "enhanced-resolve": ["enhanced-resolve@5.21.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-QyL119InA+XXEkNLNTPCXPugSvOfhwv0JOlGNzvxs0hZaiHLNvXSpudUWsOlsXGWJh8G6ckCScEkVHfX3kw/2Q=="], + "entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="], + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "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-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], "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=="], + "esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], "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=="], @@ -975,18 +943,10 @@ "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=="], @@ -997,16 +957,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=="], "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=="], @@ -1021,12 +977,6 @@ "get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="], - "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=="], @@ -1037,6 +987,8 @@ "h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="], + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], @@ -1055,9 +1007,9 @@ "hono": ["hono@4.12.19", "", {}, "sha512-xa3eYXYXx68XTT4hZ7dRzsXBhaq85ToSrlUJNoR0gwz/1Ap/CNwX47wfvV7pc/xWhjKVVkLT7zBJy8chhNguqQ=="], - "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=="], "human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="], @@ -1085,20 +1037,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-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], "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=="], @@ -1119,22 +1069,12 @@ "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], - "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=="], @@ -1187,7 +1127,7 @@ "loupe": ["loupe@2.3.7", "", { "dependencies": { "get-func-name": "^2.0.1" } }, "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA=="], - "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "lru-cache": ["lru-cache@11.4.0", "", {}, "sha512-W+R+kFL4HgVxONq2bhXPi3bGpzGe/yEhVOp233qw9wCRtgncJ15P3bC+e4zZMu4Cq7d+WAJjXGW0uUkifhcatA=="], "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], @@ -1211,12 +1151,12 @@ "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], + "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], + "media-query-parser": ["media-query-parser@2.0.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" } }, "sha512-1N4qp+jE0pL5Xv4uEcwVUhIkwdUO3S/9gML90nqKA7v7FcOS5vUtatfzok9S9U1EJU8dHWlcv95WLnKmmxZI9w=="], "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], - "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], - "micro-ftch": ["micro-ftch@0.3.1", "", {}, "sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg=="], "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], @@ -1263,10 +1203,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-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], @@ -1277,14 +1213,8 @@ "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=="], - "mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="], "modern-ahocorasick": ["modern-ahocorasick@1.1.0", "", {}, "sha512-sEKPVl2rM+MNVkGQt3ChdmD8YsigmXdn5NifZn6jiwn9LRJpWm8F3guhaqrJT/JOat6pwpbXEk6kv+b9DMIjsQ=="], @@ -1293,8 +1223,6 @@ "motion-utils": ["motion-utils@12.36.0", "", {}, "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg=="], - "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], @@ -1303,9 +1231,11 @@ "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + "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=="], @@ -1313,10 +1243,6 @@ "node-mock-http": ["node-mock-http@1.0.4", "", {}, "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ=="], - "node-releases": ["node-releases@2.0.44", "", {}, "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ=="], - - "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=="], "npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="], @@ -1327,8 +1253,6 @@ "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=="], @@ -1349,27 +1273,19 @@ "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-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - "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@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], "pathval": ["pathval@1.1.1", "", {}, "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="], @@ -1381,8 +1297,6 @@ "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=="], - "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], "pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="], @@ -1399,8 +1313,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=="], @@ -1421,8 +1333,6 @@ "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=="], @@ -1439,16 +1349,12 @@ "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.13.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw=="], - "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.46.0", "", { "dependencies": { "@internationalized/date": "^3.12.1", "@internationalized/number": "^3.6.6", "@internationalized/string": "^3.2.8", "@react-types/shared": "^3.34.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-OdxhWvHgs2L4OJGIs7hnuTr5WjjMM6enhNEAMRqiekhF8+ITvA2LRwNftOZwcogaoCslGYq5S2VQTQwnm0GbCA=="], @@ -1477,19 +1383,13 @@ "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.60.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.4", "@rollup/rollup-android-arm64": "4.60.4", "@rollup/rollup-darwin-arm64": "4.60.4", "@rollup/rollup-darwin-x64": "4.60.4", "@rollup/rollup-freebsd-arm64": "4.60.4", "@rollup/rollup-freebsd-x64": "4.60.4", "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", "@rollup/rollup-linux-arm-musleabihf": "4.60.4", "@rollup/rollup-linux-arm64-gnu": "4.60.4", "@rollup/rollup-linux-arm64-musl": "4.60.4", "@rollup/rollup-linux-loong64-gnu": "4.60.4", "@rollup/rollup-linux-loong64-musl": "4.60.4", "@rollup/rollup-linux-ppc64-gnu": "4.60.4", "@rollup/rollup-linux-ppc64-musl": "4.60.4", "@rollup/rollup-linux-riscv64-gnu": "4.60.4", "@rollup/rollup-linux-riscv64-musl": "4.60.4", "@rollup/rollup-linux-s390x-gnu": "4.60.4", "@rollup/rollup-linux-x64-gnu": "4.60.4", "@rollup/rollup-linux-x64-musl": "4.60.4", "@rollup/rollup-openbsd-x64": "4.60.4", "@rollup/rollup-openharmony-arm64": "4.60.4", "@rollup/rollup-win32-arm64-msvc": "4.60.4", "@rollup/rollup-win32-ia32-msvc": "4.60.4", "@rollup/rollup-win32-x64-gnu": "4.60.4", "@rollup/rollup-win32-x64-msvc": "4.60.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g=="], - "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], @@ -1497,32 +1397,34 @@ "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.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], "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=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], + "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=="], @@ -1547,13 +1449,13 @@ "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], - "string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="], + "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=="], "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], - "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="], @@ -1563,48 +1465,50 @@ "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=="], + "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + + "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], + "tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="], "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.15", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ=="], - "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=="], - "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], - "tinypool": ["tinypool@0.8.4", "", {}, "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ=="], "tinyspy": ["tinyspy@2.2.1", "", {}, "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A=="], + "tldts": ["tldts@7.0.30", "", { "dependencies": { "tldts-core": "^7.0.30" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw=="], + + "tldts-core": ["tldts-core@7.0.30", "", {}, "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q=="], + "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=="], - "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], + "tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="], - "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], - "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=="], + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], - "ts-toolbelt": ["ts-toolbelt@6.15.5", "", {}, "sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A=="], + "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], "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=="], - "type-detect": ["type-detect@4.1.0", "", {}, "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw=="], "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=="], @@ -1621,7 +1525,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.25.0", "", {}, "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ=="], "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], @@ -1637,14 +1541,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=="], @@ -1673,19 +1571,23 @@ "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.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": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], "vite-node": ["vite-node@1.6.1", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.3.4", "pathe": "^1.1.1", "picocolors": "^1.0.0", "vite": "^5.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA=="], "vitest": ["vitest@1.6.1", "", { "dependencies": { "@vitest/expect": "1.6.1", "@vitest/runner": "1.6.1", "@vitest/snapshot": "1.6.1", "@vitest/spy": "1.6.1", "@vitest/utils": "1.6.1", "acorn-walk": "^8.3.2", "chai": "^4.3.10", "debug": "^4.3.4", "execa": "^8.0.1", "local-pkg": "^0.5.0", "magic-string": "^0.30.5", "pathe": "^1.1.1", "picocolors": "^1.0.0", "std-env": "^3.5.0", "strip-literal": "^2.0.0", "tinybench": "^2.5.1", "tinypool": "^0.8.3", "vite": "^5.0.0", "vite-node": "1.6.1", "why-is-node-running": "^2.2.2" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", "@vitest/browser": "1.6.1", "@vitest/ui": "1.6.1", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag=="], + "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": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], @@ -1701,19 +1603,21 @@ "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=="], + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], "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=="], + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], - "yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], "yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="], @@ -1725,8 +1629,6 @@ "@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "@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=="], @@ -1751,9 +1653,11 @@ "@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=="], + "@internationalized/date/@swc/helpers": ["@swc/helpers@0.5.21", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg=="], + + "@internationalized/number/@swc/helpers": ["@swc/helpers@0.5.21", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg=="], - "@mapbox/node-pre-gyp/semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], + "@internationalized/string/@swc/helpers": ["@swc/helpers@0.5.21", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg=="], "@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=="], @@ -1775,10 +1679,12 @@ "@metamask/sdk-communication-layer/debug": ["debug@4.3.4", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="], - "@metamask/utils/semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], - "@metamask/utils/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], + "@react-aria/focus/@swc/helpers": ["@swc/helpers@0.5.21", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg=="], + + "@react-aria/interactions/@swc/helpers": ["@swc/helpers@0.5.21", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg=="], + "@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=="], "@reown/appkit/@walletconnect/universal-provider": ["@walletconnect/universal-provider@2.21.0", "", { "dependencies": { "@walletconnect/events": "1.0.1", "@walletconnect/jsonrpc-http-connection": "1.0.8", "@walletconnect/jsonrpc-provider": "1.0.14", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "2.1.2", "@walletconnect/sign-client": "2.21.0", "@walletconnect/types": "2.21.0", "@walletconnect/utils": "2.21.0", "es-toolkit": "1.33.0", "events": "3.3.0" } }, "sha512-mtUQvewt+X0VBQay/xOJBvxsB3Xsm1lTwFjZ6WUwSOTR1X+FNb71hSApnV5kbsdDIpYPXeQUbGt2se1n5E5UBg=="], @@ -1789,8 +1695,6 @@ "@reown/appkit-wallet/zod": ["zod@3.22.4", "", {}, "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg=="], - "@rollup/pluginutils/@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], - "@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/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], @@ -1803,20 +1707,10 @@ "@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=="], - - "@types/estree-jsx/@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], - - "@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=="], + "@vanilla-extract/css/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "@vitest/snapshot/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], - "@vitest/utils/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - "@vitest/utils/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], "@wagmi/core/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], @@ -1853,28 +1747,20 @@ "@walletconnect/window-metadata/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], - "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - "cbw-sdk/clsx": ["clsx@1.2.1", "", {}, "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="], "cbw-sdk/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], "cbw-sdk/preact": ["preact@10.29.2", "", {}, "sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ=="], - "cliui/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=="], - - "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "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=="], + "cli-truncate/string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="], - "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=="], + "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], "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=="], "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=="], @@ -1889,22 +1775,22 @@ "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=="], - "execa/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - - "hast-util-to-jsx-runtime/@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], - "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=="], "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=="], - "loose-envify/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "log-update/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], - "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + "loose-envify/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "mlly/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "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=="], + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], "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=="], @@ -1915,8 +1801,6 @@ "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], - "path-scurry/lru-cache": ["lru-cache@11.4.0", "", {}, "sha512-W+R+kFL4HgVxONq2bhXPi3bGpzGe/yEhVOp233qw9wCRtgncJ15P3bC+e4zZMu4Cq7d+WAJjXGW0uUkifhcatA=="], - "pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "porto/idb-keyval": ["idb-keyval@6.2.2", "", {}, "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg=="], @@ -1925,37 +1809,37 @@ "porto/zustand": ["zustand@5.0.13", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ=="], + "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + "pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "qrcode/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=="], + + "react-aria/@swc/helpers": ["@swc/helpers@0.5.21", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg=="], + + "react-stately/@swc/helpers": ["@swc/helpers@0.5.21", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg=="], + "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], - "restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "rollup/@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - "strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], "to-buffer/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], - "unstorage/lru-cache": ["lru-cache@11.4.0", "", {}, "sha512-W+R+kFL4HgVxONq2bhXPi3bGpzGe/yEhVOp233qw9wCRtgncJ15P3bC+e4zZMu4Cq7d+WAJjXGW0uUkifhcatA=="], - "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=="], - "vite-node/vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], - - "vitest/vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], - "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=="], "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], - "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=="], + "wrap-ansi/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], "@base-org/account/ox/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], @@ -1969,22 +1853,14 @@ "@metamask/eth-json-rpc-provider/@metamask/json-rpc-engine/@metamask/utils": ["@metamask/utils@8.5.0", "", { "dependencies": { "@ethereumjs/tx": "^4.2.0", "@metamask/superstruct": "^3.0.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-I6bkduevXb72TIM9q2LRO63JSsF9EXduh3sBr9oybNX2hNNpr/j1tEjXrsG0Uabm4MJ1xkGAQEMwifvKZIkyxQ=="], - "@metamask/eth-json-rpc-provider/@metamask/utils/semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], - "@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/semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], - "@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/semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], - "@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/semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], - "@metamask/providers/@metamask/utils/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], "@metamask/sdk-communication-layer/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], @@ -2007,13 +1883,9 @@ "@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.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], + "@vitest/snapshot/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - "@vercel/node/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], - - "@vitest/utils/estree-walker/@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], - - "@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=="], + "@vitest/utils/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], "@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=="], @@ -2027,13 +1899,7 @@ "@walletconnect/utils/viem/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], - "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "cliui/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - - "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "eth-block-tracker/@metamask/utils/semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], + "cli-truncate/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], "ethereum-cryptography/@scure/bip32/@scure/base": ["@scure/base@1.1.9", "", {}, "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg=="], @@ -2041,34 +1907,36 @@ "log-update/slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "log-update/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], + + "log-update/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "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=="], "obj-multiplex/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], "porto/ox/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], - "vite-node/vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + "qrcode/yargs/cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], - "vitest/vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + "qrcode/yargs/y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="], - "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "qrcode/yargs/yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], - "yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], - "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "@metamask/eth-json-rpc-provider/@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/eth-json-rpc-provider/@metamask/json-rpc-engine/@metamask/utils/semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], - "@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/json-rpc-engine/@metamask/rpc-errors/@metamask/utils/semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], - "@metamask/json-rpc-engine/@metamask/rpc-errors/@metamask/utils/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], - "@metamask/providers/@metamask/rpc-errors/@metamask/utils/semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], - "@metamask/providers/@metamask/rpc-errors/@metamask/utils/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], "@reown/appkit-controllers/@walletconnect/universal-provider/@walletconnect/sign-client/@walletconnect/core": ["@walletconnect/core@2.21.0", "", { "dependencies": { "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-provider": "1.0.14", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/jsonrpc-ws-connection": "1.0.16", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "2.1.2", "@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/utils": "2.21.0", "@walletconnect/window-getters": "1.0.1", "es-toolkit": "1.33.0", "events": "3.3.0", "uint8arrays": "3.1.0" } }, "sha512-o6R7Ua4myxR8aRUAJ1z3gT9nM+jd2B2mfamu6arzy1Cc6vi10fIwFWb6vg3bC8xJ6o9H3n/cN5TOW3aA9Y1XVw=="], @@ -2101,8 +1969,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=="], @@ -2115,99 +1981,9 @@ "@walletconnect/utils/viem/ox/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], - "vite-node/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], - - "vite-node/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], - - "vite-node/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], - - "vite-node/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], - - "vite-node/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], - - "vite-node/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], - - "vite-node/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], - - "vite-node/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], - - "vite-node/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], - - "vite-node/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], - - "vite-node/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], - - "vite-node/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], - - "vite-node/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], - - "vite-node/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], - - "vite-node/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], - - "vite-node/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], - - "vite-node/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], - - "vite-node/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], - - "vite-node/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], - - "vite-node/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], - - "vite-node/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], - - "vite-node/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], - - "vite-node/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], - - "vitest/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], - - "vitest/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], - - "vitest/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], - - "vitest/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], - - "vitest/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], - - "vitest/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], - - "vitest/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], - - "vitest/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], - - "vitest/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], - - "vitest/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], - - "vitest/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], - - "vitest/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], - - "vitest/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], - - "vitest/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], - - "vitest/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], - - "vitest/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], - - "vitest/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], - - "vitest/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], - - "vitest/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], - - "vitest/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], - - "vitest/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], - - "vitest/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], - - "vitest/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + "cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - "@metamask/eth-json-rpc-provider/@metamask/json-rpc-engine/@metamask/rpc-errors/@metamask/utils/semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], + "qrcode/yargs/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=="], "@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=="], diff --git a/index.html b/index.html deleted file mode 100644 index ce5d40c77..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/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/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..9edff1c7c --- /dev/null +++ b/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +import "./.next/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..2c81466d7 --- /dev/null +++ b/next.config.ts @@ -0,0 +1,75 @@ +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: { + 'react-router': './src/navigation/react-router.tsx', + '@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 dd2b6359f..e6655b8c9 100644 --- a/package.json +++ b/package.json @@ -2,17 +2,16 @@ "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 .", + "check:client-server-boundary": "bun scripts/check-client-server-boundary.ts", "tslint": "tsc -p tsconfig.json --noEmit", "bump": "bump", "test": "vitest run", @@ -32,12 +31,12 @@ "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.13.0", "recharts": "2.15.4", "use-indexeddb": "2.0.2", "vaul": "1.1.2", @@ -53,15 +52,13 @@ "@types/node": "25.3.0", "@types/react": "19.2.14", "@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", "postcss": "8.5.14", "tailwindcss": "4.1.18", "typescript": "5.9.3", - "vite": "7.3.2", "vitest": "1.6.1" } } 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..34f82b65b --- /dev/null +++ b/scripts/check-client-server-boundary.ts @@ -0,0 +1,242 @@ +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 + +const exactAliases: Record = { + 'react-router': 'src/navigation/react-router.tsx' +} + +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 in exactAliases) { + return resolveCandidate(path.join(PROJECT_ROOT, exactAliases[specifier])) + } + + 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/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..3218508e8 100644 --- a/scripts/tenderly-vnet-status.ts +++ b/scripts/tenderly-vnet-status.ts @@ -2,7 +2,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 +662,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) 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..85782a3ed 100644 --- a/scripts/tenderly-vnet.ts +++ b/scripts/tenderly-vnet.ts @@ -406,11 +406,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') } diff --git a/src/App.tsx b/src/App.tsx index 987423c36..82e7d592a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,12 +1,11 @@ +'use client' + 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' @@ -16,172 +15,139 @@ 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 type { ReactElement, ReactNode } from 'react' +import { useEffect, useLayoutEffect, useState } 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' +import { usePathname } from '@/hooks/usePathname' +import { initializePlausible } from '@/hooks/usePlausible' +import { NextNavigationProvider } from '@/navigation/NextNavigationProvider' +import { disableServiceWorkerDev } from '@/utils/disableServiceWorkerDev' -const queryClient = new QueryClient() +const bigintPrototype = BigInt.prototype as unknown as { toJSON?: () => string } +if (!bigintPrototype.toJSON) { + bigintPrototype.toJSON = function toJSON() { + return this.toString() + } +} -function WithLayout(): ReactElement { +function WithLayout({ children }: { children: ReactNode }): ReactElement { return ( <>
-
- -
+
{children}
) } -function App(): ReactElement { - const location = useLocation() - const { manifest } = useCurrentApp() +function AppContent({ children }: { children: ReactNode }): ReactElement { + const [queryClient] = useState(() => new QueryClient()) + const pathname = usePathname() // Scroll to top on route change useLayoutEffect(() => { window.scrollTo(0, 0) - }, [location.pathname]) + }, [pathname]) - // Determine dynamic meta for vault detail pages - const asPath = location.pathname + useEffect(() => { + initializePlausible() - // 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/og.png' - const pageUri = isVaultDetailPage ? `https://yearn.fi${asPath}` : manifest.uri || 'https://yearn.fi' + if (process.env.NODE_ENV === 'development') { + void disableServiceWorkerDev() + } + }, []) 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%' }} /> - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - , - 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%' }} - /> -
-
- + + ) +} + +function App({ children }: { children: ReactNode }): ReactElement { + return ( + + {children} + ) } 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' } - } + const html = renderToStaticMarkup(Example) - 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 - } + expect(html).toContain('href="https://example.com"') + expect(html).toContain('target="_blank"') + expect(html).toContain('rel="noopener noreferrer"') }) 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' } - } + const html = renderToStaticMarkup( + + Vaults + + ) + + expect(html).toContain('href="/vaults"') + expect(html).not.toContain('target="_blank"') + }) - try { - const html = renderToStaticMarkup( - - Vaults - - ) + it('normalizes first-party absolute URLs for stable SSR', () => { + const html = renderToStaticMarkup( + + Yearn V3 + + ) - expect(html).toContain('href="/vaults"') - expect(html).not.toContain('target="_blank"') - } finally { - ;(globalThis as unknown as { window: unknown }).window = originalWindow - } + expect(html).toContain('href="/v3"') + expect(html).not.toContain('target="_blank"') }) }) diff --git a/src/components/Link.tsx b/src/components/Link.tsx index e74e537e3..673e8ba98 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -1,6 +1,7 @@ import type { ReactNode } from 'react' import type { LinkProps as RouterLinkProps } from 'react-router' import { Link as RouterLink } from 'react-router' +import { resolveLinkTarget } from '@/navigation/url' export type LinkProps = { href?: string @@ -12,23 +13,14 @@ export type LinkProps = { 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 || '' + const { href: url, isExternal } = resolveLinkTarget(href || to || '') // Check if it's an external link - if (isExternalLink(url)) { + if (isExternal) { return ( & { + 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/portfolio/components/PortfolioHistoryBreakdownModal.tsx b/src/components/pages/portfolio/components/PortfolioHistoryBreakdownModal.tsx index 54c36090f..e9428bdc0 100644 --- a/src/components/pages/portfolio/components/PortfolioHistoryBreakdownModal.tsx +++ b/src/components/pages/portfolio/components/PortfolioHistoryBreakdownModal.tsx @@ -15,6 +15,7 @@ import { cl, formatUSD, SUPPORTED_NETWORKS, toAddress } from '@shared/utils' import type { ReactElement } from 'react' import { Fragment, useEffect, useMemo, useState } from 'react' import { Link } from 'react-router' +import { env } from '@/env' import { usePortfolioBreakdown } from '../hooks/usePortfolioBreakdown' import type { TPortfolioBreakdownResponse, TPortfolioBreakdownVault } from '../types/api' @@ -159,8 +160,8 @@ export function PortfolioHistoryBreakdownModal({ const currentVault = allVaults[normalizedVaultAddress] as TKongVaultInput | undefined const fallbackTokenAddress = vault.metadata?.tokenAddress const fallbackLogoSrc = fallbackTokenAddress - ? `${import.meta.env.VITE_BASE_YEARN_ASSETS_URI}/tokens/${vault.chainId}/${toAddress(fallbackTokenAddress).toLowerCase()}/logo-128.png` - : `${import.meta.env.VITE_BASE_YEARN_ASSETS_URI}/tokens/${vault.chainId}/${normalizedVaultAddress.toLowerCase()}/logo-128.png` + ? `${env.NEXT_PUBLIC_BASE_YEARN_ASSETS_URI}/tokens/${vault.chainId}/${toAddress(fallbackTokenAddress).toLowerCase()}/logo-128.png` + : `${env.NEXT_PUBLIC_BASE_YEARN_ASSETS_URI}/tokens/${vault.chainId}/${normalizedVaultAddress.toLowerCase()}/logo-128.png` return { chainId: vault.chainId, @@ -171,7 +172,7 @@ export function PortfolioHistoryBreakdownModal({ displaySymbol: currentVault ? getVaultSymbol(currentVault) : vault.metadata?.symbol || 'Unknown', logoSrc: currentVault ? getVaultPrimaryLogoSrc(currentVault) : fallbackLogoSrc, altLogoSrc: currentVault - ? `${import.meta.env.VITE_BASE_YEARN_ASSETS_URI}/tokens/${getVaultChainID(currentVault)}/${toAddress(getVaultToken(currentVault).address).toLowerCase()}/logo-128.png` + ? `${env.NEXT_PUBLIC_BASE_YEARN_ASSETS_URI}/tokens/${getVaultChainID(currentVault)}/${toAddress(getVaultToken(currentVault).address).toLowerCase()}/logo-128.png` : undefined, usdValue: vault.usdValue, status: vault.status diff --git a/src/components/pages/portfolio/index.tsx b/src/components/pages/portfolio/index.tsx index 2f8a08d0e..b1db2b459 100644 --- a/src/components/pages/portfolio/index.tsx +++ b/src/components/pages/portfolio/index.tsx @@ -33,13 +33,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' @@ -78,19 +76,9 @@ import { getNetwork } from '@shared/utils/wagmi' 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 { env } from '@/env' +import Image from '/src/components/Image' import Link from '/src/components/Link' -import { - doesActivityEntryMatchSearch, - doesLocalActivityMatchFilters, - formatIndexedActivityDate, - getActivityChainName, - getActivityEntryTitle, - isRecentLocalActivityEntry, - isZapNotification, - type TActivityModalFilters, - toLocalActivityEntry -} from './activity.helpers' import type { TGrowthDisplayMode, TPortfolioHistoryChartTab, @@ -123,7 +111,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' }, @@ -160,6 +147,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' }, @@ -169,7 +164,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' }, @@ -180,6 +174,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 { @@ -264,8 +263,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 { @@ -381,6 +392,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, @@ -1216,7 +1247,7 @@ function IndexedActivityRow({

- {chainName} {tokenAddress ? ( - {chainName} (b.timeFinished ?? 0) - (a.timeFinished ?? 0)), [cachedEntries] ) - const indexedTxHashes = useMemo( - () => new Set(indexedEntries.map((entry) => entry.txHash.toLowerCase())), - [indexedEntries] - ) - const recentLocalEntries = useMemo( - () => - cachedEntries - .filter((entry) => isRecentLocalActivityEntry(entry, indexedTxHashes)) - .filter((entry) => - doesLocalActivityMatchFilters({ - chainId: activityChainId, - endTimestamp: activityEndTimestamp, - filters: activityFilters, - notification: entry, - startTimestamp: activityStartTimestamp - }) - ) - .filter((entry) => !isActivityZapFilterActive || isZapNotification(entry)) - .toSorted((a, b) => (b.timeFinished ?? 0) - (a.timeFinished ?? 0)), - [ - activityChainId, - activityEndTimestamp, - activityFilters, - activityStartTimestamp, - cachedEntries, - indexedTxHashes, - isActivityZapFilterActive - ] - ) - const recentLocalActivityEntries = useMemo( - () => - recentLocalEntries - .map(toLocalActivityEntry) - .filter((entry): entry is TPortfolioActivityEntry => Boolean(entry)) - .filter((entry) => doesActivityEntryMatchSearch(entry, activitySearch, allVaults)), - [activitySearch, allVaults, recentLocalEntries] - ) const hasUnresolvedLocalEntries = unresolvedLocalEntries.length > 0 - const hasLocalEntries = hasUnresolvedLocalEntries || recentLocalActivityEntries.length > 0 const hasIndexedEntries = indexedEntries.length > 0 const hasActiveIndexedFilters = activityChainId !== null || @@ -1783,6 +1776,8 @@ function PortfolioActivitySection({ isActive, openLoginModal }: TPortfolioActivi activityFilters.startDate !== '' || activityFilters.endDate !== DEFAULT_ACTIVITY_MODAL_FILTERS.endDate const visibleIndexedEntries = useMemo(() => { + const normalizedSearch = activitySearch.trim().toLowerCase() + return indexedEntries.filter((entry) => { if (activityFilters.types.length > 0 && !activityFilters.types.includes(entry.action)) { return false @@ -1795,16 +1790,28 @@ function PortfolioActivitySection({ isActive, openLoginModal }: TPortfolioActivi return false } - return doesActivityEntryMatchSearch(entry, activitySearch, allVaults) + if (!normalizedSearch) { + return true + } + + const familyVault = allVaults[toAddress(entry.familyVaultAddress)] + const activityVault = allVaults[toAddress(entry.vaultAddress)] + const displayName = familyVault + ? getVaultName(familyVault) + : activityVault + ? getVaultName(activityVault) + : truncateHex(entry.familyVaultAddress, 5) + const chainName = getActivityChainName(entry.chainId) + const actionLabel = getActivityEntryTitle(entry) + const formattedDate = formatIndexedActivityDate(entry.timestamp) + const symbols = [entry.assetSymbol, entry.inputTokenSymbol, entry.outputTokenSymbol].filter(Boolean).join(' ') + + return [displayName, chainName, actionLabel, formattedDate, symbols, entry.txHash] + .join(' ') + .toLowerCase() + .includes(normalizedSearch) }) }, [activityFilters.types, activitySearch, allVaults, indexedEntries, isActivityZapFilterActive]) - const visibleActivityEntries = useMemo( - () => - [...recentLocalActivityEntries, ...visibleIndexedEntries].toSorted( - (firstEntry, secondEntry) => secondEntry.timestamp - firstEntry.timestamp - ), - [recentLocalActivityEntries, visibleIndexedEntries] - ) function handleActivityChainSelect(chainId: number): void { setActivityChainId(resolveNextSingleChainSelection(selectedActivityChains, chainId)?.[0] ?? null) @@ -1945,71 +1952,8 @@ function PortfolioActivitySection({ isActive, openLoginModal }: TPortfolioActivi ) } - function renderActivityRow(entry: TPortfolioActivityEntry, index: number): ReactElement { - const familyVault = allVaults[toAddress(entry.familyVaultAddress)] - const activityVault = allVaults[toAddress(entry.vaultAddress)] - const sourceVault = - entry.action === 'swap' && entry.inputTokenAddress ? allVaults[toAddress(entry.inputTokenAddress)] : null - const assetToken = familyVault ? getVaultToken(familyVault) : activityVault ? getVaultToken(activityVault) : null - const familyVaultSymbol = familyVault - ? getVaultSymbol(familyVault) - : activityVault - ? getVaultSymbol(activityVault) - : entry.assetSymbol - const displayName = familyVault - ? getVaultName(familyVault) - : activityVault - ? getVaultName(activityVault) - : (entry.assetSymbol ?? truncateHex(entry.familyVaultAddress, 5)) - const stakingToken = - entry.action === 'stake' || entry.action === 'unstake' - ? getToken({ address: toAddress(entry.vaultAddress), chainID: entry.chainId }) - : null - const stakingTokenSymbol = - stakingToken && toAddress(stakingToken.address) === toAddress(entry.vaultAddress) - ? stakingToken.symbol || null - : null - const shareSymbol = - entry.action === 'stake' || entry.action === 'unstake' - ? (stakingTokenSymbol ?? familyVaultSymbol) - : familyVault - ? getVaultSymbol(familyVault) - : activityVault - ? getVaultSymbol(activityVault) - : entry.assetSymbol - const fallbackLogoAddress = - entry.familyVaultAddress && !isZeroAddress(entry.familyVaultAddress) - ? entry.familyVaultAddress - : entry.vaultAddress - const assetAddress = - assetToken && !isZeroAddress(assetToken.address) - ? assetToken.address - : fallbackLogoAddress && !isZeroAddress(fallbackLogoAddress) - ? fallbackLogoAddress - : null - - return ( - - ) - } - - function renderActivityList(): ReactElement { - if (indexedLoading && visibleActivityEntries.length === 0) { + function renderIndexedActivity(): ReactElement { + if (indexedLoading) { return (
@@ -2018,7 +1962,7 @@ function PortfolioActivitySection({ isActive, openLoginModal }: TPortfolioActivi ) } - if (indexedError && visibleActivityEntries.length === 0) { + if (indexedError) { return (

{'Error loading activity'}

@@ -2027,30 +1971,95 @@ function PortfolioActivitySection({ isActive, openLoginModal }: TPortfolioActivi ) } - if (visibleActivityEntries.length === 0) { + if (indexedEmpty || visibleIndexedEntries.length === 0) { return (
{hasActiveIndexedFilters || activitySearch.trim() - ? 'No activity matches these filters.' - : 'No activity to show.'} + ? 'No indexed activity matches these filters.' + : 'No indexed activity to show.'}
) } return ( { + const familyVault = allVaults[toAddress(entry.familyVaultAddress)] + const activityVault = allVaults[toAddress(entry.vaultAddress)] + const sourceVault = + entry.action === 'swap' && entry.inputTokenAddress ? allVaults[toAddress(entry.inputTokenAddress)] : null + const assetToken = familyVault + ? getVaultToken(familyVault) + : activityVault + ? getVaultToken(activityVault) + : null + const familyVaultSymbol = familyVault + ? getVaultSymbol(familyVault) + : activityVault + ? getVaultSymbol(activityVault) + : entry.assetSymbol + const displayName = familyVault + ? getVaultName(familyVault) + : activityVault + ? getVaultName(activityVault) + : truncateHex(entry.familyVaultAddress, 5) + const stakingToken = + entry.action === 'stake' || entry.action === 'unstake' + ? getToken({ address: toAddress(entry.vaultAddress), chainID: entry.chainId }) + : null + const stakingTokenSymbol = + stakingToken && toAddress(stakingToken.address) === toAddress(entry.vaultAddress) + ? stakingToken.symbol || null + : null + const shareSymbol = + entry.action === 'stake' || entry.action === 'unstake' + ? (stakingTokenSymbol ?? familyVaultSymbol) + : familyVault + ? getVaultSymbol(familyVault) + : activityVault + ? getVaultSymbol(activityVault) + : entry.assetSymbol + const fallbackLogoAddress = + entry.familyVaultAddress && !isZeroAddress(entry.familyVaultAddress) + ? entry.familyVaultAddress + : entry.vaultAddress + const assetAddress = + assetToken && !isZeroAddress(assetToken.address) + ? assetToken.address + : fallbackLogoAddress && !isZeroAddress(fallbackLogoAddress) + ? fallbackLogoAddress + : null + + return ( + + ) + }} /> ) } function renderActivityContent(): ReactElement { - if (notificationsLoading && indexedLoading && !hasLocalEntries) { + if (notificationsLoading && indexedLoading && !hasUnresolvedLocalEntries) { return (
@@ -2059,7 +2068,7 @@ function PortfolioActivitySection({ isActive, openLoginModal }: TPortfolioActivi ) } - if (notificationsError && !hasLocalEntries && !hasIndexedEntries && indexedEmpty) { + if (notificationsError && !hasUnresolvedLocalEntries && !hasIndexedEntries && indexedEmpty) { return (

{'Error loading activity'}

@@ -2068,7 +2077,7 @@ function PortfolioActivitySection({ isActive, openLoginModal }: TPortfolioActivi ) } - if (!hasLocalEntries && !hasIndexedEntries && indexedEmpty && !hasActiveIndexedFilters) { + if (!hasUnresolvedLocalEntries && !hasIndexedEntries && indexedEmpty && !hasActiveIndexedFilters) { return
{'No transactions to show.'}
} @@ -2093,7 +2102,7 @@ function PortfolioActivitySection({ isActive, openLoginModal }: TPortfolioActivi
{renderActivityFilters()}
- {renderActivityList()} + {renderIndexedActivity()}
{indexedHasMore && (
@@ -2254,86 +2263,6 @@ function ChainMerkleRewardsFetcher({ return null } -function getMerkleRewardKeys(groupedRewards: TGroupedMerkleReward[]): string[] { - return groupedRewards.flatMap((groupedReward) => - 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() @@ -2516,7 +2445,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 { @@ -2542,7 +2471,7 @@ function PortfolioClaimRewardsSection({ isActive, openLoginModal }: TPortfolioCl
{showChainHeader && (
- {chainData.chainName} switchChainAsync({ chainId: chainData.chainId })} - claimButtonClassName={portfolioRewardClaimButtonClassName} /> )) )} @@ -2578,15 +2506,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 && }
diff --git a/src/components/pages/vaults/components/SuggestedVaultCard.tsx b/src/components/pages/vaults/components/SuggestedVaultCard.tsx index d9b047910..e1f16acba 100644 --- a/src/components/pages/vaults/components/SuggestedVaultCard.tsx +++ b/src/components/pages/vaults/components/SuggestedVaultCard.tsx @@ -24,6 +24,7 @@ import { TokenLogo } from '@shared/components/TokenLogo' import { formatApyDisplay, toAddress } from '@shared/utils' import { getNetwork } from '@shared/utils/wagmi' import type { ReactElement } from 'react' +import { env } from '@/env' export function SuggestedVaultCard({ vault, @@ -55,8 +56,8 @@ export function SuggestedVaultCard({ const chain = getNetwork(chainID) const tokenIcon = isYvUsd ? getVaultPrimaryLogoSrc(vault) - : `${import.meta.env.VITE_BASE_YEARN_ASSETS_URI}/tokens/${chainID}/${toAddress(token.address).toLowerCase()}/logo-128.png` - const chainLogoSrc = `${import.meta.env.VITE_BASE_YEARN_ASSETS_URI}/chains/${chainID}/logo-32.png` + : `${env.NEXT_PUBLIC_BASE_YEARN_ASSETS_URI}/tokens/${chainID}/${toAddress(token.address).toLowerCase()}/logo-128.png` + const chainLogoSrc = `${env.NEXT_PUBLIC_BASE_YEARN_ASSETS_URI}/chains/${chainID}/logo-32.png` const listKind = deriveListKind(vault) const isAllocatorVault = listKind === 'allocator' || listKind === 'strategy' const isLegacyVault = listKind === 'legacy' diff --git a/src/components/pages/vaults/components/TrendingVaults.tsx b/src/components/pages/vaults/components/TrendingVaults.tsx index 375ac7baa..00a1d49ed 100644 --- a/src/components/pages/vaults/components/TrendingVaults.tsx +++ b/src/components/pages/vaults/components/TrendingVaults.tsx @@ -19,6 +19,7 @@ import { formatPercent } from '@shared/utils/format' import { getNetwork } from '@shared/utils/wagmi' import type { CSSProperties, ReactElement } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { env } from '@/env' type TTrendingVaultsProps = { suggestedVaults: TKongVaultInput[] @@ -65,8 +66,8 @@ function TrendingVaultMarqueeItem({ vault }: { vault: TKongVaultInput }): ReactE const chain = getNetwork(chainID) const staking = getVaultStaking(vault) const tvl = getVaultTVL(vault) - const tokenIcon = `${import.meta.env.VITE_BASE_YEARN_ASSETS_URI}/tokens/${chainID}/${toAddress(token.address).toLowerCase()}/logo-128.png` - const chainLogoSrc = `${import.meta.env.VITE_BASE_YEARN_ASSETS_URI}/chains/${chainID}/logo-32.png` + const tokenIcon = `${env.NEXT_PUBLIC_BASE_YEARN_ASSETS_URI}/tokens/${chainID}/${toAddress(token.address).toLowerCase()}/logo-128.png` + const chainLogoSrc = `${env.NEXT_PUBLIC_BASE_YEARN_ASSETS_URI}/chains/${chainID}/logo-32.png` const apyDisplay = useMemo((): string => { if (apyData.mode === 'historical' || apyData.mode === 'noForward') { diff --git a/src/components/pages/vaults/components/compare/CompareVaultCard.tsx b/src/components/pages/vaults/components/compare/CompareVaultCard.tsx index 969e95f2c..f0fc6c5e9 100644 --- a/src/components/pages/vaults/components/compare/CompareVaultCard.tsx +++ b/src/components/pages/vaults/components/compare/CompareVaultCard.tsx @@ -26,6 +26,7 @@ import { cl, formatPercent, formatTvlDisplay, toAddress } from '@shared/utils' import { PLAUSIBLE_EVENTS } from '@shared/utils/plausible' import { getNetwork } from '@shared/utils/wagmi' import type { ReactElement, ReactNode } from 'react' +import { env } from '@/env' type TCompareVaultCardProps = { vault: TKongVaultInput @@ -86,7 +87,7 @@ export function CompareVaultCard({ vault, onRemove }: TCompareVaultCardProps): R const vaultKind = getVaultKind(vault) const vaultInfo = getVaultInfo(vault) const network = getNetwork(chainID) - const chainLogoSrc = `${import.meta.env.VITE_BASE_YEARN_ASSETS_URI}/chains/${chainID}/logo-32.png` + const chainLogoSrc = `${env.NEXT_PUBLIC_BASE_YEARN_ASSETS_URI}/chains/${chainID}/logo-32.png` const vaultKey = getVaultKey(vault) const vaultHref = `/vaults/${chainID}/${toAddress(vaultAddress)}` const listKind = deriveListKind(vault) @@ -137,7 +138,7 @@ export function CompareVaultCard({ vault, onRemove }: TCompareVaultCardProps): R
[network.id, network] as const)) as ReadonlyMap< @@ -56,8 +57,8 @@ function ApproveNotificationContent({ notification }: { notification: TNotificat
): ReactElement { const width = maxWidth || 'w-full max-w-[360px]' const resolvedNativeYieldLabel = nativeYieldLabel ?? 'Katana Native 30 Day APY' - const baseAssetsUrl = import.meta.env.VITE_BASE_YEARN_ASSETS_URI + const baseAssetsUrl = env.NEXT_PUBLIC_BASE_YEARN_ASSETS_URI const chainId = getVaultChainID(currentVault) const tokenAddress = getVaultToken(currentVault).address.toLowerCase() const tokenLogoSrc = `${baseAssetsUrl}/tokens/${chainId}/${tokenAddress}/logo-32.png` diff --git a/src/components/pages/vaults/components/table/VaultForwardAPY.test.tsx b/src/components/pages/vaults/components/table/VaultForwardAPY.test.tsx index 8d979525e..33efb497d 100644 --- a/src/components/pages/vaults/components/table/VaultForwardAPY.test.tsx +++ b/src/components/pages/vaults/components/table/VaultForwardAPY.test.tsx @@ -8,7 +8,7 @@ import { describe, expect, it, vi } from 'vitest' import { VaultForwardAPY } from './VaultForwardAPY' -vi.mock('@vaults/hooks/useVaultApyData', () => ({ +vi.mock('@pages/vaults/hooks/useVaultApyData', () => ({ useVaultApyData: vi.fn() })) diff --git a/src/components/pages/vaults/components/widget/InputTokenAmount.test.tsx b/src/components/pages/vaults/components/widget/InputTokenAmount.test.tsx index fd772ab18..45b423e43 100644 --- a/src/components/pages/vaults/components/widget/InputTokenAmount.test.tsx +++ b/src/components/pages/vaults/components/widget/InputTokenAmount.test.tsx @@ -37,7 +37,7 @@ vi.mock('@shared/utils', () => ({ })) vi.mock('@shared/components/TokenLogo', () => ({ - TokenLogo: (props: { src?: string; altSrc?: string }) => + TokenLogo: (props: { src?: string; altSrc?: string }) => })) describe('InputTokenAmount', () => { diff --git a/src/components/pages/vaults/components/widget/InputTokenAmount.tsx b/src/components/pages/vaults/components/widget/InputTokenAmount.tsx index 4f3ddb5ac..7a2122a70 100644 --- a/src/components/pages/vaults/components/widget/InputTokenAmount.tsx +++ b/src/components/pages/vaults/components/widget/InputTokenAmount.tsx @@ -7,6 +7,7 @@ import { formatCounterValue } from '@shared/utils/format' import { type ChangeEvent, type FC, useMemo } from 'react' import { formatUnits } from 'viem' import { useAccount } from 'wagmi' +import { env } from '@/env' import { getTokenLogoSources } from './tokenLogo.utils' interface Props { @@ -344,7 +345,7 @@ export const InputTokenAmount: FC = ({ > {zapToken.address && zapToken.chainId && ( = ({ type="button" > { - if (!import.meta.env.DEV) { + if (!env.DEV) { return } @@ -490,7 +491,7 @@ export function WidgetDeposit({ useEffect(() => { if ( - !import.meta.env.DEV || + !env.DEV || routeType !== 'ENSO' || isLoadingQuote || depositAmount.isDebouncing || diff --git a/src/components/pages/vaults/components/widget/migrate/index.tsx b/src/components/pages/vaults/components/widget/migrate/index.tsx index 77771b98b..d20e5adcd 100644 --- a/src/components/pages/vaults/components/widget/migrate/index.tsx +++ b/src/components/pages/vaults/components/widget/migrate/index.tsx @@ -13,6 +13,7 @@ import { type FC, useCallback, useEffect, useMemo, useState } from 'react' import { hexToNumber, slice } from 'viem' import { useAccount } from 'wagmi' import { useYearn } from '@/components/shared/contexts/useYearn' +import { env } from '@/env' import { TransactionOverlay, type TransactionStep } from '../shared/TransactionOverlay' import { formatWidgetValue } from '../shared/valueDisplay' import { WidgetHeader } from '../shared/WidgetHeader' @@ -460,9 +461,9 @@ export const WidgetMigrate: FC = ({ { - if (!import.meta.env.DEV) { + if (!env.DEV) { return } @@ -592,7 +593,7 @@ export function WidgetWithdraw({ useEffect(() => { if ( - !import.meta.env.DEV || + !env.DEV || routeType !== 'ENSO' || isFetchingQuote || withdrawAmount.isDebouncing || diff --git a/src/components/pages/vaults/components/widget/yvUSD/YvUsdWithdraw.helpers.ts b/src/components/pages/vaults/components/widget/yvUSD/YvUsdWithdraw.helpers.ts index b6635dc28..f8c8360d2 100644 --- a/src/components/pages/vaults/components/widget/yvUSD/YvUsdWithdraw.helpers.ts +++ b/src/components/pages/vaults/components/widget/yvUSD/YvUsdWithdraw.helpers.ts @@ -154,10 +154,6 @@ export function resolveLockedRequestedWithdrawAssets({ return 0n } - if (maxDisplayAmount > 0n && requestedDisplayAmount >= maxDisplayAmount) { - return maxWithdrawAssets - } - if (typeof previewWithdrawShares !== 'bigint' || previewWithdrawShares <= 0n) { return 0n } @@ -166,6 +162,10 @@ export function resolveLockedRequestedWithdrawAssets({ return previewWithdrawShares } + if (maxDisplayAmount > 0n && requestedDisplayAmount === maxDisplayAmount) { + return maxWithdrawAssets + } + return previewWithdrawShares > maxWithdrawAssets ? maxWithdrawAssets : previewWithdrawShares } diff --git a/src/components/pages/vaults/components/yvUSD/YvUsdHeaderBanner.tsx b/src/components/pages/vaults/components/yvUSD/YvUsdHeaderBanner.tsx index 0eed5aa47..b73af73f5 100644 --- a/src/components/pages/vaults/components/yvUSD/YvUsdHeaderBanner.tsx +++ b/src/components/pages/vaults/components/yvUSD/YvUsdHeaderBanner.tsx @@ -1,9 +1,10 @@ import { YVUSD_LEARN_MORE_URL } from '@pages/vaults/utils/yvUsd' import { cl } from '@shared/utils' import type { CSSProperties, ReactElement } from 'react' +import { env } from '@/env' const BANNER_BACKGROUND_STYLE: CSSProperties = { - backgroundImage: `url(${import.meta.env.BASE_URL || '/'}yvusd-banner-bg.png)`, + backgroundImage: `url(${env.BASE_URL || '/'}yvusd-banner-bg.png)`, backgroundPosition: 'center', backgroundRepeat: 'no-repeat', backgroundSize: 'cover' diff --git a/src/components/pages/vaults/hooks/solvers/useSolverEnso.ts b/src/components/pages/vaults/hooks/solvers/useSolverEnso.ts index 195014770..92e9b8d5b 100644 --- a/src/components/pages/vaults/hooks/solvers/useSolverEnso.ts +++ b/src/components/pages/vaults/hooks/solvers/useSolverEnso.ts @@ -4,6 +4,7 @@ import { isZeroAddress, toNormalizedBN } from '@shared/utils' import { getApproveAbi } from '@shared/utils/approve' import { useCallback, useEffect, useRef, useState } from 'react' import type { Address } from 'viem' +import { env } from '@/env' import { useTokenAllowance } from '../useTokenAllowance' import { type EnsoError, type EnsoRouteResponse, normalizeEnsoRouteResponse, routeHasSwapStep } from './ensoRoute' @@ -159,7 +160,7 @@ export const useSolverEnso = ({ setRoute(resolvedRoute) setResolvedRequestKey(requestKey) setErrorRequestKey(undefined) - if (import.meta.env.DEV && resolvedRoute) { + if (env.DEV && resolvedRoute) { console.log('[ENSO] route response', { chainId, destinationChainId, diff --git a/src/components/pages/vaults/hooks/useEnsoEnabled.ts b/src/components/pages/vaults/hooks/useEnsoEnabled.ts index bdcc7b8ee..68e021e5e 100644 --- a/src/components/pages/vaults/hooks/useEnsoEnabled.ts +++ b/src/components/pages/vaults/hooks/useEnsoEnabled.ts @@ -1,6 +1,7 @@ import { isVaultEnsoDisabled } from '@pages/vaults/constants/ensoDisabledVaults' import { useEnsoStatus } from '@pages/vaults/contexts/useEnsoStatus' import type { Address } from 'viem' +import { env } from '@/env' interface UseEnsoEnabledOptions { chainId?: number @@ -9,7 +10,7 @@ interface UseEnsoEnabledOptions { export function useEnsoEnabled({ chainId, vaultAddress }: UseEnsoEnabledOptions = {}): boolean { const { isEnsoFailed } = useEnsoStatus() - const envDisabled = import.meta.env.VITE_ENSO_DISABLED === 'true' + const envDisabled = env.NEXT_PUBLIC_ENSO_DISABLED === 'true' if (envDisabled || isEnsoFailed) { return false diff --git a/src/components/pages/vaults/hooks/useVaultsPageModel.ts b/src/components/pages/vaults/hooks/useVaultsPageModel.ts index 0215413ed..fe8f08157 100644 --- a/src/components/pages/vaults/hooks/useVaultsPageModel.ts +++ b/src/components/pages/vaults/hooks/useVaultsPageModel.ts @@ -63,6 +63,7 @@ import { useRef, useState } from 'react' +import { env } from '@/env' import { useVaultsListModel } from './useVaultsListModel' import { useVaultsQueryState } from './useVaultsQueryState' import { VAULTS_FILTERS_STORAGE_KEY } from './vaultsFiltersStorage' @@ -704,7 +705,7 @@ export function useVaultsPageModel(): TVaultsPageModel { const label = getUnderlyingAssetLabel(assetKey) const token = getVaultToken(vault) const tokenAddress = token.address.toLowerCase() - const tokenLogoSrc = `${import.meta.env.VITE_BASE_YEARN_ASSETS_URI}/tokens/${getVaultChainID(vault)}/${tokenAddress}/logo-32.png` + const tokenLogoSrc = `${env.NEXT_PUBLIC_BASE_YEARN_ASSETS_URI}/tokens/${getVaultChainID(vault)}/${tokenAddress}/logo-32.png` return { label, value: assetKey, diff --git a/src/components/pages/vaults/utils/kongRest.ts b/src/components/pages/vaults/utils/kongRest.ts index ea9c551b3..4f983c107 100644 --- a/src/components/pages/vaults/utils/kongRest.ts +++ b/src/components/pages/vaults/utils/kongRest.ts @@ -1,10 +1,8 @@ -export const KONG_REST_BASE = (import.meta.env.VITE_KONG_REST_URL || 'https://kong.yearn.fi/api/rest').replace( - /\/$/, - '' -) +import { env } from '@/env' +export const KONG_REST_BASE = (env.NEXT_PUBLIC_KONG_REST_URL || 'https://kong.yearn.fi/api/rest').replace(/\/$/, '') const derivedGqlBase = KONG_REST_BASE.endsWith('/api/rest') ? KONG_REST_BASE.replace(/\/api\/rest$/, '/api/gql') : 'https://kong.yearn.fi/api/gql' -export const KONG_GQL_BASE = (import.meta.env.VITE_KONG_GQL_URL || derivedGqlBase).replace(/\/$/, '') +export const KONG_GQL_BASE = (env.NEXT_PUBLIC_KONG_GQL_URL || derivedGqlBase).replace(/\/$/, '') diff --git a/src/components/pages/vaults/utils/vaultLogo.ts b/src/components/pages/vaults/utils/vaultLogo.ts index c6c0bd028..90796ba45 100644 --- a/src/components/pages/vaults/utils/vaultLogo.ts +++ b/src/components/pages/vaults/utils/vaultLogo.ts @@ -1,14 +1,15 @@ import { getVaultChainID, getVaultToken, type TKongVaultInput } from '@pages/vaults/domain/kongVaultSelectors' import { toAddress } from '@shared/utils' +import { env } from '@/env' import { isYvBtcVault } from './yvBtc' import { isYvUsdVault } from './yvUsd' function getBaseUrl(): string { - return import.meta.env.BASE_URL || '/' + return env.BASE_URL || '/' } function getAssetsBaseUrl(): string { - return import.meta.env.VITE_BASE_YEARN_ASSETS_URI || '' + return env.NEXT_PUBLIC_BASE_YEARN_ASSETS_URI || '' } export function getVaultPrimaryLogoSrc(vault: TKongVaultInput): string { diff --git a/src/components/pages/vaults/utils/yvUsd.ts b/src/components/pages/vaults/utils/yvUsd.ts index 6fd5738e9..a3eefd980 100644 --- a/src/components/pages/vaults/utils/yvUsd.ts +++ b/src/components/pages/vaults/utils/yvUsd.ts @@ -1,6 +1,7 @@ import { getVaultView, type TKongVaultInput } from '@pages/vaults/domain/kongVaultSelectors' import { toAddress } from '@shared/utils' import { type Address, formatUnits, parseUnits } from 'viem' +import { env } from '@/env' export const YVUSD_CHAIN_ID = 1 export const YVUSD_DECIMALS = 6 @@ -15,7 +16,7 @@ export const YVUSD_ANNOUNCEMENT_URL = '#' export const YVUSD_LEARN_MORE_URL = 'https://x.com/Schlagonia/status/2032147643334582487' function getYvUsdAprServiceEndpoint(): string { - const configuredEndpoint = import.meta.env.VITE_YVUSD_APR_SERVICE_API?.trim().replace(/\/$/, '') + const configuredEndpoint = env.NEXT_PUBLIC_YVUSD_APR_SERVICE_API?.trim().replace(/\/$/, '') if (configuredEndpoint?.startsWith('/')) { return configuredEndpoint } diff --git a/src/components/shared/components/HeaderNavMenu.tsx b/src/components/shared/components/HeaderNavMenu.tsx index f3e28f617..5af92330f 100644 --- a/src/components/shared/components/HeaderNavMenu.tsx +++ b/src/components/shared/components/HeaderNavMenu.tsx @@ -10,6 +10,8 @@ import { LogoYearnMark } from '@shared/icons/LogoYearnMark' import { cl } from '@shared/utils' import type { ReactElement } from 'react' import { useRef, useState } from 'react' +import { env } from '@/env' +import Image from '/src/components/Image' import Link from '/src/components/Link' import { DropdownPanel } from './DropdownPanel' import type { TAppTile } from './YearnApps' @@ -26,7 +28,7 @@ type TNavTile = Pick & { iconWrapperClass?: string } -const BASE_YEARN_ASSET_URI = import.meta.env?.VITE_BASE_YEARN_ASSETS_URI ?? '' +const BASE_YEARN_ASSET_URI = env?.NEXT_PUBLIC_BASE_YEARN_ASSETS_URI ?? '' function isExternalHref(href: string): boolean { return /^https?:\/\//i.test(href) @@ -97,7 +99,7 @@ export function HeaderNavMenu({ isHomePage, isDarkTheme }: THeaderNavMenuProps): href: 'https://ycrv.yearn.fi', description: 'veCRV Liquid Locker', icon: ( - {'yCRV'} + icon: {'yYB'} }, { name: 'stYFI', href: 'https://styfi.yearn.fi', description: 'YFI Staking', - icon: {'stYFI'} + icon: {'stYFI'} } ] @@ -126,7 +128,7 @@ export function HeaderNavMenu({ isHomePage, isDarkTheme }: THeaderNavMenuProps): href: 'https://docs.yearn.fi/', description: 'Yearn Knowledge Base', icon: ( - {'GitBook'} + {'Blog'} ) }, { @@ -222,7 +224,7 @@ export function HeaderNavMenu({ isHomePage, isDarkTheme }: THeaderNavMenuProps): href: 'https://gov.yearn.fi/', description: 'Yearn Discussion Forum', icon: ( - {'Discourse'} +
{tooltip ? ( <> {label} )} -

+
) } diff --git a/src/components/shared/components/MobileNavMenu.tsx b/src/components/shared/components/MobileNavMenu.tsx index 3f716027f..9369ec949 100644 --- a/src/components/shared/components/MobileNavMenu.tsx +++ b/src/components/shared/components/MobileNavMenu.tsx @@ -23,6 +23,8 @@ import { truncateHex } from '@shared/utils/tools.address' import type { ReactElement } from 'react' import { useEffect, useState } from 'react' import { useNavigate } from 'react-router' +import { env } from '@/env' +import Image from '/src/components/Image' import Link from '/src/components/Link' import { IconTelegram } from '../icons/IconTelegram' @@ -37,7 +39,7 @@ type TNavTile = TNavItem & { iconWrapperClass?: string } -const BASE_YEARN_ASSET_URI = import.meta.env?.VITE_BASE_YEARN_ASSETS_URI ?? '' +const BASE_YEARN_ASSET_URI = env?.NEXT_PUBLIC_BASE_YEARN_ASSETS_URI ?? '' function isExternalHref(href: string): boolean { return /^https?:\/\//i.test(href) @@ -186,7 +188,7 @@ export function MobileNavMenu({ href: 'https://ycrv.yearn.fi', description: 'veCRV Liquid Locker', icon: ( - {'yCRV'} + icon: {'yYB'} }, { name: 'stYFI', href: 'https://styfi.yearn.fi', description: 'YFI Staking', - icon: {'stYFI'} + icon: {'stYFI'} } ] @@ -215,7 +217,7 @@ export function MobileNavMenu({ href: 'https://docs.yearn.fi/', description: 'Yearn Knowledge Base', icon: ( - {'GitBook'} + {'Blog'} ) }, { @@ -278,7 +280,7 @@ export function MobileNavMenu({ href: 'https://gov.yearn.fi/', description: 'Yearn Discussion Forum', icon: ( - {'Discourse'} { @@ -156,6 +157,7 @@ function TokenLogo(props: TokenLogoProps): ReactElement {
) : null} {isVisible && !hasError ? ( + // biome-ignore lint/performance/noImgElement: token logos need custom fallback and error handling. + {/* biome-ignore lint/performance/noImgElement: chain badge shares the token logo fallback primitive. */} Network { ) const trigger = container.firstChild as HTMLElement - click(trigger) + act(() => { + click(trigger) + }) expect(queryByText('Tip')).not.toBeNull() - click(trigger) + act(() => { + click(trigger) + }) expect(queryByText('Tip')).toBeNull() }) diff --git a/src/components/shared/components/YearnApps.tsx b/src/components/shared/components/YearnApps.tsx index 95f8efa86..5d255cbbd 100644 --- a/src/components/shared/components/YearnApps.tsx +++ b/src/components/shared/components/YearnApps.tsx @@ -2,9 +2,10 @@ import { IconDiscord } from '@shared/icons/IconDiscord' import { LogoGimme } from '@shared/icons/LogoGimme' import { LogoYearn } from '@shared/icons/LogoYearn' import type { ReactElement } from 'react' +import { env } from '@/env' import Image from '/src/components/Image' -const BASE_YEARN_ASSET_URI = import.meta.env?.VITE_BASE_YEARN_ASSETS_URI ?? '' +const BASE_YEARN_ASSET_URI = env?.NEXT_PUBLIC_BASE_YEARN_ASSETS_URI ?? '' type TColorProps = { front?: string @@ -38,6 +39,20 @@ export type TAppGroup = { } const CORE_APPS: TAppTile[] = [ + { + name: 'V3 Vaults', + href: '/vaults', + description: 'Automated yield vaults', + icon: yearnGlyph({ back: 'text-primary', front: 'text-white' }), + pathnames: ['/vaults', '/v3'] + }, + { + name: 'LP Vaults', + href: '/vaults?type=lp', + description: 'Curve and LP strategies', + icon: yearnGlyph({ back: 'text-neutral-900', front: 'text-primary' }), + pathnames: ['/v2'] + }, { name: 'yCRV', href: 'https://ycrv.yearn.fi', diff --git a/src/components/shared/components/YearnLogoSpinner.tsx b/src/components/shared/components/YearnLogoSpinner.tsx index c29c0e51f..98f2f80ac 100644 --- a/src/components/shared/components/YearnLogoSpinner.tsx +++ b/src/components/shared/components/YearnLogoSpinner.tsx @@ -1,5 +1,6 @@ import { cl } from '@shared/utils' import type { ReactElement } from 'react' +import Image from '/src/components/Image' type TYearnLogoSpinnerProps = { className?: string @@ -10,7 +11,7 @@ export function YearnLogoSpinner({ className, logoClassName }: TYearnLogoSpinner return (
- {''} { - const isDevelopment = import.meta.env.MODE === 'development' + const isDevelopment = env.MODE === 'development' const { chainID } = useWeb3() const { value: extraTokenlist } = useLocalStorageValue('extraTokenlists') const { value: extraTokens, set: setExtraTokens } = useLocalStorageValue('extraTokens') @@ -130,7 +131,7 @@ export const WithTokenList = ({ ** If we are in development mode, we also want to add the token to our list, but only ** if the token's chainID is 1 (Ethereum). *************************************************************************************/ - if (isDevelopment && Boolean(import.meta.env.VITE_SHOULD_USE_FORKNET) && eachToken.chainId === 1) { + if (isDevelopment && Boolean(env.NEXT_PUBLIC_SHOULD_USE_FORKNET) && eachToken.chainId === 1) { if (!tokenListTokens[1337]) { tokenListTokens[1337] = {} } @@ -192,7 +193,7 @@ export const WithTokenList = ({ ** If we are in development mode, we also want to add the token to our list, but only ** if the token's chainID is 1 (Ethereum). *************************************************************************************/ - if (isDevelopment && Boolean(import.meta.env.VITE_SHOULD_USE_FORKNET) && chainId === 1) { + if (isDevelopment && Boolean(env.NEXT_PUBLIC_SHOULD_USE_FORKNET) && chainId === 1) { if (!tokenListTokens[1337]) { tokenListTokens[1337] = {} } @@ -246,7 +247,7 @@ export const WithTokenList = ({ ** If we are in development mode, we also want to add the token to our list, but only ** if the token's chainID is 1 (Ethereum). *************************************************************************************/ - if (isDevelopment && Boolean(import.meta.env.VITE_SHOULD_USE_FORKNET) && eachToken.chainId === 1) { + if (isDevelopment && Boolean(env.NEXT_PUBLIC_SHOULD_USE_FORKNET) && eachToken.chainId === 1) { if (!tokenListTokens[1337]) { tokenListTokens[1337] = {} } diff --git a/src/components/shared/contexts/useWallet.tsx b/src/components/shared/contexts/useWallet.tsx index 9826d76e8..84df3e3ba 100644 --- a/src/components/shared/contexts/useWallet.tsx +++ b/src/components/shared/contexts/useWallet.tsx @@ -15,6 +15,7 @@ import type { QueryKey } from '@tanstack/react-query' import { useQueryClient } from '@tanstack/react-query' import type { ReactElement } from 'react' import { createContext, memo, useCallback, useContext, useDeferredValue, useMemo, useRef } from 'react' +import { env } from '@/env' import type { TUseBalancesTokens } from '../hooks/useBalances.multichains' import { useBalancesCombined } from '../hooks/useBalancesCombined' import { useBalancesWithQuery } from '../hooks/useBalancesWithQuery' @@ -29,7 +30,7 @@ import { useYearn } from './useYearn' import { useYearnTokens } from './useYearn.helper' import { useTokenList } from './WithTokenList' -const USE_ENSO_BALANCES = import.meta.env.VITE_BALANCE_SOURCE !== 'multicall' +const USE_ENSO_BALANCES = env.NEXT_PUBLIC_BALANCE_SOURCE !== 'multicall' type TTokenAndChain = { address: TAddress; chainID: number } diff --git a/src/components/shared/contexts/useYearn.tsx b/src/components/shared/contexts/useYearn.tsx index 2a34e59ee..8e6dd4113 100755 --- a/src/components/shared/contexts/useYearn.tsx +++ b/src/components/shared/contexts/useYearn.tsx @@ -13,6 +13,7 @@ import type { ReactElement } from 'react' import { createContext, memo, useCallback, useContext, useEffect, useState } from 'react' import { useLocation } from 'react-router' import { deserialize, serialize } from 'wagmi' +import { env } from '@/env' export const DEFAULT_SLIPPAGE = 0.5 export const DEFAULT_MAX_LOSS = 1n @@ -41,7 +42,7 @@ export type TYearnContext = { } const YearnContext = createContext({ - currentPartner: toAddress(import.meta.env.VITE_PARTNER_ID_ADDRESS), + currentPartner: toAddress(env.NEXT_PUBLIC_PARTNER_ID_ADDRESS), earned: { earned: {}, totalRealizedGainsUSD: 0, @@ -121,7 +122,7 @@ export const YearnContextApp = memo(function YearnContextApp({ children }: { chi return ( + {chain.name} ) }) } diff --git a/src/components/shared/hooks/useYDaemonBaseURI.ts b/src/components/shared/hooks/useYDaemonBaseURI.ts index f11d3c10b..6470c7483 100644 --- a/src/components/shared/hooks/useYDaemonBaseURI.ts +++ b/src/components/shared/hooks/useYDaemonBaseURI.ts @@ -1,10 +1,12 @@ +import { env } from '@/env' + type TProps = { chainID: number | string baseURI?: string } export function useYDaemonBaseURI(props?: TProps): { yDaemonBaseUri: string } { - const yDaemonBaseUri = props?.baseURI || import.meta.env.VITE_YDAEMON_BASE_URI + const yDaemonBaseUri = props?.baseURI || env.NEXT_PUBLIC_YDAEMON_BASE_URI if (!yDaemonBaseUri) { throw new Error('YDAEMON_BASE_URI is not defined') diff --git a/src/components/shared/icons/IconLock.tsx b/src/components/shared/icons/IconLock.tsx index e1d3cc18a..09824a211 100644 --- a/src/components/shared/icons/IconLock.tsx +++ b/src/components/shared/icons/IconLock.tsx @@ -6,5 +6,8 @@ type TProps = { } export function IconLock({ className, size = 16 }: TProps): ReactElement { - return + return ( + // biome-ignore lint/performance/noImgElement: icon component wraps a static SVG asset and must stay server-safe. + + ) } diff --git a/src/components/shared/icons/IconLockOpen.tsx b/src/components/shared/icons/IconLockOpen.tsx index ecf49d88c..4ecd64f53 100644 --- a/src/components/shared/icons/IconLockOpen.tsx +++ b/src/components/shared/icons/IconLockOpen.tsx @@ -7,6 +7,7 @@ type TProps = { export function IconLockOpen({ className, size = 16 }: TProps): ReactElement { return ( + // biome-ignore lint/performance/noImgElement: icon component wraps a static SVG asset and must stay server-safe. () const getApiBaseUrl = (): string => { - const envUrl = import.meta.env.VITE_CLUSTERS_API_URL + const envUrl = env.NEXT_PUBLIC_CLUSTERS_API_URL return (envUrl && envUrl.trim().length > 0 ? envUrl : DEFAULT_API_URL).replace(/\/$/, '') } const getApiKey = (): string | undefined => { - const key = import.meta.env.VITE_CLUSTERS_API_KEY + const key = env.NEXT_PUBLIC_CLUSTERS_API_KEY return key && key.trim().length > 0 ? key : undefined } diff --git a/src/components/shared/utils/tools.address.ts b/src/components/shared/utils/tools.address.ts index edb76a874..781b0f3dd 100644 --- a/src/components/shared/utils/tools.address.ts +++ b/src/components/shared/utils/tools.address.ts @@ -1,4 +1,5 @@ import { getAddress, zeroAddress } from 'viem' +import { env } from '@/env' import type { TAddress, TAddressLike, TAddressSmol, TDict } from '../types' import { isTAddress, isZeroAddress } from './tools.is' @@ -113,7 +114,7 @@ export function toENS(address: string | null | undefined, format?: boolean, size return address || '' } const _address = toAddress(address) - const knownENS = import.meta.env.VITE_KNOWN_ENS as unknown as TDict + const knownENS = env.NEXT_PUBLIC_KNOWN_ENS as unknown as TDict if (knownENS?.[_address]) { return knownENS[_address] } diff --git a/src/components/shared/utils/wagmi/actions.ts b/src/components/shared/utils/wagmi/actions.ts index ab9a9890d..d0ff2254a 100755 --- a/src/components/shared/utils/wagmi/actions.ts +++ b/src/components/shared/utils/wagmi/actions.ts @@ -16,6 +16,7 @@ import { erc20Abi } from 'viem' import type { Connector } from 'wagmi' import { readContract } from 'wagmi/actions' import { resolveExecutionChainId } from '@/config/tenderly' +import { env } from '@/env' function getExecutionChainID(chainID: number): number { const executionChainId = resolveExecutionChainId(chainID) @@ -41,7 +42,7 @@ const ALTERNATE_ERC20_APPROVE_ABI = [ } ] as const -const isDevelopment = import.meta.env.MODE === 'development' +const isDevelopment = env.MODE === 'development' /******************************************************************************* ** isApprovedERC20 is a _VIEW_ function that checks if a token is approved for @@ -220,7 +221,7 @@ export async function depositViaPartner(props: TDepositViaPartner): Promise chain.id === executionChainId) const newRPC = getRpcUriFor(executionChainId) - const oldRPC = - import.meta.env.VITE_JSON_RPC_URI?.[executionChainId] || import.meta.env.VITE_JSON_RPC_URL?.[executionChainId] + const oldRPC = env.NEXT_PUBLIC_JSON_RPC_URI?.[executionChainId] || env.NEXT_PUBLIC_JSON_RPC_URL?.[executionChainId] const url = newRPC || diff --git a/src/config/tenderly.test.ts b/src/config/tenderly.test.ts index 3253148d0..c70568d27 100644 --- a/src/config/tenderly.test.ts +++ b/src/config/tenderly.test.ts @@ -33,32 +33,32 @@ describe('parseTenderlyRuntime', () => { it('requires both chain id and rpc uri when a Tenderly chain is configured', () => { expect(() => parseTenderlyRuntime({ - VITE_TENDERLY_MODE: 'true', - VITE_TENDERLY_CHAIN_ID_FOR_1: '73571' + NEXT_PUBLIC_TENDERLY_MODE: 'true', + NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_1: '73571' }) - ).toThrow(/requires both VITE_TENDERLY_CHAIN_ID_FOR_1 and VITE_TENDERLY_RPC_URI_FOR_1/) + ).toThrow(/requires both NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_1 and NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_1/) }) it('rejects duplicate Tenderly execution chain ids', () => { expect(() => parseTenderlyRuntime({ - VITE_TENDERLY_MODE: 'true', - VITE_TENDERLY_CHAIN_ID_FOR_1: '73571', - VITE_TENDERLY_RPC_URI_FOR_1: 'https://rpc.tenderly.ethereum.example', - VITE_TENDERLY_CHAIN_ID_FOR_10: '73571', - VITE_TENDERLY_RPC_URI_FOR_10: 'https://rpc.tenderly.optimism.example' + NEXT_PUBLIC_TENDERLY_MODE: 'true', + NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_1: '73571', + NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_1: 'https://rpc.tenderly.ethereum.example', + NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_10: '73571', + NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_10: 'https://rpc.tenderly.optimism.example' }) ).toThrow(/Duplicate Tenderly execution chain ID 73571 configured for canonical chains 1 and 10/) }) it('filters canonical chains and resolves execution chain ids from runtime config', () => { const runtime = parseTenderlyRuntime({ - VITE_TENDERLY_MODE: 'true', - VITE_TENDERLY_CHAIN_ID_FOR_1: '73571', - VITE_TENDERLY_RPC_URI_FOR_1: 'https://rpc.tenderly.ethereum.example', - VITE_TENDERLY_EXPLORER_URI_FOR_1: 'https://explorer.tenderly.ethereum.example', - VITE_TENDERLY_CHAIN_ID_FOR_10: '73572', - VITE_TENDERLY_RPC_URI_FOR_10: 'https://rpc.tenderly.optimism.example' + NEXT_PUBLIC_TENDERLY_MODE: 'true', + NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_1: '73571', + NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_1: 'https://rpc.tenderly.ethereum.example', + NEXT_PUBLIC_TENDERLY_EXPLORER_URI_FOR_1: 'https://explorer.tenderly.ethereum.example', + NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_10: '73572', + NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_10: 'https://rpc.tenderly.optimism.example' }) const supportedCanonicalChains = getSupportedCanonicalChainsForRuntime(runtime) @@ -106,9 +106,9 @@ describe('parseTenderlyRuntime', () => { it('does not reuse canonical explorers for execution chains without explicit Tenderly explorer URIs', () => { const runtime = parseTenderlyRuntime({ - VITE_TENDERLY_MODE: 'true', - VITE_TENDERLY_CHAIN_ID_FOR_1: '73571', - VITE_TENDERLY_RPC_URI_FOR_1: 'https://rpc.tenderly.ethereum.example' + NEXT_PUBLIC_TENDERLY_MODE: 'true', + NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_1: '73571', + NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_1: 'https://rpc.tenderly.ethereum.example' }) const supportedCanonicalChains = getSupportedCanonicalChainsForRuntime(runtime) const supportedExecutionChains = getSupportedExecutionChainsForRuntime(runtime, supportedCanonicalChains) @@ -163,9 +163,9 @@ describe('resolveInitialTenderlyModeEnabled', () => { it('identifies connected Tenderly execution chains that should be switched off before disabling', () => { const runtime = parseTenderlyRuntime({ - VITE_TENDERLY_MODE: 'true', - VITE_TENDERLY_CHAIN_ID_FOR_1: '73571', - VITE_TENDERLY_RPC_URI_FOR_1: 'https://rpc.tenderly.ethereum.example' + NEXT_PUBLIC_TENDERLY_MODE: 'true', + NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_1: '73571', + NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_1: 'https://rpc.tenderly.ethereum.example' }) expect(resolveConnectedTenderlyExecutionChainForRuntime(runtime, 73571)).toEqual({ @@ -177,9 +177,9 @@ describe('resolveInitialTenderlyModeEnabled', () => { it('does not flag canonical chains as Tenderly execution chains', () => { const runtime = parseTenderlyRuntime({ - VITE_TENDERLY_MODE: 'true', - VITE_TENDERLY_CHAIN_ID_FOR_1: '73571', - VITE_TENDERLY_RPC_URI_FOR_1: 'https://rpc.tenderly.ethereum.example' + NEXT_PUBLIC_TENDERLY_MODE: 'true', + NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_1: '73571', + NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_1: 'https://rpc.tenderly.ethereum.example' }) expect(resolveConnectedTenderlyExecutionChainForRuntime(runtime, 1)).toBeUndefined() diff --git a/src/config/tenderly.ts b/src/config/tenderly.ts index fdb55750f..9ba00ca02 100644 --- a/src/config/tenderly.ts +++ b/src/config/tenderly.ts @@ -1,4 +1,5 @@ import type { Chain } from 'viem' +import { env } from '@/env' import { canonicalChains, type TCanonicalChainId } from './chainDefinitions' type TEnvValue = boolean | string | undefined @@ -131,7 +132,7 @@ export function resolveInitialTenderlyModeEnabled({ } export function parseTenderlyRuntime(env: TTenderlyEnv): TTenderlyRuntime { - const isEnabled = isTruthyEnvValue(env.VITE_TENDERLY_MODE) + const isEnabled = isTruthyEnvValue(env.NEXT_PUBLIC_TENDERLY_MODE) if (!isEnabled) { return { @@ -144,9 +145,9 @@ export function parseTenderlyRuntime(env: TTenderlyEnv): TTenderlyRuntime { } const configuredChains = canonicalChains.reduce((accumulator, chain) => { - const rawExecutionChainId = readEnvString(env[`VITE_TENDERLY_CHAIN_ID_FOR_${chain.id}`]) - const rawRpcUri = readEnvString(env[`VITE_TENDERLY_RPC_URI_FOR_${chain.id}`]) - const rawExplorerUri = readEnvString(env[`VITE_TENDERLY_EXPLORER_URI_FOR_${chain.id}`]) + const rawExecutionChainId = readEnvString(env[`NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_${chain.id}`]) + const rawRpcUri = readEnvString(env[`NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_${chain.id}`]) + const rawExplorerUri = readEnvString(env[`NEXT_PUBLIC_TENDERLY_EXPLORER_URI_FOR_${chain.id}`]) const hasAnyConfig = Boolean(rawExecutionChainId || rawRpcUri || rawExplorerUri) if (!hasAnyConfig) { @@ -155,7 +156,7 @@ export function parseTenderlyRuntime(env: TTenderlyEnv): TTenderlyRuntime { if (!rawExecutionChainId || !rawRpcUri) { throw new Error( - `Tenderly chain ${chain.id} requires both VITE_TENDERLY_CHAIN_ID_FOR_${chain.id} and VITE_TENDERLY_RPC_URI_FOR_${chain.id}` + `Tenderly chain ${chain.id} requires both NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_${chain.id} and NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_${chain.id}` ) } @@ -223,11 +224,11 @@ function readStoredTenderlyModePreference(): string | null | undefined { const disabledTenderlyRuntime = parseTenderlyRuntime({}) const tenderlyModeCanToggle = canToggleTenderlyModeForRuntime({ - isDev: import.meta.env.DEV, + isDev: env.DEV, hostname: typeof window === 'undefined' ? undefined : window.location.hostname }) -export const tenderlyConfiguredRuntime = parseTenderlyRuntime(import.meta.env) +export const tenderlyConfiguredRuntime = parseTenderlyRuntime(env) export const tenderlyRuntime = resolveInitialTenderlyModeEnabled({ isConfigured: tenderlyConfiguredRuntime.isEnabled, canToggle: tenderlyModeCanToggle, diff --git a/api/tenderly.helpers.ts b/src/config/tenderlyServer.ts similarity index 90% rename from api/tenderly.helpers.ts rename to src/config/tenderlyServer.ts index 5b22c0077..e9de44863 100644 --- a/api/tenderly.helpers.ts +++ b/src/config/tenderlyServer.ts @@ -4,8 +4,8 @@ import type { TTenderlyPanelStatus, TTenderlyRevertResponse, TTenderlySnapshotRecord -} from '../src/components/shared/types/tenderly' -import { canonicalChains } from '../src/config/chainDefinitions' +} from '@/components/shared/types/tenderly' +import { canonicalChains } from '@/config/chainDefinitions' type TTenderlyServerEnv = Record @@ -26,13 +26,13 @@ function isTruthyEnvValue(value: string | undefined): boolean { } export function parseTenderlyServerChains(env: TTenderlyServerEnv): TTenderlyServerChainConfig[] { - if (!isTruthyEnvValue(env.VITE_TENDERLY_MODE)) { + if (!isTruthyEnvValue(env.NEXT_PUBLIC_TENDERLY_MODE)) { return [] } return canonicalChains.reduce((accumulator, chain) => { - const rawExecutionChainId = readEnvString(env[`VITE_TENDERLY_CHAIN_ID_FOR_${chain.id}`]) - const rawRpcUri = readEnvString(env[`VITE_TENDERLY_RPC_URI_FOR_${chain.id}`]) + const rawExecutionChainId = readEnvString(env[`NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_${chain.id}`]) + const rawRpcUri = readEnvString(env[`NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_${chain.id}`]) const rawAdminRpcUri = readEnvString(env[`TENDERLY_ADMIN_RPC_URI_FOR_${chain.id}`]) const hasAnyConfig = Boolean(rawExecutionChainId || rawRpcUri || rawAdminRpcUri) @@ -42,7 +42,7 @@ export function parseTenderlyServerChains(env: TTenderlyServerEnv): TTenderlySer if (!rawExecutionChainId || !rawRpcUri) { throw new Error( - `Tenderly chain ${chain.id} requires both VITE_TENDERLY_CHAIN_ID_FOR_${chain.id} and VITE_TENDERLY_RPC_URI_FOR_${chain.id}` + `Tenderly chain ${chain.id} requires both NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_${chain.id} and NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_${chain.id}` ) } @@ -65,7 +65,7 @@ export function parseTenderlyServerChains(env: TTenderlyServerEnv): TTenderlySer export function buildTenderlyPanelStatus(env: TTenderlyServerEnv): TTenderlyPanelStatus { return { - isTenderlyModeEnabled: isTruthyEnvValue(env.VITE_TENDERLY_MODE), + isTenderlyModeEnabled: isTruthyEnvValue(env.NEXT_PUBLIC_TENDERLY_MODE), configuredChains: parseTenderlyServerChains(env).map((chain) => ({ canonicalChainId: chain.canonicalChainId, canonicalChainName: chain.canonicalChainName, diff --git a/src/config/wagmi.ts b/src/config/wagmi.ts index 902c6c820..890972eab 100644 --- a/src/config/wagmi.ts +++ b/src/config/wagmi.ts @@ -1,3 +1,4 @@ +import { env } from '@/env' import { registerConfig } from '@shared/utils/wagmi' import { connectorsForWallets } from '@rainbow-me/rainbowkit' import { @@ -14,8 +15,8 @@ import { supportedAppChains, supportedWalletChains } from './supportedChains' import { getWagmiConfigChains } from './wagmiChains' import { buildTransports } from './wagmiTransports' -const projectId = import.meta.env.VITE_WALLETCONNECT_PROJECT_ID as string -const appName = (import.meta.env.VITE_WALLETCONNECT_PROJECT_NAME as string) || 'Yearn Finance' +const projectId = env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID as string +const appName = (env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_NAME as string) || 'Yearn Finance' const connectors = connectorsForWallets( [ diff --git a/src/config/wagmiTransports.ts b/src/config/wagmiTransports.ts index 54d519b19..c85c0cb26 100644 --- a/src/config/wagmiTransports.ts +++ b/src/config/wagmiTransports.ts @@ -2,9 +2,10 @@ import { getNetwork, getRpcUriFor } from '@shared/utils/wagmi' import type { Chain, Transport } from 'viem' import { mainnet } from 'viem/chains' import { fallback, http } from 'wagmi' +import { env } from '@/env' function getLegacyRpcUri(chainId: number): string { - const value = import.meta.env.VITE_JSON_RPC_URI?.[chainId] || import.meta.env.VITE_JSON_RPC_URL?.[chainId] + const value = env.NEXT_PUBLIC_JSON_RPC_URI?.[chainId] || env.NEXT_PUBLIC_JSON_RPC_URL?.[chainId] return typeof value === 'string' ? value.trim() : '' } diff --git a/src/contexts/useDevFlags.tsx b/src/contexts/useDevFlags.tsx index e9e4e54b2..f1263bc05 100644 --- a/src/contexts/useDevFlags.tsx +++ b/src/contexts/useDevFlags.tsx @@ -1,4 +1,5 @@ import { createContext, type ReactElement, type ReactNode, useCallback, useContext, useMemo, useState } from 'react' +import { env } from '@/env' export type HeaderDisplayMode = 'collapsible' | 'full' | 'minimal' | 'sticky-name' @@ -20,8 +21,7 @@ const DEFAULT_FLAGS: DevFlagsContextValue = { const DevFlagsContext = createContext(undefined) const HEADER_DISPLAY_MODE_STORAGE_KEY = 'dev-header-display-mode' -const ENABLE_TOOLBAR = - !import.meta.env.PROD || import.meta.env.VITE_ENABLE_DEV_TOOLBAR === 'true' || import.meta.env.MODE !== 'production' +const ENABLE_TOOLBAR = !env.PROD || env.NEXT_PUBLIC_ENABLE_DEV_TOOLBAR === 'true' || env.MODE !== 'production' const VALID_MODES: HeaderDisplayMode[] = ['collapsible', 'full', 'minimal', 'sticky-name'] diff --git a/src/env.ts b/src/env.ts new file mode 100644 index 000000000..175e4d321 --- /dev/null +++ b/src/env.ts @@ -0,0 +1,69 @@ +type TClientEnv = { + readonly BASE_URL: string + readonly DEV: boolean + readonly MODE: string + readonly PROD: boolean + readonly [key: `NEXT_PUBLIC_${string}`]: string | undefined + readonly [key: string]: string | boolean | undefined +} + +export const env: TClientEnv = { + BASE_URL: '/', + DEV: process.env.NODE_ENV !== 'production', + MODE: process.env.NODE_ENV || 'development', + PROD: process.env.NODE_ENV === 'production', + NEXT_PUBLIC_ALCHEMY_KEY: process.env.NEXT_PUBLIC_ALCHEMY_KEY, + NEXT_PUBLIC_BALANCE_SOURCE: process.env.NEXT_PUBLIC_BALANCE_SOURCE, + NEXT_PUBLIC_BASE_YEARN_ASSETS_URI: process.env.NEXT_PUBLIC_BASE_YEARN_ASSETS_URI, + NEXT_PUBLIC_CLUSTERS_API_KEY: process.env.NEXT_PUBLIC_CLUSTERS_API_KEY, + NEXT_PUBLIC_CLUSTERS_API_URL: process.env.NEXT_PUBLIC_CLUSTERS_API_URL, + NEXT_PUBLIC_ENABLE_DEV_TOOLBAR: process.env.NEXT_PUBLIC_ENABLE_DEV_TOOLBAR, + NEXT_PUBLIC_ENSO_DISABLED: process.env.NEXT_PUBLIC_ENSO_DISABLED, + NEXT_PUBLIC_INFURA_PROJECT_ID: process.env.NEXT_PUBLIC_INFURA_PROJECT_ID, + NEXT_PUBLIC_JSON_RPC_URI: process.env.NEXT_PUBLIC_JSON_RPC_URI, + NEXT_PUBLIC_JSON_RPC_URL: process.env.NEXT_PUBLIC_JSON_RPC_URL, + NEXT_PUBLIC_KNOWN_ENS: process.env.NEXT_PUBLIC_KNOWN_ENS, + NEXT_PUBLIC_KONG_GQL_URL: process.env.NEXT_PUBLIC_KONG_GQL_URL, + NEXT_PUBLIC_KONG_REST_URL: process.env.NEXT_PUBLIC_KONG_REST_URL, + NEXT_PUBLIC_PARTNER_ID_ADDRESS: process.env.NEXT_PUBLIC_PARTNER_ID_ADDRESS, + NEXT_PUBLIC_PLAUSIBLE_TRACK_LOCALHOST: process.env.NEXT_PUBLIC_PLAUSIBLE_TRACK_LOCALHOST, + NEXT_PUBLIC_SHOULD_USE_FORKNET: process.env.NEXT_PUBLIC_SHOULD_USE_FORKNET, + NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID, + NEXT_PUBLIC_WALLETCONNECT_PROJECT_NAME: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_NAME, + NEXT_PUBLIC_YDAEMON_BASE_URI: process.env.NEXT_PUBLIC_YDAEMON_BASE_URI, + NEXT_PUBLIC_YVUSD_APR_SERVICE_API: process.env.NEXT_PUBLIC_YVUSD_APR_SERVICE_API, + NEXT_PUBLIC_RPC_URI_FOR_1: process.env.NEXT_PUBLIC_RPC_URI_FOR_1, + NEXT_PUBLIC_RPC_URI_FOR_10: process.env.NEXT_PUBLIC_RPC_URI_FOR_10, + NEXT_PUBLIC_RPC_URI_FOR_100: process.env.NEXT_PUBLIC_RPC_URI_FOR_100, + NEXT_PUBLIC_RPC_URI_FOR_137: process.env.NEXT_PUBLIC_RPC_URI_FOR_137, + NEXT_PUBLIC_RPC_URI_FOR_146: process.env.NEXT_PUBLIC_RPC_URI_FOR_146, + NEXT_PUBLIC_RPC_URI_FOR_250: process.env.NEXT_PUBLIC_RPC_URI_FOR_250, + NEXT_PUBLIC_RPC_URI_FOR_8453: process.env.NEXT_PUBLIC_RPC_URI_FOR_8453, + NEXT_PUBLIC_RPC_URI_FOR_42161: process.env.NEXT_PUBLIC_RPC_URI_FOR_42161, + NEXT_PUBLIC_RPC_URI_FOR_747474: process.env.NEXT_PUBLIC_RPC_URI_FOR_747474, + NEXT_PUBLIC_TENDERLY_MODE: process.env.NEXT_PUBLIC_TENDERLY_MODE, + NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_1: process.env.NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_1, + NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_1: process.env.NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_1, + NEXT_PUBLIC_TENDERLY_EXPLORER_URI_FOR_1: process.env.NEXT_PUBLIC_TENDERLY_EXPLORER_URI_FOR_1, + NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_10: process.env.NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_10, + NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_10: process.env.NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_10, + NEXT_PUBLIC_TENDERLY_EXPLORER_URI_FOR_10: process.env.NEXT_PUBLIC_TENDERLY_EXPLORER_URI_FOR_10, + NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_137: process.env.NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_137, + NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_137: process.env.NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_137, + NEXT_PUBLIC_TENDERLY_EXPLORER_URI_FOR_137: process.env.NEXT_PUBLIC_TENDERLY_EXPLORER_URI_FOR_137, + NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_146: process.env.NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_146, + NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_146: process.env.NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_146, + NEXT_PUBLIC_TENDERLY_EXPLORER_URI_FOR_146: process.env.NEXT_PUBLIC_TENDERLY_EXPLORER_URI_FOR_146, + NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_250: process.env.NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_250, + NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_250: process.env.NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_250, + NEXT_PUBLIC_TENDERLY_EXPLORER_URI_FOR_250: process.env.NEXT_PUBLIC_TENDERLY_EXPLORER_URI_FOR_250, + NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_8453: process.env.NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_8453, + NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_8453: process.env.NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_8453, + NEXT_PUBLIC_TENDERLY_EXPLORER_URI_FOR_8453: process.env.NEXT_PUBLIC_TENDERLY_EXPLORER_URI_FOR_8453, + NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_42161: process.env.NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_42161, + NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_42161: process.env.NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_42161, + NEXT_PUBLIC_TENDERLY_EXPLORER_URI_FOR_42161: process.env.NEXT_PUBLIC_TENDERLY_EXPLORER_URI_FOR_42161, + NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_747474: process.env.NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_747474, + NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_747474: process.env.NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_747474, + NEXT_PUBLIC_TENDERLY_EXPLORER_URI_FOR_747474: process.env.NEXT_PUBLIC_TENDERLY_EXPLORER_URI_FOR_747474 +} diff --git a/src/hooks/usePlausible.ts b/src/hooks/usePlausible.ts index 60ae79ee0..cd9b320a3 100644 --- a/src/hooks/usePlausible.ts +++ b/src/hooks/usePlausible.ts @@ -1,12 +1,42 @@ -import { init, track } from '@plausible-analytics/tracker' +import { useCallback, useEffect } from 'react' +import { env } from '@/env' -init({ - domain: 'yearn.fi', - endpoint: '/proxy/plausible/api/event', - captureOnLocalhost: import.meta.env.VITE_PLAUSIBLE_TRACK_LOCALHOST === 'true', - autoCapturePageviews: true -}) +type TPlausibleTracker = typeof import('@plausible-analytics/tracker') +type TTrack = TPlausibleTracker['track'] + +let isInitialized = false +let trackEvent: TTrack | undefined +let initializationPromise: Promise | undefined + +export function initializePlausible(): void { + if (isInitialized || initializationPromise || typeof window === 'undefined') { + return + } + + initializationPromise = import('@plausible-analytics/tracker').then(({ init, track }) => { + trackEvent = track + init({ + domain: 'yearn.fi', + endpoint: '/proxy/plausible/api/event', + captureOnLocalhost: env.NEXT_PUBLIC_PLAUSIBLE_TRACK_LOCALHOST === 'true', + autoCapturePageviews: true + }) + isInitialized = true + }) +} export function usePlausible() { - return track + useEffect(() => { + initializePlausible() + }, []) + + return useCallback((...args: Parameters): void => { + initializePlausible() + if (trackEvent) { + trackEvent(...args) + return + } + + void initializationPromise?.then(() => trackEvent?.(...args)) + }, []) } diff --git a/src/main.tsx b/src/main.tsx deleted file mode 100644 index bd1837dd2..000000000 --- a/src/main.tsx +++ /dev/null @@ -1,55 +0,0 @@ -// Polyfill BigInt serialization for React DevTools compatibility -// React DevTools tries to JSON.stringify component props/state for display, -// but BigInt values can't be serialized natively. This converts them to strings. -;(BigInt.prototype as unknown as { toJSON: () => string }).toJSON = function () { - return this.toString() -} - -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import { BrowserRouter } from 'react-router' -import App from './App' -import '../style.css' -import { disableServiceWorkerDev } from './utils/disableServiceWorkerDev' - -// In dev, proactively disable any existing ServiceWorkers that could intercept cross-origin requests -if (import.meta.env.DEV) { - void disableServiceWorkerDev() -} - -// Handle stale chunk errors during deployments -// When a new deployment happens, chunk hashes change and old chunks return 404 or HTML instead of JS -// This catches those errors and reloads the page to get fresh chunks -window.addEventListener('vite:preloadError', (event) => { - const RELOAD_KEY = 'vite-preload-error-reload-attempt' - const MAX_RETRIES = 1 - const RETRY_WINDOW_MS = 10000 // Reset counter after 10 seconds - - const now = Date.now() - const stored = sessionStorage.getItem(RELOAD_KEY) - const data = stored ? JSON.parse(stored) : { count: 0, timestamp: now } - - // Reset counter if enough time has passed - if (now - data.timestamp > RETRY_WINDOW_MS) { - data.count = 0 - data.timestamp = now - } - - if (data.count < MAX_RETRIES) { - console.warn('Vite preload error detected, reloading page...', event) - data.count++ - sessionStorage.setItem(RELOAD_KEY, JSON.stringify(data)) - window.location.reload() - } else { - console.error('Vite preload error: Max reload attempts reached. Please manually refresh the page.', event) - sessionStorage.removeItem(RELOAD_KEY) - } -}) - -createRoot(document.getElementById('root')!).render( - - - - - -) diff --git a/src/navigation/NextNavigationProvider.tsx b/src/navigation/NextNavigationProvider.tsx new file mode 100644 index 000000000..e20803cae --- /dev/null +++ b/src/navigation/NextNavigationProvider.tsx @@ -0,0 +1,33 @@ +'use client' + +import { useParams, usePathname, useRouter, useSearchParams } from 'next/navigation' +import type { ReactElement, ReactNode } from 'react' +import { useMemo } from 'react' +import { NavigationContext, type TNavigationContext, type TRouteParams } from './context' + +export function NextNavigationProvider({ children }: { children: ReactNode }): ReactElement { + const router = useRouter() + const pathname = usePathname() || '/' + const searchParams = useSearchParams() + const params = useParams() + const search = searchParams.toString() + + const value = useMemo(() => { + const normalizedParams = Object.entries(params).reduce((nextParams, [key, value]) => { + nextParams[key] = value + return nextParams + }, {}) + + return { + pathname, + search: search ? `?${search}` : '', + hash: typeof window === 'undefined' ? '' : window.location.hash, + params: normalizedParams, + push: (href, options) => router.push(href, { scroll: options?.scroll }), + replace: (href, options) => router.replace(href, { scroll: options?.scroll }), + go: (delta) => window.history.go(delta) + } + }, [params, pathname, router, search]) + + return {children} +} diff --git a/src/navigation/context.tsx b/src/navigation/context.tsx new file mode 100644 index 000000000..3686cb5a6 --- /dev/null +++ b/src/navigation/context.tsx @@ -0,0 +1,23 @@ +'use client' + +import { createContext } from 'react' + +export type TRouteParams = Record + +export type TNavigateOptions = { + replace?: boolean + scroll?: boolean + state?: unknown +} + +export type TNavigationContext = { + pathname: string + search: string + hash: string + params: TRouteParams + push: (href: string, options?: TNavigateOptions) => void + replace: (href: string, options?: TNavigateOptions) => void + go: (delta: number) => void +} + +export const NavigationContext = createContext(null) diff --git a/src/navigation/react-router.tsx b/src/navigation/react-router.tsx new file mode 100644 index 000000000..96992923d --- /dev/null +++ b/src/navigation/react-router.tsx @@ -0,0 +1,170 @@ +'use client' + +import NextLink from 'next/link' +import type { AnchorHTMLAttributes, ReactElement, ReactNode } from 'react' +import { useCallback, useContext, useEffect, useMemo, useState } from 'react' +import { NavigationContext, type TNavigateOptions, type TNavigationContext } from './context' +import { resolveLinkTarget } from './url' + +type TUrlSearchParamsInit = ConstructorParameters[0] + +export type LinkProps = Omit, 'href'> & { + to: string + children?: ReactNode + preventScrollReset?: boolean + reloadDocument?: boolean + replace?: boolean + state?: unknown +} + +type TLocation = { + pathname: string + search: string + hash: string + state: unknown + key: string +} + +type TNavigateFunction = (to: string | number, options?: TNavigateOptions) => void + +function parseEntry(entry: string): Pick { + const url = new URL(entry, 'https://yearn.fi') + return { + pathname: url.pathname, + search: url.search, + hash: url.hash + } +} + +function getBrowserNavigationContext(): TNavigationContext { + const location = typeof window === 'undefined' ? parseEntry('/') : window.location + + return { + pathname: location.pathname, + search: location.search, + hash: location.hash, + params: {}, + push: (href) => window.history.pushState(null, '', href), + replace: (href) => window.history.replaceState(null, '', href), + go: (delta) => window.history.go(delta) + } +} + +export function MemoryRouter({ + children, + initialEntries = ['/'] +}: { + children: ReactNode + initialEntries?: string[] +}): ReactElement { + const [entry, setEntry] = useState(initialEntries[0] || '/') + + const value = useMemo(() => { + const parsedEntry = parseEntry(entry) + return { + ...parsedEntry, + params: {}, + push: (href) => setEntry(href), + replace: (href) => setEntry(href), + go: () => undefined + } + }, [entry]) + + return {children} +} + +export function BrowserRouter({ children }: { children: ReactNode }): ReactElement { + return <>{children} +} + +export function Link(props: LinkProps): ReactElement { + const { to, children, preventScrollReset, reloadDocument, replace, state, ...anchorProps } = props + const { href, isExternal } = resolveLinkTarget(to || '') + + if (isExternal || reloadDocument) { + return ( + + {children} + + ) + } + + return ( + + {children} + + ) +} + +export function Navigate({ to, replace = false }: { to: string; replace?: boolean }): null { + const navigate = useNavigate() + + useEffect(() => { + navigate(to, { replace }) + }, [navigate, replace, to]) + + return null +} + +export function useLocation(): TLocation { + const navigation = useContext(NavigationContext) ?? getBrowserNavigationContext() + + return useMemo( + () => ({ + pathname: navigation.pathname, + search: navigation.search, + hash: navigation.hash, + state: null, + key: `${navigation.pathname}${navigation.search}${navigation.hash}` + }), + [navigation.hash, navigation.pathname, navigation.search] + ) +} + +export function useNavigate(): TNavigateFunction { + const navigation = useContext(NavigationContext) ?? getBrowserNavigationContext() + + return useCallback( + (to, options) => { + if (typeof to === 'number') { + navigation.go(to) + return + } + + if (options?.replace) { + navigation.replace(to, options) + return + } + + navigation.push(to, options) + }, + [navigation] + ) +} + +export function useParams< + TParams extends Record = Record +>(): TParams { + const navigation = useContext(NavigationContext) ?? getBrowserNavigationContext() + return navigation.params as TParams +} + +export function useSearchParams(): [ + URLSearchParams, + (nextInit: TUrlSearchParamsInit, options?: TNavigateOptions) => void +] { + const location = useLocation() + const navigate = useNavigate() + const searchParams = useMemo(() => new URLSearchParams(location.search), [location.search]) + + const setSearchParams = useCallback( + (nextInit: TUrlSearchParamsInit, options?: TNavigateOptions): void => { + const nextSearchParams = new URLSearchParams(nextInit) + const search = nextSearchParams.toString() + navigate(`${location.pathname}${search ? `?${search}` : ''}`, options) + }, + [location.pathname, navigate] + ) + + return [searchParams, setSearchParams] +} diff --git a/src/navigation/url.ts b/src/navigation/url.ts new file mode 100644 index 000000000..fd2fc5740 --- /dev/null +++ b/src/navigation/url.ts @@ -0,0 +1,25 @@ +const FIRST_PARTY_HOSTS = new Set(['yearn.fi', 'www.yearn.fi']) +const DEFAULT_ORIGIN = 'https://yearn.fi' + +function hasExplicitProtocolOrHost(url: string): boolean { + return /^[a-z][a-z\d+.-]*:/i.test(url) || url.startsWith('//') +} + +export function resolveLinkTarget(url: string): { href: string; isExternal: boolean } { + try { + const parsedUrl = new URL(url, DEFAULT_ORIGIN) + const isWebUrl = parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:' + const isExternal = !isWebUrl || !FIRST_PARTY_HOSTS.has(parsedUrl.hostname) + + if (!isExternal && hasExplicitProtocolOrHost(url)) { + return { + href: `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`, + isExternal: false + } + } + + return { href: url, isExternal } + } catch { + return { href: url, isExternal: false } + } +} diff --git a/src/routes.tsx b/src/routes.tsx deleted file mode 100644 index f2a4f779b..000000000 --- a/src/routes.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import PortfolioPage from '@pages/portfolio/index' -import VaultsPage from '@pages/vaults/index' -import type { ReactElement } from 'react' -import { lazy, Suspense } from 'react' -import { Navigate, Route, Routes as RouterRoutes } from 'react-router' - -// Lazy load all page components -const HomePage = lazy(() => import('@pages/landing')) -const VaultsDetailPage = lazy(() => import('@pages/vaults/[chainID]/[address]')) -const IconListPage = import.meta.env.DEV ? lazy(() => import('@pages/icon-list/index')) : null - -// Loading component -const PageLoader = (): ReactElement => ( -
-
- -
-
-) - -// External redirect component -const ExternalRedirect = ({ to }: { to: string }): ReactElement => { - window.location.href = to - return -} - -// Main routes component -export function Routes(): ReactElement { - return ( - }> - - {/* Home page */} - } /> - - {/* Portfolio page */} - } /> - - {/* Icon inventory page */} - {IconListPage ? } /> : null} - - {/* Unified Vaults routes */} - - } /> - } /> - - - {/* Legacy redirects to new /vaults routes */} - } /> - } /> - } /> - {/* Legacy v3 vault detail alias */} - } /> - } /> - - {/* External redirects */} - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - {/* Catch all - redirect to home */} - } /> - - - ) -} diff --git a/src/server/README.md b/src/server/README.md new file mode 100644 index 000000000..5e4449652 --- /dev/null +++ b/src/server/README.md @@ -0,0 +1,112 @@ +# Server API Handlers + +This directory contains focused API endpoint implementations and shared server-side helpers. Next exposes them through explicit App Router files under `app/api/**/route.ts`. + +There is no standalone Bun API mirror and no catch-all pathname dispatcher. Local development and production both serve `/api/*` through Next. + +## Route Layout + +- `app/api/**/route.ts` files define the public Next route surface and export HTTP methods directly. +- `src/server/**` endpoint modules contain the implementation for each route. +- `src/server/lib/**`, `src/server/optimization/_lib/**`, and `src/server/tenderly.helpers.ts` contain shared service logic. + +## Route Inventory + +| Route | Method | Runtime | Purpose | +|-------|--------|---------|---------| +| `/api/holdings/history` | `GET` | Next Node | Daily holdings chart, USD or ETH-denominated | +| `/api/holdings/progress` | `GET` | Next Node | Durable progress state for long holdings requests | +| `/api/holdings/breakdown` | `GET` | Next Node | Per-vault breakdown for a settled UTC day | +| `/api/holdings/activity` | `GET` | Next Node | Recent classified vault activity | +| `/api/holdings/activity-facets` | `GET` | Next Node | Chain facets for holdings activity filters | +| `/api/holdings/protocol-return/history` | `GET` | Next Node | Protocol-return history for vault exposure | +| `/api/holdings/pnl/simple-history` | `GET` | Next Node | Compatibility alias for protocol-return history | +| `/api/admin/invalidate-cache` | `POST` | Next Node | Lazy vault cache invalidation, admin-protected | +| `/api/enso/status` | `GET` | Next Node | Returns whether `ENSO_API_KEY` is configured | +| `/api/enso/balances` | `GET` | Next Node | Proxies Enso wallet balances | +| `/api/enso/route` | `GET` | Next Node | Proxies Enso route quotes/transactions | +| `/api/optimization/change` | `GET` | Next Node | Latest or historical optimization payloads from Redis | +| `/api/optimization/alignment` | `GET` | Next Node | Envio keeper-event alignment for a selected optimization | +| `/api/optimization/vault-state` | `POST` | Next Node | Live vault and strategy debt state from chain RPCs | +| `/api/yvusd/aprs` | `GET` | Next Node | Proxies the yvUSD APR service | +| `/api/tenderly/status` | `GET` | Next Node | Returns configured local Tenderly chains | +| `/api/tenderly/snapshot` | `POST` | Next Node | Creates a Tenderly EVM snapshot | +| `/api/tenderly/revert` | `POST` | Next Node | Reverts a Tenderly EVM snapshot | +| `/api/tenderly/increase-time` | `POST` | Next Node | Advances Tenderly chain time and optionally mines | +| `/api/tenderly/fund` | `POST` | Next Node | 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 }`. +- `/api/enso/balances` requires `eoaAddress` and requests `chainId=all` upstream. +- `/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. +- Cache headers: `change` uses `public, s-maxage=600, stale-while-revalidate=60`; `alignment` and `vault-state` use `public, s-maxage=60, stale-while-revalidate=30`. + +## 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`. +- Cache header: `public, s-maxage=30, stale-while-revalidate=120`. + +## Vault Metadata + +Vault detail metadata is now generated by Next route metadata in `app/vaults/[chainID]/[address]/page.tsx` and `app/v3/[chainID]/[address]/page.tsx`. The old SPA HTML injection route was removed. + +## Tenderly Local Routes + +Tenderly admin routes are exposed through `app/api/tenderly/**/route.ts` and are blocked unless the request comes from localhost. + +Required env for a configured chain: + +- `NEXT_PUBLIC_TENDERLY_MODE=true`. +- `NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_`. +- `NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_`. +- `TENDERLY_ADMIN_RPC_URI_FOR_` for snapshot, revert, time travel, and funding actions. + +## Environment Variables + +| Variable | Used by | Description | +|----------|---------|-------------| +| `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 | +| `RPC_URI_FOR_` | holdings activity | Optional server-only chain RPC URL for receipt enrichment; falls back to `NEXT_PUBLIC_RPC_URI_FOR_` | +| `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 | +| `ADMIN_SECRET` | holdings admin | Required for `/api/admin/invalidate-cache` | +| `UPSTASH_REDIS_REST_URL_PORTFOLIO` | holdings | Upstash Redis REST URL for holdings cache/progress/rate limits | +| `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` | holdings | Enables holdings debug logs | +| `NEXT_PUBLIC_TENDERLY_MODE` | local Tenderly | Enables Tenderly config parsing | +| `NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_` | local Tenderly | Tenderly execution chain ID for a canonical chain | +| `NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_` | local Tenderly | Public Tenderly RPC URI | +| `TENDERLY_ADMIN_RPC_URI_FOR_` | local Tenderly | Admin Tenderly RPC URI | diff --git a/src/server/admin/invalidate-cache.ts b/src/server/admin/invalidate-cache.ts new file mode 100644 index 000000000..3a4e1f108 --- /dev/null +++ b/src/server/admin/invalidate-cache.ts @@ -0,0 +1,99 @@ +import { ADMIN_POST_CORS_HEADERS, json, noContent, readJsonBody } from '../http' +import { ensureHoldingsStorageInitialized, isHoldingsStorageEnabled } from '../lib/holdings' +import { invalidateVaults, type VaultIdentifier } from '../lib/holdings/services/cache' + +function isValidAddress(address: string): boolean { + return /^0x[a-fA-F0-9]{40}$/.test(address) +} + +interface InvalidateRequestBody { + vaults: Array<{ address: string; chainId: number }> +} + +function validateBody(body: unknown): body is InvalidateRequestBody { + if (!body || typeof body !== 'object') return false + const b = body as Record + if (!Array.isArray(b.vaults)) return false + if (b.vaults.length === 0) return false + + for (const vault of b.vaults) { + if (!vault || typeof vault !== 'object') return false + const v = vault as Record + if (typeof v.address !== 'string' || !isValidAddress(v.address)) return false + if (typeof v.chainId !== 'number' || !Number.isInteger(v.chainId)) return false + } + + return true +} + +export function OPTIONS(): Response { + return noContent(ADMIN_POST_CORS_HEADERS) +} + +export async function POST(request: Request): Promise { + // Check admin secret + const adminSecret = process.env.ADMIN_SECRET + if (!adminSecret) { + return json({ error: 'Admin endpoint not configured' }, { status: 503, headers: ADMIN_POST_CORS_HEADERS }) + } + + const providedSecret = request.headers.get('x-admin-secret') + if (providedSecret !== adminSecret) { + return json({ error: 'Unauthorized' }, { status: 401, headers: ADMIN_POST_CORS_HEADERS }) + } + + // Check Redis storage is enabled + if (!isHoldingsStorageEnabled()) { + return json( + { error: 'Caching not enabled (UPSTASH_REDIS_REST_URL_PORTFOLIO/TOKEN_PORTFOLIO not configured)' }, + { status: 503, headers: ADMIN_POST_CORS_HEADERS } + ) + } + + // Validate request body + const body = await readJsonBody(request) + if (!validateBody(body)) { + return json( + { + error: 'Invalid request body', + expected: { + vaults: [{ address: '0x...', chainId: 1 }] + } + }, + { status: 400, headers: ADMIN_POST_CORS_HEADERS } + ) + } + + try { + await ensureHoldingsStorageInitialized() + if (!isHoldingsStorageEnabled()) { + return json( + { error: 'Caching not enabled (UPSTASH_REDIS_REST_URL_PORTFOLIO/TOKEN_PORTFOLIO not configured)' }, + { status: 503, headers: ADMIN_POST_CORS_HEADERS } + ) + } + + const vaults: VaultIdentifier[] = body.vaults.map((v) => ({ + address: v.address, + chainId: v.chainId + })) + + const invalidatedCount = await invalidateVaults(vaults) + const timestamp = new Date().toISOString() + + return json( + { + success: true, + invalidated: invalidatedCount, + vaults: vaults.map((v) => `${v.chainId}:${v.address.toLowerCase()}`), + timestamp + }, + { headers: ADMIN_POST_CORS_HEADERS } + ) + } catch (error) { + console.error('[Admin] Invalidate cache error:', error) + return json({ error: 'Failed to invalidate cache' }, { status: 500, headers: ADMIN_POST_CORS_HEADERS }) + } +} + +export default POST diff --git a/src/server/enso/balances.ts b/src/server/enso/balances.ts new file mode 100644 index 000000000..fea69aab0 --- /dev/null +++ b/src/server/enso/balances.ts @@ -0,0 +1,65 @@ +import { GET_CORS_HEADERS, json, noContent, queryString } from '../http' +import { ENSO_BALANCES_CACHE_CONTROL } from './cache' + +const ENSO_API_BASE = 'https://api.enso.finance' + +export function OPTIONS(): Response { + return noContent(GET_CORS_HEADERS) +} + +export async function GET(request: Request): Promise { + const eoaAddress = queryString(request, 'eoaAddress') + + if (!eoaAddress) { + return json({ error: 'Missing or invalid eoaAddress' }, { status: 400, headers: GET_CORS_HEADERS }) + } + + const apiKey = process.env.ENSO_API_KEY + if (!apiKey) { + console.error('ENSO_API_KEY not configured') + return json({ error: 'Enso API not configured' }, { status: 500, headers: GET_CORS_HEADERS }) + } + + 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 json( + { + error: 'Enso API error', + status: response.status, + details: errorText + }, + { status: response.status, headers: GET_CORS_HEADERS } + ) + } + + const data = await response.json() + + return json(data, { + headers: { + ...GET_CORS_HEADERS, + 'Cache-Control': ENSO_BALANCES_CACHE_CONTROL + } + }) + } catch (error) { + console.error('Error proxying Enso request:', error) + return json({ error: 'Internal server error' }, { status: 500, headers: GET_CORS_HEADERS }) + } +} + +export default GET diff --git a/api/enso/cache.test.ts b/src/server/enso/cache.test.ts similarity index 100% rename from api/enso/cache.test.ts rename to src/server/enso/cache.test.ts diff --git a/api/enso/cache.ts b/src/server/enso/cache.ts similarity index 100% rename from api/enso/cache.ts rename to src/server/enso/cache.ts diff --git a/src/server/enso/route.ts b/src/server/enso/route.ts new file mode 100644 index 000000000..dea23e032 --- /dev/null +++ b/src/server/enso/route.ts @@ -0,0 +1,84 @@ +import { GET_CORS_HEADERS, json, noContent, queryString } from '../http' + +const ENSO_API_BASE = 'https://api.enso.finance' + +export function OPTIONS(): Response { + return noContent(GET_CORS_HEADERS) +} + +export async function GET(request: Request): Promise { + const fromAddress = queryString(request, 'fromAddress') + const chainId = queryString(request, 'chainId') + const tokenIn = queryString(request, 'tokenIn') + const tokenOut = queryString(request, 'tokenOut') + const amountIn = queryString(request, 'amountIn') + const slippage = queryString(request, 'slippage') + const routingStrategy = queryString(request, 'routingStrategy') + const destinationChainId = queryString(request, 'destinationChainId') + const receiver = queryString(request, 'receiver') + + if (!fromAddress) { + return json({ error: 'Missing or invalid fromAddress' }, { status: 400, headers: GET_CORS_HEADERS }) + } + if (!chainId) { + return json({ error: 'Missing or invalid chainId' }, { status: 400, headers: GET_CORS_HEADERS }) + } + if (!tokenIn) { + return json({ error: 'Missing or invalid tokenIn' }, { status: 400, headers: GET_CORS_HEADERS }) + } + if (!tokenOut) { + return json({ error: 'Missing or invalid tokenOut' }, { status: 400, headers: GET_CORS_HEADERS }) + } + if (!amountIn) { + return json({ error: 'Missing or invalid amountIn' }, { status: 400, headers: GET_CORS_HEADERS }) + } + + const apiKey = process.env.ENSO_API_KEY + if (!apiKey) { + console.error('ENSO_API_KEY not configured') + return json({ error: 'Enso API not configured' }, { status: 500, headers: GET_CORS_HEADERS }) + } + + try { + const params = new URLSearchParams({ + fromAddress, + chainId, + tokenIn, + tokenOut, + amountIn, + slippage: (slippage as string) || '100' + }) + + if (destinationChainId) { + params.set('destinationChainId', destinationChainId) + } + if (receiver) { + params.set('receiver', receiver) + } + if (routingStrategy) { + 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 json(data, { status: response.status, headers: GET_CORS_HEADERS }) + } + + return json(data, { headers: GET_CORS_HEADERS }) + } catch (error) { + console.error('Error proxying Enso route request:', error) + return json({ error: 'Internal server error' }, { status: 500, headers: GET_CORS_HEADERS }) + } +} + +export default GET diff --git a/src/server/enso/status.ts b/src/server/enso/status.ts new file mode 100644 index 000000000..88789b021 --- /dev/null +++ b/src/server/enso/status.ts @@ -0,0 +1,14 @@ +import { GET_CORS_HEADERS, json, noContent } from '../http' + +export function OPTIONS(): Response { + return noContent(GET_CORS_HEADERS) +} + +export function GET(): Response { + const apiKey = process.env.ENSO_API_KEY + const isConfigured = !!apiKey + + return json({ configured: isConfigured }, { headers: GET_CORS_HEADERS }) +} + +export default GET diff --git a/api/holdings/activity-facets.test.ts b/src/server/holdings/activity-facets.test.ts similarity index 100% rename from api/holdings/activity-facets.test.ts rename to src/server/holdings/activity-facets.test.ts diff --git a/src/server/holdings/activity-facets.ts b/src/server/holdings/activity-facets.ts new file mode 100644 index 000000000..5dc8c2649 --- /dev/null +++ b/src/server/holdings/activity-facets.ts @@ -0,0 +1,131 @@ +import { GET_CORS_HEADERS, getClientIdentifier, json, noContent, queryValue } from '../http' +import type { VaultVersion } from '../lib/holdings' +import { checkRateLimit, ensureHoldingsStorageInitialized } from '../lib/holdings' + +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' +} + +function parsePositiveInteger(value: string | string[] | undefined, fallback: number, max: number): number { + const rawValue = Array.isArray(value) ? value[0] : value + const parsedValue = Number(rawValue) + + return Number.isInteger(parsedValue) && parsedValue > 0 ? Math.min(parsedValue, max) : fallback +} + +function parseNonNegativeInteger(value: string | string[] | undefined): number { + const rawValue = Array.isArray(value) ? value[0] : value + const parsedValue = Number(rawValue) + + return Number.isInteger(parsedValue) && parsedValue >= 0 ? parsedValue : 0 +} + +export function OPTIONS(): Response { + return noContent(GET_CORS_HEADERS) +} + +export async function GET(request: Request): Promise { + try { + await ensureHoldingsStorageInitialized() + } catch (error) { + console.error('Holdings activity facets storage initialization error:', error) + return json({ error: 'Failed to initialize holdings storage' }, { status: 500, headers: GET_CORS_HEADERS }) + } + + const clientId = getClientIdentifier(request) + const rateCheck = await checkRateLimit(clientId) + if (!rateCheck.allowed) { + return json( + { error: 'Too many requests', retryAfter: rateCheck.retryAfter }, + { status: 429, headers: { ...GET_CORS_HEADERS, 'Retry-After': String(rateCheck.retryAfter) } } + ) + } + + const envioUrl = process.env.ENVIO_GRAPHQL_URL + if (!envioUrl) { + return json( + { + error: 'Holdings activity API not configured', + details: 'ENVIO_GRAPHQL_URL environment variable is not set. This feature requires a running Envio indexer.' + }, + { status: 503, headers: GET_CORS_HEADERS } + ) + } + + const address = queryValue(request, 'address') + const versionParam = queryValue(request, 'version') + const limitPerSourceParam = queryValue(request, 'limitPerSource') + const offsetPerSourceParam = queryValue(request, 'offsetPerSource') + + if (!address || typeof address !== 'string') { + return json({ error: 'Missing required parameter: address' }, { status: 400, headers: GET_CORS_HEADERS }) + } + + if (!isValidAddress(address)) { + return json({ error: 'Invalid Ethereum address' }, { status: 400, headers: GET_CORS_HEADERS }) + } + + try { + const { fetchRecentAddressScopedActivityEvents } = await import('../lib/holdings') + const version = parseVersion(versionParam) + const limitPerSource = parsePositiveInteger(limitPerSourceParam, 250, 1000) + const offsetPerSource = parseNonNegativeInteger(offsetPerSourceParam) + const events = await fetchRecentAddressScopedActivityEvents( + address, + version, + limitPerSource, + undefined, + offsetPerSource + ) + const hasMore = + events.hasMoreDeposits || events.hasMoreWithdrawals || events.hasMoreTransfersIn || events.hasMoreTransfersOut + const chainIds = Array.from( + new Set( + [...events.deposits, ...events.withdrawals, ...events.transfersIn, ...events.transfersOut].map( + (event) => event.chainId + ) + ) + ).sort((firstChainId, secondChainId) => firstChainId - secondChainId) + + return json( + { + address: address.toLowerCase(), + version, + facets: { chainIds }, + pageInfo: { + hasMore, + nextOffsetPerSource: hasMore ? offsetPerSource + limitPerSource : null + } + }, + { + headers: { + ...GET_CORS_HEADERS, + 'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=900' + } + } + ) + } 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 json( + { + error: 'Failed to fetch holdings activity facets', + message, + stack + }, + { status: 502, headers: GET_CORS_HEADERS } + ) + } + + return json({ error: 'Failed to fetch holdings activity facets' }, { status: 502, headers: GET_CORS_HEADERS }) + } +} + +export default GET diff --git a/api/holdings/activity.test.ts b/src/server/holdings/activity.test.ts similarity index 57% rename from api/holdings/activity.test.ts rename to src/server/holdings/activity.test.ts index b05596de9..2a0e28032 100644 --- a/api/holdings/activity.test.ts +++ b/src/server/holdings/activity.test.ts @@ -12,36 +12,8 @@ vi.mock('../lib/holdings', () => ({ getHoldingsActivity: getHoldingsActivityMock })) -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 - } - } +function createRequest(query: Record): Request { + return new Request(`https://yearn.fi/api/holdings/activity?${new URLSearchParams(query)}`) } describe('holdings activity route', () => { @@ -83,27 +55,29 @@ describe('holdings activity route', () => { }) const { default: handler } = await import('./activity') - const req = { - method: 'GET', - query: { + const response = await handler( + createRequest({ address: TEST_ADDRESS, limit: '10', offset: '0' - }, - headers: {} - } as any - const res = createMockResponse() + }) + ) - await handler(req, res as any) - - expect(getHoldingsActivityMock).toHaveBeenCalledWith(TEST_ADDRESS, 'all', 10, 0, { - type: 'all', - chainId: null, - startTimestamp: null, - endTimestamp: null - }) - expect(res.statusCode).toBe(200) - expect(res.body).toEqual({ + expect(getHoldingsActivityMock).toHaveBeenCalledWith( + TEST_ADDRESS, + 'all', + 10, + 0, + { + type: 'all', + chainId: null, + startTimestamp: null, + endTimestamp: null + }, + false + ) + expect(response.status).toBe(200) + await expect(response.json()).resolves.toEqual({ address: TEST_ADDRESS, version: 'all', limit: 10, @@ -132,41 +106,6 @@ describe('holdings activity route', () => { }) }) - it('allows large initial activity pages up to 500 rows', async () => { - getHoldingsActivityMock.mockResolvedValue({ - address: TEST_ADDRESS, - version: 'all', - limit: 500, - offset: 0, - pageInfo: { - hasMore: false, - nextOffset: null - }, - entries: [] - }) - - const { default: handler } = await import('./activity') - const req = { - method: 'GET', - query: { - address: TEST_ADDRESS, - limit: '500' - }, - headers: {} - } as any - const res = createMockResponse() - - await handler(req, res as any) - - expect(getHoldingsActivityMock).toHaveBeenCalledWith(TEST_ADDRESS, 'all', 500, 0, { - type: 'all', - chainId: null, - startTimestamp: null, - endTimestamp: null - }) - expect(res.statusCode).toBe(200) - }) - it('returns an empty collection when no indexed activity exists', async () => { getHoldingsActivityMock.mockResolvedValue({ address: TEST_ADDRESS, @@ -181,19 +120,14 @@ describe('holdings activity route', () => { }) const { default: handler } = await import('./activity') - const req = { - method: 'GET', - query: { + const response = await handler( + createRequest({ address: TEST_ADDRESS - }, - headers: {} - } as any - const res = createMockResponse() - - await handler(req, res as any) + }) + ) - expect(res.statusCode).toBe(200) - expect(res.body).toEqual({ + expect(response.status).toBe(200) + await expect(response.json()).resolves.toEqual({ address: TEST_ADDRESS, version: 'all', limit: 10, @@ -220,28 +154,30 @@ describe('holdings activity route', () => { }) const { default: handler } = await import('./activity') - const req = { - method: 'GET', - query: { + const response = await handler( + createRequest({ address: TEST_ADDRESS, type: 'withdraw', chainId: '137', startTimestamp: '1776729600', endTimestamp: '1777334399' - }, - headers: {} - } as any - const res = createMockResponse() + }) + ) - await handler(req, res as any) - - expect(getHoldingsActivityMock).toHaveBeenCalledWith(TEST_ADDRESS, 'all', 10, 0, { - type: 'withdraw', - chainId: 137, - startTimestamp: 1776729600, - endTimestamp: 1777334399 - }) - expect(res.statusCode).toBe(200) + expect(getHoldingsActivityMock).toHaveBeenCalledWith( + TEST_ADDRESS, + 'all', + 10, + 0, + { + type: 'withdraw', + chainId: 137, + startTimestamp: 1776729600, + endTimestamp: 1777334399 + }, + false + ) + expect(response.status).toBe(200) }) it('passes through the transfer activity filter', async () => { @@ -258,25 +194,27 @@ describe('holdings activity route', () => { }) const { default: handler } = await import('./activity') - const req = { - method: 'GET', - query: { + const response = await handler( + createRequest({ address: TEST_ADDRESS, type: 'transfer' - }, - headers: {} - } as any - const res = createMockResponse() + }) + ) - await handler(req, res as any) - - expect(getHoldingsActivityMock).toHaveBeenCalledWith(TEST_ADDRESS, 'all', 10, 0, { - type: 'transfer', - chainId: null, - startTimestamp: null, - endTimestamp: null - }) - expect(res.statusCode).toBe(200) + expect(getHoldingsActivityMock).toHaveBeenCalledWith( + TEST_ADDRESS, + 'all', + 10, + 0, + { + type: 'transfer', + chainId: null, + startTimestamp: null, + endTimestamp: null + }, + false + ) + expect(response.status).toBe(200) }) it('passes through the swap activity filter', async () => { @@ -293,24 +231,26 @@ describe('holdings activity route', () => { }) const { default: handler } = await import('./activity') - const req = { - method: 'GET', - query: { + const response = await handler( + createRequest({ address: TEST_ADDRESS, type: 'swap' - }, - headers: {} - } as any - const res = createMockResponse() - - await handler(req, res as any) + }) + ) - expect(getHoldingsActivityMock).toHaveBeenCalledWith(TEST_ADDRESS, 'all', 10, 0, { - type: 'swap', - chainId: null, - startTimestamp: null, - endTimestamp: null - }) - expect(res.statusCode).toBe(200) + expect(getHoldingsActivityMock).toHaveBeenCalledWith( + TEST_ADDRESS, + 'all', + 10, + 0, + { + type: 'swap', + chainId: null, + startTimestamp: null, + endTimestamp: null + }, + false + ) + expect(response.status).toBe(200) }) }) diff --git a/api/holdings/activity.ts b/src/server/holdings/activity.ts similarity index 52% rename from api/holdings/activity.ts rename to src/server/holdings/activity.ts index cd2d0404b..eb7d60918 100644 --- a/api/holdings/activity.ts +++ b/src/server/holdings/activity.ts @@ -1,30 +1,7 @@ -import type { VercelRequest, VercelResponse } from '@vercel/node' -import { setVercelCdnCacheHeaders } from '../lib/cacheHeaders' +import { GET_CORS_HEADERS, getClientIdentifier, json, noContent, queryValue } from '../http' import type { HoldingsActivityTypeFilter, VaultVersion } from '../lib/holdings' import { checkRateLimit, ensureHoldingsStorageInitialized } from '../lib/holdings' -const HOLDINGS_ACTIVITY_CDN_CACHE_CONTROL = 'public, s-maxage=60, stale-while-revalidate=300' - -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) } @@ -41,7 +18,7 @@ function parseLimit(value: string | string[] | undefined): number { return 10 } - return Math.min(Math.max(parsedValue, 1), 500) + return Math.min(Math.max(parsedValue, 1), 50) } function parseOffset(value: string | string[] | undefined): number { @@ -86,58 +63,53 @@ function parseTimestamp(value: string | string[] | undefined): number | null { return Number.isInteger(parsedValue) && parsedValue >= 0 ? parsedValue : 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' }) - } +export function OPTIONS(): Response { + return noContent(GET_CORS_HEADERS) +} +export async function GET(request: Request): Promise { try { await ensureHoldingsStorageInitialized() } catch (error) { console.error('Holdings activity storage initialization error:', error) - return res.status(500).json({ error: 'Failed to initialize holdings storage' }) + return json({ error: 'Failed to initialize holdings storage' }, { status: 500, headers: GET_CORS_HEADERS }) } - const clientId = getClientIdentifier(req) - const rateCheck = await checkRateLimit(clientId, req.headers) + const clientId = getClientIdentifier(request) + const rateCheck = await checkRateLimit(clientId) if (!rateCheck.allowed) { - res.setHeader('Retry-After', String(rateCheck.retryAfter)) - return res.status(429).json({ error: 'Too many requests', retryAfter: rateCheck.retryAfter }) + return json( + { error: 'Too many requests', retryAfter: rateCheck.retryAfter }, + { status: 429, headers: { ...GET_CORS_HEADERS, 'Retry-After': String(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.' - }) + return json( + { + error: 'Holdings activity API not configured', + details: 'ENVIO_GRAPHQL_URL environment variable is not set. This feature requires a running Envio indexer.' + }, + { status: 503, headers: GET_CORS_HEADERS } + ) } - const { - address, - version: versionParam, - limit: limitParam, - offset: offsetParam, - type: typeParam, - chainId: chainIdParam, - startTimestamp: startTimestampParam, - endTimestamp: endTimestampParam - } = req.query + const address = queryValue(request, 'address') + const versionParam = queryValue(request, 'version') + const limitParam = queryValue(request, 'limit') + const offsetParam = queryValue(request, 'offset') + const typeParam = queryValue(request, 'type') + const chainIdParam = queryValue(request, 'chainId') + const startTimestampParam = queryValue(request, 'startTimestamp') + const endTimestampParam = queryValue(request, 'endTimestamp') if (!address || typeof address !== 'string') { - return res.status(400).json({ error: 'Missing required parameter: address' }) + return json({ error: 'Missing required parameter: address' }, { status: 400, headers: GET_CORS_HEADERS }) } if (!isValidAddress(address)) { - return res.status(400).json({ error: 'Invalid Ethereum address' }) + return json({ error: 'Invalid Ethereum address' }, { status: 400, headers: GET_CORS_HEADERS }) } try { @@ -155,21 +127,30 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { } ) - setVercelCdnCacheHeaders(res, HOLDINGS_ACTIVITY_CDN_CACHE_CONTROL) - return res.status(200).json(activity) + return json(activity, { + headers: { + ...GET_CORS_HEADERS, + 'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300' + } + }) } catch (error) { console.error('Holdings activity 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', - message, - stack - }) + return json( + { + error: 'Failed to fetch holdings activity', + message, + stack + }, + { status: 502, headers: GET_CORS_HEADERS } + ) } - return res.status(502).json({ error: 'Failed to fetch holdings activity' }) + return json({ error: 'Failed to fetch holdings activity' }, { status: 502, headers: GET_CORS_HEADERS }) } } + +export default GET diff --git a/api/holdings/breakdown.test.ts b/src/server/holdings/breakdown.test.ts similarity index 100% rename from api/holdings/breakdown.test.ts rename to src/server/holdings/breakdown.test.ts diff --git a/src/server/holdings/breakdown.ts b/src/server/holdings/breakdown.ts new file mode 100644 index 000000000..18f8d9841 --- /dev/null +++ b/src/server/holdings/breakdown.ts @@ -0,0 +1,136 @@ +import { GET_CORS_HEADERS, getClientIdentifier, json, noContent, queryValue } from '../http' +import type { HoldingsEventFetchType, HoldingsEventPaginationMode, VaultVersion } from '../lib/holdings' +import { checkRateLimit, ensureHoldingsStorageInitialized } from '../lib/holdings' + +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 function OPTIONS(): Response { + return noContent(GET_CORS_HEADERS) +} + +export async function GET(request: Request): Promise { + try { + await ensureHoldingsStorageInitialized() + } catch (error) { + console.error('Holdings breakdown storage initialization error:', error) + return json({ error: 'Failed to initialize holdings storage' }, { status: 500, headers: GET_CORS_HEADERS }) + } + + const clientId = getClientIdentifier(request) + const rateCheck = await checkRateLimit(clientId) + if (!rateCheck.allowed) { + return json( + { error: 'Too many requests', retryAfter: rateCheck.retryAfter }, + { status: 429, headers: { ...GET_CORS_HEADERS, 'Retry-After': String(rateCheck.retryAfter) } } + ) + } + + const envioUrl = process.env.ENVIO_GRAPHQL_URL + if (!envioUrl) { + return json( + { + error: 'Holdings breakdown API not configured', + details: 'ENVIO_GRAPHQL_URL environment variable is not set. This feature requires a running Envio indexer.' + }, + { status: 503, headers: GET_CORS_HEADERS } + ) + } + + const address = queryValue(request, 'address') + const dateParam = queryValue(request, 'date') + const versionParam = queryValue(request, 'version') + const fetchTypeParam = queryValue(request, 'fetchType') + const paginationModeParam = queryValue(request, 'paginationMode') + + if (!address || typeof address !== 'string') { + return json({ error: 'Missing required parameter: address' }, { status: 400, headers: GET_CORS_HEADERS }) + } + + if (!isValidAddress(address)) { + return json({ error: 'Invalid Ethereum address' }, { status: 400, headers: GET_CORS_HEADERS }) + } + + const breakdownTimestamp = parseUtcDateParam(dateParam) + if (dateParam && breakdownTimestamp === null) { + return json({ error: 'Invalid date format, expected YYYY-MM-DD' }, { status: 400, headers: GET_CORS_HEADERS }) + } + + 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 + ) + + return json(breakdown, { + headers: { + ...GET_CORS_HEADERS, + 'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=600' + } + }) + } 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 json( + { + error: 'Failed to fetch holdings breakdown', + message, + stack + }, + { status: 502, headers: GET_CORS_HEADERS } + ) + } + + return json({ error: 'Failed to fetch holdings breakdown' }, { status: 502, headers: GET_CORS_HEADERS }) + } +} + +export default GET diff --git a/api/holdings/history.test.ts b/src/server/holdings/history.test.ts similarity index 70% rename from api/holdings/history.test.ts rename to src/server/holdings/history.test.ts index be25c7bfa..69e12b16d 100644 --- a/api/holdings/history.test.ts +++ b/src/server/holdings/history.test.ts @@ -11,36 +11,8 @@ vi.mock('../lib/holdings', () => ({ getHistoricalHoldingsChart: getHistoricalHoldingsChartMock })) -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 - } - } +function createRequest(query: Record): Request { + return new Request(`https://yearn.fi/api/holdings/history?${new URLSearchParams(query)}`) } describe('holdings history route', () => { @@ -66,19 +38,14 @@ describe('holdings history route', () => { }) const { default: handler } = await import('./history') - const req = { - method: 'GET', - query: { + const response = await handler( + createRequest({ address: TEST_WALLET_ADDRESS - }, - headers: {} - } as any - const res = createMockResponse() - - await handler(req, res as any) + }) + ) - expect(res.statusCode).toBe(200) - expect(res.body).toEqual({ + expect(response.status).toBe(200) + await expect(response.json()).resolves.toEqual({ address: TEST_WALLET_ADDRESS, version: 'all', denomination: 'usd', @@ -101,19 +68,14 @@ describe('holdings history route', () => { }) const { default: handler } = await import('./history') - const req = { - method: 'GET', - query: { + const response = await handler( + createRequest({ address: TEST_WALLET_ADDRESS, vaults: '1:0x696d02Db93291651ED510704c9b286841d506987,1:0xAaaFEa48472f77563961Cdb53291DEDfB46F9040' - }, - headers: {} - } as any - const res = createMockResponse() - - await handler(req, res as any) + }) + ) - expect(res.statusCode).toBe(200) + expect(response.status).toBe(200) expect(getHistoricalHoldingsChartMock).toHaveBeenCalledWith( TEST_WALLET_ADDRESS, 'all', diff --git a/api/holdings/history.ts b/src/server/holdings/history.ts similarity index 66% rename from api/holdings/history.ts rename to src/server/holdings/history.ts index f2189ce8a..aa5a83db5 100644 --- a/api/holdings/history.ts +++ b/src/server/holdings/history.ts @@ -1,5 +1,4 @@ -import type { VercelRequest, VercelResponse } from '@vercel/node' -import { setVercelCdnCacheHeaders } from '../lib/cacheHeaders' +import { GET_CORS_HEADERS, getClientIdentifier, json, noContent, queryValue } from '../http' import type { HoldingsEventFetchType, HoldingsEventPaginationMode, @@ -17,31 +16,6 @@ import { } from '../lib/holdings/services/debug' import { startHoldingsProgress, updateHoldingsProgress } from '../lib/holdings/services/progress' -const HOLDINGS_HISTORY_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() - } - - // Fallback: fingerprint from headers - 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) } @@ -118,64 +92,58 @@ function parseHoldingsHistoryTimeframe(value: string | string[] | undefined): Ho return value === 'all' ? 'all' : '1y' } -export default async function handler(req: VercelRequest, res: VercelResponse) { - // CORS headers - 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' }) - } +export function OPTIONS(): Response { + return noContent(GET_CORS_HEADERS) +} +export async function GET(request: Request): Promise { try { await ensureHoldingsStorageInitialized() } catch (error) { console.error('Holdings history storage initialization error:', error) - return res.status(500).json({ error: 'Failed to initialize holdings storage' }) + return json({ error: 'Failed to initialize holdings storage' }, { status: 500, headers: GET_CORS_HEADERS }) } // Rate limiting - const clientId = getClientIdentifier(req) - const rateCheck = await checkRateLimit(clientId, req.headers) + const clientId = getClientIdentifier(request) + const rateCheck = await checkRateLimit(clientId) if (!rateCheck.allowed) { - res.setHeader('Retry-After', String(rateCheck.retryAfter)) - return res.status(429).json({ error: 'Too many requests', retryAfter: rateCheck.retryAfter }) + return json( + { error: 'Too many requests', retryAfter: rateCheck.retryAfter }, + { status: 429, headers: { ...GET_CORS_HEADERS, 'Retry-After': String(rateCheck.retryAfter) } } + ) } // Check if Envio is configured const envioUrl = process.env.ENVIO_GRAPHQL_URL if (!envioUrl) { - return res.status(503).json({ - error: 'Holdings history API not configured', - details: 'ENVIO_GRAPHQL_URL environment variable is not set. This feature requires a running Envio indexer.' - }) + return json( + { + error: 'Holdings history API not configured', + details: 'ENVIO_GRAPHQL_URL environment variable is not set. This feature requires a running Envio indexer.' + }, + { status: 503, headers: GET_CORS_HEADERS } + ) } - const { - address, - chainId: chainIdParam, - vault: vaultParam, - vaults: vaultsParam, - version: versionParam, - fetchType: fetchTypeParam, - paginationMode: paginationModeParam, - denomination: denominationParam, - timeframe: timeframeParam, - debug: debugParam, - progressId: progressIdParam - } = req.query + const address = queryValue(request, 'address') + const chainIdParam = queryValue(request, 'chainId') + const vaultParam = queryValue(request, 'vault') + const vaultsParam = queryValue(request, 'vaults') + const versionParam = queryValue(request, 'version') + const fetchTypeParam = queryValue(request, 'fetchType') + const paginationModeParam = queryValue(request, 'paginationMode') + const denominationParam = queryValue(request, 'denomination') + const timeframeParam = queryValue(request, 'timeframe') + const debugParam = queryValue(request, 'debug') + const progressIdParam = queryValue(request, 'progressId') if (!address || typeof address !== 'string') { - return res.status(400).json({ error: 'Missing required parameter: address' }) + return json({ error: 'Missing required parameter: address' }, { status: 400, headers: GET_CORS_HEADERS }) } if (!isValidAddress(address)) { - return res.status(400).json({ error: 'Invalid Ethereum address' }) + return json({ error: 'Invalid Ethereum address' }, { status: 400, headers: GET_CORS_HEADERS }) } const vaultFilters = parseVaultFilters({ @@ -185,7 +153,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { }) if (vaultFilters === null) { - return res.status(400).json({ error: 'Invalid vault filter' }) + return json({ error: 'Invalid vault filter' }, { status: 400, headers: GET_CORS_HEADERS }) } const version: VaultVersion = versionParam === 'v2' || versionParam === 'v3' ? versionParam : 'all' @@ -255,7 +223,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { message: 'No historical holdings found', detail: null }) - return res.status(404).json({ error: 'No holdings found for address' }) + return json({ error: 'No holdings found for address' }, { status: 404, headers: GET_CORS_HEADERS }) } await updateHoldingsProgress(activeProgressId, { @@ -265,17 +233,24 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { detail: `${holdings.dataPoints.length} chart points` }) - setVercelCdnCacheHeaders(res, HOLDINGS_HISTORY_CDN_CACHE_CONTROL) - return res.status(200).json({ - address: holdings.address, - version, - denomination, - timeframe, - dataPoints: holdings.dataPoints.map((dp) => ({ - date: dp.date, - value: dp.value - })) - }) + return json( + { + address: holdings.address, + version, + denomination, + timeframe, + dataPoints: holdings.dataPoints.map((dp) => ({ + date: dp.date, + value: dp.value + })) + }, + { + headers: { + ...GET_CORS_HEADERS, + 'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=600' + } + } + ) } catch (error) { await updateHoldingsProgress(progressId, { status: 'error', @@ -287,13 +262,18 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { 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 historical holdings', - message, - stack - }) + return json( + { + error: 'Failed to fetch historical holdings', + message, + stack + }, + { status: 502, headers: GET_CORS_HEADERS } + ) } - return res.status(502).json({ error: 'Failed to fetch historical holdings' }) + return json({ error: 'Failed to fetch historical holdings' }, { status: 502, headers: GET_CORS_HEADERS }) } } + +export default GET diff --git a/api/holdings/pnl/simple-history.test.ts b/src/server/holdings/pnl/simple-history.test.ts similarity index 71% rename from api/holdings/pnl/simple-history.test.ts rename to src/server/holdings/pnl/simple-history.test.ts index 7747709ef..e10606b84 100644 --- a/api/holdings/pnl/simple-history.test.ts +++ b/src/server/holdings/pnl/simple-history.test.ts @@ -11,36 +11,8 @@ vi.mock('../../lib/holdings', () => ({ getHoldingsProtocolReturnHistory: getHoldingsProtocolReturnHistoryMock })) -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 - } - } +function createRequest(query: Record): Request { + return new Request(`https://yearn.fi/api/holdings/pnl/simple-history?${new URLSearchParams(query)}`) } describe('holdings simple pnl history route', () => { @@ -76,19 +48,14 @@ describe('holdings simple pnl history route', () => { }) const { default: handler } = await import('./simple-history') - const req = { - method: 'GET', - query: { + const response = await handler( + createRequest({ address: TEST_WALLET_ADDRESS, vaults: '1:0x696d02Db93291651ED510704c9b286841d506987,1:0xAaaFEa48472f77563961Cdb53291DEDfB46F9040' - }, - headers: {} - } as any - const res = createMockResponse() - - await handler(req, res as any) + }) + ) - expect(res.statusCode).toBe(200) + expect(response.status).toBe(200) expect(getHoldingsProtocolReturnHistoryMock).toHaveBeenCalledWith( TEST_WALLET_ADDRESS, 'all', diff --git a/src/server/holdings/pnl/simple-history.ts b/src/server/holdings/pnl/simple-history.ts new file mode 100644 index 000000000..f32c16a2a --- /dev/null +++ b/src/server/holdings/pnl/simple-history.ts @@ -0,0 +1 @@ +export { default, GET, OPTIONS } from '../protocol-return/history' diff --git a/api/holdings/progress.test.ts b/src/server/holdings/progress.test.ts similarity index 100% rename from api/holdings/progress.test.ts rename to src/server/holdings/progress.test.ts diff --git a/src/server/holdings/progress.ts b/src/server/holdings/progress.ts new file mode 100644 index 000000000..3aeda5385 --- /dev/null +++ b/src/server/holdings/progress.ts @@ -0,0 +1,23 @@ +import { GET_CORS_HEADERS, json, noContent, queryString } from '../http' +import { getHoldingsProgress } from '../lib/holdings/services/progress' + +export function OPTIONS(): Response { + return noContent(GET_CORS_HEADERS) +} + +export async function GET(request: Request): Promise { + const progress = await getHoldingsProgress(queryString(request, 'id') ?? null) + + if (!progress) { + return json({ error: 'Progress not found' }, { status: 404, headers: GET_CORS_HEADERS }) + } + + return json(progress, { + headers: { + ...GET_CORS_HEADERS, + 'Cache-Control': 'no-store' + } + }) +} + +export default GET diff --git a/api/holdings/protocol-return/history.ts b/src/server/holdings/protocol-return/history.ts similarity index 67% rename from api/holdings/protocol-return/history.ts rename to src/server/holdings/protocol-return/history.ts index 25b4040cc..049ef7686 100644 --- a/api/holdings/protocol-return/history.ts +++ b/src/server/holdings/protocol-return/history.ts @@ -1,5 +1,4 @@ -import type { VercelRequest, VercelResponse } from '@vercel/node' -import { setVercelCdnCacheHeaders } from '../../lib/cacheHeaders' +import { GET_CORS_HEADERS, getClientIdentifier, json, noContent, queryValue } from '../../http' import type { HoldingsEventFetchType, HoldingsEventPaginationMode, @@ -16,28 +15,6 @@ import { } from '../../lib/holdings/services/debug' import { startHoldingsProgress, updateHoldingsProgress } from '../../lib/holdings/services/progress' -const PROTOCOL_RETURN_HISTORY_CDN_CACHE_CONTROL = 'public, s-maxage=300, stale-while-revalidate=600' - -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) } @@ -110,60 +87,55 @@ function parseHoldingsHistoryTimeframe(value: string | string[] | undefined): Ho return value === 'all' ? 'all' : '1y' } -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' }) - } +export function OPTIONS(): Response { + return noContent(GET_CORS_HEADERS) +} +export async function GET(request: Request): Promise { try { await ensureHoldingsStorageInitialized() } catch (error) { console.error('Holdings protocol return history storage initialization error:', error) - return res.status(500).json({ error: 'Failed to initialize holdings storage' }) + return json({ error: 'Failed to initialize holdings storage' }, { status: 500, headers: GET_CORS_HEADERS }) } - const clientId = getClientIdentifier(req) - const rateCheck = await checkRateLimit(clientId, req.headers) + const clientId = getClientIdentifier(request) + const rateCheck = await checkRateLimit(clientId) if (!rateCheck.allowed) { - res.setHeader('Retry-After', String(rateCheck.retryAfter)) - return res.status(429).json({ error: 'Too many requests', retryAfter: rateCheck.retryAfter }) + return json( + { error: 'Too many requests', retryAfter: rateCheck.retryAfter }, + { status: 429, headers: { ...GET_CORS_HEADERS, 'Retry-After': String(rateCheck.retryAfter) } } + ) } const envioUrl = process.env.ENVIO_GRAPHQL_URL if (!envioUrl) { - return res.status(503).json({ - error: 'Holdings protocol return history API not configured', - details: 'ENVIO_GRAPHQL_URL environment variable is not set. This feature requires a running Envio indexer.' - }) + return json( + { + error: 'Holdings protocol return history API not configured', + details: 'ENVIO_GRAPHQL_URL environment variable is not set. This feature requires a running Envio indexer.' + }, + { status: 503, headers: GET_CORS_HEADERS } + ) } - const { - address, - chainId: chainIdParam, - vault: vaultParam, - vaults: vaultsParam, - version: versionParam, - fetchType: fetchTypeParam, - paginationMode: paginationModeParam, - timeframe: timeframeParam, - debug: debugParam, - progressId: progressIdParam - } = req.query + const address = queryValue(request, 'address') + const chainIdParam = queryValue(request, 'chainId') + const vaultParam = queryValue(request, 'vault') + const vaultsParam = queryValue(request, 'vaults') + const versionParam = queryValue(request, 'version') + const fetchTypeParam = queryValue(request, 'fetchType') + const paginationModeParam = queryValue(request, 'paginationMode') + const timeframeParam = queryValue(request, 'timeframe') + const debugParam = queryValue(request, 'debug') + const progressIdParam = queryValue(request, 'progressId') if (!address || typeof address !== 'string') { - return res.status(400).json({ error: 'Missing required parameter: address' }) + return json({ error: 'Missing required parameter: address' }, { status: 400, headers: GET_CORS_HEADERS }) } if (!isValidAddress(address)) { - return res.status(400).json({ error: 'Invalid Ethereum address' }) + return json({ error: 'Invalid Ethereum address' }, { status: 400, headers: GET_CORS_HEADERS }) } const vaultFilters = parseVaultFilters({ @@ -173,7 +145,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { }) if (vaultFilters === null) { - return res.status(400).json({ error: 'Invalid vault filter' }) + return json({ error: 'Invalid vault filter' }, { status: 400, headers: GET_CORS_HEADERS }) } const version: VaultVersion = versionParam === 'v2' || versionParam === 'v3' ? versionParam : 'all' @@ -245,7 +217,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { message: 'No historical holdings found', detail: null }) - return res.status(404).json({ error: 'No holdings found for address' }) + return json({ error: 'No holdings found for address' }, { status: 404, headers: GET_CORS_HEADERS }) } await updateHoldingsProgress(activeProgressId, { @@ -255,8 +227,12 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { detail: `${history.dataPoints.length} chart points` }) - setVercelCdnCacheHeaders(res, PROTOCOL_RETURN_HISTORY_CDN_CACHE_CONTROL) - return res.status(200).json(history) + return json(history, { + headers: { + ...GET_CORS_HEADERS, + 'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=600' + } + }) } catch (error) { await updateHoldingsProgress(progressId, { status: 'error', @@ -268,13 +244,21 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { 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 protocol return history', - message, - stack - }) + return json( + { + error: 'Failed to fetch holdings protocol return history', + message, + stack + }, + { status: 502, headers: GET_CORS_HEADERS } + ) } - return res.status(502).json({ error: 'Failed to fetch holdings protocol return history' }) + return json( + { error: 'Failed to fetch holdings protocol return history' }, + { status: 502, headers: GET_CORS_HEADERS } + ) } } + +export default GET diff --git a/src/server/http.ts b/src/server/http.ts new file mode 100644 index 000000000..7f9ee8938 --- /dev/null +++ b/src/server/http.ts @@ -0,0 +1,96 @@ +export type TQueryValue = string | string[] | undefined + +export const GET_CORS_HEADERS = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type' +} as const + +export const POST_CORS_HEADERS = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type' +} as const + +export const ADMIN_POST_CORS_HEADERS = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, x-admin-secret' +} as const + +function mergeHeaders(...headerRecords: Array): Headers { + const headers = new Headers() + headerRecords.forEach((headerRecord) => { + if (!headerRecord) { + return + } + + new Headers(headerRecord).forEach((value, key) => { + headers.set(key, value) + }) + }) + return headers +} + +export function json(data: unknown, init?: ResponseInit): Response { + return Response.json(data, { + ...init, + headers: mergeHeaders(init?.headers) + }) +} + +export function noContent(headers: HeadersInit = GET_CORS_HEADERS): Response { + return new Response(null, { + status: 204, + headers + }) +} + +export function queryValue(request: Request, key: string): TQueryValue { + const values = new URL(request.url).searchParams.getAll(key) + if (values.length === 0) { + return undefined + } + + return values.length === 1 ? values[0] : values +} + +export function queryString(request: Request, key: string): string | undefined { + const value = queryValue(request, key) + return typeof value === 'string' ? value : undefined +} + +export async function readJsonBody(request: Request): Promise { + try { + return (await request.json()) as T + } catch (_error) { + throw new Error('Invalid JSON body') + } +} + +function simpleHash(value: string): string { + const hash = Array.from(value).reduce((currentHash, char) => { + const nextHash = (currentHash << 5) - currentHash + char.charCodeAt(0) + return nextHash & nextHash + }, 0) + return Math.abs(hash).toString(36) +} + +export function getClientIdentifier(request: Request): string { + const forwarded = request.headers.get('x-forwarded-for') + if (forwarded) { + return forwarded.split(',')[0]?.trim() || forwarded + } + + const userAgent = request.headers.get('user-agent') || '' + const acceptLanguage = request.headers.get('accept-language') || '' + const acceptEncoding = request.headers.get('accept-encoding') || '' + return `fp-${simpleHash(userAgent + acceptLanguage + acceptEncoding)}` +} + +export function getRequestIp(request: Request): string | undefined { + const forwardedFor = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() + const realIp = request.headers.get('x-real-ip')?.trim() + + return forwardedFor || realIp || (process.env.NODE_ENV === 'development' ? '127.0.0.1' : undefined) +} diff --git a/api/lib/aio.test.ts b/src/server/lib/aio.test.ts similarity index 100% rename from api/lib/aio.test.ts rename to src/server/lib/aio.test.ts diff --git a/api/lib/aio.ts b/src/server/lib/aio.ts similarity index 100% rename from api/lib/aio.ts rename to src/server/lib/aio.ts diff --git a/api/lib/cacheHeaders.ts b/src/server/lib/cacheHeaders.ts similarity index 100% rename from api/lib/cacheHeaders.ts rename to src/server/lib/cacheHeaders.ts diff --git a/api/lib/holdings/README.md b/src/server/lib/holdings/README.md similarity index 77% rename from api/lib/holdings/README.md rename to src/server/lib/holdings/README.md index 4532eb2ef..80e92c7c6 100644 --- a/api/lib/holdings/README.md +++ b/src/server/lib/holdings/README.md @@ -21,11 +21,10 @@ Holdings services ├─ Envio GraphQL: deposits, withdrawals, transfers ├─ Kong: vault metadata and historical PPS ├─ yearn-prices or DefiLlama: historical token prices - ├─ Upstash Redis: optional server-side cache, progress, invalidations - └─ Vercel Firewall: public route rate limits + └─ Upstash Redis: optional server-side cache, progress, rate limits, invalidations ``` -In production, files under `api/` run as Vercel functions. In local development, `api/server.ts` exposes the same holdings routes on the Bun API server at `localhost:3001` and adds local-only debug and refresh controls. +Next exposes the holdings endpoints through explicit files under `app/api/holdings/**/route.ts`; implementation modules live under `src/server/holdings/**`. ## Core Model @@ -67,7 +66,7 @@ The API internally values each day at `23:59:59 UTC`. | `pnlSimple.ts` | Local | Protocol-return exposure history without FIFO cost-basis accounting | | `cache.ts` | Upstash Redis | Daily totals and lazy vault invalidation | | `progress.ts` | Upstash Redis | Short-lived progress records and logs for long history requests | -| `ratelimit.ts` | Vercel Firewall | Programmatic per-client rate limits for public holdings routes | +| `ratelimit.ts` | Upstash Redis | Simple per-client request windows for public holdings routes | ## Event Semantics @@ -104,7 +103,7 @@ yearn-prices behavior: - API key is sent as `Authorization: Bearer `. - `YEARN_PRICES_API_KEY` has priority; `API_KEY_PORTFOLIO` is the fallback. - Timestamps are normalized to UTC day end before the API request. -- Contiguous daily histories use `/api/prices/rangeHistorical`, split into `183`-day range windows. +- Contiguous daily histories up to `366` days use `/api/prices/rangeHistorical`. - Sparse or single-day lookups use `/api/prices/batchHistorical`. - Returned UTC day-end prices are materialized back onto the originally requested timestamps for the response map. - Prices are not read from or written to the local database. @@ -118,7 +117,7 @@ DefiLlama behavior: ## Endpoints -Public holdings data routes support CORS, `GET`, and `OPTIONS`. On Vercel, history, breakdown, activity, activity facets, and protocol-return history use the Vercel Firewall Rate Limiting SDK keyed by forwarded IP, falling back to a simple header fingerprint. `/api/holdings/progress` is read-only progress polling and does not run the rate limiter. +Public holdings data routes support CORS, `GET`, and `OPTIONS`. When database caching is enabled, history, breakdown, activity, activity facets, and protocol-return history rate-limit by forwarded IP, falling back to a simple header fingerprint. `/api/holdings/progress` is read-only progress polling and does not run the rate limiter. ### `GET /api/holdings/history` @@ -127,9 +126,9 @@ Daily holdings chart. Examples: ```bash -curl "http://localhost:3001/api/holdings/history?address=0x..." -curl "http://localhost:3001/api/holdings/history?address=0x...&denomination=eth&timeframe=all" -curl "http://localhost:3001/api/holdings/history?address=0x...&vaults=1:0x...,1:0x..." +curl "http://localhost:3000/api/holdings/history?address=0x..." +curl "http://localhost:3000/api/holdings/history?address=0x...&denomination=eth&timeframe=all" +curl "http://localhost:3000/api/holdings/history?address=0x...&vaults=1:0x...,1:0x..." ``` Query params: @@ -146,8 +145,6 @@ Query params: | `paginationMode` | No | `paged` | `paged` or `all` | | `progressId` | No | - | Stable progress ID clients can poll through `/api/holdings/progress` | | `debug` | No | - | Enables the route debug context | -| `refresh` | Local only | `false` | `true` or `1` clears the user's cached totals before computing | -| `debugLots`, `debugVault`, `debugTx` | Local only | - | Extra debug filters in `api/server.ts` | Response: @@ -173,7 +170,7 @@ Reads Redis-backed progress for long-running holdings routes. `history` and `pro Example: ```bash -curl "http://localhost:3001/api/holdings/progress?id=portfolio:0x..." +curl "http://localhost:3000/api/holdings/progress?id=portfolio:0x..." ``` Query params: @@ -199,7 +196,7 @@ Response: } ``` -Progress records expire after 10 minutes. The route returns `204` when the ID is invalid, expired, missing, or Redis progress is unavailable, and it always sends `Cache-Control: no-store`. +Progress records expire after 10 minutes. The route returns `404` when the ID is invalid, expired, missing, or Redis progress is unavailable, and it always sends `Cache-Control: no-store`. ### `GET /api/holdings/breakdown` @@ -208,8 +205,8 @@ Per-vault valuation for a settled UTC date. Without `date`, it uses the latest s Examples: ```bash -curl "http://localhost:3001/api/holdings/breakdown?address=0x..." -curl "http://localhost:3001/api/holdings/breakdown?address=0x...&date=2026-05-06" +curl "http://localhost:3000/api/holdings/breakdown?address=0x..." +curl "http://localhost:3000/api/holdings/breakdown?address=0x...&date=2026-05-06" ``` Query params: @@ -221,7 +218,7 @@ Query params: | `version` | No | `all` | `v2`, `v3`, or `all` | | `fetchType` | No | `seq` | `seq` or `parallel` | | `paginationMode` | No | `paged` | `paged` or `all` | -| `debug`, `debugLots`, `debugVault`, `debugTx` | Local only | - | Debug logging controls in `api/server.ts` | +| `debug` | No | - | Enables the route debug context | Response is intentionally verbose because it is used to explain the latest chart point: @@ -269,9 +266,9 @@ Response is intentionally verbose because it is used to explain the latest chart Recent classified vault activity. ```bash -curl "http://localhost:3001/api/holdings/activity?address=0x..." -curl "http://localhost:3001/api/holdings/activity?address=0x...&limit=20&offset=20" -curl "http://localhost:3001/api/holdings/activity?address=0x...&type=withdraw&chainId=1" +curl "http://localhost:3000/api/holdings/activity?address=0x..." +curl "http://localhost:3000/api/holdings/activity?address=0x...&limit=20&offset=20" +curl "http://localhost:3000/api/holdings/activity?address=0x...&type=withdraw&chainId=1&includeFacets=1" ``` Query params: @@ -279,15 +276,16 @@ Query params: | Param | Required | Default | Description | |-------|----------|---------|-------------| | `address` | Yes | - | User EVM address | -| `version` | No | `all` | Backend vault generation scope: `v2`, `v3`, or `all`. The portfolio activity UI uses `all`. | -| `limit` | No | `10` | Integer clamped to `1..500` | +| `version` | No | `all` | `v2`, `v3`, or `all` | +| `limit` | No | `10` | Integer clamped to `1..50` | | `offset` | No | `0` | Non-negative integer | | `type` | No | `all` | `deposit`, `withdraw`, `stake`, `unstake`, `transfer`, `swap`, or `all` | | `chainId` | No | - | Positive integer chain filter | | `startTimestamp` | No | - | Inclusive Unix timestamp lower bound | | `endTimestamp` | No | - | Inclusive Unix timestamp upper bound | +| `includeFacets` | No | `false` | `true` or `1` includes `facets.chainIds` for the returned page | -Response: +Response (`facets` appears only when `includeFacets=true` or `includeFacets=1`): ```json { @@ -295,6 +293,9 @@ Response: "version": "all", "limit": 10, "offset": 0, + "facets": { + "chainIds": [1, 8453] + }, "pageInfo": { "hasMore": true, "nextOffset": 10 @@ -328,15 +329,15 @@ Response: } ``` -Activity classification merges address-scoped events with transaction-scoped context, so router-mediated staking, unstaking, deposit, withdraw, transfer, and swap flows can be represented as user actions. Configure `VITE_RPC_URI_FOR_` for richer receipt enrichment of zaps, reward claims, and direct V2 vault actions; without it the API still returns indexed activity rows, but some enriched input/output fields may be absent. +Activity classification merges address-scoped events with transaction-scoped context, so router-mediated staking, unstaking, deposit, withdraw, transfer, and swap flows can be represented as user actions. Configure `RPC_URI_FOR_` for richer server-side receipt enrichment of zaps, reward claims, and direct V2 vault actions; without it the API falls back to `NEXT_PUBLIC_RPC_URI_FOR_`. If neither is configured, the API still returns indexed activity rows, but some enriched input/output fields may be absent. ### `GET /api/holdings/activity-facets` Returns activity chain facets without fetching the full paginated activity response. This is useful for building chain filter controls before the user requests activity rows. ```bash -curl "http://localhost:3001/api/holdings/activity-facets?address=0x..." -curl "http://localhost:3001/api/holdings/activity-facets?address=0x...&version=all" +curl "http://localhost:3000/api/holdings/activity-facets?address=0x..." +curl "http://localhost:3000/api/holdings/activity-facets?address=0x...&limitPerSource=500&offsetPerSource=500" ``` Query params: @@ -344,7 +345,9 @@ Query params: | Param | Required | Default | Description | |-------|----------|---------|-------------| | `address` | Yes | - | User EVM address | -| `version` | No | `all` | Backend vault generation scope: `v2`, `v3`, or `all`. The portfolio activity UI uses `all`. | +| `version` | No | `all` | `v2`, `v3`, or `all` | +| `limitPerSource` | No | `250` | Per-event-source page size, clamped to `1..1000` | +| `offsetPerSource` | No | `0` | Per-event-source non-negative offset | Response: @@ -354,6 +357,10 @@ Response: "version": "all", "facets": { "chainIds": [1, 8453] + }, + "pageInfo": { + "hasMore": false, + "nextOffsetPerSource": null } } ``` @@ -367,9 +374,9 @@ The compatibility alias `/api/holdings/pnl/simple-history` routes to the same ha Examples: ```bash -curl "http://localhost:3001/api/holdings/protocol-return/history?address=0x..." -curl "http://localhost:3001/api/holdings/protocol-return/history?address=0x...&timeframe=all" -curl "http://localhost:3001/api/holdings/protocol-return/history?address=0x...&vaults=1:0x...,1:0x..." +curl "http://localhost:3000/api/holdings/protocol-return/history?address=0x..." +curl "http://localhost:3000/api/holdings/protocol-return/history?address=0x...&timeframe=all" +curl "http://localhost:3000/api/holdings/protocol-return/history?address=0x...&vaults=1:0x...,1:0x..." ``` Query params: @@ -385,7 +392,6 @@ Query params: | `paginationMode` | No | `paged` | `paged` or `all` | | `progressId` | No | - | Stable progress ID clients can poll through `/api/holdings/progress` | | `debug` | No | - | Enables the route debug context | -| `debugLots`, `debugVault`, `debugTx` | Local only | - | Extra debug filters in `api/server.ts` | Metric model: @@ -437,28 +443,27 @@ Response: When a vault filter is present, each history point can also include `currentUnderlying`, `growthUnderlying`, `sharesFormatted`, and `pricePerShare`. -### Manual Vault Invalidation - -Mark vaults as invalidated by writing Redis keys directly in Upstash. Affected user daily totals are lazily cleared and recomputed on the next cached history request that includes the invalidated vault. - -Key format: +### `POST /api/admin/invalidate-cache` -```text -holdings:vault-invalidated:: -``` - -Value format: +Marks vaults as invalidated so affected user daily totals are lazily cleared and recomputed on the next cached history request. Requires `x-admin-secret: $ADMIN_SECRET` and DB caching. -```text - +```bash +curl -X POST \ + -H "content-type: application/json" \ + -H "x-admin-secret: $ADMIN_SECRET" \ + -d '{"vaults":[{"address":"0x...","chainId":1}]}' \ + "http://localhost:3000/api/admin/invalidate-cache" ``` -Example: +Response: -```text -key: holdings:vault-invalidated:1:0xbe53a109b494e5c9f97b9cd39fe969be68bf6204 -value: 1779564000000 -ttl: none +```json +{ + "success": true, + "invalidated": 1, + "vaults": ["1:0x..."], + "timestamp": "2026-05-07T00:00:00.000Z" +} ``` ## Supported Chains @@ -473,7 +478,7 @@ ttl: none | Arbitrum | 42161 | `arbitrum` | | Katana | 747474 | `katana` | -Unknown chain IDs fail historical price resolution instead of falling back to Ethereum. Add new chains to `SUPPORTED_CHAINS` before requests are expected to value correctly. +`getChainPrefix` falls back to `ethereum` for unknown chain IDs, so new chains should be added to `SUPPORTED_CHAINS` before requests are expected to value correctly. ## Environment Variables @@ -481,17 +486,17 @@ Unknown chain IDs fail historical price resolution instead of falling back to Et |----------|----------|---------|-------------| | `ENVIO_GRAPHQL_URL` | No | `http://localhost:8080/v1/graphql` | Envio indexer GraphQL endpoint | | `ENVIO_PASSWORD` | No | `''` | Envio Hasura admin secret; skipped when empty or `testing` | -| `VERCEL_HOLDINGS_RATE_LIMIT_ID` | No | `holdings-public-api` | Vercel Firewall rate limit ID for public holdings routes | -| `UPSTASH_REDIS_REST_URL_PORTFOLIO` | No | `null` | Upstash Redis REST URL for holdings cache, progress, and invalidations | +| `UPSTASH_REDIS_REST_URL_PORTFOLIO` | No | `null` | Upstash Redis REST URL for holdings cache, progress, rate limits, and invalidations | | `UPSTASH_REDIS_REST_TOKEN_PORTFOLIO` | No | `null` | Upstash Redis REST token for holdings storage | -| `VITE_RPC_URI_FOR_` | No | `null` | Optional chain RPC URL for activity receipt and transaction enrichment | +| `RPC_URI_FOR_` | No | `NEXT_PUBLIC_RPC_URI_FOR_` | Optional server-only chain RPC URL for activity receipt and transaction enrichment | | `HOLDINGS_PRICE_PROVIDER` | No | `auto` | `auto`, `yearn-prices`, or `defillama` | | `YEARN_PRICES_BASE_URL` | No | `https://prices.yearn.dev` | Base URL for yearn-prices; `/api/prices/...` is appended automatically | | `YEARN_PRICES_API_URL` | No | `YEARN_PRICES_BASE_URL` fallback | Legacy alias for `YEARN_PRICES_BASE_URL` | | `YEARN_PRICES_API_KEY` | No | `API_KEY_PORTFOLIO` fallback | Bearer token for yearn-prices | | `API_KEY_PORTFOLIO` | No | `''` | Shared portfolio API key used as the yearn-prices fallback token | | `DEFILLAMA_API_KEY` | No | `''` | Enables DefiLlama Pro GET route | -| `HOLDINGS_DEBUG` | Local only | `false` | Enables holdings debug logs in `api/server.ts` | +| `ADMIN_SECRET` | Admin only | `null` | Secret for `/api/admin/invalidate-cache` | +| `HOLDINGS_DEBUG` | No | `false` | Enables holdings debug logs | Hardcoded service bases: @@ -524,19 +529,15 @@ Server-side cache is optional. When `UPSTASH_REDIS_REST_URL_PORTFOLIO` or `UPSTA 1. Upstash Redis: - `holdings:totals::`: daily USD totals per hashed user address, vault version, and date. Hash fields are `YYYY-MM-DD`; values include `usdValue` and `updatedAt`. + - `holdings:rate-limit:`: simple per-client request windows with a 60-second TTL. - `holdings:vault-invalidated::`: per-vault invalidation timestamps for lazy cache clearing. - `holdings:progress:`: authoritative short-lived progress records keyed by caller-supplied progress ID for long history requests across Vercel function instances. -2. Vercel Firewall: - - `VERCEL_HOLDINGS_RATE_LIMIT_ID` identifies the dashboard Firewall rule used by `@vercel/firewall`. - - The default rule ID is `holdings-public-api`. - - Configure the dashboard rule with the desired window and request count, currently intended to match the old `10` requests per minute limit. -3. HTTP cache: - - Cacheable API responses put shared-cache policy in `Vercel-CDN-Cache-Control` and keep browser-facing `Cache-Control` at `public, max-age=0, must-revalidate`. - - History, breakdown, and protocol-return history CDN cache: `s-maxage=300, stale-while-revalidate=600`. - - Activity CDN cache: `s-maxage=60, stale-while-revalidate=300`. - - Activity facets CDN cache: `s-maxage=300, stale-while-revalidate=900`. - - Progress: `Cache-Control: no-store`. -4. Client TanStack Query cache: +2. HTTP cache: + - History, breakdown, and protocol-return history: `s-maxage=300, stale-while-revalidate=600`. + - Activity: `s-maxage=60, stale-while-revalidate=300`. + - Activity facets: `s-maxage=300, stale-while-revalidate=900`. + - Progress: `no-store`. +3. Client TanStack Query cache: - Portfolio history and protocol-return history hooks keep chart responses fresh for one hour. - Other frontend hooks configure their own durations. @@ -551,15 +552,12 @@ Cache behavior: - Cache staleness is checked against `holdings:vault-invalidated::` only after the request has enough cached daily totals to potentially serve from cache. - If any relevant vault was invalidated after the oldest cached row was written, the user's cached totals for that version are cleared and recomputed. - Recalculated totals are not cached when any token price batch failed, because partial price data can undercount chart totals. -- `refresh=true` or `refresh=1` in the local Bun server clears the user's cached totals before recomputing. ### Progress and Rate Limits - Progress writes only when Redis persistence is enabled and the supplied `progressId` matches `[a-zA-Z0-9:_-]{1,160}`. - Progress status is `running`, `complete`, or `error`; progress is clamped to `0..100`, logs are capped to the latest `20` entries, and rows expire after `10 minutes`. -- Vercel Firewall rate limiting only runs when `VERCEL=1`. Local development allows requests without calling the Firewall SDK. -- Configure a Vercel Firewall rule with condition `@vercel/firewall` and the rate limit ID from `VERCEL_HOLDINGS_RATE_LIMIT_ID` or the default `holdings-public-api`. -- The intended production limit is `10` requests per minute per client identifier. If the Firewall SDK check fails, the rate limiter allows the request and logs the failure. +- Redis-backed rate limiting allows `10` requests per minute per client identifier. If Redis access fails, the rate limiter allows the request and logs the failure. ### Token Prices @@ -572,6 +570,7 @@ No schema migration is required. Redis keys are created lazily: | Key | Type | TTL | Purpose | |-----|------|-----|---------| | `holdings:totals::` | Hash | 30 days from write | Daily holdings chart totals. | +| `holdings:rate-limit:` | String counter | 60 seconds | Public route rate limiting. | | `holdings:vault-invalidated::` | String timestamp | None | Lazy invalidation marker for totals cache. | | `holdings:progress:` | String JSON record | 10 minutes | Progress polling state for long requests. | @@ -579,9 +578,9 @@ No schema migration is required. Redis keys are created lazily: - Enable Redis storage in shared environments; otherwise a history request must rebuild events, PPS, and prices every time. - Keep `API_KEY_PORTFOLIO` or `YEARN_PRICES_API_KEY` configured if `HOLDINGS_PRICE_PROVIDER=auto` should prefer yearn-prices. -- Configure `VITE_RPC_URI_FOR_` for chains where activity rows should include richer zap, reward-claim, and direct V2 vault enrichment. +- Configure `RPC_URI_FOR_` for chains where activity rows should include richer zap, reward-claim, and direct V2 vault enrichment. - Pass a stable `progressId` from the frontend for long history and protocol-return requests, then poll `/api/holdings/progress?id=...`; progress rows are Redis-backed and expire quickly. -- Write `holdings:vault-invalidated::` markers in Upstash after indexer deployments add or repair vault coverage. +- Use `/api/admin/invalidate-cache` after indexer deployments add or repair vault coverage. - Rate-limit and progress cleanup is handled by Redis TTLs. - If Redis progress is unavailable, clients show a neutral loading placeholder instead of estimated progress. - `timeframe=all` grows over time from `2024-01-01`, so cache row counts are no longer fixed at `365` per user/version. diff --git a/api/lib/holdings/config.ts b/src/server/lib/holdings/config.ts similarity index 100% rename from api/lib/holdings/config.ts rename to src/server/lib/holdings/config.ts diff --git a/api/lib/holdings/index.ts b/src/server/lib/holdings/index.ts similarity index 100% rename from api/lib/holdings/index.ts rename to src/server/lib/holdings/index.ts diff --git a/api/lib/holdings/services/activity.test.ts b/src/server/lib/holdings/services/activity.test.ts similarity index 99% rename from api/lib/holdings/services/activity.test.ts rename to src/server/lib/holdings/services/activity.test.ts index f006ecc92..b2b253244 100644 --- a/api/lib/holdings/services/activity.test.ts +++ b/src/server/lib/holdings/services/activity.test.ts @@ -213,7 +213,7 @@ function mockReceiptEnrichmentRpc(args: { tokenDecimals: number logs?: ReturnType[] }) { - process.env.VITE_RPC_URI_FOR_1 = 'https://rpc.example' + process.env.RPC_URI_FOR_1 = 'https://rpc.example' vi.stubGlobal( 'fetch', vi.fn(async (_url: string, init?: RequestInit) => { @@ -272,7 +272,7 @@ function mockYcrvZapRpc(args: { inputTokenSymbol: string inputTokenDecimals: number }) { - process.env.VITE_RPC_URI_FOR_1 = 'https://rpc.example' + process.env.RPC_URI_FOR_1 = 'https://rpc.example' vi.stubGlobal( 'fetch', vi.fn(async (_url: string, init?: RequestInit) => { @@ -323,7 +323,7 @@ function mockYcrvZapRpc(args: { } function mockRewardDistributorRpc(transactionTargets: Record) { - process.env.VITE_RPC_URI_FOR_1 = 'https://rpc.example' + process.env.RPC_URI_FOR_1 = 'https://rpc.example' vi.stubGlobal( 'fetch', vi.fn(async (_url: string, init?: RequestInit) => { @@ -355,7 +355,7 @@ function mockDirectV2VaultRpc(args: { assetAmount: bigint underlyingTokenAddress?: string }) { - process.env.VITE_RPC_URI_FOR_1 = 'https://rpc.example' + process.env.RPC_URI_FOR_1 = 'https://rpc.example' vi.stubGlobal( 'fetch', vi.fn(async (_url: string, init?: RequestInit) => { @@ -412,7 +412,7 @@ function mockZapperV2Rpc(args: { inputTokenSymbol?: string inputTokenDecimals?: number }) { - process.env.VITE_RPC_URI_FOR_1 = 'https://rpc.example' + process.env.RPC_URI_FOR_1 = 'https://rpc.example' vi.stubGlobal( 'fetch', vi.fn(async (_url: string, init?: RequestInit) => { @@ -500,7 +500,7 @@ function mockZapperV2ZapOutRpc(args: { outputTokenSymbol?: string outputTokenDecimals?: number }) { - process.env.VITE_RPC_URI_FOR_1 = 'https://rpc.example' + process.env.RPC_URI_FOR_1 = 'https://rpc.example' vi.stubGlobal( 'fetch', vi.fn(async (_url: string, init?: RequestInit) => { @@ -583,7 +583,7 @@ describe('getHoldingsActivity', () => { vi.resetModules() vi.clearAllMocks() vi.unstubAllGlobals() - delete process.env.VITE_RPC_URI_FOR_1 + delete process.env.RPC_URI_FOR_1 fetchActivityEventsByTransactionHashesMock.mockResolvedValue({ deposits: [], withdrawals: [], @@ -2302,7 +2302,7 @@ describe('getHoldingsActivity', () => { ] ]) ) - process.env.VITE_RPC_URI_FOR_1 = 'https://rpc.example' + process.env.RPC_URI_FOR_1 = 'https://rpc.example' vi.stubGlobal( 'fetch', vi.fn(async (_url: string, init?: RequestInit) => { diff --git a/api/lib/holdings/services/activity.ts b/src/server/lib/holdings/services/activity.ts similarity index 100% rename from api/lib/holdings/services/activity.ts rename to src/server/lib/holdings/services/activity.ts diff --git a/api/lib/holdings/services/activityFacets.ts b/src/server/lib/holdings/services/activityFacets.ts similarity index 100% rename from api/lib/holdings/services/activityFacets.ts rename to src/server/lib/holdings/services/activityFacets.ts diff --git a/api/lib/holdings/services/activityReceiptEnrichment.ts b/src/server/lib/holdings/services/activityReceiptEnrichment.ts similarity index 99% rename from api/lib/holdings/services/activityReceiptEnrichment.ts rename to src/server/lib/holdings/services/activityReceiptEnrichment.ts index df779e833..eb6fbbb17 100644 --- a/api/lib/holdings/services/activityReceiptEnrichment.ts +++ b/src/server/lib/holdings/services/activityReceiptEnrichment.ts @@ -194,7 +194,7 @@ function toDecodeTopics(topics: string[]): TDecodeTopics { } function getChainRpcUrl(chainId: number): string | null { - const rpcUrl = process.env[`VITE_RPC_URI_FOR_${chainId}`]?.trim() + const rpcUrl = (process.env[`RPC_URI_FOR_${chainId}`] || process.env[`NEXT_PUBLIC_RPC_URI_FOR_${chainId}`])?.trim() return rpcUrl && rpcUrl.length > 0 ? rpcUrl : null } diff --git a/api/lib/holdings/services/aggregator.test.ts b/src/server/lib/holdings/services/aggregator.test.ts similarity index 100% rename from api/lib/holdings/services/aggregator.test.ts rename to src/server/lib/holdings/services/aggregator.test.ts diff --git a/api/lib/holdings/services/aggregator.ts b/src/server/lib/holdings/services/aggregator.ts similarity index 100% rename from api/lib/holdings/services/aggregator.ts rename to src/server/lib/holdings/services/aggregator.ts diff --git a/api/lib/holdings/services/cache.test.ts b/src/server/lib/holdings/services/cache.test.ts similarity index 100% rename from api/lib/holdings/services/cache.test.ts rename to src/server/lib/holdings/services/cache.test.ts diff --git a/api/lib/holdings/services/cache.ts b/src/server/lib/holdings/services/cache.ts similarity index 90% rename from api/lib/holdings/services/cache.ts rename to src/server/lib/holdings/services/cache.ts index ab6a37f48..96fe77614 100644 --- a/api/lib/holdings/services/cache.ts +++ b/src/server/lib/holdings/services/cache.ts @@ -213,6 +213,36 @@ export async function clearUserCache(userAddress: string, version?: string): Pro } } +export async function invalidateVaults(vaults: VaultIdentifier[]): Promise { + if (!isHoldingsStorageEnabled() || vaults.length === 0) { + if (vaults.length > 0) { + debugLog('cache', 'skipping vault invalidation because Redis storage is disabled', { vaults: vaults.length }) + } + return 0 + } + + const redis = getHoldingsRedisClient() + if (!redis) { + debugLog('cache', 'skipping vault invalidation because Redis client is unavailable', { vaults: vaults.length }) + return 0 + } + + try { + const invalidatedAt = Date.now() + const results = await Promise.all(vaults.map((vault) => redis.set(getVaultInvalidationKey(vault), invalidatedAt))) + const invalidatedCount = results.filter(Boolean).length + debugLog('cache', 'invalidated vault cache timestamps in Redis', { + vaults: vaults.length, + invalidatedCount + }) + return invalidatedCount + } catch (error) { + handleHoldingsRedisError('vault cache invalidation failed', error) + debugError('cache', 'vault cache invalidation failed', error, { vaults: vaults.length }) + return 0 + } +} + export async function checkCacheStaleness( vaults: VaultIdentifier[], cacheOldestTimestamp: Date | null diff --git a/api/lib/holdings/services/debug.ts b/src/server/lib/holdings/services/debug.ts similarity index 100% rename from api/lib/holdings/services/debug.ts rename to src/server/lib/holdings/services/debug.ts diff --git a/api/lib/holdings/services/defillama.test.ts b/src/server/lib/holdings/services/defillama.test.ts similarity index 100% rename from api/lib/holdings/services/defillama.test.ts rename to src/server/lib/holdings/services/defillama.test.ts diff --git a/api/lib/holdings/services/defillama.ts b/src/server/lib/holdings/services/defillama.ts similarity index 100% rename from api/lib/holdings/services/defillama.ts rename to src/server/lib/holdings/services/defillama.ts diff --git a/api/lib/holdings/services/graphql.test.ts b/src/server/lib/holdings/services/graphql.test.ts similarity index 100% rename from api/lib/holdings/services/graphql.test.ts rename to src/server/lib/holdings/services/graphql.test.ts diff --git a/api/lib/holdings/services/graphql.ts b/src/server/lib/holdings/services/graphql.ts similarity index 100% rename from api/lib/holdings/services/graphql.ts rename to src/server/lib/holdings/services/graphql.ts diff --git a/api/lib/holdings/services/holdings.test.ts b/src/server/lib/holdings/services/holdings.test.ts similarity index 100% rename from api/lib/holdings/services/holdings.test.ts rename to src/server/lib/holdings/services/holdings.test.ts diff --git a/api/lib/holdings/services/holdings.ts b/src/server/lib/holdings/services/holdings.ts similarity index 100% rename from api/lib/holdings/services/holdings.ts rename to src/server/lib/holdings/services/holdings.ts diff --git a/api/lib/holdings/services/kong.test.ts b/src/server/lib/holdings/services/kong.test.ts similarity index 100% rename from api/lib/holdings/services/kong.test.ts rename to src/server/lib/holdings/services/kong.test.ts diff --git a/api/lib/holdings/services/kong.ts b/src/server/lib/holdings/services/kong.ts similarity index 100% rename from api/lib/holdings/services/kong.ts rename to src/server/lib/holdings/services/kong.ts diff --git a/api/lib/holdings/services/nestedVaultPrices.test.ts b/src/server/lib/holdings/services/nestedVaultPrices.test.ts similarity index 100% rename from api/lib/holdings/services/nestedVaultPrices.test.ts rename to src/server/lib/holdings/services/nestedVaultPrices.test.ts diff --git a/api/lib/holdings/services/nestedVaultPrices.ts b/src/server/lib/holdings/services/nestedVaultPrices.ts similarity index 100% rename from api/lib/holdings/services/nestedVaultPrices.ts rename to src/server/lib/holdings/services/nestedVaultPrices.ts diff --git a/api/lib/holdings/services/pnlEvents.ts b/src/server/lib/holdings/services/pnlEvents.ts similarity index 100% rename from api/lib/holdings/services/pnlEvents.ts rename to src/server/lib/holdings/services/pnlEvents.ts diff --git a/api/lib/holdings/services/pnlShared.ts b/src/server/lib/holdings/services/pnlShared.ts similarity index 100% rename from api/lib/holdings/services/pnlShared.ts rename to src/server/lib/holdings/services/pnlShared.ts diff --git a/api/lib/holdings/services/pnlSimple.test.ts b/src/server/lib/holdings/services/pnlSimple.test.ts similarity index 100% rename from api/lib/holdings/services/pnlSimple.test.ts rename to src/server/lib/holdings/services/pnlSimple.test.ts diff --git a/api/lib/holdings/services/pnlSimple.ts b/src/server/lib/holdings/services/pnlSimple.ts similarity index 100% rename from api/lib/holdings/services/pnlSimple.ts rename to src/server/lib/holdings/services/pnlSimple.ts diff --git a/api/lib/holdings/services/pnlSimpleHistory.test.ts b/src/server/lib/holdings/services/pnlSimpleHistory.test.ts similarity index 100% rename from api/lib/holdings/services/pnlSimpleHistory.test.ts rename to src/server/lib/holdings/services/pnlSimpleHistory.test.ts diff --git a/api/lib/holdings/services/pnlTypes.ts b/src/server/lib/holdings/services/pnlTypes.ts similarity index 100% rename from api/lib/holdings/services/pnlTypes.ts rename to src/server/lib/holdings/services/pnlTypes.ts diff --git a/api/lib/holdings/services/progress.test.ts b/src/server/lib/holdings/services/progress.test.ts similarity index 100% rename from api/lib/holdings/services/progress.test.ts rename to src/server/lib/holdings/services/progress.test.ts diff --git a/api/lib/holdings/services/progress.ts b/src/server/lib/holdings/services/progress.ts similarity index 100% rename from api/lib/holdings/services/progress.ts rename to src/server/lib/holdings/services/progress.ts diff --git a/api/lib/holdings/services/ratelimit.test.ts b/src/server/lib/holdings/services/ratelimit.test.ts similarity index 100% rename from api/lib/holdings/services/ratelimit.test.ts rename to src/server/lib/holdings/services/ratelimit.test.ts diff --git a/api/lib/holdings/services/ratelimit.ts b/src/server/lib/holdings/services/ratelimit.ts similarity index 100% rename from api/lib/holdings/services/ratelimit.ts rename to src/server/lib/holdings/services/ratelimit.ts diff --git a/api/lib/holdings/services/settledHoldingsContext.ts b/src/server/lib/holdings/services/settledHoldingsContext.ts similarity index 100% rename from api/lib/holdings/services/settledHoldingsContext.ts rename to src/server/lib/holdings/services/settledHoldingsContext.ts diff --git a/api/lib/holdings/services/staking.ts b/src/server/lib/holdings/services/staking.ts similarity index 100% rename from api/lib/holdings/services/staking.ts rename to src/server/lib/holdings/services/staking.ts diff --git a/api/lib/holdings/services/vaults.test.ts b/src/server/lib/holdings/services/vaults.test.ts similarity index 100% rename from api/lib/holdings/services/vaults.test.ts rename to src/server/lib/holdings/services/vaults.test.ts diff --git a/api/lib/holdings/services/vaults.ts b/src/server/lib/holdings/services/vaults.ts similarity index 100% rename from api/lib/holdings/services/vaults.ts rename to src/server/lib/holdings/services/vaults.ts diff --git a/api/lib/holdings/storage/redis.ts b/src/server/lib/holdings/storage/redis.ts similarity index 100% rename from api/lib/holdings/storage/redis.ts rename to src/server/lib/holdings/storage/redis.ts diff --git a/api/lib/holdings/types.ts b/src/server/lib/holdings/types.ts similarity index 100% rename from api/lib/holdings/types.ts rename to src/server/lib/holdings/types.ts diff --git a/api/optimization/_lib/assetLogos.ts b/src/server/optimization/_lib/assetLogos.ts similarity index 96% rename from api/optimization/_lib/assetLogos.ts rename to src/server/optimization/_lib/assetLogos.ts index 3fbf1cf03..ccdd81afa 100644 --- a/api/optimization/_lib/assetLogos.ts +++ b/src/server/optimization/_lib/assetLogos.ts @@ -126,20 +126,6 @@ const VAULT_ASSET_TOKENS: Record = { } } -function _inferVaultTokenSymbol(vaultLabel: string | null): string | null { - if (!vaultLabel) { - return null - } - - const firstToken = vaultLabel.trim().split(/\s+/)[0] - if (!firstToken) { - return null - } - - const symbol = firstToken.replace(/-\d+$/, '') - return symbol || null -} - export function getVaultAssetToken(vaultAddress: string): VaultAssetToken | null { return VAULT_ASSET_TOKENS[vaultAddress.toLowerCase()] ?? null } diff --git a/api/optimization/_lib/colors.ts b/src/server/optimization/_lib/colors.ts similarity index 100% rename from api/optimization/_lib/colors.ts rename to src/server/optimization/_lib/colors.ts diff --git a/api/optimization/_lib/envio.ts b/src/server/optimization/_lib/envio.ts similarity index 100% rename from api/optimization/_lib/envio.ts rename to src/server/optimization/_lib/envio.ts diff --git a/api/optimization/_lib/explain-parse.ts b/src/server/optimization/_lib/explain-parse.ts similarity index 100% rename from api/optimization/_lib/explain-parse.ts rename to src/server/optimization/_lib/explain-parse.ts diff --git a/api/optimization/_lib/formatters.ts b/src/server/optimization/_lib/formatters.ts similarity index 100% rename from api/optimization/_lib/formatters.ts rename to src/server/optimization/_lib/formatters.ts diff --git a/api/optimization/_lib/normalize.ts b/src/server/optimization/_lib/normalize.ts similarity index 100% rename from api/optimization/_lib/normalize.ts rename to src/server/optimization/_lib/normalize.ts diff --git a/api/optimization/_lib/redis.test.ts b/src/server/optimization/_lib/redis.test.ts similarity index 100% rename from api/optimization/_lib/redis.test.ts rename to src/server/optimization/_lib/redis.test.ts diff --git a/api/optimization/_lib/redis.ts b/src/server/optimization/_lib/redis.ts similarity index 100% rename from api/optimization/_lib/redis.ts rename to src/server/optimization/_lib/redis.ts diff --git a/api/optimization/_lib/rpc.test.ts b/src/server/optimization/_lib/rpc.test.ts similarity index 100% rename from api/optimization/_lib/rpc.test.ts rename to src/server/optimization/_lib/rpc.test.ts diff --git a/api/optimization/_lib/rpc.ts b/src/server/optimization/_lib/rpc.ts similarity index 100% rename from api/optimization/_lib/rpc.ts rename to src/server/optimization/_lib/rpc.ts diff --git a/api/optimization/_lib/schema.ts b/src/server/optimization/_lib/schema.ts similarity index 100% rename from api/optimization/_lib/schema.ts rename to src/server/optimization/_lib/schema.ts diff --git a/api/optimization/alignment.ts b/src/server/optimization/alignment.ts similarity index 56% rename from api/optimization/alignment.ts rename to src/server/optimization/alignment.ts index f5315e6b5..5727558c2 100644 --- a/api/optimization/alignment.ts +++ b/src/server/optimization/alignment.ts @@ -1,7 +1,5 @@ -import type { VercelRequest, VercelResponse } from '@vercel/node' -import { setVercelCdnCacheHeaders } from '../lib/cacheHeaders' +import { GET_CORS_HEADERS, json, noContent, queryString } from '../http' import { getVaultDecimals } from './_lib/assetLogos' -import { OPTIMIZATION_GET_CORS_HEADERS, setCorsHeaders } from './_lib/cors' import { fetchAlignedEvents } from './_lib/envio' import { parseExplainMetadata } from './_lib/explain-parse' import { @@ -15,36 +13,30 @@ import { const CACHE_CONTROL = 'public, s-maxage=60, stale-while-revalidate=30' -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' }) - } +export function OPTIONS(): Response { + return noContent(GET_CORS_HEADERS) +} - const vault = req.query.vault as string | undefined +export async function GET(request: Request): Promise { + const vault = queryString(request, 'vault') if (!vault) { - return res.status(400).json({ error: 'vault parameter required' }) + return json({ error: 'vault parameter required' }, { status: 400, headers: GET_CORS_HEADERS }) } const envioUrl = process.env.ENVIO_GRAPHQL_URL if (!envioUrl) { - return res.status(503).json({ error: 'ENVIO_GRAPHQL_URL not configured' }) + return json({ error: 'ENVIO_GRAPHQL_URL not configured' }, { status: 503, headers: GET_CORS_HEADERS }) } try { const optimizations = await readOptimizations() if (!optimizations || optimizations.length === 0) { - return res.status(404).json({ error: 'No optimization data available' }) + return json({ error: 'No optimization data available' }, { status: 404, headers: GET_CORS_HEADERS }) } const optimization = findVaultOptimization(optimizations, vault) if (!optimization) { - return res.status(404).json({ error: `Vault not found: ${vault}` }) + return json({ error: `Vault not found: ${vault}` }, { status: 404, headers: GET_CORS_HEADERS }) } let chainId = optimization.source.chainId @@ -53,12 +45,12 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { chainId = metadata.chainId } if (!chainId) { - return res.status(400).json({ error: 'Could not determine chain ID for vault' }) + return json({ error: 'Could not determine chain ID for vault' }, { status: 400, headers: GET_CORS_HEADERS }) } const timestampStr = optimization.source.latestMatchedTimestampUtc ?? optimization.source.timestampUtc if (!timestampStr) { - return res.status(400).json({ error: 'No timestamp available for vault snapshot' }) + return json({ error: 'No timestamp available for vault snapshot' }, { status: 400, headers: GET_CORS_HEADERS }) } const fromTs = Math.floor(new Date(timestampStr.replace(' UTC', 'Z').replace(' ', 'T')).getTime() / 1000) const numStrategies = optimization.strategyDebtRatios.length @@ -76,18 +68,19 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { decimals ) - setVercelCdnCacheHeaders(res, CACHE_CONTROL) - return res.status(200).json(events) + return json(events, { headers: { ...GET_CORS_HEADERS, 'Cache-Control': CACHE_CONTROL } }) } catch (error) { if (isRedisAuthenticationError(error)) { - return res.status(500).json({ error: REDIS_AUTHENTICATION_ERROR_MESSAGE }) + return json({ error: REDIS_AUTHENTICATION_ERROR_MESSAGE }, { status: 500, headers: GET_CORS_HEADERS }) } if (isRedisConnectivityError(error)) { - return res.status(503).json({ error: REDIS_CONNECTIVITY_ERROR_MESSAGE }) + return json({ error: REDIS_CONNECTIVITY_ERROR_MESSAGE }, { status: 503, headers: GET_CORS_HEADERS }) } const message = error instanceof Error ? error.message : String(error) - return res.status(500).json({ error: message }) + return json({ error: message }, { status: 500, headers: GET_CORS_HEADERS }) } } + +export default GET diff --git a/src/server/optimization/change.ts b/src/server/optimization/change.ts new file mode 100644 index 000000000..f057e2433 --- /dev/null +++ b/src/server/optimization/change.ts @@ -0,0 +1,75 @@ +import { GET_CORS_HEADERS, json, noContent, queryValue } from '../http' +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 | undefined): boolean { + const value = historyParam + return value === '1' || value === 'true' +} + +export function OPTIONS(): Response { + return noContent(GET_CORS_HEADERS) +} + +export async function GET(request: Request): Promise { + try { + const optimizations = await readOptimizations() + if (!optimizations || optimizations.length === 0) { + return json({ error: 'No optimization data available' }, { status: 404, headers: GET_CORS_HEADERS }) + } + + const requestedVault = queryValue(request, 'vault') + if (requestedVault) { + if (typeof requestedVault !== 'string') { + return json({ error: 'Invalid vault parameter' }, { status: 400, headers: GET_CORS_HEADERS }) + } + + if (isHistoryQueryEnabled(queryValue(request, 'history') as string | undefined)) { + const selectedHistory = optimizations.filter((optimization) => { + return optimization.vault.toLowerCase() === requestedVault.toLowerCase() + }) + if (selectedHistory.length === 0) { + return json( + { error: `Vault not found in optimization payload: ${requestedVault}` }, + { status: 404, headers: GET_CORS_HEADERS } + ) + } + + return json(selectedHistory, { headers: { ...GET_CORS_HEADERS, 'Cache-Control': CACHE_CONTROL } }) + } + + const selected = findVaultOptimization(optimizations, requestedVault) + if (!selected) { + return json( + { error: `Vault not found in optimization payload: ${requestedVault}` }, + { status: 404, headers: GET_CORS_HEADERS } + ) + } + + return json(selected, { headers: { ...GET_CORS_HEADERS, 'Cache-Control': CACHE_CONTROL } }) + } + + return json(optimizations, { headers: { ...GET_CORS_HEADERS, 'Cache-Control': CACHE_CONTROL } }) + } catch (error) { + if (isRedisAuthenticationError(error)) { + return json({ error: REDIS_AUTHENTICATION_ERROR_MESSAGE }, { status: 500, headers: GET_CORS_HEADERS }) + } + + if (isRedisConnectivityError(error)) { + return json({ error: REDIS_CONNECTIVITY_ERROR_MESSAGE }, { status: 503, headers: GET_CORS_HEADERS }) + } + + const message = error instanceof Error ? error.message : String(error) + return json({ error: message }, { status: 500, headers: GET_CORS_HEADERS }) + } +} + +export default GET diff --git a/api/optimization/handlers.test.ts b/src/server/optimization/handlers.test.ts similarity index 61% rename from api/optimization/handlers.test.ts rename to src/server/optimization/handlers.test.ts index 46940a662..171f5d06f 100644 --- a/api/optimization/handlers.test.ts +++ b/src/server/optimization/handlers.test.ts @@ -1,4 +1,3 @@ -import type { VercelRequest, VercelResponse } from '@vercel/node' import { afterEach, describe, expect, it, vi } from 'vitest' const { @@ -54,46 +53,17 @@ vi.mock('./_lib/rpc', () => ({ import alignmentHandler from './alignment' import changeHandler from './change' -import vaultStateHandler from './vault-state' +import vaultStateHandler, { OPTIONS as vaultStateOptions } from './vault-state' -type TMockVercelResponse = VercelResponse & { - body: unknown - headers: Record - statusCode: number +function createGetRequest(path: string, query: Record = {}): Request { + return new Request(`https://yearn.fi${path}?${new URLSearchParams(query)}`) } -function createMockResponse(): TMockVercelResponse { - const response: { - body: unknown - headers: Record - json: (payload: unknown) => unknown - send: (payload: unknown) => unknown - setHeader: (name: string, value: string) => unknown - status: (code: number) => unknown - statusCode: number - } = { - body: undefined, - headers: {}, - statusCode: 200, - json(payload: unknown) { - response.body = payload - return response - }, - send(payload: unknown) { - response.body = payload - return response - }, - setHeader(name: string, value: string) { - response.headers[name] = value - return response - }, - status(code: number) { - response.statusCode = code - return response - } - } - - return response as unknown as TMockVercelResponse +function createPostRequest(path: string, body: unknown): Request { + return new Request(`https://yearn.fi${path}`, { + method: 'POST', + body: JSON.stringify(body) + }) } describe('optimization handlers', () => { @@ -103,14 +73,12 @@ describe('optimization handlers', () => { }) it('returns POST preflight headers for vault-state', async () => { - const res = createMockResponse() - - await vaultStateHandler({ method: 'OPTIONS' } as VercelRequest, res) + const response = vaultStateOptions() - expect(res.statusCode).toBe(204) - expect(res.headers['Access-Control-Allow-Origin']).toBe('*') - expect(res.headers['Access-Control-Allow-Methods']).toBe('POST, OPTIONS') - expect(res.headers['Access-Control-Allow-Headers']).toBe('Content-Type') + expect(response.status).toBe(204) + expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*') + expect(response.headers.get('Access-Control-Allow-Methods')).toBe('POST, OPTIONS') + expect(response.headers.get('Access-Control-Allow-Headers')).toBe('Content-Type') }) it('keeps CORS headers on vault-state POST responses', async () => { @@ -119,23 +87,17 @@ describe('optimization handlers', () => { strategyDebts: new Map([['0x2222222222222222222222222222222222222222', 400n]]), unallocatedBps: 6000 }) - const res = createMockResponse() - - await vaultStateHandler( - { - body: { - chainId: 1, - strategies: ['0x2222222222222222222222222222222222222222'], - vault: '0x1111111111111111111111111111111111111111' - }, - method: 'POST' - } as VercelRequest, - res + const response = await vaultStateHandler( + createPostRequest('/api/optimization/vault-state', { + chainId: 1, + strategies: ['0x2222222222222222222222222222222222222222'], + vault: '0x1111111111111111111111111111111111111111' + }) ) - expect(res.statusCode).toBe(200) - expect(res.headers['Access-Control-Allow-Origin']).toBe('*') - expect(res.body).toEqual({ + expect(response.status).toBe(200) + expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*') + await expect(response.json()).resolves.toEqual({ totalAssets: '1000', strategyDebts: { '0x2222222222222222222222222222222222222222': '400' @@ -146,26 +108,22 @@ describe('optimization handlers', () => { it('keeps CORS headers on change Redis authentication failures', async () => { readOptimizationsMock.mockRejectedValue(new MockRedisAuthenticationError('invalid token')) - const res = createMockResponse() + const response = await changeHandler(createGetRequest('/api/optimization/change')) - await changeHandler({ method: 'GET', query: {} } as VercelRequest, res) - - expect(res.statusCode).toBe(500) - expect(res.headers['Access-Control-Allow-Origin']).toBe('*') - expect(res.body).toEqual({ + expect(response.status).toBe(500) + expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*') + await expect(response.json()).resolves.toEqual({ error: 'Backend Redis authentication failed. Check UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN credentials.' }) }) it('keeps CORS headers on alignment validation responses', async () => { - const res = createMockResponse() - - await alignmentHandler({ method: 'GET', query: {} } as VercelRequest, res) + const response = await alignmentHandler(createGetRequest('/api/optimization/alignment')) - expect(res.statusCode).toBe(400) - expect(res.headers['Access-Control-Allow-Origin']).toBe('*') - expect(res.body).toEqual({ error: 'vault parameter required' }) + expect(response.status).toBe(400) + expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*') + await expect(response.json()).resolves.toEqual({ error: 'vault parameter required' }) }) it('returns vault history when change history query is enabled', async () => { @@ -221,22 +179,16 @@ describe('optimization handlers', () => { } } ]) - const res = createMockResponse() - - await changeHandler( - { - method: 'GET', - query: { - vault: targetVault, - history: '1' - } - } as VercelRequest, - res + const response = await changeHandler( + createGetRequest('/api/optimization/change', { + vault: targetVault, + history: '1' + }) ) - expect(res.statusCode).toBe(200) - expect(res.headers['Access-Control-Allow-Origin']).toBe('*') - expect(res.body).toEqual(targetHistory) + expect(response.status).toBe(200) + expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*') + await expect(response.json()).resolves.toEqual(targetHistory) expect(findVaultOptimizationMock).not.toHaveBeenCalled() }) }) diff --git a/src/server/optimization/vault-state.ts b/src/server/optimization/vault-state.ts new file mode 100644 index 000000000..c000c1647 --- /dev/null +++ b/src/server/optimization/vault-state.ts @@ -0,0 +1,49 @@ +import { json, noContent, POST_CORS_HEADERS, readJsonBody } from '../http' +import { fetchVaultOnChainState } from './_lib/rpc' + +const CACHE_CONTROL = 'public, s-maxage=60, stale-while-revalidate=30' + +export function OPTIONS(): Response { + return noContent(POST_CORS_HEADERS) +} + +export async function POST(request: Request): Promise { + const payload = await readJsonBody>(request) + 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 json({ error: 'Invalid vault address' }, { status: 400, headers: POST_CORS_HEADERS }) + } + if (chainId === null || !Number.isFinite(chainId)) { + return json({ error: 'Invalid chainId' }, { status: 400, headers: POST_CORS_HEADERS }) + } + if (strategies.length === 0) { + return json({ error: 'No strategy addresses provided' }, { status: 400, headers: POST_CORS_HEADERS }) + } + + try { + const state = await fetchVaultOnChainState(chainId, vault, strategies) + + const strategyDebts = Object.fromEntries( + Array.from(state.strategyDebts, ([address, debt]) => [address, debt.toString()]) + ) + + return json( + { + totalAssets: state.totalAssets.toString(), + strategyDebts, + unallocatedBps: state.unallocatedBps + }, + { headers: { ...POST_CORS_HEADERS, 'Cache-Control': CACHE_CONTROL } } + ) + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + return json({ error: message }, { status: 503, headers: POST_CORS_HEADERS }) + } +} + +export default POST diff --git a/api/sitemap.ts b/src/server/sitemap.ts similarity index 100% rename from api/sitemap.ts rename to src/server/sitemap.ts diff --git a/api/tenderly.helpers.test.ts b/src/server/tenderly.helpers.test.ts similarity index 88% rename from api/tenderly.helpers.test.ts rename to src/server/tenderly.helpers.test.ts index 1c180b6a6..4ec3c994c 100644 --- a/api/tenderly.helpers.test.ts +++ b/src/server/tenderly.helpers.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { TTenderlyFundRequest } from '../src/components/shared/types/tenderly' +import type { TTenderlyFundRequest } from '@/components/shared/types/tenderly' import { buildTenderlyPanelStatus, buildTenderlyRevertResponse, @@ -12,9 +12,9 @@ describe('parseTenderlyServerChains', () => { it('parses configured Tenderly chains from environment variables', () => { expect( parseTenderlyServerChains({ - VITE_TENDERLY_MODE: 'true', - VITE_TENDERLY_CHAIN_ID_FOR_1: '694201', - VITE_TENDERLY_RPC_URI_FOR_1: 'https://public.rpc', + NEXT_PUBLIC_TENDERLY_MODE: 'true', + NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_1: '694201', + NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_1: 'https://public.rpc', TENDERLY_ADMIN_RPC_URI_FOR_1: 'https://admin.rpc' }) ).toEqual([ @@ -33,9 +33,9 @@ describe('buildTenderlyPanelStatus', () => { it('reports admin availability per configured chain', () => { expect( buildTenderlyPanelStatus({ - VITE_TENDERLY_MODE: 'true', - VITE_TENDERLY_CHAIN_ID_FOR_1: '694201', - VITE_TENDERLY_RPC_URI_FOR_1: 'https://public.rpc' + NEXT_PUBLIC_TENDERLY_MODE: 'true', + NEXT_PUBLIC_TENDERLY_CHAIN_ID_FOR_1: '694201', + NEXT_PUBLIC_TENDERLY_RPC_URI_FOR_1: 'https://public.rpc' }) ).toEqual({ isTenderlyModeEnabled: true, diff --git a/src/server/tenderly.helpers.ts b/src/server/tenderly.helpers.ts new file mode 100644 index 000000000..291300952 --- /dev/null +++ b/src/server/tenderly.helpers.ts @@ -0,0 +1 @@ +export * from '@/config/tenderlyServer' diff --git a/src/server/tenderly/fund.ts b/src/server/tenderly/fund.ts new file mode 100644 index 000000000..bb8bf83d9 --- /dev/null +++ b/src/server/tenderly/fund.ts @@ -0,0 +1,38 @@ +import type { TTenderlyFundRequest } from '@/components/shared/types/tenderly' +import { ADMIN_POST_CORS_HEADERS, getRequestIp, json, noContent, readJsonBody } from '../http' +import { resolveTenderlyFundRpcRequest } from '../tenderly.helpers' +import { buildTenderlyAdminAccessDeniedResponse } from '../tenderlyAccess' +import { callTenderlyAdminRpc } from './rpc' + +export function OPTIONS(): Response { + return noContent(ADMIN_POST_CORS_HEADERS) +} + +export async function POST(request: Request): Promise { + const accessDeniedResponse = buildTenderlyAdminAccessDeniedResponse(getRequestIp(request)) + if (accessDeniedResponse) { + return accessDeniedResponse + } + + try { + const body = await readJsonBody(request) + const { method, params } = resolveTenderlyFundRpcRequest(body) + const result = await callTenderlyAdminRpc(body.canonicalChainId, method, params) + + return json( + { + method, + result + }, + { headers: ADMIN_POST_CORS_HEADERS } + ) + } catch (error) { + console.error('Error funding Tenderly wallet:', error) + return json( + { error: error instanceof Error ? error.message : 'Failed to fund wallet on Tenderly' }, + { status: 400, headers: ADMIN_POST_CORS_HEADERS } + ) + } +} + +export default POST diff --git a/src/server/tenderly/increase-time.ts b/src/server/tenderly/increase-time.ts new file mode 100644 index 000000000..d99ae0a0d --- /dev/null +++ b/src/server/tenderly/increase-time.ts @@ -0,0 +1,43 @@ +import type { TTenderlyIncreaseTimeRequest } from '@/components/shared/types/tenderly' +import { ADMIN_POST_CORS_HEADERS, getRequestIp, json, noContent, readJsonBody } from '../http' +import { buildTenderlyAdminAccessDeniedResponse } from '../tenderlyAccess' +import { callTenderlyAdminRpc } from './rpc' + +export function OPTIONS(): Response { + return noContent(ADMIN_POST_CORS_HEADERS) +} + +export async function POST(request: Request): Promise { + const accessDeniedResponse = buildTenderlyAdminAccessDeniedResponse(getRequestIp(request)) + if (accessDeniedResponse) { + return accessDeniedResponse + } + + try { + const body = await readJsonBody(request) + 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 json( + { + timeResult, + mineResult + }, + { headers: ADMIN_POST_CORS_HEADERS } + ) + } catch (error) { + console.error('Error increasing Tenderly time:', error) + return json( + { error: error instanceof Error ? error.message : 'Failed to increase Tenderly time' }, + { status: 400, headers: ADMIN_POST_CORS_HEADERS } + ) + } +} + +export default POST diff --git a/src/server/tenderly/revert.ts b/src/server/tenderly/revert.ts new file mode 100644 index 000000000..1306a7e90 --- /dev/null +++ b/src/server/tenderly/revert.ts @@ -0,0 +1,31 @@ +import type { TTenderlyRevertRequest } from '@/components/shared/types/tenderly' +import { ADMIN_POST_CORS_HEADERS, getRequestIp, json, noContent, readJsonBody } from '../http' +import { buildTenderlyRevertResponse } from '../tenderly.helpers' +import { buildTenderlyAdminAccessDeniedResponse } from '../tenderlyAccess' +import { callTenderlyAdminRpc } from './rpc' + +export function OPTIONS(): Response { + return noContent(ADMIN_POST_CORS_HEADERS) +} + +export async function POST(request: Request): Promise { + const accessDeniedResponse = buildTenderlyAdminAccessDeniedResponse(getRequestIp(request)) + if (accessDeniedResponse) { + return accessDeniedResponse + } + + try { + const body = await readJsonBody(request) + const result = await callTenderlyAdminRpc(body.canonicalChainId, 'evm_revert', [body.snapshotId]) + + return json(buildTenderlyRevertResponse(result, body.snapshotId), { headers: ADMIN_POST_CORS_HEADERS }) + } catch (error) { + console.error('Error reverting Tenderly snapshot:', error) + return json( + { error: error instanceof Error ? error.message : 'Failed to revert Tenderly snapshot' }, + { status: 400, headers: ADMIN_POST_CORS_HEADERS } + ) + } +} + +export default POST diff --git a/src/server/tenderly/rpc.ts b/src/server/tenderly/rpc.ts new file mode 100644 index 000000000..61ada7fe5 --- /dev/null +++ b/src/server/tenderly/rpc.ts @@ -0,0 +1,47 @@ +import { requireTenderlyServerChain } from '../tenderly.helpers' + +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 + } +} + +export 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 +} diff --git a/src/server/tenderly/snapshot.ts b/src/server/tenderly/snapshot.ts new file mode 100644 index 000000000..dd02d2a58 --- /dev/null +++ b/src/server/tenderly/snapshot.ts @@ -0,0 +1,39 @@ +import type { TTenderlySnapshotRequest } from '@/components/shared/types/tenderly' +import { ADMIN_POST_CORS_HEADERS, getRequestIp, json, noContent, readJsonBody } from '../http' +import { buildTenderlySnapshotRecord, requireTenderlyServerChain } from '../tenderly.helpers' +import { buildTenderlyAdminAccessDeniedResponse } from '../tenderlyAccess' +import { callTenderlyAdminRpc } from './rpc' + +export function OPTIONS(): Response { + return noContent(ADMIN_POST_CORS_HEADERS) +} + +export async function POST(request: Request): Promise { + const accessDeniedResponse = buildTenderlyAdminAccessDeniedResponse(getRequestIp(request)) + if (accessDeniedResponse) { + return accessDeniedResponse + } + + try { + const body = await readJsonBody(request) + 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 json(snapshotRecord, { headers: ADMIN_POST_CORS_HEADERS }) + } catch (error) { + console.error('Error creating Tenderly snapshot:', error) + return json( + { error: error instanceof Error ? error.message : 'Failed to create Tenderly snapshot' }, + { status: 400, headers: ADMIN_POST_CORS_HEADERS } + ) + } +} + +export default POST diff --git a/src/server/tenderly/status.ts b/src/server/tenderly/status.ts new file mode 100644 index 000000000..6a37a5b57 --- /dev/null +++ b/src/server/tenderly/status.ts @@ -0,0 +1,20 @@ +import { GET_CORS_HEADERS, json, noContent } from '../http' +import { buildTenderlyPanelStatus } from '../tenderly.helpers' + +export function OPTIONS(): Response { + return noContent(GET_CORS_HEADERS) +} + +export function GET(): Response { + try { + return json(buildTenderlyPanelStatus(process.env), { headers: GET_CORS_HEADERS }) + } catch (error) { + console.error('Error building Tenderly status:', error) + return json( + { error: error instanceof Error ? error.message : 'Failed to build Tenderly status' }, + { status: 500, headers: GET_CORS_HEADERS } + ) + } +} + +export default GET diff --git a/api/tenderlyAccess.test.ts b/src/server/tenderlyAccess.test.ts similarity index 100% rename from api/tenderlyAccess.test.ts rename to src/server/tenderlyAccess.test.ts diff --git a/api/tenderlyAccess.ts b/src/server/tenderlyAccess.ts similarity index 84% rename from api/tenderlyAccess.ts rename to src/server/tenderlyAccess.ts index 949848e10..48b5bd96a 100644 --- a/api/tenderlyAccess.ts +++ b/src/server/tenderlyAccess.ts @@ -1,3 +1,5 @@ +import { ADMIN_POST_CORS_HEADERS } from './http' + const LOOPBACK_ADDRESSES = new Set(['localhost', '::1']) function isLoopbackIpv4(address: string): boolean { @@ -46,5 +48,8 @@ export function buildTenderlyAdminAccessDeniedResponse( return undefined } - return Response.json({ error: 'Tenderly admin routes are only available from localhost' }, { status: 403 }) + return Response.json( + { error: 'Tenderly admin routes are only available from localhost' }, + { status: 403, headers: ADMIN_POST_CORS_HEADERS } + ) } diff --git a/api/vault/markdown.ts b/src/server/vault/markdown.ts similarity index 100% rename from api/vault/markdown.ts rename to src/server/vault/markdown.ts diff --git a/api/vault/vault-markdown.ts b/src/server/vault/vault-markdown.ts similarity index 100% rename from api/vault/vault-markdown.ts rename to src/server/vault/vault-markdown.ts diff --git a/api/vaults/markdown.ts b/src/server/vaults/markdown.ts similarity index 100% rename from api/vaults/markdown.ts rename to src/server/vaults/markdown.ts diff --git a/src/server/yvusd/aprs.ts b/src/server/yvusd/aprs.ts new file mode 100644 index 000000000..cd01a45df --- /dev/null +++ b/src/server/yvusd/aprs.ts @@ -0,0 +1,49 @@ +import { GET_CORS_HEADERS, json, noContent } from '../http' + +const YVUSD_APR_SERVICE_API = ( + process.env.YVUSD_APR_SERVICE_API || 'https://yearn-yvusd-apr-service.vercel.app/api/aprs' +).replace(/\/$/, '') + +export function OPTIONS(): Response { + return noContent(GET_CORS_HEADERS) +} + +export async function GET(request: Request): Promise { + try { + const upstreamUrl = new URL(YVUSD_APR_SERVICE_API) + new URL(request.url).searchParams.forEach((value, key) => { + upstreamUrl.searchParams.set(key, value) + }) + + const response = await fetch(upstreamUrl.toString(), { + headers: { + Accept: 'application/json' + } + }) + + if (!response.ok) { + const details = await response.text() + return json( + { + error: 'yvUSD APR upstream error', + status: response.status, + details + }, + { status: response.status, headers: GET_CORS_HEADERS } + ) + } + + const data = await response.json() + return json(data, { + headers: { + ...GET_CORS_HEADERS, + 'Cache-Control': 'public, s-maxage=30, stale-while-revalidate=120' + } + }) + } catch (error) { + console.error('Error proxying yvUSD APR request:', error) + return json({ error: 'Internal server error' }, { status: 500, headers: GET_CORS_HEADERS }) + } +} + +export default GET diff --git a/style.css b/style.css index dbc212e44..62c14061a 100755 --- a/style.css +++ b/style.css @@ -10,11 +10,11 @@ @theme { /* Font families */ --font-family-aeonik: - 'Aeonik', Helvetica, Arial, system-ui, sans-serif; + var(--font-aeonik), Helvetica, Arial, system-ui, sans-serif; --font-family-aeonik-fono: - 'Aeonik Fono', Helvetica, Arial, system-ui, sans-serif; + var(--font-aeonik-fono), Helvetica, Arial, system-ui, sans-serif; --font-family-aeonik-mono: - 'Aeonik Mono', Menlo, Consolas, 'Liberation Mono', monospace; + var(--font-aeonik-mono), Menlo, Consolas, 'Liberation Mono', monospace; --color-black: hsl(0 0% 0%); --color-white: hsl(0 0% 100%); @@ -163,7 +163,7 @@ html { body { background-color: var(--color-app); color: var(--color-text-primary); - font-family: 'Aeonik', Helvetica, Arial, system-ui, sans-serif; + font-family: var(--font-aeonik), Helvetica, Arial, system-ui, sans-serif; /* Mobile-optimized line height for readability */ line-height: 1.5; } @@ -176,7 +176,7 @@ body { @layer utilities { .font-number { font-family: - 'Aeonik Mono', Menlo, Consolas, 'Liberation Mono', monospace; + var(--font-aeonik-mono), Menlo, Consolas, 'Liberation Mono', monospace; } .scrollbar-none { diff --git a/tsconfig.json b/tsconfig.json index ba0fd4aae..157514c0b 100755 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ "@pages/*": ["src/components/pages/*"], "@components/*": ["src/components/*"], "@hooks/*": ["src/hooks/*"], + "react-router": ["src/navigation/react-router.tsx"], "/src/*": ["src/*"] }, /* Basic Options */ @@ -15,7 +16,7 @@ "module": "ESNext", "lib": ["es6", "es7", "ESNext", "dom"], "allowJs": true, - "jsx": "preserve", + "jsx": "react-jsx", "removeComments": false, "strict": true, "noImplicitAny": true, @@ -39,19 +40,33 @@ "noEmit": true, "incremental": true, "isolatedModules": true, - "types": ["vite/client"] + "plugins": [ + { + "name": "next" + } + ] }, - "exclude": ["node_modules", "_app", "api", "worktrees", "vitest.config.ts", "docs/temp/yvusd-gpt-pro-bundle"], + "exclude": [ + "node_modules", + "_app", + "worktrees", + "vitest.config.ts", + "src/server/**/*.test.ts", + "docs/temp/yvusd-gpt-pro-bundle" + ], "include": [ "**/*.css", "**/*.ts", "**/*.tsx", "**/*.d.tsx", + "app", + "next-env.d.ts", "public", "postcss.config.js", "tailwind.config.js", - "vite.config.ts", + "next.config.ts", "worker/index.js", - "env.d.ts" + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" ] } diff --git a/vercel.json b/vercel.json index 36d3dc6d8..a3165b18b 100644 --- a/vercel.json +++ b/vercel.json @@ -1,69 +1,4 @@ { "installCommand": "bun install --frozen-lockfile", - "buildCommand": "bun run build", - "outputDirectory": "dist", - "redirects": [ - { - "source": "/_next/image/:path*", - "destination": "https://localhost", - "permanent": true - } - ], - "rewrites": [ - { - "source": "/proxy/plausible/:path*", - "destination": "https://plausible.io/:path*" - }, - { - "source": "/assets/:path*", - "destination": "/assets/:path*" - }, - { - "source": "/:file*.(js|css|svg|png|jpg|jpeg|gif|ico|woff|woff2|ttf|eot)", - "destination": "/:file*.$1" - }, - { - "source": "/v3/:chainId/:address", - "destination": "/api/vault/meta?chainId=$1&address=$2" - }, - { - "source": "/vaults/:chainId/:address", - "destination": "/api/vault/meta?chainId=$1&address=$2" - }, - { - "source": "/vaults-beta/:chainId/:address", - "destination": "/api/vault/meta?chainId=$1&address=$2" - }, - { - "source": "/sitemap.xml", - "destination": "/api/sitemap" - }, - { - "source": "/(.*)", - "destination": "/" - } - ], - "headers": [ - { - "source": "/(.*)", - "headers": [ - { - "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;" - } - ] - } - ] + "buildCommand": "bun run build" } diff --git a/vite.config.ts b/vite.config.ts deleted file mode 100644 index 8008629bc..000000000 --- a/vite.config.ts +++ /dev/null @@ -1,193 +0,0 @@ -import react from '@vitejs/plugin-react' -import path from 'path' -import { defineConfig, loadEnv } from 'vite' - -const DEFAULT_HOST = '127.0.0.1' -const DEFAULT_ALLOWED_HOSTS = ['localhost', '127.0.0.1'] -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 API_HEALTHCHECK_RETRIES = 10 -const API_HEALTHCHECK_DELAY_MS = 300 - -function resolveApiProxyTarget(env: Record) { - const explicitTarget = env.API_PROXY_TARGET?.trim() || env.VITE_API_PROXY_TARGET?.trim() - if (explicitTarget) { - return explicitTarget.replace(/\/$/, '') - } - - const host = env.API_PROXY_HOST || DEFAULT_HOST - const apiPort = env.API_PORT?.trim() || env.VITE_API_PORT?.trim() || env.API_SERVER_PORT?.trim() || DEFAULT_API_PORT - return `http://${host}:${apiPort}` -} - -function resolveClientHost(env: Record) { - return env.HOST || DEFAULT_HOST -} - -function resolveClientPort(env: Record) { - const configuredPort = Number(env.PORT) - return Number.isInteger(configuredPort) && configuredPort > 0 ? configuredPort : 3000 -} - -function resolveAllowedHosts(env: Record) { - const configuredHosts = env.ALLOWED_HOSTS?.split(',').map((host) => host.trim().toLowerCase()) ?? [] - return [...new Set([...DEFAULT_ALLOWED_HOSTS, ...configuredHosts].filter(Boolean))] -} - -function buildProxy(apiProxyTarget: string) { - return { - '/sitemap.xml': { - target: apiProxyTarget, - changeOrigin: true, - rewrite: () => '/api/sitemap' - }, - '/api': { - target: apiProxyTarget, - changeOrigin: true - }, - '/proxy/plausible': { - target: 'https://plausible.io', - changeOrigin: true, - rewrite: (path: string) => path.replace('/proxy/plausible', '') - } - } -} - -function shouldSuppressRollupWarning(warning: { code?: string; id?: string }): boolean { - return warning.code === 'INVALID_ANNOTATION' && warning.id?.includes('/node_modules/') === true -} - -function sleep(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)) -} - -async function pingApi(apiProxyTarget: string) { - const controller = new AbortController() - const timeout = setTimeout(() => controller.abort(), API_HEALTHCHECK_TIMEOUT_MS) - - try { - const response = await fetch(`${apiProxyTarget}${API_HEALTHCHECK_PATH}`, { signal: controller.signal }) - const data = await response.json().catch(() => null) - if (response.status !== 400 || data?.error !== API_HEALTHCHECK_EXPECTED_ERROR) { - throw new Error('Unexpected API healthcheck response') - } - } finally { - clearTimeout(timeout) - } -} - -function previewApiGuard(apiProxyTarget: string) { - return { - name: 'preview-api-guard', - async configurePreviewServer(server: { config: { logger: { error: (msg: string) => void } } }) { - for (let attempt = 0; attempt < API_HEALTHCHECK_RETRIES; attempt += 1) { - try { - await pingApi(apiProxyTarget) - return - } catch (_error) { - if (attempt < API_HEALTHCHECK_RETRIES - 1) { - await sleep(API_HEALTHCHECK_DELAY_MS) - } - } - } - - const message = `Preview requires the API server running at ${apiProxyTarget}. Start it with: bun run dev:server` - server.config.logger.error(message) - throw new Error(message) - } - } -} - -export default defineConfig(({ mode }) => { - const env = { - ...loadEnv(mode, process.cwd(), ''), - ...Object.fromEntries( - Object.entries(process.env).filter((entry): entry is [string, string] => typeof entry[1] === 'string') - ) - } - const apiProxyTarget = resolveApiProxyTarget(env) - const host = resolveClientHost(env) - const port = resolveClientPort(env) - const allowedHosts = resolveAllowedHosts(env) - const proxy = buildProxy(apiProxyTarget) - - return { - plugins: [react(), previewApiGuard(apiProxyTarget)], - resolve: { - alias: { - '@': path.resolve(__dirname, './src'), - '@shared': path.resolve(__dirname, './src/components/shared'), - '@pages': path.resolve(__dirname, './src/components/pages'), - '@components': path.resolve(__dirname, './src/components'), - '@hooks': path.resolve(__dirname, './src/hooks'), - '@safe-global/safe-apps-sdk': path.resolve(__dirname, 'node_modules/@safe-global/safe-apps-sdk/dist/esm') - } - }, - define: { - 'process.env': {}, - global: 'globalThis' - }, - server: { - host, - port, - allowedHosts, - proxy - }, - preview: { - host, - port, - allowedHosts, - proxy - }, - build: { - outDir: 'dist', - sourcemap: true, - chunkSizeWarningLimit: 700, - rollupOptions: { - onwarn(warning, warn) { - if (shouldSuppressRollupWarning(warning)) { - return - } - - warn(warning) - }, - output: { - manualChunks: { - 'react-vendor': ['react', 'react-dom', 'react-router'], - 'wagmi-vendor': ['wagmi', 'viem'], - rainbowkit: ['@rainbow-me/rainbowkit'], - 'ui-vendor': ['@headlessui/react'], - 'motion-vendor': ['framer-motion'], - 'charts-vendor': ['recharts'] - } - } - } - }, - optimizeDeps: { - include: [ - 'react', - 'react-dom', - 'react-router', - 'viem', - 'viem/chains', - 'viem/actions', - 'wagmi', - 'wagmi/actions', - '@rainbow-me/rainbowkit', - '@rainbow-me/rainbowkit/wallets', - '@headlessui/react', - '@tanstack/react-query', - 'recharts', - 'framer-motion', - '@react-hookz/web' - ], - esbuildOptions: { - define: { - global: 'globalThis' - } - } - } - } -}) diff --git a/vitest.config.ts b/vitest.config.ts index c8da8698e..e564218ae 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -12,7 +12,12 @@ export default defineConfig({ '@shared': path.resolve(__dirname, './src/components/shared'), '@pages': path.resolve(__dirname, './src/components/pages'), '@components': path.resolve(__dirname, './src/components'), - '@hooks': path.resolve(__dirname, './src/hooks') + '@hooks': path.resolve(__dirname, './src/hooks'), + '@plausible-analytics/tracker': path.resolve( + __dirname, + './node_modules/@plausible-analytics/tracker/plausible.js' + ), + 'react-router': path.resolve(__dirname, './src/navigation/react-router.tsx') } }, test: { From d26ff8d600ac797e1c06c5b3ba3c186da143d6d0 Mon Sep 17 00:00:00 2001 From: w84april Date: Fri, 22 May 2026 13:46:29 +0300 Subject: [PATCH 02/59] feat: cleanup + remove more vite --- CLAUDE.md | 2 +- next.config.ts | 1 - plans/2026-04-06-withdraw-zap-usd-amounts.md | 2 +- scripts/check-client-server-boundary.ts | 8 -------- scripts/tenderly-vnet-status.ts | 4 +--- scripts/tenderly-vnet.ts | 9 +++++---- scripts/tenderly.ts | 3 +-- src/components/Link.test.tsx | 12 ++++++------ src/components/Link.tsx | 12 ++++++------ .../portfolio/components/EmptySectionCard.tsx | 2 +- .../PortfolioHistoryBreakdownModal.tsx | 2 +- .../components/PortfolioHistoryChart.tsx | 2 +- src/components/pages/portfolio/index.tsx | 2 +- .../pages/vaults/[chainID]/[address].tsx | 2 +- .../components/SuggestedVaultCard.test.tsx | 6 +++--- .../detail/VaultDetailsHeader.test.tsx | 18 +++++++++--------- .../components/detail/VaultDetailsHeader.tsx | 2 +- .../filters/VaultVersionToggle.test.tsx | 6 +++--- .../components/filters/VaultVersionToggle.tsx | 2 +- .../components/list/VaultsListRow.test.tsx | 6 +++--- .../vaults/components/list/VaultsListRow.tsx | 2 +- .../vaults/components/widget/WalletPanel.tsx | 2 +- .../pages/vaults/hooks/useVaultsQueryArgs.ts | 2 +- .../pages/vaults/hooks/useVaultsQueryState.ts | 2 +- .../shared/components/AccountDropdown.tsx | 2 +- src/components/shared/components/Header.tsx | 2 +- .../shared/components/MobileNavMenu.tsx | 2 +- src/components/shared/contexts/useYearn.tsx | 2 +- src/components/shared/hooks/useCurrentApp.tsx | 2 +- src/context/ChainsProvider.tsx | 2 +- src/hooks/usePathname.ts | 2 +- .../{react-router.tsx => client.tsx} | 18 ++---------------- tsconfig.json | 3 +-- vitest.config.ts => vitest.config.mts | 10 +++++----- 34 files changed, 65 insertions(+), 91 deletions(-) rename src/navigation/{react-router.tsx => client.tsx} (90%) rename vitest.config.ts => vitest.config.mts (81%) diff --git a/CLAUDE.md b/CLAUDE.md index ce610f7de..1802a7821 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -77,7 +77,7 @@ When writing a new `useEffect`, add a brief comment explaining why an alternativ **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 the compatibility layer for existing React Router-style hooks/components while the app runs on Next navigation +- `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/` diff --git a/next.config.ts b/next.config.ts index 2c81466d7..6f89f22e7 100644 --- a/next.config.ts +++ b/next.config.ts @@ -25,7 +25,6 @@ const nextConfig: NextConfig = { poweredByHeader: false, turbopack: { resolveAlias: { - 'react-router': './src/navigation/react-router.tsx', '@safe-global/safe-apps-sdk': './node_modules/@safe-global/safe-apps-sdk/dist/esm' } }, 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/scripts/check-client-server-boundary.ts b/scripts/check-client-server-boundary.ts index 34f82b65b..b1c57c7a7 100644 --- a/scripts/check-client-server-boundary.ts +++ b/scripts/check-client-server-boundary.ts @@ -30,10 +30,6 @@ const aliasPrefixes = [ ['src/', 'src/'] ] as const -const exactAliases: Record = { - 'react-router': 'src/navigation/react-router.tsx' -} - function toDisplayPath(filePath: string): string { return path.relative(PROJECT_ROOT, filePath).split(path.sep).join('/') } @@ -87,10 +83,6 @@ function resolveCandidate(candidatePath: string): string | undefined { } function resolveImport(importer: string, specifier: string): string | undefined { - if (specifier in exactAliases) { - return resolveCandidate(path.join(PROJECT_ROOT, exactAliases[specifier])) - } - if (specifier.startsWith('.')) { return resolveCandidate(path.resolve(path.dirname(importer), specifier)) } diff --git a/scripts/tenderly-vnet-status.ts b/scripts/tenderly-vnet-status.ts index 3218508e8..a5660bc31 100644 --- a/scripts/tenderly-vnet-status.ts +++ b/scripts/tenderly-vnet-status.ts @@ -1,7 +1,6 @@ /// import { resolve as resolvePath } from 'node:path' -import { fileURLToPath } from 'node:url' import { parseTenderlyServerChains, type TTenderlyServerChainConfig } from '../src/config/tenderlyServer' import { buildPredictablePublicRpcUrl, readEnvFile, sanitizeConsoleText } from './tenderly-vnet' @@ -912,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.ts b/scripts/tenderly-vnet.ts index 85782a3ed..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 @@ -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/components/Link.test.tsx b/src/components/Link.test.tsx index 4388b97a9..129994fdc 100644 --- a/src/components/Link.test.tsx +++ b/src/components/Link.test.tsx @@ -1,6 +1,6 @@ import { renderToStaticMarkup } from 'react-dom/server' -import { MemoryRouter } from 'react-router' import { describe, expect, it } from 'vitest' +import { MemoryNavigationProvider } from '@/navigation/client' import Link from './Link' describe('Link', () => { @@ -12,11 +12,11 @@ describe('Link', () => { expect(html).toContain('rel="noopener noreferrer"') }) - it('renders internal links through react-router', () => { + it('renders internal links through the navigation adapter', () => { const html = renderToStaticMarkup( - + Vaults - + ) expect(html).toContain('href="/vaults"') @@ -25,9 +25,9 @@ describe('Link', () => { it('normalizes first-party absolute URLs for stable SSR', () => { const html = renderToStaticMarkup( - + Yearn V3 - + ) expect(html).toContain('href="/v3"') diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 673e8ba98..a1420786c 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from 'react' -import type { LinkProps as RouterLinkProps } from 'react-router' -import { Link as RouterLink } from 'react-router' +import type { LinkProps as NavigationLinkProps } from '@/navigation/client' +import { Link as NavigationLink } from '@/navigation/client' import { resolveLinkTarget } from '@/navigation/url' export type LinkProps = { @@ -11,7 +11,7 @@ export type LinkProps = { target?: string rel?: string onClick?: (e: React.MouseEvent) => void -} & Omit +} & Omit export default function Link(props: LinkProps): React.ReactElement { const { href, to, children, className, target, rel, onClick, ...rest } = props @@ -35,10 +35,10 @@ export default function Link(props: LinkProps): React.ReactElement { ) } - // Internal link using React Router + // Internal link using the Next navigation adapter. return ( - + {children} - + ) } diff --git a/src/components/pages/portfolio/components/EmptySectionCard.tsx b/src/components/pages/portfolio/components/EmptySectionCard.tsx index d390e9b16..79dfd401b 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 type { ReactElement } from 'react' -import { Link } from 'react-router' +import { Link } from '@/navigation/client' type TEmptySectionCardProps = { title: string diff --git a/src/components/pages/portfolio/components/PortfolioHistoryBreakdownModal.tsx b/src/components/pages/portfolio/components/PortfolioHistoryBreakdownModal.tsx index e9428bdc0..1c7d5b308 100644 --- a/src/components/pages/portfolio/components/PortfolioHistoryBreakdownModal.tsx +++ b/src/components/pages/portfolio/components/PortfolioHistoryBreakdownModal.tsx @@ -14,8 +14,8 @@ import { IconSpinner } from '@shared/icons/IconSpinner' import { cl, formatUSD, SUPPORTED_NETWORKS, toAddress } from '@shared/utils' import type { ReactElement } from 'react' import { Fragment, useEffect, useMemo, useState } from 'react' -import { Link } from 'react-router' import { env } from '@/env' +import { Link } from '@/navigation/client' import { usePortfolioBreakdown } from '../hooks/usePortfolioBreakdown' import type { TPortfolioBreakdownResponse, TPortfolioBreakdownVault } from '../types/api' diff --git a/src/components/pages/portfolio/components/PortfolioHistoryChart.tsx b/src/components/pages/portfolio/components/PortfolioHistoryChart.tsx index 938c37dea..5217963d2 100644 --- a/src/components/pages/portfolio/components/PortfolioHistoryChart.tsx +++ b/src/components/pages/portfolio/components/PortfolioHistoryChart.tsx @@ -23,9 +23,9 @@ import { cl, formatPercent, formatUSD, SELECTOR_BAR_STYLES } from '@shared/utils import { getVaultName as getDisplayVaultName } from '@shared/utils/helpers' import type { ReactElement } from 'react' import { useEffect, useId, useMemo, useState } from 'react' -import { Link } from 'react-router' import { Area, CartesianGrid, ComposedChart, Line, XAxis, YAxis } from 'recharts' import type { AxisDomain } from 'recharts/types/util/types' +import { Link } from '@/navigation/client' import type { TPortfolioHistoryChartData, TPortfolioHistoryDenomination, diff --git a/src/components/pages/portfolio/index.tsx b/src/components/pages/portfolio/index.tsx index b1db2b459..d2a82f603 100644 --- a/src/components/pages/portfolio/index.tsx +++ b/src/components/pages/portfolio/index.tsx @@ -75,8 +75,8 @@ import { PLAUSIBLE_EVENTS } from '@shared/utils/plausible' import { getNetwork } from '@shared/utils/wagmi' import type { CSSProperties, ReactElement } from 'react' import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { useSearchParams } from 'react-router' import { env } from '@/env' +import { useSearchParams } from '@/navigation/client' import Image from '/src/components/Image' import Link from '/src/components/Link' import type { diff --git a/src/components/pages/vaults/[chainID]/[address].tsx b/src/components/pages/vaults/[chainID]/[address].tsx index 02049bb34..3f7f141de 100644 --- a/src/components/pages/vaults/[chainID]/[address].tsx +++ b/src/components/pages/vaults/[chainID]/[address].tsx @@ -78,7 +78,6 @@ import { useRef, useState } from 'react' -import { useLocation, useNavigate, useParams } from 'react-router' import { isAddressEqual } from 'viem' import { VaultsListChip } from '@/components/pages/vaults/components/list/VaultsListChip' import { deriveListKind } from '@/components/pages/vaults/utils/vaultListFacets' @@ -86,6 +85,7 @@ import { getVaultPrimaryLogoSrc } from '@/components/pages/vaults/utils/vaultLog import { getCategoryDescription, getProductTypeDescription } from '@/components/pages/vaults/utils/vaultTagCopy' import { useWeb3 } from '@/components/shared/contexts/useWeb3' import { useDevFlags } from '@/contexts/useDevFlags' +import { useLocation, useNavigate, useParams } from '@/navigation/client' const resolveHeaderOffset = (): number => { if (typeof window === 'undefined') return 0 diff --git a/src/components/pages/vaults/components/SuggestedVaultCard.test.tsx b/src/components/pages/vaults/components/SuggestedVaultCard.test.tsx index 37d3dd496..3f0daf189 100644 --- a/src/components/pages/vaults/components/SuggestedVaultCard.test.tsx +++ b/src/components/pages/vaults/components/SuggestedVaultCard.test.tsx @@ -2,8 +2,8 @@ import type { TKongVaultInput } from '@pages/vaults/domain/kongVaultSelectors' import { useVaultApyData } from '@pages/vaults/hooks/useVaultApyData' import { YVUSD_LOCKED_ADDRESS, YVUSD_UNLOCKED_ADDRESS } from '@pages/vaults/utils/yvUsd' import { renderToStaticMarkup } from 'react-dom/server' -import { MemoryRouter } from 'react-router' import { describe, expect, it, vi } from 'vitest' +import { MemoryNavigationProvider } from '@/navigation/client' import { SuggestedVaultCard } from './SuggestedVaultCard' @@ -46,9 +46,9 @@ function renderCard(vault: TKongVaultInput): string { try { return renderToStaticMarkup( - + - + ) } finally { ;(globalThis as unknown as { window: unknown }).window = originalWindow diff --git a/src/components/pages/vaults/components/detail/VaultDetailsHeader.test.tsx b/src/components/pages/vaults/components/detail/VaultDetailsHeader.test.tsx index 82a8abacc..95944106d 100644 --- a/src/components/pages/vaults/components/detail/VaultDetailsHeader.test.tsx +++ b/src/components/pages/vaults/components/detail/VaultDetailsHeader.test.tsx @@ -2,8 +2,8 @@ import { YVBTC_LOCKED_ADDRESS, YVBTC_UNLOCKED_ADDRESS } from '@pages/vaults/util import { YVUSD_LOCKED_ADDRESS, YVUSD_UNLOCKED_ADDRESS } from '@pages/vaults/utils/yvUsd' import type { ReactNode } from 'react' import { renderToStaticMarkup } from 'react-dom/server' -import { MemoryRouter } from 'react-router' import { beforeEach, describe, expect, it, vi } from 'vitest' +import { MemoryNavigationProvider } from '@/navigation/client' const useVaultUserDataMock = vi.hoisted(() => vi.fn(() => ({ @@ -303,9 +303,9 @@ describe('VaultDetailsHeaderPresentation', () => { it('uses the standard compressed token logo size for yvUSD', () => { const html = renderToStaticMarkup( - + - + ) expect(html).toContain('style="width:32px;height:32px"') @@ -315,9 +315,9 @@ describe('VaultDetailsHeaderPresentation', () => { it('keeps the explorer link visible when header is compressed', () => { const html = renderToStaticMarkup( - + - + ) expect(html).toContain('View vault on block explorer') @@ -326,9 +326,9 @@ describe('VaultDetailsHeaderPresentation', () => { it('hides cooldown information when there is no locked yvUSD deposit', () => { const html = renderToStaticMarkup( - + - + ) expect(html).toContain('Your Deposits') @@ -339,9 +339,9 @@ describe('VaultDetailsHeaderPresentation', () => { it('does not enable locked yvBTC user-data reads while the locked address is a placeholder', () => { renderToStaticMarkup( - + - + ) expect(useVaultUserDataMock).toHaveBeenCalledWith( diff --git a/src/components/pages/vaults/components/detail/VaultDetailsHeader.tsx b/src/components/pages/vaults/components/detail/VaultDetailsHeader.tsx index 0ee231d41..f7e974214 100755 --- a/src/components/pages/vaults/components/detail/VaultDetailsHeader.tsx +++ b/src/components/pages/vaults/components/detail/VaultDetailsHeader.tsx @@ -72,8 +72,8 @@ import { getVaultName } from '@shared/utils/helpers' import { getNetwork } from '@shared/utils/wagmi/utils' import type { ReactElement, Ref } from 'react' import { useEffect, useRef, useState } from 'react' -import { Link } from 'react-router' import { env } from '@/env' +import { Link } from '@/navigation/client' type TVaultKindType = 'multi' | 'single' | undefined diff --git a/src/components/pages/vaults/components/filters/VaultVersionToggle.test.tsx b/src/components/pages/vaults/components/filters/VaultVersionToggle.test.tsx index ca3068ed5..56e21f814 100644 --- a/src/components/pages/vaults/components/filters/VaultVersionToggle.test.tsx +++ b/src/components/pages/vaults/components/filters/VaultVersionToggle.test.tsx @@ -1,14 +1,14 @@ import { renderToStaticMarkup } from 'react-dom/server' -import { MemoryRouter } from 'react-router' import { describe, expect, it } from 'vitest' +import { MemoryNavigationProvider } from '@/navigation/client' import { VaultVersionToggle } from './VaultVersionToggle' function renderToggle(entry: string): string { return renderToStaticMarkup( - + - + ) } diff --git a/src/components/pages/vaults/components/filters/VaultVersionToggle.tsx b/src/components/pages/vaults/components/filters/VaultVersionToggle.tsx index 2bb33b2db..b92c46f01 100644 --- a/src/components/pages/vaults/components/filters/VaultVersionToggle.tsx +++ b/src/components/pages/vaults/components/filters/VaultVersionToggle.tsx @@ -11,7 +11,7 @@ import { Tooltip } from '@shared/components/Tooltip' import { cl } from '@shared/utils' import { PLAUSIBLE_EVENTS } from '@shared/utils/plausible' import type { ReactElement } from 'react' -import { useSearchParams } from 'react-router' +import { useSearchParams } from '@/navigation/client' type TVaultVersionToggleProps = { className?: string diff --git a/src/components/pages/vaults/components/list/VaultsListRow.test.tsx b/src/components/pages/vaults/components/list/VaultsListRow.test.tsx index 3a41c17b0..47db9ef5f 100644 --- a/src/components/pages/vaults/components/list/VaultsListRow.test.tsx +++ b/src/components/pages/vaults/components/list/VaultsListRow.test.tsx @@ -5,8 +5,8 @@ import { YVUSD_UNLOCKED_ADDRESS } from '@pages/vaults/utils/yvUsd' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import type { ComponentProps } from 'react' import { renderToStaticMarkup } from 'react-dom/server' -import { MemoryRouter } from 'react-router' import { beforeEach, describe, expect, it, vi } from 'vitest' +import { MemoryNavigationProvider } from '@/navigation/client' import { VaultsListRow } from './VaultsListRow' @@ -73,9 +73,9 @@ function renderRowHtml(vault: TKongVaultInput, props?: Partial - + - + ) } diff --git a/src/components/pages/vaults/components/list/VaultsListRow.tsx b/src/components/pages/vaults/components/list/VaultsListRow.tsx index 26cc4ddca..0698f9850 100755 --- a/src/components/pages/vaults/components/list/VaultsListRow.tsx +++ b/src/components/pages/vaults/components/list/VaultsListRow.tsx @@ -67,8 +67,8 @@ import { getNetwork } from '@shared/utils/wagmi' import { useQueryClient } from '@tanstack/react-query' import type { MouseEvent, ReactElement } from 'react' import { lazy, memo, Suspense, useCallback, useEffect, useMemo, useState } from 'react' -import { useNavigate } from 'react-router' import { env } from '@/env' +import { useNavigate } from '@/navigation/client' import type { TVaultsExpandedView } from './VaultsExpandedSelector' import { VaultsListChip } from './VaultsListChip' diff --git a/src/components/pages/vaults/components/widget/WalletPanel.tsx b/src/components/pages/vaults/components/widget/WalletPanel.tsx index 7eec98685..a3ac969eb 100644 --- a/src/components/pages/vaults/components/widget/WalletPanel.tsx +++ b/src/components/pages/vaults/components/widget/WalletPanel.tsx @@ -36,7 +36,7 @@ import { } from '@shared/utils' import { getNetwork } from '@shared/utils/wagmi/utils' import { type FC, type ReactElement, useCallback, useMemo, useState } from 'react' -import { useNavigate } from 'react-router' +import { useNavigate } from '@/navigation/client' import { getAwaitingExecutionEntries } from './WalletPanel.helpers' import { formatDuration, parseCooldownStatus, resolveCooldownWindowState } from './yvUSD/cooldownUtils' diff --git a/src/components/pages/vaults/hooks/useVaultsQueryArgs.ts b/src/components/pages/vaults/hooks/useVaultsQueryArgs.ts index a5b72ef62..a07d0f0af 100644 --- a/src/components/pages/vaults/hooks/useVaultsQueryArgs.ts +++ b/src/components/pages/vaults/hooks/useVaultsQueryArgs.ts @@ -2,7 +2,7 @@ import type { TPossibleSortBy } from '@pages/vaults/hooks/useSortVaults' import { useSupportedChains } from '@shared/hooks/useSupportedChains' import type { TDict, TSortDirection } from '@shared/types' import { useCallback, useMemo } from 'react' -import { useSearchParams } from 'react-router' +import { useSearchParams } from '@/navigation/client' type TQueryArgs = { search: string | null | undefined diff --git a/src/components/pages/vaults/hooks/useVaultsQueryState.ts b/src/components/pages/vaults/hooks/useVaultsQueryState.ts index 111f04a50..b13c401b6 100644 --- a/src/components/pages/vaults/hooks/useVaultsQueryState.ts +++ b/src/components/pages/vaults/hooks/useVaultsQueryState.ts @@ -6,7 +6,7 @@ import { getSupportedChainsForVaultType, normalizeVaultTypeParam } from '@pages/ import type { TSortDirection } from '@shared/types' import { copyToClipboard } from '@shared/utils/helpers' import { startTransition, useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { useLocation, useSearchParams } from 'react-router' +import { useLocation, useSearchParams } from '@/navigation/client' type TVaultsQueryStateConfig = { defaultTypes?: string[] diff --git a/src/components/shared/components/AccountDropdown.tsx b/src/components/shared/components/AccountDropdown.tsx index 7b8990788..9ff918904 100644 --- a/src/components/shared/components/AccountDropdown.tsx +++ b/src/components/shared/components/AccountDropdown.tsx @@ -18,7 +18,7 @@ import { cl, formatUSD } from '@shared/utils' import { truncateHex } from '@shared/utils/tools.address' import type { ReactElement } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react' -import { useNavigate } from 'react-router' +import { useNavigate } from '@/navigation/client' import { DropdownPanel } from './DropdownPanel' type TAccountDropdownProps = { diff --git a/src/components/shared/components/Header.tsx b/src/components/shared/components/Header.tsx index 56fb41a8e..a812a32e4 100644 --- a/src/components/shared/components/Header.tsx +++ b/src/components/shared/components/Header.tsx @@ -15,7 +15,6 @@ import { normalizePathname } from '@shared/utils/routes' import { truncateHex } from '@shared/utils/tools.address' import type { KeyboardEvent, MouseEvent, ReactElement } from 'react' import { useMemo, useState } from 'react' -import { useLocation } from 'react-router' import { useAccount, useSwitchChain } from 'wagmi' import { canToggleTenderlyMode, @@ -25,6 +24,7 @@ import { resolveConnectedTenderlyExecutionChain, tenderlyConfiguredRuntime } from '@/config/tenderly' +import { useLocation } from '@/navigation/client' import Link from '/src/components/Link' import { AccountDropdown } from './AccountDropdown' import { HeaderNavMenu } from './HeaderNavMenu' diff --git a/src/components/shared/components/MobileNavMenu.tsx b/src/components/shared/components/MobileNavMenu.tsx index 9369ec949..70b48479f 100644 --- a/src/components/shared/components/MobileNavMenu.tsx +++ b/src/components/shared/components/MobileNavMenu.tsx @@ -22,8 +22,8 @@ import { cl, formatUSD } from '@shared/utils' import { truncateHex } from '@shared/utils/tools.address' import type { ReactElement } from 'react' import { useEffect, useState } from 'react' -import { useNavigate } from 'react-router' import { env } from '@/env' +import { useNavigate } from '@/navigation/client' import Image from '/src/components/Image' import Link from '/src/components/Link' import { IconTelegram } from '../icons/IconTelegram' diff --git a/src/components/shared/contexts/useYearn.tsx b/src/components/shared/contexts/useYearn.tsx index 8e6dd4113..66ca47305 100755 --- a/src/components/shared/contexts/useYearn.tsx +++ b/src/components/shared/contexts/useYearn.tsx @@ -11,9 +11,9 @@ import { clampZapSlippage } from '@shared/utils/slippage' import type { QueryObserverResult } from '@tanstack/react-query' import type { ReactElement } from 'react' import { createContext, memo, useCallback, useContext, useEffect, useState } from 'react' -import { useLocation } from 'react-router' import { deserialize, serialize } from 'wagmi' import { env } from '@/env' +import { useLocation } from '@/navigation/client' export const DEFAULT_SLIPPAGE = 0.5 export const DEFAULT_MAX_LOSS = 1n diff --git a/src/components/shared/hooks/useCurrentApp.tsx b/src/components/shared/hooks/useCurrentApp.tsx index 8953dc010..6ef9eb279 100644 --- a/src/components/shared/hooks/useCurrentApp.tsx +++ b/src/components/shared/hooks/useCurrentApp.tsx @@ -2,7 +2,7 @@ import landingManifest from '@shared/data/landing-manifest.json' import vaultsManifest from '@shared/data/vaults-manifest.json' import type { TDict } from '@shared/types' import { useMemo } from 'react' -import { useLocation } from 'react-router' +import { useLocation } from '@/navigation/client' type TCurrentApp = { name: 'Home' | 'Vaults' | string diff --git a/src/context/ChainsProvider.tsx b/src/context/ChainsProvider.tsx index 9ac208a8a..76dd65d29 100644 --- a/src/context/ChainsProvider.tsx +++ b/src/context/ChainsProvider.tsx @@ -1,10 +1,10 @@ import type { FC, PropsWithChildren } from 'react' import { useMemo } from 'react' -import { useLocation } from 'react-router' import { useAccount, useChainId, useSwitchChain } from 'wagmi' import { supportedAppChains, type TSupportedChainId } from '@/config/supportedChains' import { getCanonicalChain, resolveConnectedCanonicalChainId, resolveExecutionChainId } from '@/config/tenderly' import { chainsContext, type TChainsContext } from '@/context/chainsContext' +import { useLocation } from '@/navigation/client' const DEFAULT_CHAIN_ID = 1 as TSupportedChainId diff --git a/src/hooks/usePathname.ts b/src/hooks/usePathname.ts index 94a40ef92..3877172c6 100644 --- a/src/hooks/usePathname.ts +++ b/src/hooks/usePathname.ts @@ -1,4 +1,4 @@ -import { useLocation } from 'react-router' +import { useLocation } from '@/navigation/client' export function usePathname(): string { const location = useLocation() diff --git a/src/navigation/react-router.tsx b/src/navigation/client.tsx similarity index 90% rename from src/navigation/react-router.tsx rename to src/navigation/client.tsx index 96992923d..62ca011cb 100644 --- a/src/navigation/react-router.tsx +++ b/src/navigation/client.tsx @@ -2,7 +2,7 @@ import NextLink from 'next/link' import type { AnchorHTMLAttributes, ReactElement, ReactNode } from 'react' -import { useCallback, useContext, useEffect, useMemo, useState } from 'react' +import { useCallback, useContext, useMemo, useState } from 'react' import { NavigationContext, type TNavigateOptions, type TNavigationContext } from './context' import { resolveLinkTarget } from './url' @@ -50,7 +50,7 @@ function getBrowserNavigationContext(): TNavigationContext { } } -export function MemoryRouter({ +export function MemoryNavigationProvider({ children, initialEntries = ['/'] }: { @@ -73,10 +73,6 @@ export function MemoryRouter({ return {children} } -export function BrowserRouter({ children }: { children: ReactNode }): ReactElement { - return <>{children} -} - export function Link(props: LinkProps): ReactElement { const { to, children, preventScrollReset, reloadDocument, replace, state, ...anchorProps } = props const { href, isExternal } = resolveLinkTarget(to || '') @@ -96,16 +92,6 @@ export function Link(props: LinkProps): ReactElement { ) } -export function Navigate({ to, replace = false }: { to: string; replace?: boolean }): null { - const navigate = useNavigate() - - useEffect(() => { - navigate(to, { replace }) - }, [navigate, replace, to]) - - return null -} - export function useLocation(): TLocation { const navigation = useContext(NavigationContext) ?? getBrowserNavigationContext() diff --git a/tsconfig.json b/tsconfig.json index 157514c0b..e89393516 100755 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,6 @@ "@pages/*": ["src/components/pages/*"], "@components/*": ["src/components/*"], "@hooks/*": ["src/hooks/*"], - "react-router": ["src/navigation/react-router.tsx"], "/src/*": ["src/*"] }, /* Basic Options */ @@ -50,7 +49,7 @@ "node_modules", "_app", "worktrees", - "vitest.config.ts", + "vitest.config.mts", "src/server/**/*.test.ts", "docs/temp/yvusd-gpt-pro-bundle" ], diff --git a/vitest.config.ts b/vitest.config.mts similarity index 81% rename from vitest.config.ts rename to vitest.config.mts index e564218ae..4322c1b29 100644 --- a/vitest.config.ts +++ b/vitest.config.mts @@ -1,7 +1,6 @@ import path from 'path' import { defineConfig } from 'vitest/config' -// https://vitejs.dev/config/ export default defineConfig({ esbuild: { jsx: 'automatic' @@ -16,15 +15,16 @@ export default defineConfig({ '@plausible-analytics/tracker': path.resolve( __dirname, './node_modules/@plausible-analytics/tracker/plausible.js' - ), - 'react-router': path.resolve(__dirname, './src/navigation/react-router.tsx') + ) } }, test: { globals: true, environment: 'node', - deps: { - inline: ['@shared'] + server: { + deps: { + inline: ['@shared'] + } } } }) From 893f216257da1b516d4476ace3fc11f3cdbcbaea Mon Sep 17 00:00:00 2001 From: w84april Date: Fri, 22 May 2026 14:31:01 +0300 Subject: [PATCH 03/59] feat: make routing next native --- src/App.tsx | 9 +- src/components/Link.test.tsx | 36 ---- src/components/Link.tsx | 44 ----- .../landing/components/sections/Footer.tsx | 3 +- .../landing/components/sections/Hero.tsx | 2 +- .../components/sections/Integrations.tsx | 4 +- .../landing/components/sections/Partners.tsx | 4 +- .../landing/components/sections/Security.tsx | 4 +- .../portfolio/components/EmptySectionCard.tsx | 6 +- .../PortfolioHistoryBreakdownModal.tsx | 4 +- .../components/PortfolioHistoryChart.tsx | 4 +- src/components/pages/portfolio/index.tsx | 22 ++- .../pages/vaults/[chainID]/[address].tsx | 18 +- .../components/SuggestedVaultCard.test.tsx | 7 +- .../vaults/components/SuggestedVaultCard.tsx | 4 +- .../vaults/components/TrendingVaults.tsx | 4 +- .../components/compare/CompareVaultCard.tsx | 2 +- .../components/compare/VaultsCompareModal.tsx | 2 +- .../detail/VaultDetailsHeader.test.tsx | 17 +- .../components/detail/VaultDetailsHeader.tsx | 6 +- .../components/detail/VaultsListStrategy.tsx | 2 +- .../filters/VaultVersionToggle.test.tsx | 24 ++- .../components/filters/VaultVersionToggle.tsx | 11 +- .../components/list/VaultsListRow.test.tsx | 32 ++-- .../vaults/components/list/VaultsListRow.tsx | 8 +- .../components/notifications/Notification.tsx | 2 +- .../vaults/components/widget/WalletPanel.tsx | 6 +- .../pages/vaults/hooks/useVaultsQueryArgs.ts | 11 +- .../pages/vaults/hooks/useVaultsQueryState.ts | 44 +++-- .../shared/components/AccountDropdown.tsx | 12 +- .../shared/components/AllocationTooltip.tsx | 2 +- .../shared/components/Breadcrumbs.tsx | 4 +- src/components/shared/components/Header.tsx | 7 +- .../shared/components/HeaderNavMenu.tsx | 8 +- .../shared/components/MobileNavMenu.tsx | 21 ++- .../shared/components/SectionHeader.tsx | 18 +- src/components/shared/contexts/useYearn.tsx | 10 +- src/components/shared/hooks/useCurrentApp.tsx | 5 +- src/context/ChainsProvider.tsx | 4 +- src/hooks/usePathname.ts | 6 - src/navigation/NextNavigationProvider.tsx | 33 ---- src/navigation/client.tsx | 156 ------------------ src/navigation/context.tsx | 23 --- src/navigation/url.ts | 25 --- 44 files changed, 201 insertions(+), 475 deletions(-) delete mode 100644 src/components/Link.test.tsx delete mode 100644 src/components/Link.tsx delete mode 100644 src/hooks/usePathname.ts delete mode 100644 src/navigation/NextNavigationProvider.tsx delete mode 100644 src/navigation/client.tsx delete mode 100644 src/navigation/context.tsx delete mode 100644 src/navigation/url.ts diff --git a/src/App.tsx b/src/App.tsx index 82e7d592a..652f24374 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -21,6 +21,7 @@ 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 { usePathname } from 'next/navigation' import type { ReactElement, ReactNode } from 'react' import { useEffect, useLayoutEffect, useState } from 'react' import { Toaster } from 'react-hot-toast' @@ -28,9 +29,7 @@ import { WagmiProvider } from 'wagmi' import { TenderlyControlPanel } from '@/components/shared/components/TenderlyControlPanel' import { wagmiConfig } from '@/config/wagmi' import { ChainsProvider } from '@/context/ChainsProvider' -import { usePathname } from '@/hooks/usePathname' import { initializePlausible } from '@/hooks/usePlausible' -import { NextNavigationProvider } from '@/navigation/NextNavigationProvider' import { disableServiceWorkerDev } from '@/utils/disableServiceWorkerDev' const bigintPrototype = BigInt.prototype as unknown as { toJSON?: () => string } @@ -144,11 +143,7 @@ function AppContent({ children }: { children: ReactNode }): ReactElement { } function App({ children }: { children: ReactNode }): ReactElement { - return ( - - {children} - - ) + return {children} } export default App diff --git a/src/components/Link.test.tsx b/src/components/Link.test.tsx deleted file mode 100644 index 129994fdc..000000000 --- a/src/components/Link.test.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { renderToStaticMarkup } from 'react-dom/server' -import { describe, expect, it } from 'vitest' -import { MemoryNavigationProvider } from '@/navigation/client' -import Link from './Link' - -describe('Link', () => { - it('renders external links as with safe defaults', () => { - const html = renderToStaticMarkup(Example) - - expect(html).toContain('href="https://example.com"') - expect(html).toContain('target="_blank"') - expect(html).toContain('rel="noopener noreferrer"') - }) - - it('renders internal links through the navigation adapter', () => { - const html = renderToStaticMarkup( - - Vaults - - ) - - expect(html).toContain('href="/vaults"') - expect(html).not.toContain('target="_blank"') - }) - - it('normalizes first-party absolute URLs for stable SSR', () => { - const html = renderToStaticMarkup( - - Yearn V3 - - ) - - expect(html).toContain('href="/v3"') - expect(html).not.toContain('target="_blank"') - }) -}) diff --git a/src/components/Link.tsx b/src/components/Link.tsx deleted file mode 100644 index a1420786c..000000000 --- a/src/components/Link.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import type { ReactNode } from 'react' -import type { LinkProps as NavigationLinkProps } from '@/navigation/client' -import { Link as NavigationLink } from '@/navigation/client' -import { resolveLinkTarget } from '@/navigation/url' - -export type LinkProps = { - href?: string - to?: string - children: ReactNode - className?: string - target?: string - rel?: string - onClick?: (e: React.MouseEvent) => void -} & Omit - -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 { href: url, isExternal } = resolveLinkTarget(href || to || '') - - // Check if it's an external link - if (isExternal) { - return ( - - {children} - - ) - } - - // Internal link using the Next navigation adapter. - return ( - - {children} - - ) -} 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..1ce8ac953 100644 --- a/src/components/pages/landing/components/sections/Hero.tsx +++ b/src/components/pages/landing/components/sections/Hero.tsx @@ -4,10 +4,10 @@ import { SectionHeader } from '@shared/components/SectionHeader' import { TvlStat } from '@shared/components/TvlStat' import { useFetch } from '@shared/hooks/useFetch' import { PLAUSIBLE_EVENTS } from '@shared/utils/plausible' +import Link from 'next/link' import type { ReactElement } from 'react' import * as z from 'zod' import Image from '/src/components/Image' -import Link from '/src/components/Link' function AnimatedLogos(): ReactElement { return ( diff --git a/src/components/pages/landing/components/sections/Integrations.tsx b/src/components/pages/landing/components/sections/Integrations.tsx index 740218b10..85faefcc5 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 79dfd401b..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 '@/navigation/client' 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/index.tsx b/src/components/pages/portfolio/index.tsx index d2a82f603..48c18137d 100644 --- a/src/components/pages/portfolio/index.tsx +++ b/src/components/pages/portfolio/index.tsx @@ -73,12 +73,12 @@ 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 { env } from '@/env' -import { useSearchParams } from '@/navigation/client' import Image from '/src/components/Image' -import Link from '/src/components/Link' import type { TGrowthDisplayMode, TPortfolioHistoryChartTab, @@ -2858,7 +2858,9 @@ 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) @@ -2867,6 +2869,14 @@ function PortfolioPage(): ReactElement { 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') { @@ -2931,15 +2941,15 @@ function PortfolioPage(): ReactElement { const handleTabSelect = useCallback( (tab: TPortfolioTabKey) => { - 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) }, - [searchParams, setSearchParams] + [replaceSearchParams, searchParams] ) useEffect(() => { diff --git a/src/components/pages/vaults/[chainID]/[address].tsx b/src/components/pages/vaults/[chainID]/[address].tsx index 3f7f141de..a34dae14b 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 { @@ -66,6 +65,8 @@ import { IconChevron } from '@shared/icons/IconChevron' 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, @@ -85,7 +86,6 @@ import { getVaultPrimaryLogoSrc } from '@/components/pages/vaults/utils/vaultLog import { getCategoryDescription, getProductTypeDescription } from '@/components/pages/vaults/utils/vaultTagCopy' import { useWeb3 } from '@/components/shared/contexts/useWeb3' import { useDevFlags } from '@/contexts/useDevFlags' -import { useLocation, useNavigate, useParams } from '@/navigation/client' const resolveHeaderOffset = (): number => { if (typeof window === 'undefined') return 0 @@ -451,9 +451,9 @@ 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 { address } = useWeb3() @@ -476,7 +476,9 @@ function Index(): ReactElement | null { const isDualVariantVault = isYvUsd || isYvBtc 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) @@ -546,8 +548,8 @@ 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 diff --git a/src/components/pages/vaults/components/SuggestedVaultCard.test.tsx b/src/components/pages/vaults/components/SuggestedVaultCard.test.tsx index 3f0daf189..fd85b0774 100644 --- a/src/components/pages/vaults/components/SuggestedVaultCard.test.tsx +++ b/src/components/pages/vaults/components/SuggestedVaultCard.test.tsx @@ -3,7 +3,6 @@ import { useVaultApyData } from '@pages/vaults/hooks/useVaultApyData' import { YVUSD_LOCKED_ADDRESS, YVUSD_UNLOCKED_ADDRESS } from '@pages/vaults/utils/yvUsd' import { renderToStaticMarkup } from 'react-dom/server' import { describe, expect, it, vi } from 'vitest' -import { MemoryNavigationProvider } from '@/navigation/client' import { SuggestedVaultCard } from './SuggestedVaultCard' @@ -45,11 +44,7 @@ function renderCard(vault: TKongVaultInput): string { } try { - return renderToStaticMarkup( - - - - ) + return renderToStaticMarkup() } finally { ;(globalThis as unknown as { window: unknown }).window = originalWindow } diff --git a/src/components/pages/vaults/components/SuggestedVaultCard.tsx b/src/components/pages/vaults/components/SuggestedVaultCard.tsx index e1f16acba..6d1a9f6c5 100644 --- a/src/components/pages/vaults/components/SuggestedVaultCard.tsx +++ b/src/components/pages/vaults/components/SuggestedVaultCard.tsx @@ -1,4 +1,3 @@ -import Link from '@components/Link' import { VaultsListChip } from '@pages/vaults/components/list/VaultsListChip' import { VaultForwardAPY } from '@pages/vaults/components/table/VaultForwardAPY' import { VaultTVL } from '@pages/vaults/components/table/VaultTVL' @@ -23,6 +22,7 @@ import { isYvUsdVault, YVUSD_LOCKED_ADDRESS, YVUSD_LOCKED_COOLDOWN_DAYS } from ' import { TokenLogo } from '@shared/components/TokenLogo' import { formatApyDisplay, toAddress } from '@shared/utils' import { getNetwork } from '@shared/utils/wagmi' +import Link from 'next/link' import type { ReactElement } from 'react' import { env } from '@/env' @@ -74,7 +74,7 @@ export function SuggestedVaultCard({ return ( vi.fn(() => ({ @@ -303,9 +302,7 @@ describe('VaultDetailsHeaderPresentation', () => { it('uses the standard compressed token logo size for yvUSD', () => { const html = renderToStaticMarkup( - - - + ) expect(html).toContain('style="width:32px;height:32px"') @@ -315,9 +312,7 @@ describe('VaultDetailsHeaderPresentation', () => { it('keeps the explorer link visible when header is compressed', () => { const html = renderToStaticMarkup( - - - + ) expect(html).toContain('View vault on block explorer') @@ -326,9 +321,7 @@ describe('VaultDetailsHeaderPresentation', () => { it('hides cooldown information when there is no locked yvUSD deposit', () => { const html = renderToStaticMarkup( - - - + ) expect(html).toContain('Your Deposits') @@ -339,9 +332,7 @@ describe('VaultDetailsHeaderPresentation', () => { it('does not enable locked yvBTC user-data reads while the locked address is a placeholder', () => { renderToStaticMarkup( - - - + ) expect(useVaultUserDataMock).toHaveBeenCalledWith( diff --git a/src/components/pages/vaults/components/detail/VaultDetailsHeader.tsx b/src/components/pages/vaults/components/detail/VaultDetailsHeader.tsx index f7e974214..3bc4a8d45 100755 --- a/src/components/pages/vaults/components/detail/VaultDetailsHeader.tsx +++ b/src/components/pages/vaults/components/detail/VaultDetailsHeader.tsx @@ -70,10 +70,10 @@ import { } from '@shared/utils' import { getVaultName } from '@shared/utils/helpers' import { getNetwork } from '@shared/utils/wagmi/utils' +import Link from 'next/link' import type { ReactElement, Ref } from 'react' import { useEffect, useRef, useState } from 'react' import { env } from '@/env' -import { Link } from '@/navigation/client' type TVaultKindType = 'multi' | 'single' | undefined @@ -1099,11 +1099,11 @@ export function VaultDetailsHeaderPresentation({ className={'grid w-full grid-cols-1 gap-y-0 gap-x-6 text-left md:auto-rows-min md:grid-cols-20 bg-app rounded-lg'} >
- + {'Home'} {'>'} - + {'Vaults'} {'>'} diff --git a/src/components/pages/vaults/components/detail/VaultsListStrategy.tsx b/src/components/pages/vaults/components/detail/VaultsListStrategy.tsx index 717f8c161..636c6dbb4 100644 --- a/src/components/pages/vaults/components/detail/VaultsListStrategy.tsx +++ b/src/components/pages/vaults/components/detail/VaultsListStrategy.tsx @@ -9,10 +9,10 @@ import { cl, toAddress, truncateHex } from '@shared/utils' import { formatDuration } from '@shared/utils/format.time' import { copyToClipboard } from '@shared/utils/helpers' import { getNetwork } from '@shared/utils/wagmi/utils' +import Link from 'next/link' import type { ReactElement } from 'react' import { useState } from 'react' import { env } from '@/env' -import Link from '/src/components/Link' import { STRATEGY_PANEL_ROW_DESKTOP_LAYOUT } from './strategiesLayout' import { formatStrategiesApy, formatStrategiesPercent } from './strategiesPercentFormat' diff --git a/src/components/pages/vaults/components/filters/VaultVersionToggle.test.tsx b/src/components/pages/vaults/components/filters/VaultVersionToggle.test.tsx index 56e21f814..1ccc6eb7e 100644 --- a/src/components/pages/vaults/components/filters/VaultVersionToggle.test.tsx +++ b/src/components/pages/vaults/components/filters/VaultVersionToggle.test.tsx @@ -1,15 +1,25 @@ import { renderToStaticMarkup } from 'react-dom/server' -import { describe, expect, it } from 'vitest' -import { MemoryNavigationProvider } from '@/navigation/client' +import { describe, expect, it, vi } from 'vitest' import { VaultVersionToggle } from './VaultVersionToggle' +const { mockRouterReplace, mockSearchParams } = vi.hoisted(() => ({ + mockRouterReplace: vi.fn(), + mockSearchParams: vi.fn(() => new URLSearchParams()) +})) + +vi.mock('next/navigation', () => ({ + usePathname: () => '/vaults', + useRouter: () => ({ + replace: mockRouterReplace + }), + useSearchParams: () => mockSearchParams() +})) + function renderToggle(entry: string): string { - return renderToStaticMarkup( - - - - ) + const url = new URL(entry, 'https://yearn.fi') + mockSearchParams.mockReturnValue(new URLSearchParams(url.search)) + return renderToStaticMarkup() } describe('VaultVersionToggle', () => { diff --git a/src/components/pages/vaults/components/filters/VaultVersionToggle.tsx b/src/components/pages/vaults/components/filters/VaultVersionToggle.tsx index b92c46f01..0ff099e8e 100644 --- a/src/components/pages/vaults/components/filters/VaultVersionToggle.tsx +++ b/src/components/pages/vaults/components/filters/VaultVersionToggle.tsx @@ -10,8 +10,8 @@ import { import { Tooltip } from '@shared/components/Tooltip' import { cl } from '@shared/utils' import { PLAUSIBLE_EVENTS } from '@shared/utils/plausible' +import { usePathname, useRouter, useSearchParams } from 'next/navigation' import type { ReactElement } from 'react' -import { useSearchParams } from '@/navigation/client' type TVaultVersionToggleProps = { className?: string @@ -39,7 +39,9 @@ export function VaultVersionToggle({ onTypeChange, isPending }: TVaultVersionToggleProps): ReactElement { - const [searchParams, setSearchParams] = useSearchParams() + const searchParams = useSearchParams() + const pathname = usePathname() || '/vaults' + const router = useRouter() const trackEvent = usePlausible() const normalizedType = normalizeVaultTypeParam(searchParams.get('type')) const resolvedType = activeType ?? normalizedType @@ -50,14 +52,15 @@ export function VaultVersionToggle({ onTypeChange(config.type) return } - const nextParams = new URLSearchParams(searchParams) + const nextParams = new URLSearchParams(searchParams.toString()) if (config.typeParam === 'all') { nextParams.delete('type') } else { nextParams.set('type', config.typeParam) } sanitizeChainsParam(nextParams, getSupportedChainsForVaultType(config.type)) - setSearchParams(nextParams, { replace: true }) + const query = nextParams.toString() + router.replace(`${pathname}${query ? `?${query}` : ''}`, { scroll: false }) } function isActive(type: TVaultType): boolean { diff --git a/src/components/pages/vaults/components/list/VaultsListRow.test.tsx b/src/components/pages/vaults/components/list/VaultsListRow.test.tsx index 47db9ef5f..b204d381c 100644 --- a/src/components/pages/vaults/components/list/VaultsListRow.test.tsx +++ b/src/components/pages/vaults/components/list/VaultsListRow.test.tsx @@ -6,19 +6,27 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import type { ComponentProps } from 'react' import { renderToStaticMarkup } from 'react-dom/server' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { MemoryNavigationProvider } from '@/navigation/client' import { VaultsListRow } from './VaultsListRow' -const { mockUseMediaQuery, mockUseYvUsdVaults, mockUseVaultSnapshot, mockVaultForwardAPY } = vi.hoisted(() => ({ - mockUseMediaQuery: vi.fn(() => false), - mockUseYvUsdVaults: vi.fn((): any => ({ - metrics: undefined, - unlockedVault: undefined, - lockedVault: undefined - })), - mockUseVaultSnapshot: vi.fn((): any => ({ data: undefined })), - mockVaultForwardAPY: vi.fn((_props?: unknown) =>
{'APY'}
) +const { mockRouterPush, mockUseMediaQuery, mockUseYvUsdVaults, mockUseVaultSnapshot, mockVaultForwardAPY } = vi.hoisted( + () => ({ + mockRouterPush: vi.fn(), + mockUseMediaQuery: vi.fn(() => false), + mockUseYvUsdVaults: vi.fn((): any => ({ + metrics: undefined, + unlockedVault: undefined, + lockedVault: undefined + })), + mockUseVaultSnapshot: vi.fn((): any => ({ data: undefined })), + mockVaultForwardAPY: vi.fn((_props?: unknown) =>
{'APY'}
) + }) +) + +vi.mock('next/navigation', () => ({ + useRouter: () => ({ + push: mockRouterPush + }) })) vi.mock('@react-hookz/web', () => ({ @@ -73,9 +81,7 @@ function renderRowHtml(vault: TKongVaultInput, props?: Partial - - - + ) } diff --git a/src/components/pages/vaults/components/list/VaultsListRow.tsx b/src/components/pages/vaults/components/list/VaultsListRow.tsx index 0698f9850..83c27d233 100755 --- a/src/components/pages/vaults/components/list/VaultsListRow.tsx +++ b/src/components/pages/vaults/components/list/VaultsListRow.tsx @@ -1,4 +1,3 @@ -import Link from '@components/Link' import { usePlausible } from '@hooks/usePlausible' import { APYDetailsModal } from '@pages/vaults/components/table/APYDetailsModal' import { type TVaultForwardAPYVariant, VaultForwardAPY } from '@pages/vaults/components/table/VaultForwardAPY' @@ -65,10 +64,11 @@ import { PLAUSIBLE_EVENTS } from '@shared/utils/plausible' import { kongVaultSnapshotSchema } from '@shared/utils/schemas/kongVaultSnapshotSchema' import { getNetwork } from '@shared/utils/wagmi' import { useQueryClient } from '@tanstack/react-query' +import Link from 'next/link' +import { useRouter } from 'next/navigation' import type { MouseEvent, ReactElement } from 'react' import { lazy, memo, Suspense, useCallback, useEffect, useMemo, useState } from 'react' import { env } from '@/env' -import { useNavigate } from '@/navigation/client' import type { TVaultsExpandedView } from './VaultsExpandedSelector' import { VaultsListChip } from './VaultsListChip' @@ -272,7 +272,7 @@ function VaultsListRowComponent({ showAllocatorChip = true, expandedChartVariant = 'default' }: TVaultsListRowProps): ReactElement { - const navigate = useNavigate() + const router = useRouter() const trackEvent = usePlausible() const chainID = getVaultChainID(currentVault) const vaultAddress = getVaultAddress(currentVault) @@ -914,7 +914,7 @@ function VaultsListRowComponent({ currentVault={currentVault} expandedView={expandedView} onExpandedViewChange={setExpandedView} - onNavigateToVault={() => navigate(href)} + onNavigateToVault={() => router.push(href)} showKindTag={showKindChip} showHiddenTag={isHiddenVault} isHidden={isHiddenVault} diff --git a/src/components/pages/vaults/components/notifications/Notification.tsx b/src/components/pages/vaults/components/notifications/Notification.tsx index d9f0ba632..8b631ccc0 100644 --- a/src/components/pages/vaults/components/notifications/Notification.tsx +++ b/src/components/pages/vaults/components/notifications/Notification.tsx @@ -9,10 +9,10 @@ import { IconLoader } from '@shared/icons/IconLoader' import type { TNotification, TNotificationStatus } from '@shared/types/notifications' import { cl, SUPPORTED_NETWORKS, truncateHex } from '@shared/utils' import { getNetwork } from '@shared/utils/wagmi' +import Link from 'next/link' import type { ReactElement } from 'react' import { memo, useCallback, useMemo, useState } from 'react' import { env } from '@/env' -import Link from '/src/components/Link' const NETWORK_BY_CHAIN_ID = new Map(SUPPORTED_NETWORKS.map((network) => [network.id, network] as const)) as ReadonlyMap< number, diff --git a/src/components/pages/vaults/components/widget/WalletPanel.tsx b/src/components/pages/vaults/components/widget/WalletPanel.tsx index a3ac969eb..563b65bc4 100644 --- a/src/components/pages/vaults/components/widget/WalletPanel.tsx +++ b/src/components/pages/vaults/components/widget/WalletPanel.tsx @@ -35,8 +35,8 @@ import { truncateHex } from '@shared/utils' import { getNetwork } from '@shared/utils/wagmi/utils' +import { useRouter } from 'next/navigation' import { type FC, type ReactElement, useCallback, useMemo, useState } from 'react' -import { useNavigate } from '@/navigation/client' import { getAwaitingExecutionEntries } from './WalletPanel.helpers' import { formatDuration, parseCooldownStatus, resolveCooldownWindowState } from './yvUSD/cooldownUtils' @@ -236,7 +236,7 @@ export const WalletPanel: FC = ({ }) => { const { address, isActive: isWalletActive, openLoginModal } = useWeb3() const { cachedEntries } = useNotifications() - const navigate = useNavigate() + const router = useRouter() const { getPrice } = useYearn() const [activeTab, setActiveTab] = useState('balances') const { @@ -500,7 +500,7 @@ export const WalletPanel: FC = ({

Recent transactions