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 handling of _minConfirmations reserved parameter #1615

Merged
merged 17 commits into from
Feb 2, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
8 changes: 8 additions & 0 deletions .changeset/swift-kangaroos-cross.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@api3/airnode-adapter': minor
'@api3/airnode-deployer': minor
'@api3/airnode-node': minor
'@api3/airnode-validator': minor
---

Add new \_minConfirmations reserved parameter to allow minConfirmations to be specified by a requester
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;

// Reserved parameters specific for response processing i.e. excludes _gasPrice
// Reserved parameters specific for response processing
// i.e. excludes _gasPrice and _minConfirmations
export interface ResponseReservedParameters {
_path?: string;
_times?: string;
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 @@ -78,6 +78,7 @@ export function buildOIS(overrides?: Partial<OIS>): OIS {
default: '100000',
},
{ name: '_gasPrice' },
{ name: '_minConfirmations' },
],
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 @@ -206,6 +206,9 @@
},
{
"name": "_gasPrice"
},
{
"name": "_minConfirmations"
}
],
"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 @@ -206,6 +206,9 @@
},
{
"name": "_gasPrice"
},
{
"name": "_minConfirmations"
}
],
"parameters": [
Expand Down
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 @@ -29,6 +29,7 @@ export function getReservedParameters(endpoint: Endpoint, requestParameters: Api
const _times = getReservedParameterValue('_times', endpoint, requestParameters);
const _type = getReservedParameterValue('_type', endpoint, requestParameters);
const _gasPrice = getReservedParameterValue('_gasPrice', endpoint, requestParameters);
const _minConfirmations = getReservedParameterValue('_minConfirmations', endpoint, requestParameters);

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

const [logs, res] = await callApi({
type: 'regular',
Expand All @@ -32,6 +39,7 @@ describe('callApi', () => {
},
reservedParameterOverrides: {
gasPrice: requestedGasPrice,
minConfirmations: requestedMinConfirmations,
},
});
expect(spy).toHaveBeenCalledTimes(1);
Expand Down
6 changes: 4 additions & 2 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 { _type, _path, _times, _gasPrice } = getReservedParameters(endpoint, parameters);
const { _type, _path, _times, _gasPrice, _minConfirmations } = getReservedParameters(endpoint, parameters);

const goPostProcessApiSpecifications = await go(() => postProcessApiSpecifications(rawResponse.data, endpoint));
if (!goPostProcessApiSpecifications.success) {
Expand Down Expand Up @@ -263,7 +263,9 @@ export async function processSuccessfulApiCall(
{
success: true,
data: { encodedValue: response.encodedValue, signature: goSignWithRequestId.data },
reservedParameterOverrides: _gasPrice ? { gasPrice: _gasPrice } : undefined,
reservedParameterOverrides:
// TODO is this exactly what we want? i.e. undefined behavior
_gasPrice || _minConfirmations ? { gasPrice: _gasPrice, minConfirmations: _minConfirmations } : undefined,
},
];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export async function fetchPendingRequests(state: ProviderState<EVMProviderState
blockHistoryLimit: state.settings.blockHistoryLimit,
currentBlock: state.currentBlock!,
minConfirmations: state.settings.minConfirmations,
mayOverrideMinConfirmations: state.settings.mayOverrideMinConfirmations,
provider: state.provider,
chainId,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ async function fetchTransactionCounts(currentState: ProviderState<EVMProviderSta
masterHDNode: currentState.masterHDNode,
provider: currentState.provider,
minConfirmations: currentState.settings.minConfirmations,
mayOverrideMinConfirmations: currentState.settings.mayOverrideMinConfirmations,
};
// This should not throw
const [logs, res] = await transactionCounts.fetchBySponsor(sponsors, fetchOptions);
Expand Down
5 changes: 5 additions & 0 deletions packages/airnode-node/src/evm/requests/event-logs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ describe('EVM event logs - fetch', () => {
blockHistoryLimit: 300,
currentBlock: 10716084,
minConfirmations: 1,
mayOverrideMinConfirmations: false,
provider: new ethers.providers.JsonRpcProvider(),
chainId: '31137',
};
Expand Down Expand Up @@ -109,6 +110,7 @@ describe('EVM event logs - fetch', () => {
blockHistoryLimit: 30,
currentBlock: 10716084,
minConfirmations: 0,
mayOverrideMinConfirmations: false,
provider: new ethers.providers.JsonRpcProvider(),
chainId: '31137',
};
Expand Down Expand Up @@ -139,6 +141,7 @@ describe('EVM event logs - fetch', () => {
blockHistoryLimit: 300,
currentBlock: 10716084,
minConfirmations: 0,
mayOverrideMinConfirmations: false,
provider: new ethers.providers.JsonRpcProvider(),
chainId: '31137',
};
Expand All @@ -154,6 +157,7 @@ describe('EVM event logs - fetch', () => {
blockHistoryLimit: 99999999,
currentBlock: 10716084,
minConfirmations: 10,
mayOverrideMinConfirmations: false,
provider: new ethers.providers.JsonRpcProvider(),
chainId: '31137',
};
Expand All @@ -177,6 +181,7 @@ describe('EVM event logs - fetch', () => {
blockHistoryLimit: 30,
currentBlock: 10716084,
minConfirmations: 99999999,
mayOverrideMinConfirmations: false,
provider: new ethers.providers.JsonRpcProvider(),
chainId: '31137',
};
Expand Down
6 changes: 5 additions & 1 deletion packages/airnode-node/src/evm/requests/event-logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface FetchOptions {
readonly blockHistoryLimit: number;
readonly currentBlock: number;
readonly minConfirmations: number;
readonly mayOverrideMinConfirmations: boolean;
readonly provider: ethers.providers.JsonRpcProvider;
readonly chainId: string;
}
Expand All @@ -37,7 +38,10 @@ export async function fetch(options: FetchOptions): Promise<EVMEventLog[]> {
// Protect against a potential negative fromBlock value
const fromBlock = Math.max(0, options.currentBlock - options.blockHistoryLimit);
// toBlock should always be >= fromBlock
const toBlock = Math.max(fromBlock, options.currentBlock - options.minConfirmations);
const toBlock = Math.max(
fromBlock,
options.currentBlock - (options.mayOverrideMinConfirmations ? 0 : options.minConfirmations)
);

const filter: ethers.providers.Filter = {
fromBlock,
Expand Down
5 changes: 5 additions & 0 deletions packages/airnode-node/src/evm/transaction-counts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe('fetchBySponsor', () => {
masterHDNode: wallet.getMasterHDNode(config),
provider: new ethers.providers.JsonRpcProvider(),
minConfirmations: 0,
mayOverrideMinConfirmations: false,
};
const addresses = ['0x69e2B095fbAc6C3f9E528Ef21882b86BF1595181', '0x69e2B095fbAc6C3f9E528Ef21882b86BF1595181'];
const [logs, res] = await transactions.fetchBySponsor(addresses, options);
Expand All @@ -41,6 +42,7 @@ describe('fetchBySponsor', () => {
masterHDNode: wallet.getMasterHDNode(config),
provider: new ethers.providers.JsonRpcProvider(),
minConfirmations: 2,
mayOverrideMinConfirmations: false,
};
const addresses = ['0x69e2B095fbAc6C3f9E528Ef21882b86BF1595181', '0x69e2B095fbAc6C3f9E528Ef21882b86BF1595181'];
const [logs, res] = await transactions.fetchBySponsor(addresses, options);
Expand All @@ -61,6 +63,7 @@ describe('fetchBySponsor', () => {
masterHDNode: wallet.getMasterHDNode(config),
provider: new ethers.providers.JsonRpcProvider(),
minConfirmations: 0,
mayOverrideMinConfirmations: false,
};
const addresses = ['0x69e2B095fbAc6C3f9E528Ef21882b86BF1595181', '0x99bd3a5A045066F1CEf37A0A952DFa87Af9D898E'];
const [logs, res] = await transactions.fetchBySponsor(addresses, options);
Expand All @@ -84,6 +87,7 @@ describe('fetchBySponsor', () => {
masterHDNode: wallet.getMasterHDNode(config),
provider: new ethers.providers.JsonRpcProvider(),
minConfirmations: 0,
mayOverrideMinConfirmations: false,
};
const addresses = ['0x69e2B095fbAc6C3f9E528Ef21882b86BF1595181', '0x69e2B095fbAc6C3f9E528Ef21882b86BF1595181'];
const [logs, res] = await transactions.fetchBySponsor(addresses, options);
Expand All @@ -104,6 +108,7 @@ describe('fetchBySponsor', () => {
masterHDNode: wallet.getMasterHDNode(config),
provider: new ethers.providers.JsonRpcProvider(),
minConfirmations: 0,
mayOverrideMinConfirmations: false,
};
const addresses = ['0x69e2B095fbAc6C3f9E528Ef21882b86BF1595181', '0x69e2B095fbAc6C3f9E528Ef21882b86BF1595181'];
const [logs, res] = await transactions.fetchBySponsor(addresses, options);
Expand Down
6 changes: 5 additions & 1 deletion packages/airnode-node/src/evm/transaction-counts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface FetchOptions {
readonly masterHDNode: ethers.utils.HDNode;
readonly provider: ethers.providers.JsonRpcProvider;
readonly minConfirmations: number;
readonly mayOverrideMinConfirmations: boolean;
}

async function getWalletTransactionCount(
Expand All @@ -24,7 +25,10 @@ async function getWalletTransactionCount(
): Promise<LogsData<TransactionCountBySponsorAddress | null>> {
const address = wallet.deriveSponsorWallet(options.masterHDNode, sponsorAddress).address;
const operation = () =>
options.provider.getTransactionCount(address, options.currentBlock - options.minConfirmations);
options.provider.getTransactionCount(
address,
options.currentBlock - (options.mayOverrideMinConfirmations ? 0 : options.minConfirmations)
);
const goCount = await go(operation, { retries: 1, attemptTimeoutMs: BLOCKCHAIN_CALL_ATTEMPT_TIMEOUT });
if (!goCount.success) {
const log = logger.pend('ERROR', `Unable to fetch transaction count for wallet:${address}`, goCount.error);
Expand Down
2 changes: 2 additions & 0 deletions packages/airnode-node/src/providers/actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ describe('initialize', () => {
logFormat: 'plain',
logLevel: 'DEBUG',
minConfirmations: 0,
mayOverrideMinConfirmations: true,
name: 'Pocket Ethereum Mainnet',
cloudProvider: {
type: 'local',
Expand Down Expand Up @@ -204,6 +205,7 @@ describe('initialize', () => {
logFormat: 'plain',
logLevel: 'DEBUG',
minConfirmations: 0,
mayOverrideMinConfirmations: true,
name: 'Infura Sepolia',
cloudProvider: {
type: 'local',
Expand Down
19 changes: 19 additions & 0 deletions packages/airnode-node/src/providers/state.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ethers } from 'ethers';
import * as state from './state';
import * as fixtures from '../../test/fixtures';
import { BLOCK_MIN_CONFIRMATIONS } from '../constants';
import { EVMProviderState, ProviderState } from '../types';
import { ChainConfig } from '../config';

Expand Down Expand Up @@ -79,6 +80,7 @@ describe('create', () => {
logFormat: 'plain',
logLevel: 'DEBUG',
minConfirmations: 0,
mayOverrideMinConfirmations: true,
name: 'Ganache test',
cloudProvider: {
type: 'local',
Expand Down Expand Up @@ -174,6 +176,7 @@ describe('create', () => {
logFormat: 'plain',
logLevel: 'DEBUG',
minConfirmations: 3,
mayOverrideMinConfirmations: true,
name: 'Ganache test',
cloudProvider: {
type: 'local',
Expand Down Expand Up @@ -339,3 +342,19 @@ describe('splitStatesBySponsorAddress', () => {
]);
});
});

it('checks for the presence of a _minConfirmations reserved parameter', () => {
const newState = fixtures.buildEVMProviderState();

// buildEVMProviderState() doesn't set minConfirmations in chainConfig
expect(newState.settings.minConfirmations).toEqual(BLOCK_MIN_CONFIRMATIONS);

// _minConfirmations reserved parameter is set in buildOIS()
expect(newState.settings.mayOverrideMinConfirmations).toBe(true);

// Remove _minConfirmations reserved parameter
newState.config!.ois[0].endpoints[0].reservedParameters =
newState.config!.ois[0].endpoints[0].reservedParameters.filter((param) => param.name !== '_minConfirmations');

expect(state.checkForMinConfirmationsReservedParam(newState.config!)).toBe(false);
});
13 changes: 13 additions & 0 deletions packages/airnode-node/src/providers/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ export function buildEVMState(
logFormat: config.nodeSettings.logFormat,
logLevel: config.nodeSettings.logLevel,
minConfirmations: chain.minConfirmations || BLOCK_MIN_CONFIRMATIONS,
// If the _minConfirmations reserved parameter is set for one or more endpoints,
// a request may override minConfirmations, but we don't know if it will or the value
// until we actually fetch the requests, perform API calls, and extract reserved
// parameter overrides in processSuccessfulApiCall
dcroote marked this conversation as resolved.
Show resolved Hide resolved
mayOverrideMinConfirmations: checkForMinConfirmationsReservedParam(config),
dcroote marked this conversation as resolved.
Show resolved Hide resolved
name: chainProviderName,
cloudProvider: config.nodeSettings.cloudProvider,
stage: config.nodeSettings.stage,
Expand All @@ -58,6 +63,14 @@ export function buildEVMState(
};
}

// Checks all OIS endpoints for the presence of a _minConfirmations reserved parameter
// which means that a requester may use a parameter to override the value.
export function checkForMinConfirmationsReservedParam(config: Config): boolean {
return config.ois.some((ois) =>
ois.endpoints.some((endpoint) => endpoint.reservedParameters.some((param) => param.name === '_minConfirmations'))
);
}

export function update<T>(state: ProviderState<T>, newState: Partial<ProviderState<T>>): ProviderState<T> {
return { ...state, ...newState };
}
Expand Down
3 changes: 3 additions & 0 deletions packages/airnode-node/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ export interface ProviderSettings extends CoordinatorSettings {
readonly chainType: ChainType;
readonly chainOptions: ChainOptions;
readonly minConfirmations: number;
readonly mayOverrideMinConfirmations: boolean;
readonly name: string;
readonly url: string;
readonly xpub: string;
Expand Down Expand Up @@ -231,6 +232,7 @@ export interface RegularApiCallSuccessResponse {
data: { encodedValue: string; signature: string };
reservedParameterOverrides?: {
gasPrice?: string;
minConfirmations?: string;
};
}

Expand All @@ -249,6 +251,7 @@ export interface ApiCallErrorResponse {
errorMessage: string;
reservedParameterOverrides?: {
gasPrice?: string;
minConfirmations?: string;
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,5 +84,5 @@ it('submits fulfillment with the gas price overridden', async () => {
tx.gasPrice?.toString() === requestedGasPrice;
});

expect(fulfillmentLog).not.toBeUndefined();
expect(fulfillmentLog).toBeDefined();
});
3 changes: 3 additions & 0 deletions packages/airnode-node/test/fixtures/config/config.valid.json
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,9 @@
},
{
"name": "_gasPrice"
},
{
"name": "_minConfirmations"
}
],
"parameters": [
Expand Down
1 change: 1 addition & 0 deletions packages/airnode-node/test/fixtures/config/ois.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export function buildOIS(ois?: Partial<OIS>): OIS {
default: '100000',
},
{ name: '_gasPrice' },
{ name: '_minConfirmations' },
],
parameters: [
{
Expand Down
4 changes: 3 additions & 1 deletion packages/airnode-validator/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,9 @@ export const chainConfigSchema = z
blockHistoryLimit: z.number().int().optional(), // Defaults to BLOCK_COUNT_HISTORY_LIMIT defined in airnode-node
contracts: chainContractsSchema,
id: z.string(),
minConfirmations: z.number().int().optional(), // Defaults to BLOCK_MIN_CONFIRMATIONS defined in airnode-node
// Defaults to BLOCK_MIN_CONFIRMATIONS defined in airnode-node but may be overridden
// by a requester if the _minConfirmations reserved parameter is configured
minConfirmations: z.number().int().optional(),
type: chainTypeSchema,
options: chainOptionsSchema,
providers: providersSchema,
Expand Down
3 changes: 3 additions & 0 deletions packages/airnode-validator/test/fixtures/config.valid.json
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,9 @@
},
{
"name": "_gasPrice"
},
{
"name": "_minConfirmations"
}
],
"parameters": [
Expand Down
Loading