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
84pub const DEFAULT_GAP_LIMIT: u32 = 20;
89
90pub const DEFAULT_BOLTZ_REFERRAL_ID: &str = "arkade-rs-SDK";
93
94#[derive(Clone)]
294pub struct OfflineClient<B, W, S, K> {
295 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
310pub 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 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 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 #[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 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 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 #[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 #[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 pub fn delegator_pk(&self) -> Option<XOnlyPublicKey> {
572 self.delegator_pk
573 }
574
575 pub fn boltz_referral_id(&self) -> Option<&str> {
577 self.boltz_referral_id.as_deref()
578 }
579
580 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 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 pub fn delegator_pk(&self) -> Option<XOnlyPublicKey> {
675 self.inner.delegator_pk()
676 }
677
678 pub fn boltz_referral_id(&self) -> Option<&str> {
680 self.inner.boltz_referral_id()
681 }
682
683 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 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 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 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 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 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 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 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 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 let addresses = batch.iter().flat_map(|(_, _, addrs)| addrs.iter().copied());
857
858 let vtxo_list = self.list_vtxos_for_addresses(addresses).await?;
859
860 let used_scripts: HashSet<&ScriptBuf> = vtxo_list.all().map(|v| &v.script).collect();
862
863 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 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 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 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 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 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 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 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 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 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 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 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 self.inner
1258 .wallet
1259 .sign(&mut psbt)
1260 .context("failed to sign bump TX")?;
1261
1262 let tx = psbt.extract_tx().map_err(Error::ad_hoc)?;
1264
1265 Ok(tx)
1266 }
1267
1268 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 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 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}