Skip to content

Commit 8d548c5

Browse files
murderteethclaude
andcommitted
Switch price source from ydaemon to Enso API
Add server-side proxy at /api/enso/prices (Vercel Edge Function) that fetches the Kong vault list, extracts all vault + asset addresses per chain, and queries Enso's per-address price API with rate limiting (concurrency 5, 200ms batch delay, retry on 429). Returns the same { [chainId]: { [address]: priceString } } shape as ydaemon so no consumer code changes are needed. Edge caching: s-maxage=120, stale-while-revalidate=600. Client-side changes: - useFetchYDaemonPrices: preserved original ydaemon implementation - useFetchEnsoPrices: new hook that fetches from /api/enso/prices - useFetchYearnPrices: now delegates to useFetchEnsoPrices by default, swap import to useFetchYDaemonPrices to revert Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f7b4d54 commit 8d548c5

File tree

5 files changed

+255
-24
lines changed

5 files changed

+255
-24
lines changed

api/enso/prices.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
export const config = { runtime: 'edge' }
2+
3+
const ENSO_API_BASE = 'https://api.enso.finance'
4+
const KONG_REST_BASE = 'https://kong.yearn.fi/api/rest'
5+
const CONCURRENCY = 5
6+
const DELAY_MS = 200
7+
const ENSO_CHAINS = [1, 8453, 747474, 10, 137, 42161, 100, 146, 80094]
8+
9+
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
10+
11+
async function fetchEnsoPrice(
12+
chainId: number,
13+
address: string,
14+
apiKey: string,
15+
retries = 2
16+
): Promise<{ address: string; price: number } | null> {
17+
for (let attempt = 0; attempt <= retries; attempt++) {
18+
try {
19+
const res = await fetch(`${ENSO_API_BASE}/api/v1/prices/${chainId}/${address}`, {
20+
headers: { Authorization: `Bearer ${apiKey}` }
21+
})
22+
if (res.status === 429) {
23+
await sleep(1000 * (attempt + 1))
24+
continue
25+
}
26+
if (!res.ok) return null
27+
const data = await res.json()
28+
return { address, price: data.price }
29+
} catch {
30+
if (attempt < retries) {
31+
await sleep(500 * (attempt + 1))
32+
continue
33+
}
34+
return null
35+
}
36+
}
37+
return null
38+
}
39+
40+
export default async function handler(req: Request): Promise<Response> {
41+
if (req.method !== 'GET') {
42+
return new Response(JSON.stringify({ error: 'Method not allowed' }), {
43+
status: 405,
44+
headers: { 'Content-Type': 'application/json' }
45+
})
46+
}
47+
48+
const apiKey = process.env.ENSO_API_KEY
49+
if (!apiKey) {
50+
return new Response(JSON.stringify({ error: 'Enso API not configured' }), {
51+
status: 500,
52+
headers: { 'Content-Type': 'application/json' }
53+
})
54+
}
55+
56+
try {
57+
const vaults: { chainId: number; address: string; asset?: { address?: string } }[] = await fetch(
58+
`${KONG_REST_BASE}/list/vaults`
59+
).then((r) => r.json())
60+
61+
const addressesByChain = new Map<number, Set<string>>()
62+
for (const v of vaults) {
63+
if (!ENSO_CHAINS.includes(v.chainId)) continue
64+
if (!addressesByChain.has(v.chainId)) addressesByChain.set(v.chainId, new Set())
65+
const set = addressesByChain.get(v.chainId)!
66+
if (v.address) set.add(v.address.toLowerCase())
67+
if (v.asset?.address) set.add(v.asset.address.toLowerCase())
68+
}
69+
70+
const result: Record<string, Record<string, string>> = {}
71+
72+
for (const [chainId, addresses] of addressesByChain) {
73+
const chainKey = String(chainId)
74+
result[chainKey] = {}
75+
const addressList = [...addresses]
76+
77+
for (let i = 0; i < addressList.length; i += CONCURRENCY) {
78+
const batch = addressList.slice(i, i + CONCURRENCY)
79+
const results = await Promise.all(batch.map((a) => fetchEnsoPrice(chainId, a, apiKey)))
80+
for (const r of results) {
81+
if (r && r.price != null) {
82+
result[chainKey][r.address] = Math.round(r.price * 1e6).toString()
83+
}
84+
}
85+
await sleep(DELAY_MS)
86+
}
87+
}
88+
89+
return new Response(JSON.stringify(result), {
90+
status: 200,
91+
headers: {
92+
'Content-Type': 'application/json',
93+
'Cache-Control': 'public, s-maxage=120, stale-while-revalidate=600'
94+
}
95+
})
96+
} catch (error) {
97+
console.error('Error fetching Enso prices:', error)
98+
return new Response(JSON.stringify({ error: 'Internal server error' }), {
99+
status: 500,
100+
headers: { 'Content-Type': 'application/json' }
101+
})
102+
}
103+
}

