Skip to content

Commit

Permalink
fix: calculate transaction fee (#1102)
Browse files Browse the repository at this point in the history
  • Loading branch information
Torres-ssf authored Jul 19, 2023
1 parent b151c80 commit 1b74426
Show file tree
Hide file tree
Showing 7 changed files with 274 additions and 28 deletions.
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

0 comments on commit 1b74426

Please sign in to comment.