Repository: FuelLabs/fuel-core (Rust)

Issue:
Make `submitAndAwait` errors immediately identifiable
I've been trying to implement support for the `submitAndAwait` subscription but came across a problem related to invalid transactions. The current workflow of the TS SDK is as follows:
- Submit a transaction via the `submit` mutation
	- If the transaction is invalid, throw immediately
	- Else, continue

Because the `submit` endpoint is a mutation, its response is immediate - either it fails or succeeds in tx submission.

When implementing `submitAndAwait`, I wanted to have behavior similar to the one described above:
- If the transaction is invalid, throw immediately
- Else, return an async iterator which has a `.next()` method which the user can call to await the subscription whenever they see fit

The problem I encountered is that I can't know if a transaction validity error happened until I read the response body of the subscription, but the response isn't necessarily immediate because the transaction might _not_ have failed and is awaiting execution, in which case me trying to read the body would hang until the status update is streamed, which will block us from returning the iterator to the user.

The problem in code looks like this:
```ts
/**
* Create a connection.
* This will be a 200 OK response that has no info itself on if the transaction failed.
* All the info is in the response body.
*/
const response = await fetchFn(`${url}-sub`, {
  method: 'POST',  
  body, 
  headers,
});

/**
* The body is a readable byte stream.
* We `tee` it to get two independent streams that have the same underlying source from the subscription, 
* so that we can read the error while retaining one stream that we return to the user at the end.
* Note that reading the body is an inherently asynchronous operation, 
* regardless if the request is an SSE or a "regular" request.
* The only difference is that the byte stream is immediately closed once read in regular requests, 
* but it remains open in SSE until the fuel-core node closes it.
*/
const [errorReader, resultReader] = response.body.tee().map((stream) => stream.getReader());

/**
* FuelGraphqlSubscriber is an iterator that is able to read subscriptions returned from our fuel-core nodes.
*/
const errorChecker = new FuelGraphqlSubscriber(errorReader);

/**
* We call the .next() method to check if there's an error.
* This will throw if the transaction is invalid - which is what we want,
* but it will also hang if the transaction was submitted until its status updates.
* The hanging is a problem because it blocks the user 
* and defeats the purpose of the async iterator returned at the end.
*/
await errorChecker.next();

return new FuelGraphqlSubscriber(resultReader);
```

