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

chore!: enhance TX error handling and message formatting #1895

Merged
merged 53 commits into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
11a2139
add signal for assert_ne
Torres-ssf Mar 19, 2024
47f39fc
update list of panic reasons
Torres-ssf Mar 19, 2024
c886859
wip: implemented extractTxError
Torres-ssf Mar 19, 2024
c49321c
using extractTxError
Torres-ssf Mar 19, 2024
ef6e32c
Merge branch 'master' into st/chore/refact-script-call-errors
Torres-ssf Mar 19, 2024
40cf105
using proper error code
Torres-ssf Mar 19, 2024
a492b4c
avoid throwing generic errors when tx fails
Torres-ssf Mar 20, 2024
57952c7
remove error removed from Sway
Torres-ssf Mar 20, 2024
15d40ea
remove deprecated test
Torres-ssf Mar 20, 2024
8ba4dbb
lint error message
Torres-ssf Mar 20, 2024
3c2fc02
avoid throwing meaningless TX revert error
Torres-ssf Mar 20, 2024
e658f05
remove use of FAILED_SEND_MESSAGE_SIGNAL
Torres-ssf Mar 20, 2024
69dc7ab
ajusting test
Torres-ssf Mar 20, 2024
2855562
ajusting revert test for dryRun and simulate
Torres-ssf Mar 20, 2024
3938046
ajusting revert errors tests
Torres-ssf Mar 20, 2024
78c93fe
add function to call-test-contract project
Torres-ssf Mar 20, 2024
9e870db
fix skipped test
Torres-ssf Mar 20, 2024
733447e
ajusting error
Torres-ssf Mar 20, 2024
432a00b
linting
Torres-ssf Mar 20, 2024
9a08f77
refact code
Torres-ssf Mar 20, 2024
b10b3a9
move PANIC_REASONS to transactions package
Torres-ssf Mar 20, 2024
52071c9
remove config exports from program package
Torres-ssf Mar 20, 2024
342e8be
move extractTxError to provider utils
Torres-ssf Mar 20, 2024
1f735a5
remove code related to ScriptResultDecoderError
Torres-ssf Mar 20, 2024
fb1cb68
Merge branch 'master' into st/chore/refact-tx-error-assembling
Torres-ssf Mar 25, 2024
90642e2
Merge branch 'master' into st/chore/refact-tx-error-assembling
Torres-ssf Mar 27, 2024
b546ec3
ajusting code snippets for debbuging revert errors page
Torres-ssf Mar 27, 2024
e8971cc
refact test suite for doc snippets
Torres-ssf Mar 27, 2024
680960d
remove unused code snippet error
Torres-ssf Mar 27, 2024
7eea655
add more functions to revert error contract
Torres-ssf Mar 27, 2024
028d97d
making some ajustments on extractTxError
Torres-ssf Mar 27, 2024
9aee7a2
ensure tx revert erros are also thrown on waitForResult
Torres-ssf Mar 27, 2024
8ec132b
adding more test cases
Torres-ssf Mar 27, 2024
aa4a543
update changeset
Torres-ssf Mar 27, 2024
38d64d7
refact code
Torres-ssf Mar 28, 2024
3e07a3b
remove redudant code
Torres-ssf Mar 28, 2024
d278fa0
fixing error messages
Torres-ssf Mar 28, 2024
36adb15
add JSDoc
Torres-ssf Mar 28, 2024
7905d3c
Merge branch 'master' into st/chore/refact-tx-error-assembling
Torres-ssf Mar 28, 2024
c57fbe1
Merge branch 'master' into st/chore/refact-tx-error-assembling
Torres-ssf Mar 28, 2024
ebbf095
Merge branch 'master' into st/chore/refact-tx-error-assembling
danielbate Mar 29, 2024
0476c96
Merge branch 'master' into st/chore/refact-tx-error-assembling
Torres-ssf Mar 29, 2024
4d82a06
Merge branch 'master' into st/chore/refact-tx-error-assembling
arboleya Mar 29, 2024
117fb76
Merge branch 'master' into st/chore/refact-tx-error-assembling
danielbate Apr 1, 2024
1cf4f4c
Merge branch 'master' into st/chore/refact-tx-error-assembling
Torres-ssf Apr 1, 2024
e892230
Merge branch 'master' into st/chore/refact-tx-error-assembling
Torres-ssf Apr 2, 2024
6c3b87a
add metadata to FuelError
Torres-ssf Apr 3, 2024
c28bbeb
use metadata when throwing TX error
Torres-ssf Apr 3, 2024
be1b342
update expectToThrowFuelError
Torres-ssf Apr 3, 2024
4a639de
update error cases
Torres-ssf Apr 3, 2024
2119afa
fixing tests
Torres-ssf Apr 3, 2024
2fd6932
Merge branch 'master' into st/chore/refact-tx-error-assembling
Torres-ssf Apr 4, 2024
fcf5f4b
Merge branch 'master' into st/chore/refact-tx-error-assembling
Dhaiwat10 Apr 4, 2024
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
8 changes: 8 additions & 0 deletions .changeset/popular-hairs-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@fuel-ts/account": minor
"fuels": minor
"@fuel-ts/program": minor
"@fuel-ts/transactions": minor
---

