|
| 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