Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/FuelLabs/fuels-ts into ps…
Browse files Browse the repository at this point in the history
…/chore/adding-ts-checking
  • Loading branch information
petertonysmith94 committed Sep 3, 2024
2 parents b0e5238 + 482bbf0 commit eb7c153
Show file tree
Hide file tree
Showing 30 changed files with 275 additions and 251 deletions.
6 changes: 6 additions & 0 deletions .changeset/gentle-files-pay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@fuel-ts/contract": patch
"@fuel-ts/account": patch
---

chore: deprecate `BaseTransactionRequest.fundWithFakeUtxos`
6 changes: 6 additions & 0 deletions .changeset/gorgeous-laws-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@fuel-ts/contract": patch
"@fuel-ts/account": patch
---

feat: validate blob IDs against chain in chunk deploys
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
6 changes: 6 additions & 0 deletions .changeset/wise-deers-press.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@fuel-ts/crypto": patch
"@fuel-ts/errors": patch
---

chore: removed redundant crypto functionality
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
11 changes: 6 additions & 5 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 Expand Up @@ -300,11 +306,6 @@ When the workspace is not detected in the directory indicated in the message.

Ensure that the workspace is present in the directory specified.

### `HASHER_LOCKED`

The hashing algorithm is currently locked, any subsequent attempts to register a new implementation will throw this error.
The purpose of the lock function is to provide a way to ensure that the implementation of the specific hashing algorithm cannot be changed once it is locked. This can be useful in scenarios where you want to guarantee the integrity and consistency of the hashing function throughout your application.

### `UNKNOWN`

In cases where the error hasn't been mapped yet, this code will be used.
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
58 changes: 53 additions & 5 deletions packages/account/src/providers/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { equalBytes } from '@noble/curves/abstract/utils';
import type { DocumentNode } from 'graphql';
import { GraphQLClient } from 'graphql-request';
import type { GraphQLResponse } from 'graphql-request/src/types';
import gql from 'graphql-tag';
import { clone } from 'ramda';

import { getSdk as getOperationsSdk } from './__generated__/operations';
Expand All @@ -27,6 +28,7 @@ import type {
GqlPageInfo,
GqlRelayedTransactionFailed,
GqlMessage,
Requester,
} from './__generated__/operations';
import type { Coin } from './coin';
import type { CoinQuantity, CoinQuantityLike } from './coin-quantity';
Expand Down Expand Up @@ -59,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 @@ -376,6 +379,7 @@ type SdkOperations = Omit<Operations, 'submitAndAwait' | 'statusChange'> & {
statusChange: (
...args: Parameters<Operations['statusChange']>
) => Promise<ReturnType<Operations['statusChange']>>;
getBlobs: (variables: { blobIds: string[] }) => Promise<{ blob: { id: string } | null }[]>;
};

/**
Expand Down Expand Up @@ -602,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 All @@ -629,8 +633,33 @@ Supported fuel-core version: ${supportedVersion}.`
return gqlClient.request(query, vars);
};

const customOperations = (requester: Requester) => ({
getBlobs(variables: { blobIds: string[] }) {
const queryParams = variables.blobIds.map((_, i) => `$blobId${i}: BlobId!`).join(', ');
const blobParams = variables.blobIds
.map((_, i) => `blob${i}: blob(id: $blobId${i}) { id }`)
.join('\n');

const updatedVariables = variables.blobIds.reduce(
(acc, blobId, i) => {
acc[`blobId${i}`] = blobId;
return acc;
},
{} as Record<string, string>
);

const document = gql`
query getBlobs(${queryParams}) {
${blobParams}
}
`;

return requester(document, updatedVariables);
},
});

// @ts-expect-error This is due to this function being generic. Its type is specified when calling a specific operation via provider.operations.xyz.
return getOperationsSdk(executeQuery);
return { ...getOperationsSdk(executeQuery), ...customOperations(executeQuery) };
}

/**
Expand Down Expand Up @@ -1356,6 +1385,25 @@ Supported fuel-core version: ${supportedVersion}.`
return coins;
}

/**
* Returns an array of blobIds that exist on chain, for a given array of blobIds.
*
* @param blobIds - blobIds to check.
* @returns - A promise that resolves to an array of blobIds that exist on chain.
*/
async getBlobs(blobIds: string[]): Promise<string[]> {
const res = await this.operations.getBlobs({ blobIds });
const blobs: (string | null)[] = [];

Object.keys(res).forEach((key) => {
// @ts-expect-error keys are strings
const val = res[key];
blobs.push(val?.id ?? null);
});

return blobs.filter((v) => v) as string[];
}