chore!: enhance TX error handling and message formatting
Original file line number Diff line number Diff line change
@@ -1,56 +1,37 @@
import { generateTestWallet } from '@fuel-ts/account/test-utils';
import { BaseAssetId, FUEL_NETWORK_URL, Provider, Script } from 'fuels';

import {
DocSnippetProjectsEnum,
getDocsSnippetsForcProject,
} from '../../../test/fixtures/forc-projects';
import { DocSnippetProjectsEnum } from '../../../test/fixtures/forc-projects';
import { createAndDeployContractFromProject } from '../../utils';

/**
* @group node
*/
test('logs out custom require messages for error enums when tx reverts', async () => {
const contract = await createAndDeployContractFromProject(DocSnippetProjectsEnum.REVERT_ERRORS);

// #region revert-errors-4
expect(() => contract.functions.test_function_with_custom_error().call()).rejects.toThrow(
'The script reverted with reason RequireFailed. (Reason: "InvalidInput")'
);
// #endregion revert-errors-4
});

test('logs out custom require messages for require statements using str array when tx reverts', async () => {
const contract = await createAndDeployContractFromProject(DocSnippetProjectsEnum.REVERT_ERRORS);

// #region revert-errors-7
expect(() => contract.functions.test_function_with_str_array_message().call()).rejects.toThrow(
'The script reverted with reason RequireFailed. (Reason: "This is also a revert error")'
);
// #endregion revert-errors-7
});

test('logs out a generic error message for require statements with a simple string message', async () => {
const contract = await createAndDeployContractFromProject(DocSnippetProjectsEnum.REVERT_ERRORS);

// #region revert-errors-5
expect(() => contract.functions.test_function().call()).rejects.toThrow(
'String slices can not be decoded from logs. Convert the slice to `str[N]` with `__to_str_array`'
);
// #endregion revert-errors-5
});

