Skip to main content

o2_tools/
wallet_ext.rs

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