diff --git a/package.json b/package.json index cd42eaa..3046677 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,9 @@ "dev": "envio dev", "codegen": "envio codegen", "start": "ts-node generated/src/Index.bs.js", - "test": "mocha --recursive --require ts-node/register \"test/**/*.ts\" --timeout 30000", + "dottest": "mocha -w --recursive -R dot --require ts-node/register \"test/**/*.ts\" --timeout 30000", + "test": "mocha --recursive --R spec --require ts-node/register \"test/**/*.ts\" --timeout 30000", + "test-watch": "mocha -w --recursive --R dot --require ts-node/register \"test/**/*.ts\" --timeout 30000", "enable-hydra": "./hack/hydra-mode/hydra-mode.sh" }, "devDependencies": { diff --git a/src/EventHandlers/CLFactory.ts b/src/EventHandlers/CLFactory.ts index acf10e4..c79c489 100644 --- a/src/EventHandlers/CLFactory.ts +++ b/src/EventHandlers/CLFactory.ts @@ -1,9 +1,9 @@ -import { CLFactory, CLFactory_PoolCreated, LiquidityPoolAggregator } from "generated"; +import { CLFactory, CLFactory_PoolCreated, LiquidityPoolAggregator, Token } from "generated"; import { updateLiquidityPoolAggregator } from "../Aggregators/LiquidityPoolAggregator"; import { TokenEntityMapping } from "../CustomTypes"; -import { getErc20TokenDetails } from "../Erc20"; import { TokenIdByChain } from "../Constants"; import { generatePoolName } from "../Helpers"; +import { createTokenEntity } from "../PriceOracle"; CLFactory.PoolCreated.contractRegister( ({ event, context }) => { @@ -44,11 +44,9 @@ CLFactory.PoolCreated.handlerWithLoader({ for (let poolTokenAddressMapping of poolTokenAddressMappings) { if (poolTokenAddressMapping.tokenInstance == undefined) { try { - const { symbol: tokenSymbol } = await getErc20TokenDetails( - poolTokenAddressMapping.address, - event.chainId - ); - poolTokenSymbols.push(tokenSymbol); + poolTokenAddressMapping.tokenInstance = await createTokenEntity( + poolTokenAddressMapping.address, event.chainId, event.block.number, context); + poolTokenSymbols.push(poolTokenAddressMapping.tokenInstance.symbol); } catch (error) { context.log.error(`Error in cl factory fetching token details` + ` for ${poolTokenAddressMapping.address} on chain ${event.chainId}: ${error}`); diff --git a/src/EventHandlers/CLPool.ts b/src/EventHandlers/CLPool.ts index 25ed13e..0ec527c 100644 --- a/src/EventHandlers/CLPool.ts +++ b/src/EventHandlers/CLPool.ts @@ -12,7 +12,7 @@ import { LiquidityPoolAggregator, Token, } from "generated"; -import { set_whitelisted_prices } from "../PriceOracle"; +import { refreshTokenPrice } from "../PriceOracle"; import { normalizeTokenAmountTo1e18 } from "../Helpers"; import { multiplyBase1e18, abs } from "../Maths"; import { updateLiquidityPoolAggregator } from "../Aggregators/LiquidityPoolAggregator"; @@ -472,6 +472,7 @@ CLPool.Swap.handlerWithLoader({ return { liquidityPoolAggregator, token0Instance, token1Instance }; }, handler: async ({ event, context, loaderReturn }) => { + const blockDatetime = new Date(event.block.timestamp * 1000); const entity: CLPool_Swap = { id: `${event.chainId}_${event.block.number}_${event.logIndex}`, sender: event.params.sender, @@ -482,7 +483,7 @@ CLPool.Swap.handlerWithLoader({ liquidity: event.params.liquidity, tick: event.params.tick, sourceAddress: event.srcAddress, - timestamp: new Date(event.block.timestamp * 1000), + timestamp: blockDatetime, chainId: event.chainId, }; @@ -490,6 +491,8 @@ CLPool.Swap.handlerWithLoader({ if (loaderReturn && loaderReturn.liquidityPoolAggregator) { const { liquidityPoolAggregator, token0Instance, token1Instance } = loaderReturn; + let token0 = token0Instance; + let token1 = token1Instance; // Delta that will be added to the liquidity pool aggregator let tokenUpdateData = { @@ -503,25 +506,35 @@ CLPool.Swap.handlerWithLoader({ tokenUpdateData.netAmount0 = abs(event.params.amount0); tokenUpdateData.netAmount1 = abs(event.params.amount1); - if (token0Instance) { + if (token0) { + try { + token0 = await refreshTokenPrice(token0, event.block.number, event.block.timestamp, event.chainId, context); + } catch (error) { + context.log.error(`Error refreshing token price for ${token0?.address} on chain ${event.chainId}: ${error}`); + } const normalizedAmount0 = normalizeTokenAmountTo1e18( abs(event.params.amount0), - Number(token0Instance.decimals) + Number(token0.decimals) ); tokenUpdateData.netVolumeToken0USD = multiplyBase1e18( normalizedAmount0, - token0Instance.pricePerUSDNew + token0.pricePerUSDNew ); } - if (token1Instance) { + if (token1) { + try { + token1 = await refreshTokenPrice(token1, event.block.number, event.block.timestamp, event.chainId, context); + } catch (error) { + context.log.error(`Error refreshing token price for ${token1?.address} on chain ${event.chainId}: ${error}`); + } const normalizedAmount1 = normalizeTokenAmountTo1e18( abs(event.params.amount1), - Number(token1Instance.decimals) + Number(token1.decimals) ); tokenUpdateData.netVolumeToken1USD = multiplyBase1e18( normalizedAmount1, - token1Instance.pricePerUSDNew + token1.pricePerUSDNew ); } @@ -534,8 +547,8 @@ CLPool.Swap.handlerWithLoader({ const reserveResult = updateCLPoolLiquidity( liquidityPoolAggregator, event, - token0Instance, - token1Instance + token0, + token1 ); const liquidityPoolAggregatorDiff: Partial = { @@ -558,21 +571,10 @@ CLPool.Swap.handlerWithLoader({ updateLiquidityPoolAggregator( liquidityPoolAggregatorDiff, liquidityPoolAggregator, - new Date(event.block.timestamp * 1000), - context - ); - } - - const blockDatetime = new Date(event.block.timestamp * 1000); - try { - await set_whitelisted_prices( - event.chainId, - event.block.number, blockDatetime, context ); - } catch (error) { - console.log(`Error updating token prices on CLPool swap on chain ${event.chainId}:`, error); } + }, }); diff --git a/src/EventHandlers/Pool.ts b/src/EventHandlers/Pool.ts index c008a33..1100fbf 100644 --- a/src/EventHandlers/Pool.ts +++ b/src/EventHandlers/Pool.ts @@ -3,7 +3,7 @@ import { Pool, Pool_Swap, Pool_Sync, Pool_Mint, Pool_Burn } from "generated"; import { LiquidityPoolAggregator, User } from "./../src/Types.gen"; import { normalizeTokenAmountTo1e18 } from "./../Helpers"; import { abs, multiplyBase1e18 } from "./../Maths"; -import { set_whitelisted_prices } from "../PriceOracle"; +import { refreshTokenPrice } from "../PriceOracle"; import { updateLiquidityPoolAggregator } from "../Aggregators/LiquidityPoolAggregator"; import { TokenIdByChain } from "../Constants"; @@ -143,6 +143,8 @@ Pool.Swap.handlerWithLoader({ }; }, handler: async ({ event, context, loaderReturn }) => { + const blockDatetime = new Date(event.block.timestamp * 1000); + const entity: Pool_Swap = { id: `${event.chainId}_${event.block.number}_${event.logIndex}`, sender: event.params.sender, @@ -152,7 +154,7 @@ Pool.Swap.handlerWithLoader({ amount0Out: event.params.amount0Out, amount1Out: event.params.amount1Out, sourceAddress: event.srcAddress, // Add sourceAddress - timestamp: new Date(event.block.timestamp * 1000), // Convert to Date + timestamp: blockDatetime, // Convert to Date chainId: event.chainId, }; @@ -161,6 +163,9 @@ Pool.Swap.handlerWithLoader({ const { liquidityPoolAggregator, token0Instance, token1Instance, to_address, user } = loaderReturn; + let token0 = token0Instance; + let token1 = token1Instance; + let tokenUpdateData = { netAmount0: 0n, netAmount1: 0n, @@ -170,26 +175,36 @@ Pool.Swap.handlerWithLoader({ }; tokenUpdateData.netAmount0 = event.params.amount0In + event.params.amount0Out; - if (token0Instance) { + if (token0) { + try { + token0 = await refreshTokenPrice(token0, event.block.number, event.block.timestamp, event.chainId, context); + } catch (error) { + context.log.error(`Error refreshing token price for ${token0?.address} on chain ${event.chainId}: ${error}`); + } const normalizedAmount0 = normalizeTokenAmountTo1e18( event.params.amount0In + event.params.amount0Out, - Number(token0Instance.decimals) + Number(token0.decimals) ); tokenUpdateData.netVolumeToken0USD = multiplyBase1e18( normalizedAmount0, - token0Instance.pricePerUSDNew + token0.pricePerUSDNew ); } tokenUpdateData.netAmount1 = event.params.amount1In + event.params.amount1Out; - if (token1Instance) { + if (token1) { + try { + token1 = await refreshTokenPrice(token1, event.block.number, event.block.timestamp, event.chainId, context); + } catch (error) { + context.log.error(`Error refreshing token price for ${token1?.address} on chain ${event.chainId}: ${error}`); + } const normalizedAmount1 = normalizeTokenAmountTo1e18( event.params.amount1In + event.params.amount1Out, - Number(token1Instance.decimals) + Number(token1.decimals) ); tokenUpdateData.netVolumeToken1USD = multiplyBase1e18( normalizedAmount1, - token1Instance.pricePerUSDNew + token1.pricePerUSDNew ); } @@ -220,14 +235,13 @@ Pool.Swap.handlerWithLoader({ context ); - const blockDatetime = new Date(event.block.timestamp * 1000); // Update user and create if they don't exist if (!user) { let newUser: User = { id: to_address, numberOfSwaps: 1n, - joined_at_timestamp: new Date(event.block.timestamp * 1000), + joined_at_timestamp: blockDatetime, }; context.User.set(newUser); } else { @@ -236,9 +250,9 @@ Pool.Swap.handlerWithLoader({ numberOfSwaps: user.numberOfSwaps + 1n, joined_at_timestamp: user.joined_at_timestamp < - new Date(event.block.timestamp * 1000) + blockDatetime ? user.joined_at_timestamp - : new Date(event.block.timestamp * 1000), + : blockDatetime, }; // for unordered head mode this correctly categorizes base users who may have joined early on optimism. try { context.User.set(existingUser); @@ -246,18 +260,6 @@ Pool.Swap.handlerWithLoader({ console.log("Error updating user:", error); } } - - // Try to set prices - try { - await set_whitelisted_prices( - event.chainId, - event.block.number, - blockDatetime, - context - ); - } catch (error) { - console.log("Error updating token prices on pool sync:", error); - } } }, }); @@ -286,6 +288,7 @@ Pool.Sync.handlerWithLoader({ }, handler: async ({ event, context, loaderReturn }) => { if (!loaderReturn) return; + const blockDatetime = new Date(event.block.timestamp * 1000); const { liquidityPoolAggregator, token0Instance, token1Instance } = loaderReturn; @@ -294,7 +297,7 @@ Pool.Sync.handlerWithLoader({ reserve0: event.params.reserve0, reserve1: event.params.reserve1, sourceAddress: event.srcAddress, - timestamp: new Date(event.block.timestamp * 1000), + timestamp: blockDatetime, chainId: event.chainId, }; @@ -313,7 +316,7 @@ Pool.Sync.handlerWithLoader({ if (token0Instance) { tokenUpdateData.normalizedReserve0 += normalizeTokenAmountTo1e18( event.params.reserve0, - Number(token0Instance?.decimals || 18) + Number(token0Instance.decimals) ); tokenUpdateData.token0PricePerUSDNew = token0Instance.pricePerUSDNew; tokenUpdateData.totalLiquidityUSD += multiplyBase1e18( @@ -325,8 +328,8 @@ Pool.Sync.handlerWithLoader({ if (token1Instance) { tokenUpdateData.normalizedReserve1 += normalizeTokenAmountTo1e18( event.params.reserve1, - Number(token1Instance?.decimals || 18) - ); + Number(token1Instance.decimals) + ); tokenUpdateData.token1PricePerUSDNew = token1Instance.pricePerUSDNew; tokenUpdateData.totalLiquidityUSD += multiplyBase1e18( @@ -341,7 +344,7 @@ Pool.Sync.handlerWithLoader({ totalLiquidityUSD: tokenUpdateData.totalLiquidityUSD || liquidityPoolAggregator.totalLiquidityUSD, token0Price: tokenUpdateData.token0PricePerUSDNew, token1Price: tokenUpdateData.token1PricePerUSDNew, - lastUpdatedTimestamp: new Date(event.block.timestamp * 1000), + lastUpdatedTimestamp: blockDatetime, }; updateLiquidityPoolAggregator( diff --git a/src/EventHandlers/PoolFactory.ts b/src/EventHandlers/PoolFactory.ts index b1415a2..515bd76 100644 --- a/src/EventHandlers/PoolFactory.ts +++ b/src/EventHandlers/PoolFactory.ts @@ -1,12 +1,10 @@ import { PoolFactory, PoolFactory_SetCustomFee } from "generated"; - -import { getErc20TokenDetails } from "./../Erc20"; - import { TokenEntityMapping } from "./../CustomTypes"; -import { Token, LiquidityPoolAggregator } from "./../src/Types.gen"; +import { LiquidityPoolAggregator } from "./../src/Types.gen"; import { generatePoolName } from "./../Helpers"; import { TokenIdByChain } from "../Constants"; import { updateLiquidityPoolAggregator } from "../Aggregators/LiquidityPoolAggregator"; +import { createTokenEntity } from "../PriceOracle"; PoolFactory.PoolCreated.contractRegister( ({ event, context }) => { @@ -36,11 +34,9 @@ PoolFactory.PoolCreated.handlerWithLoader({ for (let poolTokenAddressMapping of poolTokenAddressMappings) { if (poolTokenAddressMapping.tokenInstance == undefined) { try { - const { symbol: tokenSymbol } = await getErc20TokenDetails( - poolTokenAddressMapping.address, - event.chainId - ); - poolTokenSymbols.push(tokenSymbol); + poolTokenAddressMapping.tokenInstance = await createTokenEntity( + poolTokenAddressMapping.address, event.chainId, event.block.number, context); + poolTokenSymbols.push(poolTokenAddressMapping.tokenInstance.symbol); } catch (error) { context.log.error(`Error in pool factory fetching token details` + ` for ${poolTokenAddressMapping.address} on chain ${event.chainId}: ${error}`); diff --git a/src/PriceOracle.ts b/src/PriceOracle.ts index dea342d..7e4c428 100644 --- a/src/PriceOracle.ts +++ b/src/PriceOracle.ts @@ -15,7 +15,83 @@ export interface TokenPriceData { decimals: bigint; } -export async function getTokenPriceData(tokenAddress: string, blockNumber: number, chainId: number): Promise { +export async function createTokenEntity(tokenAddress: string, chainId: number, blockNumber: number, context: any) { + const blockDatetime = new Date(blockNumber * 1000); + const tokenDetails = await getErc20TokenDetails(tokenAddress, chainId); + + const tokenEntity: Token = { + id: TokenIdByChain(tokenAddress, chainId), + address: toChecksumAddress(tokenAddress), + symbol: tokenDetails.symbol, + name: tokenDetails.symbol, // Using symbol as name, update if you have a separate name field + chainId: chainId, + decimals: BigInt(tokenDetails.decimals), + pricePerUSDNew: BigInt(0), + lastUpdatedTimestamp: blockDatetime, + isWhitelisted: false, + }; + + context.Token.set(tokenEntity); + return tokenEntity; +} + +const ONE_HOUR_MS = 60 * 60 * 1000; // 1 hour in milliseconds + +/** + * Refreshes a token's price data if the update interval has passed. + * + * This function checks if enough time has passed since the last update (1 hour), + * and if so, fetches new price data for the token. The token entity is updated + * in the database with the new price and timestamp. + * + * @param {Token} token - The token entity to refresh + * @param {number} blockNumber - The block number to fetch price data from + * @param {number} blockTimestamp - The timestamp of the block in seconds + * @param {number} chainId - The chain ID where the token exists + * @param {any} context - The database context for updating entities + * @returns {Promise} The updated token entity + */ +export async function refreshTokenPrice( + token: Token, + blockNumber: number, + blockTimestamp: number, + chainId: number, + context: any +): Promise { + if (blockTimestamp - token.lastUpdatedTimestamp.getTime() < ONE_HOUR_MS) { + return token; + } + + const tokenPriceData = await getTokenPriceData(token.address, blockNumber, chainId); + const updatedToken: Token = { + ...token, + pricePerUSDNew: tokenPriceData.pricePerUSDNew, + decimals: tokenPriceData.decimals, + lastUpdatedTimestamp: new Date(blockTimestamp * 1000) + }; + context.Token.set(updatedToken); + return updatedToken; +} + +/** + * Fetches current price data for a specific token. + * + * Retrieves the token's price and decimals by: + * 1. Getting token details from the contract + * 2. Fetching price data from the price oracle + * 3. Converting the price to the appropriate format + * + * @param {string} tokenAddress - The token's contract address + * @param {number} blockNumber - The block number to fetch price data from + * @param {number} chainId - The chain ID where the token exists + * @returns {Promise} Object containing the token's price and decimals + * @throws {Error} If there's an error fetching the token price + */ +export async function getTokenPriceData( + tokenAddress: string, + blockNumber: number, + chainId: number +): Promise { const tokenDetails = await getErc20TokenDetails( tokenAddress, chainId diff --git a/test/EventHandlers/CLPool/CLPool.test.ts b/test/EventHandlers/CLPool/CLPool.test.ts index 3246863..e51d8e1 100644 --- a/test/EventHandlers/CLPool/CLPool.test.ts +++ b/test/EventHandlers/CLPool/CLPool.test.ts @@ -14,7 +14,7 @@ import { setupCommon } from "../Pool/common"; describe("CLPool Event Handlers", () => { let mockDb: any; let updateLiquidityPoolAggregatorStub: sinon.SinonStub; - let setPricesStub: sinon.SinonStub; + let mockPriceOracle: sinon.SinonStub; beforeEach(() => { mockDb = MockDb.createMockDb(); @@ -23,9 +23,11 @@ describe("CLPool Event Handlers", () => { LiquidityPoolAggregatorFunctions, "updateLiquidityPoolAggregator" ); - setPricesStub = sinon - .stub(PriceOracle, "set_whitelisted_prices") - .resolves(); + mockPriceOracle = sinon + .stub(PriceOracle, "refreshTokenPrice") + .callsFake(async (...args) => { + return args[0]; // Return the token that was passed in + }); }); @@ -480,15 +482,13 @@ describe("CLPool Event Handlers", () => { const [diff] = aggregatorCalls; expect(diff.totalLiquidityUSD).to.equal(expectations.totalLiquidityUSD); }); - - 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 call refreshTokenPrice on token0", () => { + const calledToken = mockPriceOracle.firstCall.args[0]; + expect(calledToken.address).to.equal(mockToken0Data.address); + }); + it("should call refreshTokenPrice on token1", () => { + const calledToken = mockPriceOracle.secondCall.args[0]; + expect(calledToken.address).to.equal(mockToken1Data.address); }); }); diff --git a/test/EventHandlers/Pool/Swap.test.ts b/test/EventHandlers/Pool/Swap.test.ts index c6a16bf..4838132 100644 --- a/test/EventHandlers/Pool/Swap.test.ts +++ b/test/EventHandlers/Pool/Swap.test.ts @@ -56,8 +56,11 @@ describe("Pool Swap Event", () => { (mockToken1Data.pricePerUSDNew / TEN_TO_THE_18_BI); mockPriceOracle = sinon - .stub(PriceOracle, "set_whitelisted_prices") - .resolves(); + .stub(PriceOracle, "refreshTokenPrice") + .callsFake(async (...args) => { + return args[0]; // Return the token that was passed in + }); + mockDb = MockDb.createMockDb(); eventData = { @@ -140,8 +143,8 @@ describe("Pool Swap Event", () => { modifiedMockLiquidityPoolData.totalVolumeUSD ); }); - it("should call set_whitelisted_prices", () => { - expect(mockPriceOracle.calledOnce).to.be.true; + it("should not call refreshTokenPrice", () => { + expect(mockPriceOracle.calledOnce).to.be.false; }); }); describe("when token0 is missing", () => { @@ -193,10 +196,12 @@ describe("Pool Swap Event", () => { mockLiquidityPoolData.numberOfSwaps + 1n, "Swap count should be updated" ); - expect( - mockPriceOracle.calledOnce, - "set_whitelisted_prices should be called" - ).to.be.true; + }); + + it("should call refreshTokenPrice on token1", () => { + expect(mockPriceOracle.calledOnce).to.be.true; + const calledToken = mockPriceOracle.firstCall.args[0]; + expect(calledToken.address).to.equal(mockToken1Data.address); }); }); @@ -239,13 +244,6 @@ describe("Pool Swap Event", () => { }); - it('should update the liquidity pool token prices', () => { - expect( - mockPriceOracle.calledOnce, - "set_whitelisted_prices should be called" - ).to.be.true; - }); - it('should update swap count', () => { expect(updatedPool?.numberOfSwaps).to.equal( mockLiquidityPoolData.numberOfSwaps + 1n, @@ -260,6 +258,11 @@ describe("Pool Swap Event", () => { ); }); + it("should call refreshTokenPrice on token0", () => { + expect(mockPriceOracle.calledOnce).to.be.true; + const calledToken = mockPriceOracle.firstCall.args[0]; + expect(calledToken.address).to.equal(mockToken0Data.address); + }); }); describe("when both tokens exist", () => { @@ -290,7 +293,6 @@ describe("Pool Swap Event", () => { expect(swapEvent?.timestamp).to.deep.equal( new Date(eventData.mockEventData.block.timestamp * 1000) ); - expect(mockPriceOracle.calledOnce).to.be.true; }); it("should update the Liquidity Pool aggregator", async () => { @@ -313,7 +315,14 @@ describe("Pool Swap Event", () => { expect(updatedPool?.lastUpdatedTimestamp).to.deep.equal( new Date(eventData.mockEventData.block.timestamp * 1000) ); - expect(mockPriceOracle.calledOnce).to.be.true; + }); + it("should call refreshTokenPrice on token0", () => { + const calledToken = mockPriceOracle.firstCall.args[0]; + expect(calledToken.address).to.equal(mockToken0Data.address); + }); + it("should call refreshTokenPrice on token1", () => { + const calledToken = mockPriceOracle.secondCall.args[0]; + expect(calledToken.address).to.equal(mockToken1Data.address); }); }); }); diff --git a/test/EventHandlers/Pool/common.ts b/test/EventHandlers/Pool/common.ts index b7947ab..9fd5259 100644 --- a/test/EventHandlers/Pool/common.ts +++ b/test/EventHandlers/Pool/common.ts @@ -11,6 +11,7 @@ export function setupCommon() { decimals: 18n, pricePerUSDNew: 1n * TEN_TO_THE_18_BI, // 1 USD chainId: 10, + isWhitelisted: true, }; const mockToken1Data = { @@ -21,6 +22,7 @@ export function setupCommon() { decimals: 6n, pricePerUSDNew: 1n * TEN_TO_THE_18_BI, // 1 USD chainId: 10, + isWhitelisted: true, }; const mockLiquidityPoolData = { diff --git a/test/EventHandlers/PoolFactory.test.ts b/test/EventHandlers/PoolFactory.test.ts index 64ad728..5588fd4 100644 --- a/test/EventHandlers/PoolFactory.test.ts +++ b/test/EventHandlers/PoolFactory.test.ts @@ -1,20 +1,33 @@ import { expect } from "chai"; import { MockDb, PoolFactory } from "../../generated/src/TestHelpers.gen"; -import { LiquidityPoolAggregator, Token } from "../../generated/src/Types.gen"; -import { TEN_TO_THE_18_BI } from "../../src/Constants"; import { toChecksumAddress } from "../../src/Constants"; +import * as PriceOracle from "../../src/PriceOracle"; +import sinon from "sinon"; +import { setupCommon } from "./Pool/common"; +import { Token } from "../../generated/src/Types.gen"; describe("PoolFactory Events", () => { - const token0Address = "0x1111111111111111111111111111111111111111"; - const token1Address = "0x2222222222222222222222222222222222222222"; - const poolAddress = "0x3333333333333333333333333333333333333333"; + const { mockToken0Data, mockToken1Data, mockLiquidityPoolData } = setupCommon(); + const token0Address = mockToken0Data.address; + const token1Address = mockToken1Data.address; + const poolAddress = mockLiquidityPoolData.id; const chainId = 10; - describe("PoolCreated event", () => { + let mockPriceOracle: sinon.SinonStub; + describe("PoolCreated event", () => { let createdPool: any; + beforeEach(async () => { + + mockPriceOracle = sinon + .stub(PriceOracle, "createTokenEntity") + .callsFake(async (...args) => { + if (args[0] === token0Address) return mockToken0Data as Token; + return mockToken1Data as Token; + }); + const mockDb = MockDb.createMockDb(); const mockEvent = PoolFactory.PoolCreated.createMockEvent({ token0: token0Address, @@ -33,6 +46,15 @@ describe("PoolFactory Events", () => { const result = await PoolFactory.PoolCreated.processEvent({ event: mockEvent, mockDb }); createdPool = result.entities.LiquidityPoolAggregator.get(toChecksumAddress(poolAddress)); }); + + afterEach(() => { + mockPriceOracle.restore(); + }); + + it('should create token entities', async () => { + expect(mockPriceOracle.calledTwice).to.be.true; + }); + it("should create a new LiquidityPool entity and Token entities", async () => { expect(createdPool).to.not.be.undefined; expect(createdPool?.isStable).to.be.false; diff --git a/test/PriceOracle.test.ts b/test/PriceOracle.test.ts index 6ec55f9..f8792e5 100644 --- a/test/PriceOracle.test.ts +++ b/test/PriceOracle.test.ts @@ -1,10 +1,12 @@ import { expect } from "chai"; import sinon from "sinon"; -import { Token } from "../generated/src/Types.gen"; import * as PriceOracle from "../src/PriceOracle"; -import { OPTIMISM_WHITELISTED_TOKENS, CHAIN_CONSTANTS, TokenIdByChain, TokenIdByBlock } from "../src/Constants"; +import * as Erc20 from "../src/Erc20"; +import { CHAIN_CONSTANTS } from "../src/Constants"; import { Cache } from "../src/cache"; +import { setupCommon } from "./EventHandlers/Pool/common"; + describe("PriceOracle", () => { let mockContext: any ; let mockContract: any; @@ -16,6 +18,7 @@ describe("PriceOracle", () => { let addStub: sinon.SinonStub; let readStub: sinon.SinonStub; + const { mockToken0Data } = setupCommon(); beforeEach(() => { addStub = sinon.stub(); @@ -32,44 +35,88 @@ describe("PriceOracle", () => { TokenPriceSnapshot: { set: sinon.stub(), get: sinon.stub() } }; - mockContract = sinon.stub(CHAIN_CONSTANTS[chainId].eth_client, "simulateContract") - .returns({ result: ["1000000000000000000", "2000000000000000000"] } as any); }); afterEach(() => { sinon.restore(); }); + describe("refreshTokenPrice", () => { + let mockERC20Details: sinon.SinonStub; + let testLastUpdated: Date; + + const mockTokenPriceData: any = { + pricePerUSDNew: 2n * 10n ** 18n, + decimals: mockToken0Data.decimals, + }; + + beforeEach(() => { + mockContract = sinon.stub(CHAIN_CONSTANTS[chainId].eth_client, "simulateContract") + .returns({ + result: [mockTokenPriceData.pricePerUSDNew.toString(), "2000000000000000000"] + } as any); + mockERC20Details = sinon.stub(Erc20, "getErc20TokenDetails") + .returns({ + decimals: mockTokenPriceData.decimals + } as any); + }); + + describe("if the update interval hasn't passed", () => { + let updatedToken: any; + beforeEach(async () => { + testLastUpdated = new Date(blockDatetime.getTime() - 1000); + const fetchedToken = { + ...mockToken0Data, + lastUpdatedTimestamp: testLastUpdated + }; + await PriceOracle.refreshTokenPrice(fetchedToken, blockNumber, blockDatetime.getTime(), chainId, mockContext); + }); + it("should not update prices if the update interval hasn't passed", async () => { + expect(mockContract.called).to.be.false; + }); + }); + describe("if the update interval has passed", () => { + let updatedToken: any; + let testLastUpdated: Date; + beforeEach(async () => { + testLastUpdated = new Date(blockDatetime.getTime() - (61 * 60 * 1000)); + const fetchedToken = { + ...mockToken0Data, + lastUpdatedTimestamp: testLastUpdated + }; + await PriceOracle.refreshTokenPrice(fetchedToken, blockNumber, blockDatetime.getTime(), chainId, mockContext); + updatedToken = mockContext.Token.set.lastCall.args[0]; + }); + it("should update prices if the update interval has passed", async () => { + expect(updatedToken.pricePerUSDNew).to.equal(mockTokenPriceData.pricePerUSDNew); + expect(updatedToken.lastUpdatedTimestamp).greaterThan(testLastUpdated); + }); + }); + }); + describe("set_whitelisted_prices", () => { + beforeEach(() => { + mockContract = sinon.stub(CHAIN_CONSTANTS[chainId].eth_client, "simulateContract") + .returns({ result: ["1000000000000000000", "2000000000000000000"] } as any); + }); + it("should update existing tokens and create TokenPrice entities", async () => { - // Setup existing token - const existingToken: Token = { - id: TokenIdByChain(OPTIMISM_WHITELISTED_TOKENS[0].address, chainId), - address: OPTIMISM_WHITELISTED_TOKENS[0].address, - symbol: OPTIMISM_WHITELISTED_TOKENS[0].symbol, - name: OPTIMISM_WHITELISTED_TOKENS[0].symbol, - chainId: chainId, - isWhitelisted: false, - decimals: BigInt(OPTIMISM_WHITELISTED_TOKENS[0].decimals), - pricePerUSDNew: BigInt(0), - lastUpdatedTimestamp: new Date("2022-01-01T00:00:00Z") - }; - - mockContext.Token.get.returns(existingToken); + + mockContext.Token.get.returns(mockToken0Data); await PriceOracle.set_whitelisted_prices(chainId, blockNumber, blockDatetime, mockContext); // Check if token was updated const updatedToken = mockContext.Token.set.args[0][0]; expect(updatedToken).to.not.be.undefined; - expect(updatedToken?.pricePerUSDNew).to.equal(BigInt("1000000000000000000")); + expect(updatedToken?.pricePerUSDNew).to.equal(mockToken0Data.pricePerUSDNew); expect(updatedToken?.lastUpdatedTimestamp).to.deep.equal(blockDatetime); // Check if TokenPrice was created const tokenPrice = mockContext.TokenPriceSnapshot.set.args[0][0]; expect(tokenPrice).to.not.be.undefined; - expect(tokenPrice?.pricePerUSDNew).to.equal(BigInt("1000000000000000000")); + expect(tokenPrice?.pricePerUSDNew).to.equal(mockToken0Data.pricePerUSDNew); expect(tokenPrice?.lastUpdatedTimestamp).to.deep.equal(blockDatetime); }); @@ -84,13 +131,13 @@ describe("PriceOracle", () => { // Check if new token was created const newToken = mockContext.Token.set.args[0][0]; expect(newToken).to.not.be.undefined; - expect(newToken?.pricePerUSDNew).to.equal(BigInt("1000000000000000000")); + expect(newToken?.pricePerUSDNew).to.equal(mockToken0Data.pricePerUSDNew); expect(newToken?.lastUpdatedTimestamp).to.deep.equal(updatedBlockDatetime); // Check if TokenPrice was created const tokenPrice = mockContext.TokenPriceSnapshot.set.args[0][0]; expect(tokenPrice).to.not.be.undefined; - expect(tokenPrice?.pricePerUSDNew).to.equal(BigInt("1000000000000000000")); + expect(tokenPrice?.pricePerUSDNew).to.equal(mockToken0Data.pricePerUSDNew); expect(tokenPrice?.lastUpdatedTimestamp).to.deep.equal(updatedBlockDatetime); });