diff --git a/.changeset/orange-cherries-clean.md b/.changeset/orange-cherries-clean.md new file mode 100644 index 0000000000..c6216827fb --- /dev/null +++ b/.changeset/orange-cherries-clean.md @@ -0,0 +1,5 @@ +--- +"@fuel-ts/account": patch +--- + +feat: implement `sendAndAwaitStatus` subscription diff --git a/apps/docs/.vitepress/config.ts b/apps/docs/.vitepress/config.ts index 05ea76b776..3a14a57d85 100644 --- a/apps/docs/.vitepress/config.ts +++ b/apps/docs/.vitepress/config.ts @@ -336,10 +336,6 @@ export default defineConfig({ text: 'Transaction Request', link: '/guide/transactions/transaction-request', }, - { - text: 'Transaction Response', - link: '/guide/transactions/transaction-response', - }, { text: 'Transaction Parameters', link: '/guide/transactions/transaction-parameters', @@ -348,6 +344,14 @@ export default defineConfig({ text: 'Transaction Policies', link: '/guide/transactions/transaction-policies', }, + { + text: 'Transaction Response', + link: '/guide/transactions/transaction-response', + }, + { + text: 'Transaction Subscriptions', + link: '/guide/transactions/transaction-subscriptions', + }, ], }, { diff --git a/apps/docs/spell-check-custom-words.txt b/apps/docs/spell-check-custom-words.txt index bd79fd9cf8..ea86d3e55f 100644 --- a/apps/docs/spell-check-custom-words.txt +++ b/apps/docs/spell-check-custom-words.txt @@ -344,4 +344,7 @@ Workspaces WSL XOR XORs -YAML \ No newline at end of file +YAML +TransactionRequest +TransactionResponse +frictionless \ No newline at end of file diff --git a/apps/docs/src/guide/transactions/snippets/transaction-subscriptions/contract-call.ts b/apps/docs/src/guide/transactions/snippets/transaction-subscriptions/contract-call.ts new file mode 100644 index 0000000000..22e41d7e50 --- /dev/null +++ b/apps/docs/src/guide/transactions/snippets/transaction-subscriptions/contract-call.ts @@ -0,0 +1,31 @@ +import { Provider, Wallet } from 'fuels'; + +import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from '../../../../env'; +import { CounterFactory } from '../../../../typegend'; + +const provider = new Provider(LOCAL_NETWORK_URL); +const wallet = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider); + +const deploy = await CounterFactory.deploy(wallet); +const { contract } = await deploy.waitForResult(); + +// #region main +// Create a new transaction request from a contract call +const txRequest = await contract.functions + .increment_count(1) + .getTransactionRequest(); + +// Fund the transaction +await txRequest.autoCost(wallet); + +// Sign the transaction +const txSignature = await wallet.signTransaction(txRequest); +txRequest.updateWitnessByOwner(wallet.address, txSignature); + +// Send the transaction and await it's result via the opened subscription +const result = await provider.sendTransactionAndAwaitStatus(txRequest); +// #endregion main + +console.log('transactionId', result.id); +console.log('status', result.status); +console.log('receipts', result.receipts); diff --git a/apps/docs/src/guide/transactions/snippets/transaction-subscriptions/transaction-request.ts b/apps/docs/src/guide/transactions/snippets/transaction-subscriptions/transaction-request.ts new file mode 100644 index 0000000000..23ae5806db --- /dev/null +++ b/apps/docs/src/guide/transactions/snippets/transaction-subscriptions/transaction-request.ts @@ -0,0 +1,31 @@ +import { Provider, ScriptTransactionRequest, Wallet } from 'fuels'; + +import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from '../../../../env'; + +const provider = new Provider(LOCAL_NETWORK_URL); +const wallet = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider); +const recipient = Wallet.generate(); + +// #region main +// Create a new transaction request +const txRequest = new ScriptTransactionRequest(); +txRequest.addCoinOutput( + recipient.address, + 1_000, + await provider.getBaseAssetId() +); + +// Fund the transaction +await txRequest.autoCost(wallet); + +// Sign the transaction +const txSignature = await wallet.signTransaction(txRequest); +txRequest.updateWitnessByOwner(wallet.address, txSignature); + +// Send the transaction and await it's result via the opened subscription +const result = await provider.sendTransactionAndAwaitStatus(txRequest); +// #endregion main + +console.log('transactionId', result.id); +console.log('status', result.status); +console.log('receipts', result.receipts); diff --git a/apps/docs/src/guide/transactions/transaction-subscriptions.md b/apps/docs/src/guide/transactions/transaction-subscriptions.md new file mode 100644 index 0000000000..5534558fba --- /dev/null +++ b/apps/docs/src/guide/transactions/transaction-subscriptions.md @@ -0,0 +1,16 @@ +# Transaction Subscriptions + +When submitting transactions via the SDK, usually this is done by calling a method in a [script](../scripts/running-scripts.md) or [contract](../contracts/methods.md#call), or sending a transaction via a [predicate](../predicates/methods.md#sendtransaction) or [wallet](../wallets/index.md). These methods submit the transaction and then return a [TransactionResponse](./transaction-response.md) that allows you to view the result of a transaction at your convenience, leaving the rest of your app processing unblocked. + +However, if you want to send a transaction and wait until it's processed, for convenience the SDK also exposes the `sendTransactionAndAwaitStatus` available on a [Provider](../provider/index.md) which behaves the same as `sendTransaction` but waits until the transaction is processed by the node and then returns the transaction result. + +This functionality can be used like so: + +<<< @./snippets/transaction-subscriptions/transaction-request.ts#main{ts:line-numbers} + +Or used with a contract call like so: + +<<< @./snippets/transaction-subscriptions/contract-call.ts#main{ts:line-numbers} + +> [!NOTE] Note +> This is a blocking call, which means the rest of your app logic could be blocked for several seconds until the transaction is processed. If this is a problem for your app then we recommend using the previous submission methods mentioned in this guide. diff --git a/packages/account/src/providers/provider.test.ts b/packages/account/src/providers/provider.test.ts index 49cb704f0d..c72bd13fb8 100644 --- a/packages/account/src/providers/provider.test.ts +++ b/packages/account/src/providers/provider.test.ts @@ -2262,4 +2262,62 @@ Supported fuel-core version: ${mock.supportedVersion}.` expect(fetchChainAndNodeInfo).toHaveBeenCalledTimes(2); }); + + it('submits transaction and awaits status [success]', async () => { + using launched = await setupTestProviderAndWallets(); + const { + provider, + wallets: [wallet], + } = launched; + + const transactionRequest = await wallet.createTransfer(wallet.address, 100_000); + const signedTransaction = await wallet.signTransaction(transactionRequest); + transactionRequest.updateWitnessByOwner(wallet.address, signedTransaction); + const transactionId = transactionRequest.getTransactionId(await provider.getChainId()); + const response = await provider.sendTransactionAndAwaitStatus(transactionRequest, { + estimateTxDependencies: false, + }); + expect(response.status).toBe('success'); + expect(response.receipts.length).not.toBe(0); + expect(response.id).toBe(transactionId); + }); + + it('submits transaction and awaits status [success with estimation]', async () => { + using launched = await setupTestProviderAndWallets(); + const { + provider, + wallets: [wallet], + } = launched; + + const transactionRequest = await wallet.createTransfer(wallet.address, 100_000); + const signedTransaction = await wallet.signTransaction(transactionRequest); + transactionRequest.updateWitnessByOwner(wallet.address, signedTransaction); + const transactionId = transactionRequest.getTransactionId(await provider.getChainId()); + const response = await provider.sendTransactionAndAwaitStatus(transactionRequest); + expect(response.status).toBe('success'); + expect(response.receipts.length).not.toBe(0); + expect(response.id).toBe(transactionId); + }); + + it('submits transaction and awaits status [failure]', async () => { + using launched = await setupTestProviderAndWallets(); + const { + provider, + wallets: [wallet], + } = launched; + + const transactionRequest = await wallet.createTransfer(wallet.address, 100_000); + transactionRequest.gasLimit = bn(0); // force fail + const signedTransaction = await wallet.signTransaction(transactionRequest); + transactionRequest.updateWitnessByOwner(wallet.address, signedTransaction); + await expectToThrowFuelError( + () => + provider.sendTransactionAndAwaitStatus(transactionRequest, { + estimateTxDependencies: false, + }), + { + code: ErrorCode.SCRIPT_REVERTED, + } + ); + }); }); diff --git a/packages/account/src/providers/provider.ts b/packages/account/src/providers/provider.ts index 5cd0cddcd8..2a7543da94 100644 --- a/packages/account/src/providers/provider.ts +++ b/packages/account/src/providers/provider.ts @@ -50,7 +50,7 @@ import { isTransactionTypeScript, transactionRequestify, } from './transaction-request'; -import type { TransactionResultReceipt } from './transaction-response'; +import type { TransactionResult, TransactionResultReceipt } from './transaction-response'; import { TransactionResponse, getDecodedLogs } from './transaction-response'; import { processGqlReceipt } from './transaction-summary/receipt'; import { @@ -880,6 +880,22 @@ Supported fuel-core version: ${supportedVersion}.` return new TransactionResponse(transactionRequest, this, chainId, abis, subscription); } + /** + * Submits a transaction to the chain and awaits its status response. + * + * @param transactionRequestLike - the request to submit. + * @param sendTransactionParams - The provider send transaction parameters (optional). + * @returns A promise that resolves to a settled transaction. + */ + async sendTransactionAndAwaitStatus( + transactionRequestLike: TransactionRequestLike, + providerSendTxParams: ProviderSendTxParams = {} + ): Promise> { + const response = await this.sendTransaction(transactionRequestLike, providerSendTxParams); + const result = await response.waitForResult(); + return result; + } + /** * Executes a transaction without actually submitting it to the chain. *