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

fix: calculate transaction fee #1102

Merged
merged 10 commits into from
Jul 19, 2023
5 changes: 5 additions & 0 deletions .changeset/serious-pans-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fuel-ts/providers": minor
---

Fix incorrect gasUsed and fee calculation in calculateTransactionFee function
7 changes: 7 additions & 0 deletions packages/providers/src/operations.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,13 @@ query getInfo {
nodeVersion
minGasPrice
}
chain {
consensusParameters {
gasPerByte
maxGasPerTx
gasPriceFactor
}
}
}

query getChain {
Expand Down
31 changes: 23 additions & 8 deletions packages/providers/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
ReceiptCoder,
TransactionCoder,
} from '@fuel-ts/transactions';
import { MAX_GAS_PER_TX } from '@fuel-ts/transactions/configs';
import { GraphQLClient } from 'graphql-request';
import cloneDeep from 'lodash.clonedeep';

Expand Down Expand Up @@ -102,9 +101,12 @@ export type ChainInfo = {
/**
* Node information
*/
export type NodeInfo = {
export type NodeInfoAndConsensusParameters = {
minGasPrice: BN;
nodeVersion: string;
gasPerByte: BN;
gasPriceFactor: BN;
maxGasPerTx: BN;
};

// #region cost-estimation-1
Expand Down Expand Up @@ -172,9 +174,15 @@ const processGqlChain = (chain: GqlChainInfoFragmentFragment): ChainInfo => {
};
};

const processNodeInfo = (nodeInfo: GqlGetInfoQuery['nodeInfo']) => ({
const processNodeInfoAndConsensusParameters = (
nodeInfo: GqlGetInfoQuery['nodeInfo'],
consensusParameters: GqlGetInfoQuery['chain']['consensusParameters']
) => ({
minGasPrice: bn(nodeInfo.minGasPrice),
nodeVersion: nodeInfo.nodeVersion,
gasPerByte: bn(consensusParameters.gasPerByte),
gasPriceFactor: bn(consensusParameters.gasPriceFactor),
maxGasPerTx: bn(consensusParameters.maxGasPerTx),
});

/**
Expand Down Expand Up @@ -280,9 +288,9 @@ export default class Provider {
/**
* Returns node information
*/
async getNodeInfo(): Promise<NodeInfo> {
const { nodeInfo } = await this.operations.getInfo();
return processNodeInfo(nodeInfo);
async getNodeInfo(): Promise<NodeInfoAndConsensusParameters> {
const { nodeInfo, chain } = await this.operations.getInfo();
return processNodeInfoAndConsensusParameters(nodeInfo, chain.consensusParameters);
}

/**
Expand Down Expand Up @@ -487,22 +495,29 @@ export default class Provider {
tolerance: number = 0.2
): Promise<TransactionCost> {
const transactionRequest = transactionRequestify(cloneDeep(transactionRequestLike));
const { minGasPrice } = await this.getNodeInfo();
const { minGasPrice, gasPerByte, gasPriceFactor, maxGasPerTx } = await this.getNodeInfo();
const gasPrice = max(transactionRequest.gasPrice, minGasPrice);
const margin = 1 + tolerance;

// Set gasLimit to the maximum of the chain
// and gasPrice to 0 for measure
// Transaction without arrive to OutOfGas
transactionRequest.gasLimit = MAX_GAS_PER_TX;
transactionRequest.gasLimit = maxGasPerTx;
transactionRequest.gasPrice = bn(0);

// Execute dryRun not validated transaction to query gasUsed
const { receipts } = await this.call(transactionRequest);
const transaction = transactionRequest.toTransaction();

const { gasUsed, fee } = calculateTransactionFee({
gasPrice,
receipts,
margin,
gasPerByte,
gasPriceFactor,
transactionBytes: transactionRequest.toTransactionBytes(),
transactionType: transactionRequest.type,
transactionWitnesses: transaction.witnesses,
});

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
ReceiptScriptResult,
ReceiptMessageOut,
Transaction,
TransactionCreate,
} from '@fuel-ts/transactions';
import { TransactionCoder, ReceiptType, ReceiptCoder } from '@fuel-ts/transactions';

Expand Down Expand Up @@ -143,9 +144,16 @@ export class TransactionResponse {
}
case 'FailureStatus': {
const receipts = transactionWithReceipts.receipts!.map(processGqlReceipt);

const decodedTransaction =
this.decodeTransaction<TTransactionType>(transactionWithReceipts);

const { gasUsed, fee } = calculateTransactionFee({
receipts,
gasPrice: bn(transactionWithReceipts?.gasPrice),
transactionBytes: arrayify(transactionWithReceipts.rawPayload),
transactionType: decodedTransaction.type,
transactionWitnesses: (<TransactionCreate>decodedTransaction).witnesses || [],
});

this.gasUsed = gasUsed;
Expand All @@ -157,14 +165,21 @@ export class TransactionResponse {
time: transactionWithReceipts.status.time,
gasUsed,
fee,
transaction: this.decodeTransaction(transactionWithReceipts),
transaction: decodedTransaction,
};
}
case 'SuccessStatus': {
const receipts = transactionWithReceipts.receipts?.map(processGqlReceipt) || [];

const decodedTransaction =
this.decodeTransaction<TTransactionType>(transactionWithReceipts);

const { gasUsed, fee } = calculateTransactionFee({
receipts,
gasPrice: bn(transactionWithReceipts?.gasPrice),
transactionBytes: arrayify(transactionWithReceipts.rawPayload),
transactionType: decodedTransaction.type,
transactionWitnesses: (<TransactionCreate>decodedTransaction).witnesses || [],
});

return {
Expand All @@ -175,7 +190,7 @@ export class TransactionResponse {
time: transactionWithReceipts.status.time,
gasUsed,
fee,
transaction: this.decodeTransaction(transactionWithReceipts),
transaction: decodedTransaction,
};
}
default: {
Expand Down
143 changes: 143 additions & 0 deletions packages/providers/src/utils/fee.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { BN } from '@fuel-ts/math';
import { ReceiptType, type Witness } from '@fuel-ts/transactions';

import type { TransactionResultReceipt } from '../transaction-response';

import {
calculatePriceWithFactor,
getGasUsedForContractCreated,
getGasUsedFromReceipts,
} from './fee';

describe(__filename, () => {
describe('calculatePriceWithFactor', () => {
it('should correctly calculate the price with factor', () => {
const gasUsed = new BN(10);
const gasPrice = new BN(2);
const priceFactor = new BN(5);

const result = calculatePriceWithFactor(gasUsed, gasPrice, priceFactor);

expect(result.toNumber()).toEqual(4); // ceil(10 / 5) * 2 = 4
});

it('should correctly round up the result', () => {
const gasUsed = new BN(11);
const gasPrice = new BN(2);
const priceFactor = new BN(5);

const result = calculatePriceWithFactor(gasUsed, gasPrice, priceFactor);

expect(result.toNumber()).toEqual(6); // ceil(11 / 5) * 2 = 6
});
});

describe('getGasUsedForContractCreated', () => {
it('should calculate gas used for contract created correctly', () => {
const transactionBytes = new Uint8Array([0, 1, 2, 3, 4, 5]);
const gasPerByte = new BN(1);
const gasPriceFactor = new BN(2);
const transactionWitnesses: Witness[] = [{ dataLength: 2, data: 'data' }];

const result = getGasUsedForContractCreated({
transactionBytes,
gasPerByte,
gasPriceFactor,
transactionWitnesses,
});

expect(result.toNumber()).toEqual(2); // (6-2)*1/2 = 2
});

it('should handle an empty witnesses array', () => {
const transactionBytes = new Uint8Array([0, 1, 2, 3, 4, 5]);
const gasPerByte = new BN(1);
const gasPriceFactor = new BN(2);
const transactionWitnesses: Witness[] = [];

const result = getGasUsedForContractCreated({
transactionBytes,
gasPerByte,
gasPriceFactor,
transactionWitnesses,
});

expect(result.toNumber()).toEqual(3); // 6*1/2 = 3
});

it('should round up the result', () => {
const transactionBytes = new Uint8Array([0, 1, 2]);
const gasPerByte = new BN(1);
const gasPriceFactor = new BN(2);
const transactionWitnesses: Witness[] = [];

const result = getGasUsedForContractCreated({
transactionBytes,
gasPerByte,
gasPriceFactor,
transactionWitnesses,
});

expect(result.toNumber()).toEqual(2); // 3*1/2 = 1.5 which rounds up to 2
});
});

describe('getGasUsedFromReceipts', () => {
it('should return correct total gas used from ScriptResult receipts', () => {
const receipts: Array<TransactionResultReceipt> = [
{
type: ReceiptType.Return,
id: '0xbebd3baab326f895289ecbd4210cf886ce41952316441ae4cac35f00f0e882a6',
val: new BN(1),
pc: new BN(2),
is: new BN(3),
},
{
type: ReceiptType.ScriptResult,
result: new BN(4),
gasUsed: new BN(5),
},
{
type: ReceiptType.ScriptResult,
result: new BN(6),
gasUsed: new BN(7),
},
];

const result = getGasUsedFromReceipts(receipts);

expect(result.toNumber()).toEqual(12); // 5 + 7 = 12
});

it('should return zero if there are no ScriptResult receipts', () => {
const receipts: Array<TransactionResultReceipt> = [
{
type: ReceiptType.Return,
id: '0xbebd3baab326f895289ecbd4210cf886ce41952316441ae4cac35f00f0e882a6',
val: new BN(1),
pc: new BN(2),
is: new BN(3),
},
{
type: ReceiptType.Return,
id: '0xa703b26833939dabc41d3fcaefa00e62cee8e1ac46db37e0fa5d4c9fe30b4132',
val: new BN(4),
pc: new BN(5),
is: new BN(6),
},
];

const result = getGasUsedFromReceipts(receipts);

expect(result.toNumber()).toEqual(0);
});

it('should return zero if the receipts array is empty', () => {
const receipts: Array<TransactionResultReceipt> = [];

const result = getGasUsedFromReceipts(receipts);

expect(result.toNumber()).toEqual(0);
});
});
});
Loading
Loading