From 22f34b28e6a0fdd847fa86e1c844ee06cb720b2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emanuel=20Tesa=C5=99?= Date: Tue, 4 Apr 2023 12:44:16 +0200 Subject: [PATCH 1/2] Revert "Estimate gas for RRP fulfillments" --- .changeset/eighty-goats-provide.md | 6 - .../src/evm/fulfillments/api-calls.test.ts | 1216 +++++------------ .../src/evm/fulfillments/api-calls.ts | 102 +- .../src/evm/fulfillments/index.test.ts | 4 +- packages/airnode-node/src/types.ts | 1 - .../airnode-validator/src/config/config.ts | 2 +- 6 files changed, 360 insertions(+), 971 deletions(-) delete mode 100644 .changeset/eighty-goats-provide.md diff --git a/.changeset/eighty-goats-provide.md b/.changeset/eighty-goats-provide.md deleted file mode 100644 index c528bc5544..0000000000 --- a/.changeset/eighty-goats-provide.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@api3/airnode-validator': minor -'@api3/airnode-node': minor ---- - -Estimate gas for RRP fulfillments diff --git a/packages/airnode-node/src/evm/fulfillments/api-calls.test.ts b/packages/airnode-node/src/evm/fulfillments/api-calls.test.ts index 2dfc924617..9d494e7962 100644 --- a/packages/airnode-node/src/evm/fulfillments/api-calls.test.ts +++ b/packages/airnode-node/src/evm/fulfillments/api-calls.test.ts @@ -3,15 +3,11 @@ import { mockEthers } from '../../../test/mock-utils'; const failMock = jest.fn(); const fulfillMock = jest.fn(); const staticFulfillMock = jest.fn(); -const estimateFulfillMock = jest.fn(); mockEthers({ airnodeRrpMocks: { callStatic: { fulfill: staticFulfillMock, }, - estimateGas: { - fulfill: estimateFulfillMock, - }, fail: failMock, fulfill: fulfillMock, }, @@ -31,87 +27,379 @@ const config = fixtures.buildConfig(); describe('submitApiCall', () => { const masterHDNode = wallet.getMasterHDNode(config); - - const gasTarget: GasTarget = { - type: 2, - maxPriorityFeePerGas: ethers.BigNumber.from(1), - maxFeePerGas: ethers.BigNumber.from(1000), - gasLimit: ethers.BigNumber.from(500_000), - }; - const gasTargetFallback: GasTarget = { + const gasPriceFallback: GasTarget = { type: 0, gasPrice: ethers.BigNumber.from('1000'), gasLimit: ethers.BigNumber.from(500_000), }; - - const gasTargetWithoutGasLimit: GasTarget = { + const gasPrice: GasTarget = { type: 2, maxPriorityFeePerGas: ethers.BigNumber.from(1), maxFeePerGas: ethers.BigNumber.from(1000), + gasLimit: ethers.BigNumber.from(500_000), }; - const gasTargetFallbackWithoutGasLimit: GasTarget = { - type: 0, - gasPrice: ethers.BigNumber.from('1000'), - }; - test.each([gasTarget, gasTargetFallback, gasTargetWithoutGasLimit, gasTargetFallbackWithoutGasLimit])( - `does nothing for API call requests that do not have a nonce - %#`, - async (gasTarget) => { + describe('Pending API calls', () => { + test.each([gasPrice, gasPriceFallback])( + `does nothing for API call requests that do not have a nonce - %#`, + async (gasTarget) => { + const provider = new ethers.providers.JsonRpcProvider(); + const apiCall = fixtures.requests.buildSuccessfulApiCall({ nonce: undefined }); + const [logs, err, data] = await apiCalls.submitApiCall(createAirnodeRrpFake(), apiCall, { + gasTarget, + masterHDNode, + provider, + }); + expect(logs).toEqual([ + { + level: 'ERROR', + message: `API call for Request:${apiCall.id} cannot be submitted as it does not have a nonce`, + }, + ]); + expect(err).toEqual(null); + expect(data).toEqual(null); + expect(staticFulfillMock).not.toHaveBeenCalled(); + expect(fulfillMock).not.toHaveBeenCalled(); + expect(failMock).not.toHaveBeenCalled(); + } + ); + + test.each([gasPrice, gasPriceFallback])( + `successfully tests and submits a fulfill transaction for pending requests - %#`, + async (gasTarget) => { + const txOpts = { ...gasTarget, nonce: 5 }; + const provider = new ethers.providers.JsonRpcProvider(); + staticFulfillMock.mockResolvedValueOnce({ callSuccess: true, callData: '0x' }); + fulfillMock.mockResolvedValueOnce({ hash: '0xtransactionId' }); + + const apiCall = fixtures.requests.buildSuccessfulApiCall({ + id: '0xb56b66dc089eab3dc98672ea5e852488730a8f76621fd9ea719504ea205980f8', + data: { + encodedValue: '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', + signature: + '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', + }, + nonce: 5, + }); + const [logs, err, data] = await apiCalls.submitApiCall(createAirnodeRrpFake(), apiCall, { + gasTarget, + masterHDNode, + provider, + }); + expect(logs).toEqual([ + { level: 'DEBUG', message: `Attempting to fulfill API call for Request:${apiCall.id}...` }, + { level: 'INFO', message: `Submitting API call fulfillment for Request:${apiCall.id}...` }, + ]); + expect(err).toEqual(null); + expect(data).toEqual({ + ...apiCall, + fulfillment: { hash: '0xtransactionId' }, + }); + expect(staticFulfillMock).toHaveBeenCalledTimes(1); + expect(staticFulfillMock).toHaveBeenCalledWith( + apiCall.id, + apiCall.airnodeAddress, + apiCall.fulfillAddress, + apiCall.fulfillFunctionId, + '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', + '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', + txOpts + ); + expect(fulfillMock).toHaveBeenCalledTimes(1); + expect(fulfillMock).toHaveBeenCalledWith( + apiCall.id, + apiCall.airnodeAddress, + apiCall.fulfillAddress, + apiCall.fulfillFunctionId, + '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', + '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', + txOpts + ); + expect(failMock).not.toHaveBeenCalled(); + } + ); + + test.each([gasPrice, gasPriceFallback])( + `returns an error if the fulfill transaction for pending requests fails - %#`, + async (gasTarget) => { + const txOpts = { ...gasTarget, nonce: 5 }; + const provider = new ethers.providers.JsonRpcProvider(); + staticFulfillMock.mockResolvedValueOnce({ callSuccess: true, callData: '0x' }); + (fulfillMock as any).mockRejectedValueOnce(new Error('Server did not respond')); + (fulfillMock as any).mockRejectedValueOnce(new Error('Server did not respond')); + + const apiCall = fixtures.requests.buildSuccessfulApiCall({ + id: '0xb56b66dc089eab3dc98672ea5e852488730a8f76621fd9ea719504ea205980f8', + data: { + encodedValue: '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', + signature: + '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', + }, + nonce: 5, + }); + const [logs, err, data] = await apiCalls.submitApiCall(createAirnodeRrpFake(), apiCall, { + gasTarget, + masterHDNode, + provider, + }); + expect(logs).toEqual([ + { level: 'DEBUG', message: `Attempting to fulfill API call for Request:${apiCall.id}...` }, + { level: 'INFO', message: `Submitting API call fulfillment for Request:${apiCall.id}...` }, + { + error: new Error('Server did not respond'), + level: 'ERROR', + message: + 'Error submitting API call fulfillment transaction for Request:0xb56b66dc089eab3dc98672ea5e852488730a8f76621fd9ea719504ea205980f8', + }, + ]); + expect(err).toEqual(new Error('Server did not respond')); + expect(data).toEqual(null); + expect(staticFulfillMock).toHaveBeenCalledTimes(1); + expect(staticFulfillMock).toHaveBeenCalledWith( + apiCall.id, + apiCall.airnodeAddress, + apiCall.fulfillAddress, + apiCall.fulfillFunctionId, + '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', + '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', + txOpts + ); + expect(fulfillMock).toHaveBeenCalledTimes(2); + expect(fulfillMock).toHaveBeenCalledWith( + apiCall.id, + apiCall.airnodeAddress, + apiCall.fulfillAddress, + apiCall.fulfillFunctionId, + '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', + '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', + txOpts + ); + expect(failMock).not.toHaveBeenCalled(); + } + ); + + test.each([gasPrice, gasPriceFallback])( + `submits a fail transaction if the fulfill call would revert with empty string - %#`, + async (gasTarget) => { + const txOpts = { ...gasTarget, nonce: 5 }; + const provider = new ethers.providers.JsonRpcProvider(); + staticFulfillMock.mockResolvedValueOnce({ callSuccess: false, callData: '0x' }); + (failMock as jest.Mock).mockResolvedValueOnce({ hash: '0xfailtransaction' }); + const apiCall = fixtures.requests.buildSuccessfulApiCall({ + id: '0xb56b66dc089eab3dc98672ea5e852488730a8f76621fd9ea719504ea205980f8', + data: { + encodedValue: '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', + signature: + '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', + }, + nonce: 5, + }); + const [logs, err, data] = await apiCalls.submitApiCall(createAirnodeRrpFake(), apiCall, { + gasTarget, + masterHDNode, + provider, + }); + expect(logs).toEqual([ + { level: 'DEBUG', message: `Attempting to fulfill API call for Request:${apiCall.id}...` }, + { level: 'INFO', message: `Submitting API call fail for Request:${apiCall.id}...` }, + ]); + expect(err).toEqual(null); + expect(data).toEqual({ + ...apiCall, + fulfillment: { hash: '0xfailtransaction' }, + errorMessage: 'Fulfill transaction failed', + }); + expect(staticFulfillMock).toHaveBeenCalledTimes(1); + expect(staticFulfillMock).toHaveBeenCalledWith( + apiCall.id, + apiCall.airnodeAddress, + apiCall.fulfillAddress, + apiCall.fulfillFunctionId, + '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', + '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', + txOpts + ); + expect(fulfillMock).not.toHaveBeenCalled(); + expect(failMock).toHaveBeenCalledTimes(1); + expect(failMock).toHaveBeenCalledWith( + apiCall.id, + apiCall.airnodeAddress, + apiCall.fulfillAddress, + apiCall.fulfillFunctionId, + 'No revert string', + txOpts + ); + } + ); + + test.each([gasPrice, gasPriceFallback])( + `submits a fail transaction if the fulfill call would revert with a revert string - %#`, + async (gasTarget) => { + const txOpts = { ...gasTarget, nonce: 5 }; + const provider = new ethers.providers.JsonRpcProvider(); + staticFulfillMock.mockResolvedValueOnce({ + callSuccess: false, + callData: + '0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000e416c776179732072657665727473000000000000000000000000000000000000', + }); + (failMock as jest.Mock).mockResolvedValueOnce({ hash: '0xfailtransaction' }); + const apiCall = fixtures.requests.buildSuccessfulApiCall({ + id: '0xb56b66dc089eab3dc98672ea5e852488730a8f76621fd9ea719504ea205980f8', + data: { + encodedValue: '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', + signature: + '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', + }, + nonce: 5, + }); + const [logs, err, data] = await apiCalls.submitApiCall(createAirnodeRrpFake(), apiCall, { + gasTarget, + masterHDNode, + provider, + }); + expect(logs).toEqual([ + { level: 'DEBUG', message: `Attempting to fulfill API call for Request:${apiCall.id}...` }, + { level: 'INFO', message: `Submitting API call fail for Request:${apiCall.id}...` }, + ]); + expect(err).toEqual(null); + expect(data).toEqual({ + ...apiCall, + fulfillment: { hash: '0xfailtransaction' }, + errorMessage: 'Fulfill transaction failed', + }); + expect(staticFulfillMock).toHaveBeenCalledTimes(1); + expect(staticFulfillMock).toHaveBeenCalledWith( + apiCall.id, + apiCall.airnodeAddress, + apiCall.fulfillAddress, + apiCall.fulfillFunctionId, + '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', + '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', + txOpts + ); + expect(fulfillMock).not.toHaveBeenCalled(); + expect(failMock).toHaveBeenCalledTimes(1); + expect(failMock).toHaveBeenCalledWith( + apiCall.id, + apiCall.airnodeAddress, + apiCall.fulfillAddress, + apiCall.fulfillFunctionId, + 'Always reverts', + txOpts + ); + } + ); + + test.each([gasPrice, gasPriceFallback])( + `does nothing if the fulfill test returns nothing - %#`, + async (gasTarget) => { + const txOpts = { ...gasTarget, nonce: 5 }; + const provider = new ethers.providers.JsonRpcProvider(); + staticFulfillMock.mockResolvedValueOnce(null); + const apiCall = fixtures.requests.buildSuccessfulApiCall({ + id: '0xb56b66dc089eab3dc98672ea5e852488730a8f76621fd9ea719504ea205980f8', + data: { + encodedValue: '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', + signature: + '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', + }, + nonce: 5, + }); + const [logs, err, data] = await apiCalls.submitApiCall(createAirnodeRrpFake(), apiCall, { + gasTarget, + masterHDNode, + provider, + }); + expect(logs).toEqual([ + { level: 'DEBUG', message: `Attempting to fulfill API call for Request:${apiCall.id}...` }, + { + level: 'ERROR', + message: `Fulfill attempt for Request:${apiCall.id} responded with unexpected value: 'null'`, + }, + ]); + expect(err).toEqual(null); + expect(data).toEqual(null); + expect(staticFulfillMock).toHaveBeenCalledTimes(1); + expect(staticFulfillMock).toHaveBeenCalledWith( + apiCall.id, + apiCall.airnodeAddress, + apiCall.fulfillAddress, + apiCall.fulfillFunctionId, + '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', + '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', + txOpts + ); + expect(fulfillMock).not.toHaveBeenCalled(); + expect(failMock).not.toHaveBeenCalled(); + } + ); + + test.each([gasPrice, gasPriceFallback])(`returns an error if everything fails - %#`, async (gasTarget) => { + const txOpts = { ...gasTarget, nonce: 5 }; const provider = new ethers.providers.JsonRpcProvider(); - const apiCall = fixtures.requests.buildSuccessfulApiCall({ nonce: undefined }); + const staticCallError = new Error('Static call error'); + staticFulfillMock.mockRejectedValueOnce(staticCallError); + staticFulfillMock.mockRejectedValueOnce(staticCallError); + const failTxError = new Error('Fail transaction error'); + failMock.mockRejectedValueOnce(failTxError); + failMock.mockRejectedValueOnce(failTxError); + const apiCall = fixtures.requests.buildSuccessfulApiCall({ + id: '0xb56b66dc089eab3dc98672ea5e852488730a8f76621fd9ea719504ea205980f8', + data: { + encodedValue: '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', + signature: + '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', + }, + nonce: 5, + }); const [logs, err, data] = await apiCalls.submitApiCall(createAirnodeRrpFake(), apiCall, { gasTarget, masterHDNode, provider, }); expect(logs).toEqual([ + { level: 'DEBUG', message: `Attempting to fulfill API call for Request:${apiCall.id}...` }, { + error: staticCallError, level: 'ERROR', - message: `API call for Request:${apiCall.id} cannot be submitted as it does not have a nonce`, + message: `Static call fulfillment failed for Request:${apiCall.id} with ${staticCallError}`, + }, + { level: 'INFO', message: `Submitting API call fail for Request:${apiCall.id}...` }, + { + error: failTxError, + level: 'ERROR', + message: `Error submitting API call fail transaction for Request:${apiCall.id}`, }, ]); - expect(err).toEqual(null); + expect(err).toEqual(failTxError); expect(data).toEqual(null); - expect(staticFulfillMock).not.toHaveBeenCalled(); + expect(staticFulfillMock).toHaveBeenCalledTimes(2); + expect(staticFulfillMock).toHaveBeenNthCalledWith( + 2, + apiCall.id, + apiCall.airnodeAddress, + apiCall.fulfillAddress, + apiCall.fulfillFunctionId, + '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', + '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', + txOpts + ); expect(fulfillMock).not.toHaveBeenCalled(); - expect(failMock).not.toHaveBeenCalled(); - } - ); - - test.each([gasTarget, gasTargetFallback])( - `does not estimate gas if 'gasLimit' is specified - %#`, - async (gasTarget) => { - const provider = new ethers.providers.JsonRpcProvider(); - const apiCall = fixtures.requests.buildSuccessfulApiCall({ - nonce: 5, - }); - await apiCalls.submitApiCall(createAirnodeRrpFake(), apiCall, { - gasTarget, - masterHDNode, - provider, - }); - expect(estimateFulfillMock).not.toHaveBeenCalled(); - } - ); - - test.each([gasTargetWithoutGasLimit, gasTargetFallbackWithoutGasLimit])( - `estimates gas if 'gasLimit' isn't specified - %#`, - async (gasTarget) => { - const provider = new ethers.providers.JsonRpcProvider(); - const apiCall = fixtures.requests.buildSuccessfulApiCall({ - nonce: 5, - }); - await apiCalls.submitApiCall(createAirnodeRrpFake(), apiCall, { - gasTarget, - masterHDNode, - provider, - }); - expect(estimateFulfillMock).toHaveBeenCalled(); - } - ); + expect(failMock).toHaveBeenCalledTimes(2); + expect(failMock).toHaveBeenNthCalledWith( + 2, + apiCall.id, + apiCall.airnodeAddress, + apiCall.fulfillAddress, + apiCall.fulfillFunctionId, + 'Static call error', + txOpts + ); + }); + }); describe('Errored API calls', () => { - test.each([gasTarget, gasTargetFallback, gasTargetWithoutGasLimit, gasTargetFallbackWithoutGasLimit])( + test.each([gasPrice, gasPriceFallback])( `submits a fail transaction with errorMessage for errored requests - %#`, async (gasTarget) => { const txOpts = { ...gasTarget, nonce: 5 }; @@ -148,12 +436,11 @@ describe('submitApiCall', () => { txOpts ); expect(staticFulfillMock).not.toHaveBeenCalled(); - expect(estimateFulfillMock).not.toHaveBeenCalled(); expect(fulfillMock).not.toHaveBeenCalled(); } ); - test.each([gasTarget, gasTargetFallback])( + test.each([gasPrice, gasPriceFallback])( `submits a fail transaction with a trimmed errorMessage for errored requests - %#`, async (gasTarget) => { const txOpts = { ...gasTarget, nonce: 5 }; @@ -194,12 +481,11 @@ describe('submitApiCall', () => { txOpts ); expect(staticFulfillMock).not.toHaveBeenCalled(); - expect(estimateFulfillMock).not.toHaveBeenCalled(); expect(fulfillMock).not.toHaveBeenCalled(); } ); - test.each([gasTarget, gasTargetFallback])( + test.each([gasPrice, gasPriceFallback])( `returns an error if the error transaction fails - %#`, async (gasTarget) => { const txOpts = { ...gasTarget, nonce: 5 }; @@ -240,808 +526,8 @@ describe('submitApiCall', () => { `${RequestErrorMessage.ApiCallFailed} with error: Server did not respond`, txOpts ); - expect(staticFulfillMock).not.toHaveBeenCalled(); - expect(estimateFulfillMock).not.toHaveBeenCalled(); expect(fulfillMock).not.toHaveBeenCalled(); } ); }); - - describe('testAndSubmitFulfill', () => { - describe('Pending API calls', () => { - test.each([gasTarget, gasTargetFallback])( - `successfully tests and submits a fulfill transaction for pending requests - %#`, - async (gasTarget) => { - const txOpts = { ...gasTarget, nonce: 5 }; - const provider = new ethers.providers.JsonRpcProvider(); - staticFulfillMock.mockResolvedValueOnce({ callSuccess: true, callData: '0x' }); - fulfillMock.mockResolvedValueOnce({ hash: '0xtransactionId' }); - - const apiCall = fixtures.requests.buildSuccessfulApiCall({ - id: '0xb56b66dc089eab3dc98672ea5e852488730a8f76621fd9ea719504ea205980f8', - data: { - encodedValue: '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', - signature: - '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', - }, - nonce: 5, - }); - const [logs, err, data] = await apiCalls.testAndSubmitFulfill(createAirnodeRrpFake(), apiCall, { - gasTarget, - masterHDNode, - provider, - }); - expect(logs).toEqual([ - { level: 'DEBUG', message: `Attempting to fulfill API call for Request:${apiCall.id}...` }, - { level: 'INFO', message: `Submitting API call fulfillment for Request:${apiCall.id}...` }, - ]); - expect(err).toEqual(null); - expect(data).toEqual({ - ...apiCall, - fulfillment: { hash: '0xtransactionId' }, - }); - expect(staticFulfillMock).toHaveBeenCalledTimes(1); - expect(staticFulfillMock).toHaveBeenCalledWith( - apiCall.id, - apiCall.airnodeAddress, - apiCall.fulfillAddress, - apiCall.fulfillFunctionId, - '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', - '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', - txOpts - ); - expect(fulfillMock).toHaveBeenCalledTimes(1); - expect(fulfillMock).toHaveBeenCalledWith( - apiCall.id, - apiCall.airnodeAddress, - apiCall.fulfillAddress, - apiCall.fulfillFunctionId, - '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', - '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', - txOpts - ); - expect(failMock).not.toHaveBeenCalled(); - } - ); - - test.each([gasTarget, gasTargetFallback])( - `returns an error if the fulfill transaction for pending requests fails - %#`, - async (gasTarget) => { - const txOpts = { ...gasTarget, nonce: 5 }; - const provider = new ethers.providers.JsonRpcProvider(); - staticFulfillMock.mockResolvedValueOnce({ callSuccess: true, callData: '0x' }); - (fulfillMock as any).mockRejectedValueOnce(new Error('Server did not respond')); - (fulfillMock as any).mockRejectedValueOnce(new Error('Server did not respond')); - - const apiCall = fixtures.requests.buildSuccessfulApiCall({ - id: '0xb56b66dc089eab3dc98672ea5e852488730a8f76621fd9ea719504ea205980f8', - data: { - encodedValue: '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', - signature: - '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', - }, - nonce: 5, - }); - const [logs, err, data] = await apiCalls.testAndSubmitFulfill(createAirnodeRrpFake(), apiCall, { - gasTarget, - masterHDNode, - provider, - }); - expect(logs).toEqual([ - { level: 'DEBUG', message: `Attempting to fulfill API call for Request:${apiCall.id}...` }, - { level: 'INFO', message: `Submitting API call fulfillment for Request:${apiCall.id}...` }, - { - error: new Error('Server did not respond'), - level: 'ERROR', - message: - 'Error submitting API call fulfillment transaction for Request:0xb56b66dc089eab3dc98672ea5e852488730a8f76621fd9ea719504ea205980f8', - }, - ]); - expect(err).toEqual(new Error('Server did not respond')); - expect(data).toEqual(null); - expect(staticFulfillMock).toHaveBeenCalledTimes(1); - expect(staticFulfillMock).toHaveBeenCalledWith( - apiCall.id, - apiCall.airnodeAddress, - apiCall.fulfillAddress, - apiCall.fulfillFunctionId, - '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', - '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', - txOpts - ); - expect(fulfillMock).toHaveBeenCalledTimes(2); - expect(fulfillMock).toHaveBeenCalledWith( - apiCall.id, - apiCall.airnodeAddress, - apiCall.fulfillAddress, - apiCall.fulfillFunctionId, - '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', - '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', - txOpts - ); - expect(failMock).not.toHaveBeenCalled(); - } - ); - - test.each([gasTarget, gasTargetFallback])( - `submits a fail transaction if the fulfill call would revert with empty string - %#`, - async (gasTarget) => { - const txOpts = { ...gasTarget, nonce: 5 }; - const provider = new ethers.providers.JsonRpcProvider(); - staticFulfillMock.mockResolvedValueOnce({ callSuccess: false, callData: '0x' }); - (failMock as jest.Mock).mockResolvedValueOnce({ hash: '0xfailtransaction' }); - const apiCall = fixtures.requests.buildSuccessfulApiCall({ - id: '0xb56b66dc089eab3dc98672ea5e852488730a8f76621fd9ea719504ea205980f8', - data: { - encodedValue: '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', - signature: - '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', - }, - nonce: 5, - }); - const [logs, err, data] = await apiCalls.testAndSubmitFulfill(createAirnodeRrpFake(), apiCall, { - gasTarget, - masterHDNode, - provider, - }); - expect(logs).toEqual([ - { level: 'DEBUG', message: `Attempting to fulfill API call for Request:${apiCall.id}...` }, - { level: 'INFO', message: `Submitting API call fail for Request:${apiCall.id}...` }, - ]); - expect(err).toEqual(null); - expect(data).toEqual({ - ...apiCall, - fulfillment: { hash: '0xfailtransaction' }, - errorMessage: 'Fulfill transaction failed', - }); - expect(staticFulfillMock).toHaveBeenCalledTimes(1); - expect(staticFulfillMock).toHaveBeenCalledWith( - apiCall.id, - apiCall.airnodeAddress, - apiCall.fulfillAddress, - apiCall.fulfillFunctionId, - '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', - '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', - txOpts - ); - expect(fulfillMock).not.toHaveBeenCalled(); - expect(failMock).toHaveBeenCalledTimes(1); - expect(failMock).toHaveBeenCalledWith( - apiCall.id, - apiCall.airnodeAddress, - apiCall.fulfillAddress, - apiCall.fulfillFunctionId, - 'No revert string', - txOpts - ); - } - ); - - test.each([gasTarget, gasTargetFallback])( - `submits a fail transaction if the fulfill call would revert with a revert string - %#`, - async (gasTarget) => { - const txOpts = { ...gasTarget, nonce: 5 }; - const provider = new ethers.providers.JsonRpcProvider(); - staticFulfillMock.mockResolvedValueOnce({ - callSuccess: false, - callData: - '0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000e416c776179732072657665727473000000000000000000000000000000000000', - }); - (failMock as jest.Mock).mockResolvedValueOnce({ hash: '0xfailtransaction' }); - const apiCall = fixtures.requests.buildSuccessfulApiCall({ - id: '0xb56b66dc089eab3dc98672ea5e852488730a8f76621fd9ea719504ea205980f8', - data: { - encodedValue: '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', - signature: - '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', - }, - nonce: 5, - }); - const [logs, err, data] = await apiCalls.testAndSubmitFulfill(createAirnodeRrpFake(), apiCall, { - gasTarget, - masterHDNode, - provider, - }); - expect(logs).toEqual([ - { level: 'DEBUG', message: `Attempting to fulfill API call for Request:${apiCall.id}...` }, - { level: 'INFO', message: `Submitting API call fail for Request:${apiCall.id}...` }, - ]); - expect(err).toEqual(null); - expect(data).toEqual({ - ...apiCall, - fulfillment: { hash: '0xfailtransaction' }, - errorMessage: 'Fulfill transaction failed', - }); - expect(staticFulfillMock).toHaveBeenCalledTimes(1); - expect(staticFulfillMock).toHaveBeenCalledWith( - apiCall.id, - apiCall.airnodeAddress, - apiCall.fulfillAddress, - apiCall.fulfillFunctionId, - '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', - '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', - txOpts - ); - expect(fulfillMock).not.toHaveBeenCalled(); - expect(failMock).toHaveBeenCalledTimes(1); - expect(failMock).toHaveBeenCalledWith( - apiCall.id, - apiCall.airnodeAddress, - apiCall.fulfillAddress, - apiCall.fulfillFunctionId, - 'Always reverts', - txOpts - ); - } - ); - - test.each([gasTarget, gasTargetFallback])( - `does nothing if the fulfill test returns nothing - %#`, - async (gasTarget) => { - const txOpts = { ...gasTarget, nonce: 5 }; - const provider = new ethers.providers.JsonRpcProvider(); - staticFulfillMock.mockResolvedValueOnce(null); - const apiCall = fixtures.requests.buildSuccessfulApiCall({ - id: '0xb56b66dc089eab3dc98672ea5e852488730a8f76621fd9ea719504ea205980f8', - data: { - encodedValue: '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', - signature: - '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', - }, - nonce: 5, - }); - const [logs, err, data] = await apiCalls.testAndSubmitFulfill(createAirnodeRrpFake(), apiCall, { - gasTarget, - masterHDNode, - provider, - }); - expect(logs).toEqual([ - { level: 'DEBUG', message: `Attempting to fulfill API call for Request:${apiCall.id}...` }, - { - level: 'ERROR', - message: `Fulfill attempt for Request:${apiCall.id} responded with unexpected value: 'null'`, - }, - ]); - expect(err).toEqual(null); - expect(data).toEqual(null); - expect(staticFulfillMock).toHaveBeenCalledTimes(1); - expect(staticFulfillMock).toHaveBeenCalledWith( - apiCall.id, - apiCall.airnodeAddress, - apiCall.fulfillAddress, - apiCall.fulfillFunctionId, - '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', - '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', - txOpts - ); - expect(fulfillMock).not.toHaveBeenCalled(); - expect(failMock).not.toHaveBeenCalled(); - } - ); - - test.each([gasTarget, gasTargetFallback])(`returns an error if everything fails - %#`, async (gasTarget) => { - const txOpts = { ...gasTarget, nonce: 5 }; - const provider = new ethers.providers.JsonRpcProvider(); - const staticCallError = new Error('Static call error'); - staticFulfillMock.mockRejectedValueOnce(staticCallError); - staticFulfillMock.mockRejectedValueOnce(staticCallError); - const failTxError = new Error('Fail transaction error'); - failMock.mockRejectedValueOnce(failTxError); - failMock.mockRejectedValueOnce(failTxError); - const apiCall = fixtures.requests.buildSuccessfulApiCall({ - id: '0xb56b66dc089eab3dc98672ea5e852488730a8f76621fd9ea719504ea205980f8', - data: { - encodedValue: '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', - signature: - '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', - }, - nonce: 5, - }); - const [logs, err, data] = await apiCalls.testAndSubmitFulfill(createAirnodeRrpFake(), apiCall, { - gasTarget, - masterHDNode, - provider, - }); - expect(logs).toEqual([ - { level: 'DEBUG', message: `Attempting to fulfill API call for Request:${apiCall.id}...` }, - { - error: staticCallError, - level: 'ERROR', - message: `Static call fulfillment failed for Request:${apiCall.id} with ${staticCallError}`, - }, - { level: 'INFO', message: `Submitting API call fail for Request:${apiCall.id}...` }, - { - error: failTxError, - level: 'ERROR', - message: `Error submitting API call fail transaction for Request:${apiCall.id}`, - }, - ]); - expect(err).toEqual(failTxError); - expect(data).toEqual(null); - expect(staticFulfillMock).toHaveBeenCalledTimes(2); - expect(staticFulfillMock).toHaveBeenNthCalledWith( - 2, - apiCall.id, - apiCall.airnodeAddress, - apiCall.fulfillAddress, - apiCall.fulfillFunctionId, - '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', - '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', - txOpts - ); - expect(fulfillMock).not.toHaveBeenCalled(); - expect(failMock).toHaveBeenCalledTimes(2); - expect(failMock).toHaveBeenNthCalledWith( - 2, - apiCall.id, - apiCall.airnodeAddress, - apiCall.fulfillAddress, - apiCall.fulfillFunctionId, - 'Static call error', - txOpts - ); - }); - }); - }); - - describe('estimateGasAndSubmitFulfill', () => { - describe('Pending API calls', () => { - test.each([gasTargetWithoutGasLimit, gasTargetFallbackWithoutGasLimit])( - `successfully estimates gas and submits a fulfill transaction for pending requests - %#`, - async (gasTarget) => { - const txOpts = { ...gasTarget, nonce: 5 }; - const provider = new ethers.providers.JsonRpcProvider(); - estimateFulfillMock.mockResolvedValueOnce(73804); - fulfillMock.mockResolvedValueOnce({ hash: '0xtransactionId' }); - - const apiCall = fixtures.requests.buildSuccessfulApiCall({ - id: '0xb56b66dc089eab3dc98672ea5e852488730a8f76621fd9ea719504ea205980f8', - data: { - encodedValue: '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', - signature: - '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', - }, - nonce: 5, - }); - const [logs, err, data] = await apiCalls.estimateGasAndSubmitFulfill(createAirnodeRrpFake(), apiCall, { - gasTarget, - masterHDNode, - provider, - }); - expect(logs).toEqual([ - { - level: 'DEBUG', - message: `Attempting to estimate gas for API call fulfillment for Request:${apiCall.id}...`, - }, - { level: 'INFO', message: `Gas limit is set to ${73804} for Request:${apiCall.id}.` }, - { level: 'INFO', message: `Submitting API call fulfillment for Request:${apiCall.id}...` }, - ]); - expect(err).toEqual(null); - expect(data).toEqual({ - ...apiCall, - fulfillment: { hash: '0xtransactionId' }, - }); - expect(estimateFulfillMock).toHaveBeenCalledTimes(1); - expect(estimateFulfillMock).toHaveBeenCalledWith( - apiCall.id, - apiCall.airnodeAddress, - apiCall.fulfillAddress, - apiCall.fulfillFunctionId, - '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', - '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b' - ); - expect(fulfillMock).toHaveBeenCalledTimes(1); - expect(fulfillMock).toHaveBeenCalledWith( - apiCall.id, - apiCall.airnodeAddress, - apiCall.fulfillAddress, - apiCall.fulfillFunctionId, - '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', - '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', - { ...txOpts, gasLimit: 73804 } - ); - expect(failMock).not.toHaveBeenCalled(); - } - ); - - test.each([gasTargetWithoutGasLimit, gasTargetFallbackWithoutGasLimit])( - `returns an error if the fulfill transaction for pending requests fails - %#`, - async (gasTarget) => { - const txOpts = { ...gasTarget, nonce: 5 }; - const provider = new ethers.providers.JsonRpcProvider(); - estimateFulfillMock.mockResolvedValueOnce(73804); - (fulfillMock as any).mockRejectedValueOnce(new Error('Server did not respond')); - (fulfillMock as any).mockRejectedValueOnce(new Error('Server did not respond')); - - const apiCall = fixtures.requests.buildSuccessfulApiCall({ - id: '0xb56b66dc089eab3dc98672ea5e852488730a8f76621fd9ea719504ea205980f8', - data: { - encodedValue: '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', - signature: - '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', - }, - nonce: 5, - }); - const [logs, err, data] = await apiCalls.estimateGasAndSubmitFulfill(createAirnodeRrpFake(), apiCall, { - gasTarget, - masterHDNode, - provider, - }); - expect(logs).toEqual([ - { - level: 'DEBUG', - message: `Attempting to estimate gas for API call fulfillment for Request:${apiCall.id}...`, - }, - { level: 'INFO', message: `Gas limit is set to ${73804} for Request:${apiCall.id}.` }, - { level: 'INFO', message: `Submitting API call fulfillment for Request:${apiCall.id}...` }, - { - error: new Error('Server did not respond'), - level: 'ERROR', - message: - 'Error submitting API call fulfillment transaction for Request:0xb56b66dc089eab3dc98672ea5e852488730a8f76621fd9ea719504ea205980f8', - }, - ]); - expect(err).toEqual(new Error('Server did not respond')); - expect(data).toEqual(null); - expect(estimateFulfillMock).toHaveBeenCalledTimes(1); - expect(estimateFulfillMock).toHaveBeenCalledWith( - apiCall.id, - apiCall.airnodeAddress, - apiCall.fulfillAddress, - apiCall.fulfillFunctionId, - '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', - '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b' - ); - expect(fulfillMock).toHaveBeenCalledTimes(2); - expect(fulfillMock).toHaveBeenCalledWith( - apiCall.id, - apiCall.airnodeAddress, - apiCall.fulfillAddress, - apiCall.fulfillFunctionId, - '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', - '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', - { ...txOpts, gasLimit: 73804 } - ); - expect(failMock).not.toHaveBeenCalled(); - } - ); - - test.each([gasTargetWithoutGasLimit, gasTargetFallbackWithoutGasLimit])( - `submits a fail transaction if the gas estimation is failed but following static call is successful - %#`, - async (gasTarget) => { - const txOpts = { ...gasTarget, nonce: 5 }; - const provider = new ethers.providers.JsonRpcProvider(); - const estimateGasError = new Error('Estimate gas error'); - estimateFulfillMock.mockRejectedValueOnce(estimateGasError); - estimateFulfillMock.mockRejectedValueOnce(estimateGasError); - staticFulfillMock.mockResolvedValueOnce({ callSuccess: true, callData: '0x' }); - (failMock as jest.Mock).mockResolvedValueOnce({ hash: '0xfailtransaction' }); - const apiCall = fixtures.requests.buildSuccessfulApiCall({ - id: '0xb56b66dc089eab3dc98672ea5e852488730a8f76621fd9ea719504ea205980f8', - data: { - encodedValue: '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', - signature: - '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', - }, - nonce: 5, - }); - const [logs, err, data] = await apiCalls.estimateGasAndSubmitFulfill(createAirnodeRrpFake(), apiCall, { - gasTarget, - masterHDNode, - provider, - }); - expect(logs).toEqual([ - { - level: 'DEBUG', - message: `Attempting to estimate gas for API call fulfillment for Request:${apiCall.id}...`, - }, - { - error: estimateGasError, - level: 'ERROR', - message: `Gas estimation for API call fulfillment failed for Request:${apiCall.id} with ${estimateGasError}`, - }, - { level: 'DEBUG', message: `Attempting to fulfill API call for Request:${apiCall.id}...` }, - { level: 'INFO', message: `Submitting API call fail for Request:${apiCall.id}...` }, - ]); - expect(err).toEqual(null); - expect(data).toEqual({ - ...apiCall, - fulfillment: { hash: '0xfailtransaction' }, - errorMessage: `Gas estimation failed with error: ${estimateGasError.message}`, - }); - expect(estimateFulfillMock).toHaveBeenCalledTimes(2); - expect(estimateFulfillMock).toHaveBeenCalledWith( - apiCall.id, - apiCall.airnodeAddress, - apiCall.fulfillAddress, - apiCall.fulfillFunctionId, - '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', - '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b' - ); - expect(staticFulfillMock).toHaveBeenCalledTimes(1); - expect(fulfillMock).not.toHaveBeenCalled(); - expect(failMock).toHaveBeenCalledTimes(1); - expect(failMock).toHaveBeenCalledWith( - apiCall.id, - apiCall.airnodeAddress, - apiCall.fulfillAddress, - apiCall.fulfillFunctionId, - estimateGasError.message, - txOpts - ); - } - ); - - test.each([gasTargetWithoutGasLimit, gasTargetFallbackWithoutGasLimit])( - `submits a fail transaction if the gas estimation is failed and following static call is also failed with revert string - %#`, - async (gasTarget) => { - const txOpts = { ...gasTarget, nonce: 5 }; - const provider = new ethers.providers.JsonRpcProvider(); - const estimateGasError = new Error('Estimate gas error'); - estimateFulfillMock.mockRejectedValueOnce(estimateGasError); - estimateFulfillMock.mockRejectedValueOnce(estimateGasError); - staticFulfillMock.mockResolvedValueOnce({ - callSuccess: false, - callData: - '0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000e416c776179732072657665727473000000000000000000000000000000000000', - }); - (failMock as jest.Mock).mockResolvedValueOnce({ hash: '0xfailtransaction' }); - const apiCall = fixtures.requests.buildSuccessfulApiCall({ - id: '0xb56b66dc089eab3dc98672ea5e852488730a8f76621fd9ea719504ea205980f8', - data: { - encodedValue: '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', - signature: - '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', - }, - nonce: 5, - }); - const [logs, err, data] = await apiCalls.estimateGasAndSubmitFulfill(createAirnodeRrpFake(), apiCall, { - gasTarget, - masterHDNode, - provider, - }); - expect(logs).toEqual([ - { - level: 'DEBUG', - message: `Attempting to estimate gas for API call fulfillment for Request:${apiCall.id}...`, - }, - { - error: estimateGasError, - level: 'ERROR', - message: `Gas estimation for API call fulfillment failed for Request:${apiCall.id} with ${estimateGasError}`, - }, - { level: 'DEBUG', message: `Attempting to fulfill API call for Request:${apiCall.id}...` }, - { level: 'INFO', message: `Submitting API call fail for Request:${apiCall.id}...` }, - ]); - expect(err).toEqual(null); - expect(data).toEqual({ - ...apiCall, - fulfillment: { hash: '0xfailtransaction' }, - errorMessage: `Fulfill transaction failed`, - }); - expect(estimateFulfillMock).toHaveBeenCalledTimes(2); - expect(estimateFulfillMock).toHaveBeenCalledWith( - apiCall.id, - apiCall.airnodeAddress, - apiCall.fulfillAddress, - apiCall.fulfillFunctionId, - '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', - '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b' - ); - expect(staticFulfillMock).toHaveBeenCalledTimes(1); - expect(fulfillMock).not.toHaveBeenCalled(); - expect(failMock).toHaveBeenCalledTimes(1); - expect(failMock).toHaveBeenCalledWith( - apiCall.id, - apiCall.airnodeAddress, - apiCall.fulfillAddress, - apiCall.fulfillFunctionId, - `Always reverts`, - txOpts - ); - } - ); - - test.each([gasTargetWithoutGasLimit, gasTargetFallbackWithoutGasLimit])( - `submits a fail transaction if the gas estimation is resolved with null but following static call is successful - %#`, - async (gasTarget) => { - const txOpts = { ...gasTarget, nonce: 5 }; - const provider = new ethers.providers.JsonRpcProvider(); - estimateFulfillMock.mockResolvedValueOnce(null); - staticFulfillMock.mockResolvedValueOnce({ callSuccess: true, callData: '0x' }); - (failMock as jest.Mock).mockResolvedValueOnce({ hash: '0xfailtransaction' }); - const apiCall = fixtures.requests.buildSuccessfulApiCall({ - id: '0xb56b66dc089eab3dc98672ea5e852488730a8f76621fd9ea719504ea205980f8', - data: { - encodedValue: '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', - signature: - '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', - }, - nonce: 5, - }); - const [logs, err, data] = await apiCalls.estimateGasAndSubmitFulfill(createAirnodeRrpFake(), apiCall, { - gasTarget, - masterHDNode, - provider, - }); - expect(logs).toEqual([ - { - level: 'DEBUG', - message: `Attempting to estimate gas for API call fulfillment for Request:${apiCall.id}...`, - }, - { level: 'DEBUG', message: `Attempting to fulfill API call for Request:${apiCall.id}...` }, - { level: 'INFO', message: `Submitting API call fail for Request:${apiCall.id}...` }, - ]); - expect(err).toEqual(null); - expect(data).toEqual({ - ...apiCall, - fulfillment: { hash: '0xfailtransaction' }, - errorMessage: `Gas estimation failed`, - }); - expect(estimateFulfillMock).toHaveBeenCalledTimes(1); - expect(estimateFulfillMock).toHaveBeenCalledWith( - apiCall.id, - apiCall.airnodeAddress, - apiCall.fulfillAddress, - apiCall.fulfillFunctionId, - '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', - '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b' - ); - expect(staticFulfillMock).toHaveBeenCalledTimes(1); - expect(fulfillMock).not.toHaveBeenCalled(); - expect(failMock).toHaveBeenCalledTimes(1); - expect(failMock).toHaveBeenCalledWith( - apiCall.id, - apiCall.airnodeAddress, - apiCall.fulfillAddress, - apiCall.fulfillFunctionId, - 'Gas estimation failed', - txOpts - ); - } - ); - - test.each([gasTargetWithoutGasLimit, gasTargetFallbackWithoutGasLimit])( - `submits a fail transaction if the gas estimation is resolved with null and following static call is also failed with revert string - %#`, - async (gasTarget) => { - const txOpts = { ...gasTarget, nonce: 5 }; - const provider = new ethers.providers.JsonRpcProvider(); - estimateFulfillMock.mockResolvedValueOnce(null); - staticFulfillMock.mockResolvedValueOnce({ - callSuccess: false, - callData: - '0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000e416c776179732072657665727473000000000000000000000000000000000000', - }); - (failMock as jest.Mock).mockResolvedValueOnce({ hash: '0xfailtransaction' }); - const apiCall = fixtures.requests.buildSuccessfulApiCall({ - id: '0xb56b66dc089eab3dc98672ea5e852488730a8f76621fd9ea719504ea205980f8', - data: { - encodedValue: '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', - signature: - '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', - }, - nonce: 5, - }); - const [logs, err, data] = await apiCalls.estimateGasAndSubmitFulfill(createAirnodeRrpFake(), apiCall, { - gasTarget, - masterHDNode, - provider, - }); - expect(logs).toEqual([ - { - level: 'DEBUG', - message: `Attempting to estimate gas for API call fulfillment for Request:${apiCall.id}...`, - }, - { level: 'DEBUG', message: `Attempting to fulfill API call for Request:${apiCall.id}...` }, - { level: 'INFO', message: `Submitting API call fail for Request:${apiCall.id}...` }, - ]); - expect(err).toEqual(null); - expect(data).toEqual({ - ...apiCall, - fulfillment: { hash: '0xfailtransaction' }, - errorMessage: `Fulfill transaction failed`, - }); - expect(estimateFulfillMock).toHaveBeenCalledTimes(1); - expect(estimateFulfillMock).toHaveBeenCalledWith( - apiCall.id, - apiCall.airnodeAddress, - apiCall.fulfillAddress, - apiCall.fulfillFunctionId, - '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', - '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b' - ); - expect(staticFulfillMock).toHaveBeenCalledTimes(1); - expect(fulfillMock).not.toHaveBeenCalled(); - expect(failMock).toHaveBeenCalledTimes(1); - expect(failMock).toHaveBeenCalledWith( - apiCall.id, - apiCall.airnodeAddress, - apiCall.fulfillAddress, - apiCall.fulfillFunctionId, - `Always reverts`, - txOpts - ); - } - ); - - test.each([gasTargetWithoutGasLimit, gasTargetFallbackWithoutGasLimit])( - `returns an error if everything fails - %#`, - async (gasTarget) => { - const txOpts = { ...gasTarget, nonce: 5 }; - const provider = new ethers.providers.JsonRpcProvider(); - const estimateGasError = new Error('Estimate gas error'); - const staticCallError = new Error('Static call error'); - estimateFulfillMock.mockRejectedValueOnce(estimateGasError); - estimateFulfillMock.mockRejectedValueOnce(estimateGasError); - staticFulfillMock.mockRejectedValueOnce(staticCallError); - staticFulfillMock.mockRejectedValueOnce(staticCallError); - - const failTxError = new Error('Fail transaction error'); - failMock.mockRejectedValueOnce(failTxError); - failMock.mockRejectedValueOnce(failTxError); - const apiCall = fixtures.requests.buildSuccessfulApiCall({ - id: '0xb56b66dc089eab3dc98672ea5e852488730a8f76621fd9ea719504ea205980f8', - data: { - encodedValue: '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', - signature: - '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b', - }, - nonce: 5, - }); - const [logs, err, data] = await apiCalls.estimateGasAndSubmitFulfill(createAirnodeRrpFake(), apiCall, { - gasTarget, - masterHDNode, - provider, - }); - expect(logs).toEqual([ - { - level: 'DEBUG', - message: `Attempting to estimate gas for API call fulfillment for Request:${apiCall.id}...`, - }, - { - error: estimateGasError, - level: 'ERROR', - message: `Gas estimation for API call fulfillment failed for Request:${apiCall.id} with ${estimateGasError}`, - }, - { level: 'DEBUG', message: `Attempting to fulfill API call for Request:${apiCall.id}...` }, - { - error: staticCallError, - level: 'ERROR', - message: `Static call fulfillment failed for Request:${apiCall.id} with ${staticCallError}`, - }, - { level: 'INFO', message: `Submitting API call fail for Request:${apiCall.id}...` }, - { - error: failTxError, - level: 'ERROR', - message: `Error submitting API call fail transaction for Request:${apiCall.id}`, - }, - ]); - expect(err).toEqual(failTxError); - expect(data).toEqual(null); - expect(estimateFulfillMock).toHaveBeenCalledTimes(2); - expect(estimateFulfillMock).toHaveBeenNthCalledWith( - 2, - apiCall.id, - apiCall.airnodeAddress, - apiCall.fulfillAddress, - apiCall.fulfillFunctionId, - '0x448b8ad3a330cf8f269f487881b59efff721b3dfa8e61f7c8fd2480389459ed3', - '0xda6d5aa27f48aa951ba401c8a779645f7d1fa4a46a5e99eb7da04b4e059449a834ca1058c85dfe8117305265228f8cf7ae64c3ef3c4d1cc191f77807227dac461b' - ); - expect(staticFulfillMock).toHaveBeenCalledTimes(2); - expect(fulfillMock).not.toHaveBeenCalled(); - expect(failMock).toHaveBeenCalledTimes(2); - expect(failMock).toHaveBeenNthCalledWith( - 2, - apiCall.id, - apiCall.airnodeAddress, - apiCall.fulfillAddress, - apiCall.fulfillFunctionId, - 'Static call error', - txOpts - ); - } - ); - }); - }); }); diff --git a/packages/airnode-node/src/evm/fulfillments/api-calls.ts b/packages/airnode-node/src/evm/fulfillments/api-calls.ts index e1fd0544d9..1e0d2554c5 100644 --- a/packages/airnode-node/src/evm/fulfillments/api-calls.ts +++ b/packages/airnode-node/src/evm/fulfillments/api-calls.ts @@ -75,38 +75,6 @@ async function testFulfill( return [[noticeLog], null, goRes.data]; } -async function estimateGasToFulfill( - airnodeRrp: AirnodeRrpV0, - request: Request -): Promise> { - const noticeLog = logger.pend( - 'DEBUG', - `Attempting to estimate gas for API call fulfillment for Request:${request.id}...` - ); - if (!request.success) return [[], new Error('Only successful API can be submitted'), null]; - - const operation = (): Promise => - airnodeRrp.estimateGas.fulfill( - request.id, - request.airnodeAddress, - request.fulfillAddress, - request.fulfillFunctionId, - request.data.encodedValue, - request.data.signature - ); - const goRes = await go(operation, { retries: 1, attemptTimeoutMs: BLOCKCHAIN_CALL_ATTEMPT_TIMEOUT }); - if (!goRes.success) { - const errorLog = logger.pend( - 'ERROR', - `Gas estimation for API call fulfillment failed for Request:${request.id} with ${goRes.error}`, - goRes.error - ); - return [[noticeLog, errorLog], goRes.error, null]; - } - - return [[noticeLog], null, goRes.data]; -} - async function submitFulfill( airnodeRrp: AirnodeRrpV0, request: Request, @@ -140,11 +108,16 @@ async function submitFulfill( return [[noticeLog], null, applyTransactionResult(request, goRes.data)]; } -export async function testAndSubmitFulfill( +async function testAndSubmitFulfill( airnodeRrp: AirnodeRrpV0, request: Request, options: TransactionOptions ): Promise>> { + const errorMessage = requests.getErrorMessage(request); + if (errorMessage) { + return submitFail(airnodeRrp, request, errorMessage, options); + } + // Should not throw const [testLogs, testErr, testData] = await testFulfill(airnodeRrp, request, options); @@ -178,60 +151,6 @@ export async function testAndSubmitFulfill( return [[...testLogs, errorLog], testErr, null]; } -export async function estimateGasAndSubmitFulfill( - airnodeRrp: AirnodeRrpV0, - request: Request, - options: TransactionOptions -): Promise>> { - // Should not throw - const [estimateGasLogs, estimateGasErr, estimateGasData] = await estimateGasToFulfill(airnodeRrp, request); - - if (estimateGasErr || !estimateGasData) { - // Make static test call to get revert string - const [testLogs, testErr, testData] = await testFulfill(airnodeRrp, request, options); - - if (testErr || !testData || !testData.callSuccess) { - const updatedRequest: Request = { - ...request, - errorMessage: testErr - ? `${RequestErrorMessage.FulfillTransactionFailed} with error: ${testErr.message}` - : RequestErrorMessage.FulfillTransactionFailed, - }; - const [submitLogs, submitErr, submittedRequest] = await submitFail( - airnodeRrp, - updatedRequest, - testErr?.message ?? decodeRevertString(testData?.callData || '0x'), - options - ); - return [[...estimateGasLogs, ...testLogs, ...submitLogs], submitErr, submittedRequest]; - } - - // If static test call is successful even though gas estimation failure, - // it'll be caused by an RPC issue, so submit specific reason to chain - const updatedRequest: Request = { - ...request, - errorMessage: estimateGasErr - ? `${RequestErrorMessage.GasEstimationFailed} with error: ${estimateGasErr.message}` - : RequestErrorMessage.GasEstimationFailed, - }; - const [submitLogs, submitErr, submittedRequest] = await submitFail( - airnodeRrp, - updatedRequest, - estimateGasErr?.message ?? RequestErrorMessage.GasEstimationFailed, - options - ); - return [[...estimateGasLogs, ...testLogs, ...submitLogs], submitErr, submittedRequest]; - } - - // If gas estimation is success, submit fulfillment without making static test call - const gasLimitNoticeLog = logger.pend('INFO', `Gas limit is set to ${estimateGasData} for Request:${request.id}.`); - const [submitLogs, submitErr, submitData] = await submitFulfill(airnodeRrp, request, { - ...options, - gasTarget: { ...options.gasTarget, gasLimit: estimateGasData }, - }); - return [[...estimateGasLogs, gasLimitNoticeLog, ...submitLogs], submitErr, submitData]; -} - // ================================================================= // Failures // ================================================================= @@ -284,13 +203,6 @@ export const submitApiCall: SubmitRequest = (airnodeRrp, re return Promise.resolve([[log], null, null]); } - const errorMessage = requests.getErrorMessage(request); - if (errorMessage) { - return submitFail(airnodeRrp, request, errorMessage, options); - } - // Should not throw - if (options.gasTarget.gasLimit) return testAndSubmitFulfill(airnodeRrp, request, options); - - return estimateGasAndSubmitFulfill(airnodeRrp, request, options); + return testAndSubmitFulfill(airnodeRrp, request, options); }; diff --git a/packages/airnode-node/src/evm/fulfillments/index.test.ts b/packages/airnode-node/src/evm/fulfillments/index.test.ts index 7617dba240..7aea9d1beb 100644 --- a/packages/airnode-node/src/evm/fulfillments/index.test.ts +++ b/packages/airnode-node/src/evm/fulfillments/index.test.ts @@ -4,13 +4,12 @@ const failMock = jest.fn(); const fulfillMock = jest.fn(); const fulfillWithdrawalMock = jest.fn(); const staticFulfillMock = jest.fn(); -const estimateFulfillMock = jest.fn(); mockEthers({ airnodeRrpMocks: { callStatic: { fulfill: staticFulfillMock, }, - estimateGas: { fulfill: estimateFulfillMock, fulfillWithdrawal: estimateWithdrawalGasMock }, + estimateGas: { fulfillWithdrawal: estimateWithdrawalGasMock }, fail: failMock, fulfill: fulfillMock, fulfillWithdrawal: fulfillWithdrawalMock, @@ -58,7 +57,6 @@ describe('submit', () => { const provider = new ethers.providers.JsonRpcProvider(); const state = providerState.update(mutableInitialState, { gasTarget, provider, requests }); - estimateFulfillMock.mockResolvedValue(73804); staticFulfillMock.mockResolvedValue({ callSuccess: true }); fulfillMock.mockResolvedValueOnce({ hash: '0xapicall_tx1' }); fulfillMock.mockResolvedValueOnce({ hash: '0xapicall_tx2' }); diff --git a/packages/airnode-node/src/types.ts b/packages/airnode-node/src/types.ts index 586d9c1fe7..fd4fbe340b 100644 --- a/packages/airnode-node/src/types.ts +++ b/packages/airnode-node/src/types.ts @@ -45,7 +45,6 @@ export enum RequestErrorMessage { ApiCallFailed = 'API call failed', ReservedParametersInvalid = 'Reserved parameters are invalid', FulfillTransactionFailed = 'Fulfill transaction failed', - GasEstimationFailed = 'Gas estimation failed', SponsorRequestLimitExceeded = 'Sponsor request limit exceeded', } diff --git a/packages/airnode-validator/src/config/config.ts b/packages/airnode-validator/src/config/config.ts index 940aa8280d..f7eec806e9 100644 --- a/packages/airnode-validator/src/config/config.ts +++ b/packages/airnode-validator/src/config/config.ts @@ -147,7 +147,7 @@ export const gasPriceOracleSchema = z export const chainOptionsSchema = z .object({ - fulfillmentGasLimit: z.number().int().optional(), + fulfillmentGasLimit: z.number().int(), withdrawalRemainder: amountSchema.optional(), gasPriceOracle: gasPriceOracleSchema, }) From 0e1980a9cc05c5cc5148df18178051f5263e1bd0 Mon Sep 17 00:00:00 2001 From: Emanuel Tesar Date: Tue, 4 Apr 2023 12:50:55 +0200 Subject: [PATCH 2/2] Add empty changeset --- .changeset/stupid-cups-dance.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changeset/stupid-cups-dance.md diff --git a/.changeset/stupid-cups-dance.md b/.changeset/stupid-cups-dance.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/stupid-cups-dance.md @@ -0,0 +1,2 @@ +--- +---