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