Skip to main content

o2_tools/
wallet_ext.rs

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        // Get the max fee based on the current info for the chain
88        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            // Just submitting transactions is faster than submit and subscribe.
294            // Subscription requires 3 round trips to the server under the hood
295            // to establish connection.
296            // So we subscribe separately to statuses, because we want network see transaction
297            // as soon as possible.
298            // Plus, if the caller doesn't wait for send result, they can get notification for
299            // events from API faster.
300            // TODO: Use indexer to subscribe for transaction status to reduce load on the node.
301            //  and remove additional latency here.
302            //  https://github.com/FuelLabs/fuel-o2/issues/1358
303            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        // Get the max fee based on the current info for the chain
489        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}