Skip to content

Commit

Permalink
Add clearing update timestamp for feeds with no update (#81)
Browse files Browse the repository at this point in the history
* Add clearing update timestamp for feeds with no update

* Fix tests

* Update src/update-feeds/update-feeds.ts

Co-authored-by: Emanuel Tesař <[email protected]>

---------

Co-authored-by: Emanuel Tesař <[email protected]>
  • Loading branch information
vponline and Siegrift authored Nov 8, 2023
1 parent 7907dbc commit 7ac9a9e
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 36 deletions.
17 changes: 10 additions & 7 deletions src/update-feeds/update-feeds.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<stateModule.State>({
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand All @@ -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<Chain>({
dataFeedBatchSize: 2,
Expand Down Expand Up @@ -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<Chain>({
dataFeedBatchSize: 1,
Expand Down
70 changes: 42 additions & 28 deletions src/update-feeds/update-feeds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -17,6 +18,7 @@ import {
verifyMulticallResponse,
type ReadDapiWithIndexResponse,
} from './dapi-data-registry';
import { deriveSponsorWallet } from './update-transactions';

export const startUpdateFeedLoops = async () => {
const state = getState();
Expand Down Expand Up @@ -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<typeof getFeedsToUpdate>, _chainId: string) => {
// TODO implement
Expand All @@ -164,6 +161,9 @@ export const updateFeeds = async (_batch: ReturnType<typeof getFeedsToUpdate>, _

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) {
Expand All @@ -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
);
};
4 changes: 4 additions & 0 deletions test/e2e/update-feeds.feature.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -30,6 +31,7 @@ it('reads blockchain data', async () => {

it('updates blockchain data', async () => {
const {
config,
api3ServerV1,
dapiDataRegistry,
krakenBtcBeacon,
Expand All @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion test/fixtures/mock-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down

0 comments on commit 7ac9a9e

Please sign in to comment.