diff --git a/.changeset/lazy-readers-flash.md b/.changeset/lazy-readers-flash.md new file mode 100644 index 00000000000..532354c46ed --- /dev/null +++ b/.changeset/lazy-readers-flash.md @@ -0,0 +1,5 @@ +--- +"@fuel-ts/account": patch +--- + +feat: implement `generateFakeResources` on `Account` class diff --git a/apps/docs-snippets/src/guide/cookbook/generate-fake-resources.test.ts b/apps/docs-snippets/src/guide/cookbook/generate-fake-resources.test.ts new file mode 100644 index 00000000000..c169a7aed39 --- /dev/null +++ b/apps/docs-snippets/src/guide/cookbook/generate-fake-resources.test.ts @@ -0,0 +1,57 @@ +import type { TransactionResultReturnDataReceipt } from 'fuels'; +import { + FUEL_NETWORK_URL, + Provider, + ReceiptType, + ScriptTransactionRequest, + Wallet, + bn, +} from 'fuels'; + +import { + DocSnippetProjectsEnum, + getDocsSnippetsForcProject, +} from '../../../test/fixtures/forc-projects'; + +/** + * @group node + */ +describe(__filename, () => { + it('should generate fake resources just fine', async () => { + const provider = await Provider.create(FUEL_NETWORK_URL); + const wallet = Wallet.generate({ provider }); + const baseAssetId = provider.getBaseAssetId(); + + const { binHexlified: scriptHexBytes } = getDocsSnippetsForcProject( + DocSnippetProjectsEnum.RETURN_SCRIPT + ); + + // #region generate-fake-resources-2 + const transactionRequest = new ScriptTransactionRequest({ + gasLimit: bn(62_000), + maxFee: bn(60_000), + script: scriptHexBytes, + }); + + const resources = wallet.generateFakeResources([ + { + amount: bn(100_000), + assetId: baseAssetId, + }, + ]); + + transactionRequest.addResources(resources); + + const dryrunResult = await provider.call(transactionRequest); + + const returnReceipt = dryrunResult.receipts.find( + (receipt) => receipt.type === ReceiptType.ReturnData + ) as TransactionResultReturnDataReceipt; + + const { data: returnedValue } = returnReceipt; + // #endregion generate-fake-resources-2 + + expect(bn(returnedValue).toNumber()).toBe(1337); + expect(dryrunResult.dryRunStatus?.type).toBe('DryRunSuccessStatus'); + }); +}); diff --git a/apps/docs-snippets/test/fixtures/forc-projects/Forc.toml b/apps/docs-snippets/test/fixtures/forc-projects/Forc.toml index d0d87a6a099..1d453e4e9e3 100644 --- a/apps/docs-snippets/test/fixtures/forc-projects/Forc.toml +++ b/apps/docs-snippets/test/fixtures/forc-projects/Forc.toml @@ -16,6 +16,7 @@ members = [ "simple-token-abi", "echo-configurables", "transfer-to-address", + "return-script", "return-true-predicate", "echo-employee-data-vector", "whitelisted-address-predicate", diff --git a/apps/docs-snippets/test/fixtures/forc-projects/index.ts b/apps/docs-snippets/test/fixtures/forc-projects/index.ts index dd0ea7f8ac8..1db65624c5a 100644 --- a/apps/docs-snippets/test/fixtures/forc-projects/index.ts +++ b/apps/docs-snippets/test/fixtures/forc-projects/index.ts @@ -12,6 +12,7 @@ export enum DocSnippetProjectsEnum { SUM_OPTION_U8 = 'sum-option-u8', ECHO_U64_ARRAY = 'echo-u64-array', RETURN_CONTEXT = 'return-context', + RETURN_SCRIPT = 'return-script', TOKEN_DEPOSITOR = 'token-depositor', TOKEN = 'token', LIQUIDITY_POOL = 'liquidity-pool', diff --git a/apps/docs-snippets/test/fixtures/forc-projects/return-script/Forc.toml b/apps/docs-snippets/test/fixtures/forc-projects/return-script/Forc.toml new file mode 100644 index 00000000000..e8fdc98ec27 --- /dev/null +++ b/apps/docs-snippets/test/fixtures/forc-projects/return-script/Forc.toml @@ -0,0 +1,6 @@ +[project] +entry = "main.sw" +license = "Apache-2.0" +name = "return-script" + +[dependencies] diff --git a/apps/docs-snippets/test/fixtures/forc-projects/return-script/src/main.sw b/apps/docs-snippets/test/fixtures/forc-projects/return-script/src/main.sw new file mode 100644 index 00000000000..290ac83ef0d --- /dev/null +++ b/apps/docs-snippets/test/fixtures/forc-projects/return-script/src/main.sw @@ -0,0 +1,7 @@ +// #region generate-fake-resources-1 +script; + +fn main() -> u64 { + return 1337; +} +// #endregion generate-fake-resources-1 diff --git a/apps/docs/.vitepress/config.ts b/apps/docs/.vitepress/config.ts index f5103710a36..8ccb9a333f0 100644 --- a/apps/docs/.vitepress/config.ts +++ b/apps/docs/.vitepress/config.ts @@ -358,6 +358,10 @@ export default defineConfig({ text: 'Custom Transactions from Contract Calls', link: '/guide/cookbook/custom-transactions-from-contract-calls', }, + { + text: 'Generate Fake Resources', + link: '/guide/cookbook/generate-fake-resources', + }, { text: 'Transactions with Multiple Signers', link: '/guide/cookbook/transactions-with-multiple-signers', diff --git a/apps/docs/src/guide/cookbook/generate-fake-resources.md b/apps/docs/src/guide/cookbook/generate-fake-resources.md new file mode 100644 index 00000000000..7ceb8fd8ab2 --- /dev/null +++ b/apps/docs/src/guide/cookbook/generate-fake-resources.md @@ -0,0 +1,13 @@ +# Generate Fake Resources + +When working with an unfunded account, you can generate fake resources to perform a dry-run on your transactions. This is useful for testing purposes without the need for real funds. + +Below is an example script that returns the value `1337`. You can use fake resources to execute a dry-run of this script and obtain the returned value. + +<<< @/../../docs-snippets/test/fixtures/forc-projects/return-script/src/main.sw#generate-fake-resources-1{rust:line-numbers} + +To execute a dry-run, use the `Provider.call` method. Ensure you set the `utxo_validation` flag to true, as this script uses fake UTXOs: + +<<< @/../../docs-snippets/src/guide/cookbook/generate-fake-resources.test.ts#generate-fake-resources-2{ts:line-numbers} + +By setting `utxo_validation` to `true`, you can successfully execute the dry-run and retrieve the returned value from the script without requiring actual funds. diff --git a/packages/account/src/account.test.ts b/packages/account/src/account.test.ts index a77ca0db12f..73b7d9f368e 100644 --- a/packages/account/src/account.test.ts +++ b/packages/account/src/account.test.ts @@ -5,7 +5,7 @@ import { bn } from '@fuel-ts/math'; import { PolicyType } from '@fuel-ts/transactions'; import { ASSET_A, ASSET_B } from '@fuel-ts/utils/test-utils'; -import type { TransferParams } from './account'; +import type { FakeResources, TransferParams } from './account'; import { Account } from './account'; import { FUEL_NETWORK_URL } from './configs'; import { ScriptTransactionRequest, Provider } from './providers'; @@ -659,6 +659,41 @@ describe('Account', () => { expect(receiverBalances).toEqual([{ assetId: baseAssetId, amount: bn(110) }]); }); + it('can generate and use fake coins', async () => { + const sender = Wallet.generate({ + provider, + }); + + const amount1 = bn(100_000); + const amount2 = bn(200_000); + const amount3 = bn(300_000); + const amountToTransferBaseAsset = bn(1000); + + const fakeCoinsConfig: FakeResources[] = [ + { amount: amount1, assetId: baseAssetId }, + { amount: amount2, assetId: ASSET_A }, + { amount: amount3, assetId: ASSET_B }, + ]; + + const fakeCoins = sender.generateFakeResources(fakeCoinsConfig); + const request = new ScriptTransactionRequest({ + gasLimit: bn(60_000), + maxFee: bn(62_000), + }); + + request.addResources(fakeCoins); + request.addCoinOutput(Address.fromRandom(), amountToTransferBaseAsset, baseAssetId); + request.addCoinOutput(Address.fromRandom(), amount2, ASSET_A); + request.addCoinOutput(Address.fromRandom(), amount3, ASSET_B); + + const { dryRunStatus } = await provider.call(request, { + utxoValidation: false, + estimateTxDependencies: false, + }); + + expect(dryRunStatus?.type).toBe('DryRunSuccessStatus'); + }); + it('can withdraw an amount of base asset using mutiple uxtos', async () => { const sender = Wallet.generate({ provider, diff --git a/packages/account/src/account.ts b/packages/account/src/account.ts index cb759c12a9b..2da178ef222 100644 --- a/packages/account/src/account.ts +++ b/packages/account/src/account.ts @@ -1,10 +1,12 @@ +import { UTXO_ID_LEN } from '@fuel-ts/abi-coder'; import { Address } from '@fuel-ts/address'; +import { randomBytes } from '@fuel-ts/crypto'; import { ErrorCode, FuelError } from '@fuel-ts/errors'; import { AbstractAccount } from '@fuel-ts/interfaces'; import type { AbstractAddress, BytesLike } from '@fuel-ts/interfaces'; import type { BigNumberish, BN } from '@fuel-ts/math'; import { bn } from '@fuel-ts/math'; -import { arrayify, isDefined } from '@fuel-ts/utils'; +import { arrayify, hexlify, isDefined } from '@fuel-ts/utils'; import { clone } from 'ramda'; import type { FuelConnector } from './connectors'; @@ -56,6 +58,8 @@ export type EstimatedTxParams = Pick< >; const MAX_FUNDING_ATTEMPTS = 2; +export type FakeResources = Partial & Required>; + /** * `Account` provides an abstraction for interacting with accounts or wallets on the network. */ @@ -640,6 +644,22 @@ export class Account extends AbstractAccount { return this.provider.simulate(transactionRequest, { estimateTxDependencies: false }); } + /** + * Generates an array of fake resources based on the provided coins. + * + * @param coins - An array of `FakeResources` objects representing the coins. + * @returns An array of `Resource` objects with generated properties. + */ + generateFakeResources(coins: FakeResources[]): Array { + return coins.map((coin) => ({ + id: hexlify(randomBytes(UTXO_ID_LEN)), + owner: this.address, + blockCreated: bn(1), + txCreatedIdx: bn(1), + ...coin, + })); + } + /** @hidden * */ private validateTransferAmount(amount: BigNumberish) { if (bn(amount).lte(0)) { diff --git a/packages/account/src/predicate/predicate.ts b/packages/account/src/predicate/predicate.ts index d367e6bde45..8c5a390ae40 100644 --- a/packages/account/src/predicate/predicate.ts +++ b/packages/account/src/predicate/predicate.ts @@ -5,6 +5,7 @@ import { ErrorCode, FuelError } from '@fuel-ts/errors'; import type { BytesLike } from '@fuel-ts/interfaces'; import { arrayify, hexlify } from '@fuel-ts/utils'; +import type { FakeResources } from '../account'; import { Account } from '../account'; import { transactionRequestify, @@ -196,6 +197,20 @@ export class Predicate extends Account { })); } + /** + * Generates an array of fake resources based on the provided coins. + * + * @param coins - An array of `FakeResources` objects representing the coins. + * @returns An array of `Resource` objects with generated properties. + */ + generateFakeResources(coins: FakeResources[]): Array { + return super.generateFakeResources(coins).map((coin) => ({ + ...coin, + predicate: hexlify(this.bytes), + predicateData: hexlify(this.getPredicateData()), + })); + } + /** * Sets the configurable constants for the predicate. * diff --git a/packages/fuel-gauge/src/predicate/predicate-general.test.ts b/packages/fuel-gauge/src/predicate/predicate-general.test.ts new file mode 100644 index 00000000000..773317b1474 --- /dev/null +++ b/packages/fuel-gauge/src/predicate/predicate-general.test.ts @@ -0,0 +1,72 @@ +import { ASSET_A, ASSET_B } from '@fuel-ts/utils/test-utils'; +import type { BN, FakeResources } from 'fuels'; +import { + Address, + FUEL_NETWORK_URL, + Predicate, + Provider, + ScriptTransactionRequest, + bn, +} from 'fuels'; + +import { FuelGaugeProjectsEnum, getFuelGaugeForcProject } from '../../test/fixtures'; + +/** + * @group node + */ +describe('Predicate', () => { + it('can generate and use fake predicate coins', async () => { + const provider = await Provider.create(FUEL_NETWORK_URL); + const baseAssetId = provider.getBaseAssetId(); + const { binHexlified, abiContents } = getFuelGaugeForcProject( + FuelGaugeProjectsEnum.PREDICATE_SUM + ); + + const amount1 = bn(500_000); + const amount2 = bn(200_000); + const amount3 = bn(300_000); + const amountToTransferBaseAsset = bn(1000); + + const fakeCoinsConfig: FakeResources[] = [ + { amount: amount1, assetId: baseAssetId }, + { amount: amount2, assetId: ASSET_A }, + { amount: amount3, assetId: ASSET_B }, + ]; + + const value2 = bn(200); + const value1 = bn(100); + + const predicate = new Predicate<[BN, BN]>({ + bytecode: binHexlified, + abi: abiContents, + provider, + inputData: [value1, value2], + }); + + const fakeCoins = predicate.generateFakeResources(fakeCoinsConfig); + + let request = new ScriptTransactionRequest({ + gasLimit: bn(270_000), + maxFee: bn(250_000), + }); + + fakeCoins.forEach((coin) => { + expect(coin.predicate).toBeDefined(); + expect(coin.predicateData).toBeDefined(); + }); + + request.addResources(fakeCoins); + request.addCoinOutput(Address.fromRandom(), amountToTransferBaseAsset, baseAssetId); + request.addCoinOutput(Address.fromRandom(), amount2, ASSET_A); + request.addCoinOutput(Address.fromRandom(), amount3, ASSET_B); + + request = await provider.estimatePredicates(request); + + const { dryRunStatus } = await provider.call(request, { + utxoValidation: false, + estimateTxDependencies: false, + }); + + expect(dryRunStatus?.type).toBe('DryRunSuccessStatus'); + }); +});