-
Notifications
You must be signed in to change notification settings - Fork 5
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
Fetch gas price using low-level RPC method #321
Changes from all commits
e149df5
bed77bf
36a95ea
9b49c52
48dfeab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
import type { Hex } from '@api3/commons'; | ||
import type { AirseekerRegistry } from '@api3/contracts'; | ||
import { ethers } from 'ethers'; | ||
import { ethers, toBeHex } from 'ethers'; | ||
import { omit } from 'lodash'; | ||
|
||
import { generateTestConfig } from '../../test/fixtures/mock-config'; | ||
|
@@ -128,8 +128,12 @@ describe(updateFeedsLoopsModule.startUpdateFeedsLoops.name, () => { | |
|
||
describe(updateFeedsLoopsModule.runUpdateFeeds.name, () => { | ||
it('aborts when fetching first data feed batch fails', async () => { | ||
const getBlockNumberSpy = jest.fn().mockResolvedValue(123n); | ||
jest | ||
.spyOn(contractsModule, 'createProvider') | ||
.mockResolvedValue({ getBlockNumber: getBlockNumberSpy } as any as ethers.JsonRpcProvider); | ||
|
||
const airseekerRegistry = generateMockAirseekerRegistry(); | ||
jest.spyOn(contractsModule, 'createProvider').mockResolvedValue(123 as any as ethers.JsonRpcProvider); | ||
jest | ||
.spyOn(contractsModule, 'getAirseekerRegistry') | ||
.mockReturnValue(airseekerRegistry as unknown as AirseekerRegistry); | ||
|
@@ -157,6 +161,66 @@ describe(updateFeedsLoopsModule.runUpdateFeeds.name, () => { | |
); | ||
}); | ||
|
||
it('handles error when fetching block number call fails', async () => { | ||
// Prepare the mocked contract so it returns two batch (of size 2) of data feeds. | ||
const firstDataFeed = generateActiveDataFeedResponse(); | ||
const secondDataFeed = generateActiveDataFeedResponse(); | ||
const getBlockNumberSpy = jest.fn(); | ||
// Prepare the first batch to be fetched successfully and the second batch to fail. | ||
getBlockNumberSpy.mockResolvedValueOnce(123n); | ||
getBlockNumberSpy.mockRejectedValueOnce(new Error('provider-error-get-block-number')); | ||
jest | ||
.spyOn(contractsModule, 'createProvider') | ||
.mockResolvedValue({ getBlockNumber: getBlockNumberSpy } as any as ethers.JsonRpcProvider); | ||
|
||
const airseekerRegistry = generateMockAirseekerRegistry(); | ||
jest | ||
.spyOn(contractsModule, 'getAirseekerRegistry') | ||
.mockReturnValue(airseekerRegistry as unknown as AirseekerRegistry); | ||
airseekerRegistry.interface.decodeFunctionResult.mockImplementation((_fn, value) => value); | ||
const chainId = BigInt(31_337); | ||
airseekerRegistry.tryMulticall.staticCall.mockResolvedValueOnce({ | ||
successes: [true, true, true], | ||
returndata: [2n, chainId, firstDataFeed], | ||
}); | ||
airseekerRegistry.tryMulticall.staticCall.mockResolvedValueOnce({ | ||
successes: [true, true], | ||
returndata: [chainId, secondDataFeed], | ||
}); | ||
|
||
jest.spyOn(logger, 'error'); | ||
jest.spyOn(stateModule, 'updateState').mockImplementation(); | ||
jest.spyOn(updateFeedsLoopsModule, 'processBatch').mockResolvedValueOnce({ | ||
signedApiUrlsFromConfig: [], | ||
signedApiUrlsFromContract: [], | ||
beaconIds: [], | ||
successCount: 1, | ||
errorCount: 0, | ||
}); | ||
|
||
await updateFeedsLoopsModule.runUpdateFeeds( | ||
'provider-name', | ||
allowPartial<Chain>({ | ||
dataFeedBatchSize: 1, | ||
dataFeedUpdateInterval: 0.3, // 300ms update interval to make the test run quicker. | ||
providers: { ['provider-name']: { url: 'provider-url' } }, | ||
contracts: { | ||
AirseekerRegistry: '0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0', | ||
}, | ||
}), | ||
'31337' | ||
); | ||
|
||
// Expect the logs to be called with the correct context. | ||
expect(logger.error).toHaveBeenCalledTimes(1); | ||
expect(logger.error).toHaveBeenCalledWith( | ||
'Failed to get active data feeds batch.', | ||
new Error('provider-error-get-block-number') | ||
); | ||
// Expect the processBatch to be called only once for the first batch. | ||
expect(updateFeedsLoopsModule.processBatch).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('fetches and processes other batches in a staggered way and logs errors', async () => { | ||
// Prepare the mocked contract so it returns three batches (of size 1) of data feeds and the second batch fails to load. | ||
const firstDataFeed = generateActiveDataFeedResponse(); | ||
|
@@ -182,27 +246,27 @@ describe(updateFeedsLoopsModule.runUpdateFeeds.name, () => { | |
), | ||
} as contractsModule.DecodedActiveDataFeedResponse; | ||
const airseekerRegistry = generateMockAirseekerRegistry(); | ||
const getFeeDataSpy = jest.fn().mockResolvedValue({ gasPrice: ethers.parseUnits('5', 'gwei') }); | ||
const getBlockNumberSpy = jest.fn().mockResolvedValue(123n); | ||
const getGasPriceSpy = jest.fn().mockResolvedValue(toBeHex(ethers.parseUnits('5', 'gwei'))); | ||
jest | ||
.spyOn(contractsModule, 'createProvider') | ||
.mockResolvedValue({ getFeeData: getFeeDataSpy } as any as ethers.JsonRpcProvider); | ||
.mockResolvedValue({ send: getGasPriceSpy, getBlockNumber: getBlockNumberSpy } as any as ethers.JsonRpcProvider); | ||
jest | ||
.spyOn(contractsModule, 'getAirseekerRegistry') | ||
.mockReturnValue(airseekerRegistry as unknown as AirseekerRegistry); | ||
airseekerRegistry.interface.decodeFunctionResult.mockImplementation((_fn, value) => value); | ||
const blockNumber = 123n; | ||
const chainId = BigInt(31_337); | ||
airseekerRegistry.tryMulticall.staticCall.mockResolvedValueOnce({ | ||
successes: [true, true, true, true], | ||
returndata: [3n, blockNumber, chainId, firstDataFeed], | ||
successes: [true, true, true], | ||
returndata: [3n, chainId, firstDataFeed], | ||
}); | ||
airseekerRegistry.tryMulticall.staticCall.mockResolvedValueOnce({ | ||
successes: [true, true, false], | ||
returndata: [blockNumber, chainId, '0x'], | ||
successes: [true, false], | ||
returndata: [chainId, '0x'], | ||
}); | ||
airseekerRegistry.tryMulticall.staticCall.mockResolvedValueOnce({ | ||
successes: [true, true, true], | ||
returndata: [blockNumber, chainId, thirdDataFeed], | ||
successes: [true, true], | ||
returndata: [chainId, thirdDataFeed], | ||
}); | ||
const sleepCalls = [] as number[]; | ||
const originalSleep = utilsModule.sleep; | ||
|
@@ -317,16 +381,18 @@ describe(updateFeedsLoopsModule.runUpdateFeeds.name, () => { | |
it('catches unhandled error', async () => { | ||
const dataFeed = generateActiveDataFeedResponse(); | ||
const airseekerRegistry = generateMockAirseekerRegistry(); | ||
jest.spyOn(contractsModule, 'createProvider').mockResolvedValue(123 as any as ethers.JsonRpcProvider); | ||
const getBlockNumberSpy = jest.fn().mockResolvedValue(123n); | ||
jest | ||
.spyOn(contractsModule, 'createProvider') | ||
.mockResolvedValue({ getBlockNumber: getBlockNumberSpy } as any as ethers.JsonRpcProvider); | ||
jest | ||
.spyOn(contractsModule, 'getAirseekerRegistry') | ||
.mockReturnValue(airseekerRegistry as unknown as AirseekerRegistry); | ||
airseekerRegistry.interface.decodeFunctionResult.mockImplementation((_fn, value) => value); | ||
const blockNumber = 123n; | ||
const chainId = BigInt(31_337); | ||
airseekerRegistry.tryMulticall.staticCall.mockResolvedValueOnce({ | ||
successes: [true, true, true, true], | ||
returndata: [1n, blockNumber, chainId, dataFeed], | ||
successes: [true, true, true], | ||
returndata: [1n, chainId, dataFeed], | ||
}); | ||
const testConfig = generateTestConfig(); | ||
jest.spyOn(stateModule, 'getState').mockReturnValue( | ||
|
@@ -443,6 +509,7 @@ describe(updateFeedsLoopsModule.processBatch.name, () => { | |
jest.spyOn(logger, 'info'); | ||
|
||
// Skip actions other than generating signed api urls. | ||
jest.spyOn(gasPriceModule, 'fetchAndStoreGasPrice').mockImplementation(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mhh, why do these need to be mocked now? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes it looks redundant but I wanted to add them because There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nah, leave them. I was just surprised why the test was passing, but I guess either Ethers made a noop call or silently did nothing 🤷 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test was passing probably because |
||
jest.spyOn(getUpdatableFeedsModule, 'getUpdatableFeeds').mockReturnValue([]); | ||
jest.spyOn(submitTransactionModule, 'getDerivedSponsorWallet').mockReturnValue(ethers.Wallet.createRandom()); | ||
|
||
|
@@ -485,6 +552,7 @@ describe(updateFeedsLoopsModule.processBatch.name, () => { | |
jest.spyOn(logger, 'info'); | ||
|
||
// Skip actions other than generating signed api urls. | ||
jest.spyOn(gasPriceModule, 'fetchAndStoreGasPrice').mockImplementation(); | ||
jest.spyOn(getUpdatableFeedsModule, 'getUpdatableFeeds').mockReturnValue([]); | ||
jest.spyOn(submitTransactionModule, 'getDerivedSponsorWallet').mockReturnValue(ethers.Wallet.createRandom()); | ||
|
||
|
@@ -532,6 +600,7 @@ describe(updateFeedsLoopsModule.processBatch.name, () => { | |
jest.spyOn(provider, 'getTransactionCount').mockResolvedValue(123); | ||
|
||
// Skip actions other than generating signed api urls. | ||
jest.spyOn(gasPriceModule, 'fetchAndStoreGasPrice').mockImplementation(); | ||
jest.spyOn(getUpdatableFeedsModule, 'getUpdatableFeeds').mockReturnValue([ | ||
allowPartial<getUpdatableFeedsModule.UpdatableDataFeed>({ | ||
dataFeedInfo: { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add one more test when the provider fails? I checked Ethereum RPC spec (and some other sources) and I think we can rely that when the API returns something it will be a hex string...