Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new values to "_types" parameter #620

Merged
merged 26 commits into from
Nov 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3cde5f0
Minor adapter refactor
Siegrift Oct 12, 2021
0b5d907
Remove confusing method name suffix
Siegrift Oct 12, 2021
95aa6c3
Add address, bytes and string values in _type
Siegrift Oct 12, 2021
36be161
Implement better casting and tests
Siegrift Oct 13, 2021
100f9bd
Add encoding tests
Siegrift Oct 13, 2021
2febc5f
Add contract testing configuration
Siegrift Oct 13, 2021
fb6cd09
Implement contract decoding tests
Siegrift Nov 2, 2021
4e69408
Fix rebase issues
Siegrift Nov 6, 2021
1dc133b
Finish array support
Siegrift Nov 3, 2021
e5c476c
Fix conflicting global types declarations
Siegrift Nov 9, 2021
a38dbba
Fix solidity linter
Siegrift Nov 9, 2021
8328355
Refactor casting tests
Siegrift Nov 9, 2021
8756437
Remove unnecessary encoding tests
Siegrift Nov 9, 2021
0549526
Do not check fixed size array lenghts while casting
Siegrift Nov 9, 2021
fd3e863
Refactor e2e tests
Siegrift Nov 9, 2021
71afa74
Using _times with (nested) array types multiples all elements
Siegrift Nov 9, 2021
80d058d
Implement more e2e tests
Siegrift Nov 9, 2021
20fac8a
Self review
Siegrift Nov 9, 2021
0112fda
Fix build
Siegrift Nov 9, 2021
606b6e0
Add changeset
Siegrift Nov 9, 2021
1747daa
Simplify implementation, move bytes32 casting logic outside of encoding
Siegrift Nov 10, 2021
7b2a545
Apply PR suggestions
Siegrift Nov 11, 2021
03fa045
Bytes32 now accept hex string, refactor error handling
Siegrift Nov 11, 2021
2ee8d3e
Implement string32 type
Siegrift Nov 11, 2021
3ad8bb7
Update changeset
Siegrift Nov 11, 2021
f54124c
Migrate README to docs, implement a few more tests
Siegrift Nov 12, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/happy-goats-juggle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@api3/airnode-adapter': minor
'@api3/airnode-node': minor
---

Implement new values for "\_type" reserved parameter (address, bytes, string, string32 and arrays)
6 changes: 6 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,9 @@

packages/airnode-validator/test/fixtures
packages/airnode-protocol/src/contracts

# https://github.com/protofire/solhint/issues/317
#
# /* solhint-disable */ comment doesn't work either because this is a parser issue
# Prettier uses this parser for formatting
packages/airnode-adapter/contracts/TestDecoder.sol
5 changes: 5 additions & 0 deletions .solhintignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
**/node_modules/**
**/artifacts/**

# https://github.com/protofire/solhint/issues/317
#
# /* solhint-disable */ comment doesn't work either because this is a parser issue
./packages/airnode-adapter/contracts/TestDecoder.sol
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
"prettier": "^2.3.2",
"prettier-plugin-solidity": "^1.0.0-beta.18",
"rimraf": "^3.0.2",
"solhint": "^3.2.0",
"solhint": "^3.3.6",
"ts-jest": "^26.3.0",
"ts-node": "^10.0.0",
"typescript": "^4.2.4"
Expand Down
55 changes: 55 additions & 0 deletions packages/airnode-adapter/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,58 @@
module.exports = {
extends: ['../../.eslintrc.fp.js', '../../.eslintrc.js'],
overrides: [
{
env: {
browser: false,
jest: false,
mocha: true,
node: true,
},
files: ['e2e/**/*.ts'],
rules: {
'jest/consistent-test-it': 'off',
'jest/expect-expect': 'off',
'jest/lowercase-name': 'off',
'jest/max-nested-describe': 'off',
'jest/no-alias-methods': 'off',
'jest/no-commented-out-tests': 'off',
'jest/no-conditional-expect': 'off',
'jest/no-deprecated-functions': 'off',
'jest/no-disabled-tests': 'off',
'jest/no-done-callback': 'off',
'jest/no-duplicate-hooks': 'off',
'jest/no-export': 'off',
'jest/no-focused-tests': 'off',
'jest/no-hooks': 'off',
'jest/no-identical-title': 'off',
'jest/no-if': 'off',
'jest/no-interpolation-in-snapshots': 'off',
'jest/no-jasmine-globals': 'off',
'jest/no-jest-import': 'off',
'jest/no-large-snapshots': 'off',
'jest/no-mocks-import': 'off',
'jest/no-restricted-matchers': 'off',
'jest/no-standalone-expect': 'off',
'jest/no-test-prefixes': 'off',
'jest/no-test-return-statement': 'off',
'jest/no-try-expect': 'off',
'jest/prefer-called-with': 'off',
'jest/prefer-expect-assertions': 'off',
'jest/prefer-hooks-on-top': 'off',
'jest/prefer-spy-on': 'off',
'jest/prefer-strict-equal': 'off',
'jest/prefer-to-be-null': 'off',
'jest/prefer-to-be-undefined': 'off',
'jest/prefer-to-contain': 'off',
'jest/prefer-to-have-length': 'off',
'jest/prefer-todo': 'off',
'jest/require-to-throw-message': 'off',
'jest/require-top-level-describe': 'off',
'jest/valid-describe': 'off',
'jest/valid-expect': 'off',
'jest/valid-expect-in-promise': 'off',
'jest/valid-title': 'off',
},
},
],
};
4 changes: 4 additions & 0 deletions packages/airnode-adapter/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@

# Test files
coverage/

# Hardhat files
cache
artifacts
263 changes: 6 additions & 257 deletions packages/airnode-adapter/README.md
Original file line number Diff line number Diff line change
@@ -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<any>
```

### buildAndExecuteRequest

Builds and executes a request in a single call as a convenience function.

```ts
buildAndExecuteRequest(options: Options, config?: Config): AxiosPromise<any>
```

### 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).
Loading