Skip to content

Commit

Permalink
Improve code and fix tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Siegrift committed Apr 25, 2024
1 parent d0b5a0c commit 59a0ad9
Show file tree
Hide file tree
Showing 6 changed files with 344 additions and 151 deletions.
134 changes: 134 additions & 0 deletions src/update-feeds-loops/gas-estimation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import type { Api3ServerV1 } from '@api3/contracts';

import { generateMockApi3ServerV1 } from '../../test/fixtures/mock-contract';
import { logger } from '../logger';

import { estimateMulticallGasLimit, estimateSingleBeaconGasLimit, handleRpcGasLimitFailure } from './gas-estimation';

describe(estimateMulticallGasLimit.name, () => {
it('estimates the gas limit for a multicall', async () => {
const mockApi3ServerV1 = generateMockApi3ServerV1();
mockApi3ServerV1.multicall.estimateGas.mockResolvedValueOnce(BigInt(500_000));

const gasLimit = await estimateMulticallGasLimit(
mockApi3ServerV1 as unknown as Api3ServerV1,
['0xBeaconId1Calldata', '0xBeaconId2Calldata', '0xBeaconSetCalldata'],
undefined
);

expect(gasLimit).toStrictEqual(BigInt(550_000)); // Note that the gas limit is increased by 10%.
});

it('uses fallback gas limit when dummy data estimation fails', async () => {
const mockApi3ServerV1 = generateMockApi3ServerV1();
mockApi3ServerV1.multicall.estimateGas.mockRejectedValue(new Error('some-error'));
jest.spyOn(logger, 'warn');

const gasLimit = await estimateMulticallGasLimit(
mockApi3ServerV1 as unknown as Api3ServerV1,
['0xBeaconId1Calldata', '0xBeaconId2Calldata', '0xBeaconSetCalldata'],
2_000_000
);

expect(gasLimit).toStrictEqual(BigInt(2_000_000));
expect(logger.warn).toHaveBeenCalledTimes(1);
expect(logger.warn).toHaveBeenCalledWith('Unable to estimate gas using provider.', {
errorMessage: 'some-error',
});
});

it('returns null if no fallback is provided', async () => {
const mockApi3ServerV1 = generateMockApi3ServerV1();
mockApi3ServerV1.multicall.estimateGas.mockRejectedValue(new Error('some-error'));
jest.spyOn(logger, 'info');
jest.spyOn(logger, 'warn');

const gasLimit = await estimateMulticallGasLimit(
mockApi3ServerV1 as unknown as Api3ServerV1,
['0xBeaconId1Calldata', '0xBeaconId2Calldata', '0xBeaconSetCalldata'],
undefined
);

expect(gasLimit).toBeNull();
expect(logger.info).toHaveBeenCalledTimes(1);
expect(logger.info).toHaveBeenCalledWith('No fallback gas limit provided. No gas limit to use.');
expect(logger.warn).toHaveBeenCalledTimes(1);
expect(logger.warn).toHaveBeenCalledWith('Unable to estimate gas using provider.', {
errorMessage: 'some-error',
});
});

it('detects a contract revert due to timestamp check', async () => {
const mockApi3ServerV1 = generateMockApi3ServerV1();
mockApi3ServerV1.multicall.estimateGas.mockRejectedValue(new Error('Does not update timestamp'));
jest.spyOn(logger, 'info');
jest.spyOn(logger, 'warn');

const gasLimit = await estimateMulticallGasLimit(
mockApi3ServerV1 as unknown as Api3ServerV1,
['0xBeaconId1Calldata', '0xBeaconId2Calldata', '0xBeaconSetCalldata'],
2_000_000
);

expect(gasLimit).toBe(2_000_000n);
expect(logger.info).toHaveBeenCalledTimes(1);
expect(logger.info).toHaveBeenCalledWith('Gas estimation failed because of a contract revert.', {
errorMessage: 'Does not update timestamp',
});
expect(logger.warn).toHaveBeenCalledTimes(0);
});
});