test('logs out custom require messages for script calls', async () => {
const { binHexlified, abiContents } = getDocsSnippetsForcProject(
DocSnippetProjectsEnum.REVERT_ERRORS_SCRIPT
);

const provider = await Provider.create(FUEL_NETWORK_URL);
const wallet = await generateTestWallet(provider, [[1_000_000, BaseAssetId]]);

const script = new Script(binHexlified, abiContents, wallet);

expect(() => script.functions.main().call()).rejects.toThrow(
'The script reverted with reason RequireFailed. (Reason: "This is a revert error")'
);
describe(__filename, () => {
it('logs out custom require messages for error enums when tx reverts', async () => {
const contract = await createAndDeployContractFromProject(DocSnippetProjectsEnum.REVERT_ERRORS);

// #region revert-errors-4
expect(() => contract.functions.test_function_with_custom_error().call()).rejects.toThrow(
'The transaction reverted because a "require" statement has thrown "InvalidInput".'
);
// #endregion revert-errors-4
});

it('logs out custom require messages for require statements using str array when tx reverts', async () => {
const contract = await createAndDeployContractFromProject(DocSnippetProjectsEnum.REVERT_ERRORS);

// #region revert-errors-7
expect(() => contract.functions.test_function_with_str_array_message().call()).rejects.toThrow(
'The transaction reverted because a "require" statement has thrown "This is also a revert error".'
);
// #endregion revert-errors-7
});

it('logs out a generic error message for require statements with a simple string message', async () => {
const contract = await createAndDeployContractFromProject(DocSnippetProjectsEnum.REVERT_ERRORS);

// #region revert-errors-5
expect(() => contract.functions.test_function().call()).rejects.toThrow(
'String slices can not be decoded from logs. Convert the slice to `str[N]` with `__to_str_array`'
);
// #endregion revert-errors-5
});
});
2 changes: 1 addition & 1 deletion internal/check-imports/src/references.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ import { BN } from '@fuel-ts/math';
import { DEFAULT_PRECISION, DEFAULT_MIN_PRECISION } from '@fuel-ts/math/configs';
import { SparseMerkleTree, constructTree } from '@fuel-ts/merkle';
import { FunctionInvocationScope } from '@fuel-ts/program';
import { PANIC_REASONS } from '@fuel-ts/program/configs';
import { Script } from '@fuel-ts/script';
import { InputCoinCoder } from '@fuel-ts/transactions';
import { PANIC_REASONS } from '@fuel-ts/transactions/configs';
import { versions } from '@fuel-ts/versions';
import { runVersions } from '@fuel-ts/versions/cli';
// TODO: Add `launchNode` and `launchNodeAndGetWallets` here
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,8 @@ import type Provider from '../provider';
import type { JsonAbisFromAllCalls } from '../transaction-request';
import { assembleTransactionSummary } from '../transaction-summary/assemble-transaction-summary';
import { processGqlReceipt } from '../transaction-summary/receipt';
import type {
TransactionSummary,
FailureStatus,
GqlTransaction,
AbiMap,
} from '../transaction-summary/types';
import type { TransactionSummary, GqlTransaction, AbiMap } from '../transaction-summary/types';
import { extractTxError } from '../utils';

import { getDecodedLogs } from './getDecodedLogs';

Expand Down Expand Up @@ -250,8 +246,10 @@ export class TransactionResponse {
...transactionSummary,
};

let logs: Array<unknown> = [];

if (this.abis) {
const logs = getDecodedLogs(
logs = getDecodedLogs(
transactionSummary.receipts,
this.abis.main,
this.abis.otherContractsAbis
Expand All @@ -260,6 +258,19 @@ export class TransactionResponse {
transactionResult.logs = logs;
}

if (transactionResult.isStatusFailure) {
nedsalk marked this conversation as resolved.
Show resolved Hide resolved
const {
receipts,
gqlTransaction: { status },
} = transactionResult;

throw extractTxError({
receipts,
status,
logs,
});
}

return transactionResult;
}

Expand All @@ -271,15 +282,6 @@ export class TransactionResponse {
async wait<TTransactionType = void>(
contractsAbiMap?: AbiMap
): Promise<TransactionResult<TTransactionType>> {
const result = await this.waitForResult<TTransactionType>(contractsAbiMap);

if (result.isStatusFailure) {
throw new FuelError(
ErrorCode.TRANSACTION_FAILED,
`Transaction failed: ${(<FailureStatus>result.gqlTransaction.status).reason}`
);
}

return result;
return this.waitForResult<TTransactionType>(contractsAbiMap);
}
}
133 changes: 133 additions & 0 deletions packages/account/src/providers/utils/extract-tx-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { ErrorCode, FuelError } from '@fuel-ts/errors';
import { bn } from '@fuel-ts/math';
import type { ReceiptRevert } from '@fuel-ts/transactions';
import { ReceiptType } from '@fuel-ts/transactions';
import {
FAILED_REQUIRE_SIGNAL,
FAILED_ASSERT_EQ_SIGNAL,
FAILED_ASSERT_NE_SIGNAL,
FAILED_ASSERT_SIGNAL,
FAILED_TRANSFER_TO_ADDRESS_SIGNAL,
PANIC_REASONS,
PANIC_DOC_URL,
} from '@fuel-ts/transactions/configs';

import type { GqlTransactionStatusFragmentFragment } 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) => {
Torres-ssf marked this conversation as resolved.
Show resolved Hide resolved
let errorMessage = `The transaction reverted with reason: "${status.reason}".`;
const reason = status.reason;

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}`;
}

return { errorMessage, reason };
};

/** @hidden */
const stringify = (obj: unknown) => JSON.stringify(obj, null, 2);

/**
* Assembles an error message for a revert status.
* @param receipts - The transaction result processed receipts.
* @param logs - The transaction decoded logs.
* @returns The error message.
*/
export const assembleRevertError = (
Torres-ssf marked this conversation as resolved.
Show resolved Hide resolved
receipts: Array<TransactionResultReceipt>,
logs: Array<unknown>
) => {
let errorMessage = 'The transaction reverted with an unknown reason.';

const revertReceipt = receipts.find(({ type }) => type === ReceiptType.Revert) as ReceiptRevert;
let reason = '';

if (revertReceipt) {
const reasonHex = bn(revertReceipt.val).toHex();

switch (reasonHex) {
case FAILED_REQUIRE_SIGNAL: {
reason = 'require';
errorMessage = `The transaction reverted because a "require" statement has thrown ${
logs.length ? stringify(logs[0]) : 'an error.'
}.`;
break;
}

case FAILED_ASSERT_EQ_SIGNAL: {
const sufix =
logs.length >= 2 ? ` comparing ${stringify(logs[1])} and ${stringify(logs[0])}.` : '.';

reason = 'assert_eq';
errorMessage = `The transaction reverted because of an "assert_eq" statement${sufix}`;
break;
}

case FAILED_ASSERT_NE_SIGNAL: {
const sufix =
logs.length >= 2 ? ` comparing ${stringify(logs[1])} and ${stringify(logs[0])}.` : '.';

reason = 'assert_ne';
errorMessage = `The transaction reverted because of an "assert_ne" statement${sufix}`;
break;
}

case FAILED_ASSERT_SIGNAL:
reason = 'assert';
errorMessage = `The transaction reverted because an "assert" statement failed to evaluate to true.`;
break;

case FAILED_TRANSFER_TO_ADDRESS_SIGNAL:
reason = 'MissingOutputChange';
errorMessage = `The transaction reverted because it's missing an "OutputChange".`;
break;

default:
reason = 'unknown';
errorMessage = `The transaction reverted with an unknown reason: ${revertReceipt.val}`;
}
}

return { errorMessage, reason };
};

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

