1use crate::error::ErrorContext;
2use crate::key_provider::KeypairIndex;
3use crate::utils::sleep;
4use crate::utils::timeout_op;
5use crate::utils::unix_now;
6use crate::wallet::OnchainWallet;
7use ark_core::asset::AssetId;
8use ark_core::build_anchor_tx;
9use ark_core::contract::BoardingContract;
10use ark_core::contract::ContractContext;
11use ark_core::contract::ContractState;
12use ark_core::contract::ContractType;
13use ark_core::contract::DefaultVtxoContract;
14use ark_core::contract::DelegateVtxoContract;
15use ark_core::contract::StoredContract;
16use ark_core::contract::VhtlcContract;
17use ark_core::history;
18use ark_core::history::generate_incoming_vtxo_transaction_history;
19use ark_core::history::generate_outgoing_vtxo_transaction_history;
20use ark_core::history::sort_transactions_by_created_at;
21use ark_core::history::OutgoingTransaction;
22use ark_core::server;
23use ark_core::server::GetVtxosRequest;
24use ark_core::server::SubscriptionResponse;
25use ark_core::server::VirtualTxOutPoint;
26use ark_core::ArkAddress;
27use ark_core::ExplorerUtxo;
28use ark_core::UtxoCoinSelection;
29use ark_core::Vtxo;
30use ark_core::VtxoList;
31use ark_core::DEFAULT_DERIVATION_PATH;
32use ark_grpc::VtxoChainResponse;
33use bitcoin::bip32::DerivationPath;
34use bitcoin::bip32::Xpriv;
35use bitcoin::key::Keypair;
36use bitcoin::key::Secp256k1;
37use bitcoin::secp256k1::schnorr::Signature;
38use bitcoin::secp256k1::All;
39use bitcoin::secp256k1::Message;
40use bitcoin::Address;
41use bitcoin::Amount;
42use bitcoin::Network;
43use bitcoin::OutPoint;
44use bitcoin::ScriptBuf;
45use bitcoin::Transaction;
46use bitcoin::Txid;
47use bitcoin::XOnlyPublicKey;
48use futures::Future;
49use futures::Stream;
50use std::collections::HashMap;
51use std::collections::HashSet;
52use std::str::FromStr;
53use std::sync::Arc;
54use std::sync::Mutex;
55use std::sync::RwLock;
56use std::time::Duration;
57use std::time::Instant;
58
59pub mod contract;
60pub mod error;
61pub mod key_provider;
62pub mod swap_storage;
63pub mod vtxo_watcher;
64pub mod wallet;
65
66mod asset;
67mod batch;
68mod boltz;
69mod coin_select;
70mod fee_estimation;
71mod migration;
72mod send_vtxo;
73mod unilateral_exit;
74mod utils;
75
76pub use ark_core::server::DeprecatedSignerStatus;
77pub use ark_core::server::ServerSignerStatus;
78pub use asset::IssueAssetResult;
79pub use boltz::ChainSwapAmount;
80pub use boltz::ChainSwapData;
81pub use boltz::ChainSwapDirection;
82pub use boltz::ChainSwapResult;
83pub use boltz::PendingVhtlcSpendTx;
84pub use boltz::PendingVhtlcSpendType;
85pub use boltz::ReverseSwapData;
86pub use boltz::SubmarineSwapData;
87pub use boltz::SwapAmount;
88pub use boltz::SwapStatus;
89pub use boltz::SwapStatusInfo;
90pub use boltz::SwapType;
91pub use boltz::TimeoutBlockHeights;
92pub use contract::AnnotatedBoardingOutput;
93pub use contract::AnnotatedVtxo;
94pub use contract::AnnotatedVtxoList;
95pub use contract::ContractManager;
96pub use contract::ContractRegistry;
97pub use contract::ContractStore;
98pub use contract::MemoryContractStore;
99#[cfg(feature = "sqlite")]
100pub use contract::SqliteContractStore;
101pub use error::Error;
102pub use key_provider::Bip32KeyProvider;
103pub use key_provider::DiscoverableKeyProvider;
104pub use key_provider::KeyProvider;
105pub use key_provider::StaticKeyProvider;
106pub use lightning_invoice;
107pub use migration::DeprecatedSignerMigrationReport;
108pub use migration::DeprecatedSignerReport;
109pub use migration::MigrationLegReport;
110pub use migration::MigrationSkipReason;
111pub use migration::MigrationVtxoRef;
112pub use migration::MAX_VTXOS_PER_SETTLEMENT;
113pub use swap_storage::InMemorySwapStorage;
114#[cfg(feature = "sqlite")]
115pub use swap_storage::SqliteSwapStorage;
116pub use swap_storage::SwapStorage;
117
118pub const DEFAULT_GAP_LIMIT: u32 = 20;
123
124pub const DEFAULT_BOLTZ_REFERRAL_ID: &str = "arkade-rs-SDK";
127
128#[derive(Clone, Debug, Default, PartialEq, Eq)]
130pub struct ContractRestoreReport {
131 pub gap_limit: u32,
133 pub scanned_from: Option<u32>,
135 pub scanned_to_exclusive: Option<u32>,
137 pub scanned_keys: u32,
139 pub discovered_key_indexes: Vec<u32>,
141 pub last_used_key_index: Option<u32>,
143 pub next_key_index: Option<u32>,
145 pub offchain_contracts: u32,
147 pub boarding_contracts: u32,
149 pub inserted_contracts: u32,
151 pub known_contracts: u32,
153 pub entries: Vec<ContractRestoreEntry>,
155}
156
157impl ContractRestoreReport {
158 pub fn discovered_keys(&self) -> u32 {
159 self.discovered_key_indexes.len() as u32
160 }
161
162 pub fn discovered_contracts(&self) -> u32 {
163 self.entries.len() as u32
164 }
165}
166
167#[derive(Clone, Debug, PartialEq, Eq)]
168pub struct ContractRestoreEntry {
169 pub key_index: u32,
170 pub contract_type: ContractType,
171 pub script_pubkey: ScriptBuf,
172 pub status: ContractRestoreEntryStatus,
173 pub discovery: ContractRestoreDiscovery,
174}
175
176#[derive(Clone, Copy, Debug, PartialEq, Eq)]
177pub enum ContractRestoreEntryStatus {
178 Inserted,
179 Known,
180}
181
182#[derive(Clone, Debug, PartialEq, Eq)]
183pub enum ContractRestoreDiscovery {
184 Offchain {
185 vtxos: Vec<ContractRestoreVtxo>,
186 },
187 Boarding {
188 outpoints: Vec<ContractRestoreOutpoint>,
189 },
190}
191
192#[derive(Clone, Debug, PartialEq, Eq)]
193pub struct ContractRestoreVtxo {
194 pub outpoint: OutPoint,
195 pub amount: Amount,
196 pub is_spent: bool,
197 pub is_swept: bool,
198 pub is_unrolled: bool,
199}
200
201#[derive(Clone, Debug, PartialEq, Eq)]
202pub struct ContractRestoreOutpoint {
203 pub outpoint: OutPoint,
204 pub amount: Amount,
205 pub confirmation_blocktime: Option<u64>,
206 pub confirmations: u64,
207}
208
209#[derive(Clone, Debug, PartialEq, Eq)]
211pub struct ContractInfo {
212 pub contract: StoredContract,
214 pub address: Option<String>,
216 pub address_kind: Option<ContractAddressKind>,
218 pub server_pk: Option<XOnlyPublicKey>,
220 pub signer_status: Option<ServerSignerStatus>,
222}
223
224#[derive(Clone, Copy, Debug, PartialEq, Eq)]
225pub enum ContractAddressKind {
226 Ark,
227 Bitcoin,
228}
229
230pub const ARKADE_MAINNET_URL: &str = "https://arkade.computer";
232
233pub const ARKADE_MUTINYNET_URL: &str = "https://mutinynet.arkade.sh";
235
236pub const BOLTZ_MAINNET_URL: &str = "https://api.boltz.exchange";
238
239pub const BOLTZ_MUTINYNET_URL: &str = "https://api.boltz.mutinynet.arkade.sh";
241
242pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
244
245pub const DEFAULT_SERVER_INFO_TTL: Duration = Duration::from_secs(15 * 60);
247
248#[derive(Clone, Debug, Default)]
250pub enum BoltzReferralId {
251 #[default]
253 Default,
254 Disabled,
256 Custom(String),
258}
259
260#[derive(Clone, Debug)]
265pub struct OfflineClientConfig {
266 pub ark_server_url: String,
267 pub boltz_url: String,
268 pub timeout: Duration,
269 pub server_info_ttl: Duration,
270 pub boltz_referral_id: BoltzReferralId,
271 pub delegator_pk: Option<XOnlyPublicKey>,
272 pub historical_delegator_pks: Vec<XOnlyPublicKey>,
273}
274
275impl Default for OfflineClientConfig {
276 fn default() -> Self {
277 Self {
278 ark_server_url: ARKADE_MAINNET_URL.to_string(),
279 boltz_url: BOLTZ_MAINNET_URL.to_string(),
280 timeout: DEFAULT_TIMEOUT,
281 server_info_ttl: DEFAULT_SERVER_INFO_TTL,
282 boltz_referral_id: BoltzReferralId::default(),
283 delegator_pk: None,
284 historical_delegator_pks: Vec::new(),
285 }
286 }
287}
288
289#[derive(Clone)]
441pub struct OfflineClient<B, W, S> {
442 network_client: ark_grpc::Client,
444 key_provider: Arc<dyn KeyProvider>,
445 discoverable_key_provider: Option<Arc<dyn DiscoverableKeyProvider>>,
446 blockchain: Arc<B>,
447 secp: Secp256k1<All>,
448 wallet: Arc<W>,
449 swap_storage: Arc<S>,
450 boltz_url: String,
451 boltz_referral_id: Option<String>,
452 timeout: Duration,
453 server_info_ttl: Duration,
454 contract_store: Arc<Mutex<Option<Box<dyn ContractStore>>>>,
455 delegator_pk: Option<XOnlyPublicKey>,
456 historical_delegator_pks: Vec<XOnlyPublicKey>,
457}
458
459pub struct Client<B, W, S> {
463 inner: OfflineClient<B, W, S>,
464 state: Arc<RwLock<ServerState>>,
465 server_info_refresh_lock: Arc<tokio::sync::Mutex<()>>,
466}
467
468struct ServerState {
469 server_info: server::Info,
470 fee_estimator: ark_fees::Estimator,
471 server_info_refreshed_at: Instant,
472 contract_manager: Mutex<ContractManager>,
473}
474
475#[derive(Clone, Debug)]
476enum RestoreCandidate {
477 DefaultVtxo(DefaultVtxoContract),
478 DelegateVtxo(DelegateVtxoContract),
479 Boarding(BoardingContract),
480}
481
482#[derive(Clone, Debug)]
483enum RestoreDiscoveryTarget {
484 Offchain(ArkAddress),
485 Boarding(Address),
486}
487
488impl RestoreCandidate {
489 fn discovery_target(
490 &self,
491 secp: &Secp256k1<All>,
492 ctx: &ContractContext,
493 ) -> Result<RestoreDiscoveryTarget, Error> {
494 match self {
495 Self::DefaultVtxo(contract) => Ok(RestoreDiscoveryTarget::Offchain(
496 Vtxo::new_default(
497 secp,
498 contract.server,
499 contract.owner,
500 contract.exit_delay,
501 ctx.network(),
502 )?
503 .to_ark_address(),
504 )),
505 Self::DelegateVtxo(contract) => Ok(RestoreDiscoveryTarget::Offchain(
506 Vtxo::new_with_delegator(
507 secp,
508 contract.server,
509 contract.owner,
510 contract.delegator,
511 contract.exit_delay,
512 ctx.network(),
513 )?
514 .to_ark_address(),
515 )),
516 Self::Boarding(contract) => Ok(RestoreDiscoveryTarget::Boarding(
517 contract.boarding_output(ctx)?.address().clone(),
518 )),
519 }
520 }
521
522 fn script_pubkey(
523 &self,
524 secp: &Secp256k1<All>,
525 ctx: &ContractContext,
526 ) -> Result<ScriptBuf, Error> {
527 match self {
528 Self::DefaultVtxo(contract) => Ok(Vtxo::new_default(
529 secp,
530 contract.server,
531 contract.owner,
532 contract.exit_delay,
533 ctx.network(),
534 )?
535 .script_pubkey()),
536 Self::DelegateVtxo(contract) => Ok(Vtxo::new_with_delegator(
537 secp,
538 contract.server,
539 contract.owner,
540 contract.delegator,
541 contract.exit_delay,
542 ctx.network(),
543 )?
544 .script_pubkey()),
545 Self::Boarding(contract) => Ok(contract.boarding_output(ctx)?.script_pubkey()),
546 }
547 }
548
549 fn contract_type(&self) -> ContractType {
550 match self {
551 Self::DefaultVtxo(_) => ContractType::default_vtxo(),
552 Self::DelegateVtxo(_) => ContractType::delegate_vtxo(),
553 Self::Boarding(_) => ContractType::boarding(),
554 }
555 }
556}
557
558#[derive(Clone, Copy, Debug)]
559pub struct TxStatus {
560 pub confirmed_at: Option<i64>,
561}
562
563#[derive(Clone, Copy, Debug)]
564pub struct SpendStatus {
565 pub spend_txid: Option<Txid>,
566}
567
568pub struct AddressVtxos {
569 pub unspent: Vec<VirtualTxOutPoint>,
570 pub spent: Vec<VirtualTxOutPoint>,
571}
572
573#[derive(Clone, Debug, Default)]
574pub struct OffChainBalance {
575 pre_confirmed: Amount,
576 confirmed: Amount,
577 recoverable: Amount,
578 pending_recovery: Amount,
582 asset_balances: HashMap<AssetId, u64>,
583}
584
585impl OffChainBalance {
586 pub fn pre_confirmed(&self) -> Amount {
587 self.pre_confirmed
588 }
589
590 pub fn confirmed(&self) -> Amount {
591 self.confirmed
592 }
593
594 pub fn recoverable(&self) -> Amount {
596 self.recoverable
597 }
598
599 pub fn pending_recovery(&self) -> Amount {
602 self.pending_recovery
603 }
604
605 pub fn total(&self) -> Amount {
606 self.pre_confirmed + self.confirmed + self.recoverable + self.pending_recovery
607 }
608
609 pub fn asset_balances(&self) -> &HashMap<AssetId, u64> {
611 &self.asset_balances
612 }
613}
614
615pub trait Blockchain {
616 fn find_outpoints(
617 &self,
618 address: &Address,
619 ) -> impl Future<Output = Result<Vec<ExplorerUtxo>, Error>> + Send;
620
621 fn find_tx(
622 &self,
623 txid: &Txid,
624 ) -> impl Future<Output = Result<Option<Transaction>, Error>> + Send;
625
626 fn get_tx_status(&self, txid: &Txid) -> impl Future<Output = Result<TxStatus, Error>> + Send;
627
628 fn get_output_status(
629 &self,
630 txid: &Txid,
631 vout: u32,
632 ) -> impl Future<Output = Result<SpendStatus, Error>> + Send;
633
634 fn broadcast(&self, tx: &Transaction) -> impl Future<Output = Result<(), Error>> + Send;
635
636 fn get_fee_rate(&self) -> impl Future<Output = Result<f64, Error>> + Send;
637
638 fn broadcast_package(
639 &self,
640 txs: &[&Transaction],
641 ) -> impl Future<Output = Result<(), Error>> + Send;
642}
643
644impl<B, W, S> OfflineClient<B, W, S>
645where
646 B: Blockchain,
647 W: OnchainWallet,
648 S: SwapStorage + 'static,
649{
650 pub fn with_key_provider(
652 config: OfflineClientConfig,
653 key_provider: Arc<dyn KeyProvider>,
654 blockchain: Arc<B>,
655 wallet: Arc<W>,
656 swap_storage: Arc<S>,
657 ) -> Self {
658 Self::with_key_provider_parts(config, key_provider, None, blockchain, wallet, swap_storage)
659 }
660
661 pub fn with_discoverable_key_provider<P>(
663 config: OfflineClientConfig,
664 key_provider: Arc<P>,
665 blockchain: Arc<B>,
666 wallet: Arc<W>,
667 swap_storage: Arc<S>,
668 ) -> Self
669 where
670 P: DiscoverableKeyProvider + 'static,
671 {
672 let core_key_provider: Arc<dyn KeyProvider> = key_provider.clone();
673 let discoverable_key_provider: Arc<dyn DiscoverableKeyProvider> = key_provider;
674 Self::with_key_provider_parts(
675 config,
676 core_key_provider,
677 Some(discoverable_key_provider),
678 blockchain,
679 wallet,
680 swap_storage,
681 )
682 }
683
684 fn with_key_provider_parts(
685 config: OfflineClientConfig,
686 key_provider: Arc<dyn KeyProvider>,
687 discoverable_key_provider: Option<Arc<dyn DiscoverableKeyProvider>>,
688 blockchain: Arc<B>,
689 wallet: Arc<W>,
690 swap_storage: Arc<S>,
691 ) -> Self {
692 let secp = Secp256k1::new();
693 let network_client = ark_grpc::Client::new(config.ark_server_url);
694
695 let mut seen = HashSet::new();
698 let mut historical_delegator_pks: Vec<_> = config
699 .historical_delegator_pks
700 .into_iter()
701 .filter(|pk| seen.insert(*pk))
702 .collect();
703
704 if let Some(pk) = config.delegator_pk {
705 historical_delegator_pks.retain(|k| *k != pk);
706 historical_delegator_pks.insert(0, pk);
707 }
708
709 let boltz_referral_id = match config.boltz_referral_id {
710 BoltzReferralId::Default => Some(DEFAULT_BOLTZ_REFERRAL_ID.to_string()),
711 BoltzReferralId::Disabled => None,
712 BoltzReferralId::Custom(referral_id) => Some(referral_id),
713 };
714 Self {
715 network_client,
716 key_provider,
717 discoverable_key_provider,
718 blockchain,
719 secp,
720 wallet,
721 swap_storage,
722 boltz_url: config.boltz_url.trim_end_matches('/').to_string(),
723 boltz_referral_id,
724 timeout: config.timeout,
725 server_info_ttl: config.server_info_ttl,
726 contract_store: Arc::new(Mutex::new(None)),
727 delegator_pk: config.delegator_pk,
728 historical_delegator_pks,
729 }
730 }
731
732 pub fn with_keypair(
734 config: OfflineClientConfig,
735 kp: Keypair,
736 blockchain: Arc<B>,
737 wallet: Arc<W>,
738 swap_storage: Arc<S>,
739 ) -> Self {
740 let key_provider = Arc::new(StaticKeyProvider::new(kp));
741 Self::with_key_provider(config, key_provider, blockchain, wallet, swap_storage)
742 }
743
744 pub fn with_bip32(
746 config: OfflineClientConfig,
747 xpriv: Xpriv,
748 path: Option<DerivationPath>,
749 blockchain: Arc<B>,
750 wallet: Arc<W>,
751 swap_storage: Arc<S>,
752 ) -> Self {
753 let path = path.unwrap_or(
754 DerivationPath::from_str(DEFAULT_DERIVATION_PATH).expect("valid derivation path"),
755 );
756 let key_provider = Arc::new(Bip32KeyProvider::new(xpriv, path));
757 Self::with_discoverable_key_provider(config, key_provider, blockchain, wallet, swap_storage)
758 }
759
760 pub fn with_contract_store(self, store: Box<dyn ContractStore>) -> Self {
764 let mut contract_store = self
765 .contract_store
766 .lock()
767 .expect("contract store lock should not be poisoned");
768 *contract_store = Some(store);
769 drop(contract_store);
770 self
771 }
772
773 pub fn delegator_pk(&self) -> Option<XOnlyPublicKey> {
775 self.delegator_pk
776 }
777
778 pub fn boltz_referral_id(&self) -> Option<&str> {
780 self.boltz_referral_id.as_deref()
781 }
782
783 fn contract_manager(&self, network: Network) -> Result<ContractManager, Error> {
784 let store = self
785 .contract_store
786 .lock()
787 .map_err(|_| Error::ad_hoc("contract store lock poisoned"))?
788 .take()
789 .unwrap_or_else(|| Box::new(MemoryContractStore::new()));
790 Ok(ContractManager::new(network, store))
791 }
792
793 pub async fn connect(mut self) -> Result<Client<B, W, S>, Error> {
799 timeout_op(self.timeout, self.network_client.connect())
800 .await
801 .context("Failed to connect to Ark server")??;
802
803 self.finish_connect().await
804 }
805
806 pub async fn connect_with_retries(
814 mut self,
815 max_retries: usize,
816 ) -> Result<Client<B, W, S>, Error> {
817 let mut n_retries = 0;
818 while n_retries < max_retries {
819 let res = timeout_op(self.timeout, self.network_client.connect())
820 .await
821 .context("Failed to connect to Ark server")?;
822
823 match res {
824 Ok(()) => break,
825 Err(error) => {
826 tracing::warn!(?error, "Failed to connect to Ark server, retrying");
827
828 sleep(Duration::from_secs(2)).await;
829
830 n_retries += 1;
831
832 continue;
833 }
834 };
835 }
836
837 self.finish_connect().await
838 }
839
840 async fn finish_connect(mut self) -> Result<Client<B, W, S>, Error> {
841 let server_info = timeout_op(self.timeout, self.network_client.get_info())
842 .await
843 .context("Failed to get Ark server info")??;
844
845 tracing::debug!(ark_server_url = ?self.network_client, "Connected to Ark server");
846
847 let fee_estimator = build_fee_estimator(&server_info)?;
848 let mut contract_manager = self.contract_manager(server_info.network)?;
849 contract_manager.register_builtins()?;
850 let state = Arc::new(RwLock::new(ServerState {
851 server_info: server_info.clone(),
852 fee_estimator,
853 server_info_refreshed_at: Instant::now(),
854 contract_manager: Mutex::new(contract_manager),
855 }));
856 let hook_state = state.clone();
857 self.network_client
858 .set_info_refresh_hook(move |server_info| {
859 update_server_state(&hook_state, server_info)
860 .map_err(|err| Box::new(err) as Box<dyn std::error::Error + Send + Sync>)
861 });
862
863 let client = Client {
864 inner: self,
865 state,
866 server_info_refresh_lock: Arc::new(tokio::sync::Mutex::new(())),
867 };
868
869 client.hydrate_persisted_contract_keys()?;
870
871 if let Err(error) = client.persist_baseline_contracts(&server_info) {
875 tracing::warn!(?error, "Failed to persist baseline contracts at connect");
876 }
877
878 Ok(client)
879 }
880}
881
882fn contract_info_from_stored(
883 contract: StoredContract,
884 server_info: &server::Info,
885 now_unix_secs: i64,
886) -> Result<ContractInfo, Error> {
887 let ctx = ContractContext::new(server_info.network);
888 let (address, address_kind, server_pk) = match &contract.contract_type {
889 contract_type if *contract_type == ContractType::default_vtxo() => {
890 let data: DefaultVtxoContract =
891 serde_json::from_value(contract.data.clone()).map_err(|e| {
892 Error::ad_hoc(format!("failed to decode default vtxo contract: {e}"))
893 })?;
894 (
895 Some(data.vtxo(&ctx)?.to_ark_address().to_string()),
896 Some(ContractAddressKind::Ark),
897 Some(data.server),
898 )
899 }
900 contract_type if *contract_type == ContractType::delegate_vtxo() => {
901 let data: DelegateVtxoContract = serde_json::from_value(contract.data.clone())
902 .map_err(|e| {
903 Error::ad_hoc(format!("failed to decode delegate vtxo contract: {e}"))
904 })?;
905 (
906 Some(data.vtxo(&ctx)?.to_ark_address().to_string()),
907 Some(ContractAddressKind::Ark),
908 Some(data.server),
909 )
910 }
911 contract_type if *contract_type == ContractType::boarding() => {
912 let data: BoardingContract = serde_json::from_value(contract.data.clone())
913 .map_err(|e| Error::ad_hoc(format!("failed to decode boarding contract: {e}")))?;
914 (
915 Some(data.boarding_output(&ctx)?.address().to_string()),
916 Some(ContractAddressKind::Bitcoin),
917 Some(data.server),
918 )
919 }
920 contract_type if *contract_type == ContractType::vhtlc() => {
921 let data: VhtlcContract = serde_json::from_value(contract.data.clone())
922 .map_err(|e| Error::ad_hoc(format!("failed to decode vhtlc contract: {e}")))?;
923 let address =
924 ark_core::vhtlc::VhtlcScript::new(data.options.clone(), server_info.network)
925 .map_err(|e| Error::ad_hoc(format!("failed to build vhtlc address: {e}")))?
926 .address()
927 .to_string();
928 (
929 Some(address),
930 Some(ContractAddressKind::Ark),
931 Some(data.options.server),
932 )
933 }
934 _ => {
935 let address = Address::from_script(&contract.script_pubkey, server_info.network)
936 .ok()
937 .map(|address| address.to_string());
938 let address_kind = address.as_ref().map(|_| ContractAddressKind::Bitcoin);
939 (address, address_kind, None)
940 }
941 };
942 let signer_status =
943 server_pk.map(|server_pk| server_info.signer_status_at(server_pk, now_unix_secs));
944
945 Ok(ContractInfo {
946 contract,
947 address,
948 address_kind,
949 server_pk,
950 signer_status,
951 })
952}
953
954fn build_fee_estimator(server_info: &server::Info) -> Result<ark_fees::Estimator, Error> {
955 let fee_estimator_config = server_info
956 .fees
957 .clone()
958 .map(|fees| ark_fees::Config {
959 intent_offchain_input_program: fees.intent_fee.offchain_input.unwrap_or_default(),
960 intent_onchain_input_program: fees.intent_fee.onchain_input.unwrap_or_default(),
961 intent_offchain_output_program: fees.intent_fee.offchain_output.unwrap_or_default(),
962 intent_onchain_output_program: fees.intent_fee.onchain_output.unwrap_or_default(),
963 })
964 .unwrap_or_default();
965
966 ark_fees::Estimator::new(fee_estimator_config).map_err(Error::ark_server)
967}
968
969fn update_server_state(
970 state: &Arc<RwLock<ServerState>>,
971 server_info: server::Info,
972) -> Result<(), Error> {
973 let fee_estimator = build_fee_estimator(&server_info)?;
974 let mut state = state
975 .write()
976 .map_err(|_| Error::ad_hoc("client server state lock poisoned"))?;
977 state.server_info = server_info;
978 state.fee_estimator = fee_estimator;
979 state.server_info_refreshed_at = Instant::now();
980 Ok(())
981}
982
983impl<B, W, S> Client<B, W, S>
984where
985 B: Blockchain,
986 W: OnchainWallet,
987 S: SwapStorage + 'static,
988{
989 pub async fn server_info(&self) -> Result<server::Info, Error> {
991 if let Some(server_info) = self.cached_server_info_if_fresh()? {
993 return Ok(server_info);
994 }
995
996 let _guard = self.server_info_refresh_lock.lock().await;
997 if let Some(server_info) = self.cached_server_info_if_fresh()? {
999 return Ok(server_info);
1000 }
1001
1002 self.refresh_server_info_unlocked().await
1003 }
1004
1005 fn cached_server_info_if_fresh(&self) -> Result<Option<server::Info>, Error> {
1006 self.state
1007 .read()
1008 .map(|state| {
1009 (state.server_info_refreshed_at.elapsed() < self.inner.server_info_ttl)
1010 .then(|| state.server_info.clone())
1011 })
1012 .map_err(|_| Error::ad_hoc("client server state lock poisoned"))
1013 }
1014
1015 fn with_server_state<T>(&self, f: impl FnOnce(&ServerState) -> T) -> Result<T, Error> {
1016 self.state
1017 .read()
1018 .map(|state| f(&state))
1019 .map_err(|_| Error::ad_hoc("client server state lock poisoned"))
1020 }
1021
1022 fn eval_onchain_output_fee(&self, output: ark_fees::Output) -> Result<Amount, Error> {
1023 self.with_server_state(|state| state.fee_estimator.eval_onchain_output(output))?
1024 .map(|fee| Amount::from_sat(fee.to_satoshis()))
1025 .map_err(Error::ad_hoc)
1026 }
1027
1028 pub async fn refresh_server_info(&self) -> Result<server::Info, Error> {
1037 let _guard = self.server_info_refresh_lock.lock().await;
1038 self.refresh_server_info_unlocked().await
1039 }
1040
1041 async fn refresh_server_info_unlocked(&self) -> Result<server::Info, Error> {
1046 let server_info = timeout_op(self.inner.timeout, self.network_client().get_info())
1047 .await
1048 .context("Failed to refresh Ark server info")??;
1049
1050 update_server_state(&self.state, server_info.clone())?;
1051
1052 Ok(server_info)
1053 }
1054
1055 pub fn delegator_pk(&self) -> Option<XOnlyPublicKey> {
1057 self.inner.delegator_pk()
1058 }
1059
1060 pub fn boltz_referral_id(&self) -> Option<&str> {
1062 self.inner.boltz_referral_id()
1063 }
1064
1065 pub async fn list_contracts(&self) -> Result<Vec<ContractInfo>, Error> {
1071 let server_info = self.server_info().await?;
1072 let now = unix_now()?;
1073 let contracts = {
1074 let state = self
1075 .state
1076 .read()
1077 .map_err(|_| Error::ad_hoc("client server state lock poisoned"))?;
1078 let contracts = state
1079 .contract_manager
1080 .lock()
1081 .map_err(|_| Error::ad_hoc("contract manager lock poisoned"))?
1082 .list()?;
1083 contracts
1084 };
1085
1086 contracts
1087 .into_iter()
1088 .map(|contract| contract_info_from_stored(contract, &server_info, now))
1089 .collect()
1090 }
1091
1092 pub async fn get_offchain_address(&self) -> Result<(ArkAddress, Vtxo), Error> {
1100 let server_info = self.server_info().await?;
1101 self.get_offchain_address_with_server_info(&server_info)
1102 }
1103
1104 pub(crate) fn get_offchain_address_with_server_info(
1105 &self,
1106 server_info: &server::Info,
1107 ) -> Result<(ArkAddress, Vtxo), Error> {
1108 let server_signer = server_info.signer_pk.into();
1109 let owner = self
1110 .next_keypair(KeypairIndex::LastUnused)?
1111 .public_key()
1112 .into();
1113
1114 self.persist_offchain_vtxo_contract(server_info, server_signer, owner)
1115 }
1116
1117 pub async fn get_offchain_addresses(&self) -> Result<Vec<(ArkAddress, Vtxo)>, Error> {
1124 let server_info = self.server_info().await?;
1125 self.get_offchain_addresses_with_server_info(&server_info)
1126 }
1127
1128 fn persist_baseline_contracts(&self, server_info: &server::Info) -> Result<(), Error> {
1129 self.persist_baseline_offchain_contracts(server_info)?;
1130 self.persist_watch_boarding_outputs(server_info)?;
1131 Ok(())
1132 }
1133
1134 fn hydrate_persisted_contract_keys(&self) -> Result<(), Error> {
1135 let state = self
1136 .state
1137 .read()
1138 .map_err(|_| Error::ad_hoc("client server state lock poisoned"))?;
1139 let contracts = state
1140 .contract_manager
1141 .lock()
1142 .map_err(|_| Error::ad_hoc("contract manager lock poisoned"))?
1143 .list()?;
1144
1145 let mut indices: Vec<u32> = contracts
1146 .into_iter()
1147 .filter_map(|contract| contract.key_index)
1148 .collect();
1149 indices.sort_unstable();
1150 indices.dedup();
1151
1152 let Some(key_provider) = self.inner.discoverable_key_provider.as_ref() else {
1153 return Ok(());
1154 };
1155 for index in indices {
1156 key_provider.cache_keypair_at_index(index)?;
1157 }
1158
1159 Ok(())
1160 }
1161
1162 fn persist_baseline_offchain_contracts(
1163 &self,
1164 server_info: &server::Info,
1165 ) -> Result<Vec<(ArkAddress, Vtxo)>, Error> {
1166 let owner = self
1167 .next_keypair(KeypairIndex::LastUnused)?
1168 .x_only_public_key()
1169 .0;
1170 let candidate_delays = ark_core::candidate_exit_delays(
1171 server_info.unilateral_exit_delay,
1172 server_info.network,
1173 )?;
1174
1175 let mut results = Vec::new();
1176 for server_signer in server_info.all_server_keys() {
1177 for exit_delay in &candidate_delays {
1178 results.push(self.persist_default_vtxo_contract(
1179 server_info.network,
1180 server_signer,
1181 owner,
1182 *exit_delay,
1183 )?);
1184
1185 let mut seen = HashSet::new();
1186 for dpk in &self.inner.historical_delegator_pks {
1187 if !seen.insert(dpk) {
1188 continue;
1189 }
1190 results.push(self.persist_delegate_vtxo_contract(
1191 server_info.network,
1192 server_signer,
1193 owner,
1194 *dpk,
1195 *exit_delay,
1196 )?);
1197 }
1198 }
1199 }
1200 Ok(results)
1201 }
1202
1203 pub(crate) fn get_offchain_addresses_with_server_info(
1204 &self,
1205 server_info: &server::Info,
1206 ) -> Result<Vec<(ArkAddress, Vtxo)>, Error> {
1207 let pks = self.inner.key_provider.get_cached_pks()?;
1208
1209 let all_server_keys: Vec<XOnlyPublicKey> = server_info.all_server_keys().collect();
1212 let candidate_delays = ark_core::candidate_exit_delays(
1218 server_info.unilateral_exit_delay,
1219 server_info.network,
1220 )?;
1221
1222 let mut results = Vec::new();
1223
1224 for owner_pk in &pks {
1225 for server_signer in &all_server_keys {
1226 for exit_delay in &candidate_delays {
1227 results.push(self.persist_default_vtxo_contract(
1228 server_info.network,
1229 *server_signer,
1230 *owner_pk,
1231 *exit_delay,
1232 )?);
1233
1234 let mut seen = HashSet::new();
1236 for dpk in &self.inner.historical_delegator_pks {
1237 if !seen.insert(dpk) {
1238 continue;
1239 }
1240 results.push(self.persist_delegate_vtxo_contract(
1241 server_info.network,
1242 *server_signer,
1243 *owner_pk,
1244 *dpk,
1245 *exit_delay,
1246 )?);
1247 }
1248 }
1249 }
1250 }
1251
1252 Ok(results)
1253 }
1254
1255 fn persist_offchain_vtxo_contract(
1256 &self,
1257 server_info: &server::Info,
1258 server_signer: XOnlyPublicKey,
1259 owner: XOnlyPublicKey,
1260 ) -> Result<(ArkAddress, Vtxo), Error> {
1261 match self.inner.delegator_pk {
1262 Some(delegator) => self.persist_delegate_vtxo_contract(
1263 server_info.network,
1264 server_signer,
1265 owner,
1266 delegator,
1267 server_info.unilateral_exit_delay,
1268 ),
1269 None => self.persist_default_vtxo_contract(
1270 server_info.network,
1271 server_signer,
1272 owner,
1273 server_info.unilateral_exit_delay,
1274 ),
1275 }
1276 }
1277
1278 fn persist_default_vtxo_contract(
1279 &self,
1280 network: Network,
1281 server: XOnlyPublicKey,
1282 owner: XOnlyPublicKey,
1283 exit_delay: bitcoin::Sequence,
1284 ) -> Result<(ArkAddress, Vtxo), Error> {
1285 let key_index = self.derivation_index_for_pk(&owner);
1286 let contract = DefaultVtxoContract {
1287 server,
1288 owner,
1289 exit_delay,
1290 };
1291 let state = self
1292 .state
1293 .read()
1294 .map_err(|_| Error::ad_hoc("client server state lock poisoned"))?;
1295 let mut manager = state
1296 .contract_manager
1297 .lock()
1298 .map_err(|_| Error::ad_hoc("contract manager lock poisoned"))?;
1299 manager.insert_or_get(contract.clone(), ContractState::Active, key_index)?;
1300 let ctx = ContractContext::new(network);
1301 let vtxo = contract.vtxo(&ctx)?;
1305 Ok((vtxo.to_ark_address(), vtxo))
1306 }
1307
1308 fn persist_delegate_vtxo_contract(
1309 &self,
1310 network: Network,
1311 server: XOnlyPublicKey,
1312 owner: XOnlyPublicKey,
1313 delegator: XOnlyPublicKey,
1314 exit_delay: bitcoin::Sequence,
1315 ) -> Result<(ArkAddress, Vtxo), Error> {
1316 let key_index = self.derivation_index_for_pk(&owner);
1317 let contract = DelegateVtxoContract {
1318 server,
1319 owner,
1320 delegator,
1321 exit_delay,
1322 };
1323 let state = self
1324 .state
1325 .read()
1326 .map_err(|_| Error::ad_hoc("client server state lock poisoned"))?;
1327 let mut manager = state
1328 .contract_manager
1329 .lock()
1330 .map_err(|_| Error::ad_hoc("contract manager lock poisoned"))?;
1331 let stored = manager.insert_or_get(contract, ContractState::Active, key_index)?;
1332 let contract = manager
1333 .get_typed::<DelegateVtxoContract>(&stored.script_pubkey)?
1334 .ok_or_else(|| Error::ad_hoc("missing delegate vtxo contract"))?;
1335 let ctx = ContractContext::new(network);
1336 let vtxo = contract.vtxo(&ctx)?;
1337 Ok((vtxo.to_ark_address(), vtxo))
1338 }
1339
1340 pub async fn restore_contracts(&self, gap_limit: u32) -> Result<ContractRestoreReport, Error> {
1353 if gap_limit == 0 {
1354 return Err(Error::ad_hoc("restore gap limit must be greater than zero"));
1355 }
1356
1357 let Some(key_provider) = self.inner.discoverable_key_provider.as_ref() else {
1358 tracing::debug!("Key provider does not support discovery, skipping");
1359 return Ok(ContractRestoreReport {
1360 gap_limit,
1361 ..Default::default()
1362 });
1363 };
1364
1365 let server_info = &self.server_info().await?;
1366 let ctx = ContractContext::new(server_info.network);
1367 let all_server_keys: Vec<XOnlyPublicKey> = server_info.all_server_keys().collect();
1368 let offchain_exit_delays = ark_core::candidate_exit_delays(
1369 server_info.unilateral_exit_delay,
1370 server_info.network,
1371 )?;
1372 let boarding_exit_delays =
1373 ark_core::candidate_exit_delays(server_info.boarding_exit_delay, server_info.network)?;
1374
1375 let mut start_index = 0u32;
1376 let mut report = ContractRestoreReport {
1377 gap_limit,
1378 ..Default::default()
1379 };
1380
1381 tracing::info!(gap_limit, "Starting contract restore");
1382
1383 loop {
1384 let batch = self.restore_candidate_batch(
1385 start_index,
1386 gap_limit,
1387 &all_server_keys,
1388 &offchain_exit_delays,
1389 &boarding_exit_delays,
1390 )?;
1391
1392 if batch.is_empty() {
1393 break;
1394 }
1395
1396 report.scanned_from.get_or_insert(start_index);
1397 let scanned_to_exclusive = start_index
1398 .checked_add(batch.len() as u32)
1399 .ok_or_else(|| Error::ad_hoc("Key discovery index overflow"))?;
1400 report.scanned_to_exclusive = Some(scanned_to_exclusive);
1401 report.scanned_keys += batch.len() as u32;
1402
1403 let mut offchain_addresses = Vec::new();
1404 for (_, _, candidates) in &batch {
1405 for candidate in candidates {
1406 if let RestoreDiscoveryTarget::Offchain(address) =
1407 candidate.discovery_target(self.secp(), &ctx)?
1408 {
1409 offchain_addresses.push(address);
1410 }
1411 }
1412 }
1413 let vtxo_list = self
1414 .list_vtxos_for_addresses(offchain_addresses.into_iter())
1415 .await?;
1416 let mut offchain_vtxos_by_script =
1417 HashMap::<ScriptBuf, Vec<ContractRestoreVtxo>>::new();
1418 for vtxo in vtxo_list.all() {
1419 offchain_vtxos_by_script
1420 .entry(vtxo.script.clone())
1421 .or_default()
1422 .push(ContractRestoreVtxo {
1423 outpoint: vtxo.outpoint,
1424 amount: vtxo.amount,
1425 is_spent: vtxo.is_spent,
1426 is_swept: vtxo.is_swept,
1427 is_unrolled: vtxo.is_unrolled,
1428 });
1429 }
1430
1431 let mut found_any = false;
1432 for (index, kp, candidates) in batch {
1433 let mut found_for_key = false;
1434
1435 for candidate in candidates {
1436 let script = candidate.script_pubkey(self.secp(), &ctx)?;
1437 let contract_type = candidate.contract_type();
1438 let target = candidate.discovery_target(self.secp(), &ctx)?;
1439 let discovery = match target {
1440 RestoreDiscoveryTarget::Offchain(_) => offchain_vtxos_by_script
1441 .get(&script)
1442 .filter(|vtxos| !vtxos.is_empty())
1443 .cloned()
1444 .map(|vtxos| ContractRestoreDiscovery::Offchain { vtxos }),
1445 RestoreDiscoveryTarget::Boarding(address) => {
1446 let outpoints = self
1447 .blockchain()
1448 .find_outpoints(&address)
1449 .await?
1450 .into_iter()
1451 .filter(|utxo| !utxo.is_spent)
1452 .map(|utxo| ContractRestoreOutpoint {
1453 outpoint: utxo.outpoint,
1454 amount: utxo.amount,
1455 confirmation_blocktime: utxo.confirmation_blocktime,
1456 confirmations: utxo.confirmations,
1457 })
1458 .collect::<Vec<_>>();
1459 (!outpoints.is_empty())
1460 .then_some(ContractRestoreDiscovery::Boarding { outpoints })
1461 }
1462 };
1463
1464 let Some(discovery) = discovery else {
1465 continue;
1466 };
1467
1468 let inserted = self.persist_restore_candidate(candidate, index)?;
1469 let status = if inserted {
1470 report.inserted_contracts += 1;
1471 ContractRestoreEntryStatus::Inserted
1472 } else {
1473 report.known_contracts += 1;
1474 ContractRestoreEntryStatus::Known
1475 };
1476 match &discovery {
1477 ContractRestoreDiscovery::Offchain { .. } => report.offchain_contracts += 1,
1478 ContractRestoreDiscovery::Boarding { .. } => report.boarding_contracts += 1,
1479 }
1480 report.entries.push(ContractRestoreEntry {
1481 key_index: index,
1482 contract_type,
1483 script_pubkey: script,
1484 status,
1485 discovery,
1486 });
1487 found_for_key = true;
1488 }
1489
1490 if found_for_key {
1491 key_provider.cache_discovered_keypair(index, kp)?;
1492 report.discovered_key_indexes.push(index);
1493 report.last_used_key_index = Some(
1494 report
1495 .last_used_key_index
1496 .map_or(index, |last| last.max(index)),
1497 );
1498 report.next_key_index = report
1499 .last_used_key_index
1500 .and_then(|last| last.checked_add(1));
1501 found_any = true;
1502 }
1503 }
1504
1505 if !found_any {
1506 break;
1507 }
1508
1509 start_index = start_index
1510 .checked_add(gap_limit)
1511 .ok_or_else(|| Error::ad_hoc("Key discovery index overflow"))?;
1512 }
1513
1514 tracing::info!(?report, "Contract restore completed");
1515
1516 Ok(report)
1517 }
1518
1519 fn restore_candidate_batch(
1520 &self,
1521 start_index: u32,
1522 gap_limit: u32,
1523 server_keys: &[XOnlyPublicKey],
1524 offchain_exit_delays: &[bitcoin::Sequence],
1525 boarding_exit_delays: &[bitcoin::Sequence],
1526 ) -> Result<Vec<(u32, Keypair, Vec<RestoreCandidate>)>, Error> {
1527 let mut batch = Vec::with_capacity(gap_limit as usize);
1528
1529 for i in 0..gap_limit {
1530 let index = start_index
1531 .checked_add(i)
1532 .ok_or_else(|| Error::ad_hoc("Key discovery index overflow"))?;
1533 let Some(key_provider) = self.inner.discoverable_key_provider.as_ref() else {
1534 break;
1535 };
1536 let Some(kp) = key_provider.derive_at_discovery_index(index)? else {
1537 break;
1538 };
1539 let owner = kp.x_only_public_key().0;
1540 let mut candidates = Vec::new();
1541
1542 for server in server_keys {
1543 for exit_delay in offchain_exit_delays {
1544 candidates.push(RestoreCandidate::DefaultVtxo(DefaultVtxoContract {
1545 server: *server,
1546 owner,
1547 exit_delay: *exit_delay,
1548 }));
1549
1550 let mut seen_delegators = HashSet::new();
1551 for delegator in &self.inner.historical_delegator_pks {
1552 if !seen_delegators.insert(delegator) {
1553 continue;
1554 }
1555 candidates.push(RestoreCandidate::DelegateVtxo(DelegateVtxoContract {
1556 server: *server,
1557 owner,
1558 delegator: *delegator,
1559 exit_delay: *exit_delay,
1560 }));
1561 }
1562 }
1563
1564 for exit_delay in boarding_exit_delays {
1565 candidates.push(RestoreCandidate::Boarding(BoardingContract {
1566 server: *server,
1567 owner,
1568 exit_delay: *exit_delay,
1569 }));
1570 }
1571 }
1572
1573 batch.push((index, kp, candidates));
1574 }
1575
1576 Ok(batch)
1577 }
1578
1579 fn persist_restore_candidate(
1580 &self,
1581 candidate: RestoreCandidate,
1582 key_index: u32,
1583 ) -> Result<bool, Error> {
1584 let state = self
1585 .state
1586 .read()
1587 .map_err(|_| Error::ad_hoc("client server state lock poisoned"))?;
1588 let mut manager = state
1589 .contract_manager
1590 .lock()
1591 .map_err(|_| Error::ad_hoc("contract manager lock poisoned"))?;
1592 let ctx = ContractContext::new(state.server_info.network);
1593 let script = candidate.script_pubkey(self.secp(), &ctx)?;
1594 let existed = manager.get(&script)?.is_some();
1595
1596 match candidate {
1597 RestoreCandidate::DefaultVtxo(contract) => {
1598 manager.insert_or_get(contract, ContractState::Active, Some(key_index))?;
1599 }
1600 RestoreCandidate::DelegateVtxo(contract) => {
1601 manager.insert_or_get(contract, ContractState::Active, Some(key_index))?;
1602 }
1603 RestoreCandidate::Boarding(contract) => {
1604 manager.insert_or_get(contract, ContractState::Active, Some(key_index))?;
1605 }
1606 }
1607
1608 Ok(!existed)
1609 }
1610
1611 pub async fn get_boarding_address(&self) -> Result<Address, Error> {
1613 let server_info = &self.server_info().await?;
1614 let owner = self
1615 .next_keypair(KeypairIndex::LastUnused)?
1616 .x_only_public_key()
1617 .0;
1618
1619 let contract = BoardingContract {
1620 server: server_info.signer_pk.into(),
1621 owner,
1622 exit_delay: server_info.boarding_exit_delay,
1623 };
1624 let key_index = self.derivation_index_for_pk(&owner);
1625 let state = self
1626 .state
1627 .read()
1628 .map_err(|_| Error::ad_hoc("client server state lock poisoned"))?;
1629 let stored = state
1630 .contract_manager
1631 .lock()
1632 .map_err(|_| Error::ad_hoc("contract manager lock poisoned"))?
1633 .insert_or_get(contract, ContractState::Active, key_index)?;
1634
1635 Address::from_script(&stored.script_pubkey, server_info.network)
1636 .map_err(|e| Error::ad_hoc(format!("invalid boarding contract script: {e}")))
1637 }
1638
1639 pub fn get_onchain_address(&self) -> Result<Address, Error> {
1640 self.inner.wallet.get_onchain_address()
1641 }
1642
1643 pub async fn get_boarding_addresses(&self) -> Result<Vec<Address>, Error> {
1644 let server_info = &self.server_info().await?;
1645
1646 let outputs = self.persist_watch_boarding_outputs(server_info)?;
1655
1656 let mut seen = HashSet::new();
1657 let mut addresses = Vec::with_capacity(outputs.len());
1658 for output in &outputs {
1659 let address = output.address().clone();
1660 if seen.insert(address.clone()) {
1661 addresses.push(address);
1662 }
1663 }
1664
1665 Ok(addresses)
1666 }
1667
1668 fn persist_watch_boarding_outputs(
1676 &self,
1677 server_info: &server::Info,
1678 ) -> Result<Vec<AnnotatedBoardingOutput>, Error> {
1679 let candidate_delays =
1680 ark_core::candidate_exit_delays(server_info.boarding_exit_delay, server_info.network)?;
1681 let owner = self
1682 .next_keypair(KeypairIndex::LastUnused)?
1683 .x_only_public_key()
1684 .0;
1685 let key_index = self.derivation_index_for_pk(&owner);
1686 let state = self
1687 .state
1688 .read()
1689 .map_err(|_| Error::ad_hoc("client server state lock poisoned"))?;
1690 let mut manager = state
1691 .contract_manager
1692 .lock()
1693 .map_err(|_| Error::ad_hoc("contract manager lock poisoned"))?;
1694
1695 for server_pk in server_info.all_server_keys() {
1696 for exit_delay in &candidate_delays {
1697 let contract = BoardingContract {
1698 server: server_pk,
1699 owner,
1700 exit_delay: *exit_delay,
1701 };
1702 manager.insert_or_get(contract, ContractState::Active, key_index)?;
1703 }
1704 }
1705
1706 manager.annotated_boarding_outputs_for_exit_delays(&candidate_delays)
1707 }
1708
1709 pub async fn get_virtual_tx_outpoints(
1710 &self,
1711 addresses: impl Iterator<Item = ArkAddress>,
1712 ) -> Result<Vec<VirtualTxOutPoint>, Error> {
1713 let request = GetVtxosRequest::new_for_addresses(addresses);
1714 self.fetch_all_vtxos(request).await
1715 }
1716
1717 pub async fn list_vtxos(&self) -> Result<AnnotatedVtxoList, Error> {
1718 let server_info = self.server_info().await?;
1719 self.list_vtxos_with_server_info(&server_info).await
1720 }
1721
1722 pub(crate) async fn list_vtxos_with_server_info(
1723 &self,
1724 server_info: &server::Info,
1725 ) -> Result<AnnotatedVtxoList, Error> {
1726 let addresses = self.active_offchain_contract_addresses()?;
1727 let virtual_tx_outpoints = self.get_virtual_tx_outpoints(addresses.into_iter()).await?;
1728 let contract_vtxos = self.annotate_vtxos(virtual_tx_outpoints)?;
1729
1730 Ok(AnnotatedVtxoList::new(server_info.dust, contract_vtxos))
1731 }
1732
1733 pub async fn list_vtxos_for_addresses(
1734 &self,
1735 addresses: impl Iterator<Item = ArkAddress>,
1736 ) -> Result<VtxoList, Error> {
1737 let server_info = self.server_info().await?;
1738 self.list_vtxos_for_addresses_with_server_info(&server_info, addresses)
1739 .await
1740 }
1741
1742 pub(crate) async fn list_vtxos_for_addresses_with_server_info(
1743 &self,
1744 server_info: &server::Info,
1745 addresses: impl Iterator<Item = ArkAddress>,
1746 ) -> Result<VtxoList, Error> {
1747 let virtual_tx_outpoints = self
1748 .get_virtual_tx_outpoints(addresses)
1749 .await
1750 .context("failed to get VTXOs for addresses")?;
1751
1752 let vtxo_list = VtxoList::new(server_info.dust, virtual_tx_outpoints);
1753
1754 Ok(vtxo_list)
1755 }
1756
1757 pub async fn list_vtxos_for_outpoints(
1758 &self,
1759 outpoints: Vec<OutPoint>,
1760 ) -> Result<AnnotatedVtxoList, Error> {
1761 let request = GetVtxosRequest::new_for_outpoints(&outpoints);
1762 let virtual_tx_outpoints = self.fetch_all_vtxos(request).await?;
1763 let contract_vtxos = self.annotate_vtxos(virtual_tx_outpoints)?;
1764 Ok(AnnotatedVtxoList::new(
1765 self.server_info().await?.dust,
1766 contract_vtxos,
1767 ))
1768 }
1769
1770 pub async fn get_vtxo_chain(
1771 &self,
1772 out_point: OutPoint,
1773 size: i32,
1774 index: i32,
1775 ) -> Result<Option<VtxoChainResponse>, Error> {
1776 let vtxo_chain = timeout_op(
1777 self.inner.timeout,
1778 self.network_client()
1779 .get_vtxo_chain(Some(out_point), Some((size, index))),
1780 )
1781 .await
1782 .context("Failed to fetch VTXO chain")??;
1783
1784 Ok(Some(vtxo_chain))
1785 }
1786
1787 pub async fn offchain_balance(&self) -> Result<OffChainBalance, Error> {
1788 let vtxo_list = self.list_vtxos().await.context("failed to list VTXOs")?;
1789 let now = unix_now()?;
1790 let server_info = self.server_info().await?;
1791
1792 let spendable_outpoints: HashSet<OutPoint> = vtxo_list
1793 .spendable_offchain_at(&server_info, now)
1794 .map(|entry| entry.vtxo().outpoint)
1795 .collect();
1796
1797 let pre_confirmed = vtxo_list
1798 .pre_confirmed()
1799 .filter(|entry| spendable_outpoints.contains(&entry.vtxo().outpoint))
1800 .fold(Amount::ZERO, |acc, entry| acc + entry.vtxo().amount);
1801
1802 let confirmed = vtxo_list
1803 .confirmed()
1804 .filter(|entry| spendable_outpoints.contains(&entry.vtxo().outpoint))
1805 .fold(Amount::ZERO, |acc, entry| acc + entry.vtxo().amount);
1806
1807 let recoverable = vtxo_list
1808 .recoverable()
1809 .fold(Amount::ZERO, |acc, entry| acc + entry.vtxo().amount);
1810
1811 let pending_recovery = vtxo_list
1812 .pending_recovery_due_to_signer_at(&server_info, now)
1813 .fold(Amount::ZERO, |acc, entry| acc + entry.vtxo().amount);
1814
1815 let mut asset_balances: HashMap<AssetId, u64> = HashMap::new();
1817 for entry in vtxo_list.spendable_offchain_at(&server_info, now) {
1818 for asset in &entry.vtxo().assets {
1819 let total = asset_balances
1820 .get(&asset.asset_id)
1821 .copied()
1822 .unwrap_or(0)
1823 .checked_add(asset.amount)
1824 .ok_or_else(|| Error::ad_hoc("asset balance overflow"))?;
1825 asset_balances.insert(asset.asset_id, total);
1826 }
1827 }
1828
1829 Ok(OffChainBalance {
1830 pre_confirmed,
1831 confirmed,
1832 recoverable,
1833 pending_recovery,
1834 asset_balances,
1835 })
1836 }
1837
1838 pub async fn get_asset(&self, asset_id: AssetId) -> Result<server::AssetInfo, Error> {
1840 timeout_op(
1841 self.inner.timeout,
1842 self.network_client().get_asset(asset_id),
1843 )
1844 .await
1845 .context("Failed to get asset info")?
1846 .map_err(Error::ark_server)
1847 }
1848
1849 pub async fn transaction_history(&self) -> Result<Vec<history::Transaction>, Error> {
1850 let mut boarding_transactions = Vec::new();
1851 let mut boarding_commitment_transactions = Vec::new();
1852
1853 let boarding_addresses = self.get_boarding_addresses().await?;
1854 for boarding_address in boarding_addresses.iter() {
1855 let outpoints = timeout_op(
1856 self.inner.timeout,
1857 self.blockchain().find_outpoints(boarding_address),
1858 )
1859 .await
1860 .context("Failed to find outpoints")??;
1861
1862 for ExplorerUtxo {
1863 outpoint,
1864 amount,
1865 confirmation_blocktime,
1866 ..
1867 } in outpoints.iter()
1868 {
1869 let confirmed_at = confirmation_blocktime.map(|t| t as i64);
1870
1871 boarding_transactions.push(history::Transaction::Boarding {
1872 txid: outpoint.txid,
1873 amount: *amount,
1874 confirmed_at,
1875 });
1876
1877 let status = timeout_op(
1878 self.inner.timeout,
1879 self.blockchain()
1880 .get_output_status(&outpoint.txid, outpoint.vout),
1881 )
1882 .await
1883 .context("Failed to get Tx output status")??;
1884
1885 if let Some(spend_txid) = status.spend_txid {
1886 boarding_commitment_transactions.push(spend_txid);
1887 }
1888 }
1889 }
1890
1891 let vtxo_list = self.list_vtxos().await?;
1892
1893 let spent_outpoints = vtxo_list
1894 .spent()
1895 .map(|entry| entry.vtxo().clone())
1896 .collect::<Vec<_>>();
1897 let unspent_outpoints = vtxo_list
1898 .all_unspent()
1899 .map(|entry| entry.vtxo().clone())
1900 .collect::<Vec<_>>();
1901
1902 let incoming_transactions = generate_incoming_vtxo_transaction_history(
1903 &spent_outpoints,
1904 &unspent_outpoints,
1905 &boarding_commitment_transactions,
1906 )?;
1907
1908 let outgoing_txs =
1909 generate_outgoing_vtxo_transaction_history(&spent_outpoints, &unspent_outpoints)?;
1910
1911 let mut outgoing_transactions = vec![];
1912 for tx in outgoing_txs {
1913 let tx = match tx {
1914 OutgoingTransaction::Complete(tx) => tx,
1915 OutgoingTransaction::Incomplete(incomplete_tx) => {
1916 let first_outpoint = incomplete_tx.first_outpoint();
1917
1918 let request = GetVtxosRequest::new_for_outpoints(&[first_outpoint]);
1919 let vtxos = self.fetch_all_vtxos(request).await?;
1920
1921 match vtxos.first() {
1922 Some(virtual_tx_outpoint) => {
1923 match incomplete_tx.finish(virtual_tx_outpoint) {
1924 Ok(tx) => tx,
1925 Err(e) => {
1926 tracing::warn!(
1927 %first_outpoint,
1928 "Could not finish outgoing TX, skipping: {e}"
1929 );
1930 continue;
1931 }
1932 }
1933 }
1934 None => {
1935 tracing::warn!(
1936 %first_outpoint,
1937 "Could not find virtual TX outpoint for outgoing TX, skipping"
1938 );
1939 continue;
1940 }
1941 }
1942 }
1943 OutgoingTransaction::IncompleteOffboard(incomplete_offboard) => {
1944 let status = timeout_op(
1945 self.inner.timeout,
1946 self.blockchain()
1947 .get_tx_status(&incomplete_offboard.commitment_txid()),
1948 )
1949 .await
1950 .context("failed to get commitment TX status")??;
1951
1952 incomplete_offboard.finish(status.confirmed_at)
1953 }
1954 };
1955
1956 outgoing_transactions.push(tx);
1957 }
1958
1959 let mut txs = [
1960 boarding_transactions,
1961 incoming_transactions,
1962 outgoing_transactions,
1963 ]
1964 .concat();
1965
1966 sort_transactions_by_created_at(&mut txs);
1967
1968 Ok(txs)
1969 }
1970
1971 pub async fn dust(&self) -> Result<Amount, Error> {
1973 Ok(self.server_info().await?.dust)
1974 }
1975
1976 pub fn network_client(&self) -> ark_grpc::Client {
1977 self.inner.network_client.clone()
1978 }
1979
1980 async fn fetch_all_vtxos(
1982 &self,
1983 request: GetVtxosRequest,
1984 ) -> Result<Vec<VirtualTxOutPoint>, Error> {
1985 if request.reference().is_empty() {
1986 return Ok(Vec::new());
1987 }
1988
1989 let mut all_vtxos = Vec::new();
1990 let mut cursor = 0;
1991 const PAGE_SIZE: i32 = 100;
1992
1993 loop {
1994 let paged_request = request.clone().with_page(PAGE_SIZE, cursor);
1995 let response = timeout_op(
1996 self.inner.timeout,
1997 self.network_client().list_vtxos(paged_request),
1998 )
1999 .await
2000 .context("failed to fetch list of VTXOs")??;
2001
2002 all_vtxos.extend(response.vtxos);
2003
2004 match response.page {
2006 Some(page) if page.next < page.total => {
2007 cursor = page.next;
2008 }
2009 _ => break,
2010 }
2011 }
2012
2013 Ok(all_vtxos)
2014 }
2015
2016 fn next_keypair(&self, keypair_index: KeypairIndex) -> Result<Keypair, Error> {
2017 self.inner.key_provider.get_next_keypair(keypair_index)
2018 }
2019 fn keypair_by_pk(&self, pk: &XOnlyPublicKey) -> Result<Keypair, Error> {
2020 self.inner.key_provider.get_keypair_for_pk(pk)
2021 }
2022
2023 fn sign_for_pk(&self, pk: &XOnlyPublicKey, msg: &Message) -> Result<Signature, Error> {
2024 let keypair = self.keypair_by_pk(pk)?;
2025 Ok(self.secp().sign_schnorr_no_aux_rand(msg, &keypair))
2026 }
2027
2028 fn boarding_outputs(&self) -> Result<Vec<AnnotatedBoardingOutput>, Error> {
2029 let state = self
2030 .state
2031 .read()
2032 .map_err(|_| Error::ad_hoc("client server state lock poisoned"))?;
2033 let candidate_delays = ark_core::candidate_exit_delays(
2038 state.server_info.boarding_exit_delay,
2039 state.server_info.network,
2040 )?;
2041 let outputs = state
2042 .contract_manager
2043 .lock()
2044 .map_err(|_| Error::ad_hoc("contract manager lock poisoned"))?
2045 .annotated_boarding_outputs_for_exit_delays(&candidate_delays)?;
2046 Ok(outputs)
2047 }
2048
2049 fn active_offchain_contract_addresses(&self) -> Result<Vec<ArkAddress>, Error> {
2050 self.active_offchain_contracts().map(|contracts| {
2051 contracts
2052 .into_iter()
2053 .map(|contract| contract.address)
2054 .collect()
2055 })
2056 }
2057
2058 fn active_offchain_contracts(&self) -> Result<Vec<contract::ActiveOffchainContract>, Error> {
2059 let state = self
2060 .state
2061 .read()
2062 .map_err(|_| Error::ad_hoc("client server state lock poisoned"))?;
2063 let candidate_delays = ark_core::candidate_exit_delays(
2064 state.server_info.unilateral_exit_delay,
2065 state.server_info.network,
2066 )?;
2067 let contracts = state
2068 .contract_manager
2069 .lock()
2070 .map_err(|_| Error::ad_hoc("contract manager lock poisoned"))?
2071 .active_offchain_contracts(&candidate_delays)?;
2072 Ok(contracts)
2073 }
2074
2075 fn annotate_vtxos(&self, vtxos: Vec<VirtualTxOutPoint>) -> Result<Vec<AnnotatedVtxo>, Error> {
2076 let state = self
2077 .state
2078 .read()
2079 .map_err(|_| Error::ad_hoc("client server state lock poisoned"))?;
2080 let annotated = state
2081 .contract_manager
2082 .lock()
2083 .map_err(|_| Error::ad_hoc("contract manager lock poisoned"))?
2084 .annotate_vtxos(vtxos)?;
2085 Ok(annotated)
2086 }
2087
2088 fn derivation_index_for_pk(&self, pk: &XOnlyPublicKey) -> Option<u32> {
2089 self.inner
2090 .discoverable_key_provider
2091 .as_ref()
2092 .and_then(|provider| provider.get_derivation_index_for_pk(pk))
2093 }
2094
2095 fn secp(&self) -> &Secp256k1<All> {
2096 &self.inner.secp
2097 }
2098
2099 fn blockchain(&self) -> &B {
2100 &self.inner.blockchain
2101 }
2102
2103 fn swap_storage(&self) -> &S {
2104 &self.inner.swap_storage
2105 }
2106
2107 pub async fn bump_tx(&self, parent: &Transaction) -> Result<Transaction, Error> {
2109 let fee_rate = timeout_op(self.inner.timeout, self.blockchain().get_fee_rate())
2110 .await
2111 .context("Failed to retrieve fee rate")??;
2112
2113 let change_address = self.inner.wallet.get_onchain_address()?;
2114
2115 let select_coins_fn =
2117 |target_amount: Amount| -> Result<UtxoCoinSelection, ark_core::Error> {
2118 self.inner.wallet.select_coins(target_amount).map_err(|e| {
2119 ark_core::Error::ad_hoc(format!("failed to select coins for anchor TX: {e}"))
2120 })
2121 };
2122
2123 let mut psbt = build_anchor_tx(parent, change_address, fee_rate, select_coins_fn)
2125 .map_err(|e| Error::ad_hoc(e.to_string()))?;
2126
2127 self.inner
2129 .wallet
2130 .sign(&mut psbt)
2131 .context("failed to sign bump TX")?;
2132
2133 let tx = psbt.extract_tx().map_err(Error::ad_hoc)?;
2135
2136 Ok(tx)
2137 }
2138
2139 pub async fn subscribe_to_scripts(
2155 &self,
2156 scripts: Vec<ArkAddress>,
2157 subscription_id: Option<String>,
2158 ) -> Result<String, Error> {
2159 self.network_client()
2160 .subscribe_to_scripts(scripts, subscription_id)
2161 .await
2162 .map_err(Into::into)
2163 }
2164
2165 pub async fn unsubscribe_from_scripts(
2175 &self,
2176 scripts: Vec<ArkAddress>,
2177 subscription_id: String,
2178 ) -> Result<(), Error> {
2179 self.network_client()
2180 .unsubscribe_from_scripts(scripts, subscription_id)
2181 .await
2182 .map_err(Into::into)
2183 }
2184
2185 pub async fn get_subscription(
2198 &self,
2199 subscription_id: String,
2200 ) -> Result<impl Stream<Item = Result<SubscriptionResponse, ark_grpc::Error>> + Unpin, Error>
2201 {
2202 self.network_client()
2203 .get_subscription(subscription_id)
2204 .await
2205 .map_err(Into::into)
2206 }
2207}
2208
2209#[cfg(test)]
2210mod digest_guard_tests {
2211 use super::*;
2212 use ark_grpc::test_utils;
2213 use bitcoin::key::Secp256k1;
2214 use bitcoin::secp256k1::SecretKey;
2215 use bitcoin::Address;
2216 use bitcoin::FeeRate;
2217 use bitcoin::Network;
2218 use bitcoin::Psbt;
2219 use std::convert::Infallible;
2220 use std::future::Future;
2221 use std::pin::Pin;
2222 use std::sync::atomic::AtomicUsize;
2223 use std::sync::atomic::Ordering;
2224 use std::task::Context;
2225 use std::task::Poll;
2226 use tokio::net::TcpListener;
2227 use tonic::body::Body;
2228 use tonic::codegen::http;
2229 use tonic::codegen::Service;
2230 use tonic::server::NamedService;
2231 use tonic::server::UnaryService;
2232
2233 #[derive(Clone, Default)]
2234 struct MockArkServer {
2235 state: Arc<MockState>,
2236 }
2237
2238 #[derive(Default)]
2239 struct MockState {
2240 get_info_calls: AtomicUsize,
2241 list_vtxos_calls: AtomicUsize,
2242 }
2243
2244 #[derive(Clone)]
2245 struct DummyBlockchain;
2246
2247 impl Blockchain for DummyBlockchain {
2248 async fn find_outpoints(&self, _address: &Address) -> Result<Vec<ExplorerUtxo>, Error> {
2249 Ok(Vec::new())
2250 }
2251
2252 async fn find_tx(&self, _txid: &Txid) -> Result<Option<Transaction>, Error> {
2253 Ok(None)
2254 }
2255
2256 async fn get_tx_status(&self, _txid: &Txid) -> Result<TxStatus, Error> {
2257 Ok(TxStatus { confirmed_at: None })
2258 }
2259
2260 async fn get_output_status(&self, _txid: &Txid, _vout: u32) -> Result<SpendStatus, Error> {
2261 Ok(SpendStatus { spend_txid: None })
2262 }
2263
2264 async fn broadcast(&self, _tx: &Transaction) -> Result<(), Error> {
2265 Ok(())
2266 }
2267
2268 async fn get_fee_rate(&self) -> Result<f64, Error> {
2269 Ok(1.0)
2270 }
2271
2272 async fn broadcast_package(&self, _txs: &[&Transaction]) -> Result<(), Error> {
2273 Ok(())
2274 }
2275 }
2276
2277 struct DummyWallet {
2278 keypair: Keypair,
2279 secp: Secp256k1<All>,
2280 }
2281
2282 impl DummyWallet {
2283 fn new() -> Self {
2284 let secp = Secp256k1::new();
2285 let secret_key = SecretKey::from_slice(&[2; 32]).unwrap();
2286 let keypair = Keypair::from_secret_key(&secp, &secret_key);
2287 Self { keypair, secp }
2288 }
2289 }
2290
2291 impl OnchainWallet for DummyWallet {
2292 fn get_onchain_address(&self) -> Result<Address, Error> {
2293 Ok(Address::p2tr(
2294 &self.secp,
2295 self.keypair.x_only_public_key().0,
2296 None,
2297 Network::Regtest,
2298 ))
2299 }
2300
2301 async fn sync(&self) -> Result<(), Error> {
2302 Ok(())
2303 }
2304
2305 fn balance(&self) -> Result<wallet::Balance, Error> {
2306 Ok(wallet::Balance {
2307 immature: Amount::ZERO,
2308 trusted_pending: Amount::ZERO,
2309 untrusted_pending: Amount::ZERO,
2310 confirmed: Amount::ZERO,
2311 })
2312 }
2313
2314 fn prepare_send_to_address(
2315 &self,
2316 _address: Address,
2317 _amount: Amount,
2318 _fee_rate: FeeRate,
2319 ) -> Result<Psbt, Error> {
2320 Err(Error::wallet("not implemented"))
2321 }
2322
2323 fn sign(&self, _psbt: &mut Psbt) -> Result<bool, Error> {
2324 Ok(true)
2325 }
2326
2327 fn select_coins(&self, _target_amount: Amount) -> Result<UtxoCoinSelection, Error> {
2328 Err(Error::wallet("not implemented"))
2329 }
2330 }
2331
2332 impl Service<http::Request<Body>> for MockArkServer {
2333 type Response = http::Response<Body>;
2334 type Error = Infallible;
2335 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
2336
2337 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
2338 Poll::Ready(Ok(()))
2339 }
2340
2341 fn call(&mut self, req: http::Request<Body>) -> Self::Future {
2342 match req.uri().path() {
2343 "/ark.v1.ArkService/GetInfo" => {
2344 let method = GetInfoSvc {
2345 state: self.state.clone(),
2346 };
2347 Box::pin(async move {
2348 let codec = tonic_prost::ProstCodec::default();
2349 let mut grpc = tonic::server::Grpc::new(codec);
2350 Ok(grpc.unary(method, req).await)
2351 })
2352 }
2353 "/ark.v1.IndexerService/GetVtxos" => {
2354 let method = ListVtxosSvc {
2355 state: self.state.clone(),
2356 };
2357 Box::pin(async move {
2358 let codec = tonic_prost::ProstCodec::default();
2359 let mut grpc = tonic::server::Grpc::new(codec);
2360 Ok(grpc.unary(method, req).await)
2361 })
2362 }
2363 _ => Box::pin(async move {
2364 Ok(http::Response::builder()
2365 .status(200)
2366 .header("grpc-status", "12")
2367 .header("content-type", "application/grpc")
2368 .body(Body::empty())
2369 .unwrap())
2370 }),
2371 }
2372 }
2373 }
2374
2375 impl NamedService for MockArkServer {
2376 const NAME: &'static str = "ark.v1.ArkService";
2377 }
2378
2379 #[derive(Clone)]
2380 struct MockIndexerServer(MockArkServer);
2381
2382 impl Service<http::Request<Body>> for MockIndexerServer {
2383 type Response = http::Response<Body>;
2384 type Error = Infallible;
2385 type Future = <MockArkServer as Service<http::Request<Body>>>::Future;
2386
2387 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
2388 self.0.poll_ready(cx)
2389 }
2390
2391 fn call(&mut self, req: http::Request<Body>) -> Self::Future {
2392 self.0.call(req)
2393 }
2394 }
2395
2396 impl NamedService for MockIndexerServer {
2397 const NAME: &'static str = "ark.v1.IndexerService";
2398 }
2399
2400 #[derive(Clone)]
2401 struct GetInfoSvc {
2402 state: Arc<MockState>,
2403 }
2404
2405 impl UnaryService<test_utils::GetInfoRequest> for GetInfoSvc {
2406 type Response = test_utils::GetInfoResponse;
2407 type Future = Pin<
2408 Box<dyn Future<Output = Result<tonic::Response<Self::Response>, tonic::Status>> + Send>,
2409 >;
2410
2411 fn call(&mut self, _request: tonic::Request<test_utils::GetInfoRequest>) -> Self::Future {
2412 self.state.get_info_calls.fetch_add(1, Ordering::SeqCst);
2413 Box::pin(async { Ok(tonic::Response::new(info_response("fresh-digest"))) })
2414 }
2415 }
2416
2417 #[derive(Clone)]
2418 struct ListVtxosSvc {
2419 state: Arc<MockState>,
2420 }
2421
2422 impl UnaryService<test_utils::GetVtxosRequest> for ListVtxosSvc {
2423 type Response = test_utils::GetVtxosResponse;
2424 type Future = Pin<
2425 Box<dyn Future<Output = Result<tonic::Response<Self::Response>, tonic::Status>> + Send>,
2426 >;
2427
2428 fn call(&mut self, _request: tonic::Request<test_utils::GetVtxosRequest>) -> Self::Future {
2429 self.state.list_vtxos_calls.fetch_add(1, Ordering::SeqCst);
2430 Box::pin(async {
2431 Err(tonic::Status::failed_precondition(
2432 "DIGEST_MISMATCH: invalid digest header",
2433 ))
2434 })
2435 }
2436 }
2437
2438 async fn connect_test_client(
2439 mock: MockArkServer,
2440 ) -> Client<DummyBlockchain, DummyWallet, InMemorySwapStorage> {
2441 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2442 let addr = listener.local_addr().unwrap();
2443 let incoming = tokio_stream::wrappers::TcpListenerStream::new(listener);
2444 let indexer_mock = MockIndexerServer(mock.clone());
2445 tokio::spawn(async move {
2446 tonic::transport::Server::builder()
2447 .add_service(mock)
2448 .add_service(indexer_mock)
2449 .serve_with_incoming(incoming)
2450 .await
2451 .unwrap();
2452 });
2453
2454 let secp = Secp256k1::new();
2455 let keypair = Keypair::from_secret_key(&secp, &SecretKey::from_slice(&[3; 32]).unwrap());
2456 OfflineClient::<DummyBlockchain, DummyWallet, InMemorySwapStorage>::with_keypair(
2457 OfflineClientConfig {
2458 ark_server_url: format!("http://{addr}"),
2459 boltz_url: "http://127.0.0.1:1".to_string(),
2460 ..Default::default()
2461 },
2462 keypair,
2463 Arc::new(DummyBlockchain),
2464 Arc::new(DummyWallet::new()),
2465 Arc::new(InMemorySwapStorage::default()),
2466 )
2467 .connect()
2468 .await
2469 .unwrap()
2470 }
2471
2472 fn info_response(digest: &str) -> test_utils::GetInfoResponse {
2473 let secp = Secp256k1::new();
2474 let secret_key = SecretKey::from_slice(&[1; 32]).unwrap();
2475 let keypair = Keypair::from_secret_key(&secp, &secret_key);
2476 let public_key = bitcoin::secp256k1::PublicKey::from_secret_key(&secp, &secret_key);
2477 let (xonly, _) = keypair.x_only_public_key();
2478 let address = Address::p2tr(&secp, xonly, None, Network::Regtest);
2479
2480 test_utils::GetInfoResponse {
2481 version: "0.9.9".to_string(),
2482 signer_pubkey: public_key.to_string(),
2483 forfeit_pubkey: public_key.to_string(),
2484 forfeit_address: address.to_string(),
2485 checkpoint_tapscript: String::new(),
2486 network: "regtest".to_string(),
2487 session_duration: 60,
2488 unilateral_exit_delay: 144,
2489 boarding_exit_delay: 144,
2490 utxo_min_amount: 0,
2491 utxo_max_amount: 0,
2492 vtxo_min_amount: 0,
2493 vtxo_max_amount: 0,
2494 dust: 1000,
2495 fees: None,
2496 scheduled_session: None,
2497 deprecated_signers: Vec::new(),
2498 service_status: Default::default(),
2499 digest: digest.to_string(),
2500 max_tx_weight: 0,
2501 max_op_return_outputs: 0,
2502 }
2503 }
2504
2505 #[tokio::test]
2506 async fn server_info_uses_fresh_cache_without_refetching() {
2507 let _ = rustls::crypto::ring::default_provider().install_default();
2508
2509 let mock = MockArkServer::default();
2510 let state = mock.state.clone();
2511 let client = connect_test_client(mock).await;
2512 assert_eq!(state.get_info_calls.load(Ordering::SeqCst), 1);
2513
2514 let info = client.server_info().await.unwrap();
2515 assert_eq!(info.digest, "fresh-digest");
2516 assert_eq!(state.get_info_calls.load(Ordering::SeqCst), 1);
2517 }
2518
2519 #[tokio::test]
2520 async fn list_contracts_returns_wallet_contract_views() {
2521 let _ = rustls::crypto::ring::default_provider().install_default();
2522
2523 let client = connect_test_client(MockArkServer::default()).await;
2524 let contracts = client.list_contracts().await.unwrap();
2525
2526 assert!(!contracts.is_empty());
2527 assert!(contracts.iter().all(|entry| entry.address.is_some()));
2528 assert!(contracts.iter().all(|entry| entry.signer_status.is_some()));
2529 }
2530
2531 #[tokio::test]
2532 async fn boarding_addresses_include_default_row_when_scripts_overlap() {
2533 let _ = rustls::crypto::ring::default_provider().install_default();
2534
2535 let client = connect_test_client(MockArkServer::default()).await;
2536 let addresses = client.get_boarding_addresses().await.unwrap();
2537
2538 assert_eq!(addresses.len(), 1);
2539 }
2540
2541 #[tokio::test]
2542 async fn restore_contracts_rejects_zero_gap_limit() {
2543 let _ = rustls::crypto::ring::default_provider().install_default();
2544
2545 let client = connect_test_client(MockArkServer::default()).await;
2546 let err = client.restore_contracts(0).await.unwrap_err();
2547
2548 assert!(err.to_string().contains("gap limit"));
2549 }
2550
2551 #[tokio::test]
2552 async fn restore_contracts_reports_gap_limit_for_static_provider() {
2553 let _ = rustls::crypto::ring::default_provider().install_default();
2554
2555 let client = connect_test_client(MockArkServer::default()).await;
2556 let report = client.restore_contracts(20).await.unwrap();
2557
2558 assert_eq!(report.gap_limit, 20);
2559 assert_eq!(report.scanned_keys, 0);
2560 assert!(report.entries.is_empty());
2561 }
2562
2563 #[tokio::test]
2564 async fn server_info_zero_ttl_always_refreshes() {
2565 let _ = rustls::crypto::ring::default_provider().install_default();
2566
2567 let mock = MockArkServer::default();
2568 let state = mock.state.clone();
2569 let mut client = connect_test_client(mock).await;
2570 client.inner.server_info_ttl = Duration::ZERO;
2571 assert_eq!(state.get_info_calls.load(Ordering::SeqCst), 1);
2572
2573 client.server_info().await.unwrap();
2574 client.server_info().await.unwrap();
2575
2576 assert_eq!(state.get_info_calls.load(Ordering::SeqCst), 3);
2577 }
2578
2579 #[tokio::test]
2580 async fn server_info_refreshes_expired_cache_once_for_concurrent_callers() {
2581 let _ = rustls::crypto::ring::default_provider().install_default();
2582
2583 let mock = MockArkServer::default();
2584 let state = mock.state.clone();
2585 let client = Arc::new(connect_test_client(mock).await);
2586 client.state.write().unwrap().server_info_refreshed_at =
2587 Instant::now() - DEFAULT_SERVER_INFO_TTL - Duration::from_secs(1);
2588
2589 let a = Arc::clone(&client);
2590 let b = Arc::clone(&client);
2591 let (info_a, info_b) = tokio::join!(a.server_info(), b.server_info());
2592 assert_eq!(info_a.unwrap().digest, "fresh-digest");
2593 assert_eq!(info_b.unwrap().digest, "fresh-digest");
2594 assert_eq!(state.get_info_calls.load(Ordering::SeqCst), 2);
2595 }
2596
2597 #[tokio::test]
2598 async fn guarded_client_refreshes_info_and_does_not_retry_on_digest_mismatch() {
2599 let _ = rustls::crypto::ring::default_provider().install_default();
2600
2601 let mock = MockArkServer::default();
2602 let state = mock.state.clone();
2603 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2604 let addr = listener.local_addr().unwrap();
2605 let incoming = tokio_stream::wrappers::TcpListenerStream::new(listener);
2606
2607 let indexer_mock = MockIndexerServer(mock.clone());
2608 tokio::spawn(async move {
2609 tonic::transport::Server::builder()
2610 .add_service(mock)
2611 .add_service(indexer_mock)
2612 .serve_with_incoming(incoming)
2613 .await
2614 .unwrap();
2615 });
2616
2617 let mut inner = ark_grpc::Client::new(format!("http://{addr}"));
2618 inner.connect().await.unwrap();
2619
2620 let initial_info: server::Info = info_response("stale-digest").try_into().unwrap();
2621 let mut contract_manager = ContractManager::in_memory(initial_info.network);
2622 contract_manager.register_builtins().unwrap();
2623 let cached_state = Arc::new(RwLock::new(ServerState {
2624 server_info: initial_info,
2625 fee_estimator: build_fee_estimator(&info_response("stale-digest").try_into().unwrap())
2626 .unwrap(),
2627 server_info_refreshed_at: Instant::now()
2628 - DEFAULT_SERVER_INFO_TTL
2629 - Duration::from_secs(1),
2630 contract_manager: Mutex::new(contract_manager),
2631 }));
2632 let hook_state = cached_state.clone();
2633 inner.set_info_refresh_hook(move |server_info| {
2634 update_server_state(&hook_state, server_info)
2635 .map_err(|err| Box::new(err) as Box<dyn std::error::Error + Send + Sync>)
2636 });
2637
2638 let err = match inner
2639 .list_vtxos(GetVtxosRequest::new_for_outpoints(&[OutPoint::null()]))
2640 .await
2641 {
2642 Ok(_) => panic!("list_vtxos unexpectedly succeeded"),
2643 Err(err) => err,
2644 };
2645
2646 assert!(err.is_server_info_changed());
2647 assert!(Error::from(err).is_server_info_changed());
2648 assert_eq!(state.list_vtxos_calls.load(Ordering::SeqCst), 1);
2649 assert_eq!(state.get_info_calls.load(Ordering::SeqCst), 1);
2650 let refreshed_state = cached_state.read().unwrap();
2651 assert_eq!(refreshed_state.server_info.digest, "fresh-digest");
2652 assert!(refreshed_state.server_info_refreshed_at.elapsed() < DEFAULT_SERVER_INFO_TTL);
2653 }
2654}