Skip to content

Commit 287e763

Browse files
authored
Check which beacons of a beacon set need to be updated (#95)
* Initial rewrite of deeply checked feeds * Rebuild tests * Minor refactor * Minor refactor * PR comments * Fix minor issue * Remove dummy gas estimation * WIP rewrite deep check function * Fix tests * Add minor comment * PR comments
1 parent 47f69d7 commit 287e763

10 files changed

+422
-131
lines changed

src/signed-data-store/signed-data-store.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ethers } from 'ethers';
33

44
import { logger } from '../logger';
55
import { getState, updateState } from '../state';
6-
import type { SignedData, AirnodeAddress, TemplateId, DataFeedId } from '../types';
6+
import type { SignedData, AirnodeAddress, TemplateId, BeaconId } from '../types';
77
import { deriveBeaconId } from '../utils';
88

99
export const verifySignedData = ({ airnode, templateId, timestamp, signature, encodedValue }: SignedData) => {
@@ -90,7 +90,7 @@ export const setStoreDataPoint = (signedData: SignedData) => {
9090
export const getStoreDataPointByAirnodeAndTemplate = (airnode: AirnodeAddress, template: TemplateId) =>
9191
getState().signedApiStore[deriveBeaconId(airnode, template)!];
9292

93-
export const getStoreDataPoint = (dataFeedId: DataFeedId) => getState().signedApiStore[dataFeedId];
93+
export const getStoreDataPoint = (dataFeedId: BeaconId) => getState().signedApiStore[dataFeedId];
9494

9595
export const clear = () => {
9696
updateState((draft) => {

src/state/state.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type {
88
DecodedDataFeed,
99
ChainId,
1010
SignedData,
11-
DataFeedId,
11+
BeaconId,
1212
ProviderName,
1313
SignedApiUrl,
1414
AirnodeAddress,
@@ -41,7 +41,7 @@ export interface State {
4141
dataFetcherInterval?: NodeJS.Timeout;
4242
gasPriceStore: Record<ChainId, Record<ProviderName, GasState>>;
4343
derivedSponsorWallets: Record<DapiName, PrivateKey>;
44-
signedApiStore: Record<DataFeedId, SignedData>;
44+
signedApiStore: Record<BeaconId, SignedData>;
4545
signedApiUrlStore: Record<ChainId, Record<ProviderName, Record<AirnodeAddress, SignedApiUrl>>>;
4646
dapis: Record<DapiName, DapiState>;
4747
}

src/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export type AirnodeAddress = EvmAddress;
66
export type TemplateId = EvmId;
77
export type DapiName = string;
88
export type PrivateKey = string;
9-
export type DataFeedId = EvmId;
9+
export type BeaconId = EvmId;
1010
export type ChainId = string;
1111
export type ProviderName = string;
1212
export type SignedApiUrl = string;

src/update-feeds/check-feeds.test.ts

+197
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import { ethers } from 'ethers';
2+
3+
import { initializeState } from '../../test/fixtures/mock-config';
4+
import { allowPartial } from '../../test/utils';
5+
import * as signedDataStore from '../signed-data-store/signed-data-store';
6+
import { updateState } from '../state';
7+
import type { BeaconId, SignedData } from '../types';
8+
9+
import * as contractUtils from './api3-server-v1';
10+
import { multicallBeaconValues, getUpdatableFeeds } from './check-feeds';
11+
import * as checkFeedsModule from './check-feeds';
12+
import type { ReadDapiWithIndexResponse } from './dapi-data-registry';
13+
14+
// https://github.com/api3dao/airnode-protocol-v1/blob/fa95f043ce4b50e843e407b96f7ae3edcf899c32/contracts/api3-server-v1/DataFeedServer.sol#L132
15+
const encodeBeaconValue = (numericValue: string) => {
16+
const numericValueAsBigNumber = ethers.BigNumber.from(numericValue);
17+
18+
return ethers.utils.defaultAbiCoder.encode(['int256'], [numericValueAsBigNumber]);
19+
};
20+
21+
describe('getUpdatableFeeds', () => {
22+
const feedIds = [
23+
'0xf5c140bcb4814dfec311d38f6293e86c02d32ba1b7da027fe5b5202cae35dbc6',
24+
'0xf5c140bcb4814dfec311d38f6293e86c02d32ba1b7da027fe5b5202cae35dbc7',
25+
'0xf5c140bcb4814dfec311d38f6293e86c02d32ba1b7da027fe5b5202cae35dbc8',
26+
] as const;
27+
28+
beforeEach(() => {
29+
initializeState();
30+
updateState((draft) => {
31+
draft.signedApiStore = allowPartial<Record<BeaconId, SignedData>>({
32+
'0x000a': { timestamp: '100', encodedValue: encodeBeaconValue('200') },
33+
'0x000b': { timestamp: '150', encodedValue: encodeBeaconValue('250') },
34+
'0x000c': { timestamp: '200', encodedValue: encodeBeaconValue('300') },
35+
});
36+
});
37+
});
38+
39+
it('calls and parses a multicall', async () => {
40+
const tryMulticallMock = jest.fn().mockReturnValue({
41+
successes: [true, true, true],
42+
returndata: [
43+
ethers.utils.defaultAbiCoder.encode(['int224', 'uint32'], [100, 105]),
44+
ethers.utils.defaultAbiCoder.encode(['int224', 'uint32'], [101, 106]),
45+
ethers.utils.defaultAbiCoder.encode(['int224', 'uint32'], [102, 107]),
46+
],
47+
});
48+
49+
const encodeFunctionDataMock = jest.fn();
50+
encodeFunctionDataMock.mockReturnValueOnce('0xfirst');
51+
encodeFunctionDataMock.mockReturnValueOnce('0xsecond');
52+
encodeFunctionDataMock.mockReturnValueOnce('0xthird');
53+
54+
const mockContract = {
55+
connect: jest.fn().mockReturnValue({
56+
callStatic: {
57+
tryMulticall: tryMulticallMock,
58+
},
59+
}),
60+
interface: { encodeFunctionData: encodeFunctionDataMock },
61+
};
62+
63+
jest.spyOn(contractUtils, 'getApi3ServerV1').mockReturnValue(mockContract as any);
64+
65+
const callAndParseMulticallPromise = multicallBeaconValues(feedIds as unknown as string[], 'hardhat', '31337');
66+
67+
await expect(callAndParseMulticallPromise).resolves.toStrictEqual([
68+
{
69+
beaconId: '0xf5c140bcb4814dfec311d38f6293e86c02d32ba1b7da027fe5b5202cae35dbc6',
70+
onChainValue: {
71+
timestamp: ethers.BigNumber.from(105),
72+
value: ethers.BigNumber.from(100),
73+
},
74+
},
75+
{
76+
beaconId: '0xf5c140bcb4814dfec311d38f6293e86c02d32ba1b7da027fe5b5202cae35dbc7',
77+
onChainValue: {
78+
timestamp: ethers.BigNumber.from(106),
79+
value: ethers.BigNumber.from(101),
80+
},
81+
},
82+
{
83+
beaconId: '0xf5c140bcb4814dfec311d38f6293e86c02d32ba1b7da027fe5b5202cae35dbc8',
84+
onChainValue: {
85+
timestamp: ethers.BigNumber.from(107),
86+
value: ethers.BigNumber.from(102),
87+
},
88+
},
89+
]);
90+
expect(tryMulticallMock).toHaveBeenCalledWith(['0xfirst', '0xsecond', '0xthird']);
91+
});
92+
93+
it('returns updatable feeds', async () => {
94+
jest.useFakeTimers().setSystemTime(90);
95+
96+
const multicallResult = [
97+
{
98+
beaconId: '0xf5c140bcb4814dfec311d38f6293e86c02d32ba1b7da027fe5b5202cae35dbc6',
99+
onChainValue: {
100+
timestamp: ethers.BigNumber.from(150),
101+
value: ethers.BigNumber.from('400'),
102+
},
103+
},
104+
{
105+
beaconId: '0xf5c140bcb4814dfec311d38f6293e86c02d32ba1b7da027fe5b5202cae35dbc7',
106+
onChainValue: {
107+
timestamp: ethers.BigNumber.from(160),
108+
value: ethers.BigNumber.from('500'),
109+
},
110+
},
111+
{
112+
beaconId: '0xf5c140bcb4814dfec311d38f6293e86c02d32ba1b7da027fe5b5202cae35dbc8',
113+
onChainValue: {
114+
timestamp: ethers.BigNumber.from(170),
115+
value: ethers.BigNumber.from('600'),
116+
},
117+
},
118+
];
119+
120+
// Only the third feed will satisfy the timestamp check
121+
const mockSignedDataStore = allowPartial<Record<string, SignedData>>({
122+
[feedIds[0]]: {
123+
timestamp: '101',
124+
encodedValue: encodeBeaconValue('200'),
125+
},
126+
[feedIds[1]]: {
127+
timestamp: '150',
128+
encodedValue: encodeBeaconValue('250'),
129+
},
130+
[feedIds[2]]: {
131+
timestamp: '200',
132+
encodedValue: encodeBeaconValue('300'),
133+
},
134+
});
135+
136+
const getStoreDataPointSpy = jest.spyOn(signedDataStore, 'getStoreDataPoint');
137+
getStoreDataPointSpy.mockImplementation((dataFeedId: string) => mockSignedDataStore[dataFeedId]!);
138+
139+
// None of the feeds failed to update
140+
jest.spyOn(checkFeedsModule, 'multicallBeaconValues').mockResolvedValue(multicallResult);
141+
142+
const batch = allowPartial<ReadDapiWithIndexResponse[]>([
143+
{
144+
updateParameters: { deviationThresholdInPercentage: ethers.BigNumber.from(1) },
145+
dataFeedValue: {
146+
value: ethers.BigNumber.from(10),
147+
timestamp: 95,
148+
},
149+
decodedDataFeed: {
150+
dataFeedId: '0x000',
151+
beacons: [{ beaconId: feedIds[0] }, { beaconId: feedIds[1] }, { beaconId: feedIds[2] }],
152+
},
153+
dapiName: 'test',
154+
},
155+
]);
156+
157+
const checkFeedsResult = getUpdatableFeeds(batch, 1, 'hardhat', '31337');
158+
159+
await expect(checkFeedsResult).resolves.toStrictEqual([
160+
{
161+
updatableBeacons: [
162+
expect.objectContaining({
163+
beaconId: '0xf5c140bcb4814dfec311d38f6293e86c02d32ba1b7da027fe5b5202cae35dbc8',
164+
signedData: {
165+
encodedValue: '0x000000000000000000000000000000000000000000000000000000000000012c',
166+
timestamp: '200',
167+
},
168+
}),
169+
],
170+
dapiInfo: {
171+
dapiName: 'test',
172+
dataFeedValue: {
173+
timestamp: 95,
174+
value: ethers.BigNumber.from('10'),
175+
},
176+
decodedDataFeed: {
177+
beacons: [
178+
expect.objectContaining({
179+
beaconId: '0xf5c140bcb4814dfec311d38f6293e86c02d32ba1b7da027fe5b5202cae35dbc6',
180+
}),
181+
expect.objectContaining({
182+
beaconId: '0xf5c140bcb4814dfec311d38f6293e86c02d32ba1b7da027fe5b5202cae35dbc7',
183+
}),
184+
expect.objectContaining({
185+
beaconId: '0xf5c140bcb4814dfec311d38f6293e86c02d32ba1b7da027fe5b5202cae35dbc8',
186+
}),
187+
],
188+
dataFeedId: '0x000',
189+
},
190+
updateParameters: {
191+
deviationThresholdInPercentage: ethers.BigNumber.from(1),
192+
},
193+
},
194+
},
195+
]);
196+
});
197+
});

0 commit comments

Comments
 (0)