-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'master' into db/feat/support-v1-encoding
- Loading branch information
Showing
28 changed files
with
496 additions
and
562 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
81 changes: 31 additions & 50 deletions
81
apps/docs-snippets/src/guide/errors/debugging-revert-errors.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
133 changes: 133 additions & 0 deletions
133
packages/account/src/providers/utils/extract-tx-error.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) => { | ||
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 = ( | ||
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 => { | ||
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); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.