Skip to content

Commit 2c09e63

Browse files
AurelienFThal3e
andauthored
feat: add support for preconfirmations (#1644)
Closes #1642 # Release notes Add support of pre confirmations. In this release, we: - Added two need functions in the `Provider` to access preconfirmations `send_transaction_and_await_status` and `subscribe_transaction_status` # Checklist - [x] All **changes** are **covered** by **tests** (or not applicable) - [x] All **changes** are **documented** (or not applicable) - [x] I **reviewed** the **entire PR** myself (preferably, on GH UI) - [x] I **described** all **Breaking Changes** (or there's none) --------- Co-authored-by: hal3e <[email protected]>
1 parent 48edbfd commit 2c09e63

File tree

8 files changed

+192
-0
lines changed

8 files changed

+192
-0
lines changed

e2e/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ fuels = { workspace = true, features = [
3737
"accounts-signer-aws-kms",
3838
"test-helpers",
3939
] }
40+
futures = { workspace = true }
4041
testcontainers = { workspace = true }
4142
tokio = { workspace = true, features = ["test-util"] }
4243

e2e/tests/providers.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use fuels::{
1919
tx_status::{Success, TxStatus},
2020
},
2121
};
22+
use futures::StreamExt;
2223
use rand::thread_rng;
2324

2425
#[tokio::test]
@@ -1011,6 +1012,94 @@ async fn test_build_with_provider() -> Result<()> {
10111012
Ok(())
10121013
}
10131014

