diff --git a/apps/api/src/app/inversify.config.ts b/apps/api/src/app/inversify.config.ts index 831d594..1ecd3ec 100644 --- a/apps/api/src/app/inversify.config.ts +++ b/apps/api/src/app/inversify.config.ts @@ -27,11 +27,6 @@ import { viemClients, } from '@cowprotocol/repositories'; -const DEFAULT_CACHE_VALUE_SECONDS = ms('2min') / 1000; // 2min cache time by default for values -const DEFAULT_CACHE_NULL_SECONDS = ms('30min') / 1000; // 30min cache time by default for NULL values (when the repository isn't known) - -const CACHE_TOKEN_INFO_SECONDS = ms('24h') / 1000; // 24h - import { Container } from 'inversify'; import { SimulationService, @@ -47,6 +42,20 @@ import { usdServiceSymbol, } from '@cowprotocol/services'; import ms from 'ms'; +import pino from 'pino'; + +const DEFAULT_CACHE_VALUE_SECONDS = ms('2min') / 1000; // 2min cache time by default for values +const DEFAULT_CACHE_NULL_SECONDS = ms('30min') / 1000; // 30min cache time by default for NULL values (when the repository isn't known) + +const CACHE_TOKEN_INFO_SECONDS = ms('24h') / 1000; // 24h + +// Configure the logger +const logger = pino({ + level: process.env.LOG_LEVEL || 'info', + formatters: { + level: (label) => ({ level: label }), + }, +}); function getErc20Repository(cacheRepository: CacheRepository): Erc20Repository { return new Erc20RepositoryCache( @@ -133,12 +142,20 @@ function getTokenHolderRepository( ]); } +function getSimulationRepository(): SimulationRepository { + return new SimulationRepositoryTenderly(logger.child({ module: 'tenderly' })); +} + function getApiContainer(): Container { const apiContainer = new Container(); + + // Bind logger + apiContainer.bind('Logger').toConstantValue(logger); + // Repositories const cacheRepository = getCacheRepository(apiContainer); const erc20Repository = getErc20Repository(cacheRepository); - const simulationRepository = new SimulationRepositoryTenderly(); + const simulationRepository = getSimulationRepository(); const tokenHolderRepository = getTokenHolderRepository(cacheRepository); apiContainer diff --git a/apps/api/src/app/routes/__chainId/simulation/simulateBundle.ts b/apps/api/src/app/routes/__chainId/simulation/simulateBundle.ts index 484ae0b..695aaf8 100644 --- a/apps/api/src/app/routes/__chainId/simulation/simulateBundle.ts +++ b/apps/api/src/app/routes/__chainId/simulation/simulateBundle.ts @@ -130,23 +130,32 @@ const root: FastifyPluginAsync = async (fastify): Promise => { }, }, async function (request, reply) { - const { chainId } = request.params; + try { + const { chainId } = request.params; - const simulationResult = - await tenderlyService.postTenderlyBundleSimulation( - chainId, - request.body + fastify.log.info( + `Starting simulation of ${request.body.length} transactions on chain ${chainId}` ); - if (simulationResult === null) { - reply.code(400).send({ message: 'Build simulation error' }); - return; - } - fastify.log.info( - `Post Tenderly bundle of ${request.body.length} simulation on chain ${chainId}` - ); + const simulationResult = + await tenderlyService.postTenderlyBundleSimulation( + chainId, + request.body + ); + + if (simulationResult === null) { + reply.code(400).send({ message: 'Build simulation error' }); + return; + } + fastify.log.info( + `Post bundle of ${request.body.length} simulation on chain ${chainId}` + ); - reply.send(simulationResult); + reply.send(simulationResult); + } catch (e) { + fastify.log.error('Error in /simulateBundle', e); + reply.code(500).send({ message: 'Error in /simulateBundle' }); + } } ); }; diff --git a/libs/repositories/src/SimulationRepository/SimulationRepositoryTenderly.ts b/libs/repositories/src/SimulationRepository/SimulationRepositoryTenderly.ts index 7caa572..35f8229 100644 --- a/libs/repositories/src/SimulationRepository/SimulationRepositoryTenderly.ts +++ b/libs/repositories/src/SimulationRepository/SimulationRepositoryTenderly.ts @@ -10,18 +10,94 @@ import { TENDERLY_API_BASE_ENDPOINT, TENDERLY_API_KEY, } from '../datasources/tenderlyApi'; -import { injectable } from 'inversify'; +import { injectable, inject } from 'inversify'; import { SimulationData, SimulationInput, SimulationRepository, } from './SimulationRepository'; import { BigNumber } from 'ethers'; +import { Logger } from 'pino'; + +interface TenderlyRequestLog { + timestamp: string; + chainId: SupportedChainId; + endpoint: string; + method: string; + simulationsCount: number; + simulations: TenderlySimulatePayload[]; +} + +interface TenderlyResponseLog { + timestamp: string; + duration: number; + status: 'success' | 'error'; + error?: string; + simulationResults?: { + id: string; + status: boolean; + gasUsed?: string; + }[]; +} export const tenderlyRepositorySymbol = Symbol.for('TenderlyRepository'); @injectable() export class SimulationRepositoryTenderly implements SimulationRepository { + constructor(@inject('Logger') private readonly logger: Logger) {} + + private logRequest( + chainId: SupportedChainId, + simulations: TenderlySimulatePayload[] + ): TenderlyRequestLog { + const requestLog: TenderlyRequestLog = { + timestamp: new Date().toISOString(), + chainId, + endpoint: `${TENDERLY_API_BASE_ENDPOINT}/simulate-bundle`, + method: 'POST', + simulationsCount: simulations.length, + simulations, + }; + + this.logger.info({ + msg: 'Tenderly simulation request', + ...requestLog, + }); + + return requestLog; + } + + private logResponse( + startTime: number, + response: TenderlyBundleSimulationResponse | SimulationError + ): TenderlyResponseLog { + const duration = Date.now() - startTime; + const responseLog: TenderlyResponseLog = { + timestamp: new Date().toISOString(), + duration, + status: this.checkBundleSimulationError(response) ? 'error' : 'success', + }; + + if (this.checkBundleSimulationError(response)) { + responseLog.error = response.error.message; + } else { + responseLog.simulationResults = response.simulation_results.map( + (result) => ({ + id: result.simulation.id, + status: result.simulation.status, + gasUsed: result.transaction?.gas_used.toString(), + }) + ); + } + + this.logger.info({ + msg: 'Tenderly simulation response', + ...responseLog, + }); + + return responseLog; + } + async postBundleSimulation( chainId: SupportedChainId, simulationsInput: SimulationInput[] @@ -33,38 +109,55 @@ export class SimulationRepositoryTenderly implements SimulationRepository { save: true, save_if_fails: true, })) as TenderlySimulatePayload[]; - const response = (await fetch( - `${TENDERLY_API_BASE_ENDPOINT}/simulate-bundle`, - { - method: 'POST', - body: JSON.stringify({ simulations }), - headers: { - 'X-Access-Key': TENDERLY_API_KEY, - }, + + const startTime = Date.now(); + this.logRequest(chainId, simulations); + + try { + const response = (await fetch( + `${TENDERLY_API_BASE_ENDPOINT}/simulate-bundle`, + { + method: 'POST', + body: JSON.stringify({ simulations }), + headers: { + 'X-Access-Key': TENDERLY_API_KEY, + }, + } + ).then((res) => res.json())) as + | TenderlyBundleSimulationResponse + | SimulationError; + + this.logResponse(startTime, response); + + if (this.checkBundleSimulationError(response)) { + return null; } - ).then((res) => res.json())) as - | TenderlyBundleSimulationResponse - | SimulationError; - if (this.checkBundleSimulationError(response)) { - return null; - } + const balancesDiff = this.buildBalancesDiff( + response.simulation_results.map( + (result) => result.transaction?.transaction_info.asset_changes || [] + ) + ); - const balancesDiff = this.buildBalancesDiff( - response.simulation_results.map( - (result) => result.transaction?.transaction_info.asset_changes || [] - ) - ); - - return response.simulation_results.map((simulation_result, i) => { - return { - status: simulation_result.simulation.status, - id: simulation_result.simulation.id, - link: getTenderlySimulationLink(simulation_result.simulation.id), - cumulativeBalancesDiff: balancesDiff[i], - gasUsed: simulation_result.transaction?.gas_used.toString(), - }; - }); + return response.simulation_results.map((simulation_result, i) => { + return { + status: simulation_result.simulation.status, + id: simulation_result.simulation.id, + link: getTenderlySimulationLink(simulation_result.simulation.id), + cumulativeBalancesDiff: balancesDiff[i], + gasUsed: simulation_result.transaction?.gas_used.toString(), + }; + }); + } catch (error) { + this.logger.error({ + msg: 'Tenderly simulation unexpected error', + error: error instanceof Error ? error.message : 'Unknown error', + chainId, + simulationsCount: simulations.length, + duration: Date.now() - startTime, + }); + throw error; + } } checkBundleSimulationError( @@ -77,13 +170,11 @@ export class SimulationRepositoryTenderly implements SimulationRepository { assetChangesList: AssetChange[][] ): Record>[] { const cumulativeBalancesDiff: Record> = {}; - return assetChangesList.map((assetChanges) => { assetChanges.forEach((change) => { const { token_info, from, to, raw_amount } = change; const { contract_address } = token_info; - // Helper function to update balance const updateBalance = ( address: string, tokenSymbol: string, @@ -95,25 +186,21 @@ export class SimulationRepositoryTenderly implements SimulationRepository { if (!cumulativeBalancesDiff[address][tokenSymbol]) { cumulativeBalancesDiff[address][tokenSymbol] = '0'; } - const currentBalance = BigNumber.from( cumulativeBalancesDiff[address][tokenSymbol] ); const changeValue = BigNumber.from(changeAmount); const newBalance = currentBalance.add(changeValue); - cumulativeBalancesDiff[address][tokenSymbol] = newBalance.toString(); }; if (from) { updateBalance(from, contract_address, `-${raw_amount}`); } - if (to) { updateBalance(to, contract_address, raw_amount); } }); - return JSON.parse(JSON.stringify(cumulativeBalancesDiff)); }); }