api/server.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { serve } from 'bun'
22

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

164+
const ENSO_SUPPORTED_CHAINS = [1, 8453, 747474, 10, 137, 42161, 100, 146, 80094]
165+
166+
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
167+
168+
async function fetchEnsoPrice(
169+
chainId: number,
170+
address: string,
171+
apiKey: string,
172+
retries = 2
173+
): Promise<{ address: string; price: number } | null> {
174+
for (let attempt = 0; attempt <= retries; attempt++) {
175+
try {
176+
const res = await fetch(`${ENSO_API_BASE}/api/v1/prices/${chainId}/${address}`, {
177+
headers: { Authorization: `Bearer ${apiKey}` }
178+
})
179+
if (res.status === 429) {
180+
await sleep(1000 * (attempt + 1))
181+
continue
182+
}
183+
if (!res.ok) return null
184+
const data = (await res.json()) as { price: number }
185+
return { address, price: data.price }
186+
} catch {
187+
if (attempt < retries) {
188+
await sleep(500 * (attempt + 1))
189+
continue
190+
}
191+
return null
192+
}
193+
}
194+
return null
195+
}
196+
197+
async function handleEnsoPrices(): Promise<Response> {
198+
const apiKey = process.env.ENSO_API_KEY
199+
if (!apiKey) {
200+
console.error('ENSO_API_KEY not configured')
201+
return Response.json({ error: 'Enso API not configured' }, { status: 500 })
202+
}
203+
204+
try {
205+
const vaults: { chainId: number; address: string; asset?: { address?: string } }[] = await fetch(
206+
`${KONG_REST_BASE}/list/vaults`
207+
).then((r) => r.json())
208+
209+
const addressesByChain = new Map<number, Set<string>>()
210+
for (const v of vaults) {
211+
if (!ENSO_SUPPORTED_CHAINS.includes(v.chainId)) continue
212+
if (!addressesByChain.has(v.chainId)) addressesByChain.set(v.chainId, new Set())
213+
const set = addressesByChain.get(v.chainId)!
214+
if (v.address) set.add(v.address.toLowerCase())
215+
if (v.asset?.address) set.add(v.asset.address.toLowerCase())
216+
}
217+
218+
const result: Record<string, Record<string, string>> = {}
219+
220+
for (const [chainId, addresses] of addressesByChain) {
221+
const chainKey = String(chainId)
222+
result[chainKey] = {}
223+
const addressList = [...addresses]
224+
225+
for (let i = 0; i < addressList.length; i += ENSO_PRICE_CONCURRENCY) {
226+
const batch = addressList.slice(i, i + ENSO_PRICE_CONCURRENCY)
227+
const results = await Promise.all(batch.map((a) => fetchEnsoPrice(chainId, a, apiKey)))
228+
for (const r of results) {
229+
if (r && r.price != null) {
230+
result[chainKey][r.address] = Math.round(r.price * 1e6).toString()
231+
}
232+
}
233+
await sleep(ENSO_PRICE_DELAY_MS)
234+
}
235+
}
236+
237+
return Response.json(result, {
238+
headers: {
239+
'Cache-Control': 'public, s-maxage=120, stale-while-revalidate=600'
240+
}
241+
})
242+
} catch (error) {
243+
console.error('Error fetching Enso prices:', error)
244+
return Response.json({ error: 'Internal server error' }, { status: 500 })
245+
}
246+
}
247+
161248
serve({
162249
async fetch(req) {
163250
const url = new URL(req.url)
@@ -166,6 +253,10 @@ serve({
166253
return handleEnsoStatus()
167254
}
168255

256+
if (url.pathname === '/api/enso/prices') {
257+
return handleEnsoPrices()
258+
}
259+
169260
if (url.pathname === '/api/enso/balances') {
170261
return handleEnsoBalances(req)
171262
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { useDeepCompareMemo } from '@react-hookz/web'
2+
import type { TYDaemonPricesChain } from '../utils/schemas/yDaemonPricesSchema'
3+
import { yDaemonPricesChainSchema } from '../utils/schemas/yDaemonPricesSchema'
4+
import { useFetch } from './useFetch'
5+
6+
/******************************************************************************
7+
** The useFetchEnsoPrices hook fetches token prices from the Enso API via our
8+
** server-side proxy. Returns the same TYDaemonPricesChain shape so it's a
9+
** drop-in replacement for useFetchYDaemonPrices.
10+
*****************************************************************************/
11+
function useFetchEnsoPrices(): TYDaemonPricesChain {
12+
const { data: prices } = useFetch<TYDaemonPricesChain>({
13+
endpoint: '/api/enso/prices',
14+
schema: yDaemonPricesChainSchema
15+
})
16+
17+
const pricesUpdated = useDeepCompareMemo((): TYDaemonPricesChain => {
18+
if (!prices) {
19+
return {}
20+
}
21+
return prices
22+
}, [prices])
23+
24+
return pricesUpdated
25+
}
26+
27+
export { useFetchEnsoPrices }
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { useDeepCompareMemo } from '@react-hookz/web'
2+
import type { TYDaemonPricesChain } from '../utils/schemas/yDaemonPricesSchema'
3+
import { yDaemonPricesChainSchema } from '../utils/schemas/yDaemonPricesSchema'
4+
import { useFetch } from './useFetch'
5+
import { useYDaemonBaseURI } from './useYDaemonBaseURI'
6+
7+
/******************************************************************************
8+
** The useFetchYDaemonPrices hook is used to fetch the prices of the tokens
9+
** from the yDaemon API. It returns an object with the prices of the tokens,
10+
** splitted by chain.
11+
*****************************************************************************/
12+
function useFetchYDaemonPrices(): TYDaemonPricesChain {
13+
const { yDaemonBaseUri: yDaemonBaseUriWithoutChain } = useYDaemonBaseURI()
14+
const { data: prices } = useFetch<TYDaemonPricesChain>({
15+
endpoint: `${yDaemonBaseUriWithoutChain}/prices/all`,
16+
schema: yDaemonPricesChainSchema
17+
})
18+
19+
const pricesUpdated = useDeepCompareMemo((): TYDaemonPricesChain => {
20+
if (!prices) {
21+
return {}
22+
}
23+
return prices
24+
}, [prices])
25+
26+
return pricesUpdated
27+
}
28+
29+
export { useFetchYDaemonPrices }
Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,10 @@
1-
import { useDeepCompareMemo } from '@react-hookz/web'
2-
import type { TYDaemonPricesChain } from '../utils/schemas/yDaemonPricesSchema'
3-
import { yDaemonPricesChainSchema } from '../utils/schemas/yDaemonPricesSchema'
4-
import { useFetch } from './useFetch'
5-
import { useYDaemonBaseURI } from './useYDaemonBaseURI'
1+
import { useFetchEnsoPrices } from './useFetchEnsoPrices'
62

73
/******************************************************************************
8-
** The useFetchYearnPrices hook is used to fetch the prices of the tokens from
9-
** the yDaemon API. It returns an object with the prices of the tokens,
10-
** splitted by chain.
4+
** The useFetchYearnPrices hook is used to fetch the prices of the tokens.
5+
** It delegates to the Enso price provider by default. To switch back to
6+
** yDaemon, swap the import to useFetchYDaemonPrices.
117
*****************************************************************************/
12-
function useFetchYearnPrices(): TYDaemonPricesChain {
13-
const { yDaemonBaseUri: yDaemonBaseUriWithoutChain } = useYDaemonBaseURI()
14-
const { data: prices } = useFetch<TYDaemonPricesChain>({
15-
endpoint: `${yDaemonBaseUriWithoutChain}/prices/all`,
16-
schema: yDaemonPricesChainSchema
17-
})
18-
19-
const pricesUpdated = useDeepCompareMemo((): TYDaemonPricesChain => {
20-
if (!prices) {
21-
return {}
22-
}
23-
return prices
24-
}, [prices])
25-
26-
return pricesUpdated
27-
}
8+
const useFetchYearnPrices = useFetchEnsoPrices
289

2910
export { useFetchYearnPrices }

0 commit comments

Comments
 (0)