Skip to content

Commit

Permalink
feat(tx-builder): get actual gas price from node
Browse files Browse the repository at this point in the history
  • Loading branch information
davidyuk committed Mar 12, 2024
1 parent b1a5570 commit 09d19bf
Show file tree
Hide file tree
Showing 16 changed files with 503 additions and 61 deletions.
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 @@ 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;
}
};

Expand All @@ -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);

Expand All @@ -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);
});
}

/**
Expand All @@ -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<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 @@ 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}`);
Expand All @@ -163,9 +185,12 @@ export default {

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

0 comments on commit 09d19bf

Please sign in to comment.