Skip to content

Commit

Permalink
feat: map 'not enough coins' error (#2902)
Browse files Browse the repository at this point in the history
* feat: map 'not enough coins' error

* add testing group

* handle multiple errors properly

* update tests

* update test

* add `rawError` metadata

* add docs

* Update handle-gql-error-message.ts

* update test

* add description to docs for the error

Co-authored-by: Peter Smith <peter@blueoceancomputing.co.uk>

* remove 'unknown error' prefix

* update error msg

* remove from gql subscriber

* assert `FuelError`s instead of plain errors

* missing imports

* set rawError to null

* missing {

* update error msg

* fix tests

* fix test

Co-authored-by: Sérgio Torres <30977845+Torres-ssf@users.noreply.github.com>

* reword docs

Co-authored-by: Anderson Arboleya <anderson@arboleya.me>

* reword docs

Co-authored-by: Anderson Arboleya <anderson@arboleya.me>

---------

Co-authored-by: Peter Smith <peter@blueoceancomputing.co.uk>
Co-authored-by: Daniel Bate <djbate23@gmail.com>
Co-authored-by: Sérgio Torres <30977845+Torres-ssf@users.noreply.github.com>
Co-authored-by: Chad Nehemiah <chad.nehemiah94@gmail.com>
Co-authored-by: Anderson Arboleya <anderson@arboleya.me>
  • Loading branch information
6 people authored Sep 3, 2024
1 parent be1c43f commit a059ea1
Show file tree
Hide file tree
Showing 18 changed files with 157 additions and 61 deletions.
6 changes: 6 additions & 0 deletions .changeset/rare-shrimps-clap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@fuel-ts/account": patch
"@fuel-ts/errors": patch
---

feat: map 'not enough coins' error
14 changes: 8 additions & 6 deletions apps/demo-bun-fuels/src/bun.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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>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 () => {
Expand Down
14 changes: 8 additions & 6 deletions apps/demo-fuels/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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>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 () => {
Expand Down
14 changes: 9 additions & 5 deletions apps/demo-typegen/src/demo.test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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>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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>error).message).toMatch(errorMsg);
Expand Down
6 changes: 6 additions & 0 deletions apps/docs/src/guide/errors/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 2 additions & 4 deletions apps/docs/src/guide/wallets/wallet-transferring.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.
You can see how to check your balance at the [`checking-balances`](./checking-balances.md) page.
12 changes: 7 additions & 5 deletions packages/account/src/account.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
9 changes: 5 additions & 4 deletions packages/account/src/providers/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -605,11 +606,11 @@ Supported fuel-core version: ${supportedVersion}.`
responseMiddleware: (response: GraphQLResponse<unknown> | 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);
}
}
}
},
Expand Down
20 changes: 20 additions & 0 deletions packages/account/src/providers/utils/handle-gql-error-message.ts
Original file line number Diff line number Diff line change
@@ -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);
}
};
2 changes: 1 addition & 1 deletion packages/errors/src/fuel-error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,6 @@ it('converts error to plain object', () => {
message,
VERSIONS: err.VERSIONS,
metadata,
rawError: {},
rawError: null,
});
});
2 changes: 1 addition & 1 deletion packages/errors/src/fuel-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class FuelError extends Error {
code: ErrorCode,
message: string,
metadata: Record<string, unknown> = {},
rawError: unknown = {}
rawError: unknown = null
) {
super(message);
this.code = code;
Expand Down
21 changes: 13 additions & 8 deletions packages/fuel-gauge/src/contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
16 changes: 15 additions & 1 deletion packages/fuel-gauge/src/funding-transaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
22 changes: 22 additions & 0 deletions packages/fuel-gauge/src/not-enough-coins.test.ts
Original file line number Diff line number Diff line change
@@ -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.`,
});
});
27 changes: 16 additions & 11 deletions packages/fuel-gauge/src/predicate/predicate-invalidations.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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
Expand Down
12 changes: 8 additions & 4 deletions packages/fuel-gauge/src/predicate/predicate-with-script.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit a059ea1

Please sign in to comment.