Skip to main content

ark_client/
lib.rs

1use crate::error::ErrorContext;
2use crate::key_provider::KeypairIndex;
3use crate::utils::sleep;
4use crate::utils::timeout_op;
5use crate::wallet::BoardingWallet;
6use crate::wallet::OnchainWallet;
7use ark_core::asset::AssetId;
8use ark_core::build_anchor_tx;
9use ark_core::history;
10use ark_core::history::generate_incoming_vtxo_transaction_history;
11use ark_core::history::generate_outgoing_vtxo_transaction_history;
12use ark_core::history::sort_transactions_by_created_at;
13use ark_core::history::OutgoingTransaction;
14use ark_core::server;
15use ark_core::server::GetVtxosRequest;
16use ark_core::server::SubscriptionResponse;
17use ark_core::server::VirtualTxOutPoint;
18use ark_core::ArkAddress;
19use ark_core::ExplorerUtxo;
20use ark_core::UtxoCoinSelection;
21use ark_core::Vtxo;
22use ark_core::VtxoList;
23use ark_core::DEFAULT_DERIVATION_PATH;
24use ark_grpc::VtxoChainResponse;
25use bitcoin::bip32::DerivationPath;
26use bitcoin::bip32::Xpriv;
27use bitcoin::key::Keypair;
28use bitcoin::key::Secp256k1;
29use bitcoin::secp256k1::All;
30use bitcoin::Address;
31use bitcoin::Amount;
32use bitcoin::OutPoint;
33use bitcoin::ScriptBuf;
34use bitcoin::Transaction;
35use bitcoin::Txid;
36use bitcoin::XOnlyPublicKey;
37use futures::Future;
38use futures::Stream;
39use std::collections::HashMap;
40use std::collections::HashSet;
41use std::str::FromStr;
42use std::sync::Arc;
43use std::time::Duration;
44
45pub mod error;
46pub mod key_provider;
47pub mod swap_storage;
48pub mod vtxo_watcher;
49pub mod wallet;
50
51mod asset;
52mod batch;
53mod boltz;
54mod coin_select;
55mod fee_estimation;
56mod send_vtxo;
57mod unilateral_exit;
58mod utils;
59
60pub use asset::IssueAssetResult;
61pub use boltz::ChainSwapAmount;
62pub use boltz::ChainSwapData;
63pub use boltz::ChainSwapDirection;
64pub use boltz::ChainSwapResult;
65pub use boltz::PendingVhtlcSpendTx;
66pub use boltz::PendingVhtlcSpendType;
67pub use boltz::ReverseSwapData;
68pub use boltz::SubmarineSwapData;
69pub use boltz::SwapAmount;
70pub use boltz::SwapStatus;
71pub use boltz::SwapStatusInfo;
72pub use boltz::SwapType;
73pub use boltz::TimeoutBlockHeights;
74pub use error::Error;
75pub use key_provider::Bip32KeyProvider;
76pub use key_provider::KeyProvider;
77pub use key_provider::StaticKeyProvider;
78pub use lightning_invoice;
79pub use swap_storage::InMemorySwapStorage;
80#[cfg(feature = "sqlite")]
81pub use swap_storage::SqliteSwapStorage;
82pub use swap_storage::SwapStorage;
83
84/// Default gap limit for BIP44-style key discovery
85///
86/// This is the number of consecutive unused addresses to scan before
87/// assuming all used addresses have been found.
88pub const DEFAULT_GAP_LIMIT: u32 = 20;
89
90/// A client to interact with Ark Server
91///
92/// ## Example
93///
94/// ```rust
95/// # use std::future::Future;
96/// # use std::str::FromStr;
97/// # use std::time::Duration;
98/// # use ark_client::{Blockchain, Client, Error, SpendStatus, TxStatus};
99/// # use ark_client::OfflineClient;
100/// # use bitcoin::key::Keypair;
101/// # use bitcoin::secp256k1::{Message, SecretKey};
102/// # use std::sync::Arc;
103/// # use bitcoin::{Address, Amount, FeeRate, Network, Psbt, Transaction, Txid, XOnlyPublicKey};
104/// # use bitcoin::secp256k1::schnorr::Signature;
105/// # use ark_client::wallet::{Balance, BoardingWallet, OnchainWallet, Persistence};
106/// # use ark_client::InMemorySwapStorage;
107/// # use ark_core::{BoardingOutput, UtxoCoinSelection, ExplorerUtxo};
108/// # use ark_client::StaticKeyProvider;
109///
110/// struct MyBlockchain {}
111/// #
112/// # impl MyBlockchain {
113/// #     pub fn new(_url: &str) -> Self { Self {}}
114/// # }
115/// #
116/// # impl Blockchain for MyBlockchain {
117/// #
118/// #     async fn find_outpoints(&self, address: &Address) -> Result<Vec<ExplorerUtxo>, Error> {
119/// #         unimplemented!("You can implement this function using your preferred client library such as esplora_client")
120/// #     }
121/// #
122/// #     async fn find_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
123/// #         unimplemented!()
124/// #     }
125/// #
126/// #     async fn get_tx_status(&self, txid: &Txid) -> Result<TxStatus, Error> {
127/// #         unimplemented!()
128/// #     }
129/// #
130/// #     async fn get_output_status(&self, txid: &Txid, vout: u32) -> Result<SpendStatus, Error> {
131/// #         unimplemented!()
132/// #     }
133/// #
134/// #     async fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
135/// #         unimplemented!()
136/// #     }
137/// #
138/// #     async fn get_fee_rate(&self) -> Result<f64, Error> {
139/// #         unimplemented!()
140/// #     }
141/// #
142/// #     async fn broadcast_package(
143/// #         &self,
144/// #         txs: &[&Transaction],
145/// #     ) -> Result<(), Error> {
146/// #         unimplemented!()
147/// #     }
148/// # }
149///
150/// struct MyWallet {}
151/// # impl OnchainWallet for MyWallet where {
152/// #
153/// #     fn get_onchain_address(&self) -> Result<Address, Error> {
154/// #         unimplemented!("You can implement this function using your preferred client library such as bdk")
155/// #     }
156/// #
157/// #     async fn sync(&self) -> Result<(), Error> {
158/// #         unimplemented!()
159/// #     }
160/// #
161/// #     fn balance(&self) -> Result<Balance, Error> {
162/// #         unimplemented!()
163/// #     }
164/// #
165/// #     fn prepare_send_to_address(&self, address: Address, amount: Amount, fee_rate: FeeRate) -> Result<Psbt, Error> {
166/// #         unimplemented!()
167/// #     }
168/// #
169/// #     fn sign(&self, psbt: &mut Psbt) -> Result<bool, Error> {
170/// #         unimplemented!()
171/// #     }
172/// #
173/// #     fn select_coins(&self, target_amount: Amount) -> Result<ark_core::UtxoCoinSelection, Error> {
174/// #         unimplemented!()
175/// #     }
176/// # }
177/// #
178///
179/// struct InMemoryDb {}
180/// # impl Persistence for InMemoryDb {
181/// #
182/// #     fn save_boarding_output(
183/// #         &self,
184/// #         sk: SecretKey,
185/// #         boarding_output: BoardingOutput,
186/// #     ) -> Result<(), Error> {
187/// #       unimplemented!()
188/// #     }
189/// #
190/// #     fn load_boarding_outputs(&self) -> Result<Vec<BoardingOutput>, Error> {
191/// #           unimplemented!()
192/// #     }
193/// #
194/// #     fn sk_for_pk(&self, pk: &XOnlyPublicKey) -> Result<SecretKey, Error> {
195/// #         unimplemented!()
196/// #     }
197/// # }
198/// #
199/// #
200/// # impl BoardingWallet for MyWallet
201/// # where
202/// # {
203/// #     fn new_boarding_output(
204/// #         &self,
205/// #         server_pk: XOnlyPublicKey,
206/// #         exit_delay: bitcoin::Sequence,
207/// #         network: Network,
208/// #     ) -> Result<BoardingOutput, Error> {
209/// #         unimplemented!()
210/// #     }
211/// #
212/// #     fn get_boarding_outputs(&self) -> Result<Vec<BoardingOutput>, Error> {
213/// #         unimplemented!()
214/// #     }
215/// #
216/// #     fn sign_for_pk(&self, pk: &XOnlyPublicKey, msg: &Message) -> Result<Signature, Error> {
217/// #         unimplemented!()
218/// #     }
219/// # }
220/// #
221/// // Initialize the client with a static keypair
222/// async fn init_client_with_keypair() -> Result<Client<MyBlockchain, MyWallet, InMemorySwapStorage, ark_client::StaticKeyProvider>, ark_client::Error> {
223///     // Create a keypair for signing transactions
224///     let secp = bitcoin::key::Secp256k1::new();
225///     let secret_key = SecretKey::from_str("your_private_key_here").unwrap();
226///     let keypair = Keypair::from_secret_key(&secp, &secret_key);
227///
228///     // Initialize blockchain and wallet implementations
229///     let blockchain = Arc::new(MyBlockchain::new("https://esplora.example.com"));
230///     let wallet = Arc::new(MyWallet {});
231///     let timeout = Duration::from_secs(30);
232///
233///     // Create the offline client (backward compatible method)
234///     let offline_client = OfflineClient::<MyBlockchain, MyWallet, InMemorySwapStorage, StaticKeyProvider>::new_with_keypair(
235///         "my-ark-client".to_string(),
236///         keypair,
237///         blockchain,
238///         wallet,
239///         "https://ark-server.example.com".to_string(),
240///         Arc::new(InMemorySwapStorage::default()),
241///         "http://boltz.example.com".to_string(),
242///         timeout,
243///         None,
244///         vec![],
245///     );
246///
247///     // Connect to the Ark server and get server info
248///     let client = offline_client.connect().await?;
249///
250///     Ok(client)
251/// }
252///
253/// // Initialize the client with a BIP32 HD wallet
254/// # use bitcoin::bip32::{Xpriv, DerivationPath};
255/// async fn init_client_with_bip32() -> Result<Client<MyBlockchain, MyWallet, InMemorySwapStorage, ark_client::Bip32KeyProvider>, ark_client::Error> {
256///     // Create a BIP32 master key and derivation path
257///     let master_key = Xpriv::from_str("xprv...").unwrap();
258///     let derivation_path = DerivationPath::from_str("m/84'/0'/0'/0/0").unwrap();
259///
260///     let key_provider = Arc::new(ark_client::Bip32KeyProvider::new(master_key, derivation_path));
261///
262///     // Initialize blockchain and wallet implementations
263///     let blockchain = Arc::new(MyBlockchain::new("https://esplora.example.com"));
264///     let wallet = Arc::new(MyWallet {});
265///     let timeout = Duration::from_secs(30);
266///
267///     // Create the offline client with BIP32 key provider
268///     let offline_client = OfflineClient::new(
269///         "my-ark-client".to_string(),
270///         key_provider,
271///         blockchain,
272///         wallet,
273///         "https://ark-server.example.com".to_string(),
274///         Arc::new(InMemorySwapStorage::default()),
275///         "http://boltz.example.com".to_string(),
276///         timeout,
277///         None,
278///         vec![],
279///     );
280///
281///     // Connect to the Ark server and get server info
282///     let client = offline_client.connect().await?;
283///
284///     Ok(client)
285/// }
286/// ```
287#[derive(Clone)]
288pub struct OfflineClient<B, W, S, K> {
289    // TODO: We could introduce a generic interface so that consumers can use either GRPC or REST.
290    network_client: ark_grpc::Client,
291    pub name: String,
292    key_provider: Arc<K>,
293    blockchain: Arc<B>,
294    secp: Secp256k1<All>,
295    wallet: Arc<W>,
296    swap_storage: Arc<S>,
297    boltz_url: String,
298    timeout: Duration,
299    delegator_pk: Option<XOnlyPublicKey>,
300    historical_delegator_pks: Vec<XOnlyPublicKey>,
301}
302
303/// A client to interact with Ark server
304///
305/// See [`OfflineClient`] docs for details.
306pub struct Client<B, W, S, K> {
307    inner: OfflineClient<B, W, S, K>,
308    pub server_info: server::Info,
309    fee_estimator: ark_fees::Estimator,
310}
311
312#[derive(Clone, Copy, Debug)]
313pub struct TxStatus {
314    pub confirmed_at: Option<i64>,
315}
316
317#[derive(Clone, Copy, Debug)]
318pub struct SpendStatus {
319    pub spend_txid: Option<Txid>,
320}
321
322pub struct AddressVtxos {
323    pub unspent: Vec<VirtualTxOutPoint>,
324    pub spent: Vec<VirtualTxOutPoint>,
325}
326
327#[derive(Clone, Debug, Default)]
328pub struct OffChainBalance {
329    pre_confirmed: Amount,
330    confirmed: Amount,
331    recoverable: Amount,
332    asset_balances: HashMap<AssetId, u64>,
333}
334
335impl OffChainBalance {
336    pub fn pre_confirmed(&self) -> Amount {
337        self.pre_confirmed
338    }
339
340    pub fn confirmed(&self) -> Amount {
341        self.confirmed
342    }
343
344    /// Balance which can only be settled, and does not require a forfeit transaction per VTXO.
345    pub fn recoverable(&self) -> Amount {
346        self.recoverable
347    }
348
349    pub fn total(&self) -> Amount {
350        self.pre_confirmed + self.confirmed + self.recoverable
351    }
352
353    /// Asset balances keyed by asset ID.
354    pub fn asset_balances(&self) -> &HashMap<AssetId, u64> {
355        &self.asset_balances
356    }
357}
358
359pub trait Blockchain {
360    fn find_outpoints(
361        &self,
362        address: &Address,
363    ) -> impl Future<Output = Result<Vec<ExplorerUtxo>, Error>> + Send;
364
365    fn find_tx(
366        &self,
367        txid: &Txid,
368    ) -> impl Future<Output = Result<Option<Transaction>, Error>> + Send;
369
370    fn get_tx_status(&self, txid: &Txid) -> impl Future<Output = Result<TxStatus, Error>> + Send;
371
372    fn get_output_status(
373        &self,
374        txid: &Txid,
375        vout: u32,
376    ) -> impl Future<Output = Result<SpendStatus, Error>> + Send;
377
378    fn broadcast(&self, tx: &Transaction) -> impl Future<Output = Result<(), Error>> + Send;
379
380    fn get_fee_rate(&self) -> impl Future<Output = Result<f64, Error>> + Send;
381
382    fn broadcast_package(
383        &self,
384        txs: &[&Transaction],
385    ) -> impl Future<Output = Result<(), Error>> + Send;
386}
387
388impl<B, W, S, K> OfflineClient<B, W, S, K>
389where
390    B: Blockchain,
391    W: BoardingWallet + OnchainWallet,
392    S: SwapStorage + 'static,
393    K: KeyProvider,
394{
395    /// Create a new offline client with a generic key provider
396    ///
397    /// # Arguments
398    ///
399    /// * `name` - Client identifier
400    /// * `key_provider` - Implementation of KeyProvider trait (StaticKeyProvider, Bip32KeyProvider,
401    ///   etc.)
402    /// * `blockchain` - Blockchain interface implementation
403    /// * `wallet` - Wallet implementation
404    /// * `ark_server_url` - URL of the Ark server
405    /// * `swap_storage` - Storage implementation for swap data
406    /// * `boltz_url` - URL of the Boltz server
407    /// * `timeout` - Timeout duration for network operations
408    #[allow(clippy::too_many_arguments)]
409    pub fn new(
410        name: String,
411        key_provider: Arc<K>,
412        blockchain: Arc<B>,
413        wallet: Arc<W>,
414        ark_server_url: String,
415        swap_storage: Arc<S>,
416        boltz_url: String,
417        timeout: Duration,
418        delegator_pk: Option<XOnlyPublicKey>,
419        historical_delegator_pks: Vec<XOnlyPublicKey>,
420    ) -> Self {
421        let secp = Secp256k1::new();
422
423        let network_client = ark_grpc::Client::new(ark_server_url);
424
425        // Normalize historical delegator keys once (preserve order, remove duplicates), then
426        // ensure the current delegator key is present at the front.
427        let mut seen = HashSet::new();
428        let mut historical_delegator_pks: Vec<_> = historical_delegator_pks
429            .into_iter()
430            .filter(|pk| seen.insert(*pk))
431            .collect();
432
433        if let Some(pk) = delegator_pk {
434            historical_delegator_pks.retain(|k| *k != pk);
435            historical_delegator_pks.insert(0, pk);
436        }
437
438        Self {
439            network_client,
440            name,
441            key_provider,
442            blockchain,
443            secp,
444            wallet,
445            swap_storage,
446            boltz_url,
447            timeout,
448            delegator_pk,
449            historical_delegator_pks,
450        }
451    }
452
453    /// Create a new offline client with a static keypair (backward compatible)
454    ///
455    /// This is a convenience method that wraps a single keypair in a StaticKeyProvider.
456    ///
457    /// # Arguments
458    ///
459    /// * `name` - Client identifier
460    /// * `kp` - Static keypair for signing
461    /// * `blockchain` - Blockchain interface implementation
462    /// * `wallet` - Wallet implementation
463    /// * `ark_server_url` - URL of the Ark server
464    /// * `swap_storage` - Storage implementation for swap data
465    /// * `boltz_url` - URL of the Boltz server
466    /// * `timeout` - Timeout duration for network operations
467    #[allow(clippy::too_many_arguments)]
468    pub fn new_with_keypair(
469        name: String,
470        kp: Keypair,
471        blockchain: Arc<B>,
472        wallet: Arc<W>,
473        ark_server_url: String,
474        swap_storage: Arc<S>,
475        boltz_url: String,
476        timeout: Duration,
477        delegator_pk: Option<XOnlyPublicKey>,
478        historical_delegator_pks: Vec<XOnlyPublicKey>,
479    ) -> OfflineClient<B, W, S, StaticKeyProvider> {
480        let key_provider = Arc::new(StaticKeyProvider::new(kp));
481
482        OfflineClient::new(
483            name,
484            key_provider,
485            blockchain,
486            wallet,
487            ark_server_url,
488            swap_storage,
489            boltz_url,
490            timeout,
491            delegator_pk,
492            historical_delegator_pks,
493        )
494    }
495
496    /// Create a new offline client with an [`Xpriv`]
497    ///
498    /// # Arguments
499    ///
500    /// * `name` - Client identifier
501    /// * `xpriv` - BIP32 Xpriv
502    /// * `blockchain` - Blockchain interface implementation
503    /// * `wallet` - Wallet implementation
504    /// * `ark_server_url` - URL of the Ark server
505    /// * `swap_storage` - Storage implementation for swap data
506    /// * `boltz_url` - URL of the Boltz server
507    /// * `timeout` - Timeout duration for network operations
508    #[allow(clippy::too_many_arguments)]
509    pub fn new_with_bip32(
510        name: String,
511        xpriv: Xpriv,
512        path: Option<DerivationPath>,
513        blockchain: Arc<B>,
514        wallet: Arc<W>,
515        ark_server_url: String,
516        swap_storage: Arc<S>,
517        boltz_url: String,
518        timeout: Duration,
519        delegator_pk: Option<XOnlyPublicKey>,
520        historical_delegator_pks: Vec<XOnlyPublicKey>,
521    ) -> OfflineClient<B, W, S, Bip32KeyProvider> {
522        let path = path.unwrap_or(
523            DerivationPath::from_str(DEFAULT_DERIVATION_PATH).expect("valid derivation path"),
524        );
525        let key_provider = Arc::new(Bip32KeyProvider::new(xpriv, path));
526
527        OfflineClient::new(
528            name,
529            key_provider,
530            blockchain,
531            wallet,
532            ark_server_url,
533            swap_storage,
534            boltz_url,
535            timeout,
536            delegator_pk,
537            historical_delegator_pks,
538        )
539    }
540
541    /// Returns the currently configured delegator pubkey, if any.
542    pub fn delegator_pk(&self) -> Option<XOnlyPublicKey> {
543        self.delegator_pk
544    }
545
546    /// Connects to the Ark server and retrieves server information.
547    ///
548    /// # Errors
549    ///
550    /// Returns an error if the connection fails or times out.
551    pub async fn connect(mut self) -> Result<Client<B, W, S, K>, Error> {
552        timeout_op(self.timeout, self.network_client.connect())
553            .await
554            .context("Failed to connect to Ark server")??;
555
556        self.finish_connect().await
557    }
558
559    /// Connects to the Ark server and retrieves server information.
560    ///
561    /// If it encounters errors, it will retry `max_retries`.
562    ///
563    /// # Errors
564    ///
565    /// Returns an error if the connection fails or times out.
566    pub async fn connect_with_retries(
567        mut self,
568        max_retries: usize,
569    ) -> Result<Client<B, W, S, K>, Error> {
570        let mut n_retries = 0;
571        while n_retries < max_retries {
572            let res = timeout_op(self.timeout, self.network_client.connect())
573                .await
574                .context("Failed to connect to Ark server")?;
575
576            match res {
577                Ok(()) => break,
578                Err(error) => {
579                    tracing::warn!(?error, "Failed to connect to Ark server, retrying");
580
581                    sleep(Duration::from_secs(2)).await;
582
583                    n_retries += 1;
584
585                    continue;
586                }
587            };
588        }
589
590        self.finish_connect().await
591    }
592
593    async fn finish_connect(mut self) -> Result<Client<B, W, S, K>, Error> {
594        let server_info = timeout_op(self.timeout, self.network_client.get_info())
595            .await
596            .context("Failed to get Ark server info")??;
597
598        tracing::debug!(
599            name = self.name,
600            ark_server_url = ?self.network_client,
601            "Connected to Ark server"
602        );
603
604        let fee_estimator_config = server_info
605            .fees
606            .clone()
607            .map(|fees| ark_fees::Config {
608                intent_offchain_input_program: fees.intent_fee.offchain_input.unwrap_or_default(),
609                intent_onchain_input_program: fees.intent_fee.onchain_input.unwrap_or_default(),
610                intent_offchain_output_program: fees.intent_fee.offchain_output.unwrap_or_default(),
611                intent_onchain_output_program: fees.intent_fee.onchain_output.unwrap_or_default(),
612            })
613            .unwrap_or_default();
614
615        let fee_estimator =
616            ark_fees::Estimator::new(fee_estimator_config).map_err(Error::ark_server)?;
617
618        let client = Client {
619            inner: self,
620            server_info,
621            fee_estimator,
622        };
623
624        if let Err(error) = client.discover_keys(DEFAULT_GAP_LIMIT).await {
625            tracing::warn!(?error, "Failed during key discovery");
626        };
627
628        Ok(client)
629    }
630}
631
632impl<B, W, S, K> Client<B, W, S, K>
633where
634    B: Blockchain,
635    W: BoardingWallet + OnchainWallet,
636    S: SwapStorage + 'static,
637    K: KeyProvider,
638{
639    /// Returns the currently configured delegator pubkey, if any.
640    pub fn delegator_pk(&self) -> Option<XOnlyPublicKey> {
641        self.inner.delegator_pk()
642    }
643
644    /// Get a new offchain receiving address.
645    ///
646    /// When a delegator is configured (via `delegator_pk` passed to [`OfflineClient::new`]),
647    /// returns a 3-leaf delegate address. Otherwise returns a standard 2-leaf address.
648    ///
649    /// For HD wallets, this will derive a new address each time it's called.
650    /// For static key providers, this will always return the same address.
651    pub fn get_offchain_address(&self) -> Result<(ArkAddress, Vtxo), Error> {
652        let server_info = &self.server_info;
653
654        let server_signer = server_info.signer_pk.into();
655        let owner = self
656            .next_keypair(KeypairIndex::LastUnused)?
657            .public_key()
658            .into();
659
660        let vtxo = self.make_vtxo(server_signer, owner)?;
661
662        let ark_address = vtxo.to_ark_address();
663
664        Ok((ark_address, vtxo))
665    }
666
667    /// Get all known offchain addresses for this wallet.
668    ///
669    /// When a delegator is configured, this returns **both** the default (2-leaf) and delegate
670    /// (3-leaf) addresses for each key, so that VTXOs at either address are visible. If
671    /// historical delegator keys are set via `historical_delegator_pks` passed to
672    /// [`OfflineClient::new`], addresses for those are included too.
673    pub fn get_offchain_addresses(&self) -> Result<Vec<(ArkAddress, Vtxo)>, Error> {
674        let server_info = &self.server_info;
675        let server_signer = server_info.signer_pk.into();
676
677        let pks = self.inner.key_provider.get_cached_pks()?;
678
679        let mut results = Vec::new();
680
681        for owner_pk in &pks {
682            // Always include the default (2-leaf) address.
683            let default_vtxo = Vtxo::new_default(
684                self.secp(),
685                server_signer,
686                *owner_pk,
687                server_info.unilateral_exit_delay,
688                server_info.network,
689            )?;
690            results.push((default_vtxo.to_ark_address(), default_vtxo));
691
692            // Include delegate addresses for all known delegator keys.
693            let mut seen = HashSet::new();
694            for dpk in &self.inner.historical_delegator_pks {
695                if !seen.insert(dpk) {
696                    continue;
697                }
698                let delegate_vtxo = Vtxo::new_with_delegator(
699                    self.secp(),
700                    server_signer,
701                    *owner_pk,
702                    *dpk,
703                    server_info.unilateral_exit_delay,
704                    server_info.network,
705                )?;
706                results.push((delegate_vtxo.to_ark_address(), delegate_vtxo));
707            }
708        }
709
710        Ok(results)
711    }
712
713    /// Build a [`Vtxo`] for the given owner key, using a 3-leaf delegate VTXO if a delegator is
714    /// configured, otherwise a standard 2-leaf default VTXO.
715    fn make_vtxo(
716        &self,
717        server_signer: XOnlyPublicKey,
718        owner: XOnlyPublicKey,
719    ) -> Result<Vtxo, Error> {
720        let server_info = &self.server_info;
721        match self.inner.delegator_pk {
722            Some(delegator) => Vtxo::new_with_delegator(
723                self.secp(),
724                server_signer,
725                owner,
726                delegator,
727                server_info.unilateral_exit_delay,
728                server_info.network,
729            )
730            .map_err(Into::into),
731            None => Vtxo::new_default(
732                self.secp(),
733                server_signer,
734                owner,
735                server_info.unilateral_exit_delay,
736                server_info.network,
737            )
738            .map_err(Into::into),
739        }
740    }
741
742    /// Discover and cache used keys using BIP44-style gap limit
743    ///
744    /// This method derives keys in batches, checks all at once via list_vtxos,
745    /// caches used ones, and stops when a full batch has no used keys.
746    ///
747    /// Returns the number of discovered keys. No-op for StaticKeyProvider.
748    ///
749    /// # Arguments
750    ///
751    /// * `gap_limit` - Number of consecutive unused addresses before stopping
752    pub async fn discover_keys(&self, gap_limit: u32) -> Result<u32, Error> {
753        if !self.inner.key_provider.supports_discovery() {
754            tracing::debug!("Key provider does not support discovery, skipping");
755            return Ok(0);
756        }
757
758        let server_info = &self.server_info;
759        let server_signer: XOnlyPublicKey = server_info.signer_pk.into();
760
761        let mut start_index = 0u32;
762        let mut discovered_count = 0u32;
763
764        tracing::info!(gap_limit, "Starting key discovery");
765
766        loop {
767            // Generate a batch of gap_limit keys
768            let mut batch: Vec<(u32, Keypair, Vec<ArkAddress>)> =
769                Vec::with_capacity(gap_limit as usize);
770
771            for i in 0..gap_limit {
772                let index = start_index
773                    .checked_add(i)
774                    .ok_or_else(|| Error::ad_hoc("Key discovery index overflow"))?;
775
776                let kp = match self.inner.key_provider.derive_at_discovery_index(index)? {
777                    Some(kp) => kp,
778                    None => break,
779                };
780
781                let owner_pk = kp.x_only_public_key().0;
782
783                let mut addresses =
784                    Vec::with_capacity(1 + self.inner.historical_delegator_pks.len());
785
786                // Default (2-leaf) address.
787                let default_vtxo = Vtxo::new_default(
788                    self.secp(),
789                    server_signer,
790                    owner_pk,
791                    server_info.unilateral_exit_delay,
792                    server_info.network,
793                )?;
794                addresses.push(default_vtxo.to_ark_address());
795
796                // Delegate (3-leaf) addresses for each known delegator.
797                for dpk in &self.inner.historical_delegator_pks {
798                    let delegate_vtxo = Vtxo::new_with_delegator(
799                        self.secp(),
800                        server_signer,
801                        owner_pk,
802                        *dpk,
803                        server_info.unilateral_exit_delay,
804                        server_info.network,
805                    )?;
806                    addresses.push(delegate_vtxo.to_ark_address());
807                }
808
809                batch.push((index, kp, addresses));
810            }
811
812            if batch.is_empty() {
813                break;
814            }
815
816            // Query all addresses in batch at once
817            let addresses = batch.iter().flat_map(|(_, _, addrs)| addrs.iter().copied());
818
819            let vtxo_list = self.list_vtxos_for_addresses(addresses).await?;
820
821            // Build set of used scripts from response
822            let used_scripts: HashSet<&ScriptBuf> = vtxo_list.all().map(|v| &v.script).collect();
823
824            // Cache keypairs for used addresses (match by script)
825            let mut found_any = false;
826            for (index, kp, addrs) in batch {
827                let used_addr = addrs.iter().find(|addr| {
828                    let script = addr.to_p2tr_script_pubkey();
829                    used_scripts.contains(&script)
830                });
831                if let Some(addr) = used_addr {
832                    tracing::debug!(index, addr = %addr, "Found used address");
833                    self.inner
834                        .key_provider
835                        .cache_discovered_keypair(index, kp)?;
836                    discovered_count += 1;
837                    found_any = true;
838                }
839            }
840
841            // Stop if no used addresses found in this batch (gap limit reached)
842            if !found_any {
843                break;
844            }
845
846            start_index = start_index
847                .checked_add(gap_limit)
848                .ok_or_else(|| Error::ad_hoc("Key discovery index overflow"))?;
849        }
850
851        tracing::info!(discovered_count, "Key discovery completed");
852
853        Ok(discovered_count)
854    }
855
856    // At the moment we are always generating the same address.
857    pub fn get_boarding_address(&self) -> Result<Address, Error> {
858        let server_info = &self.server_info;
859
860        let boarding_output = self.inner.wallet.new_boarding_output(
861            server_info.signer_pk.into(),
862            server_info.boarding_exit_delay,
863            server_info.network,
864        )?;
865
866        Ok(boarding_output.address().clone())
867    }
868
869    pub fn get_onchain_address(&self) -> Result<Address, Error> {
870        self.inner.wallet.get_onchain_address()
871    }
872
873    pub fn get_boarding_addresses(&self) -> Result<Vec<Address>, Error> {
874        let address = self.get_boarding_address()?;
875
876        Ok(vec![address])
877    }
878
879    pub async fn get_virtual_tx_outpoints(
880        &self,
881        addresses: impl Iterator<Item = ArkAddress>,
882    ) -> Result<Vec<VirtualTxOutPoint>, Error> {
883        let request = GetVtxosRequest::new_for_addresses(addresses);
884        self.fetch_all_vtxos(request).await
885    }
886
887    pub async fn list_vtxos(&self) -> Result<(VtxoList, HashMap<ScriptBuf, Vtxo>), Error> {
888        let ark_addresses = self.get_offchain_addresses()?;
889
890        let script_pubkey_to_vtxo_map = ark_addresses
891            .iter()
892            .map(|(a, v)| (a.to_p2tr_script_pubkey(), v.clone()))
893            .collect();
894
895        let addresses = ark_addresses.iter().map(|(a, _)| a).copied();
896
897        let vtxo_list = self.list_vtxos_for_addresses(addresses).await?;
898
899        Ok((vtxo_list, script_pubkey_to_vtxo_map))
900    }
901
902    pub async fn list_vtxos_for_addresses(
903        &self,
904        addresses: impl Iterator<Item = ArkAddress>,
905    ) -> Result<VtxoList, Error> {
906        let virtual_tx_outpoints = self
907            .get_virtual_tx_outpoints(addresses)
908            .await
909            .context("failed to get VTXOs for addresses")?;
910
911        let vtxo_list = VtxoList::new(self.server_info.dust, virtual_tx_outpoints);
912
913        Ok(vtxo_list)
914    }
915
916    pub async fn list_vtxos_for_outpoints(
917        &self,
918        outpoints: Vec<OutPoint>,
919    ) -> Result<(VtxoList, HashMap<ScriptBuf, Vtxo>), Error> {
920        let ark_addresses = self.get_offchain_addresses()?;
921
922        let script_pubkey_to_vtxo_map = ark_addresses
923            .iter()
924            .map(|(a, v)| (a.to_p2tr_script_pubkey(), v.clone()))
925            .collect::<HashMap<_, _>>();
926
927        let request = GetVtxosRequest::new_for_outpoints(&outpoints);
928        let virtual_tx_outpoints = self.fetch_all_vtxos(request).await?;
929
930        // Filter out outpoints for which we don't have spend info.
931        let virtual_tx_outpoints = virtual_tx_outpoints
932            .into_iter()
933            .filter(|v| match script_pubkey_to_vtxo_map.get(&v.script) {
934                Some(_) => true,
935                None => {
936                    tracing::debug!(outpoint = %v.outpoint, "Missing spend info for VTXO");
937
938                    false
939                }
940            })
941            .collect();
942
943        let vtxo_list = VtxoList::new(self.server_info.dust, virtual_tx_outpoints);
944
945        Ok((vtxo_list, script_pubkey_to_vtxo_map))
946    }
947
948    pub async fn get_vtxo_chain(
949        &self,
950        out_point: OutPoint,
951        size: i32,
952        index: i32,
953    ) -> Result<Option<VtxoChainResponse>, Error> {
954        let vtxo_chain = timeout_op(
955            self.inner.timeout,
956            self.network_client()
957                .get_vtxo_chain(Some(out_point), Some((size, index))),
958        )
959        .await
960        .context("Failed to fetch VTXO chain")??;
961
962        Ok(Some(vtxo_chain))
963    }
964
965    pub async fn offchain_balance(&self) -> Result<OffChainBalance, Error> {
966        let (vtxo_list, _) = self.list_vtxos().await.context("failed to list VTXOs")?;
967
968        let pre_confirmed = vtxo_list
969            .pre_confirmed()
970            .fold(Amount::ZERO, |acc, x| acc + x.amount);
971
972        let confirmed = vtxo_list
973            .confirmed()
974            .fold(Amount::ZERO, |acc, x| acc + x.amount);
975
976        let recoverable = vtxo_list
977            .recoverable()
978            .fold(Amount::ZERO, |acc, x| acc + x.amount);
979
980        // Aggregate asset balances from all spendable VTXOs.
981        let mut asset_balances: HashMap<AssetId, u64> = HashMap::new();
982        for vtxo in vtxo_list.spendable_offchain() {
983            for asset in &vtxo.assets {
984                let total = asset_balances
985                    .get(&asset.asset_id)
986                    .copied()
987                    .unwrap_or(0)
988                    .checked_add(asset.amount)
989                    .ok_or_else(|| Error::ad_hoc("asset balance overflow"))?;
990                asset_balances.insert(asset.asset_id, total);
991            }
992        }
993
994        Ok(OffChainBalance {
995            pre_confirmed,
996            confirmed,
997            recoverable,
998            asset_balances,
999        })
1000    }
1001
1002    /// Get information about an asset by its ID.
1003    pub async fn get_asset(&self, asset_id: AssetId) -> Result<server::AssetInfo, Error> {
1004        timeout_op(
1005            self.inner.timeout,
1006            self.network_client().get_asset(asset_id),
1007        )
1008        .await
1009        .context("Failed to get asset info")?
1010        .map_err(Error::ark_server)
1011    }
1012
1013    pub async fn transaction_history(&self) -> Result<Vec<history::Transaction>, Error> {
1014        let mut boarding_transactions = Vec::new();
1015        let mut boarding_commitment_transactions = Vec::new();
1016
1017        let boarding_addresses = self.get_boarding_addresses()?;
1018        for boarding_address in boarding_addresses.iter() {
1019            let outpoints = timeout_op(
1020                self.inner.timeout,
1021                self.blockchain().find_outpoints(boarding_address),
1022            )
1023            .await
1024            .context("Failed to find outpoints")??;
1025
1026            for ExplorerUtxo {
1027                outpoint,
1028                amount,
1029                confirmation_blocktime,
1030                ..
1031            } in outpoints.iter()
1032            {
1033                let confirmed_at = confirmation_blocktime.map(|t| t as i64);
1034
1035                boarding_transactions.push(history::Transaction::Boarding {
1036                    txid: outpoint.txid,
1037                    amount: *amount,
1038                    confirmed_at,
1039                });
1040
1041                let status = timeout_op(
1042                    self.inner.timeout,
1043                    self.blockchain()
1044                        .get_output_status(&outpoint.txid, outpoint.vout),
1045                )
1046                .await
1047                .context("Failed to get Tx output status")??;
1048
1049                if let Some(spend_txid) = status.spend_txid {
1050                    boarding_commitment_transactions.push(spend_txid);
1051                }
1052            }
1053        }
1054
1055        let (vtxo_list, _) = self.list_vtxos().await?;
1056
1057        let spent_outpoints = vtxo_list.spent().cloned().collect::<Vec<_>>();
1058        let unspent_outpoints = vtxo_list.all_unspent().cloned().collect::<Vec<_>>();
1059
1060        let incoming_transactions = generate_incoming_vtxo_transaction_history(
1061            &spent_outpoints,
1062            &unspent_outpoints,
1063            &boarding_commitment_transactions,
1064        )?;
1065
1066        let outgoing_txs =
1067            generate_outgoing_vtxo_transaction_history(&spent_outpoints, &unspent_outpoints)?;
1068
1069        let mut outgoing_transactions = vec![];
1070        for tx in outgoing_txs {
1071            let tx = match tx {
1072                OutgoingTransaction::Complete(tx) => tx,
1073                OutgoingTransaction::Incomplete(incomplete_tx) => {
1074                    let first_outpoint = incomplete_tx.first_outpoint();
1075
1076                    let request = GetVtxosRequest::new_for_outpoints(&[first_outpoint]);
1077                    let vtxos = self.fetch_all_vtxos(request).await?;
1078
1079                    match vtxos.first() {
1080                        Some(virtual_tx_outpoint) => {
1081                            match incomplete_tx.finish(virtual_tx_outpoint) {
1082                                Ok(tx) => tx,
1083                                Err(e) => {
1084                                    tracing::warn!(
1085                                        %first_outpoint,
1086                                        "Could not finish outgoing TX, skipping: {e}"
1087                                    );
1088                                    continue;
1089                                }
1090                            }
1091                        }
1092                        None => {
1093                            tracing::warn!(
1094                                %first_outpoint,
1095                                "Could not find virtual TX outpoint for outgoing TX, skipping"
1096                            );
1097                            continue;
1098                        }
1099                    }
1100                }
1101                OutgoingTransaction::IncompleteOffboard(incomplete_offboard) => {
1102                    let status = timeout_op(
1103                        self.inner.timeout,
1104                        self.blockchain()
1105                            .get_tx_status(&incomplete_offboard.commitment_txid()),
1106                    )
1107                    .await
1108                    .context("failed to get commitment TX status")??;
1109
1110                    incomplete_offboard.finish(status.confirmed_at)
1111                }
1112            };
1113
1114            outgoing_transactions.push(tx);
1115        }
1116
1117        let mut txs = [
1118            boarding_transactions,
1119            incoming_transactions,
1120            outgoing_transactions,
1121        ]
1122        .concat();
1123
1124        sort_transactions_by_created_at(&mut txs);
1125
1126        Ok(txs)
1127    }
1128
1129    /// The server's dust threshold amount.
1130    pub fn dust(&self) -> Amount {
1131        self.server_info.dust
1132    }
1133
1134    pub fn network_client(&self) -> ark_grpc::Client {
1135        self.inner.network_client.clone()
1136    }
1137
1138    /// Fetch all VTXOs for a request, handling pagination internally.
1139    async fn fetch_all_vtxos(
1140        &self,
1141        request: GetVtxosRequest,
1142    ) -> Result<Vec<VirtualTxOutPoint>, Error> {
1143        if request.reference().is_empty() {
1144            return Ok(Vec::new());
1145        }
1146
1147        let mut all_vtxos = Vec::new();
1148        let mut cursor = 0;
1149        const PAGE_SIZE: i32 = 100;
1150
1151        loop {
1152            let paged_request = request.clone().with_page(PAGE_SIZE, cursor);
1153            let response = timeout_op(
1154                self.inner.timeout,
1155                self.network_client().list_vtxos(paged_request),
1156            )
1157            .await
1158            .context("failed to fetch list of VTXOs")??;
1159
1160            all_vtxos.extend(response.vtxos);
1161
1162            // Use server-provided cursor for next page; next == total means end
1163            match response.page {
1164                Some(page) if page.next < page.total => {
1165                    cursor = page.next;
1166                }
1167                _ => break,
1168            }
1169        }
1170
1171        Ok(all_vtxos)
1172    }
1173
1174    fn next_keypair(&self, keypair_index: KeypairIndex) -> Result<Keypair, Error> {
1175        self.inner.key_provider.get_next_keypair(keypair_index)
1176    }
1177    fn keypair_by_pk(&self, pk: &XOnlyPublicKey) -> Result<Keypair, Error> {
1178        self.inner.key_provider.get_keypair_for_pk(pk)
1179    }
1180
1181    fn derivation_index_for_pk(&self, pk: &XOnlyPublicKey) -> Option<u32> {
1182        self.inner.key_provider.get_derivation_index_for_pk(pk)
1183    }
1184
1185    fn secp(&self) -> &Secp256k1<All> {
1186        &self.inner.secp
1187    }
1188
1189    fn blockchain(&self) -> &B {
1190        &self.inner.blockchain
1191    }
1192
1193    fn swap_storage(&self) -> &S {
1194        &self.inner.swap_storage
1195    }
1196
1197    /// Use the P2A output of a transaction to bump its transaction fee with a child transaction.
1198    pub async fn bump_tx(&self, parent: &Transaction) -> Result<Transaction, Error> {
1199        let fee_rate = timeout_op(self.inner.timeout, self.blockchain().get_fee_rate())
1200            .await
1201            .context("Failed to retrieve fee rate")??;
1202
1203        let change_address = self.inner.wallet.get_onchain_address()?;
1204
1205        // Create a closure that converts CoinSelectionResult to UtxoCoinSelection
1206        let select_coins_fn =
1207            |target_amount: Amount| -> Result<UtxoCoinSelection, ark_core::Error> {
1208                self.inner.wallet.select_coins(target_amount).map_err(|e| {
1209                    ark_core::Error::ad_hoc(format!("failed to select coins for anchor TX: {e}"))
1210                })
1211            };
1212
1213        // Build the PSBT using ark-core (includes witness UTXO setup)
1214        let mut psbt = build_anchor_tx(parent, change_address, fee_rate, select_coins_fn)
1215            .map_err(|e| Error::ad_hoc(e.to_string()))?;
1216
1217        // Sign the transaction
1218        self.inner
1219            .wallet
1220            .sign(&mut psbt)
1221            .context("failed to sign bump TX")?;
1222
1223        // Extract the final transaction
1224        let tx = psbt.extract_tx().map_err(Error::ad_hoc)?;
1225
1226        Ok(tx)
1227    }
1228
1229    /// Subscribe to receive transaction notifications for specific VTXO scripts
1230    ///
1231    /// This method allows you to subscribe to get notified about transactions
1232    /// affecting the provided VTXO addresses. It can also be used to update an
1233    /// existing subscription by adding new scripts to it.
1234    ///
1235    /// # Arguments
1236    ///
1237    /// * `scripts` - Vector of ArkAddress to subscribe to
1238    /// * `subscription_id` - Unique identifier for the subscription. Use the same ID to update an
1239    ///   existing subscription. Use None for new subscriptions
1240    ///
1241    /// # Returns
1242    ///
1243    /// Returns the subscription ID if successful
1244    pub async fn subscribe_to_scripts(
1245        &self,
1246        scripts: Vec<ArkAddress>,
1247        subscription_id: Option<String>,
1248    ) -> Result<String, Error> {
1249        self.network_client()
1250            .subscribe_to_scripts(scripts, subscription_id)
1251            .await
1252            .map_err(Into::into)
1253    }
1254
1255    /// Remove scripts from an existing subscription
1256    ///
1257    /// This method allows you to unsubscribe from receiving notifications for
1258    /// specific VTXO scripts while keeping the subscription active for other scripts.
1259    ///
1260    /// # Arguments
1261    ///
1262    /// * `scripts` - Vector of ArkAddress to unsubscribe from
1263    /// * `subscription_id` - The subscription ID to update
1264    pub async fn unsubscribe_from_scripts(
1265        &self,
1266        scripts: Vec<ArkAddress>,
1267        subscription_id: String,
1268    ) -> Result<(), Error> {
1269        self.network_client()
1270            .unsubscribe_from_scripts(scripts, subscription_id)
1271            .await
1272            .map_err(Into::into)
1273    }
1274
1275    /// Get a subscription stream that returns subscription responses
1276    ///
1277    /// This method returns a stream that yields SubscriptionResponse messages
1278    /// containing information about new and spent VTXOs for the subscribed scripts.
1279    ///
1280    /// # Arguments
1281    ///
1282    /// * `subscription_id` - The subscription ID to get the stream for
1283    ///
1284    /// # Returns
1285    ///
1286    /// Returns a Stream of SubscriptionResponse messages
1287    pub async fn get_subscription(
1288        &self,
1289        subscription_id: String,
1290    ) -> Result<impl Stream<Item = Result<SubscriptionResponse, ark_grpc::Error>> + Unpin, Error>
1291    {
1292        self.network_client()
1293            .get_subscription(subscription_id)
1294            .await
1295            .map_err(Into::into)
1296    }
1297}