1015+
#[tokio::test]
1016+
async fn send_transaction_and_await_status() -> Result<()> {
1017+
let wallet = launch_provider_and_get_wallet().await?;
1018+
let provider = wallet.provider();
1019+
1020+
let consensus_parameters = provider.consensus_parameters().await?;
1021+
let inputs = wallet
1022+
.get_asset_inputs_for_amount(*consensus_parameters.base_asset_id(), 100, None)
1023+
.await?;
1024+
let outputs = wallet.get_asset_outputs_for_amount(
1025+
&Bech32Address::default(),
1026+
*consensus_parameters.base_asset_id(),
1027+
100,
1028+
);
1029+
1030+
// Given
1031+
let mut tb = ScriptTransactionBuilder::prepare_transfer(inputs, outputs, TxPolicies::default());
1032+
wallet.add_witnesses(&mut tb)?;
1033+
1034+
let tx = tb.build(provider).await?;
1035+
1036+
// When
1037+
let status = provider.send_transaction_and_await_status(tx, true).await?;
1038+
1039+
// Then
1040+
assert_eq!(status.len(), 3);
1041+
assert!(status.iter().enumerate().all(|(i, tx_status)| {
1042+
matches!(
1043+
(i, tx_status.clone().unwrap()),
1044+
(0, TxStatus::Submitted { .. })
1045+
| (1, TxStatus::PreconfirmationSuccess { .. })
1046+
| (2, TxStatus::Success { .. })
1047+
)
1048+
}));
1049+
Ok(())
1050+
}
1051+
1052+
#[tokio::test]
1053+
async fn send_transaction_and_subscribe_status() -> Result<()> {
1054+
let config = NodeConfig {
1055+
block_production: Trigger::Never,
1056+
..NodeConfig::default()
1057+
};
1058+
let wallet =
1059+
launch_custom_provider_and_get_wallets(WalletsConfig::default(), Some(config), None)
1060+
.await?[0]
1061+
.clone();
1062+
let provider = wallet.provider();
1063+
1064+
let consensus_parameters = provider.consensus_parameters().await?;
1065+
let inputs = wallet
1066+
.get_asset_inputs_for_amount(*consensus_parameters.base_asset_id(), 100, None)
1067+
.await?;
1068+
let outputs = wallet.get_asset_outputs_for_amount(
1069+
&Bech32Address::default(),
1070+
*consensus_parameters.base_asset_id(),
1071+
100,
1072+
);
1073+
1074+
// Given
1075+
let mut tb = ScriptTransactionBuilder::prepare_transfer(inputs, outputs, TxPolicies::default());
1076+
wallet.add_witnesses(&mut tb)?;
1077+
1078+
let tx = tb.build(provider).await?;
1079+
let tx_id = tx.id(consensus_parameters.chain_id());
1080+
1081+
// When
1082+
let mut statuses = provider.subscribe_transaction_status(&tx_id, true).await?;
1083+
let _ = provider.send_transaction(tx).await?;
1084+
1085+
// Then
1086+
assert!(matches!(
1087+
statuses.next().await.unwrap()?,
1088+
TxStatus::Submitted { .. }
1089+
));
1090+
provider.produce_blocks(1, None).await?;
1091+
assert!(matches!(
1092+
statuses.next().await.unwrap()?,
1093+
TxStatus::PreconfirmationSuccess { .. }
1094+
));
1095+
assert!(matches!(
1096+
statuses.next().await.unwrap()?,
1097+
TxStatus::Success { .. }
1098+
));
1099+
1100+
Ok(())
1101+
}
1102+
10141103
#[tokio::test]
10151104
async fn can_produce_blocks_with_trig_never() -> Result<()> {
10161105
let config = NodeConfig {

packages/fuels-accounts/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ fuel-crypto = { workspace = true, features = ["random"] }
2727
fuel-tx = { workspace = true }
2828
fuel-types = { workspace = true, features = ["random"] }
2929
fuels-core = { workspace = true, default-features = false }
30+
futures = { workspace = true}
3031
google-cloud-kms = { workspace = true, features = ["auth"], optional = true }
3132
itertools = { workspace = true }
3233
k256 = { workspace = true, features = ["ecdsa-core", "pem"] }

packages/fuels-accounts/src/provider.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ use fuels_core::{
4545
tx_status::TxStatus,
4646
},
4747
};
48+
use futures::StreamExt;
4849
pub use retry_util::{Backoff, RetryConfig};
4950
pub use supported_fuel_core_version::SUPPORTED_FUEL_CORE_VERSION;
5051
use tai64::Tai64;
@@ -207,6 +208,56 @@ impl Provider {
207208
Ok(tx_status)
208209
}
209210

211+
/// Similar to `send_transaction_and_await_commit`,
212+
/// but collect all the status received until a final one and return them.
213+
pub async fn send_transaction_and_await_status<T: Transaction>(
214+
&self,
215+
tx: T,
216+
include_preconfirmation: bool,
217+
) -> Result<Vec<Result<TxStatus>>> {
218+
#[cfg(feature = "coin-cache")]
219+
let base_asset_id = *self.consensus_parameters().await?.base_asset_id();
220+
#[cfg(feature = "coin-cache")]
221+
let used_base_coins = tx.used_coins(&base_asset_id);
222+
223+
#[cfg(feature = "coin-cache")]
224+
self.check_inputs_already_in_cache(&used_base_coins).await?;
225+
226+
let tx = self.prepare_transaction_for_sending(tx).await?.into();
227+
let mut stream = self
228+
.uncached_client()
229+
.submit_and_await_status(&tx, include_preconfirmation)
230+
.await?;
231+
232+
let mut statuses = Vec::new();
233+
234+
// Process stream items until we get a final status
235+
while let Some(status) = stream.next().await {
236+
let tx_status = status.map(TxStatus::from).map_err(Into::into);
237+
238+
let is_final = tx_status.as_ref().ok().is_some_and(|s| s.is_final());
239+
240+
statuses.push(tx_status);
241+
242+
if is_final {
243+
break;
244+
}
245+
}
246+
247+
// Handle cache updates for failures
248+
#[cfg(feature = "coin-cache")]
249+
if statuses.iter().any(|status| {
250+
matches!(
251+
&status,
252+
Ok(TxStatus::SqueezedOut { .. }) | Ok(TxStatus::Failure { .. }),
253+
)
254+
}) {
255+
self.coins_cache.lock().await.remove_items(used_base_coins);
256+
}
257+
258+
Ok(statuses)
259+
}
260+
210261
async fn prepare_transaction_for_sending<T: Transaction>(&self, mut tx: T) -> Result<T> {
211262
let consensus_parameters = self.consensus_parameters().await?;
212263
tx.precompute(&consensus_parameters.chain_id())?;
@@ -314,6 +365,19 @@ impl Provider {
314365
.into())
315366
}
316367

368+
pub async fn subscribe_transaction_status<'a>(
369+
&'a self,
370+
tx_id: &'a TxId,
371+
include_preconfirmation: bool,
372+
) -> Result<impl futures::Stream<Item = Result<TxStatus>> + 'a> {
373+
let stream = self
374+
.uncached_client()
375+
.subscribe_transaction_status(tx_id, include_preconfirmation)
376+
.await?;
377+
378+
Ok(stream.map(|status| status.map(Into::into).map_err(Into::into)))
379+
}
380+
317381
pub async fn chain_info(&self) -> Result<ChainInfo> {
318382
Ok(self.uncached_client().chain_info().await?.into())
319383
}

