Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions api/enso/prices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
export const config = { runtime: 'edge' }

const ENSO_API_BASE = 'https://api.enso.finance'
const KONG_REST_BASE = 'https://kong.yearn.fi/api/rest'
const CONCURRENCY = 5
const DELAY_MS = 200
const ENSO_CHAINS = [1, 8453, 747474, 10, 137, 42161, 100, 146, 80094]

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))

async function fetchEnsoPrice(
chainId: number,
address: string,
apiKey: string,
retries = 2
): Promise<{ address: string; price: number } | null> {
for (let attempt = 0; attempt <= retries; attempt++) {
try {
const res = await fetch(`${ENSO_API_BASE}/api/v1/prices/${chainId}/${address}`, {
headers: { Authorization: `Bearer ${apiKey}` }
})
if (res.status === 429) {
await sleep(1000 * (attempt + 1))
continue
}
if (!res.ok) return null
const data = await res.json()
return { address, price: data.price }
} catch {
if (attempt < retries) {
await sleep(500 * (attempt + 1))
continue
}
return null
}
}
return null
}

export default async function handler(req: Request): Promise<Response> {
if (req.method !== 'GET') {
return new Response(JSON.stringify({ error: 'Method not allowed' }), {
status: 405,
headers: { 'Content-Type': 'application/json' }
})
}

const apiKey = process.env.ENSO_API_KEY
if (!apiKey) {
return new Response(JSON.stringify({ error: 'Enso API not configured' }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
})
}

try {
const vaults: { chainId: number; address: string; asset?: { address?: string } }[] = await fetch(
`${KONG_REST_BASE}/list/vaults`
).then((r) => r.json())

const addressesByChain = new Map<number, Set<string>>()
for (const v of vaults) {
if (!ENSO_CHAINS.includes(v.chainId)) continue
if (!addressesByChain.has(v.chainId)) addressesByChain.set(v.chainId, new Set())
const set = addressesByChain.get(v.chainId)!
if (v.address) set.add(v.address.toLowerCase())
if (v.asset?.address) set.add(v.asset.address.toLowerCase())
}

const result: Record<string, Record<string, string>> = {}

for (const [chainId, addresses] of addressesByChain) {
const chainKey = String(chainId)
result[chainKey] = {}
const addressList = [...addresses]

for (let i = 0; i < addressList.length; i += CONCURRENCY) {
const batch = addressList.slice(i, i + CONCURRENCY)
const results = await Promise.all(batch.map((a) => fetchEnsoPrice(chainId, a, apiKey)))
for (const r of results) {
if (r && r.price != null) {
result[chainKey][r.address] = Math.round(r.price * 1e6).toString()
}
}
await sleep(DELAY_MS)
}
}

return new Response(JSON.stringify(result), {
status: 200,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, s-maxage=120, stale-while-revalidate=600'
}
})
} catch (error) {
console.error('Error fetching Enso prices:', error)
return new Response(JSON.stringify({ error: 'Internal server error' }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
})
}
}
91 changes: 91 additions & 0 deletions api/server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { serve } from 'bun'

const ENSO_API_BASE = 'https://api.enso.finance'
const KONG_REST_BASE = 'https://kong.yearn.fi/api/rest'
const ENSO_PRICE_CONCURRENCY = 5
const ENSO_PRICE_DELAY_MS = 200
const YVUSD_APR_SERVICE_API = (
process.env.YVUSD_APR_SERVICE_API || 'https://yearn-yvusd-apr-service.vercel.app/api/aprs'
).replace(/\/$/, '')
Expand Down Expand Up @@ -158,6 +161,90 @@ async function handleEnsoBalances(req: Request): Promise<Response> {
}
}

const ENSO_SUPPORTED_CHAINS = [1, 8453, 747474, 10, 137, 42161, 100, 146, 80094]

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))

async function fetchEnsoPrice(
chainId: number,
address: string,
apiKey: string,
retries = 2
): Promise<{ address: string; price: number } | null> {
for (let attempt = 0; attempt <= retries; attempt++) {
try {
const res = await fetch(`${ENSO_API_BASE}/api/v1/prices/${chainId}/${address}`, {
headers: { Authorization: `Bearer ${apiKey}` }
})
if (res.status === 429) {
await sleep(1000 * (attempt + 1))
continue
}
if (!res.ok) return null
const data = (await res.json()) as { price: number }
return { address, price: data.price }
} catch {
if (attempt < retries) {
await sleep(500 * (attempt + 1))
continue
}
return null
}
}
return null
}

async function handleEnsoPrices(): Promise<Response> {
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 })
}

