diff --git a/src/update-feeds/update-feeds.test.ts b/src/update-feeds/update-feeds.test.ts index 1b203589..0ec01816 100644 --- a/src/update-feeds/update-feeds.test.ts +++ b/src/update-feeds/update-feeds.test.ts @@ -10,11 +10,11 @@ import * as stateModule from '../state'; import * as utilsModule from '../utils'; import * as dapiDataRegistryModule from './dapi-data-registry'; -import { runUpdateFeed, startUpdateFeedLoops } from './update-feeds'; +import * as updateFeedsModule from './update-feeds'; jest.mock('../state'); -describe(startUpdateFeedLoops.name, () => { +describe(updateFeedsModule.startUpdateFeedLoops.name, () => { it('starts staggered update loops for a chain', async () => { jest.spyOn(stateModule, 'getState').mockReturnValue( allowPartial({ @@ -37,7 +37,7 @@ describe(startUpdateFeedLoops.name, () => { }) as any); jest.spyOn(logger, 'debug'); - await startUpdateFeedLoops(); + await updateFeedsModule.startUpdateFeedLoops(); // Expect the intervals to be called with the correct stagger time. expect(setInterval).toHaveBeenCalledTimes(2); @@ -87,7 +87,7 @@ describe(startUpdateFeedLoops.name, () => { }) as any); jest.spyOn(logger, 'debug'); - await startUpdateFeedLoops(); + await updateFeedsModule.startUpdateFeedLoops(); // Expect the intervals to be called with the correct stagger time. expect(setInterval).toHaveBeenCalledTimes(2); @@ -116,7 +116,7 @@ describe(startUpdateFeedLoops.name, () => { }); }); -describe(runUpdateFeed.name, () => { +describe(updateFeedsModule.runUpdateFeed.name, () => { it('aborts when fetching first dAPIs batch fails', async () => { const dapiDataRegistry = generateMockDapiDataRegistry(); jest @@ -125,7 +125,7 @@ describe(runUpdateFeed.name, () => { dapiDataRegistry.callStatic.tryMulticall.mockRejectedValueOnce(new Error('provider-error')); jest.spyOn(logger, 'error'); - await runUpdateFeed( + await updateFeedsModule.runUpdateFeed( 'provider-name', allowPartial({ dataFeedBatchSize: 2, @@ -185,8 +185,11 @@ describe(runUpdateFeed.name, () => { }, }) ); + jest + .spyOn(updateFeedsModule, 'getFeedsToUpdate') + .mockImplementation((feeds) => feeds.map((feed) => ({ ...feed, shouldUpdate: true }))); - await runUpdateFeed( + await updateFeedsModule.runUpdateFeed( 'provider-name', allowPartial({ dataFeedBatchSize: 1, diff --git a/src/update-feeds/update-feeds.ts b/src/update-feeds/update-feeds.ts index 9af89f3a..3e00e9c3 100644 --- a/src/update-feeds/update-feeds.ts +++ b/src/update-feeds/update-feeds.ts @@ -4,6 +4,7 @@ import { range, size } from 'lodash'; import { checkUpdateConditions } from '../condition-check'; import type { Chain } from '../config/schema'; +import { clearSponsorLastUpdateTimestampMs } from '../gas-price/gas-price'; import { logger } from '../logger'; import { getStoreDataPoint } from '../signed-data-store'; import { getState, updateState } from '../state'; @@ -17,6 +18,7 @@ import { verifyMulticallResponse, type ReadDapiWithIndexResponse, } from './dapi-data-registry'; +import { deriveSponsorWallet } from './update-transactions'; export const startUpdateFeedLoops = async () => { const state = getState(); @@ -127,35 +129,30 @@ export const runUpdateFeed = async (providerName: Provider, chain: Chain, chainI }; export const getFeedsToUpdate = (batch: ReadDapiWithIndexResponse[]) => - batch - .map((dapiResponse: ReadDapiWithIndexResponse) => { - const signedData = getStoreDataPoint(dapiResponse.decodedDataFeed.dataFeedId); - - return { - ...dapiResponse, - signedData, - }; - }) - .filter(({ signedData, updateParameters, dataFeedValue }) => { - if (!signedData) { - return false; - } - - const offChainValue = ethers.BigNumber.from(signedData.encodedValue); - const offChainTimestamp = Number.parseInt(signedData?.timestamp ?? '0', 10); - const deviationThreshold = updateParameters.deviationThresholdInPercentage; + batch.map((dapiResponse: ReadDapiWithIndexResponse) => { + const signedData = getStoreDataPoint(dapiResponse.decodedDataFeed.dataFeedId); + if (!signedData) { + return { ...dapiResponse, shouldUpdate: false }; + } - // TODO clear last update timestamps if an update is not needed + const offChainValue = ethers.BigNumber.from(signedData.encodedValue); + const offChainTimestamp = Number.parseInt(signedData?.timestamp ?? '0', 10); + const deviationThreshold = dapiResponse.updateParameters.deviationThresholdInPercentage; + const shouldUpdate = checkUpdateConditions( + dapiResponse.dataFeedValue.value, + dapiResponse.dataFeedValue.timestamp, + offChainValue, + offChainTimestamp, + dapiResponse.updateParameters.heartbeatInterval, + deviationThreshold + ); - return checkUpdateConditions( - dataFeedValue.value, - dataFeedValue.timestamp, - offChainValue, - offChainTimestamp, - updateParameters.heartbeatInterval, - deviationThreshold - ); - }); + return { + ...dapiResponse, + signedData, + shouldUpdate, + }; + }); export const updateFeeds = async (_batch: ReturnType, _chainId: string) => { // TODO implement @@ -164,6 +161,9 @@ export const updateFeeds = async (_batch: ReturnType, _ export const processBatch = async (batch: ReadDapiWithIndexResponse[], providerName: Provider, chainId: string) => { logger.debug('Processing batch of active dAPIs', { batch }); + const { + config: { sponsorWalletMnemonic }, + } = getState(); updateState((draft) => { for (const dapi of batch) { @@ -186,5 +186,19 @@ export const processBatch = async (batch: ReadDapiWithIndexResponse[], providerN } }); - return updateFeeds(getFeedsToUpdate(batch), chainId); + const feeds = getFeedsToUpdate(batch); + + // Clear last update timestamps for feeds that don't need an update + for (const feed of feeds.filter((feed) => !feed.shouldUpdate)) { + clearSponsorLastUpdateTimestampMs( + chainId, + providerName, + deriveSponsorWallet(sponsorWalletMnemonic, feed.dapiName).address + ); + } + + return updateFeeds( + feeds.filter((feed) => feed.shouldUpdate), + chainId + ); }; diff --git a/test/e2e/update-feeds.feature.ts b/test/e2e/update-feeds.feature.ts index 3c2be649..c1988ebb 100644 --- a/test/e2e/update-feeds.feature.ts +++ b/test/e2e/update-feeds.feature.ts @@ -1,6 +1,7 @@ import { ethers } from 'ethers'; import { omit } from 'lodash'; +import { initializeGasStore } from '../../src/gas-price/gas-price'; import { logger } from '../../src/logger'; import * as stateModule from '../../src/state'; import { runUpdateFeed } from '../../src/update-feeds'; @@ -30,6 +31,7 @@ it('reads blockchain data', async () => { it('updates blockchain data', async () => { const { + config, api3ServerV1, dapiDataRegistry, krakenBtcBeacon, @@ -39,6 +41,8 @@ it('updates blockchain data', async () => { airseekerSponsorWallet, walletFunder, } = await deployAndUpdate(); + init({ config }); + initializeGasStore(chainId, providerName); const btcDapi = await dapiDataRegistry.readDapiWithIndex(0); const decodedDataFeed = decodeDataFeed(btcDapi.dataFeed); diff --git a/test/fixtures/mock-contract.ts b/test/fixtures/mock-contract.ts index b54f90c8..84fbb3f9 100644 --- a/test/fixtures/mock-contract.ts +++ b/test/fixtures/mock-contract.ts @@ -5,7 +5,7 @@ import type { DapiDataRegistry } from '../../typechain-types'; import { type DeepPartial, encodeBeaconFeed } from '../utils'; export const generateReadDapiWithIndexResponse = () => ({ - dapiName: 'MOCK_FEED', + dapiName: ethers.utils.formatBytes32String('MOCK_FEED'), updateParameters: { deviationThresholdInPercentage: ethers.BigNumber.from(0.5 * 1e8), deviationReference: ethers.BigNumber.from(0.5 * 1e8),