diff --git a/.env.example b/.env.example index b8cad9d04..3fe2da903 100644 --- a/.env.example +++ b/.env.example @@ -72,3 +72,7 @@ UPSTASH_REDIS_REST_TOKEN_PORTFOLIO= UPSTASH_REDIS_REST_URL= UPSTASH_REDIS_REST_USERNAME= UPSTASH_REDIS_REST_TOKEN= + +# Lifetime earnings headline (landing page odometer). Falls back to the unsuffixed UPSTASH_REDIS_REST_* vars. +UPSTASH_REDIS_REST_URL_EARNINGS= +UPSTASH_REDIS_REST_TOKEN_EARNINGS= diff --git a/app/page-client.tsx b/app/page-client.tsx index e948b0b6c..0bebe41bf 100644 --- a/app/page-client.tsx +++ b/app/page-client.tsx @@ -1,8 +1,13 @@ 'use client' import HomePage from '@pages/landing' +import type { TLifetimeEarningsHeadline } from '@shared/utils/schemas/lifetimeEarningsSchema' import type { ReactElement } from 'react' -export default function HomePageClient(): ReactElement { - return +export default function HomePageClient({ + earningsHeadline +}: { + earningsHeadline: TLifetimeEarningsHeadline | null +}): ReactElement { + return } diff --git a/app/page.tsx b/app/page.tsx index e5bdd6303..a4089c778 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,8 +1,7 @@ import { JsonLd } from '@shared/components/JsonLd' -import { HydrationBoundary } from '@tanstack/react-query' import type { ReactElement } from 'react' import PublicApp from '@/PublicApp' -import { getLandingPageDehydratedState } from '@/server/ssr/publicDataHydration' +import { getLifetimeEarningsHeadlineOrNull } from '@/server/earnings/headline' import { landingMetadata, yearnOrganizationJsonLd } from './metadata' import HomePageClient from './page-client' @@ -51,7 +50,7 @@ const faqJsonLd = { }, { '@type': 'Question', - name: 'Are there Developer Docs?', + name: 'Are there developer docs?', acceptedAnswer: { '@type': 'Answer', text: 'Yes. Yearn has documentation for developers building on top of the protocol at https://docs.yearn.fi.' @@ -61,7 +60,7 @@ const faqJsonLd = { } export default async function Page(): Promise { - const dehydratedState = await getLandingPageDehydratedState() + const earningsHeadline = await getLifetimeEarningsHeadlineOrNull() return ( <> @@ -69,9 +68,7 @@ export default async function Page(): Promise { - - - + ) diff --git a/bun.lock b/bun.lock index 0eac13c92..8f4e91ffa 100644 --- a/bun.lock +++ b/bun.lock @@ -1,10 +1,12 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "yearnfi", "dependencies": { "@headlessui/react": "2.2.10", + "@number-flow/react": "0.6.0", "@plausible-analytics/tracker": "0.4.5", "@rainbow-me/rainbowkit": "2.2.11", "@react-hookz/web": "24.0.4", @@ -324,6 +326,8 @@ "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + "@number-flow/react": ["@number-flow/react@0.6.0", "", { "dependencies": { "esm-env": "^1.1.4", "number-flow": "0.6.0" }, "peerDependencies": { "react": "^18 || ^19", "react-dom": "^18 || ^19" } }, "sha512-77Yfc9+zkV2UDSP8phhZzxJGuwxi/Tt1TikmipL+1r3e9GFKEYDZ1XwInj67NoSt3OnOB0KLvvcl3lfPZgBHVQ=="], + "@oxc-project/types": ["@oxc-project/types@0.137.0", "", {}, "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA=="], "@paulmillr/qr": ["@paulmillr/qr@0.2.1", "", {}, "sha512-IHnV6A+zxU7XwmKFinmYjUcwlyK9+xkG3/s9KcQhI9BjQKycrJ1JRO+FbNYPwZiPKW3je/DR0k7w8/gLa5eaxQ=="], @@ -842,6 +846,8 @@ "es-toolkit": ["es-toolkit@1.33.0", "", {}, "sha512-X13Q/ZSc+vsO1q600bvNK4bxgXMkHcf//RxCmYDaRY5DAcT+eoXjY5hoAPGMdRnWQjvyLEcyauG3b6hz76LNqg=="], + "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], + "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], @@ -1158,6 +1164,8 @@ "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + "number-flow": ["number-flow@0.6.0", "", { "dependencies": { "esm-env": "^1.1.4" } }, "sha512-K8flNq2Wqus53vjp/btVo3qXFkagF8dIdYavreBfE7hlvFFG/b1HMGEH6nZL+mlrJ+4lbLP9OmPv3t2rmRkpSQ=="], + "obj-multiplex": ["obj-multiplex@1.0.0", "", { "dependencies": { "end-of-stream": "^1.4.0", "once": "^1.4.0", "readable-stream": "^2.3.3" } }, "sha512-0GNJAOsHoBHeNTvl5Vt6IWnpUEcc3uSRxzBri7EDyIcMgYvnY2JL2qdeV5zTMjWQX5OHcD5amcW2HFfDh0gjIA=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], diff --git a/package.json b/package.json index 32f615da1..582d294d2 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "@headlessui/react": "2.2.10", + "@number-flow/react": "0.6.0", "@plausible-analytics/tracker": "0.4.5", "@rainbow-me/rainbowkit": "2.2.11", "@react-hookz/web": "24.0.4", diff --git a/src/components/pages/landing/components/sections/FAQs.tsx b/src/components/pages/landing/components/sections/FAQs.tsx index ba749eb35..75acce3a0 100644 --- a/src/components/pages/landing/components/sections/FAQs.tsx +++ b/src/components/pages/landing/components/sections/FAQs.tsx @@ -68,7 +68,7 @@ const faqData = [ ) }, { - title: 'Are there Developer Docs?', + title: 'Are there developer docs?', content: (

