From 038921c4130ea7d21fb6da19eeaa9b075991e790 Mon Sep 17 00:00:00 2001 From: Dhaiwat Date: Tue, 28 Nov 2023 22:43:40 +0800 Subject: [PATCH 1/4] first draft --- ...nd-and-spend-funds-from-predicates.test.ts | 28 +++++++++-- packages/predicate/package.json | 1 + packages/predicate/src/predicate.ts | 37 +++++++++++++- packages/wallet/src/account.test.ts | 19 ++++++++ packages/wallet/src/account.ts | 48 ++++++++++++++++++- pnpm-lock.yaml | 7 ++- 6 files changed, 129 insertions(+), 11 deletions(-) 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 0f7ce1ae338..193b3c5db3a 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 @@ -9,7 +9,10 @@ import { BaseAssetId, } from 'fuels'; -import { DocSnippetProjectsEnum, getDocsSnippetsForcProject } from '../../../test/fixtures/forc-projects'; +import { + DocSnippetProjectsEnum, + getDocsSnippetsForcProject, +} from '../../../test/fixtures/forc-projects'; import { getTestWallet } from '../../utils'; describe(__filename, () => { @@ -24,7 +27,7 @@ describe(__filename, () => { ({ minGasPrice: gasPrice } = walletWithFunds.provider.getGasConfig()); }); - it('should successfully use predicate to spend assets', async () => { + it.only('should successfully use predicate to spend assets', async () => { // #region send-and-spend-funds-from-predicates-2 const provider = await Provider.create(FUEL_NETWORK_URL); const predicate = new Predicate(bin, provider, abi); @@ -37,7 +40,7 @@ describe(__filename, () => { gasPrice, }); - await tx.waitForResult(); + // await tx.waitForResult(); // #endregion send-and-spend-funds-from-predicates-3 const initialPredicateBalance = new BN(await predicate.getBalance()).toNumber(); @@ -55,6 +58,20 @@ describe(__filename, () => { provider, }); + console.log({ + predicateBalances: await predicate.getBalances(), + }); + + // #region send-and-spend-funds-from-predicates-8 + const txId = await predicate.getTransferTransactionId( + receiverWallet.address, + amountToPredicate - 150_000, + BaseAssetId, + { + gasPrice, + } + ); + const tx2 = await predicate.transfer( receiverWallet.address, amountToPredicate - 150_000, @@ -66,6 +83,11 @@ describe(__filename, () => { await tx2.waitForResult(); // #endregion send-and-spend-funds-from-predicates-5 + + const txIdFromExecutedTx = tx2.id; + + // #endregion send-and-spend-funds-from-predicates-8 + expect(txId).toEqual(txIdFromExecutedTx); }); it('should fail when trying to spend predicates entire amount', async () => { diff --git a/packages/predicate/package.json b/packages/predicate/package.json index 366e44c2f82..e42e159770b 100644 --- a/packages/predicate/package.json +++ b/packages/predicate/package.json @@ -28,6 +28,7 @@ "@fuel-ts/abi-coder": "workspace:*", "@fuel-ts/address": "workspace:*", "@fuel-ts/interfaces": "workspace:*", + "@fuel-ts/math": "workspace:*", "@fuel-ts/merkle": "workspace:*", "@fuel-ts/providers": "workspace:*", "@fuel-ts/transactions": "workspace:*", diff --git a/packages/predicate/src/predicate.ts b/packages/predicate/src/predicate.ts index 7449097e079..95d6d2d8a75 100644 --- a/packages/predicate/src/predicate.ts +++ b/packages/predicate/src/predicate.ts @@ -7,16 +7,20 @@ import { calculateVmTxMemory, } from '@fuel-ts/abi-coder'; import { Address } from '@fuel-ts/address'; +import { BaseAssetId } from '@fuel-ts/address/configs'; import { ErrorCode, FuelError } from '@fuel-ts/errors'; -import type { AbstractPredicate } from '@fuel-ts/interfaces'; +import { hashTransaction } from '@fuel-ts/hasher'; +import type { AbstractAddress, AbstractPredicate } from '@fuel-ts/interfaces'; +import type { BigNumberish } from '@fuel-ts/math'; import type { CallResult, Provider, TransactionRequestLike, TransactionResponse, } from '@fuel-ts/providers'; -import { transactionRequestify } from '@fuel-ts/providers'; +import { ScriptTransactionRequest, transactionRequestify } from '@fuel-ts/providers'; import { ByteArrayCoder, InputType } from '@fuel-ts/transactions'; +import type { TxParamsType } from '@fuel-ts/wallet'; import { Account } from '@fuel-ts/wallet'; import type { BytesLike } from 'ethers'; import { getBytesCopy, hexlify } from 'ethers'; @@ -92,6 +96,35 @@ export class Predicate extends Account implements Abs return super.sendTransaction(transactionRequest); } + /** + * Returns the transaction ID for a transfer transaction, without sending it. + * + * @param destination - The address of the destination. + * @param amount - The amount of coins to transfer. + * @param assetId - The asset ID of the coins to transfer. + * @param txParams - The transaction parameters (gasLimit, gasPrice, maturity). + * @returns A promise that resolves to the transaction ID. + */ + async getTransferTransactionId( + /** Address of the destination */ + destination: AbstractAddress, + /** Amount of coins */ + amount: BigNumberish, + /** Asset ID of coins */ + assetId: BytesLike = BaseAssetId, + /** Tx Params */ + txParams: TxParamsType = {} + ): Promise { + const request = await super.prepareTxRequestForIdCalculation( + destination, + amount, + assetId, + txParams + ); + const populatedRequest = this.populateTransactionPredicateData(request); + return hashTransaction(populatedRequest, this.provider.getChainId()); + } + /** * Simulates a transaction with the populated predicate data. * diff --git a/packages/wallet/src/account.test.ts b/packages/wallet/src/account.test.ts index 0367187d67c..fe1b659ad9e 100644 --- a/packages/wallet/src/account.test.ts +++ b/packages/wallet/src/account.test.ts @@ -458,4 +458,23 @@ describe('Account', () => { expect(simulate.mock.calls.length).toBe(1); expect(simulate.mock.calls[0][0]).toEqual(transactionRequest); }); + + it('getTransferTransactionId should return the transaction id', async () => { + const amount = bn(1); + const assetId = '0x0101010101010101010101010101010101010101010101010101010101010101'; + const destination = Address.fromAddressOrString( + '0x0101010101010101010101010101010101010101000000000000000000000000' + ); + const txParam: Pick = { + gasLimit: bn(1), + gasPrice: bn(1), + maturity: 1, + }; + const account = new Account( + '0x09c0b2d1a486c439a87bcba6b46a7a1a23f3897cc83a94521a96da5c23bc58db', + provider + ); + const txId = await account.getTransferTransactionId(destination, amount, assetId, txParam); + expect(txId.length).toEqual(66); + }); }); diff --git a/packages/wallet/src/account.ts b/packages/wallet/src/account.ts index 12695799a6b..6c7208c26c2 100644 --- a/packages/wallet/src/account.ts +++ b/packages/wallet/src/account.ts @@ -1,6 +1,7 @@ import { Address } from '@fuel-ts/address'; import { BaseAssetId } from '@fuel-ts/address/configs'; import { ErrorCode, FuelError } from '@fuel-ts/errors'; +import { hashTransaction } from '@fuel-ts/hasher'; import { AbstractAccount } from '@fuel-ts/interfaces'; import type { AbstractAddress } from '@fuel-ts/interfaces'; import type { BigNumberish, BN } from '@fuel-ts/math'; @@ -32,7 +33,7 @@ import { formatScriptDataForTransferringToContract, } from './utils'; -type TxParamsType = Pick; +export type TxParamsType = Pick; /** * `Account` provides an abstraction for interacting with accounts or wallets on the network. @@ -243,14 +244,57 @@ export class Account extends AbstractAccount { const params: TxParamsType = { gasLimit: maxGasPerTx, ...txParams }; const request = new ScriptTransactionRequest(params); request.addCoinOutput(destination, amount, assetId); - + console.log({ + inputs1: JSON.stringify(request.inputs), + length: request.inputs.length, + }); const { maxFee, requiredQuantities } = await this.provider.getTransactionCost(request); await this.fund(request, requiredQuantities, maxFee); + console.log({ + inputs2: JSON.stringify(request.inputs), + length: request.inputs.length, + }); return this.sendTransaction(request); } + /** + * Returns the transaction ID for a transfer transaction, without sending it. + * + * @param destination - The address of the destination. + * @param amount - The amount of coins to transfer. + * @param assetId - The asset ID of the coins to transfer. + * @param txParams - The transaction parameters (gasLimit, gasPrice, maturity). + * @returns A promise that resolves to the transaction ID. + */ + protected async prepareTxRequestForIdCalculation( + /** Address of the destination */ + destination: AbstractAddress, + /** Amount of coins */ + amount: BigNumberish, + /** Asset ID of coins */ + assetId: BytesLike = BaseAssetId, + /** Tx Params */ + txParams: TxParamsType = {} + ): Promise { + const { maxGasPerTx } = this.provider.getGasConfig(); + const params: TxParamsType = { gasLimit: maxGasPerTx, ...txParams }; + const request = new ScriptTransactionRequest(params); + request.addCoinOutput(destination, amount, assetId); + console.log({ + inputs3: JSON.stringify(request.inputs), + length: request.inputs.length, + }); + const { maxFee, requiredQuantities } = await this.provider.getTransactionCost(request); + await this.fund(request, requiredQuantities, maxFee); + console.log({ + inputs4: JSON.stringify(request.inputs), + length: request.inputs.length, + }); + return request; + } + /** * Transfers coins to a contract address. * diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b3e238b9cd2..ebcbec159e2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -867,6 +867,9 @@ importers: '@fuel-ts/interfaces': specifier: workspace:* version: link:../interfaces + '@fuel-ts/math': + specifier: workspace:* + version: link:../math '@fuel-ts/merkle': specifier: workspace:* version: link:../merkle @@ -888,10 +891,6 @@ importers: ethers: specifier: ^6.7.1 version: 6.7.1 - devDependencies: - '@fuel-ts/math': - specifier: workspace:* - version: link:../math packages/program: dependencies: From 3e03482f856db44590bb4a8f740e4c071ad8c0b1 Mon Sep 17 00:00:00 2001 From: Dhaiwat Date: Tue, 28 Nov 2023 23:33:00 +0800 Subject: [PATCH 2/4] feat: add `Predicate.getTransferTxId` helper --- ...nd-and-spend-funds-from-predicates.test.ts | 15 ++++-------- .../send-and-spend-funds-from-predicates.md | 6 +++++ packages/predicate/src/predicate.ts | 6 ++--- packages/wallet/src/account.test.ts | 19 --------------- packages/wallet/src/account.ts | 23 +++---------------- 5 files changed, 17 insertions(+), 52 deletions(-) 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 193b3c5db3a..6fcddf6adde 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 @@ -27,7 +27,7 @@ describe(__filename, () => { ({ minGasPrice: gasPrice } = walletWithFunds.provider.getGasConfig()); }); - it.only('should successfully use predicate to spend assets', async () => { + it('should successfully use predicate to spend assets', async () => { // #region send-and-spend-funds-from-predicates-2 const provider = await Provider.create(FUEL_NETWORK_URL); const predicate = new Predicate(bin, provider, abi); @@ -40,7 +40,7 @@ describe(__filename, () => { gasPrice, }); - // await tx.waitForResult(); + await tx.waitForResult(); // #endregion send-and-spend-funds-from-predicates-3 const initialPredicateBalance = new BN(await predicate.getBalance()).toNumber(); @@ -53,17 +53,12 @@ describe(__filename, () => { predicate.setData(inputAddress); // #endregion send-and-spend-funds-from-predicates-4 - // #region send-and-spend-funds-from-predicates-5 const receiverWallet = WalletUnlocked.generate({ provider, }); - console.log({ - predicateBalances: await predicate.getBalances(), - }); - // #region send-and-spend-funds-from-predicates-8 - const txId = await predicate.getTransferTransactionId( + const txId = await predicate.getTransferTxId( receiverWallet.address, amountToPredicate - 150_000, BaseAssetId, @@ -71,7 +66,9 @@ describe(__filename, () => { gasPrice, } ); + // #endregion send-and-spend-funds-from-predicates-8 + // #region send-and-spend-funds-from-predicates-5 const tx2 = await predicate.transfer( receiverWallet.address, amountToPredicate - 150_000, @@ -83,10 +80,8 @@ describe(__filename, () => { await tx2.waitForResult(); // #endregion send-and-spend-funds-from-predicates-5 - const txIdFromExecutedTx = tx2.id; - // #endregion send-and-spend-funds-from-predicates-8 expect(txId).toEqual(txIdFromExecutedTx); }); diff --git a/apps/docs/src/guide/predicates/send-and-spend-funds-from-predicates.md b/apps/docs/src/guide/predicates/send-and-spend-funds-from-predicates.md index 337b0d79d8c..68c8695edd3 100644 --- a/apps/docs/src/guide/predicates/send-and-spend-funds-from-predicates.md +++ b/apps/docs/src/guide/predicates/send-and-spend-funds-from-predicates.md @@ -38,6 +38,12 @@ Note the method transfer has two parameters: the recipient's address and the int Once the predicate resolves with a return value `true` based on its predefined condition, our predicate successfully spends its funds by means of a transfer to a desired wallet. +--- + +You can also use the `getTransferTxId` helper to obtain the transaction ID of the transfer beforehand, without actually executing it. + +<<< @/../../docs-snippets/src/guide/predicates/send-and-spend-funds-from-predicates.test.ts#send-and-spend-funds-from-predicates-8{ts:line-numbers} + ## Spending Entire Predicate Held Amount Trying to forward the entire amount held by the predicate results in an error because no funds are left to cover the transaction fees. Attempting this will result in an error message like: diff --git a/packages/predicate/src/predicate.ts b/packages/predicate/src/predicate.ts index 95d6d2d8a75..a572e4ef2f9 100644 --- a/packages/predicate/src/predicate.ts +++ b/packages/predicate/src/predicate.ts @@ -18,7 +18,7 @@ import type { TransactionRequestLike, TransactionResponse, } from '@fuel-ts/providers'; -import { ScriptTransactionRequest, transactionRequestify } from '@fuel-ts/providers'; +import { transactionRequestify } from '@fuel-ts/providers'; import { ByteArrayCoder, InputType } from '@fuel-ts/transactions'; import type { TxParamsType } from '@fuel-ts/wallet'; import { Account } from '@fuel-ts/wallet'; @@ -105,7 +105,7 @@ export class Predicate extends Account implements Abs * @param txParams - The transaction parameters (gasLimit, gasPrice, maturity). * @returns A promise that resolves to the transaction ID. */ - async getTransferTransactionId( + async getTransferTxId( /** Address of the destination */ destination: AbstractAddress, /** Amount of coins */ @@ -115,7 +115,7 @@ export class Predicate extends Account implements Abs /** Tx Params */ txParams: TxParamsType = {} ): Promise { - const request = await super.prepareTxRequestForIdCalculation( + const request = await super.prepareTransferTxRequestForIdCalculation( destination, amount, assetId, diff --git a/packages/wallet/src/account.test.ts b/packages/wallet/src/account.test.ts index fe1b659ad9e..0367187d67c 100644 --- a/packages/wallet/src/account.test.ts +++ b/packages/wallet/src/account.test.ts @@ -458,23 +458,4 @@ describe('Account', () => { expect(simulate.mock.calls.length).toBe(1); expect(simulate.mock.calls[0][0]).toEqual(transactionRequest); }); - - it('getTransferTransactionId should return the transaction id', async () => { - const amount = bn(1); - const assetId = '0x0101010101010101010101010101010101010101010101010101010101010101'; - const destination = Address.fromAddressOrString( - '0x0101010101010101010101010101010101010101000000000000000000000000' - ); - const txParam: Pick = { - gasLimit: bn(1), - gasPrice: bn(1), - maturity: 1, - }; - const account = new Account( - '0x09c0b2d1a486c439a87bcba6b46a7a1a23f3897cc83a94521a96da5c23bc58db', - provider - ); - const txId = await account.getTransferTransactionId(destination, amount, assetId, txParam); - expect(txId.length).toEqual(66); - }); }); diff --git a/packages/wallet/src/account.ts b/packages/wallet/src/account.ts index 6c7208c26c2..f35c9fafe51 100644 --- a/packages/wallet/src/account.ts +++ b/packages/wallet/src/account.ts @@ -1,7 +1,6 @@ import { Address } from '@fuel-ts/address'; import { BaseAssetId } from '@fuel-ts/address/configs'; import { ErrorCode, FuelError } from '@fuel-ts/errors'; -import { hashTransaction } from '@fuel-ts/hasher'; import { AbstractAccount } from '@fuel-ts/interfaces'; import type { AbstractAddress } from '@fuel-ts/interfaces'; import type { BigNumberish, BN } from '@fuel-ts/math'; @@ -244,31 +243,23 @@ export class Account extends AbstractAccount { const params: TxParamsType = { gasLimit: maxGasPerTx, ...txParams }; const request = new ScriptTransactionRequest(params); request.addCoinOutput(destination, amount, assetId); - console.log({ - inputs1: JSON.stringify(request.inputs), - length: request.inputs.length, - }); const { maxFee, requiredQuantities } = await this.provider.getTransactionCost(request); await this.fund(request, requiredQuantities, maxFee); - console.log({ - inputs2: JSON.stringify(request.inputs), - length: request.inputs.length, - }); return this.sendTransaction(request); } /** - * Returns the transaction ID for a transfer transaction, without sending it. + * A helper that prepares a transaction request for calculating the transaction ID. * * @param destination - The address of the destination. * @param amount - The amount of coins to transfer. * @param assetId - The asset ID of the coins to transfer. * @param txParams - The transaction parameters (gasLimit, gasPrice, maturity). - * @returns A promise that resolves to the transaction ID. + * @returns A promise that resolves to the prepared transaction request. */ - protected async prepareTxRequestForIdCalculation( + protected async prepareTransferTxRequestForIdCalculation( /** Address of the destination */ destination: AbstractAddress, /** Amount of coins */ @@ -282,16 +273,8 @@ export class Account extends AbstractAccount { const params: TxParamsType = { gasLimit: maxGasPerTx, ...txParams }; const request = new ScriptTransactionRequest(params); request.addCoinOutput(destination, amount, assetId); - console.log({ - inputs3: JSON.stringify(request.inputs), - length: request.inputs.length, - }); const { maxFee, requiredQuantities } = await this.provider.getTransactionCost(request); await this.fund(request, requiredQuantities, maxFee); - console.log({ - inputs4: JSON.stringify(request.inputs), - length: request.inputs.length, - }); return request; } From d97425478f4d3db9aaaa6cb4f580985377603ba7 Mon Sep 17 00:00:00 2001 From: Dhaiwat Date: Tue, 28 Nov 2023 23:36:08 +0800 Subject: [PATCH 3/4] add changeset --- .changeset/kind-glasses-brake.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/kind-glasses-brake.md diff --git a/.changeset/kind-glasses-brake.md b/.changeset/kind-glasses-brake.md new file mode 100644 index 00000000000..72349c14eda --- /dev/null +++ b/.changeset/kind-glasses-brake.md @@ -0,0 +1,6 @@ +--- +"@fuel-ts/predicate": patch +"@fuel-ts/wallet": patch +--- + +New helper method `Predicate.getTransferTxId`, which lets you calculate the transaction ID for a Predicate.transfer transaction, before actually sending it. From 3e34b3c8cf764a07b5d595085c89503526a231ad Mon Sep 17 00:00:00 2001 From: Dhaiwat Date: Wed, 29 Nov 2023 08:10:54 +0800 Subject: [PATCH 4/4] refactor --- packages/predicate/src/predicate.ts | 7 +------ packages/wallet/src/account.ts | 11 ++--------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/packages/predicate/src/predicate.ts b/packages/predicate/src/predicate.ts index a572e4ef2f9..673337bc402 100644 --- a/packages/predicate/src/predicate.ts +++ b/packages/predicate/src/predicate.ts @@ -115,12 +115,7 @@ export class Predicate extends Account implements Abs /** Tx Params */ txParams: TxParamsType = {} ): Promise { - const request = await super.prepareTransferTxRequestForIdCalculation( - destination, - amount, - assetId, - txParams - ); + const request = await super.prepareTransferTxRequest(destination, amount, assetId, txParams); const populatedRequest = this.populateTransactionPredicateData(request); return hashTransaction(populatedRequest, this.provider.getChainId()); } diff --git a/packages/wallet/src/account.ts b/packages/wallet/src/account.ts index f35c9fafe51..18bbedbc512 100644 --- a/packages/wallet/src/account.ts +++ b/packages/wallet/src/account.ts @@ -239,14 +239,7 @@ export class Account extends AbstractAccount { /** Tx Params */ txParams: TxParamsType = {} ): Promise { - const { maxGasPerTx } = this.provider.getGasConfig(); - const params: TxParamsType = { gasLimit: maxGasPerTx, ...txParams }; - const request = new ScriptTransactionRequest(params); - request.addCoinOutput(destination, amount, assetId); - const { maxFee, requiredQuantities } = await this.provider.getTransactionCost(request); - - await this.fund(request, requiredQuantities, maxFee); - + const request = await this.prepareTransferTxRequest(destination, amount, assetId, txParams); return this.sendTransaction(request); } @@ -259,7 +252,7 @@ export class Account extends AbstractAccount { * @param txParams - The transaction parameters (gasLimit, gasPrice, maturity). * @returns A promise that resolves to the prepared transaction request. */ - protected async prepareTransferTxRequestForIdCalculation( + protected async prepareTransferTxRequest( /** Address of the destination */ destination: AbstractAddress, /** Amount of coins */