1use crate::utxo_manager::{
2 FuelTxCoin,
3 UtxoProvider,
4};
5use fuel_core_client::client::{
6 FuelClient,
7 types::{
8 ResolvedOutput,
9 TransactionStatus,
10 TransactionType,
11 },
12};
13use fuel_core_types::{
14 blockchain::transaction::TransactionExt,
15 fuel_tx::{
16 Address,
17 AssetId,
18 ConsensusParameters,
19 Receipt,
20 Transaction,
21 TxId,
22 UniqueIdentifier,
23 UtxoId,
24 Witness,
25 },
26 fuel_types::ChainId,
27};
28use fuels::{
29 accounts::{
30 ViewOnlyAccount,
31 wallet::Unlocked,
32 },
33 prelude::{
34 BuildableTransaction,
35 ResourceFilter,
36 ScriptTransactionBuilder,
37 TransactionBuilder,
38 TxPolicies,
39 Wallet,
40 },
41 types::{
42 coin_type::CoinType,
43 input::Input,
44 output::Output,
45 transaction::ScriptTransaction,
46 tx_status::TxStatus,
47 },
48};
49use futures::{
50 StreamExt,
51 stream::FuturesUnordered,
52};
53use std::{
54 future::Future,
55 ops::Mul,
56 sync::Arc,
57 time::{
58 Duration,
59 Instant,
60 },
61};
62
63pub const SIGNATURE_MARGIN: usize = 100;
64
65#[derive(Clone, Debug)]
66pub struct SendResult<T = TxStatus> {
67 pub tx_id: TxId,
68 pub tx_status: T,
69 pub known_coins: Vec<FuelTxCoin>,
70 pub dynamic_coins: Vec<FuelTxCoin>,
71 pub preconf_rx_time: Option<Duration>,
72 pub receipts: Arc<Vec<Receipt>>,
82}
83
84pub fn receipts_of(status: &TxStatus) -> Arc<Vec<Receipt>> {
89 match status {
90 TxStatus::Success(success) | TxStatus::PreconfirmationSuccess(success) => {
91 success.receipts.clone()
92 }
93 TxStatus::Failure(failure) | TxStatus::PreconfirmationFailure(failure) => {
94 failure.receipts.clone()
95 }
96 TxStatus::Submitted | TxStatus::SqueezedOut(_) => Arc::new(Vec::new()),
97 }
98}
99
100#[derive(Clone)]
101pub struct BuilderData {
102 pub consensus_parameters: ConsensusParameters,
103 pub gas_price: u64,
104}
105
106impl BuilderData {
107 pub fn max_fee(&self) -> u64 {
108 let max_gas_limit = self.consensus_parameters.tx_params().max_gas_per_tx();
109 max_gas_limit
111 .mul(self.gas_price)
112 .div_ceil(self.consensus_parameters.fee_params().gas_price_factor())
113 }
114}
115
116pub trait WalletExt {
117 fn builder_data(&self) -> impl Future<Output = anyhow::Result<BuilderData>> + Send;
118
119 fn build_transfer(
120 &self,
121 asset_id: AssetId,
122 transfers: &[(Address, u64)],
123 utxo_manager: &mut dyn UtxoProvider,
124 builder_data: &BuilderData,
125 fetch_coins: bool,
126 ) -> impl Future<Output = anyhow::Result<Transaction>> + Send;
127
128 fn build_transaction(
129 &self,
130 inputs: Vec<Input>,
131 outputs: Vec<Output>,
132 witnesses: Vec<Witness>,
133 tx_policies: TxPolicies,
134 ) -> impl Future<Output = anyhow::Result<Transaction>> + Send;
135
136 fn send_transaction(
146 &self,
147 chain_id: ChainId,
148 tx: &Transaction,
149 submit_clients: &[FuelClient],
150 ) -> impl Future<Output = anyhow::Result<SendResult>> + Send;
151
152 fn transfer_many(
153 &self,
154 asset_id: AssetId,
155 transfers: &[(Address, u64)],
156 utxo_manager: &mut dyn UtxoProvider,
157 builder_data: &BuilderData,
158 fetch_coins: bool,
159 chunk_size: Option<usize>,
160 ) -> impl Future<Output = anyhow::Result<Vec<FuelTxCoin>>> + Send;
161
162 fn transfer_many_and_wait(
163 &self,
164 asset_id: AssetId,
165 transfers: &[(Address, u64)],
166 utxo_manager: &mut dyn UtxoProvider,
167 builder_data: &BuilderData,
168 fetch_coins: bool,
169 chunk_size: Option<usize>,
170 ) -> impl Future<Output = anyhow::Result<Vec<FuelTxCoin>>> + Send;
171
172 fn await_send_result(
173 &self,
174 tx_id: &TxId,
175 tx: &Transaction,
176 ) -> impl Future<Output = anyhow::Result<SendResult>> + Send;
177}
178
179impl<S> WalletExt for Wallet<Unlocked<S>>
180where
181 S: fuels::core::traits::Signer + Clone + Send + Sync + std::fmt::Debug + 'static,
182{
183 async fn builder_data(&self) -> anyhow::Result<BuilderData> {
184 let provider = self.provider();
185 let consensus_parameters = provider.consensus_parameters().await?;
186 let gas_price = provider.estimate_gas_price(10).await?;
187
188 let builder_data = BuilderData {
189 consensus_parameters,
190 gas_price: gas_price.gas_price,
191 };
192
193 Ok(builder_data)
194 }
195
196 async fn build_transaction(
197 &self,
198 inputs: Vec<Input>,
199 outputs: Vec<Output>,
200 witnesses: Vec<Witness>,
201 mut tx_policies: TxPolicies,
202 ) -> anyhow::Result<Transaction> {
203 if tx_policies.witness_limit().is_none() {
204 let witness_size = witnesses
205 .iter()
206 .map(|w| w.as_vec().len() as u64)
207 .sum::<u64>()
208 + SIGNATURE_MARGIN as u64;
209
210 tx_policies = tx_policies.with_witness_limit(witness_size);
211 }
212
213 let mut tx_builder = ScriptTransactionBuilder::prepare_transfer(
214 inputs,
215 outputs.clone(),
216 tx_policies,
217 );
218 *tx_builder.witnesses_mut() = witnesses;
219 tx_builder = tx_builder.enable_burn(true);
220 tx_builder.add_signer(self.signer().clone())?;
221
222 let tx = tx_builder.build(self.provider()).await?;
223 Ok(tx.into())
224 }
225
226 #[tracing::instrument(skip_all)]
227 async fn send_transaction(
228 &self,
229 chain_id: ChainId,
230 tx: &Transaction,
231 submit_clients: &[FuelClient],
232 ) -> anyhow::Result<SendResult> {
233 let provider_client;
234 let clients: Vec<&FuelClient> = if submit_clients.is_empty() {
235 provider_client = self.provider().client();
236 vec![provider_client]
237 } else {
238 submit_clients.iter().collect()
239 };
240
241 let tx_id = tx.id(&chain_id);
242
243 let mut tasks: FuturesUnordered<_> = clients
244 .iter()
245 .copied()
246 .map(|client| submit_and_parse(client, tx, tx_id))
247 .collect();
248
249 let mut errors: Vec<String> = Vec::with_capacity(clients.len());
250 let mut any_duplicate = false;
251
252 while let Some(result) = tasks.next().await {
253 match result {
254 Ok(result) => return Ok(result),
255 Err(err) => {
256 if err.is_duplicate() {
257 any_duplicate = true;
258 }
259 errors.push(err.to_string());
260 }
261 }
262 }
263
264 if any_duplicate {
265 tracing::info!(
266 "Transaction {tx_id} already exists on at least one submit \
267 client, awaiting confirmation. Submit errors: [{}]",
268 errors.join("; ")
269 );
270 return self.await_send_result(&tx_id, tx).await;
271 }
272
273 Err(anyhow::anyhow!(
274 "All {} submit client(s) failed for tx {tx_id}: [{}]",
275 clients.len(),
276 errors.join("; ")
277 ))
278 }
279
280 async fn build_transfer(
281 &self,
282 asset_id: AssetId,
283 transfers: &[(Address, u64)],
284 utxo_manager: &mut dyn UtxoProvider,
285 builder_data: &BuilderData,
286 fetch_coins: bool,
287 ) -> anyhow::Result<Transaction> {
288 let max_fee = builder_data.max_fee();
290
291 let base_asset_id = *builder_data.consensus_parameters.base_asset_id();
292
293 let payer: Address = self.address();
294
295 let asset_total = transfers
296 .iter()
297 .map(|(_, amount)| u128::from(*amount))
298 .sum::<u128>();
299
300 let balance_of = utxo_manager.balance_of(payer, asset_id);
301 if fetch_coins && balance_of < asset_total {
302 let asset_coins = self
303 .provider()
304 .get_spendable_resources(ResourceFilter {
305 from: self.address(),
306 asset_id: Some(asset_id),
307 amount: asset_total,
308 excluded_utxos: vec![],
309 excluded_message_nonces: vec![],
310 })
311 .await
312 .map_err(|e| {
313 anyhow::anyhow!(
314 "Failed to get spendable resources: \
315 {e} for {asset_id:?} from {payer:?} with amount {asset_total}"
316 )
317 })?
318 .into_iter()
319 .filter_map(|coin| match coin {
320 CoinType::Coin(coin) => Some(coin.into()),
321 _ => None,
322 });
323
324 utxo_manager.load_from_coins_vec(asset_coins.collect());
325 }
326
327 let fee_coins = if asset_id != base_asset_id {
328 utxo_manager.guaranteed_extract_coins(
331 payer,
332 base_asset_id,
333 max_fee as u128,
334 usize::MAX,
335 )?
336 } else {
337 vec![]
338 };
339
340 let mut total = transfers
341 .iter()
342 .map(|(_, amount)| u128::from(*amount))
343 .sum::<u128>();
344
345 if base_asset_id == asset_id {
346 total += max_fee as u128;
347 }
348
349 let asset_coins = match utxo_manager.guaranteed_extract_coins(
353 payer,
354 asset_id,
355 total,
356 usize::MAX,
357 ) {
358 Ok(coins) => coins,
359 Err(err) => {
360 utxo_manager.load_from_coins_vec(fee_coins);
361 return Err(err);
362 }
363 };
364
365 let mut output_coins = vec![];
366 for (recipient, amount) in transfers {
367 let output = Output::Coin {
368 to: *recipient,
369 amount: *amount,
370 asset_id,
371 };
372 output_coins.push(output);
373 }
374
375 output_coins.push(Output::Change {
376 to: payer,
377 amount: 0,
378 asset_id: base_asset_id,
379 });
380
381 if asset_id != base_asset_id {
382 output_coins.push(Output::Change {
383 to: payer,
384 amount: 0,
385 asset_id,
386 });
387 }
388
389 let mut input_coins = asset_coins;
390 input_coins.extend(fee_coins);
391
392 let inputs = input_coins
393 .iter()
394 .map(|coin| Input::resource_signed(CoinType::Coin((*coin).into())))
395 .collect::<Vec<_>>();
396
397 match self
398 .build_transaction(
399 inputs,
400 output_coins,
401 vec![],
402 TxPolicies::default().with_max_fee(max_fee),
403 )
404 .await
405 {
406 Ok(tx) => Ok(tx),
407 Err(err) => {
408 utxo_manager.load_from_coins_vec(input_coins);
412 Err(err)
413 }
414 }
415 }
416
417 async fn transfer_many_and_wait(
418 &self,
419 asset_id: AssetId,
420 transfers: &[(Address, u64)],
421 utxo_manager: &mut dyn UtxoProvider,
422 builder_data: &BuilderData,
423 fetch_coins: bool,
424 chunk_size: Option<usize>,
425 ) -> anyhow::Result<Vec<FuelTxCoin>> {
426 let known_coins = self
427 .transfer_many(
428 asset_id,
429 transfers,
430 utxo_manager,
431 builder_data,
432 fetch_coins,
433 chunk_size,
434 )
435 .await?;
436
437 if let Some(last_tx_id) = known_coins.last().map(|coin| coin.utxo_id.tx_id()) {
438 let tx_id = TxId::new((*last_tx_id).into());
439 self.provider()
440 .await_transaction_commit::<ScriptTransaction>(tx_id)
441 .await?;
442 }
443
444 Ok(known_coins)
445 }
446
447 async fn transfer_many(
448 &self,
449 asset_id: AssetId,
450 transfers: &[(Address, u64)],
451 utxo_manager: &mut dyn UtxoProvider,
452 builder_data: &BuilderData,
453 fetch_coins: bool,
454 chunk_size: Option<usize>,
455 ) -> anyhow::Result<Vec<FuelTxCoin>> {
456 let chain_id = builder_data.consensus_parameters.chain_id();
457 match chunk_size {
458 None => {
459 let tx = self
460 .build_transfer(
461 asset_id,
462 transfers,
463 utxo_manager,
464 builder_data,
465 fetch_coins,
466 )
467 .await?;
468 let result = self.send_transaction(chain_id, &tx, &[]).await?;
469 Ok(result.known_coins)
470 }
471 Some(chunk_size) => {
472 let mut known_coins = vec![];
473 for chunk in transfers.chunks(chunk_size) {
474 let tx = self
475 .build_transfer(
476 asset_id,
477 chunk,
478 utxo_manager,
479 builder_data,
480 fetch_coins,
481 )
482 .await?;
483 let result = self.send_transaction(chain_id, &tx, &[]).await?;
484
485 known_coins.extend(result.known_coins);
486 utxo_manager.load_from_coins_vec(result.dynamic_coins);
487 }
488
489 Ok(known_coins)
490 }
491 }
492 }
493
494 #[tracing::instrument(skip(self, tx), fields(tx_id))]
495 async fn await_send_result(
496 &self,
497 tx_id: &TxId,
498 tx: &Transaction,
499 ) -> anyhow::Result<SendResult> {
500 let fuel_client = self.provider().client();
501
502 let include_preconfirmation = true;
503 let result = fuel_client
504 .subscribe_transaction_status_opt(tx_id, Some(include_preconfirmation))
505 .await;
506 let mut stream = match result {
507 Ok(stream) => stream,
508 Err(err) => {
509 tracing::error!("Failed to subscribe to transaction status: {err:?}");
510 return Err(err.into());
511 }
512 };
513
514 let mut status;
515 let mut preconf_rx_time = None;
516 loop {
517 let now = Instant::now();
518 status = stream.next().await.transpose()?.ok_or(anyhow::anyhow!(
519 "Failed to get transaction status from stream"
520 ))?;
521
522 match status {
523 TransactionStatus::PreconfirmationSuccess { .. }
524 | TransactionStatus::PreconfirmationFailure { .. } => {
525 preconf_rx_time = Some(now.elapsed());
526 break;
527 }
528 TransactionStatus::Success { .. } | TransactionStatus::Failure { .. } => {
529 break;
530 }
531 TransactionStatus::SqueezedOut { reason } => {
532 tracing::error!(%tx_id, "Transaction was squeezed out: {reason:?}");
533 continue;
534 }
535 _ => continue,
536 }
537 }
538
539 let mut known_coins = vec![];
540 for (i, output) in tx.outputs().iter().enumerate() {
541 let utxo_id = UtxoId::new(*tx_id, i as u16);
542 if let Output::Coin {
543 amount,
544 to,
545 asset_id,
546 } = *output
547 {
548 let coin = FuelTxCoin {
549 amount,
550 asset_id,
551 utxo_id,
552 owner: to,
553 };
554
555 known_coins.push(coin);
556 }
557 }
558
559 let mut dynamic_coins = vec![];
560 match &status {
561 TransactionStatus::PreconfirmationSuccess {
562 resolved_outputs, ..
563 }
564 | TransactionStatus::PreconfirmationFailure {
565 resolved_outputs, ..
566 } => {
567 let resolved_outputs = resolved_outputs.clone().unwrap_or_default();
568
569 for output in resolved_outputs {
570 let ResolvedOutput { utxo_id, output } = output;
571 match output {
572 Output::Change {
573 amount,
574 to,
575 asset_id,
576 } => {
577 let coin = FuelTxCoin {
578 amount,
579 asset_id,
580 utxo_id,
581 owner: to,
582 };
583
584 dynamic_coins.push(coin);
585 }
586 Output::Variable {
587 amount,
588 to,
589 asset_id,
590 } => {
591 let coin = FuelTxCoin {
592 amount,
593 asset_id,
594 utxo_id,
595 owner: to,
596 };
597
598 dynamic_coins.push(coin);
599 }
600 _ => {}
601 }
602 }
603 }
604 TransactionStatus::Success { .. } | TransactionStatus::Failure { .. } => {
605 let tx = fuel_client
606 .transaction(tx_id)
607 .await?
608 .ok_or(anyhow::anyhow!("Transaction not found"))?;
609
610 match tx.transaction {
611 TransactionType::Known(tx) => {
612 for (index, output) in tx.outputs().iter().enumerate() {
613 let utxo_id = UtxoId::new(*tx_id, index as u16);
614
615 match *output {
616 Output::Change {
617 amount,
618 to,
619 asset_id,
620 } => {
621 let coin = FuelTxCoin {
622 amount,
623 asset_id,
624 utxo_id,
625 owner: to,
626 };
627
628 dynamic_coins.push(coin);
629 }
630 Output::Variable {
631 amount,
632 to,
633 asset_id,
634 } => {
635 let coin = FuelTxCoin {
636 amount,
637 asset_id,
638 utxo_id,
639 owner: to,
640 };
641
642 dynamic_coins.push(coin);
643 }
644 _ => {}
645 }
646 }
647 }
648 TransactionType::Unknown => {}
649 }
650 }
651 _ => {
652 return Err(anyhow::anyhow!(
653 "Expected pre confirmation, but received: {status:?}"
654 ));
655 }
656 }
657
658 let tx_status: TxStatus = status.into();
659 let receipts = receipts_of(&tx_status);
660 let result = SendResult {
661 tx_id: *tx_id,
662 tx_status,
663 known_coins,
664 dynamic_coins,
665 preconf_rx_time,
666 receipts,
667 };
668
669 Ok(result)
670 }
671}
672
673async fn submit_and_parse(
677 client: &FuelClient,
678 tx: &Transaction,
679 tx_id: TxId,
680) -> anyhow::Result<SendResult> {
681 let estimate_predicates = false;
682 let include_preconfirmation = true;
683 let mut stream = client
684 .submit_and_await_status_opt(
685 tx,
686 Some(estimate_predicates),
687 Some(include_preconfirmation),
688 )
689 .await?;
690
691 let now = Instant::now();
692 let status = loop {
693 let status = stream.next().await.transpose()?.ok_or(anyhow::anyhow!(
694 "Failed to get pre confirmation from the stream"
695 ))?;
696
697 if matches!(status, TransactionStatus::PreconfirmationSuccess { .. })
698 || matches!(status, TransactionStatus::PreconfirmationFailure { .. })
699 || matches!(status, TransactionStatus::Success { .. })
700 || matches!(status, TransactionStatus::Failure { .. })
701 {
702 break status;
703 }
704
705 if let TransactionStatus::SqueezedOut { reason } = &status {
706 return Err(anyhow::anyhow!("Transaction was squeezed out: {reason:?}"));
707 }
708 };
709 let preconf_rx_time = now.elapsed();
710
711 let resolved = match &status {
712 TransactionStatus::PreconfirmationSuccess {
713 resolved_outputs, ..
714 }
715 | TransactionStatus::PreconfirmationFailure {
716 resolved_outputs, ..
717 } => resolved_outputs.clone().expect("Expected resolved outputs"),
718 TransactionStatus::Success { .. } | TransactionStatus::Failure { .. } => {
719 let transaction = client
720 .transaction(&tx_id)
721 .await?
722 .ok_or(anyhow::anyhow!("Transaction not found"))?;
723
724 let TransactionType::Known(executed_tx) = transaction.transaction else {
725 return Err(anyhow::anyhow!("Expected known transaction type"));
726 };
727
728 executed_tx
729 .outputs()
730 .iter()
731 .enumerate()
732 .filter_map(|(index, output)| {
733 if output.is_change()
734 || output.is_variable() && output.amount() != Some(0)
735 {
736 Some(ResolvedOutput {
737 utxo_id: UtxoId::new(tx_id, index as u16),
738 output: *output,
739 })
740 } else {
741 None
742 }
743 })
744 .collect::<Vec<_>>()
745 }
746 _ => {
747 return Err(anyhow::anyhow!(
748 "Expected pre confirmation, but received: {status:?}"
749 ));
750 }
751 };
752
753 let mut known_coins = vec![];
754 for (i, output) in tx.outputs().iter().enumerate() {
755 let utxo_id = UtxoId::new(tx_id, i as u16);
756 if let Output::Coin {
757 amount,
758 to,
759 asset_id,
760 } = *output
761 {
762 known_coins.push(FuelTxCoin {
763 amount,
764 asset_id,
765 utxo_id,
766 owner: to,
767 });
768 }
769 }
770
771 let mut dynamic_coins = vec![];
772 for ResolvedOutput { utxo_id, output } in resolved {
773 match output {
774 Output::Change {
775 amount,
776 to,
777 asset_id,
778 }
779 | Output::Variable {
780 amount,
781 to,
782 asset_id,
783 } => {
784 dynamic_coins.push(FuelTxCoin {
785 amount,
786 asset_id,
787 utxo_id,
788 owner: to,
789 });
790 }
791 _ => {}
792 }
793 }
794
795 let tx_status: TxStatus = status.into();
796 let receipts = receipts_of(&tx_status);
797 Ok(SendResult {
798 tx_id,
799 tx_status,
800 known_coins,
801 dynamic_coins,
802 preconf_rx_time: Some(preconf_rx_time),
803 receipts,
804 })
805}
806
807pub(crate) trait ClientError {
808 fn is_duplicate(&self) -> bool;
809}
810
811impl<T> ClientError for T
812where
813 T: ToString,
814{
815 fn is_duplicate(&self) -> bool {
816 self.to_string().contains("Transaction id already exists")
817 }
818}
819
820const COIN_INVALID_PATTERNS: &[&str] = &[
829 "was already spent",
830 "does not exist",
831 "does not match the values from database",
832 "Coin owner is different from expected input",
833 "Coin output does not match expected input",
834 "asset_id does not match expected inputs",
835 "is blacklisted",
836 "Expected coin but output is contract",
837];
838
839pub fn is_coin_invalid_error(error: &str) -> bool {
843 COIN_INVALID_PATTERNS
844 .iter()
845 .any(|pattern| error.contains(pattern))
846}
847
848#[cfg(test)]
849mod coin_error_tests {
850 use super::*;
851
852 #[test]
853 fn detects_coin_invalid_errors() {
854 let cases = [
855 "The UTXO input 0xabcd was already spent",
856 "UTXO (id: 0xabcd) does not exist",
857 "Input coin does not match the values from database",
858 "Input output mismatch. Coin owner is different from expected input",
859 "Input output mismatch. Coin output does not match expected input",
860 "Input output mismatch. Coin output asset_id does not match expected inputs",
861 "The UTXO `0xabcd` is blacklisted",
862 "Input output mismatch. Expected coin but output is contract",
863 ];
864 for msg in cases {
865 assert!(is_coin_invalid_error(msg), "Should detect: {msg}");
866 }
867 }
868
869 #[test]
870 fn does_not_flag_non_coin_errors() {
871 let cases = [
872 "Transaction was squeezed out",
873 "Pool limit is hit, try to increase gas_price",
874 "The provided max fee can't cover the transaction cost",
875 "Transaction id already exists",
876 "Transaction chain dependency is already too big",
877 "Too much transactions are in queue",
878 ];
879 for msg in cases {
880 assert!(!is_coin_invalid_error(msg), "Should NOT detect: {msg}");
881 }
882 }
883}
884
885#[cfg(test)]
886#[allow(non_snake_case)]
890mod restore_on_failure_tests {
891 use super::*;
892 use crate::utxo_manager::UtxoManager;
893 use fuels::{
894 prelude::Provider,
895 test_helpers::{
896 ChainConfig,
897 WalletsConfig,
898 launch_custom_provider_and_get_wallets,
899 },
900 };
901 use std::collections::HashSet;
902
903 async fn funded_wallet() -> (Wallet, UtxoManager, BuilderData, AssetId) {
906 let mut wallets = launch_custom_provider_and_get_wallets(
907 WalletsConfig::new(Some(1), Some(10), Some(1_000_000_000)),
909 None,
910 Some(ChainConfig::local_testnet()),
911 )
912 .await
913 .expect("failed to launch test provider");
914 let wallet = wallets.pop().expect("no wallet");
915
916 let provider: &Provider = wallet.provider();
917 let base_asset_id = *provider
918 .consensus_parameters()
919 .await
920 .expect("consensus parameters")
921 .base_asset_id();
922
923 let coins = wallet
924 .get_coins(base_asset_id)
925 .await
926 .expect("failed to read coins");
927 let utxo_manager = UtxoManager::new_from_coins(coins.into_iter().map(Into::into));
928 let builder_data = wallet.builder_data().await.expect("builder data");
929
930 (wallet, utxo_manager, builder_data, base_asset_id)
931 }
932
933 fn snapshot(manager: &UtxoManager) -> (HashSet<UtxoId>, usize) {
934 (
935 manager.utxo_ids().into_iter().collect(),
936 manager.coin_count(),
937 )
938 }
939
940 #[tokio::test]
943 async fn build_transfer__returns_fee_coins_when_the_asset_cannot_be_covered() {
944 let (wallet, mut utxo_manager, builder_data, base_asset_id) =
945 funded_wallet().await;
946 let before = snapshot(&utxo_manager);
947 assert!(before.1 > 0, "test needs a funded manager");
948
949 let other_asset = AssetId::new([7u8; 32]);
952 assert_ne!(other_asset, base_asset_id);
953
954 let result = wallet
955 .build_transfer(
956 other_asset,
957 &[(wallet.address(), 1)],
958 &mut utxo_manager,
959 &builder_data,
960 false,
961 )
962 .await;
963
964 assert!(result.is_err(), "expected the transfer build to fail");
965 assert_eq!(
966 snapshot(&utxo_manager),
967 before,
968 "fee coins were taken for a transfer that never happened"
969 );
970 }
971
972 #[tokio::test]
980 async fn transfer_many__keeps_inputs_when_the_node_rejects_the_transaction() {
981 let (wallet, mut utxo_manager, builder_data, base_asset_id) =
982 funded_wallet().await;
983 let (before_ids, _) = snapshot(&utxo_manager);
984
985 let max_outputs = builder_data.consensus_parameters.tx_params().max_outputs();
988 let transfers = vec![(wallet.address(), 1u64); max_outputs as usize + 8];
989
990 let result = wallet
991 .transfer_many(
992 base_asset_id,
993 &transfers,
994 &mut utxo_manager,
995 &builder_data,
996 false,
997 None,
998 )
999 .await;
1000
1001 assert!(result.is_err(), "expected the node to reject the transfer");
1002
1003 let (after_ids, _) = snapshot(&utxo_manager);
1004 let withheld = before_ids.difference(&after_ids).count();
1005 assert!(
1006 withheld > 0,
1007 "inputs of a submitted transaction must not be returned; the \
1008 manager still holds all {} of them",
1009 before_ids.len()
1010 );
1011 }
1012
1013 #[tokio::test]
1016 async fn transfer_many__spends_its_inputs_when_the_transfer_succeeds() {
1017 let (wallet, mut utxo_manager, builder_data, base_asset_id) =
1018 funded_wallet().await;
1019 let before = snapshot(&utxo_manager);
1020
1021 let known_coins = wallet
1022 .transfer_many(
1023 base_asset_id,
1024 &[(wallet.address(), 1_000)],
1025 &mut utxo_manager,
1026 &builder_data,
1027 false,
1028 Some(1),
1029 )
1030 .await
1031 .expect("transfer should succeed");
1032
1033 assert!(!known_coins.is_empty(), "expected new coins");
1034
1035 let (after_ids, _) = snapshot(&utxo_manager);
1040 let (before_ids, _) = before;
1041 let consumed = before_ids.difference(&after_ids).count();
1042 assert!(
1043 consumed > 0,
1044 "a successful transfer must consume its inputs; all {} original \
1045 coins are still in the manager, so they were handed back",
1046 before_ids.len()
1047 );
1048 }
1049}