diff --git a/.changeset/wicked-socks-peel.md b/.changeset/wicked-socks-peel.md new file mode 100644 index 0000000000..7518d356db --- /dev/null +++ b/.changeset/wicked-socks-peel.md @@ -0,0 +1,9 @@ +--- +'@api3/airnode-adapter': minor +'@api3/airnode-deployer': minor +'@api3/airnode-examples': minor +'@api3/airnode-node': minor +'@api3/airnode-validator': minor +--- + +Added API call skip feature. diff --git a/packages/airnode-adapter/package.json b/packages/airnode-adapter/package.json index f149f6490b..442b527bcf 100644 --- a/packages/airnode-adapter/package.json +++ b/packages/airnode-adapter/package.json @@ -19,7 +19,7 @@ "test:watch": "yarn test:ts --watch" }, "dependencies": { - "@api3/ois": "1.2.0", + "@api3/ois": "1.3.0", "@api3/promise-utils": "^0.3.0", "axios": "1.1.3", "bignumber.js": "^9.1.0", diff --git a/packages/airnode-adapter/src/request-building/build-request.ts b/packages/airnode-adapter/src/request-building/build-request.ts index 8a11895672..b6b83900f2 100644 --- a/packages/airnode-adapter/src/request-building/build-request.ts +++ b/packages/airnode-adapter/src/request-building/build-request.ts @@ -10,7 +10,7 @@ function cacheRequestOptions(options: BuildRequestOptions): CachedBuildRequestOp throw new Error(`Endpoint: '${options.endpointName}' not found in the OIS.`); } - const { method, path } = endpoint.operation; + const { method, path } = endpoint.operation!; const operation = ois.apiSpecifications.paths[path][method]!; return { ...options, endpoint, operation }; } @@ -25,12 +25,12 @@ export function buildRequest(options: BuildRequestOptions): Request { // Different base URLs are not supported at the operation level const baseUrl = ois.apiSpecifications.servers[0].url; const parameters = buildParameters(cachedOptions); - const path = parsePathWithParameters(endpoint.operation.path, parameters.paths); + const path = parsePathWithParameters(endpoint.operation!.path, parameters.paths); return { baseUrl, path, - method: endpoint.operation.method, + method: endpoint.operation!.method, headers: parameters.headers, data: parameters.query, }; diff --git a/packages/airnode-adapter/test/fixtures/ois.ts b/packages/airnode-adapter/test/fixtures/ois.ts index deea87d6c7..71db60098a 100644 --- a/packages/airnode-adapter/test/fixtures/ois.ts +++ b/packages/airnode-adapter/test/fixtures/ois.ts @@ -2,7 +2,7 @@ import { OIS } from '@api3/ois'; export function buildOIS(overrides?: Partial): OIS { return { - oisFormat: '1.2.0', + oisFormat: '1.3.0', version: '1.2.3', title: 'Currency Converter API', apiSpecifications: { diff --git a/packages/airnode-adapter/test/fixtures/options.ts b/packages/airnode-adapter/test/fixtures/options.ts index 8266cab1ec..c3e69c8cba 100644 --- a/packages/airnode-adapter/test/fixtures/options.ts +++ b/packages/airnode-adapter/test/fixtures/options.ts @@ -25,7 +25,7 @@ export function buildRequestOptions(overrides?: Partial): B export function buildCacheRequestOptions(overrides?: Partial): CachedBuildRequestOptions { const options = buildRequestOptions(); const endpoint = options.ois.endpoints[0]; - const { method, path } = endpoint.operation; + const { method, path } = endpoint.operation!; const operation = options.ois.apiSpecifications.paths[path][method]!; return { ...options, diff --git a/packages/airnode-deployer/config/config.example.json b/packages/airnode-deployer/config/config.example.json index 916098a403..b018311b02 100644 --- a/packages/airnode-deployer/config/config.example.json +++ b/packages/airnode-deployer/config/config.example.json @@ -87,7 +87,7 @@ "templates": [], "ois": [ { - "oisFormat": "1.2.0", + "oisFormat": "1.3.0", "title": "CoinGecko basic request", "version": "1.0.0", "apiSpecifications": { diff --git a/packages/airnode-deployer/test/fixtures/config.aws.valid.json b/packages/airnode-deployer/test/fixtures/config.aws.valid.json index 2ef480ac71..90ba186557 100644 --- a/packages/airnode-deployer/test/fixtures/config.aws.valid.json +++ b/packages/airnode-deployer/test/fixtures/config.aws.valid.json @@ -90,7 +90,7 @@ "templates": [], "ois": [ { - "oisFormat": "1.2.0", + "oisFormat": "1.3.0", "title": "CoinGecko basic request", "version": "1.0.0", "apiSpecifications": { diff --git a/packages/airnode-deployer/test/fixtures/config.gcp.valid.json b/packages/airnode-deployer/test/fixtures/config.gcp.valid.json index 0a5104c47a..f497a1606f 100644 --- a/packages/airnode-deployer/test/fixtures/config.gcp.valid.json +++ b/packages/airnode-deployer/test/fixtures/config.gcp.valid.json @@ -90,7 +90,7 @@ "templates": [], "ois": [ { - "oisFormat": "1.2.0", + "oisFormat": "1.3.0", "title": "CoinGecko basic request", "version": "1.0.0", "apiSpecifications": { diff --git a/packages/airnode-examples/integrations/authenticated-coinmarketcap/config.example.json b/packages/airnode-examples/integrations/authenticated-coinmarketcap/config.example.json index acee6ff180..ac3a324f56 100644 --- a/packages/airnode-examples/integrations/authenticated-coinmarketcap/config.example.json +++ b/packages/airnode-examples/integrations/authenticated-coinmarketcap/config.example.json @@ -78,7 +78,7 @@ "templates": [], "ois": [ { - "oisFormat": "1.2.0", + "oisFormat": "1.3.0", "title": "CoinMarketCap Basic Authenticated Request", "version": "1.0.0", "apiSpecifications": { diff --git a/packages/airnode-examples/integrations/authenticated-coinmarketcap/create-config.ts b/packages/airnode-examples/integrations/authenticated-coinmarketcap/create-config.ts index b34252e220..1b81ecac13 100644 --- a/packages/airnode-examples/integrations/authenticated-coinmarketcap/create-config.ts +++ b/packages/airnode-examples/integrations/authenticated-coinmarketcap/create-config.ts @@ -85,7 +85,7 @@ const createConfig = async (generateExampleFile: boolean): Promise => ({ templates: [], ois: [ { - oisFormat: '1.2.0', + oisFormat: '1.3.0', title: 'CoinMarketCap Basic Authenticated Request', version: '1.0.0', apiSpecifications: { diff --git a/packages/airnode-examples/integrations/coingecko-cross-chain-authorizer/config.example.json b/packages/airnode-examples/integrations/coingecko-cross-chain-authorizer/config.example.json index 2d62297904..f00abb0c97 100644 --- a/packages/airnode-examples/integrations/coingecko-cross-chain-authorizer/config.example.json +++ b/packages/airnode-examples/integrations/coingecko-cross-chain-authorizer/config.example.json @@ -90,7 +90,7 @@ "templates": [], "ois": [ { - "oisFormat": "1.2.0", + "oisFormat": "1.3.0", "title": "CoinGecko basic request", "version": "1.0.0", "apiSpecifications": { diff --git a/packages/airnode-examples/integrations/coingecko-cross-chain-authorizer/create-config.ts b/packages/airnode-examples/integrations/coingecko-cross-chain-authorizer/create-config.ts index 1cfeeedba2..e952ed64f5 100644 --- a/packages/airnode-examples/integrations/coingecko-cross-chain-authorizer/create-config.ts +++ b/packages/airnode-examples/integrations/coingecko-cross-chain-authorizer/create-config.ts @@ -98,7 +98,7 @@ const createConfig = async (generateExampleFile: boolean): Promise => ({ templates: [], ois: [ { - oisFormat: '1.2.0', + oisFormat: '1.3.0', title: 'CoinGecko basic request', version: '1.0.0', apiSpecifications: { diff --git a/packages/airnode-examples/integrations/coingecko-post-processing/config.example.json b/packages/airnode-examples/integrations/coingecko-post-processing/config.example.json index 599b6525b1..a9e3de245c 100644 --- a/packages/airnode-examples/integrations/coingecko-post-processing/config.example.json +++ b/packages/airnode-examples/integrations/coingecko-post-processing/config.example.json @@ -78,7 +78,7 @@ "templates": [], "ois": [ { - "oisFormat": "1.2.0", + "oisFormat": "1.3.0", "title": "CoinGecko coins markets request", "version": "1.0.0", "apiSpecifications": { diff --git a/packages/airnode-examples/integrations/coingecko-post-processing/create-config.ts b/packages/airnode-examples/integrations/coingecko-post-processing/create-config.ts index b491e15d92..daf6173e06 100644 --- a/packages/airnode-examples/integrations/coingecko-post-processing/create-config.ts +++ b/packages/airnode-examples/integrations/coingecko-post-processing/create-config.ts @@ -85,7 +85,7 @@ const createConfig = async (generateExampleFile: boolean): Promise => ({ templates: [], ois: [ { - oisFormat: '1.2.0', + oisFormat: '1.3.0', title: 'CoinGecko coins markets request', version: '1.0.0', apiSpecifications: { diff --git a/packages/airnode-examples/integrations/coingecko-pre-processing/config.example.json b/packages/airnode-examples/integrations/coingecko-pre-processing/config.example.json index 8c0c6cb1bc..3be4e5579e 100644 --- a/packages/airnode-examples/integrations/coingecko-pre-processing/config.example.json +++ b/packages/airnode-examples/integrations/coingecko-pre-processing/config.example.json @@ -78,7 +78,7 @@ "templates": [], "ois": [ { - "oisFormat": "1.2.0", + "oisFormat": "1.3.0", "title": "CoinGecko history data request", "version": "1.0.0", "apiSpecifications": { diff --git a/packages/airnode-examples/integrations/coingecko-pre-processing/create-config.ts b/packages/airnode-examples/integrations/coingecko-pre-processing/create-config.ts index 0515af2a29..d1e8481afa 100644 --- a/packages/airnode-examples/integrations/coingecko-pre-processing/create-config.ts +++ b/packages/airnode-examples/integrations/coingecko-pre-processing/create-config.ts @@ -85,7 +85,7 @@ const createConfig = async (generateExampleFile: boolean): Promise => ({ templates: [], ois: [ { - oisFormat: '1.2.0', + oisFormat: '1.3.0', title: 'CoinGecko history data request', version: '1.0.0', apiSpecifications: { diff --git a/packages/airnode-examples/integrations/coingecko-signed-data/config.example.json b/packages/airnode-examples/integrations/coingecko-signed-data/config.example.json index ec69799c20..6fc754a614 100644 --- a/packages/airnode-examples/integrations/coingecko-signed-data/config.example.json +++ b/packages/airnode-examples/integrations/coingecko-signed-data/config.example.json @@ -86,7 +86,7 @@ "templates": [], "ois": [ { - "oisFormat": "1.2.0", + "oisFormat": "1.3.0", "title": "CoinGecko basic request", "version": "1.0.0", "apiSpecifications": { diff --git a/packages/airnode-examples/integrations/coingecko-signed-data/create-config.ts b/packages/airnode-examples/integrations/coingecko-signed-data/create-config.ts index b50689c2ad..48e33fe7f1 100644 --- a/packages/airnode-examples/integrations/coingecko-signed-data/create-config.ts +++ b/packages/airnode-examples/integrations/coingecko-signed-data/create-config.ts @@ -93,7 +93,7 @@ const createConfig = async (generateExampleFile: boolean): Promise => ({ templates: [], ois: [ { - oisFormat: '1.2.0', + oisFormat: '1.3.0', title: 'CoinGecko basic request', version: '1.0.0', apiSpecifications: { diff --git a/packages/airnode-examples/integrations/coingecko-template/config.example.json b/packages/airnode-examples/integrations/coingecko-template/config.example.json index 54a4e4d3b7..44cd19b80d 100644 --- a/packages/airnode-examples/integrations/coingecko-template/config.example.json +++ b/packages/airnode-examples/integrations/coingecko-template/config.example.json @@ -84,7 +84,7 @@ ], "ois": [ { - "oisFormat": "1.2.0", + "oisFormat": "1.3.0", "title": "CoinGecko basic request", "version": "1.0.0", "apiSpecifications": { diff --git a/packages/airnode-examples/integrations/coingecko-template/create-config.ts b/packages/airnode-examples/integrations/coingecko-template/create-config.ts index 9fc74d61a5..be9319f30a 100644 --- a/packages/airnode-examples/integrations/coingecko-template/create-config.ts +++ b/packages/airnode-examples/integrations/coingecko-template/create-config.ts @@ -93,7 +93,7 @@ const createConfig = async (generateExampleFile: boolean): Promise => ({ ], ois: [ { - oisFormat: '1.2.0', + oisFormat: '1.3.0', title: 'CoinGecko basic request', version: '1.0.0', apiSpecifications: { diff --git a/packages/airnode-examples/integrations/coingecko-testable/config.example.json b/packages/airnode-examples/integrations/coingecko-testable/config.example.json index 2b313d3a1d..979d087738 100644 --- a/packages/airnode-examples/integrations/coingecko-testable/config.example.json +++ b/packages/airnode-examples/integrations/coingecko-testable/config.example.json @@ -86,7 +86,7 @@ "templates": [], "ois": [ { - "oisFormat": "1.2.0", + "oisFormat": "1.3.0", "title": "CoinGecko basic request", "version": "1.0.0", "apiSpecifications": { diff --git a/packages/airnode-examples/integrations/coingecko-testable/create-config.ts b/packages/airnode-examples/integrations/coingecko-testable/create-config.ts index 7a4b305d3c..e9cf107d2f 100644 --- a/packages/airnode-examples/integrations/coingecko-testable/create-config.ts +++ b/packages/airnode-examples/integrations/coingecko-testable/create-config.ts @@ -93,7 +93,7 @@ const createConfig = async (generateExampleFile: boolean): Promise => ({ templates: [], ois: [ { - oisFormat: '1.2.0', + oisFormat: '1.3.0', title: 'CoinGecko basic request', version: '1.0.0', apiSpecifications: { diff --git a/packages/airnode-examples/integrations/coingecko/config.example.json b/packages/airnode-examples/integrations/coingecko/config.example.json index 3c4dc3bd26..415333febe 100644 --- a/packages/airnode-examples/integrations/coingecko/config.example.json +++ b/packages/airnode-examples/integrations/coingecko/config.example.json @@ -78,7 +78,7 @@ "templates": [], "ois": [ { - "oisFormat": "1.2.0", + "oisFormat": "1.3.0", "title": "CoinGecko basic request", "version": "1.0.0", "apiSpecifications": { diff --git a/packages/airnode-examples/integrations/coingecko/create-config.ts b/packages/airnode-examples/integrations/coingecko/create-config.ts index 3d09555f3c..68a2f4928d 100644 --- a/packages/airnode-examples/integrations/coingecko/create-config.ts +++ b/packages/airnode-examples/integrations/coingecko/create-config.ts @@ -85,7 +85,7 @@ const createConfig = async (generateExampleFile: boolean): Promise => ({ templates: [], ois: [ { - oisFormat: '1.2.0', + oisFormat: '1.3.0', title: 'CoinGecko basic request', version: '1.0.0', apiSpecifications: { diff --git a/packages/airnode-examples/integrations/failing-example/config.example.json b/packages/airnode-examples/integrations/failing-example/config.example.json index fe1f94954d..955dce399b 100644 --- a/packages/airnode-examples/integrations/failing-example/config.example.json +++ b/packages/airnode-examples/integrations/failing-example/config.example.json @@ -78,7 +78,7 @@ "templates": [], "ois": [ { - "oisFormat": "1.2.0", + "oisFormat": "1.3.0", "title": "Failure Example", "version": "1.0.0", "apiSpecifications": { diff --git a/packages/airnode-examples/integrations/failing-example/create-config.ts b/packages/airnode-examples/integrations/failing-example/create-config.ts index bd88389bc5..693502fd25 100644 --- a/packages/airnode-examples/integrations/failing-example/create-config.ts +++ b/packages/airnode-examples/integrations/failing-example/create-config.ts @@ -85,7 +85,7 @@ const createConfig = async (generateExampleFile: boolean): Promise => ({ templates: [], ois: [ { - oisFormat: '1.2.0', + oisFormat: '1.3.0', title: 'Failure Example', version: '1.0.0', apiSpecifications: { diff --git a/packages/airnode-examples/integrations/relay-security-schemes/config.example.json b/packages/airnode-examples/integrations/relay-security-schemes/config.example.json index 7e3120b5da..2ce4592727 100644 --- a/packages/airnode-examples/integrations/relay-security-schemes/config.example.json +++ b/packages/airnode-examples/integrations/relay-security-schemes/config.example.json @@ -78,7 +78,7 @@ "templates": [], "ois": [ { - "oisFormat": "1.2.0", + "oisFormat": "1.3.0", "title": "Relay Security Schemes via httpbin", "version": "1.0.0", "apiSpecifications": { diff --git a/packages/airnode-examples/integrations/relay-security-schemes/create-config.ts b/packages/airnode-examples/integrations/relay-security-schemes/create-config.ts index 4e0ea52cd0..6937a4ffe8 100644 --- a/packages/airnode-examples/integrations/relay-security-schemes/create-config.ts +++ b/packages/airnode-examples/integrations/relay-security-schemes/create-config.ts @@ -85,7 +85,7 @@ const createConfig = async (generateExampleFile: boolean): Promise => ({ templates: [], ois: [ { - oisFormat: '1.2.0', + oisFormat: '1.3.0', title: 'Relay Security Schemes via httpbin', version: '1.0.0', apiSpecifications: { diff --git a/packages/airnode-examples/integrations/weather-multi-value/config.example.json b/packages/airnode-examples/integrations/weather-multi-value/config.example.json index ab52079bc6..02a813a161 100644 --- a/packages/airnode-examples/integrations/weather-multi-value/config.example.json +++ b/packages/airnode-examples/integrations/weather-multi-value/config.example.json @@ -78,7 +78,7 @@ "templates": [], "ois": [ { - "oisFormat": "1.2.0", + "oisFormat": "1.3.0", "title": "OpenWeather Multiple Encoded Values", "version": "1.0.0", "apiSpecifications": { diff --git a/packages/airnode-examples/integrations/weather-multi-value/create-config.ts b/packages/airnode-examples/integrations/weather-multi-value/create-config.ts index 23c52e93df..1ed8bbf5ba 100644 --- a/packages/airnode-examples/integrations/weather-multi-value/create-config.ts +++ b/packages/airnode-examples/integrations/weather-multi-value/create-config.ts @@ -85,7 +85,7 @@ const createConfig = async (generateExampleFile: boolean): Promise => ({ templates: [], ois: [ { - oisFormat: '1.2.0', + oisFormat: '1.3.0', title: 'OpenWeather Multiple Encoded Values', version: '1.0.0', apiSpecifications: { diff --git a/packages/airnode-node/config/config.example.json b/packages/airnode-node/config/config.example.json index dd24136078..7c17209f07 100644 --- a/packages/airnode-node/config/config.example.json +++ b/packages/airnode-node/config/config.example.json @@ -79,7 +79,7 @@ "templates": [], "ois": [ { - "oisFormat": "1.2.0", + "oisFormat": "1.3.0", "version": "1.2.3", "title": "Currency Converter API", "apiSpecifications": { diff --git a/packages/airnode-node/package.json b/packages/airnode-node/package.json index cdafcc7eeb..30de5b2b24 100644 --- a/packages/airnode-node/package.json +++ b/packages/airnode-node/package.json @@ -29,7 +29,7 @@ "@api3/airnode-protocol": "^0.9.0", "@api3/airnode-utilities": "^0.9.0", "@api3/airnode-validator": "^0.9.0", - "@api3/ois": "1.2.0", + "@api3/ois": "1.3.0", "@api3/promise-utils": "^0.3.0", "aws-sdk": "^2.1243.0", "dotenv": "^16.0.3", diff --git a/packages/airnode-node/src/api/index.test.ts b/packages/airnode-node/src/api/index.test.ts index 47ab7c1a35..d511004d3c 100644 --- a/packages/airnode-node/src/api/index.test.ts +++ b/packages/airnode-node/src/api/index.test.ts @@ -414,6 +414,175 @@ describe('callApi', () => { ); }); }); + describe('skip API call', () => { + const createEncodedValue = (value: ethers.BigNumber, times = 100_000) => + `0x${value.mul(times).toHexString().substring(2).padStart(64, '0')}`; + + it('skips the API call with preProcessingSpecifications', async () => { + const config = fixtures.buildConfig(); + const parameters = { _type: 'int256', _path: 'result', parameter1: '25' }; + const aggregatedApiCall = fixtures.buildAggregatedRegularApiCall({ parameters }); + const preProcessingSpecifications = [ + { + environment: 'Node 14' as const, + value: 'const output = { result: (parseInt(input.parameter1) * 2).toString() }', + timeoutMs: 5000, + }, + ]; + + config.ois[0].endpoints[0].operation = undefined; + config.ois[0].endpoints[0].fixedOperationParameters = []; + config.ois[0].endpoints[0].preProcessingSpecifications = preProcessingSpecifications; + + const [logs, res] = await callApi({ + type: 'regular', + config, + aggregatedApiCall, + }); + + expect(logs).toEqual([]); + expect(res).toEqual({ + success: true, + data: { + encodedValue: createEncodedValue(ethers.BigNumber.from(25 * 2)), + signature: + '0x148d65210b201c3ddd6f8e08cfc29032e9f3c781919eeb80b950dbb984ebbbee289105ea8b591ace166d04b65e334801ea0177bb431775db2b4268d55fa4e2a81c', + }, + }); + }); + + it('skips the API call with postProcessingSpecifications', async () => { + const config = fixtures.buildConfig(); + const parameters = { _type: 'int256', _path: 'result', parameter1: '25' }; + const aggregatedApiCall = fixtures.buildAggregatedRegularApiCall({ parameters }); + const postProcessingSpecifications = [ + { + environment: 'Node 14' as const, + value: 'const output = { result: (parseInt(input.parameter1) * 2).toString() }', + timeoutMs: 5000, + }, + ]; + + config.ois[0].endpoints[0].operation = undefined; + config.ois[0].endpoints[0].fixedOperationParameters = []; + config.ois[0].endpoints[0].postProcessingSpecifications = postProcessingSpecifications; + + const [logs, res] = await callApi({ + type: 'regular', + config, + aggregatedApiCall, + }); + + expect(logs).toEqual([]); + expect(res).toEqual({ + success: true, + data: { + encodedValue: createEncodedValue(ethers.BigNumber.from(25 * 2)), + signature: + '0x148d65210b201c3ddd6f8e08cfc29032e9f3c781919eeb80b950dbb984ebbbee289105ea8b591ace166d04b65e334801ea0177bb431775db2b4268d55fa4e2a81c', + }, + }); + }); + + it('skips API call with both preProcessingSpecifications and postProcessingSpecifications', async () => { + const config = fixtures.buildConfig(); + const parameters = { _type: 'int256', _path: 'result', parameter1: '25' }; + const aggregatedApiCall = fixtures.buildAggregatedRegularApiCall({ parameters }); + const preProcessingSpecifications = [ + { + environment: 'Node 14' as const, + value: 'const output = {...input, addThis: "5" }', + timeoutMs: 5000, + }, + ]; + const postProcessingSpecifications = [ + { + environment: 'Node 14' as const, + value: 'const output = { result: (parseInt(input.parameter1) * 2 + parseInt(input.addThis)).toString() }', + timeoutMs: 5000, + }, + ]; + + config.ois[0].endpoints[0].operation = undefined; + config.ois[0].endpoints[0].fixedOperationParameters = []; + config.ois[0].endpoints[0].preProcessingSpecifications = preProcessingSpecifications; + config.ois[0].endpoints[0].postProcessingSpecifications = postProcessingSpecifications; + + const [logs, res] = await callApi({ + type: 'regular', + config, + aggregatedApiCall, + }); + + expect(logs).toEqual([]); + expect(res).toEqual({ + success: true, + data: { + encodedValue: createEncodedValue(ethers.BigNumber.from(25 * 2 + 5)), + signature: + '0x9796e8ba07c517a43b2ca1416b9c975541345c5b7bf73613738ed84216c4573d1656cbc385acb9111b7077fba65418b59cfe3f59ee81109523664bbc25efcfd71b', + }, + }); + }); + + it('fails when both preProcessingSpecifications and postProcessingSpecifications are empty', async () => { + const config = fixtures.buildConfig(); + const parameters = { _type: 'int256', _path: 'result', parameter1: '25' }; + const aggregatedApiCall = fixtures.buildAggregatedRegularApiCall({ parameters }); + + config.ois[0].endpoints[0].operation = undefined; + config.ois[0].endpoints[0].fixedOperationParameters = []; + config.ois[0].endpoints[0].preProcessingSpecifications = []; + config.ois[0].endpoints[0].postProcessingSpecifications = []; + + const [logs, res] = await callApi({ + type: 'regular', + config, + aggregatedApiCall, + }); + + expect(logs).toEqual([ + { + level: 'ERROR', + message: + "Failed to skip API call. Ensure at least one of 'preProcessingSpecifications' or 'postProcessingSpecifications' is defined and is not an empty array at ois 'Currency Converter API', endpoint 'convertToUSD'.", + }, + ]); + expect(res).toEqual({ + success: false, + errorMessage: `Failed to skip API call. Ensure at least one of 'preProcessingSpecifications' or 'postProcessingSpecifications' is defined and is not an empty array at ois 'Currency Converter API', endpoint 'convertToUSD'.`, + }); + }); + + it('fails when both preProcessingSpecifications and postProcessingSpecifications are undefined', async () => { + const config = fixtures.buildConfig(); + const parameters = { _type: 'int256', _path: 'result', parameter1: '25' }; + const aggregatedApiCall = fixtures.buildAggregatedRegularApiCall({ parameters }); + + config.ois[0].endpoints[0].operation = undefined; + config.ois[0].endpoints[0].fixedOperationParameters = []; + config.ois[0].endpoints[0].preProcessingSpecifications = undefined; + config.ois[0].endpoints[0].postProcessingSpecifications = undefined; + + const [logs, res] = await callApi({ + type: 'regular', + config, + aggregatedApiCall, + }); + + expect(logs).toEqual([ + { + level: 'ERROR', + message: + "Failed to skip API call. Ensure at least one of 'preProcessingSpecifications' or 'postProcessingSpecifications' is defined and is not an empty array at ois 'Currency Converter API', endpoint 'convertToUSD'.", + }, + ]); + expect(res).toEqual({ + success: false, + errorMessage: `Failed to skip API call. Ensure at least one of 'preProcessingSpecifications' or 'postProcessingSpecifications' is defined and is not an empty array at ois 'Currency Converter API', endpoint 'convertToUSD'.`, + }); + }); + }); }); describe('verifyTemplateId', () => { diff --git a/packages/airnode-node/src/api/index.ts b/packages/airnode-node/src/api/index.ts index 32905e5d4a..506eacd8b8 100644 --- a/packages/airnode-node/src/api/index.ts +++ b/packages/airnode-node/src/api/index.ts @@ -1,5 +1,6 @@ import * as adapter from '@api3/airnode-adapter'; -import { RESERVED_PARAMETERS } from '@api3/ois'; +import isEmpty from 'lodash/isEmpty'; +import { OIS, RESERVED_PARAMETERS, Endpoint } from '@api3/ois'; import { logger, removeKeys, removeKey } from '@api3/airnode-utilities'; import { go, goSync } from '@api3/promise-utils'; import axios, { AxiosError } from 'axios'; @@ -287,11 +288,25 @@ export async function callApi(payload: ApiCallPayload): Promise o.title === payload.aggregatedApiCall.oisTitle)!; + const endpoint = ois.endpoints.find((e: Endpoint) => e.name === payload.aggregatedApiCall.endpointName)!; + + // skip API call if operation is undefined and fixedOperationParameters is empty array + if (!endpoint.operation && isEmpty(endpoint.fixedOperationParameters)) { + // contents of preProcessingSpecifications or postProcessingSpecifications (or both) will simulate an API when API call is skipped + if (isEmpty(endpoint.preProcessingSpecifications) && isEmpty(endpoint.postProcessingSpecifications)) { + const message = `Failed to skip API call. Ensure at least one of 'preProcessingSpecifications' or 'postProcessingSpecifications' is defined and is not an empty array at ois '${payload.aggregatedApiCall.oisTitle}', endpoint '${payload.aggregatedApiCall.endpointName}'.`; + const log = logger.pend('ERROR', message); + return [[log], { success: false, errorMessage: message }]; + } + // output of preProcessingSpecifications can be used as output directly or + // preProcessingSpecifications can be used to manipulate parameters to use in postProcessingSpecifications + return processSuccessfulApiCall(payload, { data: processedPayload.aggregatedApiCall.parameters }); + } const [logs, response] = await performApiCall(processedPayload); if (isPerformApiCallFailure(response)) { return [logs, response]; } - return processSuccessfulApiCall(payload, response); } diff --git a/packages/airnode-node/test/fixtures/config/ois.ts b/packages/airnode-node/test/fixtures/config/ois.ts index 629d32d723..606afa9169 100644 --- a/packages/airnode-node/test/fixtures/config/ois.ts +++ b/packages/airnode-node/test/fixtures/config/ois.ts @@ -2,7 +2,7 @@ import { OIS } from '@api3/ois'; export function buildOIS(ois?: Partial): OIS { return { - oisFormat: '1.2.0', + oisFormat: '1.3.0', version: '1.2.3', title: 'Currency Converter API', apiSpecifications: { diff --git a/packages/airnode-validator/package.json b/packages/airnode-validator/package.json index 2dd464bcfe..e47f841a35 100644 --- a/packages/airnode-validator/package.json +++ b/packages/airnode-validator/package.json @@ -21,7 +21,7 @@ "test:e2e:update-snapshot": "yarn test:e2e --updateSnapshot" }, "dependencies": { - "@api3/ois": "1.2.0", + "@api3/ois": "1.3.0", "@api3/promise-utils": "^0.3.0", "dotenv": "^16.0.3", "ethers": "^5.7.2", diff --git a/packages/airnode-validator/test/fixtures/config.valid.json b/packages/airnode-validator/test/fixtures/config.valid.json index c25562f0b3..981bf020fc 100644 --- a/packages/airnode-validator/test/fixtures/config.valid.json +++ b/packages/airnode-validator/test/fixtures/config.valid.json @@ -79,7 +79,7 @@ "templates": [], "ois": [ { - "oisFormat": "1.2.0", + "oisFormat": "1.3.0", "title": "CoinGecko basic request", "version": "1.0.0", "apiSpecifications": { diff --git a/packages/airnode-validator/test/fixtures/interpolated-config.valid.json b/packages/airnode-validator/test/fixtures/interpolated-config.valid.json index 7d71136ad2..50650ac7f4 100644 --- a/packages/airnode-validator/test/fixtures/interpolated-config.valid.json +++ b/packages/airnode-validator/test/fixtures/interpolated-config.valid.json @@ -83,7 +83,7 @@ "templates": [], "ois": [ { - "oisFormat": "1.2.0", + "oisFormat": "1.3.0", "title": "CoinGecko basic request", "version": "1.0.0", "apiSpecifications": { diff --git a/packages/airnode-validator/test/fixtures/invalid-secret-name/config.json b/packages/airnode-validator/test/fixtures/invalid-secret-name/config.json index a003da66b3..57a308f0f9 100644 --- a/packages/airnode-validator/test/fixtures/invalid-secret-name/config.json +++ b/packages/airnode-validator/test/fixtures/invalid-secret-name/config.json @@ -67,7 +67,7 @@ "templates": [], "ois": [ { - "oisFormat": "1.2.0", + "oisFormat": "1.3.0", "title": "CoinGecko basic request", "version": "1.0.0", "apiSpecifications": { diff --git a/packages/airnode-validator/test/fixtures/ois.json b/packages/airnode-validator/test/fixtures/ois.json index 6fc78fd197..f97d2d2357 100644 --- a/packages/airnode-validator/test/fixtures/ois.json +++ b/packages/airnode-validator/test/fixtures/ois.json @@ -1,5 +1,5 @@ { - "oisFormat": "1.2.0", + "oisFormat": "1.3.0", "version": "1.2.3", "title": "coinlayer", "apiSpecifications": { diff --git a/yarn.lock b/yarn.lock index c2aae13a14..f1ce386821 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,10 +10,10 @@ "@jridgewell/gen-mapping" "^0.1.0" "@jridgewell/trace-mapping" "^0.3.9" -"@api3/ois@1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@api3/ois/-/ois-1.2.0.tgz#752894b7cd2968a53437b9abaa9aef07968359d3" - integrity sha512-K3d873YhmLCqL0L5op9LBtgPIgcGicC4pNq34olCjhACh3TaowNNk3mFvZrRgUj/TdapiAKvtPtPtvnj1KO2zw== +"@api3/ois@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@api3/ois/-/ois-1.3.0.tgz#3780a1bcd439e2fdb1316955d97f5124311ad316" + integrity sha512-fdUsK1WwVN6FJUPPwDWpjyJNTg2MCaWYej8rr4bIoFMPvBre9ow7vKMw99+qdAS1P5CZP0Yvba14yacy3f3P4w== dependencies: lodash "^4.17.21" zod "^3.19.1" @@ -4108,14 +4108,7 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== -axios@^0.21.0, axios@^0.21.1: - version "0.21.4" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" - integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== - dependencies: - follow-redirects "^1.14.0" - -axios@^1.0.0, axios@^1.1.3: +axios@1.1.3, axios@^1.0.0: version "1.1.3" resolved "https://registry.yarnpkg.com/axios/-/axios-1.1.3.tgz#8274250dada2edf53814ed7db644b9c2866c1e35" integrity sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA== @@ -4124,6 +4117,13 @@ axios@^1.0.0, axios@^1.1.3: form-data "^4.0.0" proxy-from-env "^1.1.0" +axios@^0.21.0, axios@^0.21.1: + version "0.21.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" + integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== + dependencies: + follow-redirects "^1.14.0" + babel-code-frame@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"