After pondering about it, two solutions came to mind:
1. If the transaction fails, add some response header indicating that it failed so that we can do an if/else immediately
```ts
if(response.headers.has('transaction-failed') {
  // will certainly throw immediately
  await errorChecker.next();
}
```
2. Instead of [awaiting the commit](https://github.com/FuelLabs/fuel-core/blob/b1f2bf445a2e9a270f1c0032f20c680d8e15d251/crates/client/src/client.rs#L465) and not returning anything until the status changes, return a stream [as is done](https://github.com/FuelLabs/fuel-core/blob/b1f2bf445a2e9a270f1c0032f20c680d8e15d251/crates/client/src/client.rs#L631) for `statusChange` . This would _immediately_ return a `SubmittedStatus` response, which is great because we would then _know_ that we'll get a response immediately from the node and can check if it's an error or not.
```ts
// will resolve immediately
// either by throwing or reading the `SubmittedStatus` which we'll just discard
await errorChecker.next(); 
```

I am more inclined to the second solution because the `SubmittedStatus` also contains the time of submission which may be of interest for some.


Discussion: I think we could add a new endpoint - `submit_and_await_status`. Which will also return the `SubmittedStatus` status.



Fix (patch):
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 782f326d2c1..b89e8df23a5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
 ### Changed
 - [2113](https://github.com/FuelLabs/fuel-core/pull/2113): Modify the way the gas price service and shared algo is initialized to have some default value based on best guess instead of `None`, and initialize service before graphql.
 - [2112](https://github.com/FuelLabs/fuel-core/pull/2112): Alter the way the sealed blocks are fetched with a given height.
+- [2120](https://github.com/FuelLabs/fuel-core/pull/2120): Added `submitAndAwaitStatus` subscription endpoint which returns the `SubmittedStatus` after the transaction is submitted as well as the `TransactionStatus` subscription.
 - [2115](https://github.com/FuelLabs/fuel-core/pull/2115): Add test for `SignMode` `is_available` method.
 - [2124](https://github.com/FuelLabs/fuel-core/pull/2124): Generalize the way p2p req/res protocol handles requests.
 
diff --git a/crates/client/assets/schema.sdl b/crates/client/assets/schema.sdl
index eb9f99325f9..8929f76b0ef 100644
--- a/crates/client/assets/schema.sdl
+++ b/crates/client/assets/schema.sdl
@@ -1136,6 +1136,12 @@ type Subscription {
 	Submits transaction to the `TxPool` and await either confirmation or failure.
 	"""
 	submitAndAwait(tx: HexString!): TransactionStatus!
+	"""
+	Submits the transaction to the `TxPool` and returns a stream of events.
+	Compared to the `submitAndAwait`, the stream also contains `
+	SubmittedStatus` as an intermediate state.
+	"""
+	submitAndAwaitStatus(tx: HexString!): TransactionStatus!
 }
 
 type SuccessStatus {
diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs
index ab04d18ebba..6f4c7c18194 100644
--- a/crates/client/src/client.rs
+++ b/crates/client/src/client.rs
@@ -63,7 +63,10 @@ use fuel_core_types::{
     },
 };
 #[cfg(feature = "subscriptions")]
-use futures::StreamExt;
+use futures::{
+    Stream,
+    StreamExt,
+};
 use itertools::Itertools;
 use pagination::{
     PageDirection,
@@ -527,6 +530,30 @@ impl FuelClient {
         Ok(status)
     }
 
+    /// Submits the transaction to the `TxPool` and returns a stream of events.
+    /// Compared to the `submit_and_await_commit`, the stream also contains
+    /// `SubmittedStatus` as an intermediate state.
+    #[cfg(feature = "subscriptions")]
+    pub async fn submit_and_await_status(
+        &self,
+        tx: &Transaction,
+    ) -> io::Result<impl Stream<Item = io::Result<TransactionStatus>>> {
+        use cynic::SubscriptionBuilder;
+        let tx = tx.clone().to_bytes();
+        let s = schema::tx::SubmitAndAwaitStatusSubscription::build(TxArg {
+            tx: HexString(Bytes(tx)),
+        });
+
+        let stream = self.subscribe(s).await?.map(
+            |r: io::Result<schema::tx::SubmitAndAwaitStatusSubscription>| {
+                let status: TransactionStatus = r?.submit_and_await_status.try_into()?;
+                Result::<_, io::Error>::Ok(status)
+            },
+        );
+
+        Ok(stream)
+    }
+
     pub async fn start_session(&self) -> io::Result<String> {
         let query = schema::StartSession::build(());
 
diff --git a/crates/client/src/client/schema/tx.rs b/crates/client/src/client/schema/tx.rs
index 6c89c230596..f97eb4aff41 100644
--- a/crates/client/src/client/schema/tx.rs
+++ b/crates/client/src/client/schema/tx.rs
@@ -432,6 +432,17 @@ pub struct SubmitAndAwaitSubscription {
     pub submit_and_await: TransactionStatus,
 }
 
+#[derive(cynic::QueryFragment, Clone, Debug)]
+#[cynic(
+    schema_path = "./assets/schema.sdl",
+    graphql_type = "Subscription",
+    variables = "TxArg"
+)]
+pub struct SubmitAndAwaitStatusSubscription {
+    #[arguments(tx: $tx)]
+    pub submit_and_await_status: TransactionStatus,
+}
+
 #[derive(cynic::QueryFragment, Clone, Debug)]
 #[cynic(schema_path = "./assets/schema.sdl", graphql_type = "Query")]
 pub struct AllReceipts {
diff --git a/crates/fuel-core/src/schema/tx.rs b/crates/fuel-core/src/schema/tx.rs
index 31e32044a19..1ee15bd46c8 100644
--- a/crates/fuel-core/src/schema/tx.rs
+++ b/crates/fuel-core/src/schema/tx.rs
@@ -52,8 +52,10 @@ use fuel_core_types::{
         Transaction as FuelTx,
         UniqueIdentifier,
     },
-    fuel_types,
-    fuel_types::canonical::Deserialize,
+    fuel_types::{
+        self,
+        canonical::Deserialize,
+    },
     fuel_vm::checked_transaction::{
         CheckPredicateParams,
         EstimatePredicates,
@@ -375,41 +377,62 @@ impl TxStatusSubscription {
     #[graphql(complexity = "QUERY_COSTS.submit_and_await + child_complexity")]
     async fn submit_and_await<'a>(
         &self,
-        ctx: &Context<'a>,
+        ctx: &'a Context<'a>,
         tx: HexString,
     ) -> async_graphql::Result<
         impl Stream<Item = async_graphql::Result<TransactionStatus>> + 'a,
     > {
-        let txpool = ctx.data_unchecked::<TxPool>();
-        let params = ctx
-            .data_unchecked::<ConsensusProvider>()
-            .latest_consensus_params();
-        let tx = FuelTx::from_bytes(&tx.0)?;
-        let tx_id = tx.id(&params.chain_id());
-        let subscription = txpool.tx_update_subscribe(tx_id)?;
-
-        let _: Vec<_> = txpool
-            .insert(vec![Arc::new(tx)])
-            .await
-            .into_iter()
-            .try_collect()?;
+        let subscription = submit_and_await_status(ctx, tx).await?;
 
         Ok(subscription
-            .skip_while(|event| {
-                matches!(
-                    event,
-                    TxStatusMessage::Status(txpool::TransactionStatus::Submitted { .. })
-                )
-            })
-            .map(move |event| match event {
-                TxStatusMessage::Status(status) => {
-                    let status = TransactionStatus::new(tx_id, status);
-                    Ok(status)
-                }
-                TxStatusMessage::FailedStatus => {
-                    Err(anyhow::anyhow!("Failed to get transaction status").into())
-                }
-            })
+            .skip_while(|event| matches!(event, Ok(TransactionStatus::Submitted(..))))
             .take(1))
     }
+
+    /// Submits the transaction to the `TxPool` and returns a stream of events.
+    /// Compared to the `submitAndAwait`, the stream also contains `
+    /// SubmittedStatus` as an intermediate state.
+    #[graphql(complexity = "QUERY_COSTS.submit_and_await + child_complexity")]
+    async fn submit_and_await_status<'a>(
+        &self,
+        ctx: &'a Context<'a>,
+        tx: HexString,
+    ) -> async_graphql::Result<
+        impl Stream<Item = async_graphql::Result<TransactionStatus>> + 'a,
+    > {
+        submit_and_await_status(ctx, tx).await
+    }
+}
+
+async fn submit_and_await_status<'a>(
+    ctx: &'a Context<'a>,
+    tx: HexString,
+) -> async_graphql::Result<
+    impl Stream<Item = async_graphql::Result<TransactionStatus>> + 'a,
+> {
+    let txpool = ctx.data_unchecked::<TxPool>();
+    let params = ctx
+        .data_unchecked::<ConsensusProvider>()
+        .latest_consensus_params();
+    let tx = FuelTx::from_bytes(&tx.0)?;
+    let tx_id = tx.id(&params.chain_id());
+    let subscription = txpool.tx_update_subscribe(tx_id)?;
+
+    let _: Vec<_> = txpool
+        .insert(vec![Arc::new(tx)])
+        .await
+        .into_iter()
+        .try_collect()?;
+
+    Ok(subscription
+        .map(move |event| match event {
+            TxStatusMessage::Status(status) => {
+                let status = TransactionStatus::new(tx_id, status);
+                Ok(status)
+            }
+            TxStatusMessage::FailedStatus => {
+                Err(anyhow::anyhow!("Failed to get transaction status").into())
+            }
+        })
+        .take(2))
 }