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

feat(tx-builder): get actual gas price from node #1884

Merged
merged 1 commit into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
2 changes: 2 additions & 0 deletions docs/guides/batch-requests.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docs/transaction-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/AeSdkMethods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ class AeSdkMethods {
};
}

// TODO: omit onNode from options, because it is already in context
async buildTx(options: TxParamsAsync): Promise<Encoded.Transaction> {
return buildTxAsync({ ...this.getContext(), ...options });
}
Expand Down
31 changes: 20 additions & 11 deletions src/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ 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 = [
'time', 'gas', 'gasUsed', 'nameSalt',
'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 {
Expand Down Expand Up @@ -112,8 +112,6 @@ interface NodeInfo {
}

export default class Node extends (NodeTransformed as unknown as NodeTransformedApi) {
#networkIdPromise?: Promise<string | Error>;

/**
* @param url - Url for node API
* @param options - Options
Expand Down Expand Up @@ -145,25 +143,36 @@ 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'),
);
}
this.intAsString = true;
}

#cachedStatusPromise?: ReturnType<Node['getStatus']>;

async _getCachedStatus(): ReturnType<Node['getStatus']> {
this.#cachedStatusPromise ??= this.getStatus();
return this.#cachedStatusPromise;
}

// @ts-expect-error use code generation to create node class?
override async getStatus(
...args: Parameters<InstanceType<NodeTransformedApi>['getStatus']>
): ReturnType<InstanceType<NodeTransformedApi>['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<string> {
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<NodeInfo> {
Expand Down
2 changes: 1 addition & 1 deletion src/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/tx/builder/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
97 changes: 61 additions & 36 deletions src/tx/builder/field-types/fee.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -36,44 +39,41 @@
[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;
}
};

Expand All @@ -91,13 +91,13 @@
}

/**
* 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);

Expand All @@ -106,11 +106,10 @@
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);
});
}

/**
Expand All @@ -127,24 +126,45 @@
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<Int | undefined> {
if (value != null) return value;
if (onNode == null) {
throw new ArgumentError('onNode', 'provided (or provide `fee` instead)', onNode);

Check warning on line 156 in src/tx/builder/field-types/fee.ts

View check run for this annotation

Codecov / codecov/patch

src/tx/builder/field-types/fee.ts#L156

Added line #L156 was not covered by tests
}
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();
Expand All @@ -153,7 +173,9 @@
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}`);
Expand All @@ -163,9 +185,12 @@

serialize(
value: Parameters<typeof coinAmount.serialize>[0],
params: Parameters<typeof coinAmount.serialize>[1],
params: Parameters<typeof coinAmount.serialize>[1] & SerializeAettosParams,
options: { _canIncreaseFee?: boolean } & Parameters<typeof coinAmount.serialize>[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);
},
};
6 changes: 3 additions & 3 deletions src/tx/builder/field-types/gas-limit.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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 {
Expand Down
Loading