describe(handleRpcGasLimitFailure.name, () => {
it('uses a fallback gas limit', () => {
expect(handleRpcGasLimitFailure(new Error('some-error'), 2_000_000)).toStrictEqual(BigInt(2_000_000));
});

it('returns null if no gas limit is provided', () => {
expect(handleRpcGasLimitFailure(new Error('some-error'), undefined)).toBeNull();
});

it('logs a warning for unknown rpc error', () => {
jest.spyOn(logger, 'warn');

handleRpcGasLimitFailure(new Error('some-error'), 2_000_000);

expect(logger.warn).toHaveBeenCalledTimes(1);
expect(logger.warn).toHaveBeenCalledWith('Unable to estimate gas using provider.', { errorMessage: 'some-error' });
});

it('logs info message when on contract revert error', () => {
jest.spyOn(logger, 'info');

handleRpcGasLimitFailure(new Error('Does not update timestamp'), 2_000_000);

expect(logger.info).toHaveBeenCalledTimes(1);
expect(logger.info).toHaveBeenCalledWith('Gas estimation failed because of a contract revert.', {
errorMessage: 'Does not update timestamp',
});
});
});

describe(estimateSingleBeaconGasLimit.name, () => {
it('estimates the gas limit for a single beacon update', async () => {
const mockApi3ServerV1 = generateMockApi3ServerV1();
mockApi3ServerV1.updateBeaconWithSignedData.estimateGas.mockResolvedValueOnce(BigInt(500_000));

const gasLimit = await estimateSingleBeaconGasLimit(
mockApi3ServerV1 as unknown as Api3ServerV1,
{
beaconId: '0xBeaconId',
signedData: {
airnode: '0xAirnode',
templateId: '0xTemplateId',
timestamp: '1000000',
encodedValue: '0xEncodedValue',
signature: '0xSignature',
},
},
undefined
);

expect(gasLimit).toStrictEqual(BigInt(500_000));
});
});
60 changes: 60 additions & 0 deletions src/update-feeds-loops/gas-estimation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { Api3ServerV1 } from '@api3/contracts';
import { go } from '@api3/promise-utils';

import { logger } from '../logger';

import type { UpdatableBeacon } from './get-updatable-feeds';

export const handleRpcGasLimitFailure = (error: Error, fallbackGasLimit: number | undefined) => {
const errorMessage = error.message;
// It is possible that the gas estimation failed because of a contract revert due to timestamp check, because the feed
// was updated by other provider in the meantime. Try to detect this expected case and log INFO instead.
if (errorMessage.includes('Does not update timestamp')) {
logger.info(`Gas estimation failed because of a contract revert.`, { errorMessage });
} else {
logger.warn(`Unable to estimate gas using provider.`, { errorMessage });
}

if (!fallbackGasLimit) {
// Logging it as an INFO because in practice this would result in double logging of the same issue. If there is no
// fallback gas limit specified it's expected that the update transcation will be skipped in case of gas limit
// estimation failure.
logger.info('No fallback gas limit provided. No gas limit to use.');
return null;
}

return BigInt(fallbackGasLimit);
};

export const estimateSingleBeaconGasLimit = async (
api3ServerV1: Api3ServerV1,
beacon: UpdatableBeacon,
fallbackGasLimit: number | undefined
) => {
const { signedData } = beacon;

const goEstimateGas = await go(async () =>
api3ServerV1.updateBeaconWithSignedData.estimateGas(
signedData.airnode,
signedData.templateId,
signedData.timestamp,
signedData.encodedValue,
signedData.signature
)
);
if (goEstimateGas.success) return BigInt(goEstimateGas.data);
return handleRpcGasLimitFailure(goEstimateGas.error, fallbackGasLimit);
};

export const estimateMulticallGasLimit = async (
api3ServerV1: Api3ServerV1,
calldatas: string[],
fallbackGasLimit: number | undefined
) => {
const goEstimateGas = await go(async () => api3ServerV1.multicall.estimateGas(calldatas));
if (goEstimateGas.success) {
// Adding a extra 10% because multicall consumes less gas than tryMulticall
return (goEstimateGas.data * BigInt(Math.round(1.1 * 100))) / 100n;
}
return handleRpcGasLimitFailure(goEstimateGas.error, fallbackGasLimit);
};
Loading

0 comments on commit 59a0ad9

Please sign in to comment.