1use crate::utxo_manager::{
2 FuelTxCoin,
3 UtxoManager,
4};
5use fuel_core_client::client::types::{
6 ResolvedOutput,
7 TransactionStatus,
8 TransactionType,
9};
10use fuel_core_types::{
11 blockchain::transaction::TransactionExt,
12 fuel_tx,
13 fuel_tx::{
14 Address,
15 AssetId,
16 ConsensusParameters,
17 Finalizable,
18 Script,
19 Transaction,
20 TxId,
21 TxPointer,
22 UniqueIdentifier,
23 UtxoId,
24 Witness,
25 },
26 fuel_types::ChainId,
27};
28use fuels::{
29 accounts::{
30 ViewOnlyAccount,
31 wallet::Unlocked,
32 },
33 crypto::SecretKey,
34 prelude::{
35 BuildableTransaction,
36 ResourceFilter,
37 ScriptTransactionBuilder,
38 TransactionBuilder,
39 TxPolicies,
40 Wallet,
41 },
42 types::{
43 coin_type::CoinType,
44 input::Input,
45 output::Output,
46 transaction::ScriptTransaction,
47 tx_status::TxStatus,
48 },
49};
50use futures::{
51 StreamExt,
52 future::{
53 Either,
54 select,
55 },
56 pin_mut,
57};
58use std::{
59 future::Future,
60 ops::Mul,
61 time::{
62 Duration,
63 Instant,
64 },
65};
66
67pub const SIGNATURE_MARGIN: usize = 100;
68
69#[derive(Clone, Debug)]
70pub struct SendResult<T = TxStatus> {
71 pub tx_id: TxId,
72 pub tx_status: T,
73 pub known_coins: Vec<FuelTxCoin>,
74 pub dynamic_coins: Vec<FuelTxCoin>,
75 pub preconf_rx_time: Option<Duration>,
76}
77
78#[derive(Clone)]
79pub struct BuilderData {
80 pub consensus_parameters: ConsensusParameters,
81 pub gas_price: u64,
82}
83
84impl BuilderData {
85 pub fn max_fee(&self) -> u64 {
86 let max_gas_limit = self.consensus_parameters.tx_params().max_gas_per_tx();
87 max_gas_limit
89 .mul(self.gas_price)
90 .div_ceil(self.consensus_parameters.fee_params().gas_price_factor())
91 }
92}
93
94pub trait WalletExt {
95 fn builder_data(&self) -> impl Future<Output = anyhow::Result<BuilderData>> + Send;
96
97 fn build_transfer(
98 &self,
99 asset_id: AssetId,
100 transfers: &[(Address, u64)],
101 utxo_manager: &mut UtxoManager,
102 builder_data: &BuilderData,
103 fetch_coins: bool,
104 ) -> impl Future<Output = anyhow::Result<Transaction>> + Send;
105
106 fn build_transaction_sync(
107 &self,
108 secret_key: SecretKey,
109 inputs: Vec<Input>,
110 outputs: Vec<Output>,
111 witnesses: Vec<Witness>,
112 builder_data: &BuilderData,
113 ) -> Transaction;
114
115 fn build_transaction(
116 &self,
117 inputs: Vec<Input>,
118 outputs: Vec<Output>,
119 witnesses: Vec<Witness>,
120 tx_policies: TxPolicies,
121 ) -> impl Future<Output = anyhow::Result<Transaction>> + Send;
122
123 fn send_transaction(
124 &self,
125 chain_id: ChainId,
126 tx: &Transaction,
127 ) -> impl Future<Output = anyhow::Result<SendResult>> + Send;
128
129 fn transfer_many(
130 &self,
131 asset_id: AssetId,
132 transfers: &[(Address, u64)],
133 utxo_manager: &mut UtxoManager,
134 builder_data: &BuilderData,
135 fetch_coins: bool,
136 chunk_size: Option<usize>,
137 ) -> impl Future<Output = anyhow::Result<Vec<FuelTxCoin>>> + Send;
138
139 fn transfer_many_and_wait(
140 &self,
141 asset_id: AssetId,
142 transfers: &[(Address, u64)],
143 utxo_manager: &mut UtxoManager,
144 builder_data: &BuilderData,
145 fetch_coins: bool,
146 chunk_size: Option<usize>,
147 ) -> impl Future<Output = anyhow::Result<Vec<FuelTxCoin>>> + Send;
148
149 fn await_send_result(
150 &self,
151 tx_id: &TxId,
152 tx: &Transaction,
153 ) -> impl Future<Output = anyhow::Result<SendResult>> + Send;
154}
155
156impl<S> WalletExt for Wallet<Unlocked<S>>
157where
158 S: fuels::core::traits::Signer + Clone + Send + Sync + std::fmt::Debug + 'static,
159{
160 async fn builder_data(&self) -> anyhow::Result<BuilderData> {
161 let provider = self.provider();
162 let consensus_parameters = provider.consensus_parameters().await?;
163 let gas_price = provider.estimate_gas_price(10).await?;
164
165 let builder_data = BuilderData {
166 consensus_parameters,
167 gas_price: gas_price.gas_price,
168 };
169
170 Ok(builder_data)
171 }
172
173 fn build_transaction_sync(
174 &self,
175 secret_key: SecretKey,
176 inputs: Vec<Input>,
177 outputs: Vec<Output>,
178 witnesses: Vec<Witness>,
179 builder_data: &BuilderData,
180 ) -> Transaction {
181 let resources_required = builder_data.max_fee();
182
183 let witness_limit = witnesses
184 .iter()
185 .map(|witness| witness.as_vec().len())
186 .sum::<usize>()
187 + SIGNATURE_MARGIN;
188
189 let mut builder = fuel_core_types::fuel_tx::TransactionBuilder::<Script>::script(
190 vec![],
191 vec![],
192 );
193 builder
194 .max_fee_limit(resources_required)
195 .witness_limit(witness_limit as u64);
196
197 for witness in witnesses {
198 builder.add_witness(witness);
199 }
200
201 for input in inputs {
202 match input {
203 Input::ResourceSigned { resource } => {
204 if let CoinType::Coin(coin) = resource {
205 builder.add_unsigned_coin_input(
206 secret_key,
207 coin.utxo_id,
208 coin.amount,
209 coin.asset_id,
210 TxPointer::default(),
211 );
212 }
213 }
214 Input::ResourcePredicate { resource, code, .. } => {
215 if let CoinType::Coin(coin) = resource {
216 builder.add_input(fuel_tx::Input::coin_predicate(
217 coin.utxo_id,
218 coin.owner,
219 coin.amount,
220 coin.asset_id,
221 TxPointer::default(),
222 0,
223 code,
224 vec![],
225 ));
226 }
227 }
228 Input::Contract { .. } => {
229 unreachable!("We don't use contract inputs")
230 }
231 }
232 }
233
234 for output in outputs {
235 builder.add_output(output);
236 }
237
238 let tx = builder.finalize();
239
240 tx.into()
241 }
242
243 async fn build_transaction(
244 &self,
245 inputs: Vec<Input>,
246 outputs: Vec<Output>,
247 witnesses: Vec<Witness>,
248 mut tx_policies: TxPolicies,
249 ) -> anyhow::Result<Transaction> {
250 if tx_policies.witness_limit().is_none() {
251 let witness_size = witnesses
252 .iter()
253 .map(|w| w.as_vec().len() as u64)
254 .sum::<u64>()
255 + SIGNATURE_MARGIN as u64;
256
257 tx_policies = tx_policies.with_witness_limit(witness_size);
258 }
259
260 let mut tx_builder = ScriptTransactionBuilder::prepare_transfer(
261 inputs,
262 outputs.clone(),
263 tx_policies,
264 );
265 *tx_builder.witnesses_mut() = witnesses;
266 tx_builder = tx_builder.enable_burn(true);
267 tx_builder.add_signer(self.signer().clone())?;
268
269 let tx = tx_builder.build(self.provider()).await?;
270 Ok(tx.into())
271 }
272
273 #[tracing::instrument(skip_all)]
274 async fn send_transaction(
275 &self,
276 chain_id: ChainId,
277 tx: &Transaction,
278 ) -> anyhow::Result<SendResult> {
279 let fuel_client = self.provider().client();
280 let tx_id = tx.id(&chain_id);
281
282 let result = async move {
283 let estimate_predicates = false;
284 let include_preconfirmation = true;
285
286 let send_future = fuel_client.submit_opt(tx, Some(estimate_predicates));
287
288 let stream_future = fuel_client
289 .subscribe_transaction_status_opt(&tx_id, Some(include_preconfirmation));
290
291 pin_mut!(send_future, stream_future);
292
293 let result = select(send_future, stream_future).await;
304
305 let mut stream = match result {
306 Either::Left((send_result, stream_future)) => {
307 send_result?;
308 stream_future.await?
309 }
310 Either::Right((stream_result, send_future)) => {
311 let stream = stream_result?;
312 send_future.await?;
313 stream
314 }
315 };
316
317 let mut status;
318 let now = Instant::now();
319
320 loop {
321 status = stream.next().await.transpose()?.ok_or(anyhow::anyhow!(
322 "Failed to get pre confirmation from the stream"
323 ))?;
324
325 if matches!(status, TransactionStatus::PreconfirmationSuccess { .. })
326 || matches!(status, TransactionStatus::PreconfirmationFailure { .. })
327 || matches!(status, TransactionStatus::Success { .. })
328 || matches!(status, TransactionStatus::Failure { .. })
329 {
330 break;
331 }
332
333 if let TransactionStatus::SqueezedOut { reason } = &status {
334 return Err(anyhow::anyhow!(
335 "Transaction was squeezed out: {reason:?}"
336 ));
337 }
338 }
339 let preconf_rx_time = now.elapsed();
340
341 let resolved;
342 match &status {
343 TransactionStatus::PreconfirmationSuccess {
344 resolved_outputs, ..
345 }
346 | TransactionStatus::PreconfirmationFailure {
347 resolved_outputs, ..
348 } => {
349 resolved =
350 resolved_outputs.clone().expect("Expected resolved outputs");
351 }
352 TransactionStatus::Success { .. } | TransactionStatus::Failure { .. } => {
353 let transaction = fuel_client
354 .transaction(&tx_id)
355 .await?
356 .ok_or(anyhow::anyhow!("Transaction not found"))?;
357
358 let TransactionType::Known(executed_tx) = transaction.transaction
359 else {
360 return Err(anyhow::anyhow!("Expected known transaction type"));
361 };
362
363 let resolved_outputs = executed_tx
364 .outputs()
365 .iter()
366 .enumerate()
367 .filter_map(|(index, output)| {
368 if output.is_change()
369 || output.is_variable() && output.amount() != Some(0)
370 {
371 Some(ResolvedOutput {
372 utxo_id: UtxoId::new(tx_id, index as u16),
373 output: *output,
374 })
375 } else {
376 None
377 }
378 })
379 .collect::<Vec<_>>();
380
381 resolved = resolved_outputs;
382 }
383 _ => {
384 return Err(anyhow::anyhow!(
385 "Expected pre confirmation, but received: {status:?}"
386 ));
387 }
388 }
389
390 let mut known_coins = vec![];
391 for (i, output) in tx.outputs().iter().enumerate() {
392 let utxo_id = UtxoId::new(tx_id, i as u16);
393 if let Output::Coin {
394 amount,
395 to,
396 asset_id,
397 } = *output
398 {
399 let coin = FuelTxCoin {
400 amount,
401 asset_id,
402 utxo_id,
403 owner: to,
404 };
405
406 known_coins.push(coin);
407 }
408 }
409
410 let mut dynamic_coins = vec![];
411 for output in resolved {
412 let ResolvedOutput { utxo_id, output } = output;
413 match output {
414 Output::Change {
415 amount,
416 to,
417 asset_id,
418 } => {
419 let coin = FuelTxCoin {
420 amount,
421 asset_id,
422 utxo_id,
423 owner: to,
424 };
425
426 dynamic_coins.push(coin);
427 }
428 Output::Variable {
429 amount,
430 to,
431 asset_id,
432 } => {
433 let coin = FuelTxCoin {
434 amount,
435 asset_id,
436 utxo_id,
437 owner: to,
438 };
439
440 dynamic_coins.push(coin);
441 }
442 _ => {}
443 }
444 }
445
446 let result = SendResult {
447 tx_id,
448 tx_status: status.into(),
449 known_coins,
450 dynamic_coins,
451 preconf_rx_time: Some(preconf_rx_time),
452 };
453
454 Ok(result)
455 }
456 .await;
457
458 match result {
459 Ok(result) => Ok(result),
460 Err(err) => {
461 if err.is_duplicate() {
462 let chain_id =
463 self.provider().consensus_parameters().await?.chain_id();
464 let tx_id = tx.id(&chain_id);
465 tracing::info!(
466 "Transaction {tx_id} already exists in the chain, \
467 waiting for confirmation."
468 );
469 self.await_send_result(&tx_id, tx).await
470 } else {
471 tracing::error!(
472 "The error is not duplicate, so returning it: {err:?}",
473 );
474 Err(err)
475 }
476 }
477 }
478 }
479
480 async fn build_transfer(
481 &self,
482 asset_id: AssetId,
483 transfers: &[(Address, u64)],
484 utxo_manager: &mut UtxoManager,
485 builder_data: &BuilderData,
486 fetch_coins: bool,
487 ) -> anyhow::Result<Transaction> {
488 let max_fee = builder_data.max_fee();
490
491 let base_asset_id = *builder_data.consensus_parameters.base_asset_id();
492
493 let payer: Address = self.address();
494
495 let asset_total = transfers
496 .iter()
497 .map(|(_, amount)| u128::from(*amount))
498 .sum::<u128>();
499
500 let balance_of = utxo_manager.balance_of(payer, asset_id);
501 if fetch_coins && balance_of < asset_total {
502 let asset_coins = self
503 .provider()
504 .get_spendable_resources(ResourceFilter {
505 from: self.address(),
506 asset_id: Some(asset_id),
507 amount: asset_total,
508 excluded_utxos: vec![],
509 excluded_message_nonces: vec![],
510 })
511 .await
512 .map_err(|e| {
513 anyhow::anyhow!(
514 "Failed to get spendable resources: \
515 {e} for {asset_id:?} from {payer:?} with amount {asset_total}"
516 )
517 })?
518 .into_iter()
519 .filter_map(|coin| match coin {
520 CoinType::Coin(coin) => Some(coin.into()),
521 _ => None,
522 });
523
524 utxo_manager.load_from_coins(asset_coins);
525 }
526
527 let fee_coins = if asset_id != base_asset_id {
528 utxo_manager.guaranteed_extract_coins(
529 payer,
530 base_asset_id,
531 max_fee as u128,
532 )?
533 } else {
534 vec![]
535 };
536
537 let mut total = transfers
538 .iter()
539 .map(|(_, amount)| u128::from(*amount))
540 .sum::<u128>();
541
542 if base_asset_id == asset_id {
543 total += max_fee as u128;
544 }
545
546 let asset_coins =
547 utxo_manager.guaranteed_extract_coins(payer, asset_id, total)?;
548
549 let mut output_coins = vec![];
550 for (recipient, amount) in transfers {
551 let output = Output::Coin {
552 to: *recipient,
553 amount: *amount,
554 asset_id,
555 };
556 output_coins.push(output);
557 }
558
559 output_coins.push(Output::Change {
560 to: payer,
561 amount: 0,
562 asset_id: base_asset_id,
563 });
564
565 if asset_id != base_asset_id {
566 output_coins.push(Output::Change {
567 to: payer,
568 amount: 0,
569 asset_id,
570 });
571 }
572
573 let mut input_coins = asset_coins;
574 input_coins.extend(fee_coins);
575
576 let inputs = input_coins
577 .into_iter()
578 .map(|coin| Input::resource_signed(CoinType::Coin(coin.into())))
579 .collect::<Vec<_>>();
580
581 let tx = self
582 .build_transaction(
583 inputs,
584 output_coins,
585 vec![],
586 TxPolicies::default().with_max_fee(max_fee),
587 )
588 .await?;
589
590 Ok(tx)
591 }
592
593 async fn transfer_many_and_wait(
594 &self,
595 asset_id: AssetId,
596 transfers: &[(Address, u64)],
597 utxo_manager: &mut UtxoManager,
598 builder_data: &BuilderData,
599 fetch_coins: bool,
600 chunk_size: Option<usize>,
601 ) -> anyhow::Result<Vec<FuelTxCoin>> {
602 let known_coins = self
603 .transfer_many(
604 asset_id,
605 transfers,
606 utxo_manager,
607 builder_data,
608 fetch_coins,
609 chunk_size,
610 )
611 .await?;
612
613 if let Some(last_tx_id) = known_coins.last().map(|coin| coin.utxo_id.tx_id()) {
614 let tx_id = TxId::new((*last_tx_id).into());
615 self.provider()
616 .await_transaction_commit::<ScriptTransaction>(tx_id)
617 .await?;
618 }
619
620 Ok(known_coins)
621 }
622
623 async fn transfer_many(
624 &self,
625 asset_id: AssetId,
626 transfers: &[(Address, u64)],
627 utxo_manager: &mut UtxoManager,
628 builder_data: &BuilderData,
629 fetch_coins: bool,
630 chunk_size: Option<usize>,
631 ) -> anyhow::Result<Vec<FuelTxCoin>> {
632 let chain_id = builder_data.consensus_parameters.chain_id();
633 match chunk_size {
634 None => {
635 let tx = self
636 .build_transfer(
637 asset_id,
638 transfers,
639 utxo_manager,
640 builder_data,
641 fetch_coins,
642 )
643 .await?;
644 let result = self.send_transaction(chain_id, &tx).await?;
645 Ok(result.known_coins)
646 }
647 Some(chunk_size) => {
648 let mut known_coins = vec![];
649 for chunk in transfers.chunks(chunk_size) {
650 let tx = self
651 .build_transfer(
652 asset_id,
653 chunk,
654 utxo_manager,
655 builder_data,
656 fetch_coins,
657 )
658 .await?;
659 let result = self.send_transaction(chain_id, &tx).await?;
660
661 known_coins.extend(result.known_coins);
662 utxo_manager.load_from_coins(result.dynamic_coins.into_iter());
663 }
664
665 Ok(known_coins)
666 }
667 }
668 }
669
670 #[tracing::instrument(skip(self, tx), fields(tx_id))]
671 async fn await_send_result(
672 &self,
673 tx_id: &TxId,
674 tx: &Transaction,
675 ) -> anyhow::Result<SendResult> {
676 let fuel_client = self.provider().client();
677
678 let include_preconfirmation = true;
679 let result = fuel_client
680 .subscribe_transaction_status_opt(tx_id, Some(include_preconfirmation))
681 .await;
682 let mut stream = match result {
683 Ok(stream) => stream,
684 Err(err) => {
685 tracing::error!("Failed to subscribe to transaction status: {err:?}");
686 return Err(err.into());
687 }
688 };
689
690 let mut status;
691 let mut preconf_rx_time = None;
692 loop {
693 let now = Instant::now();
694 status = stream.next().await.transpose()?.ok_or(anyhow::anyhow!(
695 "Failed to get transaction status from stream"
696 ))?;
697
698 match status {
699 TransactionStatus::PreconfirmationSuccess { .. }
700 | TransactionStatus::PreconfirmationFailure { .. } => {
701 preconf_rx_time = Some(now.elapsed());
702 break;
703 }
704 TransactionStatus::Success { .. } | TransactionStatus::Failure { .. } => {
705 break;
706 }
707 TransactionStatus::SqueezedOut { reason } => {
708 tracing::error!(%tx_id, "Transaction was squeezed out: {reason:?}");
709 continue;
710 }
711 _ => continue,
712 }
713 }
714
715 let mut known_coins = vec![];
716 for (i, output) in tx.outputs().iter().enumerate() {
717 let utxo_id = UtxoId::new(*tx_id, i as u16);
718 if let Output::Coin {
719 amount,
720 to,
721 asset_id,
722 } = *output
723 {
724 let coin = FuelTxCoin {
725 amount,
726 asset_id,
727 utxo_id,
728 owner: to,
729 };
730
731 known_coins.push(coin);
732 }
733 }
734
735 let mut dynamic_coins = vec![];
736 match &status {
737 TransactionStatus::PreconfirmationSuccess {
738 resolved_outputs, ..
739 }
740 | TransactionStatus::PreconfirmationFailure {
741 resolved_outputs, ..
742 } => {
743 let resolved_outputs = resolved_outputs.clone().unwrap_or_default();
744
745 for output in resolved_outputs {
746 let ResolvedOutput { utxo_id, output } = output;
747 match output {
748 Output::Change {
749 amount,
750 to,
751 asset_id,
752 } => {
753 let coin = FuelTxCoin {
754 amount,
755 asset_id,
756 utxo_id,
757 owner: to,
758 };
759
760 dynamic_coins.push(coin);
761 }
762 Output::Variable {
763 amount,
764 to,
765 asset_id,
766 } => {
767 let coin = FuelTxCoin {
768 amount,
769 asset_id,
770 utxo_id,
771 owner: to,
772 };
773
774 dynamic_coins.push(coin);
775 }
776 _ => {}
777 }
778 }
779 }
780 TransactionStatus::Success { .. } | TransactionStatus::Failure { .. } => {
781 let tx = fuel_client
782 .transaction(tx_id)
783 .await?
784 .ok_or(anyhow::anyhow!("Transaction not found"))?;
785
786 match tx.transaction {
787 TransactionType::Known(tx) => {
788 for (index, output) in tx.outputs().iter().enumerate() {
789 let utxo_id = UtxoId::new(*tx_id, index as u16);
790
791 match *output {
792 Output::Change {
793 amount,
794 to,
795 asset_id,
796 } => {
797 let coin = FuelTxCoin {
798 amount,
799 asset_id,
800 utxo_id,
801 owner: to,
802 };
803
804 dynamic_coins.push(coin);
805 }
806 Output::Variable {
807 amount,
808 to,
809 asset_id,
810 } => {
811 let coin = FuelTxCoin {
812 amount,
813 asset_id,
814 utxo_id,
815 owner: to,
816 };
817
818 dynamic_coins.push(coin);
819 }
820 _ => {}
821 }
822 }
823 }
824 TransactionType::Unknown => {}
825 }
826 }
827 _ => {
828 return Err(anyhow::anyhow!(
829 "Expected pre confirmation, but received: {status:?}"
830 ));
831 }
832 }
833
834 let result = SendResult {
835 tx_id: *tx_id,
836 tx_status: status.into(),
837 known_coins,
838 dynamic_coins,
839 preconf_rx_time,
840 };
841
842 Ok(result)
843 }
844}
845
846pub trait ClientError {
847 fn is_duplicate(&self) -> bool;
848 fn is_diamond_problem(&self) -> bool;
849 fn is_big_dependency(&self) -> bool;
850 fn is_load_related_error(&self) -> bool;
851}
852
853impl<T> ClientError for T
854where
855 T: ToString,
856{
857 fn is_duplicate(&self) -> bool {
858 self.to_string().contains("Transaction id already exists")
859 }
860
861 fn is_diamond_problem(&self) -> bool {
862 self.to_string().contains("a diamond problem")
863 }
864
865 fn is_big_dependency(&self) -> bool {
866 self.to_string()
867 .contains("chain dependency is already too big")
868 }
869
870 fn is_load_related_error(&self) -> bool {
871 self.is_diamond_problem() || self.is_big_dependency()
872 }
873}