Skip to main content

ark_client/
send_vtxo.rs

1use crate::error::ErrorContext;
2use crate::swap_storage::SwapStorage;
3use crate::utils::timeout_op;
4use crate::wallet::BoardingWallet;
5use crate::wallet::OnchainWallet;
6use crate::Blockchain;
7use crate::Client;
8use crate::Error;
9use ark_core::coin_select::select_vtxos;
10use ark_core::intent;
11use ark_core::script::extract_checksig_pubkeys;
12use ark_core::send;
13use ark_core::send::build_offchain_transactions;
14use ark_core::send::sign_ark_transaction;
15use ark_core::send::sign_checkpoint_transaction;
16use ark_core::send::OffchainTransactions;
17use ark_core::ArkAddress;
18use ark_core::ErrorContext as _;
19use bitcoin::key::Secp256k1;
20use bitcoin::psbt;
21use bitcoin::secp256k1;
22use bitcoin::secp256k1::schnorr;
23use bitcoin::Amount;
24use bitcoin::OutPoint;
25use bitcoin::TxOut;
26use bitcoin::Txid;
27use bitcoin::XOnlyPublicKey;
28use std::time::Duration;
29
30impl<B, W, S, K> Client<B, W, S, K>
31where
32    B: Blockchain,
33    W: BoardingWallet + OnchainWallet,
34    S: SwapStorage + 'static,
35    K: crate::KeyProvider,
36{
37    /// Spend confirmed and pre-confirmed VTXOs in an Ark transaction sending the given `amount` to
38    /// the given `address`.
39    ///
40    /// The Ark transaction is built in collaboration with the Ark server. The outputs of said
41    /// transaction will be pre-confirmed VTXOs.
42    ///
43    /// Coin selection is performed automatically to choose which VTXOs to spend.
44    ///
45    /// # Returns
46    ///
47    /// The [`Txid`] of the generated Ark transaction.
48    pub async fn send_vtxo(&self, address: ArkAddress, amount: Amount) -> Result<Txid, Error> {
49        let (vtxo_list, script_pubkey_to_vtxo_map) = self
50            .list_vtxos()
51            .await
52            .context("failed to get spendable VTXOs")?;
53
54        // Run coin selection algorithm on candidate spendable VTXOs.
55        let spendable_virtual_tx_outpoints = vtxo_list
56            .spendable_offchain()
57            .map(|vtxo| ark_core::coin_select::VirtualTxOutPoint {
58                outpoint: vtxo.outpoint,
59                script_pubkey: vtxo.script.clone(),
60                expire_at: vtxo.expires_at,
61                amount: vtxo.amount,
62            })
63            .collect::<Vec<_>>();
64
65        let selected_coins = select_vtxos(
66            spendable_virtual_tx_outpoints,
67            amount,
68            self.server_info.dust,
69            true,
70        )
71        .map_err(Error::from)
72        .context("failed to select coins")?;
73
74        let vtxo_inputs = selected_coins
75            .into_iter()
76            .map(|virtual_tx_outpoint| {
77                let vtxo = script_pubkey_to_vtxo_map
78                    .get(&virtual_tx_outpoint.script_pubkey)
79                    .ok_or_else(|| {
80                        ark_core::Error::ad_hoc(format!(
81                            "missing VTXO for script pubkey: {}",
82                            virtual_tx_outpoint.script_pubkey
83                        ))
84                    })?;
85
86                let (forfeit_script, control_block) = vtxo
87                    .forfeit_spend_info()
88                    .context("failed to get forfeit spend info")?;
89
90                Ok(send::VtxoInput::new(
91                    forfeit_script,
92                    None,
93                    control_block,
94                    vtxo.tapscripts(),
95                    vtxo.script_pubkey(),
96                    virtual_tx_outpoint.amount,
97                    virtual_tx_outpoint.outpoint,
98                ))
99            })
100            .collect::<Result<Vec<_>, Error>>()?;
101
102        self.build_and_sign_offchain_tx(vtxo_inputs, address, amount)
103            .await
104    }
105
106    /// Spend specific VTXOs in an Ark transaction sending the given `amount` to the given
107    /// `address`.
108    ///
109    /// The Ark transaction is built in collaboration with the Ark server. The outputs of said
110    /// transaction will be pre-confirmed VTXOs.
111    ///
112    /// Unlike [`Self::send_vtxo`], this method allows the caller to specify exactly which VTXOs
113    /// to spend by providing their outpoints. This is useful for applications that want to have
114    /// full control over VTXO selection.
115    ///
116    /// # Arguments
117    ///
118    /// * `vtxo_outpoints` - The specific VTXO outpoints to spend
119    /// * `address` - The destination Ark address
120    /// * `amount` - The amount to send
121    ///
122    /// # Returns
123    ///
124    /// The [`Txid`] of the generated Ark transaction.
125    ///
126    /// # Errors
127    ///
128    /// Returns an error if the selected VTXOs don't have enough value to cover the requested
129    /// amount.
130    pub async fn send_vtxo_selection(
131        &self,
132        vtxo_outpoints: &[OutPoint],
133        address: ArkAddress,
134        amount: Amount,
135    ) -> Result<Txid, Error> {
136        let (vtxo_list, script_pubkey_to_vtxo_map) =
137            self.list_vtxos().await.context("failed to get VTXO list")?;
138
139        // Get all spendable VTXOs for reference
140        let all_spendable = vtxo_list
141            .spendable_offchain()
142            .map(|vtxo| ark_core::coin_select::VirtualTxOutPoint {
143                outpoint: vtxo.outpoint,
144                script_pubkey: vtxo.script.clone(),
145                expire_at: vtxo.expires_at,
146                amount: vtxo.amount,
147            })
148            .collect::<Vec<_>>();
149
150        // Filter to only the specified outpoints
151        let selected_coins: Vec<_> = all_spendable
152            .into_iter()
153            .filter(|vtxo| vtxo_outpoints.contains(&vtxo.outpoint))
154            .collect();
155
156        if selected_coins.is_empty() {
157            return Err(Error::ad_hoc("no matching VTXO outpoints found"));
158        }
159
160        // Check that total amount is sufficient
161        let total_amount = selected_coins
162            .iter()
163            .fold(Amount::ZERO, |acc, vtxo| acc + vtxo.amount);
164
165        if total_amount < amount {
166            return Err(Error::coin_select(format!(
167                "insufficient VTXO amount: {} < {}",
168                total_amount, amount
169            )));
170        }
171
172        // Build VTXO inputs from selected coins
173        let vtxo_inputs = selected_coins
174            .into_iter()
175            .map(|virtual_tx_outpoint| {
176                let vtxo = script_pubkey_to_vtxo_map
177                    .get(&virtual_tx_outpoint.script_pubkey)
178                    .ok_or_else(|| {
179                        ark_core::Error::ad_hoc(format!(
180                            "missing VTXO for script pubkey: {}",
181                            virtual_tx_outpoint.script_pubkey
182                        ))
183                    })?;
184
185                let (forfeit_script, control_block) = vtxo
186                    .forfeit_spend_info()
187                    .context("failed to get forfeit spend info")?;
188
189                Ok(send::VtxoInput::new(
190                    forfeit_script,
191                    None,
192                    control_block,
193                    vtxo.tapscripts(),
194                    vtxo.script_pubkey(),
195                    virtual_tx_outpoint.amount,
196                    virtual_tx_outpoint.outpoint,
197                ))
198            })
199            .collect::<Result<Vec<_>, Error>>()?;
200
201        self.build_and_sign_offchain_tx(vtxo_inputs, address, amount)
202            .await
203    }
204
205    /// Build, sign and submit an offchain transaction to the server without finalizing.
206    ///
207    /// This is primarily useful for testing pending transaction recovery flows.
208    ///
209    /// Returns the Ark txid. The transaction will remain in a pending state on the server
210    /// until [`Self::continue_pending_offchain_txs`] or a manual finalize call completes it.
211    #[cfg(feature = "test-utils")]
212    pub async fn submit_offchain_tx(
213        &self,
214        vtxo_inputs: Vec<send::VtxoInput>,
215        address: ArkAddress,
216        amount: Amount,
217    ) -> Result<Txid, Error> {
218        let (change_address, _) = self.get_offchain_address()?;
219
220        let OffchainTransactions {
221            mut ark_tx,
222            checkpoint_txs,
223        } = build_offchain_transactions(
224            &[(&address, amount)],
225            Some(&change_address),
226            &vtxo_inputs,
227            &self.server_info,
228        )
229        .map_err(Error::from)
230        .context("failed to build offchain transactions")?;
231
232        for i in 0..checkpoint_txs.len() {
233            let sign_fn = |input: &mut psbt::Input,
234                           msg: secp256k1::Message|
235             -> Result<
236                Vec<(schnorr::Signature, XOnlyPublicKey)>,
237                ark_core::Error,
238            > {
239                match &input.witness_script {
240                    None => Err(ark_core::Error::ad_hoc(
241                        "Missing witness script for psbt::Input when signing ark transaction",
242                    )),
243                    Some(script) => {
244                        let mut res = vec![];
245                        let pks = extract_checksig_pubkeys(script);
246                        for pk in pks {
247                            if let Ok(keypair) = self.keypair_by_pk(&pk) {
248                                let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &keypair);
249                                let pk = keypair.x_only_public_key().0;
250                                res.push((sig, pk))
251                            }
252                        }
253                        Ok(res)
254                    }
255                }
256            };
257
258            sign_ark_transaction(sign_fn, &mut ark_tx, i)?;
259        }
260
261        let ark_txid = ark_tx.unsigned_tx.compute_txid();
262
263        self.network_client()
264            .submit_offchain_transaction_request(ark_tx, checkpoint_txs)
265            .await
266            .map_err(Error::ark_server)
267            .context("failed to submit offchain transaction request")?;
268
269        Ok(ark_txid)
270    }
271
272    /// Build and sign an Ark transaction with the given VTXO inputs.
273    ///
274    /// This is a shared helper used by both [`Self::send_vtxo`] and [`Self::send_vtxo_selection`].
275    async fn build_and_sign_offchain_tx(
276        &self,
277        vtxo_inputs: Vec<send::VtxoInput>,
278        address: ArkAddress,
279        amount: Amount,
280    ) -> Result<Txid, Error> {
281        let (change_address, change_address_vtxo) = self.get_offchain_address()?;
282
283        let OffchainTransactions {
284            mut ark_tx,
285            checkpoint_txs,
286        } = build_offchain_transactions(
287            &[(&address, amount)],
288            Some(&change_address),
289            &vtxo_inputs,
290            &self.server_info,
291        )
292        .map_err(Error::from)
293        .context("failed to build offchain transactions")?;
294
295        for i in 0..checkpoint_txs.len() {
296            let sign_fn = |input: &mut psbt::Input,
297                           msg: secp256k1::Message|
298             -> Result<
299                Vec<(schnorr::Signature, XOnlyPublicKey)>,
300                ark_core::Error,
301            > {
302                match &input.witness_script {
303                    None => Err(ark_core::Error::ad_hoc(
304                        "Missing witness script for psbt::Input when signing ark transaction",
305                    )),
306                    Some(script) => {
307                        let mut res = vec![];
308                        let pks = extract_checksig_pubkeys(script);
309                        for pk in pks {
310                            if let Ok(keypair) = self.keypair_by_pk(&pk) {
311                                let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &keypair);
312                                let pk = keypair.x_only_public_key().0;
313                                res.push((sig, pk))
314                            }
315                        }
316                        Ok(res)
317                    }
318                }
319            };
320
321            sign_ark_transaction(sign_fn, &mut ark_tx, i)?;
322        }
323
324        let ark_txid = ark_tx.unsigned_tx.compute_txid();
325
326        let mut res = self
327            .network_client()
328            .submit_offchain_transaction_request(ark_tx, checkpoint_txs.clone())
329            .await
330            .map_err(Error::ark_server)
331            .context("failed to submit offchain transaction request")?;
332
333        // Build a map from checkpoint txid → witness_script from the client's
334        // original checkpoints. The server may return signed checkpoints in a
335        // different order, so we match by txid rather than assuming index order.
336        let client_checkpoint_ws: std::collections::HashMap<_, _> = checkpoint_txs
337            .iter()
338            .map(|cp| {
339                let txid = cp.unsigned_tx.compute_txid();
340                let ws = cp.inputs[0].witness_script.clone();
341                (txid, ws)
342            })
343            .collect();
344
345        for checkpoint_psbt in res.signed_checkpoint_txs.iter_mut() {
346            let sign_fn = |input: &mut psbt::Input,
347                           msg: secp256k1::Message|
348             -> Result<
349                Vec<(schnorr::Signature, XOnlyPublicKey)>,
350                ark_core::Error,
351            > {
352                match &input.witness_script {
353                    None => Err(ark_core::Error::ad_hoc(
354                        "Missing witness script for psbt::Input signing checkpoint tx",
355                    )),
356                    Some(script) => {
357                        let mut res = vec![];
358                        let pks = extract_checksig_pubkeys(script);
359                        for pk in pks {
360                            if let Ok(keypair) = self.keypair_by_pk(&pk) {
361                                let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &keypair);
362                                let pk = keypair.x_only_public_key().0;
363                                res.push((sig, pk));
364                            }
365                        }
366                        Ok(res)
367                    }
368                }
369            };
370
371            let cp_txid = checkpoint_psbt.unsigned_tx.compute_txid();
372            if let Some(ws) = client_checkpoint_ws.get(&cp_txid).cloned().flatten() {
373                checkpoint_psbt.inputs[0].witness_script = Some(ws);
374            }
375
376            sign_checkpoint_transaction(sign_fn, checkpoint_psbt)?;
377        }
378
379        self.finalize_with_retry(ark_txid, res.signed_checkpoint_txs)
380            .await?;
381
382        let used_pk = change_address_vtxo.owner_pk();
383        if let Err(err) = self.inner.key_provider.mark_as_used(&used_pk) {
384            tracing::warn!(
385                "Failed updating keypair cache for used change address: {:?} ",
386                err
387            );
388        }
389
390        Ok(ark_txid)
391    }
392
393    /// Finalize an offchain transaction, retrying on transient failures.
394    ///
395    /// After submit succeeds but before finalize completes, a network error
396    /// would leave the transaction in a pending state. Retrying here avoids
397    /// that without needing full recovery via [`Self::continue_pending_offchain_txs`].
398    async fn finalize_with_retry(
399        &self,
400        ark_txid: Txid,
401        signed_checkpoint_txs: Vec<bitcoin::Psbt>,
402    ) -> Result<(), Error> {
403        const MAX_RETRIES: usize = 3;
404
405        let mut last_err = None;
406
407        for attempt in 0..=MAX_RETRIES {
408            if attempt > 0 {
409                let delay = Duration::from_millis(500 * (1 << (attempt - 1)));
410                tracing::warn!(
411                    %ark_txid,
412                    attempt,
413                    ?delay,
414                    "Retrying finalize after transient failure"
415                );
416                crate::utils::sleep(delay).await;
417            }
418
419            match timeout_op(
420                self.inner.timeout,
421                self.network_client()
422                    .finalize_offchain_transaction(ark_txid, signed_checkpoint_txs.clone()),
423            )
424            .await
425            {
426                Ok(Ok(_)) => return Ok(()),
427                Ok(Err(e)) => {
428                    last_err = Some(Error::ark_server(e));
429                }
430                Err(e) => {
431                    last_err = Some(e);
432                }
433            }
434        }
435
436        Err(last_err
437            .expect("at least one attempt was made")
438            .context("failed to finalize offchain transaction after retries"))
439    }
440
441    /// List pending (submitted but not finalized) offchain transactions.
442    ///
443    /// This retrieves any transactions that were submitted to the server but not yet finalized
444    /// (e.g. due to a crash or network error between submit and finalize).
445    ///
446    /// # Returns
447    ///
448    /// The pending transactions, or an empty vec if there are none.
449    pub async fn list_pending_offchain_txs(
450        &self,
451    ) -> Result<Vec<ark_core::server::PendingTx>, Error> {
452        self.fetch_pending_offchain_txs().await
453    }
454
455    /// Resume and finalize any pending (submitted but not finalized) offchain transactions.
456    ///
457    /// This handles the case where `send_vtxo` successfully submitted the transaction to the
458    /// server but failed before finalizing (e.g. due to a crash or network error). The server
459    /// holds the submitted-but-not-finalized transaction in a pending state. This method
460    /// retrieves it, signs the checkpoint transactions, and finalizes.
461    ///
462    /// # Returns
463    ///
464    /// The [`Txid`]s of the finalized Ark transactions, or an empty vec if there were no
465    /// pending transactions.
466    pub async fn continue_pending_offchain_txs(&self) -> Result<Vec<Txid>, Error> {
467        let pending_txs = self.fetch_pending_offchain_txs().await?;
468
469        if pending_txs.is_empty() {
470            return Ok(vec![]);
471        }
472
473        let mut finalized_txids = Vec::new();
474
475        for pending_tx in pending_txs {
476            let ark_txid = pending_tx.ark_txid;
477            let mut signed_checkpoint_txs = pending_tx.signed_checkpoint_txs;
478
479            // Build a map from checkpoint txid → ark tx input index, since the
480            // server may return checkpoint txs in a different order than the ark
481            // tx inputs reference them.
482            let ark_tx_input_index_by_checkpoint_txid: std::collections::HashMap<_, _> = pending_tx
483                .signed_ark_tx
484                .unsigned_tx
485                .input
486                .iter()
487                .enumerate()
488                .map(|(i, inp)| (inp.previous_output.txid, i))
489                .collect();
490
491            for checkpoint_psbt in signed_checkpoint_txs.iter_mut() {
492                if checkpoint_psbt.inputs[0].witness_script.is_none() {
493                    // Server stripped the witness_script — restore it from
494                    // the ark tx input that spends this checkpoint.
495                    let checkpoint_txid = checkpoint_psbt.unsigned_tx.compute_txid();
496                    let ark_input_idx = ark_tx_input_index_by_checkpoint_txid
497                        .get(&checkpoint_txid)
498                        .ok_or_else(|| {
499                            Error::ad_hoc(format!(
500                                "checkpoint txid {checkpoint_txid} not found in ark tx inputs for pending tx {ark_txid}"
501                            ))
502                        })?;
503
504                    let witness_script = pending_tx
505                        .signed_ark_tx
506                        .inputs
507                        .get(*ark_input_idx)
508                        .and_then(|input| input.witness_script.clone())
509                        .ok_or_else(|| {
510                            Error::ad_hoc(format!(
511                                "missing witness script on ark tx input {ark_input_idx} for pending tx {ark_txid}"
512                            ))
513                        })?;
514
515                    checkpoint_psbt.inputs[0].witness_script = Some(witness_script);
516                }
517
518                let sign_fn = |input: &mut psbt::Input,
519                               msg: secp256k1::Message|
520                 -> Result<
521                    Vec<(schnorr::Signature, XOnlyPublicKey)>,
522                    ark_core::Error,
523                > {
524                    match &input.witness_script {
525                        None => Err(ark_core::Error::ad_hoc(
526                            "Missing witness script for psbt::Input signing checkpoint tx",
527                        )),
528                        Some(script) => {
529                            let mut res = vec![];
530                            let pks = extract_checksig_pubkeys(script);
531                            for pk in pks {
532                                if let Ok(keypair) = self.keypair_by_pk(&pk) {
533                                    let sig =
534                                        Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &keypair);
535                                    let pk = keypair.x_only_public_key().0;
536                                    res.push((sig, pk));
537                                }
538                            }
539                            Ok(res)
540                        }
541                    }
542                };
543
544                sign_checkpoint_transaction(sign_fn, checkpoint_psbt)?;
545            }
546
547            timeout_op(
548                self.inner.timeout,
549                self.network_client()
550                    .finalize_offchain_transaction(ark_txid, signed_checkpoint_txs),
551            )
552            .await?
553            .map_err(Error::ark_server)
554            .context("failed to finalize pending offchain transaction")?;
555
556            finalized_txids.push(ark_txid);
557        }
558
559        Ok(finalized_txids)
560    }
561
562    /// Fetch pending offchain transactions from the server.
563    ///
564    /// Shared helper used by both [`Self::list_pending_offchain_txs`] and
565    /// [`Self::continue_pending_offchain_txs`].
566    async fn fetch_pending_offchain_txs(&self) -> Result<Vec<ark_core::server::PendingTx>, Error> {
567        const MAX_INPUTS_PER_INTENT: usize = 20;
568
569        let ark_addresses = self.get_offchain_addresses()?;
570
571        let script_pubkey_to_vtxo_map: std::collections::HashMap<_, _> = ark_addresses
572            .iter()
573            .map(|(a, v)| (a.to_p2tr_script_pubkey(), v.clone()))
574            .collect();
575
576        // Use pending_only filter to only fetch VTXOs that are spent but not
577        // finalized. This is much cheaper than fetching all VTXOs when there
578        // are no pending transactions (common case).
579        let addresses = ark_addresses.iter().map(|(a, _)| *a);
580        let request = ark_core::server::GetVtxosRequest::new_for_addresses(addresses)
581            .pending_only()
582            .map_err(Error::from)?;
583
584        let vtxos = self
585            .fetch_all_vtxos(request)
586            .await
587            .context("failed to fetch pending VTXOs")?;
588
589        tracing::debug!(num_pending_vtxos = vtxos.len(), "Fetched pending VTXOs");
590
591        if vtxos.is_empty() {
592            return Ok(vec![]);
593        }
594
595        let secp = Secp256k1::new();
596        let mut all_pending_txs = Vec::new();
597        let mut seen_ark_txids = std::collections::HashSet::new();
598
599        // Batch inputs to avoid oversized intents.
600        for (batch_idx, batch) in vtxos.chunks(MAX_INPUTS_PER_INTENT).enumerate() {
601            let mut vtxo_inputs = Vec::new();
602            for virtual_tx_outpoint in batch {
603                let vtxo = match script_pubkey_to_vtxo_map.get(&virtual_tx_outpoint.script) {
604                    Some(v) => v,
605                    None => {
606                        tracing::warn!(
607                            outpoint = %virtual_tx_outpoint.outpoint,
608                            script = %virtual_tx_outpoint.script,
609                            "Skipping VTXO with unknown script"
610                        );
611                        continue;
612                    }
613                };
614                let spend_info = vtxo
615                    .forfeit_spend_info()
616                    .context("failed to get forfeit spend info")?;
617
618                vtxo_inputs.push(intent::Input::new(
619                    virtual_tx_outpoint.outpoint,
620                    vtxo.exit_delay(),
621                    None,
622                    TxOut {
623                        value: virtual_tx_outpoint.amount,
624                        script_pubkey: vtxo.script_pubkey(),
625                    },
626                    vtxo.tapscripts(),
627                    spend_info,
628                    false,
629                    virtual_tx_outpoint.is_swept,
630                ));
631            }
632
633            if vtxo_inputs.is_empty() {
634                continue;
635            }
636
637            tracing::debug!(
638                batch = batch_idx,
639                num_inputs = vtxo_inputs.len(),
640                "Querying server for pending txs"
641            );
642
643            // expire_at = 0: server does not enforce expiry for get-pending-tx intents.
644            let message = intent::IntentMessage::GetPendingTx { expire_at: 0 };
645
646            let sign_for_vtxo_fn = |input: &mut psbt::Input,
647                                    msg: secp256k1::Message|
648             -> Result<
649                Vec<(schnorr::Signature, XOnlyPublicKey)>,
650                ark_core::Error,
651            > {
652                match &input.witness_script {
653                    None => Err(ark_core::Error::ad_hoc(
654                        "Missing witness script in psbt::Input when signing get-pending-tx intent",
655                    )),
656                    Some(script) => {
657                        let pks = extract_checksig_pubkeys(script);
658                        let mut res = vec![];
659                        for pk in &pks {
660                            if let Ok(keypair) = self.keypair_by_pk(pk) {
661                                let sig = secp.sign_schnorr_no_aux_rand(&msg, &keypair);
662                                res.push((sig, keypair.x_only_public_key().0));
663                            }
664                        }
665                        Ok(res)
666                    }
667                }
668            };
669
670            let sign_for_onchain_fn =
671                |_: &mut psbt::Input,
672                 _: secp256k1::Message|
673                 -> Result<(schnorr::Signature, XOnlyPublicKey), ark_core::Error> {
674                    Err(ark_core::Error::ad_hoc(
675                        "unexpected onchain input in get-pending-tx intent",
676                    ))
677                };
678
679            let get_pending_intent = intent::make_intent(
680                sign_for_vtxo_fn,
681                sign_for_onchain_fn,
682                vtxo_inputs,
683                vec![],
684                message,
685            )?;
686
687            let pending_txs = self
688                .network_client()
689                .get_pending_tx(get_pending_intent)
690                .await
691                .map_err(Error::ark_server)
692                .context("failed to get pending transactions")?;
693
694            tracing::debug!(
695                batch = batch_idx,
696                num_pending_txs = pending_txs.len(),
697                "Server response for batch"
698            );
699
700            for tx in pending_txs {
701                if seen_ark_txids.insert(tx.ark_txid) {
702                    tracing::info!(
703                        ark_txid = %tx.ark_txid,
704                        "Found pending transaction"
705                    );
706                    all_pending_txs.push(tx);
707                }
708            }
709        }
710
711        tracing::info!(
712            num_pending_txs = all_pending_txs.len(),
713            "Total pending transactions found"
714        );
715
716        Ok(all_pending_txs)
717    }
718}