diff --git a/packages/airnode-adapter/README.md b/packages/airnode-adapter/README.md index 1e85b81c18..17de3963f9 100644 --- a/packages/airnode-adapter/README.md +++ b/packages/airnode-adapter/README.md @@ -1,261 +1,10 @@ # @api3/airnode-adapter -The adapter package for @api3 contains logic for building requests from an -[Oracle Integration Specification (OIS)](https://docs.api3.org/airnode/v0.2/grp-providers/guides/build-an-airnode/api-integration.html#ois-template), -executing those requests and returning a single value from the response. +> Used for building requests from an +> [Oracle Integration Specification (OIS)](https://docs.api3.org/airnode/v0.2/grp-providers/guides/build-an-airnode/api-integration.html#ois-template), +> executing them, parsing the responses, but also converting and encoding them for on chain purposes. -## Getting Started +## User documentation -You can install `@api3/airnode-adapter` by adding it to the `package.json` file in your project. - -```sh -# NPM -npm install --save @api3/airnode-adapter - -# Yarn -yarn add @api3/airnode-adapter -``` - -## Types - -You can find the types mentioned below in -[src/types.ts](https://github.com/api3dao/airnode/blob/master/packages/airnode-adapter/src/types.ts) - -## API - -Available functions: - -- [buildRequest](#buildRequest) - -- [executeRequest](#executeRequest) - -- [buildAndExecuteRequest](#buildAndExecuteRequest) - -- [extractValue](#extractValue) - -- [castValue](#castValue) - -- [multiplyValue](#multiplyValue) - -- [encodeValue](#encodeValue) - -- [extractAndEncodeResponse](#extractAndEncodeResponse) - -### buildRequest - -Builds a request object based on the OIS, endpointName, (user) parameters and securitySchemes. The request contains all -of the necessary details for making a request - -```ts -buildRequest(options: Options): Request -``` - -### executeRequest - -Executes a request. `config` is an optional argument that provides extra details on how the request should execute, such -as a timeout. - -[axios](https://github.com/axios/axios) is used for executing requests under the hood. You can use your own HTTP library -by building the request separately and using the result. - -```ts -executeRequest(request: Request, config?: Config): AxiosPromise -``` - -### buildAndExecuteRequest - -Builds and executes a request in a single call as a convenience function. - -```ts -buildAndExecuteRequest(options: Options, config?: Config): AxiosPromise -``` - -### extractValue - -Fetches a single value from an arbitrarily complex object or array using `path`. This works in exactly the same way as -lodash's [get](https://lodash.com/docs/4.17.15#get) function. **Values are accessed by either keys or indices, separated -by `.`** - -For example, `a.3` would fetch the value of the 4th element (15) from the `a` key of an object has the shape: -`{ a: [12, 13, 14, 15] }`. - -Some APIs return a single, primitive value like a string, number or boolean - not an object or array. This is still -considered valid JSON. When this is the case, leave the `path` argument out to return the entire response. - -```ts -extractValue(data: unknown, path?: string): any -``` - -### castValue - -**NB: See below for conversion behaviour** - -Attempts to cast and normalize an input value based on the `type` argument. An error is thrown if the value cannot be -converted. - -The following options are available for `type`: - -1. `bool` converts to boolean -2. `int256` converts to [BigNumber](https://docs.ethers.io/v5/api/utils/bignumber/) -3. `bytes32` converts to string - -```ts -castValue(value: unknown, type: ResponseType): string | boolean | BigNumber -``` - -### multiplyValue - -Multiplies the input value by the `times` parameter. Returns the input value as is if `times` is undefined. - -**NB: ALL REMAINING DECIMAL PLACES WILL BE REMOVED. i.e. THE NUMBER WILL BE FLOORED**. This is necessary because -Solidity cannot natively handle floating point or decimal numbers. - -```ts -multiplyValue(value: string, times?: string): string -``` - -### encodeValue - -Encodes the input value to `bytes32` format. Values are padded if necessary to be 32 characters long. Encoding is based -on the `type` argument. - -```ts -encodeValue(value: ValueType, type: ResponseType): string -``` - -### extractAndEncodeResponse - -Extracts, casts, multiplies (if necessary) and encodes an arbitrary input in a single call as a convenience function. - -```ts -extractAndEncodeResponse(data: unknown, parameters: ReservedParameters): any -``` - -## Conversion Behaviour - -There are a few important behaviours to need to be noted when converting to various response types. The response values -in the examples are the values after extracting from the response payload using the path. - -### `bool` Behaviour - -The following response values in the example are all considered `false`. - -**ALL other values are converted to `true`**. - -```ts -const FALSE_BOOLEAN_VALUES = [0, '0', false, 'false', undefined, null]; - -const values = FALSE_BOOLEAN_VALUES.map((v) => { - return adapter.castValue(v, 'bool'); -}); - -console.log(values); -// [false, false, false, false, false, false]; -``` - -### `int256` Behaviour - -The response values in the following example will result in an error: - -```ts -// ALL OF THESE VALUES THROW ERRORS -const ERROR_VALUES = [ - null, - undefined, - Infinity, - NaN, - '', // empty string - 'randomstring', - [], // arrays of any kind - {}, // objects of any kind -]; -``` - -There are a few special strings & boolean values that are convertible to `int256`: - -```ts -const SPECIAL_INT_VALUES = [false, 'false', true, 'true']; - -const values = SPECIAL_INT_VALUES.map((v) => adapter.castValue(v, 'int256')); -console.log(values); -// [0, 0, 1, 1]; -``` - -Number strings and numbers will attempt to be converted to [BigNumbers](https://mikemcl.github.io/bignumber.js/). The -value will also be multiplied by the `times` value if it is present. - -```ts -const VALID_INT_VALUES = ['123.456', 7777]; - -const values = VALID_INT_VALUES.map((v) => adapter.castValue(v, 'int256')); -console.log(values); -// [new BigNumber(123.456), new BigNumber(7777)]; -``` - -**NB:** It is also important to note, that when a number value is multiplied (e.g. providing a `_times` to -`extractAndEncodeResponse`), **ALL REMAINING DECIMALS WILL BE REMOVED. i.e. THE NUMBER WILL BE FLOORED**. This is -because Solidity cannot handle floating point numbers or decimals natively. - -### `bytes32` Behaviour - -If the response value is an object, an error will be thrown when attempting to convert to `bytes32`. - -```ts -const ERROR_VALUES = [{}, { a: 1 }, [], ['somestring'], [{ a: 1 }]]; - -const VALID_BYTES_VALUES = [ - null, - undefined, - '', // empty string - 'random string', - 777, // numbers - true, // booleans -]; - -const values = VALID_INT_VALUES.map((v) => adapter.castValue(v, 'bytes32')); -console.log(values); -// ["null", "undefined", "", "random string", "1", "777", "true"]; -``` - -## Example - -```ts -import * as adapter from '@api3/airnode-adapter'; - -const options = { - ois: { ... }, // a valid OIS object - endpointName: 'myUniqueEndpointName', - parameters: { from: 'BTC', to: 'USD' }, - credentials: { securityScheme: 'My Security Scheme', value: 'supersecret' }, -}; -const request = adapter.buildRequest(options); - -const response = await adapter.executeRequest(request); - -// Say the response payload that gets returned looks like: -const data = { - prices: [1000.23, 750.51, 950.97], - symbol: 'BTC_USD', -}; - -// Option 1: -const rawValue = adapter.extractValue(data, 'prices.1'); -const value = adapter.castValue(rawValue, 'int256'); -const multipledValue = adapter.multiplyValue(value, '100'); -const encodedValue = adapter.encodeValue(multipledValue, 'int256'); -console.log(encodedValue); -// '0x000000000000000000000000000000000000000000000000000000000001252b' - -// Option 2: -const parameters = { - path: 'prices.1', - times: '100', - type: 'int256', -}; -const result = adapter.extractAndEncodeResponse(data, parameters); -console.log(result); -// { -// value: 75051, -// encodedValue: '0x000000000000000000000000000000000000000000000000000000000001252b' -// } -``` +The adapter documentation describing relevant user facing parts can be found in +[adapter docs](https://docs.api3.org/airnode/v0.3/reference/packages/adapter.html). diff --git a/packages/airnode-adapter/e2e/index.ts b/packages/airnode-adapter/e2e/index.ts index 6d507e2a23..2fea68d5e6 100644 --- a/packages/airnode-adapter/e2e/index.ts +++ b/packages/airnode-adapter/e2e/index.ts @@ -47,6 +47,7 @@ const apiResponse = { floatNegative: '-112233445566.778899', }, address: '0x0765baA22F6D4A53847D63B056DC79400b9A592a', + addressWithoutPrefix: '0765baA22F6D4A53847D63B056DC79400b9A592a', boolTrue: true, strFalse: false, json: { @@ -165,6 +166,13 @@ describe('Extraction, encoding and simple on chain decoding', () => { extractAndEncodeResponse(apiResponse, { _type: 'address', _path: 'address' }).encodedValue ) ).to.equal(apiResponse.address); + + expect( + await testDecoder.decodeAddress( + extractAndEncodeResponse(apiResponse, { _type: 'address', _path: 'addressWithoutPrefix' }).encodedValue + ) + // NOTE: Notice that the response is with prefix '0x' + ).to.equal(apiResponse.address); }); it('decodes bytes encoded by the adapter package', async () => { diff --git a/packages/airnode-adapter/src/response-processing/casting.test.ts b/packages/airnode-adapter/src/response-processing/casting.test.ts index 7f86b7f42d..7544082125 100644 --- a/packages/airnode-adapter/src/response-processing/casting.test.ts +++ b/packages/airnode-adapter/src/response-processing/casting.test.ts @@ -192,6 +192,8 @@ describe('castValue', () => { it('casts valid addresses', () => { const addressStr = '0xe021f6bfbdd53c3fd0c5cfd4139b51d1f3108a74'; expect(castValue(addressStr, 'address')).toBe(addressStr); + + expect(castValue(addressStr.substr(2), 'address')).toBe('e021f6bfbdd53c3fd0c5cfd4139b51d1f3108a74'); }); });