From 8181844c3c87d0797c199bf93e651f96a86d981e Mon Sep 17 00:00:00 2001 From: Denis Davidyuk Date: Tue, 13 Feb 2024 17:53:36 +0700 Subject: [PATCH 1/2] refactor(node): split into NodeBase and NodeDirect --- src/AeSdkAepp.ts | 2 +- src/AeSdkBase.ts | 2 +- src/AeSdkMethods.ts | 2 +- src/account/Base.ts | 2 +- src/account/LedgerFactory.ts | 2 +- src/aens.ts | 2 +- src/aepp-wallet-communication/rpc/types.ts | 2 +- src/chain.ts | 3 +- src/contract/Contract.ts | 3 +- src/contract/delegation-signature.ts | 2 +- src/contract/ga.ts | 2 +- src/index-browser.ts | 2 +- src/{Node.ts => node/Base.ts} | 104 +-------------------- src/node/Direct.ts | 101 ++++++++++++++++++++ src/oracle.ts | 2 +- src/tx/builder/field-types/abi-version.ts | 2 +- src/tx/builder/field-types/ct-version.ts | 2 +- src/tx/builder/field-types/nonce.ts | 2 +- src/tx/builder/field-types/query-fee.ts | 2 +- src/tx/builder/field-types/ttl.ts | 2 +- src/tx/execution-cost.ts | 2 +- src/tx/validator.ts | 3 +- 22 files changed, 128 insertions(+), 120 deletions(-) rename src/{Node.ts => node/Base.ts} (51%) create mode 100644 src/node/Direct.ts diff --git a/src/AeSdkAepp.ts b/src/AeSdkAepp.ts index 483ace100c..214e795859 100644 --- a/src/AeSdkAepp.ts +++ b/src/AeSdkAepp.ts @@ -15,7 +15,7 @@ import { UnAuthorizedAccountError, RpcConnectionError, } from './utils/errors'; -import Node from './Node'; +import Node from './node/Direct'; import BrowserConnection from './aepp-wallet-communication/connection/Browser'; /** diff --git a/src/AeSdkBase.ts b/src/AeSdkBase.ts index 869c767e1e..af28715cf3 100644 --- a/src/AeSdkBase.ts +++ b/src/AeSdkBase.ts @@ -1,4 +1,4 @@ -import Node from './Node'; +import Node from './node/Direct'; import AccountBase from './account/Base'; import { CompilerError, DuplicateNodeError, NodeNotFoundError, NotImplementedError, TypeError, diff --git a/src/AeSdkMethods.ts b/src/AeSdkMethods.ts index 64c5fdce64..c5ccd8f44f 100644 --- a/src/AeSdkMethods.ts +++ b/src/AeSdkMethods.ts @@ -7,7 +7,7 @@ import createDelegationSignature from './contract/delegation-signature'; import * as contractGaMethods from './contract/ga'; import { buildTxAsync } from './tx/builder'; import { mapObject, UnionToIntersection, wrapWithProxy } from './utils/other'; -import Node from './Node'; +import Node from './node/Direct'; import { TxParamsAsync } from './tx/builder/schema.generated'; import AccountBase from './account/Base'; import { Encoded } from './utils/encoder'; diff --git a/src/account/Base.ts b/src/account/Base.ts index 597475d548..8b6a48654c 100644 --- a/src/account/Base.ts +++ b/src/account/Base.ts @@ -1,5 +1,5 @@ import { Encoded } from '../utils/encoder'; -import Node from '../Node'; +import Node from '../node/Direct'; import CompilerBase from '../contract/compiler/Base'; import { AensName, ConsensusProtocolVersion, Int } from '../tx/builder/constants'; import { AciValue, Domain } from '../utils/typed-data'; diff --git a/src/account/LedgerFactory.ts b/src/account/LedgerFactory.ts index 568b72263b..9132e43fea 100644 --- a/src/account/LedgerFactory.ts +++ b/src/account/LedgerFactory.ts @@ -3,7 +3,7 @@ import AccountLedger, { CLA, GET_ADDRESS, GET_APP_CONFIGURATION } from './Ledger import { UnsupportedVersionError } from '../utils/errors'; import { Encoded } from '../utils/encoder'; import semverSatisfies from '../utils/semver-satisfies'; -import Node from '../Node'; +import Node from '../node/Direct'; /** * A factory class that generates instances of AccountLedger based on provided transport. diff --git a/src/aens.ts b/src/aens.ts index b1a2495f89..c903ecfeca 100644 --- a/src/aens.ts +++ b/src/aens.ts @@ -14,7 +14,7 @@ import { Encoded, Encoding } from './utils/encoder'; import { UnsupportedProtocolError } from './utils/errors'; import { sendTransaction, SendTransactionOptions, getName } from './chain'; import { buildTxAsync, BuildTxOptions } from './tx/builder'; -import { TransformNodeType } from './Node'; +import { TransformNodeType } from './node/Base'; import { NameEntry, NamePointer } from './apis/node'; import AccountBase from './account/Base'; import { AddressEncodings } from './tx/builder/field-types/address'; diff --git a/src/aepp-wallet-communication/rpc/types.ts b/src/aepp-wallet-communication/rpc/types.ts index fa6de15586..a522213266 100644 --- a/src/aepp-wallet-communication/rpc/types.ts +++ b/src/aepp-wallet-communication/rpc/types.ts @@ -1,7 +1,7 @@ import { Encoded } from '../../utils/encoder'; import { Domain, AciValue } from '../../utils/typed-data'; import { METHODS, SUBSCRIPTION_TYPES, WALLET_TYPE } from '../schema'; -import { TransformNodeType } from '../../Node'; +import { TransformNodeType } from '../../node/Base'; import { SignedTx } from '../../apis/node'; import { AensName } from '../../tx/builder/constants'; diff --git a/src/chain.ts b/src/chain.ts index f9bedc5d3a..31dacfeecd 100644 --- a/src/chain.ts +++ b/src/chain.ts @@ -10,7 +10,8 @@ import { AensPointerContextError, DryRunError, InvalidAensNameError, TransactionError, TxTimedOutError, TxNotInChainError, InternalError, } from './utils/errors'; -import Node, { TransformNodeType } from './Node'; +import { TransformNodeType } from './node/Base'; +import Node from './node/Direct'; import { Account as AccountNode, ByteCode, ContractObject, DryRunResult, DryRunResults, Generation, KeyBlock, MicroBlockHeader, NameEntry, SignedTx, diff --git a/src/contract/Contract.ts b/src/contract/Contract.ts index 37c313f0b1..3665137841 100644 --- a/src/contract/Contract.ts +++ b/src/contract/Contract.ts @@ -37,7 +37,8 @@ import { ContractCallObject as NodeContractCallObject, Event as NodeEvent, } from '../apis/node'; import CompilerBase, { Aci } from './compiler/Base'; -import Node, { TransformNodeType } from '../Node'; +import { TransformNodeType } from '../node/Base'; +import Node from '../node/Direct'; import { getAccount, getContract, getContractByteCode, resolveName, txDryRun, sendTransaction, SendTransactionOptions, diff --git a/src/contract/delegation-signature.ts b/src/contract/delegation-signature.ts index a05c36e819..9d56a87502 100644 --- a/src/contract/delegation-signature.ts +++ b/src/contract/delegation-signature.ts @@ -3,7 +3,7 @@ import { ArgumentError } from '../utils/errors'; import { AensName } from '../tx/builder/constants'; import AccountBase from '../account/Base'; import { isNameValid } from '../tx/builder/helpers'; -import Node from '../Node'; +import Node from '../node/Direct'; function ensureOracleQuery(oq: string): asserts oq is Encoded.OracleQueryId { if (!oq.startsWith('oq_')) throw new ArgumentError('oq', 'oracle query', oq); diff --git a/src/contract/ga.ts b/src/contract/ga.ts index ab62070b93..44500b7ef0 100644 --- a/src/contract/ga.ts +++ b/src/contract/ga.ts @@ -14,7 +14,7 @@ import { ArgumentError, IllegalArgumentError } from '../utils/errors'; import { concatBuffers } from '../utils/other'; import AccountBase from '../account/Base'; import Contract from './Contract'; -import Node from '../Node'; +import Node from '../node/Direct'; import { sendTransaction, SendTransactionOptions, getAccount } from '../chain'; import CompilerBase from './compiler/Base'; diff --git a/src/index-browser.ts b/src/index-browser.ts index 2c4cbc30cf..b73993d965 100644 --- a/src/index-browser.ts +++ b/src/index-browser.ts @@ -61,7 +61,7 @@ export { default as AeSdkBase } from './AeSdkBase'; export { default as AeSdk } from './AeSdk'; export { default as AeSdkAepp } from './AeSdkAepp'; export { default as AeSdkWallet } from './AeSdkWallet'; -export { default as Node } from './Node'; +export { default as Node } from './node/Direct'; export { default as verifyTransaction } from './tx/validator'; export { default as AccountBase } from './account/Base'; export { default as MemoryAccount } from './account/Memory'; diff --git a/src/Node.ts b/src/node/Base.ts similarity index 51% rename from src/Node.ts rename to src/node/Base.ts index 46cab2c9ed..18e044ee1b 100644 --- a/src/Node.ts +++ b/src/node/Base.ts @@ -1,15 +1,9 @@ // eslint-disable-next-line max-classes-per-file import BigNumber from 'bignumber.js'; import { OperationArguments, OperationSpec } from '@azure/core-client'; -import { - genRequestQueuesPolicy, genCombineGetRequestsPolicy, genErrorFormatterPolicy, - genVersionCheckPolicy, genRetryOnFailurePolicy, -} from './utils/autorest'; -import { Node as NodeApi, NodeOptionalParams, ErrorModel } from './apis/node'; -import { mapObject } from './utils/other'; -import { UnsupportedVersionError } from './utils/errors'; -import { Encoded } from './utils/encoder'; -import { ConsensusProtocolVersion } from './tx/builder/constants'; +import { Node as NodeApi } from '../apis/node'; +import { mapObject } from '../utils/other'; +import { Encoded } from '../utils/encoder'; const bigIntPropertyNames = [ 'balance', 'queryFee', 'fee', 'amount', 'nameFee', 'channelAmount', @@ -104,95 +98,5 @@ type NodeTransformedApi = new (...args: ConstructorParameters) = ? NodeApi[Name] : TransformNodeType }; -interface NodeInfo { - url: string; - nodeNetworkId: string; - version: string; - consensusProtocolVersion: ConsensusProtocolVersion; -} - -export default class Node extends (NodeTransformed as unknown as NodeTransformedApi) { - #networkIdPromise?: Promise; - - /** - * @param url - Url for node API - * @param options - Options - * @param options.ignoreVersion - Don't ensure that the node is supported - * @param options.retryCount - Amount of extra requests to do in case of failure - * @param options.retryOverallDelay - Time in ms to wait between all retries - */ - constructor( - url: string, - { - ignoreVersion = false, retryCount = 3, retryOverallDelay = 800, ...options - }: NodeOptionalParams & { - ignoreVersion?: boolean; - retryCount?: number; - retryOverallDelay?: number; - } = {}, - ) { - // eslint-disable-next-line constructor-super - super(url, { - allowInsecureConnection: true, - additionalPolicies: [ - genRequestQueuesPolicy(), - genCombineGetRequestsPolicy(), - genRetryOnFailurePolicy(retryCount, retryOverallDelay), - genErrorFormatterPolicy((body: ErrorModel) => ` ${body.reason}`), - ], - ...options, - }); - if (!ignoreVersion) { - const statusPromise = this.getStatus(); - const versionPromise = statusPromise.then(({ nodeVersion }) => nodeVersion, (error) => error); - this.#networkIdPromise = statusPromise.then(({ networkId }) => networkId, (error) => error); - this.pipeline.addPolicy( - genVersionCheckPolicy('node', '/v3/status', versionPromise, '6.2.0', '7.0.0'), - ); - } - this.intAsString = true; - } - - /** - * Returns network ID provided by node. - * This method won't do extra requests on subsequent calls. - */ - async getNetworkId(): Promise { - this.#networkIdPromise ??= this.getStatus().then(({ networkId }) => networkId); - const networkId = await this.#networkIdPromise; - if (networkId instanceof Error) throw networkId; - return networkId; - } - - async getNodeInfo(): Promise { - const { - nodeVersion, - networkId: nodeNetworkId, - protocols, - topBlockHeight, - } = await this.getStatus(); - - const consensusProtocolVersion = protocols - .filter(({ effectiveAtHeight }) => topBlockHeight >= effectiveAtHeight) - .reduce( - (acc, p) => (p.effectiveAtHeight > acc.effectiveAtHeight ? p : acc), - { effectiveAtHeight: -1, version: 0 }, - ) - .version; - if (ConsensusProtocolVersion[consensusProtocolVersion] == null) { - const version = consensusProtocolVersion.toString(); - const versions = Object.values(ConsensusProtocolVersion) - .filter((el) => typeof el === 'number').map((el) => +el); - const geVersion = Math.min(...versions).toString(); - const ltVersion = (Math.max(...versions) + 1).toString(); - throw new UnsupportedVersionError('consensus protocol', version, geVersion, ltVersion); - } - - return { - url: this.$host, - nodeNetworkId, - version: nodeVersion, - consensusProtocolVersion, - }; - } +export default class NodeBase extends (NodeTransformed as unknown as NodeTransformedApi) { } diff --git a/src/node/Direct.ts b/src/node/Direct.ts new file mode 100644 index 0000000000..ae5db01fe2 --- /dev/null +++ b/src/node/Direct.ts @@ -0,0 +1,101 @@ +import { + genRequestQueuesPolicy, genCombineGetRequestsPolicy, genErrorFormatterPolicy, + genVersionCheckPolicy, genRetryOnFailurePolicy, +} from '../utils/autorest'; +import NodeBase from './Base'; +import { UnsupportedVersionError } from '../utils/errors'; +import { ConsensusProtocolVersion } from '../tx/builder/constants'; +import { NodeOptionalParams, ErrorModel } from '../apis/node'; + +interface NodeInfo { + url: string; + nodeNetworkId: string; + version: string; + consensusProtocolVersion: ConsensusProtocolVersion; +} + +export default class NodeDefault extends NodeBase { + #networkIdPromise?: Promise; + + /** + * @param url - Url for node API + * @param options - Options + * @param options.ignoreVersion - Don't ensure that the node is supported + * @param options.retryCount - Amount of extra requests to do in case of failure + * @param options.retryOverallDelay - Time in ms to wait between all retries + */ + constructor( + url: string, + { + ignoreVersion = false, retryCount = 3, retryOverallDelay = 800, ...options + }: NodeOptionalParams & { + ignoreVersion?: boolean; + retryCount?: number; + retryOverallDelay?: number; + } = {}, + ) { + // eslint-disable-next-line constructor-super + super(url, { + allowInsecureConnection: true, + additionalPolicies: [ + genRequestQueuesPolicy(), + genCombineGetRequestsPolicy(), + genRetryOnFailurePolicy(retryCount, retryOverallDelay), + genErrorFormatterPolicy((body: ErrorModel) => ` ${body.reason}`), + ], + ...options, + }); + if (!ignoreVersion) { + const statusPromise = this.getStatus(); + const versionPromise = statusPromise.then(({ nodeVersion }) => nodeVersion, (error) => error); + this.#networkIdPromise = statusPromise.then(({ networkId }) => networkId, (error) => error); + this.pipeline.addPolicy( + genVersionCheckPolicy('node', '/v3/status', versionPromise, '6.2.0', '7.0.0'), + ); + } + this.intAsString = true; + } + + /** + * Returns network ID provided by node. + * This method won't do extra requests on subsequent calls. + */ + async getNetworkId(): Promise { + this.#networkIdPromise ??= this.getStatus().then(({ networkId }) => networkId); + const networkId = await this.#networkIdPromise; + if (networkId instanceof Error) throw networkId; + return networkId; + } + + async getNodeInfo(): Promise { + const { + nodeVersion, + networkId: nodeNetworkId, + protocols, + topBlockHeight, + } = await this.getStatus(); + + const consensusProtocolVersion = protocols + .filter(({ effectiveAtHeight }) => topBlockHeight >= effectiveAtHeight) + .reduce( + (acc, p) => (p.effectiveAtHeight > acc.effectiveAtHeight ? p : acc), + { effectiveAtHeight: -1, version: 0 }, + ) + .version; + if (ConsensusProtocolVersion[consensusProtocolVersion] == null) { + const version = consensusProtocolVersion.toString(); + const versions = Object.values(ConsensusProtocolVersion) + .filter((el) => typeof el === 'number').map((el) => +el); + const geVersion = Math.min(...versions).toString(); + const ltVersion = (Math.max(...versions) + 1).toString(); + throw new UnsupportedVersionError('consensus protocol', version, geVersion, ltVersion); + } + + return { + url: this.$host, + nodeNetworkId, + version: nodeVersion, + consensusProtocolVersion, + }; + } +} diff --git a/src/oracle.ts b/src/oracle.ts index ce94903544..add6ba45bd 100644 --- a/src/oracle.ts +++ b/src/oracle.ts @@ -17,7 +17,7 @@ import { import { _getPollInterval, getHeight, sendTransaction, SendTransactionOptions, } from './chain'; -import Node from './Node'; +import Node from './node/Direct'; import AccountBase from './account/Base'; type OracleQueries = Awaited>['oracleQueries']; diff --git a/src/tx/builder/field-types/abi-version.ts b/src/tx/builder/field-types/abi-version.ts index 50438deb65..671b6b3a79 100644 --- a/src/tx/builder/field-types/abi-version.ts +++ b/src/tx/builder/field-types/abi-version.ts @@ -1,6 +1,6 @@ import { Tag, ConsensusProtocolVersion, AbiVersion } from '../constants'; import { getProtocolDetails } from './ct-version'; -import Node from '../../../Node'; +import Node from '../../../node/Direct'; export default { _getProtocolDetails(c: ConsensusProtocolVersion, tag: Tag): AbiVersion { diff --git a/src/tx/builder/field-types/ct-version.ts b/src/tx/builder/field-types/ct-version.ts index c466445038..e1b21eabce 100644 --- a/src/tx/builder/field-types/ct-version.ts +++ b/src/tx/builder/field-types/ct-version.ts @@ -1,5 +1,5 @@ import { ConsensusProtocolVersion, VmVersion, AbiVersion } from '../constants'; -import Node from '../../../Node'; +import Node from '../../../node/Direct'; /* * First abi/vm by default diff --git a/src/tx/builder/field-types/nonce.ts b/src/tx/builder/field-types/nonce.ts index a5d814852f..a72d2e39f8 100644 --- a/src/tx/builder/field-types/nonce.ts +++ b/src/tx/builder/field-types/nonce.ts @@ -1,6 +1,6 @@ import { isAccountNotFoundError } from '../../../utils/other'; import shortUInt from './short-u-int'; -import Node from '../../../Node'; +import Node from '../../../node/Direct'; import { ArgumentError } from '../../../utils/errors'; import { NextNonceStrategy } from '../../../apis/node'; diff --git a/src/tx/builder/field-types/query-fee.ts b/src/tx/builder/field-types/query-fee.ts index 57e4bdeff1..5adaafd586 100644 --- a/src/tx/builder/field-types/query-fee.ts +++ b/src/tx/builder/field-types/query-fee.ts @@ -1,6 +1,6 @@ import coinAmount from './coin-amount'; import { Int } from '../constants'; -import Node from '../../../Node'; +import Node from '../../../node/Direct'; import { Encoded } from '../../../utils/encoder'; import { ArgumentError } from '../../../utils/errors'; diff --git a/src/tx/builder/field-types/ttl.ts b/src/tx/builder/field-types/ttl.ts index 9bb4665c9e..ca68dad94f 100644 --- a/src/tx/builder/field-types/ttl.ts +++ b/src/tx/builder/field-types/ttl.ts @@ -1,5 +1,5 @@ import shortUInt from './short-u-int'; -import Node from '../../../Node'; +import Node from '../../../node/Direct'; import { ArgumentError } from '../../../utils/errors'; /** diff --git a/src/tx/execution-cost.ts b/src/tx/execution-cost.ts index 2b49b50d30..1f777a772a 100644 --- a/src/tx/execution-cost.ts +++ b/src/tx/execution-cost.ts @@ -4,7 +4,7 @@ import { Tag } from './builder/constants'; import { verify } from '../utils/crypto'; import { getBufferToSign } from '../account/Memory'; import { IllegalArgumentError, InternalError, TransactionError } from '../utils/errors'; -import Node from '../Node'; +import Node from '../node/Direct'; import getTransactionSignerAddress from './transaction-signer'; /** diff --git a/src/tx/validator.ts b/src/tx/validator.ts index 60170f0b0d..d4a3ee9dbf 100644 --- a/src/tx/validator.ts +++ b/src/tx/validator.ts @@ -6,7 +6,8 @@ import { Tag, ConsensusProtocolVersion } from './builder/constants'; import { buildTx, unpackTx } from './builder'; import { concatBuffers, isAccountNotFoundError } from '../utils/other'; import { Encoded, decode } from '../utils/encoder'; -import Node, { TransformNodeType } from '../Node'; +import { TransformNodeType } from '../node/Base'; +import Node from '../node/Direct'; import { Account } from '../apis/node'; import { genAggressiveCacheGetResponsesPolicy } from '../utils/autorest'; import { UnexpectedTsError } from '../utils/errors'; From 27d1a4c26ca88bd70d514e35a54beca6e2d02e60 Mon Sep 17 00:00:00 2001 From: Denis Davidyuk Date: Tue, 13 Feb 2024 19:18:40 +0700 Subject: [PATCH 2/2] feat(node): add NodeGateway retrying nonce requests if resp outdated --- src/index-browser.ts | 1 + src/node/Direct.ts | 13 +++- src/node/Gateway.ts | 98 +++++++++++++++++++++++++++++ src/tx/validator.ts | 1 + src/utils/autorest.ts | 21 +++---- test/integration/NodeGateway.ts | 75 ++++++++++++++++++++++ test/integration/index.ts | 6 +- test/integration/~execution-cost.ts | 2 +- 8 files changed, 200 insertions(+), 17 deletions(-) create mode 100644 src/node/Gateway.ts create mode 100644 test/integration/NodeGateway.ts diff --git a/src/index-browser.ts b/src/index-browser.ts index b73993d965..48fee31ec1 100644 --- a/src/index-browser.ts +++ b/src/index-browser.ts @@ -62,6 +62,7 @@ export { default as AeSdk } from './AeSdk'; export { default as AeSdkAepp } from './AeSdkAepp'; export { default as AeSdkWallet } from './AeSdkWallet'; export { default as Node } from './node/Direct'; +export { default as NodeGateway } from './node/Gateway'; export { default as verifyTransaction } from './tx/validator'; export { default as AccountBase } from './account/Base'; export { default as MemoryAccount } from './account/Memory'; diff --git a/src/node/Direct.ts b/src/node/Direct.ts index ae5db01fe2..4c13a326f7 100644 --- a/src/node/Direct.ts +++ b/src/node/Direct.ts @@ -27,19 +27,30 @@ export default class NodeDefault extends NodeBase { constructor( url: string, { - ignoreVersion = false, retryCount = 3, retryOverallDelay = 800, ...options + ignoreVersion = false, _disableGatewayWarning = false, + retryCount = 3, retryOverallDelay = 800, + ...options }: NodeOptionalParams & { ignoreVersion?: boolean; + _disableGatewayWarning?: boolean; retryCount?: number; retryOverallDelay?: number; } = {}, ) { + const { hostname } = new URL(url); + if ( + !_disableGatewayWarning + && ['mainnet.aeternity.io', 'testnet.aeternity.io'].includes(hostname) + ) { + console.warn(`Node: use NodeGateway to connect to ${hostname} for better reliability.`); + } // eslint-disable-next-line constructor-super super(url, { allowInsecureConnection: true, additionalPolicies: [ genRequestQueuesPolicy(), genCombineGetRequestsPolicy(), + // TODO: move to NodeGateway in the next breaking release genRetryOnFailurePolicy(retryCount, retryOverallDelay), genErrorFormatterPolicy((body: ErrorModel) => ` ${body.reason}`), ], diff --git a/src/node/Gateway.ts b/src/node/Gateway.ts new file mode 100644 index 0000000000..beb9d50a5b --- /dev/null +++ b/src/node/Gateway.ts @@ -0,0 +1,98 @@ +import NodeDirect from './Direct'; +import { getIntervals } from '../utils/autorest'; +import { pause } from '../utils/other'; +import { buildTx, unpackTx } from '../tx/builder'; +import { Tag } from '../tx/builder/constants'; +import getTransactionSignerAddress from '../tx/transaction-signer'; +import { Encoded } from '../utils/encoder'; +import { IllegalArgumentError } from '../utils/errors'; + +/** + * Implements request retry strategies to improve reliability of connection to multiple nodes behind + * load balancer. + */ +export default class NodeGateway extends NodeDirect { + #nonces: Record = {}; + + readonly #retryIntervals: number[]; + + /** + * @param url - Url for node API + * @param options - Options + */ + constructor( + url: string, + { + retryCount = 8, retryOverallDelay = 3000, ...options + }: ConstructorParameters[1] = {}, + ) { + super(url, { + ...options, retryCount, retryOverallDelay, _disableGatewayWarning: true, + }); + this.#retryIntervals = getIntervals(retryCount, retryOverallDelay); + } + + #saveNonce(tx: Encoded.Transaction): void { + const { encodedTx } = unpackTx(tx, Tag.SignedTx); + if (encodedTx.tag === Tag.GaMetaTx) return; + if (!('nonce' in encodedTx)) { + throw new IllegalArgumentError('Transaction doesn\'t have nonce field'); + } + const address = getTransactionSignerAddress(tx); + this.#nonces[address] = encodedTx.nonce; + if (encodedTx.tag === Tag.PayingForTx) { + this.#saveNonce(buildTx(encodedTx.tx)); + } + } + + // @ts-expect-error use code generation to create node class or integrate bigint to autorest + override async postTransaction( + ...args: Parameters + ): ReturnType { + const res = super.postTransaction(...args); + try { + this.#saveNonce(args[0].tx as Encoded.Transaction); + } catch (error) { + console.warn('NodeGateway: failed to save nonce,', error); + } + return res; + } + + async #retryNonceRequest( + address: string, + doRequest: () => Promise, + getNonce: (t: T) => number, + ): Promise { + for (let attempt = 0; attempt < this.#retryIntervals.length; attempt += 1) { + const result = await doRequest(); + const nonce = getNonce(result); + if (nonce >= (this.#nonces[address] ?? -1)) { + return result; + } + await pause(this.#retryIntervals[attempt]); + } + return doRequest(); + } + + // @ts-expect-error use code generation to create node class or integrate bigint to autorest + override async getAccountByPubkey( + ...args: Parameters + ): ReturnType { + return this.#retryNonceRequest( + args[0], + async () => super.getAccountByPubkey(...args), + ({ nonce, kind }) => (kind === 'generalized' ? Number.MAX_SAFE_INTEGER : nonce), + ); + } + + // @ts-expect-error use code generation to create node class or integrate bigint to autorest + override async getAccountNextNonce( + ...args: Parameters + ): ReturnType { + return this.#retryNonceRequest( + args[0], + async () => super.getAccountNextNonce(...args), + ({ nextNonce }) => (nextNonce === 0 ? Number.MAX_SAFE_INTEGER : nextNonce - 1), + ); + } +} diff --git a/src/tx/validator.ts b/src/tx/validator.ts index d4a3ee9dbf..289cf32986 100644 --- a/src/tx/validator.ts +++ b/src/tx/validator.ts @@ -81,6 +81,7 @@ export default async function verifyTransaction( ignoreVersion: true, pipeline: nodeNotCached.pipeline.clone(), additionalPolicies: [genAggressiveCacheGetResponsesPolicy()], + _disableGatewayWarning: true, }); return verifyTransactionInternal(unpackTx(transaction), node, []); } diff --git a/src/utils/autorest.ts b/src/utils/autorest.ts index 19a8853560..0aac2badc1 100644 --- a/src/utils/autorest.ts +++ b/src/utils/autorest.ts @@ -117,6 +117,13 @@ export const genVersionCheckPolicy = ( }, }); +export const getIntervals = (retryCount: number, retryOverallDelay: number): number[] => { + const intervals = new Array(retryCount).fill(0) + .map((_, idx) => ((idx + 1) / retryCount) ** 2); + const intervalSum = intervals.reduce((a, b) => a + b, 0); + return intervals.map((el) => Math.floor((el / intervalSum) * retryOverallDelay)); +}; + export const genRetryOnFailurePolicy = ( retryCount: number, retryOverallDelay: number, @@ -125,20 +132,10 @@ export const genRetryOnFailurePolicy = ( name: 'retry-on-failure', async sendRequest(request, next) { const statusesToNotRetry = [200, 400, 403, 410, 500]; - - const intervals = new Array(retryCount).fill(0) - .map((_, idx) => ((idx + 1) / retryCount) ** 2); - const intervalSum = intervals.reduce((a, b) => a + b, 0); - const intervalsInMs = intervals.map((e) => Math.floor((e / intervalSum) * retryOverallDelay)); - + const intervals = getIntervals(retryCount, retryOverallDelay); let error = new RestError('Not expected to be thrown'); for (let attempt = 0; attempt <= retryCount; attempt += 1) { - if (attempt !== 0) { - await pause(intervalsInMs[attempt - 1]); - const urlParsed = new URL(request.url); - urlParsed.searchParams.set('__sdk-retry', attempt.toString()); - request.url = urlParsed.toString(); - } + if (attempt !== 0) await pause(intervals[attempt - 1]); try { return await next(request); } catch (e) { diff --git a/test/integration/NodeGateway.ts b/test/integration/NodeGateway.ts new file mode 100644 index 0000000000..bd0b3c5a23 --- /dev/null +++ b/test/integration/NodeGateway.ts @@ -0,0 +1,75 @@ +import { describe, it, before } from 'mocha'; +import { expect } from 'chai'; +import { getSdk, url } from '.'; +import { + NodeGateway, AeSdk, Tag, buildTx, Encoded, +} from '../../src'; +import { bindRequestCounter } from '../utils'; + +describe('NodeGateway', () => { + let aeSdk: AeSdk; + const node = new NodeGateway(url, { retryCount: 2, retryOverallDelay: 500 }); + node.pipeline.addPolicy({ + name: 'swallow-post-tx-request', + async sendRequest(request, next) { + const suffix = 'transactions?int-as-string=true'; + if (!request.url.endsWith(suffix)) return next(request); + request.url = request.url.replace(suffix, 'status'); + request.method = 'GET'; + delete request.body; + const response = await next(request); + response.bodyAsText = '{"tx_hash": "fake"}'; + return response; + }, + }); + let spendTxHighNonce: Encoded.Transaction; + + before(async () => { + aeSdk = await getSdk(); + const spendTx = buildTx({ + tag: Tag.SpendTx, recipientId: aeSdk.address, senderId: aeSdk.address, nonce: 1e10, + }); + spendTxHighNonce = await aeSdk.signTransaction(spendTx); + }); + + it('doesn\'t retries getAccountByPubkey before seeing a transaction', async () => { + const getCount = bindRequestCounter(node); + await node.getAccountByPubkey(aeSdk.address); + expect(getCount()).to.be.equal(1); + }); + + it('doesn\'t retries getAccountNextNonce before seeing a transaction', async () => { + const getCount = bindRequestCounter(node); + await node.getAccountNextNonce(aeSdk.address); + expect(getCount()).to.be.equal(1); + }); + + it('retries getAccountByPubkey', async () => { + await node.postTransaction({ tx: spendTxHighNonce }); + const getCount = bindRequestCounter(node); + await node.getAccountByPubkey(aeSdk.address); + expect(getCount()).to.be.equal(3); + }); + + it('retries getAccountNextNonce once for multiple calls', async () => { + await node.postTransaction({ tx: spendTxHighNonce }); + const getCount = bindRequestCounter(node); + const nonces = await Promise.all( + new Array(3).fill(undefined).map(async () => node.getAccountNextNonce(aeSdk.address)), + ); + expect(getCount()).to.be.equal(3); + expect(nonces).to.be.eql(nonces.map(() => ({ nextNonce: 1 }))); + }); + + it('doesn\'t retries nonce for generalized account', async () => { + const sourceCode = `contract BlindAuth = + stateful entrypoint authorize() : bool = false`; + await aeSdk.createGeneralizedAccount('authorize', [], { sourceCode }); + await node.postTransaction({ tx: spendTxHighNonce }); + + const getCount = bindRequestCounter(node); + await node.getAccountByPubkey(aeSdk.address); + await node.getAccountNextNonce(aeSdk.address); + expect(getCount()).to.be.equal(2); + }); +}); diff --git a/test/integration/index.ts b/test/integration/index.ts index b6b9ff9d08..cd9cc4bc44 100644 --- a/test/integration/index.ts +++ b/test/integration/index.ts @@ -1,6 +1,6 @@ import { after } from 'mocha'; import { - AeSdk, CompilerHttpNode, MemoryAccount, Node, Encoded, ConsensusProtocolVersion, + AeSdk, CompilerHttpNode, MemoryAccount, Node, NodeGateway, Encoded, ConsensusProtocolVersion, } from '../../src'; import '..'; @@ -70,8 +70,8 @@ export function addTransactionHandler(cb: TransactionHandler): void { transactionHandlers.push(cb); } -class NodeHandleTx extends Node { - // @ts-expect-error use code generation to create node class? +class NodeHandleTx extends (network == null ? Node : NodeGateway) { + // @ts-expect-error use code generation to create node class or integrate bigint to autorest override async postTransaction( ...args: Parameters ): ReturnType { diff --git a/test/integration/~execution-cost.ts b/test/integration/~execution-cost.ts index 66f2391e66..8a3291c148 100644 --- a/test/integration/~execution-cost.ts +++ b/test/integration/~execution-cost.ts @@ -9,7 +9,7 @@ import { } from '../../src'; import { pause } from '../../src/utils/other'; -const node = new Node(url); +const node = new Node(url, { _disableGatewayWarning: true }); interface TxDetails { tx: Encoded.Transaction; cost: bigint; blockHash: Encoded.MicroBlockHash } const sentTxPromises: Array> = [];