diff --git a/src/EventHandlers/CLPool.ts b/src/EventHandlers/CLPool.ts index efeaf01..6a5d247 100644 --- a/src/EventHandlers/CLPool.ts +++ b/src/EventHandlers/CLPool.ts @@ -10,12 +10,72 @@ import { CLPool_SetFeeProtocol, CLPool_Swap, CLPoolAggregator, + Token, } from "generated"; import { set_whitelisted_prices } from "../PriceOracle"; import { normalizeTokenAmountTo1e18 } from "../Helpers"; import { multiplyBase1e18, abs } from "../Maths"; import { updateCLPoolAggregator } from "../Aggregators/CLPoolAggregator"; +/** + * Updates the fee amounts for a CLPoolAggregator based on event data. + * + * This function calculates the new total fees for both tokens in a liquidity pool + * and their equivalent value in USD. It normalizes the token amounts to a base of 1e18 + * for consistent calculations and updates the total fees in the aggregator. + * + * @param clPoolAggregator - The current state of the CLPoolAggregator, containing existing fee data. + * @param event - The event data containing the fee amounts for token0 and token1. + * @param token0Instance - The instance of token0, containing its decimals and price per USD. + * @param token1Instance - The instance of token1, containing its decimals and price per USD. + * + * @returns An object containing the updated total fees for token0, token1, and their equivalent in USD. + * + * The returned object has the following structure: + * - `totalFees0`: The updated total fees for token0, normalized to 1e18. + * - `totalFees1`: The updated total fees for token1, normalized to 1e18. + * - `totalFeesUSD`: The updated total fees in USD, calculated using the normalized token fees and their prices. + */ +function updateCLPoolFees( + clPoolAggregator: CLPoolAggregator, + event: any, + token0Instance: Token | undefined, + token1Instance: Token | undefined +) { + + let tokenUpdateData = { + totalFees0: clPoolAggregator.totalFees0, + totalFees1: clPoolAggregator.totalFees1, + totalFeesUSD: clPoolAggregator.totalFeesUSD, + }; + + if (token0Instance) { + const incomingFees0 = normalizeTokenAmountTo1e18( + event.params.amount0, + Number(token0Instance.decimals) + ); + tokenUpdateData.totalFees0 += incomingFees0; + tokenUpdateData.totalFeesUSD += multiplyBase1e18( + incomingFees0, + token0Instance.pricePerUSDNew + ); + } + + if (token1Instance) { + const incomingFees1 = normalizeTokenAmountTo1e18( + event.params.amount1, + Number(token1Instance.decimals) + ); + tokenUpdateData.totalFees1 += incomingFees1; + tokenUpdateData.totalFeesUSD += multiplyBase1e18( + incomingFees1, + token1Instance.pricePerUSDNew + ); + } + + return tokenUpdateData; +} + CLPool.Burn.handler(async ({ event, context }) => { const entity: CLPool_Burn = { id: `${event.chainId}_${event.block.number}_${event.logIndex}`, @@ -33,35 +93,106 @@ CLPool.Burn.handler(async ({ event, context }) => { context.CLPool_Burn.set(entity); }); -CLPool.Collect.handler(async ({ event, context }) => { - const entity: CLPool_Collect = { - id: `${event.chainId}_${event.block.number}_${event.logIndex}`, - owner: event.params.owner, - recipient: event.params.recipient, - tickLower: event.params.tickLower, - tickUpper: event.params.tickUpper, - amount0: event.params.amount0, - amount1: event.params.amount1, - sourceAddress: event.srcAddress, - timestamp: new Date(event.block.timestamp * 1000), - chainId: event.chainId, - }; +CLPool.Collect.handlerWithLoader({ + loader: async ({ event, context }) => { + const pool_id = event.srcAddress; + const pool_created = await context.CLFactory_PoolCreated.getWhere.pool.eq( + pool_id + ); + + if (!pool_created || pool_created.length === 0) { + context.log.error(`Pool ${pool_id} not found during collect`); + return null; + } + + const [token0Instance, token1Instance, clPoolAggregator] = + await Promise.all([ + context.Token.get(pool_created[0].token0), + context.Token.get(pool_created[0].token1), + context.CLPoolAggregator.get(pool_id), + ]); + + return { clPoolAggregator, token0Instance, token1Instance }; + }, + handler: async ({ event, context, loaderReturn }) => { + + const entity: CLPool_Collect = { + id: `${event.chainId}_${event.block.number}_${event.logIndex}`, + owner: event.params.owner, + recipient: event.params.recipient, + tickLower: event.params.tickLower, + tickUpper: event.params.tickUpper, + amount0: event.params.amount0, + amount1: event.params.amount1, + sourceAddress: event.srcAddress, + timestamp: new Date(event.block.timestamp * 1000), + chainId: event.chainId, + }; + + context.CLPool_Collect.set(entity); + + if (loaderReturn && loaderReturn.clPoolAggregator) { + const { clPoolAggregator, token0Instance, token1Instance } = loaderReturn; - context.CLPool_Collect.set(entity); + const tokenUpdateData = updateCLPoolFees(clPoolAggregator, event, token0Instance, token1Instance); + + updateCLPoolAggregator( + tokenUpdateData, + clPoolAggregator, + new Date(event.block.timestamp * 1000), + context + ); + } + }, }); -CLPool.CollectFees.handler(async ({ event, context }) => { - const entity: CLPool_CollectFees = { - id: `${event.chainId}_${event.block.number}_${event.logIndex}`, - recipient: event.params.recipient, - amount0: event.params.amount0, - amount1: event.params.amount1, - sourceAddress: event.srcAddress, - timestamp: new Date(event.block.timestamp * 1000), - chainId: event.chainId, - }; +CLPool.CollectFees.handlerWithLoader({ + loader: async ({ event, context }) => { + const pool_id = event.srcAddress; + const pool_created = await context.CLFactory_PoolCreated.getWhere.pool.eq( + pool_id + ); + + if (!pool_created || pool_created.length === 0) { + context.log.error(`Pool ${pool_id} not found during collect`); + return null; + } + + const [token0Instance, token1Instance, clPoolAggregator] = + await Promise.all([ + context.Token.get(pool_created[0].token0), + context.Token.get(pool_created[0].token1), + context.CLPoolAggregator.get(pool_id), + ]); - context.CLPool_CollectFees.set(entity); + return { clPoolAggregator, token0Instance, token1Instance }; + }, + handler: async ({ event, context, loaderReturn }) => { + const entity: CLPool_CollectFees = { + id: `${event.chainId}_${event.block.number}_${event.logIndex}`, + recipient: event.params.recipient, + amount0: event.params.amount0, + amount1: event.params.amount1, + sourceAddress: event.srcAddress, + timestamp: new Date(event.block.timestamp * 1000), + chainId: event.chainId, + }; + + context.CLPool_CollectFees.set(entity); + + if (loaderReturn && loaderReturn.clPoolAggregator) { + const { clPoolAggregator, token0Instance, token1Instance } = loaderReturn; + + const tokenUpdateData = updateCLPoolFees(clPoolAggregator, event, token0Instance, token1Instance); + + updateCLPoolAggregator( + tokenUpdateData, + clPoolAggregator, + new Date(event.block.timestamp * 1000), + context + ); + } + } }); CLPool.Flash.handler(async ({ event, context }) => { diff --git a/test/EventHandlers/CLPool.test.ts b/test/EventHandlers/CLPool.test.ts index a182725..ec461b6 100644 --- a/test/EventHandlers/CLPool.test.ts +++ b/test/EventHandlers/CLPool.test.ts @@ -11,12 +11,13 @@ import * as PriceOracle from "../../src/PriceOracle"; import { abs } from "../../src/Maths"; describe("CLPool Event Handlers", () => { - let mockDb: ReturnType; + let mockDb: any; let updateCLPoolAggregatorStub: sinon.SinonStub; let setPricesStub: sinon.SinonStub; beforeEach(() => { mockDb = MockDb.createMockDb(); + updateCLPoolAggregatorStub = sinon.stub( CLPoolAggregatorFunctions, "updateCLPoolAggregator" @@ -28,6 +29,216 @@ describe("CLPool Event Handlers", () => { afterEach(() => { sinon.restore(); + updateCLPoolAggregatorStub.restore(); + }); + + describe("Collect Event", () => { + let mockEvent: any; + const poolId = "0x1234567890123456789012345678901234567890"; + const token0Id = "0x0000000000000000000000000000000000000001"; + const token1Id = "0x0000000000000000000000000000000000000002"; + let mockCLPoolAggregator: any; + let mockEventData: any; + let setupDB: any; + let mockToken0: Token; + let mockToken1: Token; + + const eventFees = { + amount0: 100n * 10n ** 18n, + amount1: 200n * 10n ** 6n, + }; + + beforeEach(() => { + mockToken0 = { + id: token0Id, + decimals: 18n, + pricePerUSDNew: 1n * 10n ** 18n, + } as Token; + + mockToken1 = { + id: token1Id, + decimals: 6n, + pricePerUSDNew: 1n * 10n ** 18n, + } as Token; + + mockEventData = { + amount0: eventFees.amount0, + amount1: eventFees.amount1, + mockEventData: { + block: { + number: 123456, + timestamp: 1000000, + hash: "0xblockhash", + }, + chainId: 1, + logIndex: 0, + srcAddress: poolId, + }, + }; + + mockEvent = CLPool.Collect.createMockEvent(mockEventData); + + mockCLPoolAggregator = { + id: poolId, + chainId: 1, + totalFees0: 100n * 10n ** 18n, + totalFees1: 200n * 10n ** 18n, + totalFeesUSD: 300n * 10n ** 18n, + } as CLPoolAggregator; + }); + + describe("when event is processed", () => { + beforeEach(async () => { + let updatedDB = mockDb.entities.CLFactory_PoolCreated.set({ + id: `1_123456_0`, + token0: token0Id, + token1: token1Id, + pool: poolId, + } as CLFactory_PoolCreated); + updatedDB = updatedDB.entities.Token.set(mockToken0); + updatedDB = updatedDB.entities.Token.set(mockToken1); + updatedDB = + updatedDB.entities.CLPoolAggregator.set(mockCLPoolAggregator); + + setupDB = await CLPool.Collect.processEvent({ + event: mockEvent, + mockDb: updatedDB, + }); + }); + + it("should create a CLPool_Collect entity", async () => { + const expectedId = `${mockEvent.chainId}_${mockEvent.block.number}_${mockEvent.logIndex}`; + const collectEntity = setupDB.entities.CLPool_Collect.get(expectedId); + expect(collectEntity).to.not.be.undefined; + expect(collectEntity?.amount0).to.equal(100n * 10n ** 18n); + expect(collectEntity?.amount1).to.equal(200n * 10n ** 6n); + }); + + it("should update CLPoolAggregator", async () => { + expect(updateCLPoolAggregatorStub.calledOnce).to.be.true; + const [diff] = updateCLPoolAggregatorStub.firstCall.args; + + expect(diff.totalFees0).to.equal( + mockCLPoolAggregator.totalFees0 + eventFees.amount0 + ); + expect(diff.totalFees1).to.equal( + mockCLPoolAggregator.totalFees1 + + (eventFees.amount1 * 10n ** 18n) / 10n ** 6n, + "It should normalize fees here" + ); + }); + }); + }); + describe("Collect Fees Event", () => { + let mockEvent: any; + const poolId = "0x1234567890123456789012345678901234567890"; + const token0Id = "0x0000000000000000000000000000000000000001"; + const token1Id = "0x0000000000000000000000000000000000000002"; + let mockCLPoolAggregator: any; + let mockEventData: any; + let setupDB: any; + let mockToken0: Token; + let mockToken1: Token; + + const eventFees = { + amount0: 100n * 10n ** 18n, + amount1: 200n * 10n ** 6n, + }; + + beforeEach(() => { + mockToken0 = { + id: token0Id, + decimals: 18n, + pricePerUSDNew: 1n * 10n ** 18n, + } as Token; + + mockToken1 = { + id: token1Id, + decimals: 6n, + pricePerUSDNew: 1n * 10n ** 18n, + } as Token; + + mockEventData = { + amount0: eventFees.amount0, + amount1: eventFees.amount1, + mockEventData: { + block: { + number: 123456, + timestamp: 1000000, + hash: "0xblockhash", + }, + chainId: 1, + logIndex: 0, + srcAddress: poolId, + }, + }; + + mockEvent = CLPool.CollectFees.createMockEvent(mockEventData); + + mockCLPoolAggregator = { + id: poolId, + chainId: 1, + totalFees0: 100n * 10n ** 18n, + totalFees1: 200n * 10n ** 18n, + totalFeesUSD: 300n * 10n ** 18n, + } as CLPoolAggregator; + }); + + describe("when event is processed", () => { + let collectEntity: any; + let diff: any; + + beforeEach(async () => { + let updatedDB = mockDb.entities.CLFactory_PoolCreated.set({ + id: `1_123456_0`, + token0: token0Id, + token1: token1Id, + pool: poolId, + } as CLFactory_PoolCreated); + updatedDB = updatedDB.entities.Token.set(mockToken0); + updatedDB = updatedDB.entities.Token.set(mockToken1); + updatedDB = + updatedDB.entities.CLPoolAggregator.set(mockCLPoolAggregator); + + setupDB = await CLPool.CollectFees.processEvent({ + event: mockEvent, + mockDb: updatedDB, + }); + const expectedId = `${mockEvent.chainId}_${mockEvent.block.number}_${mockEvent.logIndex}`; + collectEntity = setupDB.entities.CLPool_CollectFees.get(expectedId); + [diff] = updateCLPoolAggregatorStub.firstCall.args; + }); + + it("should create a CLPool_CollectFees entity", async () => { + expect(collectEntity).to.not.be.undefined; + expect(collectEntity?.amount0).to.equal(100n * 10n ** 18n); + expect(collectEntity?.amount1).to.equal(200n * 10n ** 6n); + }); + + it("should update CLPoolAggregator", async () => { + expect(updateCLPoolAggregatorStub.calledOnce).to.be.true; + }); + + it("should update nominal fee amounts correctly", async () => { + expect(diff.totalFees0).to.equal( + mockCLPoolAggregator.totalFees0 + eventFees.amount0 + ); + expect(diff.totalFees1).to.equal( + mockCLPoolAggregator.totalFees1 + + (eventFees.amount1 * 10n ** 18n) / 10n ** 6n, + "It should normalize fees here" + ); + }); + + it("should correctly update total fees in USD", async () => { + expect(diff.totalFeesUSD).to.equal( + mockCLPoolAggregator.totalFeesUSD + + (eventFees.amount0 * mockToken0.pricePerUSDNew) / 10n ** 18n + + (eventFees.amount1 / 10n ** 6n) * mockToken1.pricePerUSDNew, + "It should correctly update total fees in USD" + ); + }); + }); }); describe("Swap Event", () => { @@ -38,8 +249,10 @@ describe("CLPool Event Handlers", () => { let mockCLPoolAggregator: CLPoolAggregator; let mockToken0: Token; let mockToken1: Token; + let swapEntity: any; + let aggregatorCalls: any; - beforeEach(() => { + beforeEach(async () => { mockEvent = CLPool.Swap.createMockEvent({ sender: "0xsender", recipient: "0xrecipient", @@ -82,101 +295,126 @@ describe("CLPool Event Handlers", () => { decimals: 6n, pricePerUSDNew: 1n * 10n ** 18n, } as Token; - - let updatedDB = mockDb.entities.CLFactory_PoolCreated.set({ - id: `1_123456_0`, - token0: token0Id, - token1: token1Id, - pool: poolId, - } as CLFactory_PoolCreated); - - let updatedDB2 = - updatedDB.entities.CLPoolAggregator.set(mockCLPoolAggregator); - let updatedDB3 = updatedDB2.entities.Token.set(mockToken0); - let updatedDB4 = updatedDB3.entities.Token.set(mockToken1); - mockDb = updatedDB4; }); - it("should create a CLPool_Swap entity", async () => { - const result = await CLPool.Swap.processEvent({ - event: mockEvent, - mockDb, - }); - const swapEntity = result.entities.CLPool_Swap.get(`1_123456_0`); - - expect(swapEntity).to.not.be.undefined; - expect(swapEntity?.sender).to.equal("0xsender"); - expect(swapEntity?.recipient).to.equal("0xrecipient"); - expect(swapEntity?.amount0).to.equal(-100n * 10n ** 18n); - expect(swapEntity?.amount1).to.equal(200n * 10n ** 6n); - expect(swapEntity?.sqrtPriceX96).to.equal(1n << 96n); - expect(swapEntity?.liquidity).to.equal(1000000n); - expect(swapEntity?.tick).to.equal(0n); - }); + describe("when tokens exist", () => { + beforeEach(async () => { + let updatedDB = mockDb.entities.CLFactory_PoolCreated.set({ + id: `1_123456_0`, + token0: token0Id, + token1: token1Id, + pool: poolId, + } as CLFactory_PoolCreated); - it("should update CLPoolAggregator", async () => { - await CLPool.Swap.processEvent({ event: mockEvent, mockDb }); - - expect(updateCLPoolAggregatorStub.calledOnce).to.be.true; - const [diff, current, timestamp, context] = - updateCLPoolAggregatorStub.firstCall.args; - - expect(diff.totalVolume0).to.equal( - mockCLPoolAggregator.totalVolume0 + abs(mockEvent.params.amount0) - ); - expect(diff.totalVolume1).to.equal( - mockCLPoolAggregator.totalVolume1 + - (abs(mockEvent.params.amount1) * 10n ** 18n) / - 10n ** mockToken1.decimals, - "It should normalize the volume 18 decimals. Note here the incoming decimals are 6." - ); - expect(diff.totalVolumeUSD).to.equal( - mockCLPoolAggregator.totalVolumeUSD + - (abs(mockEvent.params.amount0) * mockToken0.pricePerUSDNew) / - 10n ** mockToken0.decimals - ); - expect(diff.numberOfSwaps).to.equal(11n); - expect(diff.token0Price).to.equal(1n * 10n ** 18n); - expect(diff.token1Price).to.equal(1n * 10n ** 18n); - }); + let updatedDB2 = + updatedDB.entities.CLPoolAggregator.set(mockCLPoolAggregator); + let updatedDB3 = updatedDB2.entities.Token.set(mockToken0); + let updatedDB4 = updatedDB3.entities.Token.set(mockToken1); + mockDb = updatedDB4; + + const result = await CLPool.Swap.processEvent({ + event: mockEvent, + mockDb, + }); + swapEntity = result.entities.CLPool_Swap.get(`1_123456_0`); + aggregatorCalls = updateCLPoolAggregatorStub.firstCall.args; + }); - it("should call set_whitelisted_prices", async () => { - await CLPool.Swap.processEvent({ event: mockEvent, mockDb }); + it("should create a CLPool_Swap entity", async () => { + expect(swapEntity).to.not.be.undefined; + expect(swapEntity?.sender).to.equal("0xsender"); + expect(swapEntity?.recipient).to.equal("0xrecipient"); + expect(swapEntity?.amount0).to.equal(-100n * 10n ** 18n); + expect(swapEntity?.amount1).to.equal(200n * 10n ** 6n); + expect(swapEntity?.sqrtPriceX96).to.equal(1n << 96n); + expect(swapEntity?.liquidity).to.equal(1000000n); + expect(swapEntity?.tick).to.equal(0n); + }); - expect(setPricesStub.calledOnce).to.be.true; - const [chainId, blockNumber, blockDatetime] = - setPricesStub.firstCall.args; + it("should update CLPoolAggregator", async () => { + expect(updateCLPoolAggregatorStub.calledOnce).to.be.true; + }); - expect(chainId).to.equal(1); - expect(blockNumber).to.equal(123456); - expect(blockDatetime).to.deep.equal(new Date(1000000 * 1000)); + it("should update nominal volume amounts correctly", async () => { + const [diff] = aggregatorCalls; + expect(diff.totalVolume0).to.equal( + mockCLPoolAggregator.totalVolume0 + abs(mockEvent.params.amount0) + ); + expect(diff.totalVolume1).to.equal( + mockCLPoolAggregator.totalVolume1 + + (abs(mockEvent.params.amount1) * 10n ** 18n) / + 10n ** mockToken1.decimals, + "It should normalize the volume 18 decimals. Note here the incoming decimals are 6." + ); + }); + + it("should update number of swaps correctly", async () => { + const [diff] = aggregatorCalls; + expect(diff.numberOfSwaps).to.equal(11n); + }); + + it("should correctly update total volume in USD", async () => { + const [diff] = aggregatorCalls; + expect(diff.totalVolumeUSD).to.equal( + mockCLPoolAggregator.totalVolumeUSD + + (abs(mockEvent.params.amount0) * mockToken0.pricePerUSDNew) / + 10n ** mockToken0.decimals + ); + }); + + it("should update token prices correctly", async () => { + const [diff] = aggregatorCalls; + expect(diff.token0Price).to.equal(1n * 10n ** 18n); + expect(diff.token1Price).to.equal(1n * 10n ** 18n); + }); + + it("should call set_whitelisted_prices", async () => { + expect(setPricesStub.calledOnce).to.be.true; + const [chainId, blockNumber, blockDatetime] = + setPricesStub.firstCall.args; + + expect(chainId).to.equal(1); + expect(blockNumber).to.equal(123456); + expect(blockDatetime).to.deep.equal(new Date(1000000 * 1000)); + }); }); - it("should handle missing token instances", async () => { - const updatedDBDeletedToken0 = await mockDb.entities.Token.delete( - token0Id - ); - - await CLPool.Swap.processEvent({ - event: mockEvent, - mockDb: updatedDBDeletedToken0, - }); - - expect(updateCLPoolAggregatorStub.calledOnce).to.be.true; - const [diff] = updateCLPoolAggregatorStub.firstCall.args; - - expect(diff.totalVolume0).to.equal(mockCLPoolAggregator.totalVolume0); // Unchanged - expect(diff.totalVolume1).to.equal( - mockCLPoolAggregator.totalVolume1 + - (abs(mockEvent.params.amount1) * 10n ** 18n) / - 10n ** mockToken1.decimals, - "It should normalize the volume 18 decimals. Note here the incoming decimals are 6." - ); - expect(diff.totalVolumeUSD).to.equal( - mockCLPoolAggregator.totalVolumeUSD + - (abs(mockEvent.params.amount1) * mockToken1.pricePerUSDNew) / - 10n ** mockToken1.decimals - ); + describe("when tokens do not exist", () => { + beforeEach(async () => { + let updatedDB = mockDb.entities.CLFactory_PoolCreated.set({ + id: `1_123456_0`, + token0: token0Id, + token1: token1Id, + pool: poolId, + } as CLFactory_PoolCreated); + + let updatedDB2 = + updatedDB.entities.CLPoolAggregator.set(mockCLPoolAggregator); + let updatedDB3 = updatedDB2.entities.Token.set(mockToken1); + + await CLPool.Swap.processEvent({ + event: mockEvent, + mockDb: updatedDB3, + }); + }); + + it("should handle missing token instances", async () => { + expect(updateCLPoolAggregatorStub.calledOnce).to.be.true; + const [diff] = updateCLPoolAggregatorStub.firstCall.args; + + expect(diff.totalVolume0).to.equal(mockCLPoolAggregator.totalVolume0); // Unchanged + expect(diff.totalVolume1).to.equal( + mockCLPoolAggregator.totalVolume1 + + (abs(mockEvent.params.amount1) * 10n ** 18n) / + 10n ** mockToken1.decimals, + "It should normalize the volume 18 decimals. Note here the incoming decimals are 6." + ); + expect(diff.totalVolumeUSD).to.equal( + mockCLPoolAggregator.totalVolumeUSD + + (abs(mockEvent.params.amount1) * mockToken1.pricePerUSDNew) / + 10n ** mockToken1.decimals + ); + }); }); }); });