try {
const vaults: { chainId: number; address: string; asset?: { address?: string } }[] = await fetch(
`${KONG_REST_BASE}/list/vaults`
).then((r) => r.json())

const addressesByChain = new Map<number, Set<string>>()
for (const v of vaults) {
if (!ENSO_SUPPORTED_CHAINS.includes(v.chainId)) continue
if (!addressesByChain.has(v.chainId)) addressesByChain.set(v.chainId, new Set())
const set = addressesByChain.get(v.chainId)!
if (v.address) set.add(v.address.toLowerCase())
if (v.asset?.address) set.add(v.asset.address.toLowerCase())
}

const result: Record<string, Record<string, string>> = {}

for (const [chainId, addresses] of addressesByChain) {
const chainKey = String(chainId)
result[chainKey] = {}
const addressList = [...addresses]

for (let i = 0; i < addressList.length; i += ENSO_PRICE_CONCURRENCY) {
const batch = addressList.slice(i, i + ENSO_PRICE_CONCURRENCY)
const results = await Promise.all(batch.map((a) => fetchEnsoPrice(chainId, a, apiKey)))
for (const r of results) {
if (r && r.price != null) {
result[chainKey][r.address] = Math.round(r.price * 1e6).toString()
}
}
await sleep(ENSO_PRICE_DELAY_MS)
}
}

return Response.json(result, {
headers: {
'Cache-Control': 'public, s-maxage=120, stale-while-revalidate=600'
}
})
} catch (error) {
console.error('Error fetching Enso prices:', error)
return Response.json({ error: 'Internal server error' }, { status: 500 })
}
}

serve({
async fetch(req) {
const url = new URL(req.url)
Expand All @@ -166,6 +253,10 @@ serve({
return handleEnsoStatus()
}

if (url.pathname === '/api/enso/prices') {
return handleEnsoPrices()
}

if (url.pathname === '/api/enso/balances') {
return handleEnsoBalances(req)
}
Expand Down
27 changes: 27 additions & 0 deletions src/components/shared/hooks/useFetchEnsoPrices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useDeepCompareMemo } from '@react-hookz/web'
import type { TYDaemonPricesChain } from '../utils/schemas/yDaemonPricesSchema'
import { yDaemonPricesChainSchema } from '../utils/schemas/yDaemonPricesSchema'
import { useFetch } from './useFetch'

/******************************************************************************
** The useFetchEnsoPrices hook fetches token prices from the Enso API via our
** server-side proxy. Returns the same TYDaemonPricesChain shape so it's a
** drop-in replacement for useFetchYDaemonPrices.
*****************************************************************************/
function useFetchEnsoPrices(): TYDaemonPricesChain {
const { data: prices } = useFetch<TYDaemonPricesChain>({
endpoint: '/api/enso/prices',
schema: yDaemonPricesChainSchema
})

const pricesUpdated = useDeepCompareMemo((): TYDaemonPricesChain => {
if (!prices) {
return {}
}
return prices
}, [prices])

return pricesUpdated
}

export { useFetchEnsoPrices }
29 changes: 29 additions & 0 deletions src/components/shared/hooks/useFetchYDaemonPrices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useDeepCompareMemo } from '@react-hookz/web'
import type { TYDaemonPricesChain } from '../utils/schemas/yDaemonPricesSchema'
import { yDaemonPricesChainSchema } from '../utils/schemas/yDaemonPricesSchema'
import { useFetch } from './useFetch'
import { useYDaemonBaseURI } from './useYDaemonBaseURI'

/******************************************************************************
** The useFetchYDaemonPrices hook is used to fetch the prices of the tokens
** from the yDaemon API. It returns an object with the prices of the tokens,
** splitted by chain.
*****************************************************************************/
function useFetchYDaemonPrices(): TYDaemonPricesChain {
const { yDaemonBaseUri: yDaemonBaseUriWithoutChain } = useYDaemonBaseURI()
const { data: prices } = useFetch<TYDaemonPricesChain>({
endpoint: `${yDaemonBaseUriWithoutChain}/prices/all`,
schema: yDaemonPricesChainSchema
})

const pricesUpdated = useDeepCompareMemo((): TYDaemonPricesChain => {
if (!prices) {
return {}
}
return prices
}, [prices])

return pricesUpdated
}

export { useFetchYDaemonPrices }
29 changes: 5 additions & 24 deletions src/components/shared/hooks/useFetchYearnPrices.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,10 @@
import { useDeepCompareMemo } from '@react-hookz/web'
import type { TYDaemonPricesChain } from '../utils/schemas/yDaemonPricesSchema'
import { yDaemonPricesChainSchema } from '../utils/schemas/yDaemonPricesSchema'
import { useFetch } from './useFetch'
import { useYDaemonBaseURI } from './useYDaemonBaseURI'
import { useFetchEnsoPrices } from './useFetchEnsoPrices'

/******************************************************************************
** The useFetchYearnPrices hook is used to fetch the prices of the tokens from
** the yDaemon API. It returns an object with the prices of the tokens,
** splitted by chain.
** The useFetchYearnPrices hook is used to fetch the prices of the tokens.
** It delegates to the Enso price provider by default. To switch back to
** yDaemon, swap the import to useFetchYDaemonPrices.
*****************************************************************************/
function useFetchYearnPrices(): TYDaemonPricesChain {
const { yDaemonBaseUri: yDaemonBaseUriWithoutChain } = useYDaemonBaseURI()
const { data: prices } = useFetch<TYDaemonPricesChain>({
endpoint: `${yDaemonBaseUriWithoutChain}/prices/all`,
schema: yDaemonPricesChainSchema
})

const pricesUpdated = useDeepCompareMemo((): TYDaemonPricesChain => {
if (!prices) {
return {}
}
return prices
}, [prices])

return pricesUpdated
}
const useFetchYearnPrices = useFetchEnsoPrices

export { useFetchYearnPrices }
Loading