packages/fuels-accounts/src/provider/retryable_client.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use fuel_core_types::services::executor::TransactionExecutionStatus;
1818
use fuel_tx::{BlobId, ConsensusParameters, Transaction, TxId, UtxoId};
1919
use fuel_types::{Address, AssetId, BlockHeight, ContractId, Nonce};
2020
use fuels_core::types::errors::{Error, Result, error};
21+
use futures::Stream;
2122

2223
use super::{
2324
cache::CacheableRpcs,
@@ -146,6 +147,30 @@ impl RetryableClient {
146147
self.wrap(|| self.client.submit_and_await_commit(tx)).await
147148
}
148149

150+
pub async fn submit_and_await_status<'a>(
151+
&'a self,
152+
tx: &'a Transaction,
153+
include_preconfirmation: bool,
154+
) -> RequestResult<impl Stream<Item = io::Result<TransactionStatus>> + 'a> {
155+
self.wrap(|| {
156+
self.client
157+
.submit_and_await_status_opt(tx, None, Some(include_preconfirmation))
158+
})
159+
.await
160+
}
161+
162+
pub async fn subscribe_transaction_status<'a>(
163+
&'a self,
164+
id: &'a TxId,
165+
include_preconfirmation: bool,
166+
) -> RequestResult<impl Stream<Item = io::Result<TransactionStatus>> + 'a> {
167+
self.wrap(|| {
168+
self.client
169+
.subscribe_transaction_status_opt(id, Some(include_preconfirmation))
170+
})
171+
.await
172+
}
173+
149174
pub async fn submit(&self, tx: &Transaction) -> RequestResult<TransactionId> {
150175
self.wrap(|| self.client.submit(tx)).await
151176
}

packages/fuels-core/src/types/tx_status.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,13 @@ impl TxStatus {
170170
_ => vec![],
171171
}
172172
}
173+
174+
pub fn is_final(&self) -> bool {
175+
matches!(
176+
self,
177+
TxStatus::Success(_) | TxStatus::Failure(_) | TxStatus::SqueezedOut(_)
178+
)
179+
}
173180
}
174181

175182
#[cfg(feature = "std")]

packages/fuels-test-helpers/src/fuel_bin_service.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ impl ExtendedConfig {
7272
block_time.as_millis()
7373
));
7474
}
75+
Trigger::Open { period } => {
76+
args.push(format!("--poa-open-period={}ms", period.as_millis()));
77+
}
7578
};
7679

7780
let body_limit = self.node_config.graphql_request_body_bytes_limit;

packages/fuels-test-helpers/src/node_types.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pub enum Trigger {
1111
Instant,
1212
Never,
1313
Interval { block_time: Duration },
14+
Open { period: Duration },
1415
}
1516

1617
#[cfg(feature = "fuel-core-lib")]
@@ -20,6 +21,7 @@ impl From<Trigger> for fuel_core_poa::Trigger {
2021
Trigger::Instant => fuel_core_poa::Trigger::Instant,
2122
Trigger::Never => fuel_core_poa::Trigger::Never,
2223
Trigger::Interval { block_time } => fuel_core_poa::Trigger::Interval { block_time },
24+
Trigger::Open { period } => fuel_core_poa::Trigger::Open { period },
2325
}
2426
}
2527
}

0 commit comments

Comments
 (0)