/**
* Returns block matching the given ID or height.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,8 @@ export abstract class BaseTransactionRequest implements BaseTransactionRequestLi
*
* @param quantities - CoinQuantity Array.
* @param baseAssetId - The base asset to fund the transaction.
* @deprecated - This method is deprecated and will be removed in future versions.
* Please use `Account.generateFakeResources` along with `this.addResources` instead.
*/
fundWithFakeUtxos(
quantities: CoinQuantity[],
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);
}
};
21 changes: 14 additions & 7 deletions packages/contract/src/contract-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,15 +281,19 @@ export default class ContractFactory {
...deployOptions,
});

// BlobIDs only need to be uploaded once and we can check if they exist on chain
const uniqueBlobIds = [...new Set(blobIds)];
const uploadedBlobIds = await account.provider.getBlobs(uniqueBlobIds);
const blobIdsToUpload = uniqueBlobIds.filter((id) => !uploadedBlobIds.includes(id));

// Check the account can afford to deploy all chunks and loader
let totalCost = bn(0);
const chainInfo = account.provider.getChain();
const gasPrice = await account.provider.estimateGasPrice(10);
const priceFactor = chainInfo.consensusParameters.feeParameters.gasPriceFactor;
const estimatedBlobIds: string[] = [];

for (const { transactionRequest, blobId } of chunks) {
if (!estimatedBlobIds.includes(blobId)) {
if (blobIdsToUpload.includes(blobId)) {
const minGas = transactionRequest.calculateMinGas(chainInfo);
const minFee = calculateGasFee({
gasPrice,
Expand All @@ -299,7 +303,6 @@ export default class ContractFactory {
}).add(1);

totalCost = totalCost.add(minFee);
estimatedBlobIds.push(blobId);
}
const createMinGas = createRequest.calculateMinGas(chainInfo);
const createMinFee = calculateGasFee({
Expand All @@ -325,7 +328,7 @@ export default class ContractFactory {
const uploadedBlobs: string[] = [];
// Deploy the chunks as blob txs
for (const { blobId, transactionRequest } of chunks) {
if (!uploadedBlobs.includes(blobId)) {
if (!uploadedBlobs.includes(blobId) && blobIdsToUpload.includes(blobId)) {
const fundedBlobRequest = await this.fundTransactionRequest(
transactionRequest,
deployOptions
Expand All @@ -340,6 +343,7 @@ export default class ContractFactory {
// Core will throw for blobs that have already been uploaded, but the blobId
// is still valid so we can use this for the loader contract
if ((<Error>err).message.indexOf(`BlobId is already taken ${blobId}`) > -1) {
uploadedBlobs.push(blobId);
// eslint-disable-next-line no-continue
continue;
}
Expand Down Expand Up @@ -465,8 +469,8 @@ export default class ContractFactory {
);
}

const { provider } = this.getAccount();
const { consensusParameters } = provider.getChain();
const account = this.getAccount();
const { consensusParameters } = account.provider.getChain();
const contractSizeLimit = consensusParameters.contractParameters.contractMaxSize.toNumber();
const transactionSizeLimit = consensusParameters.txParameters.maxSize.toNumber();
const maxLimit = 64000;
Expand All @@ -475,10 +479,13 @@ export default class ContractFactory {
const sizeLimit = chainLimit < maxLimit ? chainLimit : maxLimit;

// Get an estimate base tx length

const blobTx = this.blobTransactionRequest({
...deployOptions,
bytecode: randomBytes(32),
}).fundWithFakeUtxos([], provider.getBaseAssetId());
}).addResources(
account.generateFakeResources([{ assetId: account.provider.getBaseAssetId(), amount: bn(1) }])
);
// Given above, calculate the maximum chunk size
const maxChunkSize = (sizeLimit - blobTx.byteLength() - WORD_SIZE) * chunkSizeMultiplier;

Expand Down
Loading

0 comments on commit eb7c153

Please sign in to comment.