Skip to content

Commit

Permalink
fix: validate status from estimation dry-run (#2291)
Browse files Browse the repository at this point in the history
  • Loading branch information
Torres-ssf authored May 17, 2024
1 parent 3f86778 commit d5116ce
Show file tree
Hide file tree
Showing 9 changed files with 108 additions and 71 deletions.
6 changes: 6 additions & 0 deletions .changeset/chilled-bats-visit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@fuel-ts/account": patch
"@fuel-ts/program": patch
---

fix: validate status from estimation dry-run
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ describe(__filename, () => {
gasLimit: 1,
})
.call()
).rejects.toThrow(/Gas limit '1' is lower than the required: /);
).rejects.toThrow('The transaction reverted with reason: "OutOfGas"');
// #endregion call-params-2
});

Expand Down
2 changes: 2 additions & 0 deletions packages/account/src/providers/operations.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ fragment transactionEstimatePredicatesFragment on Transaction {
}

fragment dryRunFailureStatusFragment on DryRunFailureStatus {
type: __typename
totalGas
totalFee
reason
Expand All @@ -102,6 +103,7 @@ fragment dryRunFailureStatusFragment on DryRunFailureStatus {
}

fragment dryRunSuccessStatusFragment on DryRunSuccessStatus {
type: __typename
totalGas
totalFee
programState {
Expand Down
41 changes: 37 additions & 4 deletions packages/account/src/providers/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,24 @@ import type {
} from './transaction-request';
import { transactionRequestify } from './transaction-request';
import type { TransactionResultReceipt } from './transaction-response';
import { TransactionResponse } from './transaction-response';
import { TransactionResponse, getDecodedLogs } from './transaction-response';
import { processGqlReceipt } from './transaction-summary/receipt';
import { calculateGasFee, getGasUsedFromReceipts, getReceiptsWithMissingData } from './utils';
import {
calculateGasFee,
extractTxError,
getGasUsedFromReceipts,
getReceiptsWithMissingData,
} from './utils';
import type { RetryOptions } from './utils/auto-retry-fetch';
import { autoRetryFetch } from './utils/auto-retry-fetch';
import { mergeQuantities } from './utils/merge-quantities';

const MAX_RETRIES = 10;

export type DryRunStatus = GqlDryRunFailureStatusFragment | GqlDryRunSuccessStatusFragment;
export type DryRunFailureStatusFragment = GqlDryRunFailureStatusFragment;
export type DryRunSuccessStatusFragment = GqlDryRunSuccessStatusFragment;

export type DryRunStatus = DryRunFailureStatusFragment | DryRunSuccessStatusFragment;

export type CallResult = {
receipts: TransactionResultReceipt[];
Expand Down Expand Up @@ -1123,8 +1131,11 @@ Supported fuel-core version: ${supportedVersion}.`
({ receipts, missingContractIds, outputVariables, dryRunStatus } =
await this.estimateTxDependencies(txRequestClone));

gasUsed = isScriptTransaction ? getGasUsedFromReceipts(receipts) : gasUsed;
if (dryRunStatus && 'reason' in dryRunStatus) {
throw this.extractDryRunError(txRequestClone, receipts, dryRunStatus);
}

gasUsed = getGasUsedFromReceipts(receipts);
txRequestClone.gasLimit = gasUsed;

({ maxFee, maxGas, minFee, minGas, gasPrice } = await this.estimateTxGasAndFee({
Expand Down Expand Up @@ -1705,4 +1716,26 @@ Supported fuel-core version: ${supportedVersion}.`

return relayedTransactionStatus;
}

private extractDryRunError(
transactionRequest: ScriptTransactionRequest,
receipts: TransactionResultReceipt[],
dryRunStatus: DryRunStatus
): FuelError {
const status = dryRunStatus as DryRunFailureStatusFragment;
let logs: unknown[] = [];
if (transactionRequest.abis) {
logs = getDecodedLogs(
receipts,
transactionRequest.abis.main,
transactionRequest.abis.otherContractsAbis
);
}

return extractTxError({
logs,
receipts,
statusReason: status.reason,
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -268,15 +268,14 @@ export class TransactionResponse {
transactionResult.logs = logs;
}

if (transactionResult.isStatusFailure) {
const {
receipts,
gqlTransaction: { status },
} = transactionResult;
const { gqlTransaction, receipts } = transactionResult;

if (gqlTransaction.status?.type === 'FailureStatus') {
const { reason } = gqlTransaction.status;

throw extractTxError({
receipts,
status,
statusReason: reason,
logs,
});
}
Expand Down
24 changes: 10 additions & 14 deletions packages/account/src/providers/utils/extract-tx-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,21 @@ import {
PANIC_DOC_URL,
} from '@fuel-ts/transactions/configs';

import type { GqlTransactionStatusFragment } from '../__generated__/operations';
import type { TransactionResultReceipt } from '../transaction-response';
import type { FailureStatus } from '../transaction-summary';

/**
* Assembles an error message for a panic status.
* @param status - The transaction failure status.
* @returns The error message.
*/
export const assemblePanicError = (status: FailureStatus) => {
let errorMessage = `The transaction reverted with reason: "${status.reason}".`;
const reason = status.reason;
export const assemblePanicError = (statusReason: string) => {
let errorMessage = `The transaction reverted with reason: "${statusReason}".`;

if (PANIC_REASONS.includes(status.reason)) {
errorMessage = `${errorMessage}\n\nYou can read more about this error at:\n\n${PANIC_DOC_URL}#variant.${status.reason}`;
if (PANIC_REASONS.includes(statusReason)) {
errorMessage = `${errorMessage}\n\nYou can read more about this error at:\n\n${PANIC_DOC_URL}#variant.${statusReason}`;
}

return { errorMessage, reason };
return { errorMessage, reason: statusReason };
};

/** @hidden */
Expand Down Expand Up @@ -101,8 +98,8 @@ export const assembleRevertError = (

interface IExtractTxError {
receipts: Array<TransactionResultReceipt>;
status?: GqlTransactionStatusFragment | null;
logs: Array<unknown>;
statusReason: string;
}

/**
Expand All @@ -111,15 +108,14 @@ interface IExtractTxError {
* @returns The FuelError object.
*/
export const extractTxError = (params: IExtractTxError): FuelError => {
const { receipts, status, logs } = params;
const { receipts, statusReason, logs } = params;

const isPanic = receipts.some(({ type }) => type === ReceiptType.Panic);
const isRevert = receipts.some(({ type }) => type === ReceiptType.Revert);

const { errorMessage, reason } =
status?.type === 'FailureStatus' && isPanic
? assemblePanicError(status)
: assembleRevertError(receipts, logs);
const { errorMessage, reason } = isPanic
? assemblePanicError(statusReason)
: assembleRevertError(receipts, logs);

const metadata = {
logs,
Expand Down
71 changes: 27 additions & 44 deletions packages/fuel-gauge/src/dry-run-multiple-txs.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { GqlDryRunFailureStatus } from '@fuel-ts/account/dist/providers/__generated__/operations';
import { generateTestWallet } from '@fuel-ts/account/test-utils';
import type {
CallResult,
DryRunStatus,
EstimateTxDependenciesReturns,
TransactionResultReceipt,
WalletUnlocked,
} from 'fuels';
import { ContractFactory, FUEL_NETWORK_URL, Provider, Wallet, bn } from 'fuels';
import { ContractFactory, FUEL_NETWORK_URL, Provider, Wallet } from 'fuels';

import { FuelGaugeProjectsEnum, getFuelGaugeForcProject } from '../test/fixtures';

Expand Down Expand Up @@ -83,38 +83,31 @@ describe('dry-run-multiple-txs', () => {
const request1 = await revertContract.functions
.validate_inputs(10, 0)
.txParams({
gasLimit: 5000,
maxFee: 1126,
gasLimit: 100_000,
maxFee: 120_000,
})
.getTransactionRequest();

const request2 = await revertContract.functions
.validate_inputs(0, 1)
.txParams({
gasLimit: 5000,
maxFee: 1126,
gasLimit: 100_000,
maxFee: 120_000,
})
.getTransactionRequest();

const request3 = await revertContract.functions
.validate_inputs(0, 100)
.txParams({
gasLimit: 5000,
maxFee: 1126,
gasLimit: 100_000,
maxFee: 120_000,
})
.getTransactionRequest();

request1.addResources(resources);
request2.addResources(resources);
request3.addResources(resources);

request1.maxFee = bn(1380);
request1.gasLimit = bn(26775);
request2.maxFee = bn(1380);
request2.gasLimit = bn(26825);
request3.maxFee = bn(1380);
request3.gasLimit = bn(26825);

const dryRunSpy = vi.spyOn(provider.operations, 'dryRun');

const estimatedRequests = await provider.dryRunMultipleTransactions(
Expand All @@ -128,39 +121,42 @@ describe('dry-run-multiple-txs', () => {
receipts: expect.any(Array<TransactionResultReceipt>),
dryRunStatus: {
reason: expect.any(String),
type: 'DryRunFailureStatus',
programState: {
data: expect.any(String),
returnType: 'REVERT',
},
totalFee: expect.any(String),
totalGas: expect.any(String),
} as GqlDryRunFailureStatus,
} as DryRunStatus,
});

expect(estimatedRequests[1]).toStrictEqual<CallResult>({
receipts: expect.any(Array<TransactionResultReceipt>),
dryRunStatus: {
reason: expect.any(String),
type: 'DryRunFailureStatus',
programState: {
data: expect.any(String),
returnType: 'REVERT',
},
totalFee: expect.any(String),
totalGas: expect.any(String),
} as GqlDryRunFailureStatus,
} as DryRunStatus,
});

expect(estimatedRequests[2]).toStrictEqual<CallResult>({
receipts: expect.any(Array<TransactionResultReceipt>),
dryRunStatus: {
reason: expect.any(String),
type: 'DryRunFailureStatus',
programState: {
data: expect.any(String),
returnType: 'REVERT',
},
totalFee: expect.any(String),
totalGas: expect.any(String),
} as GqlDryRunFailureStatus,
} as DryRunStatus,
});
});

Expand Down Expand Up @@ -190,8 +186,8 @@ describe('dry-run-multiple-txs', () => {
const request2 = await multiTokenContract.functions
.mint_to_addresses(addresses, subId, 1000)
.txParams({
gasLimit: 60000,
maxFee: 1862,
gasLimit: 500_000,
maxFee: 520_000,
variableOutputs: 0,
})
.getTransactionRequest();
Expand All @@ -200,8 +196,8 @@ describe('dry-run-multiple-txs', () => {
const request3 = await multiTokenContract.functions
.mint_to_addresses(addresses, subId, 2000)
.txParams({
gasLimit: 60000,
maxFee: 1862,
gasLimit: 500_000,
maxFee: 520_000,
variableOutputs: 1,
})
.getTransactionRequest();
Expand All @@ -210,8 +206,8 @@ describe('dry-run-multiple-txs', () => {
const request4 = await revertContract.functions
.failed_transfer_revert()
.txParams({
gasLimit: 60000,
maxFee: 1862,
gasLimit: 500_000,
maxFee: 520_000,
variableOutputs: 1,
})
.getTransactionRequest();
Expand All @@ -220,8 +216,8 @@ describe('dry-run-multiple-txs', () => {
const request5 = await logContract.functions
.test_log_from_other_contract(10, logOtherContract.id.toB256())
.txParams({
gasLimit: 60000,
maxFee: 1862,
gasLimit: 500_000,
maxFee: 520_000,
})
.getTransactionRequest();

Expand All @@ -230,23 +226,6 @@ describe('dry-run-multiple-txs', () => {
* requests using the dry run flag utxo_validation: false)
*/

const cost1 = await wallet.provider.getTransactionCost(request2);
const cost2 = await wallet.provider.getTransactionCost(request3);
const cost3 = await wallet.provider.getTransactionCost(request4);
const cost4 = await wallet.provider.getTransactionCost(request5);

request2.gasLimit = cost1.gasUsed;
request2.maxFee = cost1.maxFee;

request3.gasLimit = cost2.gasUsed;
request3.maxFee = cost2.maxFee;

request4.maxFee = cost3.maxFee;
request4.gasLimit = cost3.gasUsed;

request5.maxFee = cost4.maxFee;
request5.gasLimit = cost4.gasUsed;

request1.addResources(resources);
request2.addResources(resources);
request3.addResources(resources);
Expand Down Expand Up @@ -276,9 +255,10 @@ describe('dry-run-multiple-txs', () => {
missingContractIds: [],
outputVariables: 3,
dryRunStatus: {
programState: expect.any(Object),
type: 'DryRunSuccessStatus',
totalFee: expect.any(String),
totalGas: expect.any(String),
programState: expect.any(Object),
},
});

Expand All @@ -288,6 +268,7 @@ describe('dry-run-multiple-txs', () => {
missingContractIds: [],
outputVariables: 2,
dryRunStatus: {
type: 'DryRunSuccessStatus',
programState: expect.any(Object),
totalFee: expect.any(String),
totalGas: expect.any(String),
Expand All @@ -300,6 +281,7 @@ describe('dry-run-multiple-txs', () => {
missingContractIds: [],
outputVariables: 0,
dryRunStatus: {
type: 'DryRunFailureStatus',
reason: 'TransferZeroCoins',
programState: expect.any(Object),
totalFee: expect.any(String),
Expand All @@ -313,6 +295,7 @@ describe('dry-run-multiple-txs', () => {
missingContractIds: [logOtherContract.id.toB256()],
outputVariables: 0,
dryRunStatus: {
type: 'DryRunSuccessStatus',
programState: expect.any(Object),
totalFee: expect.any(String),
totalGas: expect.any(String),
Expand Down
Loading

0 comments on commit d5116ce

Please sign in to comment.