/**
* Extracts the transaction error and returns a FuelError object.
* @param IExtractTxError - The parameters for extracting the error.
* @returns The FuelError object.
*/
export const extractTxError = (params: IExtractTxError): FuelError => {
Torres-ssf marked this conversation as resolved.
Show resolved Hide resolved
const { receipts, status, 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 metadata = {
logs,
receipts,
panic: isPanic,
revert: isRevert,
reason,
};

return new FuelError(ErrorCode.SCRIPT_REVERTED, errorMessage, metadata);
};
1 change: 1 addition & 0 deletions packages/account/src/providers/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './block-explorer';
export * from './gas';
export * from './json';
export * from './sleep';
export * from './extract-tx-error';
5 changes: 3 additions & 2 deletions packages/errors/src/fuel-error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ it('converts error to plain object', () => {
const code = FuelError.CODES.PARSE_FAILED;
const name = 'FuelError';
const message = 'It happens';
const err = new FuelError(code, message);
expect(err.toObject()).toEqual({ code, name, message, VERSIONS: err.VERSIONS });
const metadata = { name: 'FuelLabs' };
const err = new FuelError(code, message, metadata);
expect(err.toObject()).toEqual({ code, name, message, VERSIONS: err.VERSIONS, metadata });
});
8 changes: 5 additions & 3 deletions packages/errors/src/fuel-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ErrorCode } from './error-codes';
export class FuelError extends Error {
static readonly CODES = ErrorCode;
readonly VERSIONS = versions;
readonly metadata: Record<string, unknown>;

static parse(e: unknown) {
const error = e as FuelError;
Expand All @@ -31,14 +32,15 @@ export class FuelError extends Error {

code: ErrorCode;

constructor(code: ErrorCode, message: string) {
constructor(code: ErrorCode, message: string, metadata: Record<string, unknown> = {}) {
super(message);
this.code = code;
this.name = 'FuelError';
this.metadata = metadata;
}

toObject() {
const { code, name, message, VERSIONS } = this;
return { code, name, message, VERSIONS };
const { code, name, message, metadata, VERSIONS } = this;
return { code, name, message, metadata, VERSIONS };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export const expectToThrowFuelError = async (
);
}

if (expectedError.metadata) {
expect(thrownError.metadata).toEqual(expect.objectContaining(expectedError.metadata));
}

expect(thrownError.name).toEqual('FuelError');
expect(thrownError).toMatchObject(expectedError);
};
Loading
Loading