diff --git a/.changeset/fresh-terms-yell.md b/.changeset/fresh-terms-yell.md new file mode 100644 index 00000000000..d3600a5c0b4 --- /dev/null +++ b/.changeset/fresh-terms-yell.md @@ -0,0 +1,10 @@ +--- +"@fuel-ts/program": minor +"@fuel-ts/providers": minor +"@fuel-ts/wallet": minor +"@fuel-ts/predicate": minor +--- + +- Transaction execution can now be await with the `{awaitExecution: true}` option on `Provider.sendTransaction` +- Added same functionality to accounts (unlocked wallet, predicate) +- `BaseInvocationScope` internally now uses `{awaitExecution: true}` to reduce amount of network calls diff --git a/apps/docs-snippets/src/utils.ts b/apps/docs-snippets/src/utils.ts index aa21997bc55..06e69e3bbd7 100644 --- a/apps/docs-snippets/src/utils.ts +++ b/apps/docs-snippets/src/utils.ts @@ -45,11 +45,7 @@ export const getTestWallet = async (seedQuantities?: CoinQuantityLike[]) => { // funding the transaction with the required quantities await genesisWallet.fund(request, requiredQuantities, minFee); - // execute the transaction, transferring resources to the test wallet - const response = await genesisWallet.sendTransaction(request); - - // wait for the transaction to be confirmed - await response.wait(); + await genesisWallet.sendTransaction(request, { awaitExecution: true }); // return the test wallet return testWallet; diff --git a/packages/contract/src/contract-factory.ts b/packages/contract/src/contract-factory.ts index 430cff694f7..6838e6fbfe9 100644 --- a/packages/contract/src/contract-factory.ts +++ b/packages/contract/src/contract-factory.ts @@ -152,8 +152,9 @@ export default class ContractFactory { transactionRequest.maxFee = this.account.provider.getGasConfig().maxGasPerTx; await this.account.fund(transactionRequest, requiredQuantities, maxFee); - const response = await this.account.sendTransaction(transactionRequest); - await response.wait(); + await this.account.sendTransaction(transactionRequest, { + awaitExecution: true, + }); return new Contract(contractId, this.interface, this.account); } diff --git a/packages/fuel-gauge/src/await-execution.test.ts b/packages/fuel-gauge/src/await-execution.test.ts new file mode 100644 index 00000000000..ea5b31b7df3 --- /dev/null +++ b/packages/fuel-gauge/src/await-execution.test.ts @@ -0,0 +1,97 @@ +import { launchNode } from '@fuel-ts/wallet/test-utils'; +import { + Provider, + WalletUnlocked, + randomBytes, + Wallet, + BaseAssetId, + FUEL_NETWORK_URL, +} from 'fuels'; + +/** + * @group node + */ +describe('await-execution', () => { + test('awaiting execution of a transaction on the provider works', async () => { + const { cleanup, ip, port } = await launchNode({ + args: ['--poa-interval-period', '400ms'], + }); + const nodeProvider = await Provider.create(`http://${ip}:${port}/graphql`); + + const genesisWallet = new WalletUnlocked( + process.env.GENESIS_SECRET || randomBytes(32), + nodeProvider + ); + + const destination = Wallet.generate({ provider: nodeProvider }); + + const transfer = await genesisWallet.createTransfer(destination.address, 100, BaseAssetId, { + gasPrice: nodeProvider.getGasConfig().minGasPrice, + gasLimit: 10_000, + }); + + transfer.updateWitnessByOwner( + genesisWallet.address, + await genesisWallet.signTransaction(transfer) + ); + + const response = await nodeProvider.sendTransaction(transfer, { awaitExecution: true }); + + expect(response.gqlTransaction?.status?.type).toBe('SuccessStatus'); + + cleanup(); + }); + + test.skip('transferring funds with awaitExecution works', async () => { + const provider = await Provider.create(FUEL_NETWORK_URL); + const genesisWallet = new WalletUnlocked( + process.env.GENESIS_SECRET || randomBytes(32), + provider + ); + + const sendTransactionSpy = vi.spyOn(provider, 'sendTransaction'); + + const destination = Wallet.generate({ provider }); + + await genesisWallet.transfer( + destination.address, + 100, + BaseAssetId, + { + gasPrice: provider.getGasConfig().minGasPrice, + gasLimit: 10_000, + } + // { awaitExecution: true } + ); + + expect(sendTransactionSpy).toHaveBeenCalledTimes(1); + const awaitExecutionArg = sendTransactionSpy.mock.calls[0][1]; + expect(awaitExecutionArg).toMatchObject({ awaitExecution: true }); + }); + + test.skip('withdrawToBaseLayer works with awaitExecution', async () => { + const provider = await Provider.create(FUEL_NETWORK_URL); + const genesisWallet = new WalletUnlocked( + process.env.GENESIS_SECRET || randomBytes(32), + provider + ); + + const sendTransactionSpy = vi.spyOn(provider, 'sendTransaction'); + + const destination = Wallet.generate({ provider }); + + await genesisWallet.withdrawToBaseLayer( + destination.address, + 100, + { + gasPrice: provider.getGasConfig().minGasPrice, + gasLimit: 10_000, + } + // { awaitExecution: true } + ); + + expect(sendTransactionSpy).toHaveBeenCalledTimes(1); + const awaitExecutionArg = sendTransactionSpy.mock.calls[0][1]; + expect(awaitExecutionArg).toMatchObject({ awaitExecution: true }); + }); +}); diff --git a/packages/fuel-gauge/src/predicate/utils/predicate/fundPredicate.ts b/packages/fuel-gauge/src/predicate/utils/predicate/fundPredicate.ts index 427a53b77ff..ec42d86e5ff 100644 --- a/packages/fuel-gauge/src/predicate/utils/predicate/fundPredicate.ts +++ b/packages/fuel-gauge/src/predicate/utils/predicate/fundPredicate.ts @@ -17,8 +17,7 @@ export const fundPredicate = async ( request.gasLimit = gasUsed; await wallet.fund(request, requiredQuantities, minFee); - const tx = await wallet.sendTransaction(request); - await tx.waitForResult(); + await wallet.sendTransaction(request, { awaitExecution: true }); return predicate.getBalance(); }; diff --git a/packages/interfaces/src/index.ts b/packages/interfaces/src/index.ts index 5aed5cd5eaa..90765b34423 100644 --- a/packages/interfaces/src/index.ts +++ b/packages/interfaces/src/index.ts @@ -60,7 +60,7 @@ export abstract class AbstractAccount { abstract address: AbstractAddress; abstract provider: unknown; abstract getResourcesToSpend(quantities: any[], options?: any): any; - abstract sendTransaction(transactionRequest: any): any; + abstract sendTransaction(transactionRequest: any, options?: any): any; abstract simulateTransaction(transactionRequest: any): any; abstract fund(transactionRequest: any, quantities: any, fee: any): Promise; } @@ -74,7 +74,7 @@ export abstract class AbstractProgram { }; abstract provider: { - sendTransaction(transactionRequest: any): any; + sendTransaction(transactionRequest: any, options?: any): any; } | null; } diff --git a/packages/predicate/src/predicate.ts b/packages/predicate/src/predicate.ts index f425290b620..d6fd4c49f3c 100644 --- a/packages/predicate/src/predicate.ts +++ b/packages/predicate/src/predicate.ts @@ -14,6 +14,7 @@ import type { BigNumberish } from '@fuel-ts/math'; import type { CallResult, Provider, + ProviderSendTxParams, TransactionRequest, TransactionRequestLike, TransactionResponse, @@ -113,9 +114,12 @@ export class Predicate extends Account implements Abs * @param transactionRequestLike - The transaction request-like object. * @returns A promise that resolves to the transaction response. */ - sendTransaction(transactionRequestLike: TransactionRequestLike): Promise { + sendTransaction( + transactionRequestLike: TransactionRequestLike, + options?: Pick + ): Promise { const transactionRequest = this.populateTransactionPredicateData(transactionRequestLike); - return super.sendTransaction(transactionRequest); + return super.sendTransaction(transactionRequest, options); } /** diff --git a/packages/program/src/functions/base-invocation-scope.ts b/packages/program/src/functions/base-invocation-scope.ts index b9857c7b78e..f0e1a777bf6 100644 --- a/packages/program/src/functions/base-invocation-scope.ts +++ b/packages/program/src/functions/base-invocation-scope.ts @@ -297,7 +297,9 @@ export class BaseInvocationScope { await this.fundWithRequiredCoins(maxFee); - const response = await this.program.account.sendTransaction(transactionRequest); + const response = await this.program.account.sendTransaction(transactionRequest, { + awaitExecution: true, + }); return FunctionInvocationResult.build( this.functionInvocationScopes, diff --git a/packages/providers/src/provider.ts b/packages/providers/src/provider.ts index cf069057944..afd51f38f60 100644 --- a/packages/providers/src/provider.ts +++ b/packages/providers/src/provider.ts @@ -251,7 +251,16 @@ export type ProviderCallParams = UTXOValidationParams & EstimateTransactionParam /** * Provider Send transaction params */ -export type ProviderSendTxParams = EstimateTransactionParams; +export type ProviderSendTxParams = EstimateTransactionParams & { + /** + * By default, the promise will resolve immediately after the transaction is submitted. + * + * If set to true, the promise will resolve only when the transaction changes status + * from `SubmittedStatus` to one of `SuccessStatus`, `FailureStatus` or `SqueezedOutStatus`. + * + */ + awaitExecution?: boolean; +}; /** * URL - Consensus Params mapping. @@ -564,7 +573,7 @@ export default class Provider { // #region Provider-sendTransaction async sendTransaction( transactionRequestLike: TransactionRequestLike, - { estimateTxDependencies = true }: ProviderSendTxParams = {} + { estimateTxDependencies = true, awaitExecution = false }: ProviderSendTxParams = {} ): Promise { const transactionRequest = transactionRequestify(transactionRequestLike); this.#cacheInputs(transactionRequest.inputs); @@ -573,7 +582,6 @@ export default class Provider { } // #endregion Provider-sendTransaction - const encodedTransaction = hexlify(transactionRequest.toTransactionBytes()); const { gasUsed, minGasPrice } = await this.getTransactionCost(transactionRequest, [], { estimateTxDependencies: false, estimatePredicates: false, @@ -595,12 +603,27 @@ export default class Provider { ); } + const encodedTransaction = hexlify(transactionRequest.toTransactionBytes()); + + if (awaitExecution) { + const subscription = this.operations.submitAndAwait({ encodedTransaction }); + for await (const { submitAndAwait } of subscription) { + if (submitAndAwait.type !== 'SubmittedStatus') { + break; + } + } + + const transactionId = transactionRequest.getTransactionId(this.getChainId()); + const response = new TransactionResponse(transactionId, this); + await response.fetch(); + return response; + } + const { submit: { id: transactionId }, } = await this.operations.submit({ encodedTransaction }); - const response = new TransactionResponse(transactionId, this); - return response; + return new TransactionResponse(transactionId, this); } /** diff --git a/packages/providers/src/transaction-response/transaction-response.ts b/packages/providers/src/transaction-response/transaction-response.ts index ac5676e50aa..a0731887028 100644 --- a/packages/providers/src/transaction-response/transaction-response.ts +++ b/packages/providers/src/transaction-response/transaction-response.ts @@ -197,14 +197,12 @@ export class TransactionResponse { return transactionSummary; } - /** - * Waits for transaction to complete and returns the result. - * - * @returns The completed transaction result - */ - async waitForResult( - contractsAbiMap?: AbiMap - ): Promise> { + private async waitForStatusChange() { + const status = this.gqlTransaction?.status?.type; + if (status && status !== 'SubmittedStatus') { + return; + } + const subscription = this.provider.operations.statusChange({ transactionId: this.id, }); @@ -216,6 +214,17 @@ export class TransactionResponse { } await this.fetch(); + } + + /** + * Waits for transaction to complete and returns the result. + * + * @returns The completed transaction result + */ + async waitForResult( + contractsAbiMap?: AbiMap + ): Promise> { + await this.waitForStatusChange(); const transactionSummary = await this.getTransactionSummary(contractsAbiMap); diff --git a/packages/wallet/src/account.ts b/packages/wallet/src/account.ts index c2424033145..fdb5a204afd 100644 --- a/packages/wallet/src/account.ts +++ b/packages/wallet/src/account.ts @@ -18,6 +18,7 @@ import type { TransactionResponse, Provider, ScriptTransactionRequestLike, + ProviderSendTxParams, } from '@fuel-ts/providers'; import { withdrawScript, @@ -431,11 +432,15 @@ export class Account extends AbstractAccount { * @returns A promise that resolves to the transaction response. */ async sendTransaction( - transactionRequestLike: TransactionRequestLike + transactionRequestLike: TransactionRequestLike, + options?: Pick ): Promise { const transactionRequest = transactionRequestify(transactionRequestLike); await this.provider.estimateTxDependencies(transactionRequest); - return this.provider.sendTransaction(transactionRequest, { estimateTxDependencies: false }); + return this.provider.sendTransaction(transactionRequest, { + ...options, + estimateTxDependencies: false, + }); } /** diff --git a/packages/wallet/src/base-unlocked-wallet.ts b/packages/wallet/src/base-unlocked-wallet.ts index d26627465de..314663fcf49 100644 --- a/packages/wallet/src/base-unlocked-wallet.ts +++ b/packages/wallet/src/base-unlocked-wallet.ts @@ -4,6 +4,7 @@ import type { TransactionRequestLike, CallResult, Provider, + ProviderSendTxParams, } from '@fuel-ts/providers'; import { transactionRequestify } from '@fuel-ts/providers'; import { Signer } from '@fuel-ts/signer'; @@ -110,13 +111,14 @@ export class BaseWalletUnlocked extends Account { * @returns A promise that resolves to the TransactionResponse object. */ async sendTransaction( - transactionRequestLike: TransactionRequestLike + transactionRequestLike: TransactionRequestLike, + options?: Pick ): Promise { const transactionRequest = transactionRequestify(transactionRequestLike); await this.provider.estimateTxDependencies(transactionRequest); return this.provider.sendTransaction( await this.populateTransactionWitnessesSignature(transactionRequest), - { estimateTxDependencies: false } + { ...options, estimateTxDependencies: false } ); } diff --git a/packages/wallet/src/test-utils/seedTestWallet.ts b/packages/wallet/src/test-utils/seedTestWallet.ts index e2cb1d9bdd3..4ea14beb95f 100644 --- a/packages/wallet/src/test-utils/seedTestWallet.ts +++ b/packages/wallet/src/test-utils/seedTestWallet.ts @@ -27,7 +27,5 @@ export const seedTestWallet = async (wallet: Account, quantities: CoinQuantityLi quantities .map(coinQuantityfy) .forEach(({ amount, assetId }) => request.addCoinOutput(wallet.address, amount, assetId)); - const response = await genesisWallet.sendTransaction(request); - - await response.wait(); + await genesisWallet.sendTransaction(request, { awaitExecution: true }); };