diff --git a/packages/indexer-api/src/controllers/balances.ts b/packages/indexer-api/src/controllers/balances.ts index 2699c6d1..5001304c 100644 --- a/packages/indexer-api/src/controllers/balances.ts +++ b/packages/indexer-api/src/controllers/balances.ts @@ -22,11 +22,16 @@ export class BalancesController { }; public getSpokePoolBalance = ( - req: Request, + { query }: Request, res: Response, next: NextFunction, ) => { - req.query && s.assert(req.query, SpokePoolBalanceParams); - res.json([]); + if (!s.is(query, SpokePoolBalanceParams)) { + return res.status(400).json({ error: "Invalid query" }); + } + this.service + .spokePoolBalance(query) + .then((result) => res.json(result)) + .catch((err) => next(err)); }; } diff --git a/packages/indexer-api/src/dtos/balances.dto.ts b/packages/indexer-api/src/dtos/balances.dto.ts index 3c6efa00..6237913f 100644 --- a/packages/indexer-api/src/dtos/balances.dto.ts +++ b/packages/indexer-api/src/dtos/balances.dto.ts @@ -8,8 +8,19 @@ export const HubPoolBalanceQueryParams = s.object({ // query spokepools by chainId, must specify export const SpokePoolBalanceParams = s.object({ chainId: s.number(), + l1Token: s.optional(s.string()), // unsure why we have timestamp, implies we are storign history of balances? this is in the spec. timestamp: s.number(), - // unsure why specified as l2Token in spec, don't we have spoke pool on L1? - l2Token: s.optional(s.number()), }); + +export type SpokePoolBalanceResultElement = { + lastExecutedRunningBalance: string; + pendingRunningBalance: string | null; + pendingNetSendAmount: string | null; + currentRunningBalance: string; + currentNetSendAmount: string; +}; + +export type SpokePoolBalanceResults = { + [chainId: string]: SpokePoolBalanceResultElement; +}; diff --git a/packages/indexer-api/src/services/balances.ts b/packages/indexer-api/src/services/balances.ts index 0d0b962c..e5dfa739 100644 --- a/packages/indexer-api/src/services/balances.ts +++ b/packages/indexer-api/src/services/balances.ts @@ -1,15 +1,31 @@ import assert from "assert"; import Redis from "ioredis"; import * as Indexer from "@repo/indexer"; +import { + SpokePoolBalanceResultElement, + SpokePoolBalanceResults, +} from "../dtos/balances.dto"; export class BalancesService { hubBalancesCache: Indexer.redis.hubBalancesCache.HubPoolBalanceCache; - constructor(private redis: Redis) { + currentBundleLeavesCache: Indexer.redis.bundleLeavesCache.BundleLeavesCache; + proposedBundleLeavesCache: Indexer.redis.bundleLeavesCache.BundleLeavesCache; + constructor(redis: Redis) { this.hubBalancesCache = new Indexer.redis.hubBalancesCache.HubPoolBalanceCache({ redis, prefix: "hubBalanceCache", }); + this.currentBundleLeavesCache = + new Indexer.redis.bundleLeavesCache.BundleLeavesCache({ + redis, + prefix: "currentBundleCache", + }); + this.proposedBundleLeavesCache = + new Indexer.redis.bundleLeavesCache.BundleLeavesCache({ + redis, + prefix: "proposedBundleCache", + }); } async hubPoolBalance(params?: { l1Token?: string; @@ -22,4 +38,71 @@ export class BalancesService { return this.hubBalancesCache.getAllL1Tokens(); } } + + async spokePoolBalance(params: { + chainId: number; + l1Token?: string; + }): Promise { + const { l1Token, chainId } = params; + const bundleLeaves = await resolveAllL1Tokens( + l1Token, + chainId, + this.proposedBundleLeavesCache, + this.currentBundleLeavesCache, + ); + return bundleLeaves.reduce( + (acc, bundleLeaf) => ({ + ...acc, + [bundleLeaf.chainId]: { + lastExecutedRunningBalance: bundleLeaf.lastExecutedRunningBalance, + pendingRunningBalance: bundleLeaf.pendingRunningBalance, + pendingNetSendAmount: bundleLeaf.pendingNetSendAmount, + currentRunningBalance: bundleLeaf.currentRunningBalance, + currentNetSendAmount: bundleLeaf.currentNetSendAmount, + }, + }), + {} as SpokePoolBalanceResults, + ); + } +} + +function combineBundleLeaf( + proposedBundleLeaf?: Indexer.redis.bundleLeavesCache.BundleLeaf, + currentBundleLeaf?: Indexer.redis.bundleLeavesCache.BundleLeaf, +): SpokePoolBalanceResultElement & { chainId: number } { + assert(currentBundleLeaf, "currentBundleLeaf is required"); + return { + chainId: currentBundleLeaf.chainId, + lastExecutedRunningBalance: currentBundleLeaf.lastExecutedRunningBalance, + pendingRunningBalance: proposedBundleLeaf?.runningBalance ?? null, + pendingNetSendAmount: proposedBundleLeaf?.netSendAmount ?? null, + currentRunningBalance: currentBundleLeaf.runningBalance, + currentNetSendAmount: currentBundleLeaf.netSendAmount, + }; +} + +async function resolveAllL1Tokens( + l1Token: string | undefined, + chainId: number, + proposedCache: Indexer.redis.bundleLeavesCache.BundleLeavesCache, + currentCache: Indexer.redis.bundleLeavesCache.BundleLeavesCache, +): Promise<(SpokePoolBalanceResultElement & { chainId: number })[]> { + if (l1Token) { + const currentL1Cache = await currentCache.get(chainId, l1Token); + const proposedL1Cache = await proposedCache.get(chainId, l1Token); + return [combineBundleLeaf(proposedL1Cache, currentL1Cache)]; + } else { + const currentL1Cache = await currentCache.getByChainId(chainId); + return Promise.all( + currentL1Cache + .filter((v) => v !== undefined) + .map(async (currentCacheValue) => { + const proposedCacheValue = await proposedCache.get( + currentCacheValue.chainId, + currentCacheValue.l1Token, + ); + return combineBundleLeaf(proposedCacheValue, currentCacheValue); + }), + ); + } } diff --git a/packages/indexer/src/redis/bundleLeavesCache.ts b/packages/indexer/src/redis/bundleLeavesCache.ts index 2bde9dc8..e570217c 100644 --- a/packages/indexer/src/redis/bundleLeavesCache.ts +++ b/packages/indexer/src/redis/bundleLeavesCache.ts @@ -11,6 +11,7 @@ export const BundleLeaf = s.object({ l1Token: s.string(), netSendAmount: s.string(), runningBalance: s.string(), + lastExecutedRunningBalance: s.string(), }); export type BundleLeaf = s.Infer; export type BundleLeaves = BundleLeaf[]; diff --git a/packages/indexer/src/services/BundleBuilderService.ts b/packages/indexer/src/services/BundleBuilderService.ts index 98ccd3a9..b7c62756 100644 --- a/packages/indexer/src/services/BundleBuilderService.ts +++ b/packages/indexer/src/services/BundleBuilderService.ts @@ -245,11 +245,22 @@ export class BundleBuilderService extends BaseIndexer { // and not any specific proposal convertProposalRangeResultToProposalRange(ranges), ); + // first clear the cache to prepare for update await this.currentBundleCache.clear(); // Persist this to Redis await Promise.all( resultsToPersist.flatMap((leaf) => { + const lastExecutedRunningBalance = + lastExecutedBundle.proposal.bundleEvaluationBlockNumbers[ + lastExecutedBundle.proposal.chainIds.findIndex( + (chainId) => leaf.chainId === chainId, + ) + ]; + assert( + lastExecutedRunningBalance, + "Last executed running balance not found", + ); assert( leaf.l1Tokens.length == leaf.netSendAmounts.length, "Net send amount count does not match token counts", @@ -264,6 +275,7 @@ export class BundleBuilderService extends BaseIndexer { l1Token, netSendAmount: leaf.netSendAmounts[tokenIndex]!, runningBalance: leaf.runningBalances[tokenIndex]!, + lastExecutedRunningBalance: String(lastExecutedRunningBalance), }); }); }), @@ -309,6 +321,16 @@ export class BundleBuilderService extends BaseIndexer { // Persist this to Redis await Promise.all( resultsToPersist.flatMap((leaf) => { + const lastExecutedRunningBalance = + lastExecutedBundle.proposal.bundleEvaluationBlockNumbers[ + lastExecutedBundle.proposal.chainIds.findIndex( + (chainId) => leaf.chainId === chainId, + ) + ]; + assert( + lastExecutedRunningBalance, + "Last executed running balance not found", + ); assert( leaf.l1Tokens.length == leaf.netSendAmounts.length, "Net send amount count does not match token counts", @@ -323,6 +345,7 @@ export class BundleBuilderService extends BaseIndexer { l1Token, netSendAmount: leaf.netSendAmounts[tokenIndex]!, runningBalance: leaf.runningBalances[tokenIndex]!, + lastExecutedRunningBalance: String(lastExecutedRunningBalance), }); }); }),