diff --git a/apps/indexer/package.json b/apps/indexer/package.json index 7924c09..8cd23b6 100644 --- a/apps/indexer/package.json +++ b/apps/indexer/package.json @@ -138,6 +138,8 @@ } }, "dependencies": { + "@aave/math-utils": "1.29.1", + "@aave/contract-helpers": "1.29.1", "@bull-board/api": "6.13.0", "@bull-board/fastify": "6.13.0", "@fastify/autoload": "6.0.3", diff --git a/apps/indexer/src/app/plugins/cache.ts b/apps/indexer/src/app/plugins/cache.ts index cf37b30..cd6782b 100644 --- a/apps/indexer/src/app/plugins/cache.ts +++ b/apps/indexer/src/app/plugins/cache.ts @@ -7,8 +7,28 @@ import type { import fp from 'fastify-plugin'; import { ENV } from '../../env'; import { encode } from '../../libs/encode'; +import { logger } from '../../libs/logger'; import { createRedisConnection } from '../../libs/utils/redis'; +// Custom JSON serialization to handle BigInt values +function serializeWithBigInt(value: unknown): string { + return JSON.stringify(value, (key, val) => { + if (typeof val === 'bigint') { + return { __type: 'bigint', value: val.toString() }; + } + return val; + }); +} + +function deserializeWithBigInt(json: string): T { + return JSON.parse(json, (key, val) => { + if (val && typeof val === 'object' && val.__type === 'bigint') { + return BigInt(val.value); + } + return val; + }); +} + const cacheRedisConnection = createRedisConnection( ENV.REDIS_URL, ENV.REDIS_CLUSTER_MODE, @@ -106,7 +126,7 @@ const redisCachePlugin: FastifyPluginAsync = async ( req: FastifyRequest, ): RouteCacheOptions | null => { const rawCfg = req.routeOptions.config.cache; - if (!rawCfg) return null; + if (!rawCfg || ENV.NO_CACHE) return null; if (typeof rawCfg === 'boolean') { if (!rawCfg) return null; @@ -142,7 +162,7 @@ const redisCachePlugin: FastifyPluginAsync = async ( (cfg.key?.(req) ?? encode.sha256( `${routeUrl}:${req.raw.method}:` + - `${JSON.stringify(req.query ?? {})}:${JSON.stringify(req.body ?? {})}`, + `${JSON.stringify(req.params ?? {})}:${JSON.stringify(req.query ?? {})}:${JSON.stringify(req.body ?? {})}`, )); req.__cacheKey = key; @@ -150,7 +170,7 @@ const redisCachePlugin: FastifyPluginAsync = async ( const cached = await redis.get(key); if (!cached) return; - const entry: CacheEntry = JSON.parse(cached); + const entry: CacheEntry = deserializeWithBigInt(cached); const ageSec = (Date.now() - entry.storedAt) / 1000; const isFresh = ageSec <= entry.ttlSeconds; @@ -228,7 +248,7 @@ const redisCachePlugin: FastifyPluginAsync = async ( const cfg: RouteCacheOptions = typeof rawCfg === 'boolean' ? { enabled: rawCfg } : rawCfg; - if (cfg.enabled === false) return payload; + if (cfg.enabled === false || ENV.NO_CACHE) return payload; if (req.__cacheHit) { return payload; @@ -260,7 +280,7 @@ const redisCachePlugin: FastifyPluginAsync = async ( const expireSeconds = ttl + (cfg.staleTtlSeconds ?? defaultStaleTtl); - await redis.setex(key, expireSeconds, JSON.stringify(entry)); + await redis.setex(key, expireSeconds, serializeWithBigInt(entry)); // For "real" client requests (not internal revalidation), set MISS header if (req.headers['x-cache-revalidate'] !== '1') { @@ -272,6 +292,53 @@ const redisCachePlugin: FastifyPluginAsync = async ( ); }; +export const maybeCache = async ( + key: string, + fn: () => Promise, + opts: Pick = {}, +): Promise => { + const enabled = opts.enabled ?? true; + + if (!enabled || ENV.NO_CACHE) { + return fn(); + } + + const redis = cacheRedisConnection; + + const ttl = opts.ttlSeconds ?? 30; // 30 seconds + + const cacheKey = 'maybe-cache:fn:' + encode.sha256(key); + + const cached = await redis.get(cacheKey); + + if (cached) { + const entry: CacheEntry = deserializeWithBigInt(cached); + const ageSec = (Date.now() - entry.storedAt) / 1000; + + const isFresh = ageSec <= entry.ttlSeconds; + + logger.info({ key, ageSec, ttl: entry.ttlSeconds }, 'Cache hit'); + + if (isFresh) { + return entry.payload as T; + } + } + + logger.info({ key }, 'Cache miss, invoking function'); + + const entry = { + payload: await fn(), + headers: {}, + statusCode: 200, + storedAt: Date.now(), + ttlSeconds: ttl, + }; + + await redis.setex(cacheKey, ttl, serializeWithBigInt(entry)); + + return entry.payload as T; +}; + export default fp(redisCachePlugin, { name: 'cache-plugin', }); diff --git a/apps/indexer/src/app/routes/_chain/routes.ts b/apps/indexer/src/app/routes/_chain/routes.ts index e6d32af..aa8d2c9 100644 --- a/apps/indexer/src/app/routes/_chain/routes.ts +++ b/apps/indexer/src/app/routes/_chain/routes.ts @@ -1,11 +1,15 @@ +import { + formatReserves, + formatUserSummary, + USD_DECIMALS, +} from '@aave/math-utils'; import { areAddressesEqual, Decimal } from '@sovryn/slayer-shared'; import { and, asc, eq, gte, inArray } from 'drizzle-orm'; -import { FastifyInstance, FastifyRequest } from 'fastify'; -import { ZodTypeProvider } from 'fastify-type-provider-zod'; +import { FastifyRequest } from 'fastify'; import z from 'zod'; import { client } from '../../../database/client'; import { tTokens } from '../../../database/schema'; -import { TTokenSelected, tTokensSelectors } from '../../../database/selectors'; +import { tTokensSelectors } from '../../../database/selectors'; import { fetchPoolList, fetchPoolReserves, @@ -13,71 +17,15 @@ import { selectPoolById, } from '../../../libs/loaders/money-market'; import { paginationResponse, paginationSchema } from '../../../libs/pagination'; -import { transformUserReservesData } from '../../../libs/utils/user-reserves'; - -interface ReserveDataHumanized { - originalId: number; - id: string; - underlyingAsset: string; - - token: TTokenSelected; - - name: string; - symbol: string; - decimals: number; - baseLTVasCollateral: string; - reserveLiquidationThreshold: string; - reserveLiquidationBonus: string; - reserveFactor: string; - usageAsCollateralEnabled: boolean; - borrowingEnabled: boolean; - isActive: boolean; - isFrozen: boolean; - liquidityIndex: string; - variableBorrowIndex: string; - liquidityRate: string; - variableBorrowRate: string; - lastUpdateTimestamp: number; - aTokenAddress: string; - variableDebtTokenAddress: string; - interestRateStrategyAddress: string; - availableLiquidity: string; - totalScaledVariableDebt: string; - priceInMarketReferenceCurrency: string; - priceOracle: string; - variableRateSlope1: string; - variableRateSlope2: string; - baseVariableBorrowRate: string; - optimalUsageRatio: string; - // v3 only - isPaused: boolean; - isSiloedBorrowing: boolean; - accruedToTreasury: string; - unbacked: string; - isolationModeTotalDebt: string; - flashLoanEnabled: boolean; - debtCeiling: string; - debtCeilingDecimals: number; - borrowCap: string; - supplyCap: string; - borrowableInIsolation: boolean; - virtualAccActive: boolean; - virtualUnderlyingBalance: string; -} - -interface PoolBaseCurrencyHumanized { - marketReferenceCurrencyDecimals: number; - marketReferenceCurrencyPriceInUsd: string; - networkBaseTokenPriceInUsd: string; - networkBaseTokenPriceDecimals: number; -} +import { ZodFastifyInstance } from '../../../libs/server'; +import { ze } from '../../../libs/validators/validators'; -export default async function (fastify: FastifyInstance) { +export default async function (fastify: ZodFastifyInstance) { fastify.get('/', async (req) => { return { data: req.chain }; }); - fastify.withTypeProvider().get( + fastify.get( '/tokens', { schema: { @@ -106,7 +54,7 @@ export default async function (fastify: FastifyInstance) { }, ); - fastify.withTypeProvider().get( + fastify.get( '/money-market', { config: { @@ -129,7 +77,7 @@ export default async function (fastify: FastifyInstance) { }, ); - fastify.withTypeProvider().get( + fastify.get( '/money-market/:pool/reserves', { schema: { @@ -139,7 +87,7 @@ export default async function (fastify: FastifyInstance) { }), }, config: { - cache: true, + cache: false, }, }, async (req: FastifyRequest<{ Params: { pool: string } }>, reply) => { @@ -150,8 +98,10 @@ export default async function (fastify: FastifyInstance) { return reply.notFound('Pool not found'); } - const { 0: reservesRaw, 1: poolBaseCurrencyRaw } = - await fetchPoolReserves(req.chain.chainId, pool); + const { reservesData, baseCurrencyData } = await fetchPoolReserves( + req.chain.chainId, + pool, + ); const tokens = await client.query.tTokens.findMany({ columns: tTokensSelectors.columns, @@ -159,98 +109,53 @@ export default async function (fastify: FastifyInstance) { eq(tTokens.chainId, req.chain.chainId), inArray( tTokens.address, - reservesRaw.map((i) => i.underlyingAsset.toLowerCase()), + reservesData.map((i) => i.underlyingAsset.toLowerCase()), ), ), }); - const reservesData: Partial[] = reservesRaw.map( - (reserveRaw, index) => { - // const virtualUnderlyingBalance = - // reserveRaw.virtualUnderlyingBalance.toString(); - // const { virtualAccActive } = reserveRaw; - return { - originalId: index, - id: `${req.chain.chainId}-${reserveRaw.underlyingAsset}-${pool.address}`.toLowerCase(), - // underlyingAsset: reserveRaw.underlyingAsset.toLowerCase(), - - token: tokens.find((t) => - areAddressesEqual(t.address, reserveRaw.underlyingAsset), - ), - pool, - - // name: reserveRaw.name, - // symbol: ammSymbolMap[reserveRaw.underlyingAsset.toLowerCase()] - // ? ammSymbolMap[reserveRaw.underlyingAsset.toLowerCase()] - // : reserveRaw.symbol, - // decimals: reserveRaw.decimals.toNumber(), - baseLTVasCollateral: reserveRaw.baseLTVasCollateral.toString(), - reserveLiquidationThreshold: - reserveRaw.reserveLiquidationThreshold.toString(), - reserveLiquidationBonus: - reserveRaw.reserveLiquidationBonus.toString(), - reserveFactor: reserveRaw.reserveFactor.toString(), - usageAsCollateralEnabled: reserveRaw.usageAsCollateralEnabled, - borrowingEnabled: reserveRaw.borrowingEnabled, - isActive: reserveRaw.isActive, - isFrozen: reserveRaw.isFrozen, - liquidityIndex: reserveRaw.liquidityIndex.toString(), - variableBorrowIndex: reserveRaw.variableBorrowIndex.toString(), - liquidityRate: reserveRaw.liquidityRate.toString(), - variableBorrowRate: reserveRaw.variableBorrowRate.toString(), - lastUpdateTimestamp: reserveRaw.lastUpdateTimestamp, - aTokenAddress: reserveRaw.aTokenAddress.toString(), - variableDebtTokenAddress: - reserveRaw.variableDebtTokenAddress.toString(), - interestRateStrategyAddress: - reserveRaw.interestRateStrategyAddress.toString(), - availableLiquidity: Decimal.from( - reserveRaw.availableLiquidity, - reserveRaw.decimals.toNumber(), - ).toString(), - // availableLiquidity: reserveRaw.availableLiquidity.toString(), - totalScaledVariableDebt: - reserveRaw.totalScaledVariableDebt.toString(), - priceInMarketReferenceCurrency: - reserveRaw.priceInMarketReferenceCurrency.toString(), - // priceOracle: reserveRaw.priceOracle, - variableRateSlope1: reserveRaw.variableRateSlope1.toString(), - variableRateSlope2: reserveRaw.variableRateSlope2.toString(), - // baseVariableBorrowRate: - // reserveRaw.baseVariableBorrowRate.toString(), - // optimalUsageRatio: reserveRaw.optimalUsageRatio.toString(), - // new fields - // isPaused: reserveRaw.isPaused, - // debtCeiling: reserveRaw.debtCeiling.toString(), - // borrowCap: reserveRaw.borrowCap.toString(), - // supplyCap: reserveRaw.supplyCap.toString(), - // borrowableInIsolation: reserveRaw.borrowableInIsolation, - // accruedToTreasury: reserveRaw.accruedToTreasury.toString(), - // unbacked: reserveRaw.unbacked.toString(), - // isolationModeTotalDebt: - // reserveRaw.isolationModeTotalDebt.toString(), - // debtCeilingDecimals: reserveRaw.debtCeilingDecimals.toNumber(), - // isSiloedBorrowing: reserveRaw.isSiloedBorrowing, - // flashLoanEnabled: reserveRaw.flashLoanEnabled, - // virtualAccActive, - // virtualUnderlyingBalance, - }; - }, - ); - - const baseCurrencyData: PoolBaseCurrencyHumanized = { - // this is to get the decimals from the unit so 1e18 = string length of 19 - 1 to get the number of 0 + const data = formatReserves({ + reserves: reservesData, + currentTimestamp: Math.floor(Date.now() / 1000), + marketReferencePriceInUsd: + baseCurrencyData.marketReferenceCurrencyPriceInUsd, marketReferenceCurrencyDecimals: - poolBaseCurrencyRaw.marketReferenceCurrencyUnit.toString().length - 1, - marketReferenceCurrencyPriceInUsd: - poolBaseCurrencyRaw.marketReferenceCurrencyPriceInUsd.toString(), - networkBaseTokenPriceInUsd: - poolBaseCurrencyRaw.networkBaseTokenPriceInUsd.toString(), - networkBaseTokenPriceDecimals: - poolBaseCurrencyRaw.networkBaseTokenPriceDecimals, - }; + baseCurrencyData.marketReferenceCurrencyDecimals, + }); + + const items = data.map((item) => { + const token = tokens.find((t) => + areAddressesEqual(t.address, item.underlyingAsset), + ); - return { data: { reservesData, baseCurrencyData } }; + return { + id: item.id, + pool, + token, + priceUsd: Decimal.from(item.priceInUSD).toFixed(USD_DECIMALS), + liquidity: Decimal.from(item.totalLiquidity).toString(), + liquidityUsd: Decimal.from(item.totalLiquidity) + .mul(Decimal.from(item.priceInUSD)) + .toFixed(USD_DECIMALS), + stableBorrowApy: Decimal.from(item.stableBorrowAPY ?? 0) + .mul(100) + .toFixed(USD_DECIMALS), + variableBorrowApy: Decimal.from(item.variableBorrowAPY ?? 0) + .mul(100) + .toFixed(USD_DECIMALS), + canBeBorrowed: item.borrowingEnabled, + supplyApy: Decimal.from(item.supplyAPY) + .mul(100) + .toFixed(USD_DECIMALS), + canBeCollateral: item.usageAsCollateralEnabled, + isActive: item.isActive, + isFroze: item.isFrozen, + // eModes: item.eModes, + i: item, + }; + }); + + return { data: { reservesData: items, baseCurrencyData } }; // return { // data: items @@ -265,18 +170,22 @@ export default async function (fastify: FastifyInstance) { }, ); - fastify.withTypeProvider().get( - '/money-market/:pool/user/:address/lendings', + fastify.get( + '/money-market/:pool/user/:address/positions', { schema: { querystring: paginationSchema, params: z.object({ pool: z.string(), - address: z.string(), + address: ze.address, }), }, config: { - cache: true, + cache: { + enabled: false, + ttlSeconds: 10, + staleTtlSeconds: 15, + }, }, }, async ( @@ -288,64 +197,254 @@ export default async function (fastify: FastifyInstance) { if (!pool) return reply.notFound('Pool not found'); - const userReservesRaw = await fetchUserReserves( + const currentTimestamp = Math.floor(Date.now() / 1000); + + const { reservesData, baseCurrencyData } = await fetchPoolReserves( req.chain.chainId, pool, - req.params.address, ); - const activePositions = userReservesRaw.filter( - (r) => r.scaledATokenBalance > 0n, + const { userReserves, userEmodeCategoryId } = await fetchUserReserves( + req.chain.chainId, + pool, + req.params.address, ); - return transformUserReservesData({ - chainId: req.chain.chainId, - userAddress: req.params.address, - pool, - reserves: activePositions, + const tokens = await client.query.tTokens.findMany({ + columns: tTokensSelectors.columns, + where: and( + eq(tTokens.chainId, req.chain.chainId), + inArray( + tTokens.address, + userReserves.map((i) => i.underlyingAsset.toLowerCase()), + ), + ), }); - }, - ); - fastify.withTypeProvider().get( - '/money-market/:pool/user/:address/borrowings', - { - schema: { - querystring: paginationSchema, - params: z.object({ - pool: z.string(), - address: z.string(), + const summary = formatUserSummary({ + currentTimestamp, + marketReferencePriceInUsd: + baseCurrencyData.marketReferenceCurrencyPriceInUsd, + marketReferenceCurrencyDecimals: + baseCurrencyData.marketReferenceCurrencyDecimals, + userReserves, + userEmodeCategoryId, + formattedReserves: formatReserves({ + reserves: reservesData, + currentTimestamp, + marketReferencePriceInUsd: + baseCurrencyData.marketReferenceCurrencyPriceInUsd, + marketReferenceCurrencyDecimals: + baseCurrencyData.marketReferenceCurrencyDecimals, }), - }, - 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 netWorth = Decimal.from(summary.netWorthUSD); + const borrowBalance = Decimal.from(summary.totalBorrowsUSD); + const supplyBalance = summary.userReservesData.reduce( + (s, r) => s.add(r.underlyingBalanceUSD), + Decimal.from(0), + ); - const userReservesRaw = await fetchUserReserves( - req.chain.chainId, - pool, - req.params.address, + const collateralBalance = summary.userReservesData.reduce( + (s, r) => + r.usageAsCollateralEnabledOnUser ? s.add(r.underlyingBalanceUSD) : s, + Decimal.from(0), ); - const activeBorrows = userReservesRaw.filter( - (r) => r.scaledVariableDebt > 0n || r.principalStableDebt > 0n, + const computeWeightedSupplyApy = () => { + let totalSuppliedUsd = Decimal.from(0); + let weightedSupplyAPYSum = Decimal.from(0); + + summary.userReservesData.forEach((reserve) => { + const suppliedAmountUsd = Decimal.from(reserve.underlyingBalanceUSD); + const supplyAPY = Decimal.from(reserve.reserve.supplyAPY); + + weightedSupplyAPYSum = weightedSupplyAPYSum.add( + supplyAPY.mul(suppliedAmountUsd), + ); + totalSuppliedUsd = totalSuppliedUsd.add(suppliedAmountUsd); + }); + + if (totalSuppliedUsd.eq(0) || weightedSupplyAPYSum.eq(0)) { + return Decimal.from(0); + } + return weightedSupplyAPYSum.div(totalSuppliedUsd).mul(100); + }; + + const computeWeightedBorrowApy = () => { + let totalBorrowedUsd = Decimal.from(0); + let weightedBorrowAPYSum = Decimal.from(0); + + summary.userReservesData.forEach((reserve) => { + const borrowedAmountUsd = Decimal.from(reserve.totalBorrowsUSD); + const borrowAPY = Decimal.from(reserve.reserve.variableBorrowAPY); + + weightedBorrowAPYSum = weightedBorrowAPYSum.add( + borrowAPY.mul(borrowedAmountUsd), + ); + totalBorrowedUsd = totalBorrowedUsd.add(borrowedAmountUsd); + }); + + if (totalBorrowedUsd.eq(0) || weightedBorrowAPYSum.eq(0)) { + return Decimal.from(0); + } + return weightedBorrowAPYSum.div(totalBorrowedUsd).mul(100); + }; + + const supplyWeightedApy = computeWeightedSupplyApy(); + const borrowWeightedApy = computeWeightedBorrowApy(); + + const netApy = netWorth.eq(0) + ? Decimal.from(0) + : supplyWeightedApy + .mul(supplyBalance) + .div(netWorth) + .sub(borrowWeightedApy.mul(borrowBalance).div(netWorth)); + + const currentLiquidationThreshold = Decimal.from( + summary.currentLiquidationThreshold, ); + const borrowPower = collateralBalance + .mul(currentLiquidationThreshold) + .div(1.1); + const borrowPowerUsed = borrowPower.eq(0) + ? Decimal.from(100) + : Decimal.from(borrowBalance).div(borrowPower).mul(100); - return transformUserReservesData({ - chainId: req.chain.chainId, - userAddress: req.params.address, - pool, - reserves: activeBorrows, + const healthFactor = borrowBalance.eq(0) + ? Decimal.INFINITY + : collateralBalance.mul(currentLiquidationThreshold).div(borrowBalance); + + const collateralRatio = borrowBalance.eq(0) + ? Decimal.INFINITY + : Decimal.from(summary.healthFactor); + + const userPositions = summary.userReservesData.map((item) => { + const token = tokens.find((t) => + areAddressesEqual(t.address, item.underlyingAsset), + ); + + const availableLiquidity = Decimal.from( + item.reserve.availableLiquidity, + item.reserve.decimals, + ); + // how much the user can borrow if there is no limit of supply + const canBorrow = Decimal.max( + borrowPower.sub(borrowBalance).div(item.reserve.priceInUSD), + Decimal.ZERO, + ); + + // available to borrow for user including liquidity limitation + const availableToBorrow = availableLiquidity.lt(canBorrow) + ? availableLiquidity + : canBorrow; + const availableToBorrowUsd = availableToBorrow.mul( + item.reserve.priceInUSD, + ); + + const borrowRateMode = Decimal.from(item.variableBorrows).gt(0) ? 2 : 1; + + const canToggleCollateral = + !item.usageAsCollateralEnabledOnUser || + (borrowBalance.eq(0) + ? Decimal.INFINITY + : collateralBalance + .sub(item.underlyingBalanceUSD) + .div(borrowBalance) + ).gt(1.5); + + return { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + id: (item as any).id, + pool, + token, + reserve: { + id: item.reserve.id, + priceUsd: Decimal.from(item.reserve.priceInUSD).toFixed( + USD_DECIMALS, + ), + liquidity: Decimal.from(item.reserve.totalLiquidity).toString(), + liquidityUsd: Decimal.from(item.reserve.totalLiquidity) + .mul(Decimal.from(item.reserve.priceInUSD)) + .toFixed(USD_DECIMALS), + canBeBorrowed: item.reserve.borrowingEnabled, + supplyApy: Decimal.from(item.reserve.supplyAPY) + .mul(100) + .toFixed(USD_DECIMALS), + stableBorrowApy: Decimal.from(item.reserve.stableBorrowAPY) + .mul(100) + .toFixed(USD_DECIMALS), + variableBorrowApy: Decimal.from(item.reserve.variableBorrowAPY) + .mul(100) + .toFixed(USD_DECIMALS), + canBeCollateral: item.reserve.usageAsCollateralEnabled, + isActive: item.reserve.isActive, + isFroze: item.reserve.isFrozen, + // eModes: item.reserve.eModes, + }, + supplied: item.underlyingBalance, + suppliedUsd: item.underlyingBalanceUSD, + + supplyApy: Decimal.from(item.reserve.supplyAPY).mul(100).toString(), + canToggleCollateral, + + borrowed: item.variableBorrows, + borrowedUsd: item.variableBorrowsUSD, + + collateral: item.usageAsCollateralEnabledOnUser, + + availableToBorrow: availableToBorrow.toString(), + availableToBorrowUsd: availableToBorrowUsd.toFixed(USD_DECIMALS), + + borrowRateMode, + borrowApy: Decimal.from( + borrowRateMode === 1 + ? item.reserve.stableBorrowAPY + : item.reserve.variableBorrowAPY, + ) + .mul(100) + .toString(), + stableBorrowApy: Decimal.from(item.reserve.stableBorrowAPY ?? 0) + .mul(100) + .toString(), + variableBorrowApy: Decimal.from(item.reserve.variableBorrowAPY ?? 0) + .mul(100) + .toString(), + }; }); + + return { + data: { + positions: userPositions, + summary: { + netApy: netApy.toFixed(USD_DECIMALS), + healthFactor: healthFactor.toFixed(USD_DECIMALS), + collateralRatio: collateralRatio.toFixed(USD_DECIMALS), + borrowPower: borrowPower.toFixed(USD_DECIMALS), + borrowPowerUsed: borrowPowerUsed.toFixed(USD_DECIMALS), + + borrowWeightedApy: borrowWeightedApy.toFixed(USD_DECIMALS), + supplyWeightedApy: supplyWeightedApy.toFixed(USD_DECIMALS), + + totalLiquidityUsd: supplyBalance.toFixed(USD_DECIMALS), + totalCollateralUsd: collateralBalance.toFixed(USD_DECIMALS), + totalBorrowsUsd: borrowBalance.toFixed(USD_DECIMALS), + availableBorrowsUsd: summary.availableBorrowsUSD, + + currentLoanToValue: summary.currentLoanToValue, + currentLiquidationThreshold: summary.currentLiquidationThreshold, + + supplyBalanceUsd: supplyBalance.toFixed(USD_DECIMALS), + collateralBalanceUsd: collateralBalance.toFixed(USD_DECIMALS), + + netWorthUsd: summary.netWorthUSD, + userEmodeCategoryId: summary.userEmodeCategoryId, + isInIsolationMode: summary.isInIsolationMode, + }, + reservesData, + }, + }; }, ); } diff --git a/apps/indexer/src/app/routes/root.ts b/apps/indexer/src/app/routes/root.ts index 76a22a4..4f16a94 100644 --- a/apps/indexer/src/app/routes/root.ts +++ b/apps/indexer/src/app/routes/root.ts @@ -1,7 +1,7 @@ -import { FastifyInstance } from 'fastify'; import { chains } from '../../configs/chains'; +import { ZodFastifyInstance } from '../../libs/server'; -export default async function (fastify: FastifyInstance) { +export default async function (fastify: ZodFastifyInstance) { fastify.get('/', async function () { return { data: chains.list() }; }); diff --git a/apps/indexer/src/configs/chains.ts b/apps/indexer/src/configs/chains.ts index b3b76dc..288d82c 100644 --- a/apps/indexer/src/configs/chains.ts +++ b/apps/indexer/src/configs/chains.ts @@ -6,6 +6,7 @@ import { Chain as ViemChain, } from 'viem'; import { bobSepolia, rootstock, rootstockTestnet } from 'viem/chains'; +import { ENV } from '../env'; type ChainConfig = { key: string; chainId: number; @@ -43,7 +44,9 @@ const items = [ name: 'BOB Sepolia', rpc: createPublicClient({ chain: bobSepolia, - transport: http(bobSepolia.rpcUrls.default.http[0]), + transport: http( + ENV.RPC_BOB_TESTNET ?? bobSepolia.rpcUrls.default.http[0], + ), }) as PublicClient, aaveSubgraphUrl: 'https://bob-mm.test.sovryn.app/subgraphs/name/DistributedCollective/sov-protocol-subgraphs', diff --git a/apps/indexer/src/configs/constants.ts b/apps/indexer/src/configs/constants.ts index dd27489..6b93553 100644 --- a/apps/indexer/src/configs/constants.ts +++ b/apps/indexer/src/configs/constants.ts @@ -1,2 +1,4 @@ export const BASE_DEFINITIONS_URL = 'https://raw.githubusercontent.com/DistributedCollective/slayer-data/main'; + +export const USD_DECIMALS = 8; diff --git a/apps/indexer/src/env.ts b/apps/indexer/src/env.ts index ce8445e..b4fdc7c 100644 --- a/apps/indexer/src/env.ts +++ b/apps/indexer/src/env.ts @@ -1,5 +1,13 @@ import 'dotenv/config'; -import { bool, cleanEnv, makeValidator, port, str, testOnly } from 'envalid'; +import { + bool, + cleanEnv, + makeValidator, + port, + str, + testOnly, + url, +} from 'envalid'; export const ENV = cleanEnv(process.env, { PORT: port({ default: 8000 }), @@ -25,4 +33,8 @@ export const ENV = cleanEnv(process.env, { throw new Error('FLAGS must be a comma-separated list of strings'); } })({ default: [] }), + + RPC_BOB_TESTNET: url({ default: undefined }), + + NO_CACHE: bool({ default: false }), }); diff --git a/apps/indexer/src/libs/aave/index.ts b/apps/indexer/src/libs/aave/index.ts new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/apps/indexer/src/libs/aave/index.ts @@ -0,0 +1 @@ + diff --git a/apps/indexer/src/libs/loaders/money-market.ts b/apps/indexer/src/libs/loaders/money-market.ts index bbfd2d4..427a50b 100644 --- a/apps/indexer/src/libs/loaders/money-market.ts +++ b/apps/indexer/src/libs/loaders/money-market.ts @@ -1,41 +1,15 @@ import { Address } from 'viem'; +import { maybeCache } from '../../app/plugins/cache'; import { ChainId, chains, ChainSelector } from '../../configs/chains'; import { BASE_DEFINITIONS_URL } from '../../configs/constants'; +// uses contract from @aave/contract-helpers (legacy uiprovider ABI) +// @see https://github.com/aave/aave-utilities/blob/%40aave/contract-helpers%401.29.1/packages/contract-helpers/ const uiPoolDataProviderAbi = [ { inputs: [ { - internalType: 'contract IChainlinkAggregator', - name: '_networkBaseTokenPriceInUsdProxyAggregator', - type: 'address', - }, - { - internalType: 'contract IChainlinkAggregator', - name: '_marketReferenceCurrencyPriceInUsdProxyAggregator', - type: 'address', - }, - ], - stateMutability: 'nonpayable', - type: 'constructor', - }, - { - inputs: [], - name: 'ETH_CURRENCY_UNIT', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'contract ILendingPoolAddressesProvider', + internalType: 'contract IPoolAddressesProvider', name: 'provider', type: 'address', }, @@ -189,6 +163,11 @@ const uiPoolDataProviderAbi = [ name: 'priceInMarketReferenceCurrency', type: 'uint256', }, + { + internalType: 'address', + name: 'priceOracle', + type: 'address', + }, { internalType: 'uint256', name: 'variableRateSlope1', @@ -209,8 +188,108 @@ const uiPoolDataProviderAbi = [ name: 'stableRateSlope2', type: 'uint256', }, + { + internalType: 'uint256', + name: 'baseStableBorrowRate', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'baseVariableBorrowRate', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'optimalUsageRatio', + type: 'uint256', + }, + { + internalType: 'bool', + name: 'isPaused', + type: 'bool', + }, + { + internalType: 'bool', + name: 'isSiloedBorrowing', + type: 'bool', + }, + { + internalType: 'uint128', + name: 'accruedToTreasury', + type: 'uint128', + }, + { + internalType: 'uint128', + name: 'unbacked', + type: 'uint128', + }, + { + internalType: 'uint128', + name: 'isolationModeTotalDebt', + type: 'uint128', + }, + { + internalType: 'bool', + name: 'flashLoanEnabled', + type: 'bool', + }, + { + internalType: 'uint256', + name: 'debtCeiling', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'debtCeilingDecimals', + type: 'uint256', + }, + { + internalType: 'uint8', + name: 'eModeCategoryId', + type: 'uint8', + }, + { + internalType: 'uint256', + name: 'borrowCap', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'supplyCap', + type: 'uint256', + }, + { + internalType: 'uint16', + name: 'eModeLtv', + type: 'uint16', + }, + { + internalType: 'uint16', + name: 'eModeLiquidationThreshold', + type: 'uint16', + }, + { + internalType: 'uint16', + name: 'eModeLiquidationBonus', + type: 'uint16', + }, + { + internalType: 'address', + name: 'eModePriceSource', + type: 'address', + }, + { + internalType: 'string', + name: 'eModeLabel', + type: 'string', + }, + { + internalType: 'bool', + name: 'borrowableInIsolation', + type: 'bool', + }, ], - internalType: 'struct IUiPoolDataProvider.AggregatedReserveData[]', + internalType: 'struct IUiPoolDataProviderV3.AggregatedReserveData[]', name: '', type: 'tuple[]', }, @@ -237,7 +316,7 @@ const uiPoolDataProviderAbi = [ type: 'uint8', }, ], - internalType: 'struct IUiPoolDataProvider.BaseCurrencyInfo', + internalType: 'struct IUiPoolDataProviderV3.BaseCurrencyInfo', name: '', type: 'tuple', }, @@ -248,7 +327,7 @@ const uiPoolDataProviderAbi = [ { inputs: [ { - internalType: 'contract ILendingPoolAddressesProvider', + internalType: 'contract IPoolAddressesProvider', name: 'provider', type: 'address', }, @@ -267,7 +346,7 @@ const uiPoolDataProviderAbi = [ { inputs: [ { - internalType: 'contract ILendingPoolAddressesProvider', + internalType: 'contract IPoolAddressesProvider', name: 'provider', type: 'address', }, @@ -317,35 +396,14 @@ const uiPoolDataProviderAbi = [ type: 'uint256', }, ], - internalType: 'struct IUiPoolDataProvider.UserReserveData[]', + internalType: 'struct IUiPoolDataProviderV3.UserReserveData[]', name: '', type: 'tuple[]', }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'marketReferenceCurrencyPriceInUsdProxyAggregator', - outputs: [ - { - internalType: 'contract IChainlinkAggregator', - name: '', - type: 'address', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'networkBaseTokenPriceInUsdProxyAggregator', - outputs: [ { - internalType: 'contract IChainlinkAggregator', + internalType: 'uint8', name: '', - type: 'address', + type: 'uint8', }, ], stateMutability: 'view', @@ -368,20 +426,105 @@ export type PoolDefinition = { priceFeedURI: string; }; +export interface PoolBaseCurrencyHumanized { + marketReferenceCurrencyDecimals: number; + marketReferenceCurrencyPriceInUsd: string; + networkBaseTokenPriceInUsd: string; + networkBaseTokenPriceDecimals: number; +} + +export interface ReserveDataHumanized { + originalId: number; + id: string; + underlyingAsset: string; + name: string; + symbol: string; + decimals: number; + baseLTVasCollateral: string; + reserveLiquidationThreshold: string; + reserveLiquidationBonus: string; + reserveFactor: string; + usageAsCollateralEnabled: boolean; + borrowingEnabled: boolean; + isActive: boolean; + isFrozen: boolean; + liquidityIndex: string; + variableBorrowIndex: string; + liquidityRate: string; + variableBorrowRate: string; + lastUpdateTimestamp: number; + aTokenAddress: string; + variableDebtTokenAddress: string; + interestRateStrategyAddress: string; + availableLiquidity: string; + totalScaledVariableDebt: string; + priceInMarketReferenceCurrency: string; + priceOracle: string; + variableRateSlope1: string; + variableRateSlope2: string; + baseVariableBorrowRate: string; + optimalUsageRatio: string; + + stableBorrowRateEnabled: boolean; + stableBorrowRate: string; + stableDebtTokenAddress: string; + totalPrincipalStableDebt: string; + averageStableRate: string; + stableDebtLastUpdateTimestamp: number; + + stableRateSlope1: string; + stableRateSlope2: string; + baseStableBorrowRate: string; + + eModeCategoryId: number; + eModeLtv: number; + eModeLiquidationThreshold: number; + eModeLiquidationBonus: number; + eModePriceSource: string; + eModeLabel: string; + // v3 only + isPaused: boolean; + isSiloedBorrowing: boolean; + accruedToTreasury: string; + unbacked: string; + isolationModeTotalDebt: string; + flashLoanEnabled: boolean; + debtCeiling: string; + debtCeilingDecimals: number; + borrowCap: string; + supplyCap: string; + borrowableInIsolation: boolean; + virtualAccActive: boolean; + virtualUnderlyingBalance: string; +} + +export interface ReservesDataHumanized { + reservesData: ReserveDataHumanized[]; + baseCurrencyData: PoolBaseCurrencyHumanized; +} + export async function fetchPoolList(chainId: ChainId) { - const url = `${BASE_DEFINITIONS_URL}/chains/${chainId}/money-market.json`; + return maybeCache( + `pool:list:${chainId}`, + async () => { + const url = `${BASE_DEFINITIONS_URL}/chains/${chainId}/money-market.json`; - const response = await fetch(url); + const response = await fetch(url); - if (!response.ok) { - throw new Error( - `Failed to fetch pools for chainId ${chainId}: ${response.statusText}`, - ); - } + if (!response.ok) { + throw new Error( + `Failed to fetch pools for chainId ${chainId}: ${response.statusText}`, + ); + } - const data = (await response.json()) as { items: PoolDefinition[] }; + const data = (await response.json()) as { items: PoolDefinition[] }; - return data.items; + return data.items; + }, + { + ttlSeconds: 60 * 5, // 5 minutes + }, + ); } export function selectPoolById( @@ -400,28 +543,160 @@ export async function fetchPoolReserves( throw new Error(`Unsupported chain: ${chainId}`); } - return chain.rpc.readContract({ - address: pool.uiPoolDataProvider, - abi: uiPoolDataProviderAbi, - functionName: 'getReservesData', - args: [pool.poolAddressesProvider], - }); + return maybeCache( + `pool:reserves:${chainId}:${pool.address}`, + async () => { + const { 0: reservesRaw, 1: poolBaseCurrencyRaw } = + await chain.rpc.readContract({ + address: pool.uiPoolDataProvider, + abi: uiPoolDataProviderAbi, + functionName: 'getReservesData', + args: [pool.poolAddressesProvider], + }); + + const baseCurrencyData: PoolBaseCurrencyHumanized = { + // this is to get the decimals from the unit so 1e18 = string length of 19 - 1 to get the number of 0 + marketReferenceCurrencyDecimals: + poolBaseCurrencyRaw.marketReferenceCurrencyUnit.toString().length - 1, + marketReferenceCurrencyPriceInUsd: + poolBaseCurrencyRaw.marketReferenceCurrencyPriceInUsd.toString(), + networkBaseTokenPriceInUsd: + poolBaseCurrencyRaw.networkBaseTokenPriceInUsd.toString(), + networkBaseTokenPriceDecimals: + poolBaseCurrencyRaw.networkBaseTokenPriceDecimals, + }; + + const reserves = reservesRaw.map((reserveRaw) => { + return { + originalId: reserveRaw.liquidityIndex.toNumber(), + id: `${chainId}-${reserveRaw.underlyingAsset}-${pool.poolAddressesProvider}`.toLowerCase(), + underlyingAsset: reserveRaw.underlyingAsset.toLowerCase(), + name: reserveRaw.name, + symbol: 'todo', // todo + // symbol: ammSymbolMap[reserveRaw.underlyingAsset.toLowerCase()] + // ? ammSymbolMap[reserveRaw.underlyingAsset.toLowerCase()] + // : reserveRaw.symbol, + decimals: reserveRaw.decimals.toNumber(), + baseLTVasCollateral: reserveRaw.baseLTVasCollateral.toString(), + reserveLiquidationThreshold: + reserveRaw.reserveLiquidationThreshold.toString(), + reserveLiquidationBonus: + reserveRaw.reserveLiquidationBonus.toString(), + reserveFactor: reserveRaw.reserveFactor.toString(), + usageAsCollateralEnabled: reserveRaw.usageAsCollateralEnabled, + borrowingEnabled: reserveRaw.borrowingEnabled, + stableBorrowRateEnabled: reserveRaw.stableBorrowRateEnabled, + isActive: reserveRaw.isActive, + isFrozen: reserveRaw.isFrozen, + liquidityIndex: reserveRaw.liquidityIndex.toString(), + variableBorrowIndex: reserveRaw.variableBorrowIndex.toString(), + liquidityRate: reserveRaw.liquidityRate.toString(), + variableBorrowRate: reserveRaw.variableBorrowRate.toString(), + stableBorrowRate: reserveRaw.stableBorrowRate.toString(), + lastUpdateTimestamp: reserveRaw.lastUpdateTimestamp, + aTokenAddress: reserveRaw.aTokenAddress.toString(), + stableDebtTokenAddress: reserveRaw.stableDebtTokenAddress.toString(), + variableDebtTokenAddress: + reserveRaw.variableDebtTokenAddress.toString(), + interestRateStrategyAddress: + reserveRaw.interestRateStrategyAddress.toString(), + availableLiquidity: reserveRaw.availableLiquidity.toString(), + totalPrincipalStableDebt: + reserveRaw.totalPrincipalStableDebt.toString(), + averageStableRate: reserveRaw.averageStableRate.toString(), + stableDebtLastUpdateTimestamp: + reserveRaw.stableDebtLastUpdateTimestamp.toNumber(), + totalScaledVariableDebt: + reserveRaw.totalScaledVariableDebt.toString(), + priceInMarketReferenceCurrency: + reserveRaw.priceInMarketReferenceCurrency.toString(), + priceOracle: reserveRaw.priceOracle, + variableRateSlope1: reserveRaw.variableRateSlope1.toString(), + variableRateSlope2: reserveRaw.variableRateSlope2.toString(), + stableRateSlope1: reserveRaw.stableRateSlope1.toString(), + stableRateSlope2: reserveRaw.stableRateSlope2.toString(), + baseStableBorrowRate: reserveRaw.baseStableBorrowRate.toString(), + baseVariableBorrowRate: reserveRaw.baseVariableBorrowRate.toString(), + optimalUsageRatio: reserveRaw.optimalUsageRatio.toString(), + // new fields + isPaused: reserveRaw.isPaused, + debtCeiling: reserveRaw.debtCeiling.toString(), + eModeCategoryId: reserveRaw.eModeCategoryId, + borrowCap: reserveRaw.borrowCap.toString(), + supplyCap: reserveRaw.supplyCap.toString(), + eModeLtv: reserveRaw.eModeLtv, + eModeLiquidationThreshold: reserveRaw.eModeLiquidationThreshold, + eModeLiquidationBonus: reserveRaw.eModeLiquidationBonus, + eModePriceSource: reserveRaw.eModePriceSource.toString(), + eModeLabel: reserveRaw.eModeLabel.toString(), + borrowableInIsolation: reserveRaw.borrowableInIsolation, + accruedToTreasury: reserveRaw.accruedToTreasury.toString(), + unbacked: reserveRaw.unbacked.toString(), + isolationModeTotalDebt: reserveRaw.isolationModeTotalDebt.toString(), + debtCeilingDecimals: reserveRaw.debtCeilingDecimals.toNumber(), + isSiloedBorrowing: reserveRaw.isSiloedBorrowing, + flashLoanEnabled: reserveRaw.flashLoanEnabled, + virtualAccActive: false, + virtualUnderlyingBalance: '0', + } satisfies ReserveDataHumanized; + }); + + return { + reservesData: reserves, + baseCurrencyData, + } satisfies ReservesDataHumanized; + }, + { + ttlSeconds: 30, + }, + ); +} + +export interface UserReserveDataHumanized { + id: string; + underlyingAsset: string; + scaledATokenBalance: string; + usageAsCollateralEnabledOnUser: boolean; + stableBorrowRate: string; + scaledVariableDebt: string; + principalStableDebt: string; + stableBorrowLastUpdateTimestamp: number; } export async function fetchUserReserves( chainId: ChainSelector, pool: PoolDefinition, user: string, -) { +): Promise<{ + userReserves: UserReserveDataHumanized[]; + userEmodeCategoryId: number; +}> { 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], - }); + const { 0: userReservesRaw, 1: userEmodeCategoryId } = + await chain.rpc.readContract({ + address: pool.uiPoolDataProvider, + abi: uiPoolDataProviderAbi, + functionName: 'getUserReservesData', + args: [pool.poolAddressesProvider, user as Address], + }); + + return { + userReserves: userReservesRaw.map((userReserveRaw) => ({ + id: `${chainId}-${user}-${userReserveRaw.underlyingAsset}-${pool.poolAddressesProvider}`.toLowerCase(), + underlyingAsset: userReserveRaw.underlyingAsset.toLowerCase(), + scaledATokenBalance: userReserveRaw.scaledATokenBalance.toString(), + usageAsCollateralEnabledOnUser: + userReserveRaw.usageAsCollateralEnabledOnUser, + stableBorrowRate: userReserveRaw.stableBorrowRate.toString(), + scaledVariableDebt: userReserveRaw.scaledVariableDebt.toString(), + principalStableDebt: userReserveRaw.principalStableDebt.toString(), + stableBorrowLastUpdateTimestamp: + userReserveRaw.stableBorrowLastUpdateTimestamp.toNumber(), + })), + userEmodeCategoryId, + }; } diff --git a/apps/indexer/src/libs/server.ts b/apps/indexer/src/libs/server.ts new file mode 100644 index 0000000..c7ca110 --- /dev/null +++ b/apps/indexer/src/libs/server.ts @@ -0,0 +1,33 @@ +import { + fastify, + FastifyBaseLogger, + FastifyInstance, + FastifyServerOptions, + RawReplyDefaultExpression, + RawRequestDefaultExpression, + RawServerDefault, +} from 'fastify'; +import { + serializerCompiler, + validatorCompiler, + ZodTypeProvider, +} from 'fastify-type-provider-zod'; + +export type ZodFastifyInstance = FastifyInstance< + RawServerDefault, + RawRequestDefaultExpression, + RawReplyDefaultExpression, + FastifyBaseLogger, + ZodTypeProvider +>; + +export const zodFastify = ( + options: FastifyServerOptions = { logger: true }, +) => { + const server = fastify(options).withTypeProvider(); + + server.setValidatorCompiler(validatorCompiler); + server.setSerializerCompiler(serializerCompiler); + + return server; +}; diff --git a/apps/indexer/src/libs/utils/user-reserves.ts b/apps/indexer/src/libs/utils/user-reserves.ts deleted file mode 100644 index e65376d..0000000 --- a/apps/indexer/src/libs/utils/user-reserves.ts +++ /dev/null @@ -1,57 +0,0 @@ -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 }; -} diff --git a/apps/indexer/src/libs/validators/validators.ts b/apps/indexer/src/libs/validators/validators.ts new file mode 100644 index 0000000..c61a0ea --- /dev/null +++ b/apps/indexer/src/libs/validators/validators.ts @@ -0,0 +1,11 @@ +import { isAddress, type Address } from 'viem'; +import z from 'zod'; + +export const ze = { + address: z + .custom
( + (v) => isAddress(v as string, { strict: false }), + 'invalid address', + ) + .transform((v) => v.toLowerCase() as Address), +} as const; diff --git a/apps/indexer/src/main.ts b/apps/indexer/src/main.ts index 7436e22..9040cbd 100644 --- a/apps/indexer/src/main.ts +++ b/apps/indexer/src/main.ts @@ -4,15 +4,11 @@ import './libs/shims'; // other imports import cors from '@fastify/cors'; import { migrate } from 'drizzle-orm/node-postgres/migrator'; -import Fastify from 'fastify'; -import { - serializerCompiler, - validatorCompiler, -} from 'fastify-type-provider-zod'; import path from 'node:path'; import { app } from './app/app'; import { client } from './database/client'; import { logger } from './libs/logger'; +import { zodFastify } from './libs/server'; import { onShutdown } from './libs/shutdown'; import { notifyReady, onReady } from './libs/startup'; // spawn background jobs, workers, crontab, etc @@ -20,15 +16,12 @@ import './crontab'; import './workers/spawner'; // Instantiate Fastify with some config -const server = Fastify({ +const server = zodFastify({ loggerInstance: logger, disableRequestLogging: true, trustProxy: true, }); -server.setValidatorCompiler(validatorCompiler); -server.setSerializerCompiler(serializerCompiler); - // Enable CORS with the given origins. server.register(cors, { origin: ENV.CORS_ORIGINS, diff --git a/apps/web-app/package.json b/apps/web-app/package.json index abd5855..8d6d43b 100644 --- a/apps/web-app/package.json +++ b/apps/web-app/package.json @@ -29,6 +29,7 @@ "@tanstack/react-router": "1.131.50", "@tanstack/react-router-devtools": "1.131.50", "@tanstack/router-plugin": "1.131.50", + "@wagmi/core": "^3.0.0", "class-variance-authority": "0.7.1", "clsx": "2.1.1", "debug": "4.4.3", diff --git a/apps/web-app/src/components/MoneyMarket/MoneyMarket.constants.ts b/apps/web-app/src/components/MoneyMarket/MoneyMarket.constants.ts deleted file mode 100644 index 8d80b13..0000000 --- a/apps/web-app/src/components/MoneyMarket/MoneyMarket.constants.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const healthFactor = 1.25; -export const netApy = 12.34; -export const netWorth = 5678.9; diff --git a/apps/web-app/src/components/MoneyMarket/components/BorrowAssetsList/components/AssetsTable/AssetsTable.tsx b/apps/web-app/src/components/MoneyMarket/components/BorrowAssetsList/components/AssetsTable/AssetsTable.tsx index 9671b5f..26fdb56 100644 --- a/apps/web-app/src/components/MoneyMarket/components/BorrowAssetsList/components/AssetsTable/AssetsTable.tsx +++ b/apps/web-app/src/components/MoneyMarket/components/BorrowAssetsList/components/AssetsTable/AssetsTable.tsx @@ -19,12 +19,6 @@ type AssetsTableProps = { }; export const AssetsTable: FC = ({ assets }) => { - // const [sortedAssets, setSortedAssets] = - // useState(assets); - // useEffect(() => { - // setSortedAssets(assets); - // }, [assets]); - const handleBorrow = (reserve: MoneyMarketPoolReserve) => borrowRequestStore.getState().setReserve(reserve); @@ -36,16 +30,6 @@ export const AssetsTable: FC = ({ assets }) => {
Asset - {/* {assets.some((asset) => asset.isSortable) && ( - - )} */}
@@ -54,16 +38,6 @@ export const AssetsTable: FC = ({ assets }) => { Available - {/* {assets.some((asset) => asset.isSortable) && ( - - )} */} @@ -72,16 +46,6 @@ export const AssetsTable: FC = ({ assets }) => { APY - {/* {assets.some((asset) => asset.isSortable) && ( - - )} */} @@ -106,30 +70,32 @@ export const AssetsTable: FC = ({ assets }) => { - - - {asset.token.symbol} - -

+

+ -

+
-
-

{0}%

-
+
diff --git a/apps/web-app/src/components/MoneyMarket/components/BorrowPositionsList/BorrowPositionsList.tsx b/apps/web-app/src/components/MoneyMarket/components/BorrowPositionsList/BorrowPositionsList.tsx index 2d674f2..816c49d 100644 --- a/apps/web-app/src/components/MoneyMarket/components/BorrowPositionsList/BorrowPositionsList.tsx +++ b/apps/web-app/src/components/MoneyMarket/components/BorrowPositionsList/BorrowPositionsList.tsx @@ -1,22 +1,22 @@ import { Accordion } from '@/components/ui/accordion'; +import type { MoneyMarketPoolPosition } from '@sovryn/slayer-sdk'; import { Settings, Zap } from 'lucide-react'; import { useState, type FC } from 'react'; import { AmountRenderer } from '../../../ui/amount-renderer'; import { PoolPositionStat } from '../PoolPositionStat/PoolPositionStat'; -import type { BorrowPosition } from './BorrowPositionsList.types'; import { AssetsTable } from './components/AssetsTable/AssetsTable'; type BorrowPositionsListProps = { - supplyBalance: number; - supplyWeightedApy: number; - borrowPower: number; - borrowPositions: BorrowPosition[]; + borrowBalance: string; + borrowWeightedApy: string; + borrowPower: string; + borrowPositions: MoneyMarketPoolPosition[]; loading?: boolean; }; export const BorrowPositionsList: FC = ({ - supplyBalance, - supplyWeightedApy, + borrowBalance, + borrowWeightedApy, borrowPower, borrowPositions, }) => { @@ -48,20 +48,37 @@ export const BorrowPositionsList: FC = ({ + } /> + } /> } + value={ + + } />
diff --git a/apps/web-app/src/components/MoneyMarket/components/BorrowPositionsList/components/AssetsTable/AssetsTable.constants.tsx b/apps/web-app/src/components/MoneyMarket/components/BorrowPositionsList/components/AssetsTable/AssetsTable.constants.tsx deleted file mode 100644 index 1463265..0000000 --- a/apps/web-app/src/components/MoneyMarket/components/BorrowPositionsList/components/AssetsTable/AssetsTable.constants.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import iconTether from '@/assets/tokens/usdt.png'; -import type { BorrowPosition } from '../../BorrowPositionsList.types'; - -export const BORROW_POSITIONS: BorrowPosition[] = [ - { - symbol: 'USDT', - balance: '1.01234566', - balanceUsd: 159489.7, - apy: '15.34', - apyType: [0, 5.12], - icon: iconTether, - isSortable: true, - }, - { - symbol: 'USDT', - balance: '2.01234566', - balanceUsd: 159489.7, - apy: '4.4', - apyType: [1, 6.12], - icon: iconTether, - isSortable: true, - }, -]; diff --git a/apps/web-app/src/components/MoneyMarket/components/BorrowPositionsList/components/AssetsTable/AssetsTable.tsx b/apps/web-app/src/components/MoneyMarket/components/BorrowPositionsList/components/AssetsTable/AssetsTable.tsx index 3c8f7f1..4b11f9e 100644 --- a/apps/web-app/src/components/MoneyMarket/components/BorrowPositionsList/components/AssetsTable/AssetsTable.tsx +++ b/apps/web-app/src/components/MoneyMarket/components/BorrowPositionsList/components/AssetsTable/AssetsTable.tsx @@ -6,16 +6,8 @@ import { TableHeader, TableRow, } from '@/components/ui/table/table'; -import { - Fragment, - useCallback, - useEffect, - useMemo, - useState, - type FC, -} from 'react'; +import { Fragment, useCallback, useMemo, useState, type FC } from 'react'; -import iconSort from '@/assets/lend/icon-sort.svg'; import { AmountRenderer } from '@/components/ui/amount-renderer'; import { Button } from '@/components/ui/button'; import { InfoButton } from '@/components/ui/info-button'; @@ -26,37 +18,22 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { - OrderColumn, - OrderType, - type OrderSorting, -} from '@/components/ui/table/table.types'; +import type { MoneyMarketPoolPosition } from '@sovryn/slayer-sdk'; +import { Decimal } from '@sovryn/slayer-shared'; import type { BorrowPosition } from '../../BorrowPositionsList.types'; type AssetsTableProps = { - assets: BorrowPosition[]; + assets: MoneyMarketPoolPosition[]; }; export const AssetsTable: FC = ({ assets }) => { - const [sortDirection, setSortDirection] = useState( - OrderType.ASC, + const items = useMemo( + () => assets.filter((a) => Decimal.from(a.borrowed).gt(0)), + [assets], ); - const [sortedAssets, setSortedAssets] = useState(assets); const [selectedApy, setSelectedApy] = useState>({}); - useEffect(() => { - setSortedAssets(assets); - setSelectedApy((prev) => { - const next = { ...prev }; - assets.forEach((a, i) => { - const id = rowKey(a, i); - if (next[id] == null) next[id] = inferDefaultSelected(a); - }); - return next; - }); - }, [assets]); - const parsePct = (v: unknown): number => { if (typeof v === 'number') return v; if (typeof v === 'string') { @@ -87,49 +64,6 @@ export const AssetsTable: FC = ({ assets }) => { [selectedApy, rowKey], ); - const sortAssets = useCallback( - (column: OrderColumn) => { - const nextDir = - sortDirection === OrderType.ASC ? OrderType.DESC : OrderType.ASC; - setSortDirection(nextDir); - - const sorted = [...sortedAssets].sort((a, b) => { - switch (column) { - case OrderColumn.SYMBOL: { - const cmp = a.symbol.localeCompare(b.symbol); - return nextDir === OrderType.ASC ? cmp : -cmp; - } - case OrderColumn.BALANCE: { - const av = parseFloat(String(a.balance).replace(/,/g, '')) || 0; - const bv = parseFloat(String(b.balance).replace(/,/g, '')) || 0; - return nextDir === OrderType.ASC ? av - bv : bv - av; - } - case OrderColumn.APY: - case OrderColumn.APY_TYPE: { - const ai = assets.indexOf(a); - const bi = assets.indexOf(b); - const av = currentApy(a, ai); - const bv = currentApy(b, bi); - return nextDir === OrderType.ASC ? av - bv : bv - av; - } - default: - return 0; - } - }); - - setSortedAssets(sorted); - }, - [sortDirection, sortedAssets, assets, currentApy], - ); - - const handleApyTypeChange = useCallback( - (asset: BorrowPosition, idx: number, value: string) => { - const id = rowKey(asset, idx); - setSelectedApy((prev) => ({ ...prev, [id]: Number(value) })); - setSortedAssets((prev) => [...prev]); - }, - [rowKey], - ); return ( @@ -137,31 +71,11 @@ export const AssetsTable: FC = ({ assets }) => {
Asset - {assets.some((a) => a.isSortable) && ( - - )}
Balance - {assets.some((a) => a.isSortable) && ( - - )}
@@ -170,16 +84,6 @@ export const AssetsTable: FC = ({ assets }) => { APY - {assets.some((a) => a.isSortable) && ( - - )} @@ -188,16 +92,6 @@ export const AssetsTable: FC = ({ assets }) => { APY type - {assets.some((a) => a.isSortable) && ( - - )} @@ -205,62 +99,75 @@ export const AssetsTable: FC = ({ assets }) => {
- {sortedAssets.map((asset, index) => { - const selected = currentApy(asset, index); - const types = (asset.apyType ?? []) - .map(Number) - .filter(Number.isFinite); - const options = [selected, ...types.filter((t) => t !== selected)]; - + {items.map((asset, index) => { return ( - +
{asset.symbol}
-

{asset.symbol}

+

+ {asset.token.symbol} +

- -

- -

+
+ + +
- +
@@ -278,7 +185,7 @@ export const AssetsTable: FC = ({ assets }) => {
- {index !== sortedAssets.length - 1 && ( + {index !== items.length - 1 && ( @@ -287,7 +194,7 @@ export const AssetsTable: FC = ({ assets }) => { ); })} - {sortedAssets.length === 0 && ( + {items.length === 0 && ( No assets found. diff --git a/apps/web-app/src/components/MoneyMarket/components/LendAssetsList/LendAssetsList.tsx b/apps/web-app/src/components/MoneyMarket/components/LendAssetsList/LendAssetsList.tsx index 571e884..97369b4 100644 --- a/apps/web-app/src/components/MoneyMarket/components/LendAssetsList/LendAssetsList.tsx +++ b/apps/web-app/src/components/MoneyMarket/components/LendAssetsList/LendAssetsList.tsx @@ -1,8 +1,6 @@ import { Accordion } from '@/components/ui/accordion'; -import { Checkbox } from '@/components/ui/checkbox'; -import { Label } from '@/components/ui/label'; import type { MoneyMarketPoolReserve } from '@sovryn/slayer-sdk'; -import { useCallback, useMemo, useState, type FC } from 'react'; +import { useState, type FC } from 'react'; import { AssetsTable } from './components/AssetsTable/AssetsTable'; type LendPAssetsListProps = { @@ -12,17 +10,6 @@ type LendPAssetsListProps = { export const LendAssetsList: FC = ({ lendAssets }) => { const [open, setOpen] = useState(false); - const [showZeroBalances, setShowZeroBalances] = useState(false); - - const handleShowBalances = useCallback( - () => setShowZeroBalances((prevState) => !prevState), - [], - ); - - const filteredAssets = useMemo( - () => lendAssets, - [lendAssets, showZeroBalances], - ); return ( = ({ lendAssets }) => { open={open} onClick={setOpen} > -
-
- - -
-
- - +
); }; diff --git a/apps/web-app/src/components/MoneyMarket/components/LendAssetsList/components/AssetsTable/AssetsTable.tsx b/apps/web-app/src/components/MoneyMarket/components/LendAssetsList/components/AssetsTable/AssetsTable.tsx index e4e9f0b..63b0f51 100644 --- a/apps/web-app/src/components/MoneyMarket/components/LendAssetsList/components/AssetsTable/AssetsTable.tsx +++ b/apps/web-app/src/components/MoneyMarket/components/LendAssetsList/components/AssetsTable/AssetsTable.tsx @@ -20,45 +20,6 @@ type AssetsTableProps = { }; export const AssetsTable: FC = ({ assets }) => { - // const [sortDirection, setSortDirection] = useState( - // OrderType.ASC, - // ); - // const [sortedAssets, setSortedAssets] = - // useState(assets); - // useEffect(() => { - // setSortedAssets(assets); - // }, [assets]); - - // const sortAssets = useCallback( - // (column: keyof MoneyMarketPoolReserve) => { - // const newSortDirection = - // sortDirection === OrderType.ASC ? OrderType.DESC : OrderType.ASC; - // setSortDirection(newSortDirection); - - // // const sorted = [...sortedAssets].sort((a, b) => { - // // if (column === OrderColumn.SYMBOL) { - // // return newSortDirection === OrderType.ASC - // // ? a[column].localeCompare(b[column]) - // // : b[column].localeCompare(a[column]); - // // } else if (column === OrderColumn.BALANCE) { - // // const balanceA = parseFloat(a.balance.replace(/,/g, '')); - // // const balanceB = parseFloat(b.balance.replace(/,/g, '')); - // // return newSortDirection === OrderType.ASC - // // ? balanceA - balanceB - // // : balanceB - balanceA; - // // } else if (column === OrderColumn.APY) { - // // const apyA = parseFloat(a.apy.replace('%', '')); - // // const apyB = parseFloat(b.apy.replace('%', '')); - // // return newSortDirection === OrderType.ASC ? apyA - apyB : apyB - apyA; - // // } - // // return 0; - // // }); - - // // setSortedAssets(sorted); - // }, - // [sortDirection], - // ); - const handleLending = (reserve: MoneyMarketPoolReserve) => lendRequestStore.getState().setReserve(reserve); @@ -69,31 +30,11 @@ export const AssetsTable: FC = ({ assets }) => {
Asset - {/* {assets.some((asset) => asset.isSortable) && ( - - )} */}
- Wallet balance - {/* {assets.some((asset) => asset.isSortable) && ( - - )} */} + Liquidity
@@ -102,16 +43,6 @@ export const AssetsTable: FC = ({ assets }) => { APY - {/* {assets.some((asset) => asset.isSortable) && ( - - )} */} @@ -141,16 +72,27 @@ export const AssetsTable: FC = ({ assets }) => {
- +
+ + +
- +
- {asset.usageAsCollateralEnabled ? ( + {asset.canBeCollateral ? ( ) : ( diff --git a/apps/web-app/src/components/MoneyMarket/components/LendDialog/LendDialog.tsx b/apps/web-app/src/components/MoneyMarket/components/LendDialog/LendDialog.tsx index bee5c4c..4641a89 100644 --- a/apps/web-app/src/components/MoneyMarket/components/LendDialog/LendDialog.tsx +++ b/apps/web-app/src/components/MoneyMarket/components/LendDialog/LendDialog.tsx @@ -24,7 +24,6 @@ const LendDialogForm = () => { const { begin } = useSlayerTx({ onClosed: (ok: boolean) => { - console.log('lend tx modal closed, success:', ok); if (ok) { // close lending dialog if tx was successful lendRequestStore.getState().reset(); @@ -99,7 +98,7 @@ const LendDialogForm = () => {

{reserve.token.symbol} can be used as collateral:{' '} - {reserve.usageAsCollateralEnabled ? 'Yes' : 'No'} + {reserve.canBeCollateral ? 'Yes' : 'No'}

diff --git a/apps/web-app/src/components/MoneyMarket/components/LendPositionsList/LendPositionsList.tsx b/apps/web-app/src/components/MoneyMarket/components/LendPositionsList/LendPositionsList.tsx index bfc8f32..a75df59 100644 --- a/apps/web-app/src/components/MoneyMarket/components/LendPositionsList/LendPositionsList.tsx +++ b/apps/web-app/src/components/MoneyMarket/components/LendPositionsList/LendPositionsList.tsx @@ -1,15 +1,15 @@ import { Accordion } from '@/components/ui/accordion'; +import type { MoneyMarketPoolPosition } from '@sovryn/slayer-sdk'; import { useState, type FC } from 'react'; import { AmountRenderer } from '../../../ui/amount-renderer'; import { PoolPositionStat } from '../PoolPositionStat/PoolPositionStat'; import { AssetsTable } from './components/AssetsTable/AssetsTable'; -import type { LendPosition } from './LendPositionsList.types'; type LendPositionsListProps = { - supplyBalance: number; - supplyWeightedApy: number; - collateralBalance: number; - lendPositions: LendPosition[]; + supplyBalance: string; + supplyWeightedApy: string; + collateralBalance: string; + lendPositions: MoneyMarketPoolPosition[]; loading?: boolean; }; @@ -19,7 +19,7 @@ export const LendPositionsList: FC = ({ collateralBalance, lendPositions, }) => { - const [open, setOpen] = useState(false); + const [open, setOpen] = useState(true); return ( = ({ assets }) => { - const [sortDirection, setSortDirection] = useState( - OrderType.ASC, - ); - const [sortedAssets, setSortedAssets] = useState(assets); - useEffect(() => { - setSortedAssets(assets); - }, [assets]); - - const sortAssets = useCallback( - (column: keyof LendPosition) => { - const newSortDirection = - sortDirection === OrderType.ASC ? OrderType.DESC : OrderType.ASC; - setSortDirection(newSortDirection); - - const sorted = [...sortedAssets].sort((a, b) => { - if (column === OrderColumn.SYMBOL) { - return newSortDirection === OrderType.ASC - ? a[column].localeCompare(b[column]) - : b[column].localeCompare(a[column]); - } else if (column === OrderColumn.BALANCE) { - const balanceA = parseFloat(a.balance.replace(/,/g, '')); - const balanceB = parseFloat(b.balance.replace(/,/g, '')); - return newSortDirection === OrderType.ASC - ? balanceA - balanceB - : balanceB - balanceA; - } else if (column === OrderColumn.APY) { - const apyA = parseFloat(a.apy.replace('%', '')); - const apyB = parseFloat(b.apy.replace('%', '')); - return newSortDirection === OrderType.ASC ? apyA - apyB : apyB - apyA; - } - return 0; - }); - - setSortedAssets(sorted); - }, - [sortDirection], + const items = useMemo( + () => assets.filter((asset) => Decimal.from(asset.supplied).gt(0)), + [assets], ); const toggleCollateral = useCallback((symbol: string) => { - setSortedAssets((prevAssets) => - prevAssets.map((asset) => - asset.symbol === symbol - ? { ...asset, collateral: !asset.collateral } - : asset, - ), - ); + // setSortedAssets((prevAssets) => + // prevAssets.map((asset) => + // asset.symbol === symbol + // ? { ...asset, collateral: !asset.collateral } + // : asset, + // ), + // ); }, []); return ( @@ -80,31 +42,11 @@ export const AssetsTable: FC = ({ assets }) => {
Asset - {assets.some((asset) => asset.isSortable) && ( - - )}
Balance - {assets.some((asset) => asset.isSortable) && ( - - )}
@@ -113,16 +55,6 @@ export const AssetsTable: FC = ({ assets }) => { APY
- {assets.some((asset) => asset.isSortable) && ( - - )} @@ -134,26 +66,31 @@ export const AssetsTable: FC = ({ assets }) => {
- {sortedAssets.map((asset, index) => ( - + {items.map((asset, index) => ( +
{asset.symbol}
-

{asset.symbol}

+

+ {asset.token.symbol} +

- +

@@ -161,7 +98,12 @@ export const AssetsTable: FC = ({ assets }) => {

-

{asset.apy}

+
@@ -169,9 +111,9 @@ export const AssetsTable: FC = ({ assets }) => { toggleCollateral(asset.symbol)} - disabled={!asset.canToggleCollateral} + id={`collateral-${asset.token.address}`} + onClick={() => toggleCollateral(asset.id)} + // disabled={!asset} /> @@ -187,14 +129,14 @@ export const AssetsTable: FC = ({ assets }) => {
- {index !== sortedAssets.length - 1 && ( + {index !== items.length - 1 && ( )}
))} - {sortedAssets.length === 0 && ( + {items.length === 0 && ( No assets found. diff --git a/apps/web-app/src/components/MoneyMarket/components/TopPanel/TopPanel.tsx b/apps/web-app/src/components/MoneyMarket/components/TopPanel/TopPanel.tsx index a11de77..a9db8ad 100644 --- a/apps/web-app/src/components/MoneyMarket/components/TopPanel/TopPanel.tsx +++ b/apps/web-app/src/components/MoneyMarket/components/TopPanel/TopPanel.tsx @@ -1,17 +1,20 @@ +import { Skeleton } from '@/components/ui/skeleton'; import type { FC } from 'react'; import { AmountRenderer } from '../../../ui/amount-renderer'; import { StatisticsCard } from '../StatisticsCard/StatisticsCard'; type TopPanelProps = { - netWorth: number; - netApy: number; - healthFactor: number; + netWorth: string; + netApy: string; + healthFactor: string; + isPending?: boolean; }; export const TopPanel: FC = ({ netApy, netWorth, healthFactor, + isPending, }) => (
@@ -20,7 +23,11 @@ export const TopPanel: FC = ({ label="Net Worth" value={ - + {isPending ? ( + + ) : ( + + )} } /> @@ -29,7 +36,11 @@ export const TopPanel: FC = ({ label="Net APY" value={ - + {isPending ? ( + + ) : ( + + )} } help="Net APY is the combined effect of all supply and borrow positions on net worth, including incentives. It is possible to have a negative net APY if debt APY is higher than supply APY." @@ -38,7 +49,11 @@ export const TopPanel: FC = ({ label="Health Factor" value={ - + {isPending ? ( + + ) : ( + + )} } /> diff --git a/apps/web-app/src/components/ui/select.tsx b/apps/web-app/src/components/ui/select.tsx index 47884d1..28552d1 100644 --- a/apps/web-app/src/components/ui/select.tsx +++ b/apps/web-app/src/components/ui/select.tsx @@ -1,6 +1,6 @@ -import * as React from 'react'; import * as SelectPrimitive from '@radix-ui/react-select'; import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from 'lucide-react'; +import * as React from 'react'; import { cn } from '@/lib/utils'; diff --git a/apps/web-app/src/integrations/wagmi/config.ts b/apps/web-app/src/integrations/wagmi/config.ts index 5909633..4146190 100644 --- a/apps/web-app/src/integrations/wagmi/config.ts +++ b/apps/web-app/src/integrations/wagmi/config.ts @@ -7,6 +7,7 @@ import { rootstockTestnet, sepolia, } from 'viem/chains'; +import { getAccount } from 'wagmi/actions'; export const config = createConfig({ chains: [mainnet, sepolia, bobSepolia, rootstock, rootstockTestnet], @@ -19,6 +20,8 @@ export const config = createConfig({ }, }); +export const getConnection = () => getAccount(config); + declare module 'wagmi' { interface Register { config: typeof config; diff --git a/apps/web-app/src/main.tsx b/apps/web-app/src/main.tsx index fc807ef..418afd4 100644 --- a/apps/web-app/src/main.tsx +++ b/apps/web-app/src/main.tsx @@ -9,6 +9,7 @@ import * as Wagmi from './integrations/wagmi/root-provider.tsx'; // Import the generated route tree import { routeTree } from './routeTree.gen'; +import { getConnection } from './integrations/wagmi/config.ts'; import reportWebVitals from './reportWebVitals.ts'; import './styles.css'; @@ -19,6 +20,7 @@ const router = createRouter({ routeTree, context: { ...TanStackQueryProviderContext, + connection: getConnection, }, defaultPreload: 'intent', scrollRestoration: true, diff --git a/apps/web-app/src/routes/__root.tsx b/apps/web-app/src/routes/__root.tsx index 0eeab2a..4a279d2 100644 --- a/apps/web-app/src/routes/__root.tsx +++ b/apps/web-app/src/routes/__root.tsx @@ -21,6 +21,7 @@ import { useEffect, type PropsWithChildren } from 'react'; interface MyRouterContext { queryClient: QueryClient; + connection: typeof import('@/integrations/wagmi/config').getConnection; } export const Route = createRootRouteWithContext()({ diff --git a/apps/web-app/src/routes/money-market.tsx b/apps/web-app/src/routes/money-market.tsx index 2017e0a..64f3454 100644 --- a/apps/web-app/src/routes/money-market.tsx +++ b/apps/web-app/src/routes/money-market.tsx @@ -6,20 +6,13 @@ import { TopPanel } from '@/components/MoneyMarket/components/TopPanel/TopPanel' import { BorrowAssetsList } from '@/components/MoneyMarket/components/BorrowAssetsList/BorrowAssetsList'; import { BorrowDialog } from '@/components/MoneyMarket/components/BorrowDialog/BorrowDialog'; import { BorrowPositionsList } from '@/components/MoneyMarket/components/BorrowPositionsList/BorrowPositionsList'; -import { BORROW_POSITIONS } from '@/components/MoneyMarket/components/BorrowPositionsList/components/AssetsTable/AssetsTable.constants'; import { LendAssetsList } from '@/components/MoneyMarket/components/LendAssetsList/LendAssetsList'; import { LendDialog } from '@/components/MoneyMarket/components/LendDialog/LendDialog'; -import { LEND_POSITIONS } from '@/components/MoneyMarket/components/LendPositionsList/components/AssetsTable/AssetsTable.constants'; -import { - healthFactor, - netApy, - netWorth, -} from '@/components/MoneyMarket/MoneyMarket.constants'; import { Heading } from '@/components/ui/heading/heading'; -import { getContext } from '@/integrations/tanstack-query/root-provider'; import { sdk } from '@/lib/sdk'; import { useQuery } from '@tanstack/react-query'; import { useMemo } from 'react'; +import { useAccount } from 'wagmi'; import z from 'zod'; const STALE_TIME = 1000 * 60 * 60; // 1 hour @@ -40,8 +33,8 @@ export const Route = createFileRoute('/money-market')({ search, pool, }), - loader: ({ deps: { pool } }) => { - const client = getContext().queryClient; + loader: ({ deps: { pool }, context }) => { + const client = context.queryClient; client.prefetchQuery({ queryKey: ['money-market:pools'], queryFn: () => sdk.moneyMarket.listPools(), @@ -53,11 +46,22 @@ export const Route = createFileRoute('/money-market')({ queryFn: () => sdk.moneyMarket.listReserves(pool || 'default'), staleTime: STALE_TIME, }); + + const owner = context.connection().address; + if (owner) { + client.prefetchQuery({ + queryKey: ['money-market:positions', pool || 'default', owner], + queryFn: () => + sdk.moneyMarket.listUserPositions(pool || 'default', owner), + staleTime: STALE_TIME, + }); + } }, }); function RouteComponent() { const { pool } = Route.useLoaderDeps(); + const { address } = useAccount(); // const { data: pools } = useQuery({ // queryKey: ['money-market:pools'], @@ -71,8 +75,16 @@ function RouteComponent() { staleTime: STALE_TIME, }); + const { data: positions, isPending } = useQuery({ + queryKey: ['money-market:positions', pool || 'default', address], + queryFn: () => + sdk.moneyMarket.listUserPositions(pool || 'default', address!), + staleTime: STALE_TIME, + enabled: !!address, + }); + const borrowAssets = useMemo( - () => (reserves?.data ?? []).filter((r) => r.borrowingEnabled), + () => (reserves?.data ?? []).filter((r) => r.canBeBorrowed), [reserves], ); @@ -87,27 +99,34 @@ function RouteComponent() {
diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 0dda667..e78fa1b 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -16,8 +16,8 @@ } }, "dependencies": { - "@aave/contract-helpers": "^1.36.2", - "@aave/math-utils": "^1.36.2", + "@aave/contract-helpers": "1.29.1", + "@aave/math-utils": "1.29.1", "@sovryn/slayer-shared": "workspace:*", "debug": "4.4.3", "mobx": "^6.15.0", diff --git a/packages/sdk/src/constants.ts b/packages/sdk/src/constants.ts index 60de281..8044bfe 100644 --- a/packages/sdk/src/constants.ts +++ b/packages/sdk/src/constants.ts @@ -8,6 +8,8 @@ export type Mode = (typeof modes)[keyof typeof modes]; export const DEFAULT_PAGE_LIMIT = 100; +export const USD_DECIMALS = 8; + export const INDEXER_URL = { [modes.production]: 'https://slayer-indexer.sovryn.app', [modes.staging]: 'https://slayer-indexer.staging.sovryn.app', diff --git a/packages/sdk/src/managers/money-market/money-market.manager.ts b/packages/sdk/src/managers/money-market/money-market.manager.ts index 5f285ab..9d4434a 100644 --- a/packages/sdk/src/managers/money-market/money-market.manager.ts +++ b/packages/sdk/src/managers/money-market/money-market.manager.ts @@ -1,5 +1,5 @@ import { areAddressesEqual, Decimal, Decimalish } from '@sovryn/slayer-shared'; -import { Account, encodeFunctionData, type Chain } from 'viem'; +import { Account, Address, encodeFunctionData, type Chain } from 'viem'; import { BaseClient, type SdkRequestOptions } from '../../lib/context.js'; import { buildQuery, toAddress } from '../../lib/helpers.js'; import { @@ -9,7 +9,9 @@ import { import { BorrowRateMode, MoneyMarketPool, + MoneyMarketPoolPosition, MoneyMarketPoolReserve, + MoneyMarketUserSummary, SdkPaginatedResponse, TransactionOpts, } from '../../types.js'; @@ -125,6 +127,24 @@ export class MoneyMarketManager extends BaseClient { }; } + async listUserPositions( + pool: MoneyMarketPool['id'], + user: Address, + opts: SdkRequestOptions = {}, + ) { + const response = await this.ctx.http.request<{ + data: { + positions: MoneyMarketPoolPosition[]; + summary: MoneyMarketUserSummary; + }; + }>(`/${this.ctx.chainId}/money-market/${pool}/user/${user}/positions`, { + ...opts, + query: buildQuery(opts.query), + }); + + return response; + } + async borrow( reserve: MoneyMarketPoolReserve, amount: Decimalish, diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index 2440d69..4b87fd2 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -34,25 +34,22 @@ export type Token = Pick & export interface MoneyMarketPoolReserve { id: string; - originalId: number; + // originalId: number; token: SdkToken; pool: MoneyMarketPool; - availableLiquidity: string; - baseLTVasCollateral: string; + liquidity: string; + liquidityUsd: string; - borrowingEnabled: boolean; - usageAsCollateralEnabled: boolean; + stableBorrowApy: string; + variableBorrowApy: string; + supplyApy: string; + + canBeBorrowed: boolean; + canBeCollateral: boolean; isActive: boolean; isFrozen: boolean; - - liquidityRate: string; - variableBorrowRate: string; - - reserveFactor: string; - reserveLiquidationBonus: string; - reserveLiquidationThreshold: string; } export interface MoneyMarketPool { @@ -77,3 +74,56 @@ export const BORROW_RATE_MODES = { export type BorrowRateMode = (typeof BORROW_RATE_MODES)[keyof typeof BORROW_RATE_MODES]; + +export type MoneyMarketPoolPosition = { + id: string; + pool: MoneyMarketPool; + token: SdkToken; + reserve: Omit; + + supplied: string; + suppliedUsd: string; + + supplyApy: string; + canToggleCollateral: boolean; + + borrowed: string; + borrowedUsd: string; + + borrowApy: string; + stableBorrowApy: string; + variableBorrowApy: string; + + collateral: boolean; + + availableToBorrow: string; + availableToBorrowUsd: string; + + borrowRateMode: BorrowRateMode; +}; + +export type MoneyMarketUserSummary = { + netApy: string; + healthFactor: string; + collateralRatio: string; + borrowPower: string; + borrowPowerUsed: string; + + totalLiquidityUsd: string; + totalCollateralUsd: string; + totalBorrowsUsd: string; + availableBorrowsUsd: string; + + currentLoanToValue: string; + currentLiquidationThreshold: string; + + supplyBalanceUsd: string; + collateralBalanceUsd: string; + + netWorthUsd: string; + userEmodeCategoryId: number | null; + isInIsolationMode: boolean; + + borrowWeightedApy: string; + supplyWeightedApy: string; +}; diff --git a/packages/shared/src/lib/decimal.ts b/packages/shared/src/lib/decimal.ts index 50337f4..7d9569f 100644 --- a/packages/shared/src/lib/decimal.ts +++ b/packages/shared/src/lib/decimal.ts @@ -62,6 +62,10 @@ export class Decimal { throw new Error(`Cannot convert to Decimal: ${value}`); } + static pow(exponent: number): Decimal { + return Decimal.from(1).mul(10 ** exponent); + } + toBigInt(): bigint { return BigInt(this.d.mul(new D(10).pow(this.precision)).toFixed(0)); } diff --git a/packages/shared/src/lib/http-client.ts b/packages/shared/src/lib/http-client.ts index b0d94cb..ee296c0 100644 --- a/packages/shared/src/lib/http-client.ts +++ b/packages/shared/src/lib/http-client.ts @@ -95,6 +95,7 @@ export class HttpClient { if (isTimeout) throw new HTTPTimeoutError(url, reason); throw new HTTPAbortError(url, reason); } + console.error('HTTP request failed', { error, url }); throw new HTTPClientError(500, 'Internal Server Error', '', url); } finally { if (timeout) clearTimeout(timeout); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 61b625d..46cf34d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -152,6 +152,12 @@ importers: apps/indexer: dependencies: + '@aave/contract-helpers': + specifier: 1.29.1 + version: 1.29.1(bignumber.js@9.3.1)(encoding@0.1.13)(ethers@5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(reflect-metadata@0.1.14)(tslib@2.8.1) + '@aave/math-utils': + specifier: 1.29.1 + version: 1.29.1(bignumber.js@9.3.1)(tslib@2.8.1) '@bull-board/api': specifier: 6.13.0 version: 6.13.0(@bull-board/ui@6.13.0) @@ -321,6 +327,9 @@ importers: '@tanstack/router-plugin': specifier: 1.131.50 version: 1.131.50(@tanstack/react-router@1.131.50(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@7.1.7(@types/node@20.19.9)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))(webpack@5.103.0(@swc/core@1.5.29(@swc/helpers@0.5.17))(esbuild@0.25.0)) + '@wagmi/core': + specifier: ^3.0.0 + version: 3.0.0(@tanstack/query-core@5.90.2)(@types/react@19.1.13)(immer@10.2.0)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.6.0(react@19.1.1))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11)) class-variance-authority: specifier: 0.7.1 version: 0.7.1 @@ -407,11 +416,11 @@ importers: packages/sdk: dependencies: '@aave/contract-helpers': - specifier: ^1.36.2 - version: 1.36.2(bignumber.js@9.3.1)(encoding@0.1.13)(ethers@5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(reflect-metadata@0.1.14)(tslib@2.8.1) + specifier: 1.29.1 + version: 1.29.1(bignumber.js@9.3.1)(encoding@0.1.13)(ethers@5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(reflect-metadata@0.1.14)(tslib@2.8.1) '@aave/math-utils': - specifier: ^1.36.2 - version: 1.36.2(bignumber.js@9.3.1)(tslib@2.8.1) + specifier: 1.29.1 + version: 1.29.1(bignumber.js@9.3.1)(tslib@2.8.1) '@sovryn/slayer-shared': specifier: workspace:* version: link:../shared @@ -443,16 +452,16 @@ importers: packages: - '@aave/contract-helpers@1.36.2': - resolution: {integrity: sha512-g0z9QbppGHDXSu13OPmyMkHAw1UjCAD2xoUWO+8Vk1xT9ZEwZaT7Pn2sojwUp97FyQiiKbjmJ4KupNS4jp33GQ==} + '@aave/contract-helpers@1.29.1': + resolution: {integrity: sha512-34z5CKpNdEx26G+DSezovdR3PyAf0v0Typbf2udaKG4GrOBgqbKqxr4zqS/dpFO5aAQJJ+nuEM19lKRK4sMcpg==} peerDependencies: bignumber.js: ^9.x ethers: ^5.x reflect-metadata: ^0.1.x tslib: ^2.4.x - '@aave/math-utils@1.36.2': - resolution: {integrity: sha512-r2SnPyK7MdhlfwTTj9Wq1idRhS1dyErJKeP98MexohRhzhWzsB6Uh3/gD6iK5mZc28V1mxhnUPTi1kD+mO6+/Q==} + '@aave/math-utils@1.29.1': + resolution: {integrity: sha512-a+L2+vjza/nH4BxUTzwDcoJH/Qr6UP+g+9YSwG7YKUgoVfdy8gJs5VyXjfDDEVe9lMeZuPJq7CqrgV4P2M6Nkg==} peerDependencies: bignumber.js: ^9.x tslib: ^2.4.x @@ -4932,6 +4941,18 @@ packages: typescript: optional: true + '@wagmi/core@3.0.0': + resolution: {integrity: sha512-wOn8jwB9GNYTdrc4CP/huf1aAhDoQ5GKl5OhxGBZx9X4qE+wReW05dTcurEc+XBl9B/ZVis2JdXVU3ZiYqyS8Q==} + peerDependencies: + '@tanstack/query-core': '>=5.0.0' + typescript: '>=5.7.3' + viem: 2.38.6 + peerDependenciesMeta: + '@tanstack/query-core': + optional: true + typescript: + optional: true + '@wallet-standard/app@1.1.0': resolution: {integrity: sha512-3CijvrO9utx598kjr45hTbbeeykQrQfKmSnxeWOgU25TOEpvcipD/bYDQWIqUv1Oc6KK4YStokSMu/FBNecGUQ==} engines: {node: '>=16'} @@ -11132,7 +11153,7 @@ packages: snapshots: - '@aave/contract-helpers@1.36.2(bignumber.js@9.3.1)(encoding@0.1.13)(ethers@5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(reflect-metadata@0.1.14)(tslib@2.8.1)': + '@aave/contract-helpers@1.29.1(bignumber.js@9.3.1)(encoding@0.1.13)(ethers@5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(reflect-metadata@0.1.14)(tslib@2.8.1)': dependencies: bignumber.js: 9.3.1 ethers: 5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) @@ -11142,7 +11163,7 @@ snapshots: transitivePeerDependencies: - encoding - '@aave/math-utils@1.36.2(bignumber.js@9.3.1)(tslib@2.8.1)': + '@aave/math-utils@1.29.1(bignumber.js@9.3.1)(tslib@2.8.1)': dependencies: bignumber.js: 9.3.1 tslib: 2.8.1 @@ -16903,7 +16924,7 @@ snapshots: '@metamask/sdk': 0.33.1(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10) '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11) '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.2)(@types/react@19.1.13)(immer@10.2.0)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.6.0(react@19.1.1))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11)) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.2)(@types/react@19.1.13)(immer@10.2.0)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.1.1))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11)) '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.1.13)(bufferutil@4.0.9)(encoding@0.1.13)(ioredis@5.8.0)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11) cbw-sdk: '@coinbase/wallet-sdk@3.9.3' porto: 0.2.35(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.13)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.2)(@types/react@19.1.13)(immer@10.2.0)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.1.1))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11)))(immer@10.2.0)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.1.1))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11))(wagmi@2.19.2(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.13)(bufferutil@4.0.9)(debug@4.4.3)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.2.0)(ioredis@5.8.0)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.11)) @@ -16949,7 +16970,22 @@ snapshots: - ws - zod - '@wagmi/core@2.22.1(@tanstack/query-core@5.90.2)(@types/react@19.1.13)(immer@10.2.0)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.6.0(react@19.1.1))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11))': + '@wagmi/core@2.22.1(@tanstack/query-core@5.90.2)(@types/react@19.1.13)(immer@10.2.0)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.1.1))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11))': + dependencies: + eventemitter3: 5.0.1 + mipd: 0.0.7(typescript@5.9.2) + viem: 2.38.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11) + zustand: 5.0.0(@types/react@19.1.13)(immer@10.2.0)(react@19.1.1)(use-sync-external-store@1.4.0(react@19.1.1)) + optionalDependencies: + '@tanstack/query-core': 5.90.2 + typescript: 5.9.2 + transitivePeerDependencies: + - '@types/react' + - immer + - react + - use-sync-external-store + + '@wagmi/core@3.0.0(@tanstack/query-core@5.90.2)(@types/react@19.1.13)(immer@10.2.0)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.6.0(react@19.1.1))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11))': dependencies: eventemitter3: 5.0.1 mipd: 0.0.7(typescript@5.9.2) @@ -19950,7 +19986,7 @@ snapshots: extension-port-stream@3.0.0: dependencies: - readable-stream: 3.6.2 + readable-stream: 4.7.0 webextension-polyfill: 0.10.0 eyes@0.1.8: {} @@ -22349,7 +22385,7 @@ snapshots: porto@0.2.35(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.13)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.2)(@types/react@19.1.13)(immer@10.2.0)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.1.1))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11)))(immer@10.2.0)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.1.1))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11))(wagmi@2.19.2(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.13)(bufferutil@4.0.9)(debug@4.4.3)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.2.0)(ioredis@5.8.0)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.11)): dependencies: - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.2)(@types/react@19.1.13)(immer@10.2.0)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.6.0(react@19.1.1))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11)) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.2)(@types/react@19.1.13)(immer@10.2.0)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.1.1))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11)) hono: 4.10.6 idb-keyval: 6.2.2 mipd: 0.0.7(typescript@5.9.2) @@ -24448,7 +24484,7 @@ snapshots: dependencies: '@tanstack/react-query': 5.90.2(react@19.1.1) '@wagmi/connectors': 6.1.3(e2df5ec3edd1d01c0c5b6685ed869aa8) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.2)(@types/react@19.1.13)(immer@10.2.0)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.6.0(react@19.1.1))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11)) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.2)(@types/react@19.1.13)(immer@10.2.0)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.1.1))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11)) react: 19.1.1 use-sync-external-store: 1.4.0(react@19.1.1) viem: 2.38.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11) @@ -24736,6 +24772,13 @@ snapshots: zod@4.1.11: {} + zustand@5.0.0(@types/react@19.1.13)(immer@10.2.0)(react@19.1.1)(use-sync-external-store@1.4.0(react@19.1.1)): + optionalDependencies: + '@types/react': 19.1.13 + immer: 10.2.0 + react: 19.1.1 + use-sync-external-store: 1.4.0(react@19.1.1) + zustand@5.0.0(@types/react@19.1.13)(immer@10.2.0)(react@19.1.1)(use-sync-external-store@1.6.0(react@19.1.1)): optionalDependencies: '@types/react': 19.1.13