From b2512a53c5d52fd27ac8aa6b22958c00d74baa24 Mon Sep 17 00:00:00 2001 From: norugpull Date: Tue, 25 Nov 2025 13:29:03 +0330 Subject: [PATCH] feat: endpoint for active user --- apps/indexer/src/app/routes/_chain/routes.ts | 86 +++++++++++++++++++ apps/indexer/src/libs/loaders/money-market.ts | 20 ++++- apps/indexer/src/libs/utils/user-reserves.ts | 57 ++++++++++++ 3 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 apps/indexer/src/libs/utils/user-reserves.ts diff --git a/apps/indexer/src/app/routes/_chain/routes.ts b/apps/indexer/src/app/routes/_chain/routes.ts index e144543..e6d32af 100644 --- a/apps/indexer/src/app/routes/_chain/routes.ts +++ b/apps/indexer/src/app/routes/_chain/routes.ts @@ -9,9 +9,11 @@ import { TTokenSelected, tTokensSelectors } from '../../../database/selectors'; import { fetchPoolList, fetchPoolReserves, + fetchUserReserves, selectPoolById, } from '../../../libs/loaders/money-market'; import { paginationResponse, paginationSchema } from '../../../libs/pagination'; +import { transformUserReservesData } from '../../../libs/utils/user-reserves'; interface ReserveDataHumanized { originalId: number; @@ -262,4 +264,88 @@ export default async function (fastify: FastifyInstance) { // }; }, ); + + fastify.withTypeProvider().get( + '/money-market/:pool/user/:address/lendings', + { + schema: { + querystring: paginationSchema, + params: z.object({ + pool: z.string(), + address: z.string(), + }), + }, + config: { + cache: true, + }, + }, + async ( + req: FastifyRequest<{ Params: { pool: string; address: string } }>, + reply, + ) => { + const pools = await fetchPoolList(req.chain.chainId); + const pool = selectPoolById(req.params.pool, pools); + + if (!pool) return reply.notFound('Pool not found'); + + const userReservesRaw = await fetchUserReserves( + req.chain.chainId, + pool, + req.params.address, + ); + + const activePositions = userReservesRaw.filter( + (r) => r.scaledATokenBalance > 0n, + ); + + return transformUserReservesData({ + chainId: req.chain.chainId, + userAddress: req.params.address, + pool, + reserves: activePositions, + }); + }, + ); + + fastify.withTypeProvider().get( + '/money-market/:pool/user/:address/borrowings', + { + schema: { + querystring: paginationSchema, + params: z.object({ + pool: z.string(), + address: z.string(), + }), + }, + config: { + cache: true, + }, + }, + async ( + req: FastifyRequest<{ Params: { pool: string; address: string } }>, + reply, + ) => { + const pools = await fetchPoolList(req.chain.chainId); + const pool = selectPoolById(req.params.pool, pools); + + if (!pool) return reply.notFound('Pool not found'); + + const userReservesRaw = await fetchUserReserves( + req.chain.chainId, + pool, + req.params.address, + ); + + const activeBorrows = userReservesRaw.filter( + (r) => r.scaledVariableDebt > 0n || r.principalStableDebt > 0n, + ); + + return transformUserReservesData({ + chainId: req.chain.chainId, + userAddress: req.params.address, + pool, + reserves: activeBorrows, + }); + }, + ); } diff --git a/apps/indexer/src/libs/loaders/money-market.ts b/apps/indexer/src/libs/loaders/money-market.ts index ffbc952..bbfd2d4 100644 --- a/apps/indexer/src/libs/loaders/money-market.ts +++ b/apps/indexer/src/libs/loaders/money-market.ts @@ -353,7 +353,7 @@ const uiPoolDataProviderAbi = [ }, ] as const; -type PoolDefinition = { +export type PoolDefinition = { id: string | 'default'; name: string; logoURI: string; @@ -407,3 +407,21 @@ export async function fetchPoolReserves( args: [pool.poolAddressesProvider], }); } + +export async function fetchUserReserves( + chainId: ChainSelector, + pool: PoolDefinition, + user: string, +) { + const chain = chains.get(chainId); + if (!chain) { + throw new Error(`Unsupported chain: ${chainId}`); + } + + return chain.rpc.readContract({ + address: pool.uiPoolDataProvider, + abi: uiPoolDataProviderAbi, + functionName: 'getUserReservesData', + args: [pool.poolAddressesProvider, user as Address], + }); +} diff --git a/apps/indexer/src/libs/utils/user-reserves.ts b/apps/indexer/src/libs/utils/user-reserves.ts new file mode 100644 index 0000000..e65376d --- /dev/null +++ b/apps/indexer/src/libs/utils/user-reserves.ts @@ -0,0 +1,57 @@ +import { areAddressesEqual } from '@sovryn/slayer-shared'; +import { and, eq, inArray } from 'drizzle-orm'; +import { client } from '../../database/client'; +import { tTokens } from '../../database/schema'; +import { tTokensSelectors } from '../../database/selectors'; +import { PoolDefinition } from '../loaders/money-market'; + +export async function transformUserReservesData({ + chainId, + userAddress, + pool, + reserves, +}: { + chainId: number; + userAddress: string; + pool: PoolDefinition; + reserves: Array<{ + underlyingAsset: string; + scaledATokenBalance: bigint; + usageAsCollateralEnabledOnUser: boolean; + stableBorrowRate: bigint; + scaledVariableDebt: bigint; + principalStableDebt: bigint; + stableBorrowLastUpdateTimestamp: bigint; + }>; +}) { + if (!reserves.length) { + return { data: [], count: 0 }; + } + + const tokens = await client.query.tTokens.findMany({ + columns: tTokensSelectors.columns, + where: and( + eq(tTokens.chainId, chainId), + inArray( + tTokens.address, + reserves.map((i) => i.underlyingAsset.toLowerCase()), + ), + ), + }); + + const data = reserves.map((pos) => ({ + id: `${chainId}-${pos.underlyingAsset}-${pool.address}-${userAddress}`.toLowerCase(), + user: userAddress, + pool, + token: tokens.find((t) => + areAddressesEqual(t.address, pos.underlyingAsset), + ), + scaledVariableDebt: pos?.scaledVariableDebt.toString(), + principalStableDebt: pos?.principalStableDebt.toString(), + stableBorrowRate: pos?.stableBorrowRate.toString(), + stableBorrowLastUpdateTimestamp: pos.stableBorrowLastUpdateTimestamp, + usageAsCollateralEnabledOnUser: pos.usageAsCollateralEnabledOnUser, + })); + + return { data, count: data.length }; +}