From 09d19bff34ef5212dfa48bfe5c18bd28fd07be99 Mon Sep 17 00:00:00 2001 From: Denis Davidyuk Date: Wed, 23 Aug 2023 20:29:07 +0400 Subject: [PATCH] feat(tx-builder): get actual gas price from node --- docker-compose.yml | 2 +- docs/guides/batch-requests.md | 2 + docs/transaction-options.md | 4 +- src/AeSdkMethods.ts | 1 + src/Node.ts | 31 ++- src/chain.ts | 2 +- src/tx/builder/constants.ts | 2 +- src/tx/builder/field-types/fee.ts | 97 +++++---- src/tx/builder/field-types/gas-limit.ts | 6 +- src/tx/builder/field-types/gas-price.ts | 59 +++++- src/tx/builder/index.ts | 1 + test/dynamic-gas-price.html | 258 ++++++++++++++++++++++++ test/integration/chain.ts | 3 + test/integration/node.ts | 13 ++ test/integration/transaction.ts | 76 ++++++- tooling/autorest/node.yaml | 7 +- 16 files changed, 503 insertions(+), 61 deletions(-) create mode 100644 test/dynamic-gas-price.html diff --git a/docker-compose.yml b/docker-compose.yml index 2d7154c5fc..d7816261d1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: '3' services: node: - image: aeternity/aeternity:master@sha256:824d40d60fcf4a07d805bba6b6880e182331a971bb8e4793d45d66eaa5e568fa + image: aeternity/aeternity:master@sha256:4ab3f2e33f02b0b96a0cb1b16544cd62498d0b4b983ba74e12b2564f2b908834 hostname: node ports: ["3013:3013", "3113:3113", "3014:3014", "3114:3114"] volumes: diff --git a/docs/guides/batch-requests.md b/docs/guides/batch-requests.md index 2eefcb604b..baa8c86057 100644 --- a/docs/guides/batch-requests.md +++ b/docs/guides/batch-requests.md @@ -25,6 +25,8 @@ await Promise.all(spends.map(({ amount, address }, idx) => ``` This way, SDK would make a single request to get info about the sender account and a transaction post request per each item in the `spends` array. +Additionally, you may want to set `gasPrice` and `fee` to have predictable expenses. By default, SDK sets them based on the current network demand. + ## Multiple contract static calls Basically, the dry-run endpoint of the node is used to run them. Doing requests one by one, like ```js diff --git a/docs/transaction-options.md b/docs/transaction-options.md index f2c65b9225..912686eaa9 100644 --- a/docs/transaction-options.md +++ b/docs/transaction-options.md @@ -32,7 +32,7 @@ These options are common and can be provided to every tx-type: If the strategy is set to `continuity`, then transactions in the mempool are checked if there are gaps - missing nonces that prevent transactions with greater nonces to get included - `ttl` (default: `0`) - Should be set if you want the transaction to be only valid until a certain block height is reached. -- `fee` (default: calculated for each tx-type) +- `fee` (default: calculated for each tx-type, based on network demand) - The minimum fee is dependent on the tx-type. - You can provide a higher fee to additionally reward the miners. - `innerTx` (default: `false`) @@ -53,7 +53,7 @@ The following options are sepcific for each tx-type. - You can specify the denomination of the `amount` that will be provided to the contract related transaction. - `gasLimit` - Maximum amount of gas to be consumed by the transaction. Learn more on [How to estimate gas?](#how-to-estimate-gas) -- `gasPrice` (default: `1e9`) +- `gasPrice` (default: based on network demand, minimum: `1e9`) - To increase chances to get your transaction included quickly you can use a higher gasPrice. ### NameClaimTx diff --git a/src/AeSdkMethods.ts b/src/AeSdkMethods.ts index 7c583c49a5..968aa3e1e8 100644 --- a/src/AeSdkMethods.ts +++ b/src/AeSdkMethods.ts @@ -89,6 +89,7 @@ class AeSdkMethods { }; } + // TODO: omit onNode from options, because it is already in context async buildTx(options: TxParamsAsync): Promise { return buildTxAsync({ ...this.getContext(), ...options }); } diff --git a/src/Node.ts b/src/Node.ts index 14a03070ec..330f5832cb 100644 --- a/src/Node.ts +++ b/src/Node.ts @@ -14,7 +14,7 @@ import { ConsensusProtocolVersion } from './tx/builder/constants'; const bigIntPropertyNames = [ 'balance', 'queryFee', 'fee', 'amount', 'nameFee', 'channelAmount', 'initiatorAmount', 'responderAmount', 'channelReserve', 'initiatorAmountFinal', - 'responderAmountFinal', 'gasPrice', 'deposit', + 'responderAmountFinal', 'gasPrice', 'minGasPrice', 'deposit', ] as const; const numberPropertyNames = [ @@ -22,7 +22,7 @@ const numberPropertyNames = [ 'nonce', 'nextNonce', 'height', 'blockHeight', 'topBlockHeight', 'ttl', 'nameTtl', 'clientTtl', 'inbound', 'outbound', 'peerCount', 'pendingTransactionsCount', 'effectiveAtHeight', - 'version', 'solutions', 'round', + 'version', 'solutions', 'round', 'minutes', 'utilization', ] as const; class NodeTransformed extends NodeApi { @@ -112,8 +112,6 @@ interface NodeInfo { } export default class Node extends (NodeTransformed as unknown as NodeTransformedApi) { - #networkIdPromise?: Promise; - /** * @param url - Url for node API * @param options - Options @@ -145,9 +143,8 @@ export default class Node extends (NodeTransformed as unknown as NodeTransformed ...options, }); if (!ignoreVersion) { - const statusPromise = this.getStatus(); - const versionPromise = statusPromise.then(({ nodeVersion }) => nodeVersion, (error) => error); - this.#networkIdPromise = statusPromise.then(({ networkId }) => networkId, (error) => error); + const versionPromise = this._getCachedStatus() + .then(({ nodeVersion }) => nodeVersion, (error) => error); this.pipeline.addPolicy( genVersionCheckPolicy('node', '/v3/status', versionPromise, '6.2.0', '7.0.0'), ); @@ -155,15 +152,27 @@ export default class Node extends (NodeTransformed as unknown as NodeTransformed this.intAsString = true; } + #cachedStatusPromise?: ReturnType; + + async _getCachedStatus(): ReturnType { + this.#cachedStatusPromise ??= this.getStatus(); + return this.#cachedStatusPromise; + } + + // @ts-expect-error use code generation to create node class? + override async getStatus( + ...args: Parameters['getStatus']> + ): ReturnType['getStatus']> { + this.#cachedStatusPromise = super.getStatus(...args); + return this.#cachedStatusPromise; + } + /** * 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; + return (await this._getCachedStatus()).networkId; } async getNodeInfo(): Promise { diff --git a/src/chain.ts b/src/chain.ts index 376705cb3e..0896f517b0 100644 --- a/src/chain.ts +++ b/src/chain.ts @@ -53,7 +53,7 @@ export async function getHeight( const onNode = unwrapProxy(options.onNode); if (cached) { const cache = heightCache.get(onNode); - if (cache?.time != null && cache.time > Date.now() - _getPollInterval('block', options)) { + if (cache != null && cache.time > Date.now() - _getPollInterval('block', options)) { return cache.height; } } diff --git a/src/tx/builder/constants.ts b/src/tx/builder/constants.ts index bad55c1bd7..80b468d88d 100644 --- a/src/tx/builder/constants.ts +++ b/src/tx/builder/constants.ts @@ -9,7 +9,7 @@ export const DRY_RUN_ACCOUNT = { export const MAX_AUTH_FUN_GAS = 50000; export type Int = number | string | BigNumber; export type AensName = `${string}.chain`; -export const MIN_GAS_PRICE = 1e9; +export const MIN_GAS_PRICE = 1e9; // TODO: don't use number for ae // # see https://github.com/aeternity/aeternity/blob/72e440b8731422e335f879a31ecbbee7ac23a1cf/apps/aecore/src/aec_governance.erl#L67 export const NAME_FEE_MULTIPLIER = 1e14; // 100000000000000 export const NAME_FEE_BID_INCREMENT = 0.05; // # the increment is in percentage diff --git a/src/tx/builder/field-types/fee.ts b/src/tx/builder/field-types/fee.ts index 8a56f2129f..46937ba40c 100644 --- a/src/tx/builder/field-types/fee.ts +++ b/src/tx/builder/field-types/fee.ts @@ -1,26 +1,29 @@ import BigNumber from 'bignumber.js'; -import { IllegalArgumentError } from '../../../utils/errors'; -import { MIN_GAS_PRICE, Tag } from '../constants'; +import { ArgumentError, IllegalArgumentError } from '../../../utils/errors'; +import { Int, MIN_GAS_PRICE, Tag } from '../constants'; +import uInt from './u-int'; import coinAmount from './coin-amount'; +import { getCachedIncreasedGasPrice } from './gas-price'; import { isKeyOfObject } from '../../../utils/other'; import { decode, Encoded } from '../../../utils/encoder'; import type { unpackTx as unpackTxType, buildTx as buildTxType } from '../index'; +import Node from '../../../Node'; const BASE_GAS = 15000; const GAS_PER_BYTE = 20; const KEY_BLOCK_INTERVAL = 3; /** - * Calculate the Base fee gas + * Calculate the base gas * @see {@link https://github.com/aeternity/protocol/blob/master/consensus/README.md#gas} * @param txType - The transaction type - * @returns The base fee + * @returns The base gas * @example * ```js - * TX_FEE_BASE('channelForceProgress') => new BigNumber(30 * 15000) + * TX_BASE_GAS(Tag.ChannelForceProgressTx) => 30 * 15000 * ``` */ -const TX_FEE_BASE_GAS = (txType: Tag): BigNumber => { +const TX_BASE_GAS = (txType: Tag): number => { const feeFactors = { [Tag.ChannelForceProgressTx]: 30, [Tag.ChannelOffChainTx]: 0, @@ -36,44 +39,41 @@ const TX_FEE_BASE_GAS = (txType: Tag): BigNumber => { [Tag.PayingForTx]: 1 / 5, } as const; const factor = feeFactors[txType as keyof typeof feeFactors] ?? 1; - return new BigNumber(factor * BASE_GAS); + return factor * BASE_GAS; }; /** - * Calculate fee for Other types of transactions + * Calculate gas for other types of transactions * @see {@link https://github.com/aeternity/protocol/blob/master/consensus/README.md#gas} * @param txType - The transaction type * @param txSize - The transaction size * @returns parameters - The transaction parameters * @returns parameters.relativeTtl - The relative ttl * @returns parameters.innerTxSize - The size of the inner transaction - * @returns The Other fee + * @returns The other gas * @example * ```js - * TX_FEE_OTHER_GAS('oracleResponse',10, { relativeTtl: 10, innerTxSize: 10 }) - * => new BigNumber(10).times(20).plus(Math.ceil(32000 * 10 / Math.floor(60 * 24 * 365 / 2))) + * TX_OTHER_GAS(Tag.OracleResponseTx, 10, { relativeTtl: 12, innerTxSize: 0 }) + * => 10 * 20 + Math.ceil(32000 * 12 / Math.floor(60 * 24 * 365 / 3)) * ``` */ -const TX_FEE_OTHER_GAS = ( +const TX_OTHER_GAS = ( txType: Tag, txSize: number, { relativeTtl, innerTxSize }: { relativeTtl: number; innerTxSize: number }, -): BigNumber => { +): number => { switch (txType) { case Tag.OracleRegisterTx: case Tag.OracleExtendTx: case Tag.OracleQueryTx: case Tag.OracleResponseTx: - return new BigNumber(txSize) - .times(GAS_PER_BYTE) - .plus( - Math.ceil((32000 * relativeTtl) / Math.floor((60 * 24 * 365) / KEY_BLOCK_INTERVAL)), - ); + return txSize * GAS_PER_BYTE + + Math.ceil((32000 * relativeTtl) / Math.floor((60 * 24 * 365) / KEY_BLOCK_INTERVAL)); case Tag.GaMetaTx: case Tag.PayingForTx: - return new BigNumber(txSize).minus(innerTxSize).times(GAS_PER_BYTE); + return (txSize - innerTxSize) * GAS_PER_BYTE; default: - return new BigNumber(txSize).times(GAS_PER_BYTE); + return txSize * GAS_PER_BYTE; } }; @@ -91,13 +91,13 @@ function getOracleRelativeTtl(params: any): number { } /** - * Calculate fee based on tx type and params + * Calculate gas based on tx type and params */ -export function buildFee( +export function buildGas( builtTx: Encoded.Transaction, unpackTx: typeof unpackTxType, buildTx: typeof buildTxType, -): BigNumber { +): number { const { length } = decode(builtTx); const txObject = unpackTx(builtTx); @@ -106,11 +106,10 @@ export function buildFee( innerTxSize = decode(buildTx(txObject.tx.encodedTx)).length; } - return TX_FEE_BASE_GAS(txObject.tag) - .plus(TX_FEE_OTHER_GAS(txObject.tag, length, { + return TX_BASE_GAS(txObject.tag) + + TX_OTHER_GAS(txObject.tag, length, { relativeTtl: getOracleRelativeTtl(txObject), innerTxSize, - })) - .times(MIN_GAS_PRICE); + }); } /** @@ -127,24 +126,45 @@ function calculateMinFee( let previousFee; do { previousFee = fee; - fee = buildFee(rebuildTx(fee), unpackTx, buildTx); + fee = new BigNumber(MIN_GAS_PRICE).times(buildGas(rebuildTx(fee), unpackTx, buildTx)); } while (!fee.eq(previousFee)); return fee; } +// TODO: Get rid of this workaround. Transaction builder can't accept/return gas price instead of +// fee because it may get a decimal gas price. So, it should accept the optional `gasPrice` even +// if it is not a contract-related transaction. And use this `gasPrice` to calculate `fee`. +const gasPricePrefix = '_gas-price:'; + +export interface SerializeAettosParams { + rebuildTx: (params: any) => Encoded.Transaction; + unpackTx: typeof unpackTxType; + buildTx: typeof buildTxType; + _computingMinFee?: BigNumber; +} + export default { ...coinAmount, + async prepare( + value: Int | undefined, + params: {}, + { onNode }: { onNode?: Node }, + ): Promise { + if (value != null) return value; + if (onNode == null) { + throw new ArgumentError('onNode', 'provided (or provide `fee` instead)', onNode); + } + const gasPrice = await getCachedIncreasedGasPrice(onNode); + if (gasPrice === 0n) return undefined; + return gasPricePrefix + gasPrice; + }, + serializeAettos( _value: string | undefined, { rebuildTx, unpackTx, buildTx, _computingMinFee, - }: { - rebuildTx: (params: any) => Encoded.Transaction; - unpackTx: typeof unpackTxType; - buildTx: typeof buildTxType; - _computingMinFee?: BigNumber; - }, + }: SerializeAettosParams, { _canIncreaseFee }: { _canIncreaseFee?: boolean }, ): string { if (_computingMinFee != null) return _computingMinFee.toFixed(); @@ -153,7 +173,9 @@ export default { unpackTx, buildTx, ); - const value = new BigNumber(_value ?? minFee); + const value = _value?.startsWith(gasPricePrefix) === true + ? minFee.dividedBy(MIN_GAS_PRICE).times(_value.replace(gasPricePrefix, '')) + : new BigNumber(_value ?? minFee); if (minFee.gt(value)) { if (_canIncreaseFee === true) return minFee.toFixed(); throw new IllegalArgumentError(`Fee ${value.toString()} must be bigger than ${minFee}`); @@ -163,9 +185,12 @@ export default { serialize( value: Parameters[0], - params: Parameters[1], + params: Parameters[1] & SerializeAettosParams, options: { _canIncreaseFee?: boolean } & Parameters[2], ): Buffer { + if (typeof value === 'string' && value.startsWith(gasPricePrefix)) { + return uInt.serialize(this.serializeAettos(value, params, options)); + } return coinAmount.serialize.call(this, value, params, options); }, }; diff --git a/src/tx/builder/field-types/gas-limit.ts b/src/tx/builder/field-types/gas-limit.ts index d5f28dc2ff..94572cb1af 100644 --- a/src/tx/builder/field-types/gas-limit.ts +++ b/src/tx/builder/field-types/gas-limit.ts @@ -1,7 +1,7 @@ import { IllegalArgumentError } from '../../../utils/errors'; -import { MIN_GAS_PRICE, Tag, MAX_AUTH_FUN_GAS } from '../constants'; +import { Tag, MAX_AUTH_FUN_GAS } from '../constants'; import shortUInt from './short-u-int'; -import { buildFee } from './fee'; +import { buildGas } from './fee'; import type { unpackTx as unpackTxType, buildTx as buildTxType } from '../index'; function calculateGasLimitMax( @@ -10,7 +10,7 @@ function calculateGasLimitMax( unpackTx: typeof unpackTxType, buildTx: typeof buildTxType, ): number { - return gasMax - +buildFee(rebuildTx(gasMax), unpackTx, buildTx).dividedBy(MIN_GAS_PRICE); + return gasMax - +buildGas(rebuildTx(gasMax), unpackTx, buildTx); } export default { diff --git a/src/tx/builder/field-types/gas-price.ts b/src/tx/builder/field-types/gas-price.ts index 66a2f775b9..61330e47ae 100644 --- a/src/tx/builder/field-types/gas-price.ts +++ b/src/tx/builder/field-types/gas-price.ts @@ -1,11 +1,66 @@ +import BigNumber from 'bignumber.js'; import coinAmount from './coin-amount'; -import { IllegalArgumentError } from '../../../utils/errors'; -import { MIN_GAS_PRICE } from '../constants'; +import { ArgumentError, IllegalArgumentError } from '../../../utils/errors'; +import { Int, MIN_GAS_PRICE } from '../constants'; +import Node from '../../../Node'; +import { AE_AMOUNT_FORMATS, formatAmount } from '../../../utils/amount-formatter'; +import semverSatisfies from '../../../utils/semver-satisfies'; + +const gasPriceCache: WeakMap = new WeakMap(); + +export async function getCachedIncreasedGasPrice(node: Node): Promise { + const cache = gasPriceCache.get(node); + if (cache != null && cache.time > Date.now() - 20 * 1000) { + return cache.gasPrice; + } + + // TODO: remove after requiring node@6.13.0 + const { nodeVersion } = await node._getCachedStatus(); + // TODO: remove remove '6.12.0+' check after releasing 6.13.0 + if (!nodeVersion.startsWith('6.12.0+') && !semverSatisfies(nodeVersion, '6.13.0', '7.0.0')) { + return 0n; + } + + const { minGasPrice, utilization } = (await node.getRecentGasPrices())[0]; + let gasPrice = utilization < 70 ? 0n : BigInt( + new BigNumber(minGasPrice.toString()).times(1.01).integerValue().toFixed(), + ); + + const maxSafeGasPrice = BigInt(MIN_GAS_PRICE) * 100000n; // max microblock fee is 600ae or 35usd + if (gasPrice > maxSafeGasPrice) { + console.warn([ + `Estimated gas price ${gasPrice} exceeds the maximum safe value for unknown reason.`, + `It will be limited to ${maxSafeGasPrice}.`, + 'To overcome this restriction provide `gasPrice`/`fee` in options.', + ].join(' ')); + gasPrice = maxSafeGasPrice; + } + + gasPriceCache.set(node, { gasPrice, time: Date.now() }); + return gasPrice; +} // TODO: use withFormatting after using a single type for coins representation export default { ...coinAmount, + async prepare( + value: Int | undefined, + params: {}, + { onNode, denomination }: { + onNode?: Node; + denomination?: AE_AMOUNT_FORMATS; + }, + ): Promise { + if (value != null) return value; + if (onNode == null) { + throw new ArgumentError('onNode', 'provided (or provide `gasPrice` instead)', onNode); + } + const gasPrice = await getCachedIncreasedGasPrice(onNode); + if (gasPrice === 0n) return undefined; + return formatAmount(gasPrice, { targetDenomination: denomination }); + }, + serializeAettos(value: string | undefined = MIN_GAS_PRICE.toString()): string { if (+value < MIN_GAS_PRICE) { throw new IllegalArgumentError(`Gas price ${value.toString()} must be bigger than ${MIN_GAS_PRICE}`); diff --git a/src/tx/builder/index.ts b/src/tx/builder/index.ts index b6214ee9d3..516a17cc4a 100644 --- a/src/tx/builder/index.ts +++ b/src/tx/builder/index.ts @@ -54,6 +54,7 @@ export function buildTx( export type BuildTxOptions = Omit; +// TODO: require onNode because it is the only reason this builder is async [breaking change] /** * Build transaction async (may request node for additional data) * @category transaction builder diff --git a/test/dynamic-gas-price.html b/test/dynamic-gas-price.html new file mode 100644 index 0000000000..889c50b76f --- /dev/null +++ b/test/dynamic-gas-price.html @@ -0,0 +1,258 @@ + + + Run several clients and render stats + + + + + + + + + + + + +
+ Gas price + + + Utilization + +
+ Overall gas + + + Operation count + +
+ + + +
+ + + + + + + + diff --git a/test/integration/chain.ts b/test/integration/chain.ts index 789c0c605b..7b28128896 100644 --- a/test/integration/chain.ts +++ b/test/integration/chain.ts @@ -152,6 +152,9 @@ describe('Node Chain', () => { }); it('multiple spends from different accounts', async () => { + await aeSdkWithoutAccount.spend(0, aeSdk.address, { + onAccount: Object.values(aeSdk.accounts)[0], + }); const getCount = bindRequestCounter(aeSdkWithoutAccount.api); const spends = await Promise.all( accounts.map(async (onAccount) => aeSdkWithoutAccount.spend(1e14, aeSdk.address, { diff --git a/test/integration/node.ts b/test/integration/node.ts index c472dd6381..efb1f86621 100644 --- a/test/integration/node.ts +++ b/test/integration/node.ts @@ -89,6 +89,19 @@ describe('Node client', () => { .to.be.rejectedWith(RestError, 'v3/transactions error: Invalid tx (nonce_too_high)'); }); + it('returns recent gas prices', async () => { + const example: Awaited> = [ + { minGasPrice: 0n, minutes: 5, utilization: 0 }, + ]; + expect(example); + + const actual = await node.getRecentGasPrices(); + expect(actual).to.be.eql([1, 5, 15, 60].map((minutes, idx) => { + const { minGasPrice, utilization } = actual[idx]; + return { minGasPrice, minutes, utilization }; + })); + }); + describe('Node Pool', () => { it('Throw error on using API without node', () => { const nodes = new AeSdkBase({}); diff --git a/test/integration/transaction.ts b/test/integration/transaction.ts index b360c2dfec..8c79ad303b 100644 --- a/test/integration/transaction.ts +++ b/test/integration/transaction.ts @@ -1,9 +1,9 @@ import { describe, it, before } from 'mocha'; import { expect } from 'chai'; -import { createSandbox } from 'sinon'; +import { stub, createSandbox, replace } from 'sinon'; import { getSdk } from './index'; import { - AeSdk, Contract, + AeSdk, Contract, Node, commitmentHash, oracleQueryId, decode, encode, Encoded, Encoding, ORACLE_TTL_TYPES, Tag, AE_AMOUNT_FORMATS, buildTx, unpackTx, ConsensusProtocolVersion, } from '../../src'; @@ -36,6 +36,7 @@ contract Identity = entrypoint getArg(x : int) = x `; const gasLimit = 5e6; +const contractId = 'ct_TCQVoset7Y4qEyV5tgEAJAqa2Foz8J1EXqoGpq3fB6dWH5roe'; // Name const nameSalt = 4204563566073083; @@ -63,7 +64,76 @@ describe('Transaction', () => { spendAe.should.be.equal(spendAettos); }); - const contractId = 'ct_TCQVoset7Y4qEyV5tgEAJAqa2Foz8J1EXqoGpq3fB6dWH5roe'; + describe('gas price from node', () => { + function nodeReplaceRecentGasPrices(minGasPrice: bigint, utilization: number = 80): Node { + const node = new Node(aeSdk.api.$host); + replace(node, 'getRecentGasPrices', async () => Promise.resolve([{ + minGasPrice, + utilization, + minutes: 1, + }])); + return node; + } + + it('calculates fee in spend tx based on gas price', async () => { + const spendTx = await aeSdk.buildTx({ + tag: Tag.SpendTx, + senderId, + recipientId, + nonce, + onNode: nodeReplaceRecentGasPrices(1000000023n), + }); + const { fee } = unpackTx(spendTx, Tag.SpendTx); + expect(fee).to.be.equal((16660n * 1010000023n).toString()); + }); + + it('uses min gas price if utilization is low', async () => { + const spendTx = await aeSdk.buildTx({ + tag: Tag.SpendTx, + senderId, + recipientId, + nonce, + onNode: nodeReplaceRecentGasPrices(1000000023n, 60), + }); + const { fee } = unpackTx(spendTx, Tag.SpendTx); + expect(fee).to.be.equal((16660n * 1000000000n).toString()); + }); + + it('calculates fee in contract call based on gas price', async () => { + const spendTx = await aeSdk.buildTx({ + tag: Tag.ContractCallTx, + nonce, + callerId: address, + contractId, + amount, + gasLimit, + callData: contract._calldata.encode('Identity', 'getArg', [2]), + onNode: nodeReplaceRecentGasPrices(1000000023n), + }); + const { fee, gasPrice } = unpackTx(spendTx, Tag.ContractCallTx); + expect(fee).to.be.equal((182020n * 1010000023n).toString()); + expect(gasPrice).to.be.equal('1010000023'); + }); + + it('warns if gas price too big', async () => { + const s = stub(console, 'warn'); + const spendTx = await aeSdk.buildTx({ + tag: Tag.SpendTx, + senderId, + recipientId, + nonce, + onNode: nodeReplaceRecentGasPrices(99900000000000n), + }); + expect(s.firstCall.args).to.be.eql([ + 'Estimated gas price 100899000000000 exceeds the maximum safe value for unknown reason. ' + + 'It will be limited to 100000000000000. To overcome this restriction provide `gasPrice`/`fee` in options.', + ]); + s.restore(); + const { fee } = unpackTx(spendTx, Tag.SpendTx); + expect(fee).to.be.equal((16660n * 100000000000000n).toString()); + }); + }); + const transactions: Array<[ string, (() => Promise) | Encoded.Transaction, () => Promise, diff --git a/tooling/autorest/node.yaml b/tooling/autorest/node.yaml index 9356d34dba..9b9e2d7d91 100644 --- a/tooling/autorest/node.yaml +++ b/tooling/autorest/node.yaml @@ -7,6 +7,11 @@ directive: if (!$[key].oneOf) return; $[key] = $[key].oneOf.find(({ type }) => type === 'string'); }); + const { utilization } = $.GasPrices.items.properties; + Object.assign(utilization, { + oneOf: undefined, + ...(utilization.oneOf ?? []).find(({ type }) => type === 'string'), + }); reason: fix parsing of big numbers remove after fixing https://github.com/aeternity/aeternity/issues/3891 @@ -154,7 +159,7 @@ version: ^3.7.1 use-extension: '@autorest/typescript': ^6.0.15 '@autorest/modelerfour': ^4.27.0 -input-file: https://raw.githubusercontent.com/aeternity/aeternity/abc961d398e422c5d72f698a87ec471ffe8ff3ec/apps/aehttp/priv/oas3.yaml +input-file: https://raw.githubusercontent.com/aeternity/aeternity/51b236f8a04b349e080644c76ad8844b8b71151b/apps/aehttp/priv/oas3.yaml output-folder: ../../src/apis/node source-code-folder-path: . generator: typescript