diff --git a/packages/api/src/routes/metrics/metricController.ts b/packages/api/src/routes/metrics/metricController.ts index 7197b5c9..99031895 100644 --- a/packages/api/src/routes/metrics/metricController.ts +++ b/packages/api/src/routes/metrics/metricController.ts @@ -2676,3 +2676,48 @@ async function calculateMetricsForHistoricalRecord( function extractTvlValue(tvl: number | TvlWithChange): number { return (tvl as TvlWithChange).tvl || (tvl as number) } + +/** + * Function to handle the request for historical strategy shares data + * + * @param req + * @param res + * @returns + */ +export async function getHistoricalStrategyShares(req: Request, res: Response) { + const { strategyAddress } = req.params + + try { + if (!strategyAddress) { + return res.status(400).json({ error: 'Strategy address is required' }) + } + + const prismaClient = getPrismaClient() + + const historicalShares = await prismaClient.strategySharesDaily.findMany({ + where: { + strategyAddress: strategyAddress.toLowerCase() + }, + orderBy: { + timestamp: 'asc' + }, + select: { + strategyAddress: true, + tokenAddress: true, + sharesToUnderlying: true, + timestamp: true + } + }) + + return res.status(200).json({ + data: historicalShares.map((share) => ({ + strategyAddress: share.strategyAddress, + tokenAddress: share.tokenAddress, + exchangeRate: share.sharesToUnderlying, + timestamp: share.timestamp + })) + }) + } catch (error) { + return res.status(500).json({ error: 'Failed to fetch historical strategy shares data' }) + } +} diff --git a/packages/api/src/routes/metrics/metricRoutes.ts b/packages/api/src/routes/metrics/metricRoutes.ts index f61291ca..f02ba27b 100644 --- a/packages/api/src/routes/metrics/metricRoutes.ts +++ b/packages/api/src/routes/metrics/metricRoutes.ts @@ -23,7 +23,8 @@ import { getTotalWithdrawals, getTotalDeposits, getRestakingRatio, - getDeploymentRatio + getDeploymentRatio, + getHistoricalStrategyShares } from './metricController' import routeCache from 'route-cache' @@ -97,4 +98,10 @@ router.get('/restaking-ratio', routeCache.cacheSeconds(120), getRestakingRatio) router.get('/deployment-ratio', routeCache.cacheSeconds(120), getDeploymentRatio) +router.get( + '/historical/strategy-shares/:strategyAddress', + routeCache.cacheSeconds(120), + getHistoricalStrategyShares +) + export default router diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 832bfe99..06c23d4a 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -874,6 +874,19 @@ model EthPricesDaily { @@index([timestamp]) } +model StrategySharesDaily { + id Int @id @default(autoincrement()) + + strategyAddress String + tokenAddress String + sharesToUnderlying String + timestamp DateTime + + @@unique([strategyAddress, timestamp]) + @@index([strategyAddress]) + @@index([timestamp]) +} + // Misc model Evm_BlockData { diff --git a/packages/seeder/src/index.ts b/packages/seeder/src/index.ts index c583ba89..04ae15b6 100644 --- a/packages/seeder/src/index.ts +++ b/packages/seeder/src/index.ts @@ -29,6 +29,7 @@ import { seedMetricsRestaking } from './metrics/seedMetricsRestaking' import { seedStrategies } from './seedStrategies' import { seedRestakedStrategies } from './seedAvsRestakedStrategies' import { seedEthPricesDaily } from './seedEthPricesDaily' +import { seedStrategySharesDaily } from './seedStrategySharesDaily' import { seedMetricsEigenPods } from './metrics/seedMetricsEigenPods' import { seedMetricsTvl } from './metrics/seedMetricsTvl' import { monitorAvsMetrics } from './monitors/avsMetrics' @@ -271,6 +272,7 @@ async function seedEigenDailyData(retryCount = 0) { await seedStrategies() await seedRestakedStrategies() await seedEthPricesDaily() + await seedStrategySharesDaily() if (!process.env.FLAG_SEEDER_DISABLE_HISTORICAL_DATA) { await seedMetricsDeposit() diff --git a/packages/seeder/src/seedStrategySharesDaily.ts b/packages/seeder/src/seedStrategySharesDaily.ts new file mode 100644 index 00000000..55ec8761 --- /dev/null +++ b/packages/seeder/src/seedStrategySharesDaily.ts @@ -0,0 +1,113 @@ +import 'dotenv/config' + +import type prisma from '@prisma/client' +import { strategyAbi } from './data/abi/strategy' +import { getPrismaClient } from './utils/prismaClient' +import { bulkUpdateDbTransactions } from './utils/seeder' +import { getViemClient } from './utils/viemClient' + +export async function seedStrategySharesDaily() { + const prismaClient = getPrismaClient() + const viemClient = getViemClient() + + console.time('Done in') + + const startAt = await getLastUpdate() + const endAt = setToStartOfDay(new Date()) + + const strategySharesDailyList: Omit[] = [] + + // Bail early if there is no time diff to sync + if (endAt.getTime() - startAt.getTime() <= 0) { + console.log( + `[In Sync] [Data] Strategy Shares Daily from: ${startAt.getTime()} to: ${endAt.getTime()}` + ) + return + } + + try { + // Get all strategies from the database + const strategies = await prismaClient.strategies.findMany() + + // For each day between startAt and endAt, capture strategy shares + const currentDate = new Date(startAt) + while (currentDate < endAt) { + const dayTimestamp = setToStartOfDay(new Date(currentDate)) + + // For each strategy, get the current sharesToUnderlying rate + for (const strategy of strategies) { + const strategyAddress = strategy.address.toLowerCase() + let sharesToUnderlying = strategy.sharesToUnderlying || '1000000000000000000' // Default 1e18 + + try { + // Read current exchange rate from strategy contract + const rate = (await viemClient.readContract({ + address: strategyAddress as `0x${string}`, + abi: strategyAbi, + functionName: 'sharesToUnderlyingView', + args: [BigInt(1e18)] + })) as bigint + + sharesToUnderlying = rate.toString() + } catch (error) { + // Use fallback from database if contract call fails + console.warn(`Failed to read sharesToUnderlying for strategy ${strategyAddress}:`, error) + } + + strategySharesDailyList.push({ + strategyAddress, + tokenAddress: strategy.underlyingToken.toLowerCase(), + sharesToUnderlying, + timestamp: dayTimestamp + }) + } + + // Move to next day + currentDate.setDate(currentDate.getDate() + 1) + } + + // Save to database + if (strategySharesDailyList.length > 0) { + const dbTransactions: any[] = [] + + dbTransactions.push( + prismaClient.strategySharesDaily.createMany({ + data: strategySharesDailyList, + skipDuplicates: true + }) + ) + + await bulkUpdateDbTransactions( + dbTransactions, + `[Data] Strategy Shares Daily size: ${strategySharesDailyList.length}` + ) + } + } catch (error) { + console.log('Error seeding Strategy Shares Daily: ', error) + } + + console.timeEnd('Done in') +} + +async function getLastUpdate() { + const prismaClient = getPrismaClient() + const minimumStartAt = setToStartOfDay(new Date()) + minimumStartAt.setMonth(minimumStartAt.getMonth() - 1) + minimumStartAt.setDate(minimumStartAt.getDate() + 1) + + const latestRecord = await prismaClient.strategySharesDaily.findFirst({ + select: { timestamp: true }, + orderBy: { timestamp: 'desc' } + }) + + return (latestRecord && latestRecord.timestamp.getTime() <= minimumStartAt.getTime()) || + !latestRecord + ? minimumStartAt + : latestRecord.timestamp +} + +function setToStartOfDay(date: Date) { + const dailyDate = new Date(date) + dailyDate.setUTCHours(0, 0, 0, 0) + return dailyDate +}