{'Yes! Yearn has extensive documentation for developers looking to build on top of our protocol. Visit our'}{' '} diff --git a/src/components/pages/landing/components/sections/Hero.tsx b/src/components/pages/landing/components/sections/Hero.tsx index 02d1b171d..ea8ca94d5 100644 --- a/src/components/pages/landing/components/sections/Hero.tsx +++ b/src/components/pages/landing/components/sections/Hero.tsx @@ -1,15 +1,13 @@ import { usePlausible } from '@hooks/usePlausible' import { Button } from '@shared/components/Button' +import { LifetimeEarningsStat } from '@shared/components/LifetimeEarningsStat' import { SectionHeader } from '@shared/components/SectionHeader' -import { TvlStat } from '@shared/components/TvlStat' -import { YEARN_TVL_ENDPOINT } from '@shared/data/publicQueryEndpoints' -import { useFetch } from '@shared/hooks/useFetch' import { PLAUSIBLE_EVENTS } from '@shared/utils/plausible' +import type { TLifetimeEarningsHeadline } from '@shared/utils/schemas/lifetimeEarningsSchema' import Link from 'next/link' import { useRouter } from 'next/navigation' import type { ReactElement } from 'react' import { useEffect } from 'react' -import * as z from 'zod' import Image from '/src/components/Image' function AnimatedLogos(): ReactElement { @@ -75,13 +73,9 @@ function AnimatedLogos(): ReactElement { ) } -export function Hero(): ReactElement { +export function Hero({ earningsHeadline }: { earningsHeadline: TLifetimeEarningsHeadline | null }): ReactElement { const trackEvent = usePlausible() const router = useRouter() - const { data: tvl } = useFetch({ - endpoint: YEARN_TVL_ENDPOINT, - schema: z.number() - }) useEffect(() => { const prefetchTimer = window.setTimeout(() => { @@ -113,13 +107,13 @@ export function Hero(): ReactElement {

- + {earningsHeadline ? : null}
{'Yearn -
- -
+
{earningsHeadline ? : null}
(
{integrations.map((integration, index) => ( diff --git a/src/components/pages/landing/components/sections/Security.tsx b/src/components/pages/landing/components/sections/Security.tsx index 66e63e2dc..b89079b96 100644 --- a/src/components/pages/landing/components/sections/Security.tsx +++ b/src/components/pages/landing/components/sections/Security.tsx @@ -18,13 +18,13 @@ const Cards: { } = { [SecurityCardType.Audits]: { title: 'Audits', - description: 'Yearn Contracts are audited thoroughly by a variety of auditors.', + description: 'Yearn contracts are audited thoroughly by a variety of auditors.', href: 'https://docs.yearn.fi/developers/security/', imageSrc: '/landing/yearn-apps-logo.png', bgColor: 'bg-gradient-to-br from-[#0657F9] to-[#0B4DD0]' }, [SecurityCardType.BugBounties]: { - title: 'Bug Bounties', + title: 'Bug bounties', description: 'Security is our top priority. Report vulnerabilities and get rewarded.', href: 'https://immunefi.com/bug-bounty/yearnfinance', imageSrc: '/landing/integrations.png', @@ -50,7 +50,7 @@ const SecurityCard: FC<{ 'flex min-h-[48px] items-center text-base text-white/90 transition-colors duration-300 group-hover:text-white sm:min-h-0 sm:text-inherit' } > - {'Learn More →'} + {'Learn more →'}
@@ -66,7 +66,7 @@ export const Security: FC = () => (
diff --git a/src/components/pages/landing/components/sections/Vaults.tsx b/src/components/pages/landing/components/sections/Vaults.tsx index b3a75eb77..09cd7f10d 100644 --- a/src/components/pages/landing/components/sections/Vaults.tsx +++ b/src/components/pages/landing/components/sections/Vaults.tsx @@ -35,17 +35,17 @@ const slides: TVault[] = [ image: '/landing/safe.png', size: 250, tagline: 'Growing every day', - title: 'Compounding Vaults', + title: 'Compounding vaults', description: 'Vaults utilize DeFi opportunities to give you the best risk-adjusted yields', - cta: { label: 'View All', href: 'https://yearn.fi/v3' } + cta: { label: 'View all', href: 'https://yearn.fi/v3' } }, { background: '/landing/vault-background-y.png', image: '/landing/apps.png', size: 300, tagline: 'Growing every day', - title: 'App Ecosystem', - description: 'Apps built on Yearn vaults by contributors and the wider community' + title: 'App ecosystem', + description: 'Apps built on Yearn Vaults by contributors and the wider community' } ] @@ -85,8 +85,8 @@ const appRows: TRow[] = [ }, { icon: '/landing/apps/factory.png', - text: 'LP Token Vaults', - description: 'Permissionless Curve Autocompounders', + text: 'LP token vaults', + description: 'Permissionless Curve autocompounders', href: 'https://yearn.fi/vaults?type=lp' }, { diff --git a/src/components/pages/landing/index.tsx b/src/components/pages/landing/index.tsx index 5f4d86aa2..b447b61c7 100644 --- a/src/components/pages/landing/index.tsx +++ b/src/components/pages/landing/index.tsx @@ -1,7 +1,8 @@ +import type { TLifetimeEarningsHeadline } from '@shared/utils/schemas/lifetimeEarningsSchema' import type { ReactElement } from 'react' import { FAQs, Footer, Hero, Integrations, Partners, Security, Vaults } from './components/sections' -function Index(): ReactElement { +function Index({ earningsHeadline }: { earningsHeadline: TLifetimeEarningsHeadline | null }): ReactElement { return (
- + diff --git a/src/components/shared/components/LifetimeEarningsStat.tsx b/src/components/shared/components/LifetimeEarningsStat.tsx new file mode 100644 index 000000000..e81be25d2 --- /dev/null +++ b/src/components/shared/components/LifetimeEarningsStat.tsx @@ -0,0 +1,48 @@ +'use client' + +import NumberFlow from '@number-flow/react' +import type { TLifetimeEarningsHeadline } from '@shared/utils/schemas/lifetimeEarningsSchema' +import type { ReactElement } from 'react' +import { useEffect, useState } from 'react' + +// Tick at the whole-dollar cadence (1000 / rate ms) so each update rolls the +// last digit by ~$1, clamped for sane rates. The roll animation runs linear +// with duration matched to the tick so each roll finishes exactly as the next +// value lands — constant velocity, no rubber-band. +const TICK_MIN_MS = 50 +const TICK_MAX_MS = 1_000 + +export function LifetimeEarningsStat({ headline }: { headline: TLifetimeEarningsHeadline }): ReactElement { + const [value, setValue] = useState(headline.value) + + const tickMs = Math.min(TICK_MAX_MS, Math.max(TICK_MIN_MS, 1_000 / headline.rate_usd_per_sec)) + + // Timer-driven extrapolation between server reads: the figure must advance + // on wall-clock time, which has no declarative alternative. + useEffect(() => { + const extrapolate = (): number => + headline.value + (headline.rate_usd_per_sec * (Date.now() - headline.computed_at_ms)) / 1000 + + setValue(extrapolate()) + const timer = window.setInterval(() => setValue(extrapolate()), tickMs) + return () => window.clearInterval(timer) + }, [headline, tickMs]) + + return ( +
+

+ {'$'} + +

+

{'Lifetime earned by Yearn Vaults'}

+
+ ) +} diff --git a/src/components/shared/components/TvlStat.tsx b/src/components/shared/components/TvlStat.tsx deleted file mode 100644 index ae4ce982a..000000000 --- a/src/components/shared/components/TvlStat.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { formatAmount } from '@shared/utils' - -import type { ReactElement } from 'react' - -export function TvlStat({ tvl }: { tvl: number }): ReactElement { - return ( -
-
-
-
-
-

- {'$'} - {formatAmount(tvl ?? 0, 0, 0)} - - {' in Yearn'} -

-
- ) -} diff --git a/src/components/shared/data/publicQueryEndpoints.ts b/src/components/shared/data/publicQueryEndpoints.ts index 3b4be864f..5f8224a12 100644 --- a/src/components/shared/data/publicQueryEndpoints.ts +++ b/src/components/shared/data/publicQueryEndpoints.ts @@ -2,7 +2,6 @@ import { KONG_REST_BASE } from '@pages/vaults/utils/kongRest' import { isAddress } from 'viem' import { toAddress } from '../utils' -export const YEARN_TVL_ENDPOINT = 'https://api.llama.fi/tvl/yearn' export const YEARN_VAULT_LIST_ENDPOINT = `${KONG_REST_BASE}/list/vaults` export function buildVaultSnapshotEndpoint(chainId?: number | string, address?: string): string | null { diff --git a/src/components/shared/utils/schemas/lifetimeEarningsSchema.ts b/src/components/shared/utils/schemas/lifetimeEarningsSchema.ts new file mode 100644 index 000000000..d2c0fe338 --- /dev/null +++ b/src/components/shared/utils/schemas/lifetimeEarningsSchema.ts @@ -0,0 +1,21 @@ +import * as z from 'zod' + +/** + * Lifetime-earnings headline served by /api/earnings/headline. + * + * A compute bot writes the raw snapshot fields to Redis; the server + * extrapolates them to `value` at `computed_at_ms` and the client keeps the + * number moving from there using `rate_usd_per_sec`. + */ +export const lifetimeEarningsHeadlineSchema = z.object({ + net_yield_usd: z.number(), + as_of_ms: z.number(), + rate_usd_per_sec: z.number().nonnegative(), + prev_net_yield_usd: z.number(), + prev_as_of_ms: z.number(), + run_id: z.string(), + value: z.number(), + computed_at_ms: z.number() +}) + +export type TLifetimeEarningsHeadline = z.infer diff --git a/src/server/earnings/headline.ts b/src/server/earnings/headline.ts new file mode 100644 index 000000000..5091429ee --- /dev/null +++ b/src/server/earnings/headline.ts @@ -0,0 +1,56 @@ +import { Redis } from '@upstash/redis' +import { + lifetimeEarningsHeadlineSchema, + type TLifetimeEarningsHeadline +} from '@/components/shared/utils/schemas/lifetimeEarningsSchema' + +export const LIFETIME_EARNINGS_REDIS_KEY = 'lifetime_yield:headline' + +const rawHeadlineSchema = lifetimeEarningsHeadlineSchema.omit({ value: true, computed_at_ms: true }) + +const redisState: { client: Redis | null } = { client: null } + +function getRedisClient(): Redis { + if (!redisState.client) { + const url = process.env.UPSTASH_REDIS_REST_URL_EARNINGS || process.env.UPSTASH_REDIS_REST_URL + const token = process.env.UPSTASH_REDIS_REST_TOKEN_EARNINGS || process.env.UPSTASH_REDIS_REST_TOKEN + if (!url || !token) { + throw new Error( + 'UPSTASH_REDIS_REST_URL_EARNINGS and UPSTASH_REDIS_REST_TOKEN_EARNINGS (or the unsuffixed fallbacks) must both be set' + ) + } + + redisState.client = new Redis({ url, token }) + } + + return redisState.client +} + +/** + * Read the raw snapshot from Redis and extrapolate it to "now" on the server. + * Runs at ISR revalidation time; the client keeps extrapolating from + * `computed_at_ms` using `rate_usd_per_sec`. The 30d-averaged rate makes the + * figure tolerant of long stale periods by design, so no live re-sync channel + * is needed. + */ +export async function getLifetimeEarningsHeadline(): Promise { + const blob = await getRedisClient().get(LIFETIME_EARNINGS_REDIS_KEY) + if (blob === null || blob === undefined) { + throw new Error(`Redis key ${LIFETIME_EARNINGS_REDIS_KEY} is missing`) + } + + const raw = rawHeadlineSchema.parse(typeof blob === 'string' ? JSON.parse(blob) : blob) + const computedAtMs = Date.now() + const value = raw.net_yield_usd + (raw.rate_usd_per_sec * (computedAtMs - raw.as_of_ms)) / 1000 + return { ...raw, value, computed_at_ms: computedAtMs } +} + +/** Null on any failure — the badge simply does not render without data. */ +export async function getLifetimeEarningsHeadlineOrNull(): Promise { + try { + return await getLifetimeEarningsHeadline() + } catch (error) { + console.warn('[SSR] Failed to read lifetime earnings headline', error) + return null + } +} diff --git a/src/server/ssr/publicDataHydration.test.ts b/src/server/ssr/publicDataHydration.test.ts index d0399086f..2cf200a12 100644 --- a/src/server/ssr/publicDataHydration.test.ts +++ b/src/server/ssr/publicDataHydration.test.ts @@ -1,16 +1,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { YVBTC_CHAIN_ID, YVBTC_UNLOCKED_ADDRESS } from '@/components/pages/vaults/utils/yvBtc' import { YVUSD_CHAIN_ID, YVUSD_LOCKED_ADDRESS, YVUSD_UNLOCKED_ADDRESS } from '@/components/pages/vaults/utils/yvUsd' -import { - buildVaultSnapshotEndpoint, - YEARN_TVL_ENDPOINT, - YEARN_VAULT_LIST_ENDPOINT -} from '@/components/shared/data/publicQueryEndpoints' -import { - getLandingPageDehydratedState, - getVaultDetailPageDehydratedState, - getVaultsPageDehydratedState -} from './publicDataHydration' +import { buildVaultSnapshotEndpoint, YEARN_VAULT_LIST_ENDPOINT } from '@/components/shared/data/publicQueryEndpoints' +import { getVaultDetailPageDehydratedState, getVaultsPageDehydratedState } from './publicDataHydration' const DETAIL_ADDRESS = '0x0000000000000000000000000000000000000001' @@ -41,9 +33,6 @@ describe('public data SSR hydration', () => { 'fetch', vi.fn(async (url: string | URL | Request) => { const requestUrl = url.toString() - if (requestUrl === YEARN_TVL_ENDPOINT) { - return jsonResponse(123456) - } if (requestUrl === YEARN_VAULT_LIST_ENDPOINT) { return jsonResponse([]) } @@ -59,13 +48,6 @@ describe('public data SSR hydration', () => { vi.unstubAllGlobals() }) - it('hydrates the landing TVL query', async () => { - const state = await getLandingPageDehydratedState() - - expect(queryKeys(state)).toContainEqual(['fetch', YEARN_TVL_ENDPOINT]) - expect(state.queries[0]?.state.data).toBe(123456) - }) - it('hydrates only the main vault list query used by the vaults page', async () => { const state = await getVaultsPageDehydratedState() const keys = queryKeys(state) diff --git a/src/server/ssr/publicDataHydration.ts b/src/server/ssr/publicDataHydration.ts index 4ca1712f9..00d4b24c4 100644 --- a/src/server/ssr/publicDataHydration.ts +++ b/src/server/ssr/publicDataHydration.ts @@ -1,5 +1,5 @@ import { type DehydratedState, dehydrate, QueryClient } from '@tanstack/react-query' -import * as z from 'zod' +import type * as z from 'zod' import { buildVaultsInitialPayload, type TVaultsInitialPayload @@ -11,19 +11,13 @@ import { YVUSD_UNLOCKED_ADDRESS } from '@/components/pages/vaults/utils/yvUsd' import { PUBLIC_VAULT_DATA_CACHE_TIME } from '@/components/shared/data/publicQueryCache' -import { - buildVaultSnapshotEndpoint, - YEARN_TVL_ENDPOINT, - YEARN_VAULT_LIST_ENDPOINT -} from '@/components/shared/data/publicQueryEndpoints' +import { buildVaultSnapshotEndpoint, YEARN_VAULT_LIST_ENDPOINT } from '@/components/shared/data/publicQueryEndpoints' import { fetchWithSchema, getFetchQueryKey } from '@/components/shared/utils/fetchQuery' import { kongVaultListSchema } from '@/components/shared/utils/schemas/kongVaultListSchema' import { kongVaultSnapshotSchema } from '@/components/shared/utils/schemas/kongVaultSnapshotSchema' -const LANDING_TVL_STALE_TIME = 2 * 60 * 1000 const VAULT_LIST_STALE_TIME = PUBLIC_VAULT_DATA_CACHE_TIME const VAULT_SNAPSHOT_STALE_TIME = PUBLIC_VAULT_DATA_CACHE_TIME -const LANDING_TVL_TIMEOUT_MS = 1500 const VAULT_LIST_TIMEOUT_MS = 3500 const VAULT_SNAPSHOT_TIMEOUT_MS = 7000 @@ -124,20 +118,6 @@ async function prefetchRelatedVaultDetailSnapshots( ]) } -export async function getLandingPageDehydratedState(): Promise { - const queryClient = createSsrQueryClient() - - await prefetchPublicQuery({ - queryClient, - endpoint: YEARN_TVL_ENDPOINT, - schema: z.number(), - staleTime: LANDING_TVL_STALE_TIME, - timeoutMs: LANDING_TVL_TIMEOUT_MS - }) - - return dehydrateQueryClient(queryClient) -} - export async function getVaultsPageDehydratedState(): Promise { const queryClient = createSsrQueryClient()