diff --git a/src/update-data-feeds.test.ts b/src/update-data-feeds.test.ts index b99fe85d..20eaf87d 100644 --- a/src/update-data-feeds.test.ts +++ b/src/update-data-feeds.test.ts @@ -300,6 +300,146 @@ describe('updateDataFeedsInLoop', () => { }); }); +describe('updateBeacons', () => { + it('calls updateBeaconWithSignedData in Api3ServerV1 contract for single beacon', async () => { + state.updateState((currentState) => ({ + ...currentState, + beaconValues: { + '0x2ba0526238b0f2671b7981fd7a263730619c8e849a528088fd4a92350a8c2f2c': validSignedData, + '0xa5ddf304a7dcec62fa55449b7fe66b33339fd8b249db06c18423d5b0da7716c2': undefined as any, + '0x8fa9d00cb8f2d95b1299623d97a97696ed03d0e3350e4ea638f469beabcdabcd': validSignedData, + }, + })); + + const txCountSpy = jest.spyOn(ethers.providers.StaticJsonRpcProvider.prototype, 'getTransactionCount'); + txCountSpy.mockResolvedValueOnce(212); + + const timestamp = 1649664085; + + const updateBeaconWithSignedDataMock = jest + .fn() + .mockReturnValueOnce({ hash: ethers.utils.hexlify(ethers.utils.randomBytes(32)) }); + const callStaticTryMulticallMock = jest.fn().mockReturnValueOnce({ + successes: [true, true], + returndata: [ + ethers.utils.defaultAbiCoder.encode(['int224', 'uint32'], [ethers.BigNumber.from(41000000000), timestamp - 30]), + ethers.utils.defaultAbiCoder.encode(['int224', 'uint32'], [ethers.BigNumber.from(40000000000), timestamp]), + ], + }); + jest.spyOn(Api3ServerV1Factory, 'connect').mockImplementation( + (_dapiServerAddress, _provider) => + ({ + connect(_signerOrProvider: ethers.Signer | ethers.providers.Provider | string) { + return this; + }, + updateBeaconWithSignedData: updateBeaconWithSignedDataMock, + interface: { + encodeFunctionData: (functionFragment: string, values: [any]): string => { + if (functionFragment === 'dataFeeds') + return '0x67a7cfb741c3d6e0ee82ae3d33356c4dceb84e98d1a0b361db0f51081fc5a2541ae51683'; + + if (functionFragment === 'readDataFeedWithId') { + switch (values[0]) { + case '0x2ba0526238b0f2671b7981fd7a263730619c8e849a528088fd4a92350a8c2f2c': + return '0xa5fc076f2ba0526238b0f2671b7981fd7a263730619c8e849a528088fd4a92350a8c2f2c'; + case '0xa5ddf304a7dcec62fa55449b7fe66b33339fd8b249db06c18423d5b0da7716c2': + return '0xa5fc076fa5ddf304a7dcec62fa55449b7fe66b33339fd8b249db06c18423d5b0da7716c2'; + case '0x8fa9d00cb8f2d95b1299623d97a97696ed03d0e3350e4ea638f469beabcdabcd': + return '0xa5fc076f8fa9d00cb8f2d95b1299623d97a97696ed03d0e3350e4ea638f469beabcdabcd'; + } + } + + if (functionFragment === 'updateBeaconWithSignedData') + return '0x1a0a0b3e0000000000000000000000005656d3a378b1aadfddcf4196ea364a9d786172909ec34b00a5019442dcd05a4860ff2bf015164b368cb83fcb756088fcabcdabcd000000000000000000000000000000000000000000000000000000006253e05500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000009dc41b78000000000000000000000000000000000000000000000000000000000000000418aace553ec28f53cc976c8a2469d50f16de121d248495117aca36feb4950957827570e0648f82bdbc0afa6cb69dd9fe37dc7f9d58ae3aa06450e627e06c1b8031b00000000000000000000000000000000000000000000000000000000000000'; + + if (functionFragment === 'updateBeaconSetWithBeacons') + return '0x00aae33f00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002924b5d4cb3ec6366ae4302a1ca6aec035594ea3ea48a102d160b50b0c43ebfb5bf7ce55d109fd196de2a8bf1515d166c56c9decbe9cb473656bbca30d5743990'; + + return ''; + }, + }, + callStatic: { tryMulticall: callStaticTryMulticallMock }, + } as any) + ); + + const groups = api.groupDataFeedsByProviderSponsor(); + + await api.updateBeacons(groups[0], Date.now()); + + expect(callStaticTryMulticallMock).toHaveBeenCalledTimes(1); + expect(updateBeaconWithSignedDataMock).toHaveBeenCalledTimes(1); + }); + + it('calls tryMulticall in Api3ServerV1 contract for multiple beacon updates', async () => { + state.updateState((currentState) => ({ + ...currentState, + beaconValues: { + '0x2ba0526238b0f2671b7981fd7a263730619c8e849a528088fd4a92350a8c2f2c': validSignedData, + '0xa5ddf304a7dcec62fa55449b7fe66b33339fd8b249db06c18423d5b0da7716c2': validSignedData, + '0x8fa9d00cb8f2d95b1299623d97a97696ed03d0e3350e4ea638f469beabcdabcd': validSignedData, + }, + })); + + const txCountSpy = jest.spyOn(ethers.providers.StaticJsonRpcProvider.prototype, 'getTransactionCount'); + txCountSpy.mockResolvedValueOnce(212); + + const timestamp = 1649664085; + + const tryMulticallMock = jest + .fn() + .mockReturnValueOnce({ hash: ethers.utils.hexlify(ethers.utils.randomBytes(32)) }); + const callStaticTryMulticallMock = jest.fn().mockReturnValueOnce({ + successes: [true, true], + returndata: [ + ethers.utils.defaultAbiCoder.encode(['int224', 'uint32'], [ethers.BigNumber.from(41000000000), timestamp - 30]), + ethers.utils.defaultAbiCoder.encode(['int224', 'uint32'], [ethers.BigNumber.from(39000000000), timestamp - 30]), + ], + }); + jest.spyOn(Api3ServerV1Factory, 'connect').mockImplementation( + (_dapiServerAddress, _provider) => + ({ + connect(_signerOrProvider: ethers.Signer | ethers.providers.Provider | string) { + return this; + }, + tryMulticall: tryMulticallMock, + interface: { + encodeFunctionData: (functionFragment: string, values: [any]): string => { + if (functionFragment === 'dataFeeds') + return '0x67a7cfb741c3d6e0ee82ae3d33356c4dceb84e98d1a0b361db0f51081fc5a2541ae51683'; + + if (functionFragment === 'readDataFeedWithId') { + switch (values[0]) { + case '0x2ba0526238b0f2671b7981fd7a263730619c8e849a528088fd4a92350a8c2f2c': + return '0xa5fc076f2ba0526238b0f2671b7981fd7a263730619c8e849a528088fd4a92350a8c2f2c'; + case '0xa5ddf304a7dcec62fa55449b7fe66b33339fd8b249db06c18423d5b0da7716c2': + return '0xa5fc076fa5ddf304a7dcec62fa55449b7fe66b33339fd8b249db06c18423d5b0da7716c2'; + case '0x8fa9d00cb8f2d95b1299623d97a97696ed03d0e3350e4ea638f469beabcdabcd': + return '0xa5fc076f8fa9d00cb8f2d95b1299623d97a97696ed03d0e3350e4ea638f469beabcdabcd'; + } + } + + if (functionFragment === 'updateBeaconWithSignedData') + return '0x1a0a0b3e0000000000000000000000005656d3a378b1aadfddcf4196ea364a9d786172909ec34b00a5019442dcd05a4860ff2bf015164b368cb83fcb756088fcabcdabcd000000000000000000000000000000000000000000000000000000006253e05500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000009dc41b78000000000000000000000000000000000000000000000000000000000000000418aace553ec28f53cc976c8a2469d50f16de121d248495117aca36feb4950957827570e0648f82bdbc0afa6cb69dd9fe37dc7f9d58ae3aa06450e627e06c1b8031b00000000000000000000000000000000000000000000000000000000000000'; + + if (functionFragment === 'updateBeaconSetWithBeacons') + return '0x00aae33f00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002924b5d4cb3ec6366ae4302a1ca6aec035594ea3ea48a102d160b50b0c43ebfb5bf7ce55d109fd196de2a8bf1515d166c56c9decbe9cb473656bbca30d5743990'; + + return ''; + }, + }, + callStatic: { tryMulticall: callStaticTryMulticallMock }, + } as any) + ); + + const groups = api.groupDataFeedsByProviderSponsor(); + + await api.updateBeacons(groups[0], Date.now()); + + expect(callStaticTryMulticallMock).toHaveBeenCalledTimes(1); + expect(tryMulticallMock).toHaveBeenCalledTimes(1); + }); +}); + describe('updateBeaconSets', () => { it('calls updateBeaconSetWithBeacons in Api3ServerV1 contract', async () => { state.updateState((currentState) => ({ diff --git a/src/update-data-feeds.ts b/src/update-data-feeds.ts index c5b7f0ac..24e2f8ed 100644 --- a/src/update-data-feeds.ts +++ b/src/update-data-feeds.ts @@ -238,25 +238,25 @@ export const updateBeacons = async (providerSponsorDataFeeds: ProviderSponsorDat for (const readBatch of chunk(beaconUpdates, DATAFEED_READ_BATCH_SIZE)) { // Read beacon batch onchain values const goDatafeedsTryMulticall = await go( - () => { - const calldatas = readBatch.map((beaconUpdate) => beaconUpdate.dataFeedsCalldata); - return contract.connect(voidSigner).callStatic.tryMulticall(calldatas); - }, + () => + contract + .connect(voidSigner) + .callStatic.tryMulticall(readBatch.map((beaconUpdate) => beaconUpdate.dataFeedsCalldata)), { ...prepareGoOptions(startTime, totalTimeout), onAttemptError: (goError) => - logger.warn(`Failed attempt to read beacon data using multicall. Error ${goError.error}`, logOptions), + logger.warn(`Attempt to read beacon data using tryMulticall has failed. Error ${goError.error}`, logOptions), } ); if (!goDatafeedsTryMulticall.success) { - logger.warn(`Unable to read beacon data using multicall. Error: ${goDatafeedsTryMulticall.error}`, logOptions); + logger.warn(`Unable to read beacon data using tryMulticall. Error: ${goDatafeedsTryMulticall.error}`, logOptions); continue; } const { successes, returndata } = goDatafeedsTryMulticall.data; // Process beacon update calldatas - let beaconUpdateCalldatas: string[] = []; + let beaconUpdates: BeaconUpdate[] = []; for (let i = 0; i < readBatch.length; i++) { const beaconReturndata = returndata[i]; @@ -286,40 +286,75 @@ export const updateBeacons = async (providerSponsorDataFeeds: ProviderSponsorDat continue; } - beaconUpdateCalldatas = [ - ...beaconUpdateCalldatas, - contract.interface.encodeFunctionData('updateBeaconWithSignedData', [ - beaconUpdateData.beacon.airnode, - beaconUpdateData.beacon.templateId, - beaconUpdateData.newBeaconResponse.timestamp, - beaconUpdateData.newBeaconResponse.encodedValue, - beaconUpdateData.newBeaconResponse.signature, - ]), - ]; + beaconUpdates = [...beaconUpdates, beaconUpdateData]; } let nonce = transactionCount; - for (const updateBatch of chunk(beaconUpdateCalldatas, DATAFEED_UPDATE_BATCH_SIZE)) { + for (const updateBatch of chunk(beaconUpdates, DATAFEED_UPDATE_BATCH_SIZE)) { // Get the latest gas price const getGasFn = () => getGasPrice(provider.rpcProvider.getProvider(), config.chains[chainId].options); // We have to grab the limiter from the custom provider as the getGasPrice function contains its own timeouts const [logs, gasTarget] = await provider.rpcProvider.getLimiter().schedule({ expiration: 30_000 }, getGasFn); logger.logPending(logs, logOptions); - // Update beacon batch onchain values - const tx = await go(() => contract.connect(sponsorWallet).tryMulticall(updateBatch, { nonce, ...gasTarget }), { - ...prepareGoOptions(startTime, totalTimeout), - onAttemptError: (goError) => - logger.warn(`Failed attempt to update beacon batch. Error ${goError.error}`, logOptions), - }); + // Update beacon onchain values + const updateBatchBeaconIds = updateBatch.map((beaconUpdate) => beaconUpdate.beaconTrigger.beaconId); + logger.debug( + `About to update ${updateBatch.length} beacon(s) with nonce ${nonce}. Beacon id(s): ${updateBatchBeaconIds.join( + ', ' + )}`, + logOptions + ); + + const tx = await go( + updateBatch.length === 1 + ? () => + contract + .connect(sponsorWallet) + .updateBeaconWithSignedData( + beaconUpdates[0].beacon.airnode, + beaconUpdates[0].beacon.templateId, + beaconUpdates[0].newBeaconResponse.timestamp, + beaconUpdates[0].newBeaconResponse.encodedValue, + beaconUpdates[0].newBeaconResponse.signature, + { nonce, ...gasTarget } + ) + : () => { + return contract.connect(sponsorWallet).tryMulticall( + updateBatch.map((beaconUpdateData) => + contract.interface.encodeFunctionData('updateBeaconWithSignedData', [ + beaconUpdateData.beacon.airnode, + beaconUpdateData.beacon.templateId, + beaconUpdateData.newBeaconResponse.timestamp, + beaconUpdateData.newBeaconResponse.encodedValue, + beaconUpdateData.newBeaconResponse.signature, + ]) + ), + { nonce, ...gasTarget } + ); + }, + { + ...prepareGoOptions(startTime, totalTimeout), + onAttemptError: (goError) => + logger.warn( + `Attempt to send transaction to update ${updateBatch.length} beacon(s) has failed. Error ${goError.error}`, + logOptions + ), + } + ); if (!tx.success) { - logger.warn(`Unable send beacon batch update transaction with nonce ${nonce}. Error: ${tx.error}`, logOptions); + logger.warn( + `Unable send transaction to update ${updateBatch.length} beacon(s) with nonce ${nonce}. Error: ${tx.error}`, + logOptions + ); + logger.debug(`Beacon id(s) that failed to be updated: ${updateBatchBeaconIds.join(', ')}`, logOptions); return; } logger.info( - `Beacon batch update transaction was successfully sent with nonce ${nonce}. Tx hash ${tx.data.hash}.`, + `Transaction to update ${updateBatch.length} beacon(s) was successfully sent with nonce ${nonce}. Tx hash ${tx.data.hash}`, logOptions ); + nonce++; } }