Skip to content

Commit

Permalink
Allow gas price to be specified using _gasPrice reserved parameter (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
dcroote authored Jan 15, 2023
1 parent b7b38cc commit 620aa0e
Show file tree
Hide file tree
Showing 26 changed files with 252 additions and 33 deletions.
9 changes: 9 additions & 0 deletions .changeset/violet-walls-reflect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@api3/airnode-adapter': minor
'@api3/airnode-deployer': minor
'@api3/airnode-node': minor
'@api3/airnode-utilities': minor
'@api3/airnode-validator': minor
---

Add new \_gasPrice reserved parameter
4 changes: 2 additions & 2 deletions packages/airnode-adapter/e2e/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { expect } from 'chai';
import { ethers } from 'hardhat';
import { goSync, go, assertGoError } from '@api3/promise-utils';
import { extractAndEncodeResponse, ReservedParameters } from '../src';
import { extractAndEncodeResponse, ResponseReservedParameters } from '../src';
import type { Contract } from 'ethers';

// Chai is able to assert that "expect(BigNumber).to.equal(string)" but fails to assert
Expand Down Expand Up @@ -71,7 +71,7 @@ const apiResponse = {
},
} as const;

function extractAndEncode(reservedParams: ReservedParameters) {
function extractAndEncode(reservedParams: ResponseReservedParameters) {
const encoded = extractAndEncodeResponse(apiResponse, reservedParams);
if (Array.isArray(encoded)) expect.fail();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
unescape,
} from './extraction';
import { MAX_ENCODED_RESPONSE_SIZE } from '../constants';
import { ReservedParameters } from '../types';
import { ResponseReservedParameters } from '../types';
import { exceedsMaximumEncodedResponseSize } from '.';

describe('getRawValue', () => {
Expand Down Expand Up @@ -80,7 +80,7 @@ describe('extract and encode single value', () => {

it('extracts and encodes the value from complex objects', () => {
const data = { a: { b: [{ c: 1 }, { d: '750.51' }] } };
const parameters: ReservedParameters = { _path: 'a.b.1.d', _type: 'int256', _times: '100' };
const parameters: ResponseReservedParameters = { _path: 'a.b.1.d', _type: 'int256', _times: '100' };
const res = extractAndEncodeResponse(data, parameters);
expect(res).toEqual({
rawValue: data,
Expand All @@ -91,7 +91,7 @@ describe('extract and encode single value', () => {

it('accepts "" (empty string) for _times parameter', () => {
const data = { a: { b: [{ c: 1 }, { d: '750.51' }] } };
const parameters: ReservedParameters = { _path: 'a.b.1.d', _type: 'int256', _times: '' };
const parameters: ResponseReservedParameters = { _path: 'a.b.1.d', _type: 'int256', _times: '' };
const res = extractAndEncodeResponse(data, parameters);
expect(res).toEqual({
rawValue: data,
Expand All @@ -102,7 +102,7 @@ describe('extract and encode single value', () => {

it('empty string in path returns the whole API response', () => {
const data = 123456;
const parameters: ReservedParameters = { _path: '', _type: 'int256' };
const parameters: ResponseReservedParameters = { _path: '', _type: 'int256' };
const res = extractAndEncodeResponse(data, parameters);
expect(res).toEqual({
rawValue: 123456,
Expand All @@ -115,7 +115,7 @@ describe('extract and encode single value', () => {
describe('extract and encode multiple values', () => {
it('works for basic request', () => {
const data = { a: { b: [{ c: 1 }, { d: '750.51' }] } };
const parameters: ReservedParameters = { _path: 'a.b.1.d,a.b.0.c', _type: 'int256,bool', _times: '100,' };
const parameters: ResponseReservedParameters = { _path: 'a.b.1.d,a.b.0.c', _type: 'int256,bool', _times: '100,' };
const res = extractAndEncodeResponse(data, parameters);
expect(res).toEqual({
rawValue: data,
Expand All @@ -127,7 +127,7 @@ describe('extract and encode multiple values', () => {

it('works for more complex request', () => {
const data = [12.3, 45.6];
const parameters: ReservedParameters = { _path: ',0', _type: 'int256[],int256', _times: '100,1000' };
const parameters: ResponseReservedParameters = { _path: ',0', _type: 'int256[],int256', _times: '100,1000' };
const res = extractAndEncodeResponse(data, parameters);
expect(res).toEqual({
rawValue: data,
Expand Down
22 changes: 15 additions & 7 deletions packages/airnode-adapter/src/response-processing/extraction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ import {
MULTIPLE_PARAMETERS_DELIMETER,
PATH_DELIMETER,
} from '../constants';
import { ReservedParameters, ValueType, ExtractedAndEncodedResponse, ReservedParametersDelimeter } from '../types';
import {
ResponseReservedParameters,
ValueType,
ExtractedAndEncodedResponse,
ReservedParametersDelimeter,
} from '../types';

export function unescape(value: string, delimeter: ReservedParametersDelimeter) {
const escapedEscapeCharacter = ESCAPE_CHARACTER.repeat(2);
Expand Down Expand Up @@ -65,8 +70,8 @@ export function extractValue(data: unknown, path?: string) {
return rawValue;
}

export function splitReservedParameters(parameters: ReservedParameters): ReservedParameters[] {
const splitByDelimeter = (name: keyof ReservedParameters) => {
export function splitReservedParameters(parameters: ResponseReservedParameters): ResponseReservedParameters[] {
const splitByDelimeter = (name: keyof ResponseReservedParameters) => {
return {
name,
splitResult: parameters[name] ? escapeAwareSplit(parameters[name]!, MULTIPLE_PARAMETERS_DELIMETER) : undefined,
Expand All @@ -88,18 +93,18 @@ export function splitReservedParameters(parameters: ReservedParameters): Reserve
}
});

const reservedParameters: ReservedParameters[] = range(typesLength).map((i) =>
const reservedParameters: ResponseReservedParameters[] = range(typesLength).map((i) =>
splitParams.reduce((acc, param) => {
if (!param.splitResult) return acc;

return { ...acc, [param.name]: param.splitResult[i] };
}, {} as any as ReservedParameters)
}, {} as any as ResponseReservedParameters)
);

return reservedParameters;
}

function extractSingleResponse(data: unknown, parameters: ReservedParameters) {
function extractSingleResponse(data: unknown, parameters: ResponseReservedParameters) {
const parsedArrayType = parseArrayType(parameters._type);
const type = parsedArrayType?.baseType ?? parameters._type;

Expand Down Expand Up @@ -133,7 +138,10 @@ export function exceedsMaximumEncodedResponseSize(encodedValue: string) {
return encodedBytesLength > MAX_ENCODED_RESPONSE_SIZE;
}

export function extractAndEncodeResponse(data: unknown, parameters: ReservedParameters): ExtractedAndEncodedResponse {
export function extractAndEncodeResponse(
data: unknown,
parameters: ResponseReservedParameters
): ExtractedAndEncodedResponse {
const reservedParameters = splitReservedParameters(parameters);
if (reservedParameters.length > 1) {
const extractedValues = reservedParameters.map((params) => extractSingleResponse(data, params));
Expand Down
3 changes: 2 additions & 1 deletion packages/airnode-adapter/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ export type BaseResponseType = typeof baseResponseTypes[number];
// Use might pass a complex type (e.g. int256[3][]) which we cannot type
export type ResponseType = string;

export interface ReservedParameters {
// Reserved parameters specific for response processing i.e. excludes _gasPrice
export interface ResponseReservedParameters {
_path?: string;
_times?: string;
_type: ResponseType;
Expand Down
1 change: 1 addition & 0 deletions packages/airnode-adapter/test/fixtures/ois.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export function buildOIS(overrides?: Partial<OIS>): OIS {
name: '_times',
default: '100000',
},
{ name: '_gasPrice' },
],
parameters: [
{
Expand Down
3 changes: 3 additions & 0 deletions packages/airnode-deployer/test/fixtures/config.aws.valid.json
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,9 @@
{
"name": "_times",
"fixed": "1000000"
},
{
"name": "_gasPrice"
}
],
"parameters": [
Expand Down
3 changes: 3 additions & 0 deletions packages/airnode-deployer/test/fixtures/config.gcp.valid.json
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,9 @@
{
"name": "_times",
"fixed": "1000000"
},
{
"name": "_gasPrice"
}
],
"parameters": [
Expand Down
3 changes: 2 additions & 1 deletion packages/airnode-node/src/adapters/http/parameters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ describe('getReservedParameters', () => {
{ name: '_type', fixed: 'int256' },
{ name: '_path', default: 'prices.0.latest' },
{ name: '_times', default: '1000000' },
{ name: '_gasPrice' },
],
};
});
Expand All @@ -67,6 +68,6 @@ describe('getReservedParameters', () => {
_type: 'bytes32',
_path: 'updated.path',
});
expect(res).toEqual({ _type: 'int256', _path: 'updated.path', _times: '1000000' });
expect(res).toEqual({ _type: 'int256', _path: 'updated.path', _times: '1000000', _gasPrice: undefined });
});
});
3 changes: 2 additions & 1 deletion packages/airnode-node/src/adapters/http/parameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export function getReservedParameters(endpoint: Endpoint, requestParameters: Api
const _path = getReservedParameterValue('_path', endpoint, requestParameters);
const _times = getReservedParameterValue('_times', endpoint, requestParameters);
const _type = getReservedParameterValue('_type', endpoint, requestParameters);
const _gasPrice = getReservedParameterValue('_gasPrice', endpoint, requestParameters);

return { _type, _path, _times };
return { _type, _path, _times, _gasPrice };
}
6 changes: 5 additions & 1 deletion packages/airnode-node/src/api/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ describe('callApi', () => {
it('calls the adapter with the given parameters', async () => {
const spy = jest.spyOn(adapter, 'buildAndExecuteRequest') as any;
spy.mockResolvedValueOnce({ data: { price: 1000 } });
const parameters = { _type: 'int256', _path: 'price', from: 'ETH' };
const requestedGasPrice = '100000000';
const parameters = { _type: 'int256', _path: 'price', from: 'ETH', _gasPrice: requestedGasPrice };

const [logs, res] = await callApi({
type: 'regular',
Expand All @@ -29,6 +30,9 @@ describe('callApi', () => {
signature:
'0xe92f5ee40ddb5aa42cab65fcdc025008b2bc026af80a7c93a9aac4e474f8a88f4f2bd861b9cf9a2b050bf0fd13e9714c4575cebbea658d7501e98c0963a5a38b1c',
},
reservedParameterOverrides: {
gasPrice: requestedGasPrice,
},
});
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith(
Expand Down
17 changes: 11 additions & 6 deletions packages/airnode-node/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ export async function processSuccessfulApiCall(
const { endpointName, oisTitle, parameters } = aggregatedApiCall;
const ois = config.ois.find((o) => o.title === oisTitle)!;
const endpoint = ois.endpoints.find((e) => e.name === endpointName)!;
const reservedParameters = getReservedParameters(endpoint, parameters);
const { _type, _path, _times, _gasPrice } = getReservedParameters(endpoint, parameters);

const goPostProcessApiSpecifications = await go(() => postProcessApiSpecifications(rawResponse.data, endpoint));
if (!goPostProcessApiSpecifications.success) {
Expand All @@ -235,10 +235,11 @@ export async function processSuccessfulApiCall(
}

const goExtractAndEncodeResponse = goSync(() =>
adapter.extractAndEncodeResponse(
goPostProcessApiSpecifications.data,
reservedParameters as adapter.ReservedParameters
)
adapter.extractAndEncodeResponse(goPostProcessApiSpecifications.data, {
_type,
_path,
_times,
} as adapter.ResponseReservedParameters)
);
if (!goExtractAndEncodeResponse.success) {
const log = logger.pend('ERROR', goExtractAndEncodeResponse.error.message);
Expand All @@ -259,7 +260,11 @@ export async function processSuccessfulApiCall(

return [
[],
{ success: true, data: { encodedValue: response.encodedValue, signature: goSignWithRequestId.data } },
{
success: true,
data: { encodedValue: response.encodedValue, signature: goSignWithRequestId.data },
reservedParameterOverrides: _gasPrice ? { gasPrice: _gasPrice } : undefined,
},
];
}
case 'http-signed-data-gateway': {
Expand Down
10 changes: 9 additions & 1 deletion packages/airnode-node/src/coordinator/calls/disaggregation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,15 @@ function updateApiCallResponses(

return {
...acc,
requests: [...acc.requests, { ...apiCall, data: aggregatedApiCall.data, success: true }],
requests: [
...acc.requests,
{
...apiCall,
data: aggregatedApiCall.data,
success: true,
reservedParameterOverrides: aggregatedApiCall.reservedParameterOverrides,
},
],
};
},
{ logs: [], requests: [] } as UpdatedRequests<ApiCallWithResponse>
Expand Down
36 changes: 32 additions & 4 deletions packages/airnode-node/src/evm/fulfillments/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { logger } from '@api3/airnode-utilities';
import { LegacyTypeLiteral, logger } from '@api3/airnode-utilities';
import { BigNumber } from 'ethers';
import { submitApiCall } from './api-calls';
import { submitWithdrawal } from './withdrawals';
import * as wallet from '../wallet';
Expand All @@ -22,9 +23,32 @@ interface OrderedRequest<T> {
makeRequest: () => Promise<Request<T> | Error>;
}

function getTransactionOptions(state: ProviderState<EVMProviderSponsorState>) {
function getTransactionOptions<T>(
state: ProviderState<EVMProviderSponsorState>,
request: Request<T>,
type: RequestType
) {
let gasTarget = state.gasTarget!;

// Overwrite gasTarget with a legacy type if an override for gas price has been set
// via the presence of a _gasPrice reserved parameter
if (type === RequestType.ApiCall) {
// Cast request based on confirmed type to access property
const apiCall = request as any as Request<ApiCallWithResponse>;

if (apiCall.reservedParameterOverrides && apiCall.reservedParameterOverrides.gasPrice) {
const gasPrice = apiCall.reservedParameterOverrides.gasPrice;
gasTarget = {
type: 0 as LegacyTypeLiteral,
gasLimit: state.gasTarget!.gasLimit,
gasPrice: BigNumber.from(gasPrice),
};
logger.info(`Gas price overridden with reserved parameter value ${gasPrice} wei for Request ID:${apiCall.id}`);
}
}

return {
gasTarget: state.gasTarget!,
gasTarget: gasTarget,
masterHDNode: state.masterHDNode,
provider: state.provider,
withdrawalRemainder: state.settings.chainOptions.withdrawalRemainder,
Expand All @@ -40,7 +64,11 @@ function prepareRequestSubmissions<T>(
): OrderedRequest<T>[] {
return requests.map((request) => {
const makeRequest = async () => {
const [logs, err, submittedRequest] = await submitFunction(contract, request, getTransactionOptions(state));
const [logs, err, submittedRequest] = await submitFunction(
contract,
request,
getTransactionOptions(state, request, type)
);
logger.logPending(logs);

if (err) return err;
Expand Down
Loading

0 comments on commit 620aa0e

Please sign in to comment.