diff --git a/.changeset/rare-shrimps-clap.md b/.changeset/rare-shrimps-clap.md new file mode 100644 index 00000000000..86ccc51f001 --- /dev/null +++ b/.changeset/rare-shrimps-clap.md @@ -0,0 +1,6 @@ +--- +"@fuel-ts/account": patch +"@fuel-ts/errors": patch +--- + +feat: map 'not enough coins' error diff --git a/apps/demo-bun-fuels/src/bun.test.ts b/apps/demo-bun-fuels/src/bun.test.ts index 95d5e4722c8..15585de28a4 100644 --- a/apps/demo-bun-fuels/src/bun.test.ts +++ b/apps/demo-bun-fuels/src/bun.test.ts @@ -5,8 +5,8 @@ * It ensures that built code is fully working. */ -import { toHex, Wallet } from 'fuels'; -import { launchTestNode, safeExec } from 'fuels/test-utils'; +import { ErrorCode, FuelError, toHex, Wallet } from 'fuels'; +import { expectToThrowFuelError, launchTestNode } from 'fuels/test-utils'; import { Sample, SampleFactory } from './sway-programs-api'; @@ -78,11 +78,13 @@ describe('ExampleContract', () => { const contractInstance = new Sample(contract.id, unfundedWallet); - const { error } = await safeExec(() => - contractInstance.functions.return_input(1337).simulate() + await expectToThrowFuelError( + () => contractInstance.functions.return_input(1337).simulate(), + new FuelError( + ErrorCode.NOT_ENOUGH_FUNDS, + `The account(s) sending the transaction don't have enough funds to cover the transaction.` + ) ); - - expect((error).message).toMatch('not enough coins to fit the target'); }); it('should not throw when dry running via contract factory with wallet with no resources', async () => { diff --git a/apps/demo-fuels/src/index.test.ts b/apps/demo-fuels/src/index.test.ts index 53ca6802d6b..49e1e841f2c 100644 --- a/apps/demo-fuels/src/index.test.ts +++ b/apps/demo-fuels/src/index.test.ts @@ -5,8 +5,8 @@ * It ensures that built code is fully working. */ -import { toHex, Wallet } from 'fuels'; -import { launchTestNode, safeExec } from 'fuels/test-utils'; +import { ErrorCode, FuelError, toHex, Wallet } from 'fuels'; +import { expectToThrowFuelError, launchTestNode } from 'fuels/test-utils'; import { SampleFactory, Sample } from './sway-programs-api'; @@ -71,11 +71,13 @@ describe('ExampleContract', () => { const contractInstance = new Sample(contract.id, unfundedWallet); - const { error } = await safeExec(() => - contractInstance.functions.return_input(1337).simulate() + await expectToThrowFuelError( + () => contractInstance.functions.return_input(1337).simulate(), + new FuelError( + ErrorCode.NOT_ENOUGH_FUNDS, + `The account(s) sending the transaction don't have enough funds to cover the transaction.` + ) ); - - expect((error).message).toMatch('not enough coins to fit the target'); }); it('should not throw when dry running via contract factory with wallet with no resources', async () => { diff --git a/apps/demo-typegen/src/demo.test.ts b/apps/demo-typegen/src/demo.test.ts index b1e2d5276f2..f9b0c654d44 100644 --- a/apps/demo-typegen/src/demo.test.ts +++ b/apps/demo-typegen/src/demo.test.ts @@ -1,6 +1,6 @@ // #region Testing-in-ts-ts -import { toHex, Address, Wallet } from 'fuels'; -import { launchTestNode, safeExec } from 'fuels/test-utils'; +import { toHex, Address, Wallet, FuelError, ErrorCode } from 'fuels'; +import { expectToThrowFuelError, launchTestNode } from 'fuels/test-utils'; import storageSlots from '../demo-contract/out/release/demo-contract-storage_slots.json'; @@ -105,9 +105,13 @@ it('should throw when simulating via contract factory with wallet with no resour const { contract } = await waitForResult(); const contractInstance = new DemoContract(contract.id, unfundedWallet); - const { error } = await safeExec(() => contractInstance.functions.return_input(1337).simulate()); - - expect((error).message).toMatch('not enough coins to fit the target'); + await expectToThrowFuelError( + () => contractInstance.functions.return_input(1337).simulate(), + new FuelError( + ErrorCode.NOT_ENOUGH_FUNDS, + `The account(s) sending the transaction don't have enough funds to cover the transaction.` + ) + ); }); it('should not throw when dry running via contract factory with wallet with no resources', async () => { diff --git a/apps/docs-snippets/src/guide/predicates/send-and-spend-funds-from-predicates.test.ts b/apps/docs-snippets/src/guide/predicates/send-and-spend-funds-from-predicates.test.ts index 3908223e084..d6f9dbc64e2 100644 --- a/apps/docs-snippets/src/guide/predicates/send-and-spend-funds-from-predicates.test.ts +++ b/apps/docs-snippets/src/guide/predicates/send-and-spend-funds-from-predicates.test.ts @@ -108,7 +108,7 @@ describe('Send and Spend Funds from Predicates', () => { ); // #region send-and-spend-funds-from-predicates-6 - const errorMsg = 'not enough coins to fit the target'; + const errorMsg = `The account(s) sending the transaction don't have enough funds to cover the transaction.`; // #endregion send-and-spend-funds-from-predicates-6 expect((error).message).toMatch(errorMsg); diff --git a/apps/docs/src/guide/errors/index.md b/apps/docs/src/guide/errors/index.md index a0d612e7c7d..927962f80d4 100644 --- a/apps/docs/src/guide/errors/index.md +++ b/apps/docs/src/guide/errors/index.md @@ -264,6 +264,12 @@ When the Fuel Node info cache is empty; This is usually caused by not being conn Ensure that the provider has connected to a Fuel Node successfully. +### `NOT_ENOUGH_FUNDS` + +When the account sending the transaction does not have enough funds to cover the fee. + +Ensure that the account creating the transaction has been funded appropriately. + ### `TIMEOUT_EXCEEDED` When the timeout has been exceeded for a given operation. diff --git a/apps/docs/src/guide/wallets/wallet-transferring.md b/apps/docs/src/guide/wallets/wallet-transferring.md index 402343e5be0..9025df598c9 100644 --- a/apps/docs/src/guide/wallets/wallet-transferring.md +++ b/apps/docs/src/guide/wallets/wallet-transferring.md @@ -44,7 +44,6 @@ Here's an example demonstrating how to use `transferToContract`: Always remember to call the `waitForResult()` function on the transaction response. That ensures the transaction has been mined successfully before proceeding. - ## Transferring Assets To Multiple Wallets To transfer assets to multiple wallets, use the `Account.batchTransfer` method: @@ -53,9 +52,8 @@ To transfer assets to multiple wallets, use the `Account.batchTransfer` method: This section demonstrates additional examples of transferring assets between wallets and to contracts. - ## Checking Balances -Before transferring assets, ensure your wallet has sufficient funds. Attempting a transfer without enough funds will result in an error: `not enough coins to fit the target`. +Before you transfer assets, please make sure your wallet has enough funds. Attempting a transfer without enough funds will result in the error: `The transaction does not have enough funds to cover its execution.` -You can see how to check your balance at the [`checking-balances`](./checking-balances.md) page. \ No newline at end of file +You can see how to check your balance at the [`checking-balances`](./checking-balances.md) page. diff --git a/packages/account/src/account.test.ts b/packages/account/src/account.test.ts index b7290db6fed..3ba081c3912 100644 --- a/packages/account/src/account.test.ts +++ b/packages/account/src/account.test.ts @@ -523,11 +523,13 @@ describe('Account', () => { } // Test excludes the UTXO where the assetIdA gets added to the senders wallet - await expect( - user.getResourcesToSpend([[1, ASSET_A, 500_000]], { - utxos: [assetAUTXO.id], - }) - ).rejects.toThrow(/not enough coins to fit the target/); + await expectToThrowFuelError( + () => user.getResourcesToSpend([[1, ASSET_A, 500_000]], { utxos: [assetAUTXO.id] }), + new FuelError( + ErrorCode.NOT_ENOUGH_FUNDS, + `The account(s) sending the transaction don't have enough funds to cover the transaction.` + ) + ); }); it('can transfer multiple types of coins to multiple destinations', async () => { diff --git a/packages/account/src/providers/provider.ts b/packages/account/src/providers/provider.ts index bed4c0a2200..3fd417eb215 100644 --- a/packages/account/src/providers/provider.ts +++ b/packages/account/src/providers/provider.ts @@ -61,6 +61,7 @@ import { } from './utils'; import type { RetryOptions } from './utils/auto-retry-fetch'; import { autoRetryFetch } from './utils/auto-retry-fetch'; +import { handleGqlErrorMessage } from './utils/handle-gql-error-message'; const MAX_RETRIES = 10; @@ -605,11 +606,11 @@ Supported fuel-core version: ${supportedVersion}.` responseMiddleware: (response: GraphQLResponse | Error) => { if ('response' in response) { const graphQlResponse = response.response as GraphQLResponse; + if (Array.isArray(graphQlResponse?.errors)) { - throw new FuelError( - FuelError.CODES.INVALID_REQUEST, - graphQlResponse.errors.map((err: Error) => err.message).join('\n\n') - ); + for (const error of graphQlResponse.errors) { + handleGqlErrorMessage(error.message, error); + } } } }, diff --git a/packages/account/src/providers/utils/handle-gql-error-message.ts b/packages/account/src/providers/utils/handle-gql-error-message.ts new file mode 100644 index 00000000000..9f19a2f53b9 --- /dev/null +++ b/packages/account/src/providers/utils/handle-gql-error-message.ts @@ -0,0 +1,20 @@ +import { ErrorCode, FuelError } from '@fuel-ts/errors'; +import type { GraphQLError } from 'graphql'; + +export enum GqlErrorMessage { + NOT_ENOUGH_COINS = 'not enough coins to fit the target', +} + +export const handleGqlErrorMessage = (errorMessage: string, rawError: GraphQLError) => { + switch (errorMessage) { + case GqlErrorMessage.NOT_ENOUGH_COINS: + throw new FuelError( + ErrorCode.NOT_ENOUGH_FUNDS, + `The account(s) sending the transaction don't have enough funds to cover the transaction.`, + {}, + rawError + ); + default: + throw new FuelError(ErrorCode.INVALID_REQUEST, errorMessage); + } +}; diff --git a/packages/errors/src/fuel-error.test.ts b/packages/errors/src/fuel-error.test.ts index c5da961712c..7932421c34b 100644 --- a/packages/errors/src/fuel-error.test.ts +++ b/packages/errors/src/fuel-error.test.ts @@ -57,6 +57,6 @@ it('converts error to plain object', () => { message, VERSIONS: err.VERSIONS, metadata, - rawError: {}, + rawError: null, }); }); diff --git a/packages/errors/src/fuel-error.ts b/packages/errors/src/fuel-error.ts index 055f42a3348..3cd21ce7e71 100644 --- a/packages/errors/src/fuel-error.ts +++ b/packages/errors/src/fuel-error.ts @@ -37,7 +37,7 @@ export class FuelError extends Error { code: ErrorCode, message: string, metadata: Record = {}, - rawError: unknown = {} + rawError: unknown = null ) { super(message); this.code = code; diff --git a/packages/fuel-gauge/src/contract.test.ts b/packages/fuel-gauge/src/contract.test.ts index 8a08569d97e..ceddce13951 100644 --- a/packages/fuel-gauge/src/contract.test.ts +++ b/packages/fuel-gauge/src/contract.test.ts @@ -909,14 +909,19 @@ describe('Contract', () => { contract.account = Wallet.generate({ provider: contract.provider }); - await expect( - contract.functions - .return_context_amount() - .callParams({ - forward: [100, contract.provider.getBaseAssetId()], - }) - .simulate() - ).rejects.toThrowError('not enough coins to fit the target'); + await expectToThrowFuelError( + () => + contract.functions + .return_context_amount() + .callParams({ + forward: [100, contract.provider.getBaseAssetId()], + }) + .simulate(), + new FuelError( + ErrorCode.NOT_ENOUGH_FUNDS, + `The account(s) sending the transaction don't have enough funds to cover the transaction.` + ) + ); }); it('should throw when using "simulate" without a wallet', async () => { diff --git a/packages/fuel-gauge/src/funding-transaction.test.ts b/packages/fuel-gauge/src/funding-transaction.test.ts index 82b5fe21801..7c9d8f033ba 100644 --- a/packages/fuel-gauge/src/funding-transaction.test.ts +++ b/packages/fuel-gauge/src/funding-transaction.test.ts @@ -268,7 +268,21 @@ describe('Funding Transactions', () => { */ await expectToThrowFuelError( () => sender.fund(request, txCost), - new FuelError(FuelError.CODES.INVALID_REQUEST, 'not enough coins to fit the target') + new FuelError( + FuelError.CODES.NOT_ENOUGH_FUNDS, + `The account(s) sending the transaction don't have enough funds to cover the transaction.`, + {}, + { + locations: [ + { + column: 3, + line: 2, + }, + ], + message: 'not enough coins to fit the target', + path: ['coinsToSpend'], + } + ) ); expect(getResourcesToSpend).toHaveBeenCalled(); diff --git a/packages/fuel-gauge/src/not-enough-coins.test.ts b/packages/fuel-gauge/src/not-enough-coins.test.ts new file mode 100644 index 00000000000..273d7263426 --- /dev/null +++ b/packages/fuel-gauge/src/not-enough-coins.test.ts @@ -0,0 +1,22 @@ +import { Contract, ErrorCode, Wallet } from 'fuels'; +import { expectToThrowFuelError } from 'fuels/test-utils'; + +import { CallTestContractFactory } from '../test/typegen/contracts'; + +import { launchTestContract } from './utils'; + +/** + * @group node + */ +test('not enough coins error', async () => { + using contract = await launchTestContract({ factory: CallTestContractFactory }); + + const emptyWallet = Wallet.generate({ provider: contract.provider }); + + const emptyWalletContract = new Contract(contract.id, contract.interface.jsonAbi, emptyWallet); + + await expectToThrowFuelError(() => emptyWalletContract.functions.return_void().call(), { + code: ErrorCode.NOT_ENOUGH_FUNDS, + message: `The account(s) sending the transaction don't have enough funds to cover the transaction.`, + }); +}); diff --git a/packages/fuel-gauge/src/predicate/predicate-invalidations.test.ts b/packages/fuel-gauge/src/predicate/predicate-invalidations.test.ts index a086974bb66..a1fb59388e0 100644 --- a/packages/fuel-gauge/src/predicate/predicate-invalidations.test.ts +++ b/packages/fuel-gauge/src/predicate/predicate-invalidations.test.ts @@ -1,5 +1,5 @@ -import { Predicate, Wallet } from 'fuels'; -import { launchTestNode } from 'fuels/test-utils'; +import { ErrorCode, FuelError, Predicate, Wallet } from 'fuels'; +import { expectToThrowFuelError, launchTestNode } from 'fuels/test-utils'; import { PredicateMainArgsStruct } from '../../test/typegen'; import type { Validation } from '../types/predicate'; @@ -29,16 +29,21 @@ describe('Predicate', () => { const receiver = Wallet.generate({ provider }); - await expect( - predicate.transfer( - receiver.address, - await predicate.getBalance(), - provider.getBaseAssetId(), - { - gasLimit: 100_000_000, - } + await expectToThrowFuelError( + async () => + predicate.transfer( + receiver.address, + await predicate.getBalance(), + provider.getBaseAssetId(), + { + gasLimit: 100_000_000, + } + ), + new FuelError( + ErrorCode.NOT_ENOUGH_FUNDS, + `The account(s) sending the transaction don't have enough funds to cover the transaction.` ) - ).rejects.toThrow(/not enough coins to fit the target/i); + ); }); it('throws if the passed gas limit is too low', async () => { diff --git a/packages/fuel-gauge/src/predicate/predicate-with-contract.test.ts b/packages/fuel-gauge/src/predicate/predicate-with-contract.test.ts index 8e096f2a3ba..89cc08ed1d9 100644 --- a/packages/fuel-gauge/src/predicate/predicate-with-contract.test.ts +++ b/packages/fuel-gauge/src/predicate/predicate-with-contract.test.ts @@ -1,5 +1,5 @@ -import { Contract, Wallet } from 'fuels'; -import { launchTestNode } from 'fuels/test-utils'; +import { Contract, ErrorCode, FuelError, Wallet } from 'fuels'; +import { expectToThrowFuelError, launchTestNode } from 'fuels/test-utils'; import { CallTestContractFactory, TokenContractFactory } from '../../test/typegen/contracts'; import { PredicateMainArgsStruct, PredicateTrue } from '../../test/typegen/predicates'; @@ -59,8 +59,13 @@ describe('Predicate', () => { // calling the contract with the receiver account (no resources) contract.account = receiver; - await expect(contract.functions.mint_coins(200).call()).rejects.toThrow( - /not enough coins to fit the target/ + + await expectToThrowFuelError( + () => contract.functions.mint_coins(200).call(), + new FuelError( + ErrorCode.NOT_ENOUGH_FUNDS, + `The account(s) sending the transaction don't have enough funds to cover the transaction.` + ) ); // setup predicate diff --git a/packages/fuel-gauge/src/predicate/predicate-with-script.test.ts b/packages/fuel-gauge/src/predicate/predicate-with-script.test.ts index b2b0fd068cf..10b9e5d72a1 100644 --- a/packages/fuel-gauge/src/predicate/predicate-with-script.test.ts +++ b/packages/fuel-gauge/src/predicate/predicate-with-script.test.ts @@ -1,6 +1,6 @@ import type { BigNumberish } from 'fuels'; -import { toNumber, Script, Predicate, Wallet } from 'fuels'; -import { launchTestNode } from 'fuels/test-utils'; +import { toNumber, Script, Predicate, Wallet, FuelError, ErrorCode } from 'fuels'; +import { expectToThrowFuelError, launchTestNode } from 'fuels/test-utils'; import { PredicateMainArgsStruct, ScriptMainArgs } from '../../test/typegen'; import type { Validation } from '../types/predicate'; @@ -33,8 +33,12 @@ describe('Predicate', () => { const scriptInput = 1; scriptInstance.account = receiver; - await expect(scriptInstance.functions.main(scriptInput).call()).rejects.toThrow( - /not enough coins to fit the target/ + await expectToThrowFuelError( + () => scriptInstance.functions.main(scriptInput).call(), + new FuelError( + ErrorCode.NOT_ENOUGH_FUNDS, + `The account(s) sending the transaction don't have enough funds to cover the transaction.` + ) ); // setup predicate