Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

debug: increase logs of simulateBundle endpoint #105

Merged
merged 2 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions apps/api/src/app/inversify.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 }),
},
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yvesfracari why do we add the log config here, shouldn't we use the same one as https://github.com/cowprotocol/bff/blob/main/apps/api/src/main.ts#L5 ?

Copy link
Contributor

@anxolin anxolin Feb 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I address it, cause I want to deploy a new release of bff with some fixes for the logging and I want to fix this small detail. Let me know if it makes sense #107


function getErc20Repository(cacheRepository: CacheRepository): Erc20Repository {
return new Erc20RepositoryCache(
Expand Down Expand Up @@ -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<pino.Logger>('Logger').toConstantValue(logger);

// Repositories
const cacheRepository = getCacheRepository(apiContainer);
const erc20Repository = getErc20Repository(cacheRepository);
const simulationRepository = new SimulationRepositoryTenderly();
const simulationRepository = getSimulationRepository();
const tokenHolderRepository = getTokenHolderRepository(cacheRepository);

apiContainer
Expand Down
35 changes: 22 additions & 13 deletions apps/api/src/app/routes/__chainId/simulation/simulateBundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,23 +130,32 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
},
},
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' });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was here before, but what does null mean, and why is a 400 is an issue from the user?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It means that the Tenderly couldn't simulate the transaction. This happens often on the "build your own hook" where the user manually encodes the tx data.

We used a general 400 error but maybe we could change it to 422.

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' });
}
}
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand All @@ -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(
Expand All @@ -77,13 +170,11 @@ export class SimulationRepositoryTenderly implements SimulationRepository {
assetChangesList: AssetChange[][]
): Record<string, Record<string, string>>[] {
const cumulativeBalancesDiff: Record<string, Record<string, string>> = {};

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,
Expand All @@ -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));
});
}
Expand Down
Loading