1#[cfg(all(any(target_os = "android", target_os = "ios"), feature = "tls-native-roots"))]
295compile_error!("feature `tls-native-roots` can't be used on Android or iOS, use `tls-webpki-roots` instead");
296
297pub extern crate ark;
298
299pub extern crate bip39;
300pub extern crate lightning_invoice;
301pub extern crate lnurl as lnurllib;
302
303#[macro_use] extern crate anyhow;
304#[macro_use] extern crate async_trait;
305#[macro_use] extern crate serde;
306
307pub mod actions;
308pub mod chain;
309pub mod exit;
310pub mod fs_perms;
311pub mod movement;
312pub mod onchain;
313pub mod payment_request;
314pub mod persist;
315pub mod round;
316pub mod subsystem;
317pub mod vtxo;
318
319pub mod lock_manager;
320
321mod arkoor;
322mod board;
323mod config;
324mod daemon;
325mod fees;
326mod lightning;
327mod mailbox;
328mod notification;
329mod offboard;
330#[cfg(feature = "socks5-proxy")]
331mod proxy;
332mod recovery;
333mod psbtext;
334mod utils;
335
336pub use self::arkoor::{ArkoorCreateResult, ArkoorAddressError};
337pub use self::config::{BarkNetwork, Config};
338pub use self::daemon::DaemonHandle;
339pub use self::fees::FeeEstimate;
340pub use self::notification::{WalletNotification, NotificationStream};
341pub use self::vtxo::WalletVtxo;
342pub use self::utils::time;
343
344use std::borrow::Cow;
345use std::collections::HashSet;
346use std::path::PathBuf;
347use std::sync::Arc;
348use std::time::Duration;
349
350use anyhow::{bail, Context};
351use bip39::Mnemonic;
352use bitcoin::{Amount, Network, OutPoint};
353use bitcoin::bip32::{self, ChildNumber, Fingerprint};
354use bitcoin::secp256k1::{self, Keypair, PublicKey};
355use futures::stream::FuturesUnordered;
356use log::{debug, error, info, trace, warn};
357use tokio_stream::StreamExt;
358
359use ark::{ArkInfo, ProtocolEncoding, Vtxo, VtxoId, VtxoPolicy, VtxoRequest};
360use ark::address::VtxoDelivery;
361use ark::fees::{validate_and_subtract_fee_min_dust, VtxoFeeInfo};
362use ark::rounds::{RoundAttempt, RoundEvent};
363use ark::vtxo::{Full, PubkeyVtxoPolicy, VtxoRef, VTXO_DUST};
364use ark::vtxo::policy::signing::VtxoSigner;
365use bitcoin_ext::{BlockHeight, TxStatus};
366use server_rpc::{protos, ServerConnection};
367use server_rpc::client::{ConnectError, CreateEndpointError};
368
369use crate::chain::{ChainSource, ChainSourceSpec};
370use crate::exit::Exit;
371use crate::lock_manager::LockManager;
372use crate::movement::{Movement, MovementId, PaymentMethod};
373use crate::movement::manager::MovementManager;
374use crate::notification::NotificationDispatch;
375use crate::onchain::{OnchainWalletTrait, Utxo};
376use crate::persist::BarkPersister;
377use crate::persist::models::{RoundStateId, StoredRoundState, Unlocked};
378#[cfg(feature = "socks5-proxy")]
379use crate::proxy::proxy_for_url;
380use crate::recovery::RecoveryReport;
381use crate::round::{RoundParticipation, RoundSecretNonces, RoundStatus};
382use crate::subsystem::RoundMovement;
383use crate::utils::rejected_vtxos_from_error;
384use crate::vtxo::{FilterVtxos, RefreshStrategy, VtxoFilter, VtxoStateKind, VtxoValidationError};
385use crate::vtxo::selection::{InputSelection, SelectedFeeInfos};
386
387#[cfg(all(feature = "wasm-web", feature = "socks5-proxy"))]
388compile_error!("features `wasm-web` does not support feature `socks5-proxy");
389
390#[cfg(all(feature = "wasm-web", feature = "bitcoind-rpc"))]
391compile_error!("`wasm-web` does not support the `bitcoind-rpc` feature");
392
393const BARK_PURPOSE_INDEX: u32 = 350;
395const VTXO_KEYS_INDEX: u32 = 0;
397const MAILBOX_KEY_INDEX: u32 = 1;
399const RECOVERY_MAILBOX_KEY_INDEX: u32 = 2;
401const MISSING_SERVER_TRANSPORT_HELP: &str =
402 "This build of bark-wallet does not include an Ark server transport backend. Enable feature `bark-wallet/native` or `bark-wallet/wasm-web` to use server-backed wallet functionality.";
403
404const SUBSCRIBE_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 60);
406
407lazy_static::lazy_static! {
408 static ref SECP: secp256k1::Secp256k1<secp256k1::All> = secp256k1::Secp256k1::new();
410}
411
412fn log_server_pubkey_changed_error(expected: PublicKey, got: PublicKey) {
418 error!(
419 "
420Server public key has changed!
421
422The Ark server's public key is different from the one stored when this
423wallet was created. This typically happens when:
424
425 - The server operator has rotated their keys
426 - You are connecting to a different server
427 - The server has been replaced
428
429For safety, this wallet will not connect to the server until you
430resolve this. You can recover your funds on-chain by doing an emergency exit.
431
432This will exit your VTXOs to on-chain Bitcoin without needing the server's cooperation.
433
434Expected: {expected}
435Got: {got}")
436}
437
438fn log_server_mailbox_pubkey_changed_error(expected: PublicKey, got: PublicKey) {
440 error!(
441 "
442Server mailbox public key has changed!
443
444The Ark server's mailbox public key is different from the one stored when this
445wallet was created. This typically happens when:
446
447 - The server operator has rotated their keys
448 - You are connecting to a different server
449 - The server has been replaced
450
451For safety, this wallet will not connect to the server until you resolve this.
452
453Unlike a server pubkey change, your VTXOs are not at risk - the mailbox pubkey
454only affects address receive semantics. Any Ark addresses you previously
455shared will stop receiving new payments; you will need to share new addresses
456after reconnecting.
457
458Expected: {expected}
459Got: {got}")
460}
461
462#[derive(Debug, Clone)]
464pub struct LightningReceiveBalance {
465 pub total: Amount,
467 pub claimable: Amount,
469}
470
471#[derive(Debug, Clone)]
473pub struct Balance {
474 pub spendable: Amount,
476 pub pending_lightning_send: Amount,
478 pub claimable_lightning_receive: Amount,
480 pub pending_in_round: Amount,
482 pub pending_exit: Option<Amount>,
488 pub pending_board: Amount,
490}
491
492pub struct UtxoInfo {
493 pub outpoint: OutPoint,
494 pub amount: Amount,
495 pub confirmation_height: Option<u32>,
496}
497
498impl From<Utxo> for UtxoInfo {
499 fn from(value: Utxo) -> Self {
500 match value {
501 Utxo::Local(o) => UtxoInfo {
502 outpoint: o.outpoint,
503 amount: o.amount,
504 confirmation_height: o.confirmation_height,
505 },
506 Utxo::Exit(e) => UtxoInfo {
507 outpoint: e.vtxo.point(),
508 amount: e.vtxo.amount(),
509 confirmation_height: Some(e.height),
510 },
511 }
512 }
513}
514
515pub struct OffchainBalance {
518 pub available: Amount,
520 pub pending_in_round: Amount,
522 pub pending_exit: Amount,
525}
526
527#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
529pub struct WalletProperties {
530 pub network: Network,
534
535 pub fingerprint: Fingerprint,
539
540 pub server_pubkey: Option<PublicKey>,
547
548 pub server_mailbox_pubkey: Option<PublicKey>,
556}
557
558pub struct WalletSeed {
564 master: bip32::Xpriv,
565 vtxo: bip32::Xpriv,
566}
567
568impl WalletSeed {
569 pub fn new_from_seed(network: Network, seed: &[u8; 64]) -> Self {
571 let bark_path = [ChildNumber::from_hardened_idx(BARK_PURPOSE_INDEX).unwrap()];
572 let master = bip32::Xpriv::new_master(network, seed)
573 .expect("invalid seed")
574 .derive_priv(&SECP, &bark_path)
575 .expect("purpose is valid");
576
577 let vtxo_path = [ChildNumber::from_hardened_idx(VTXO_KEYS_INDEX).unwrap()];
578 let vtxo = master.derive_priv(&SECP, &vtxo_path)
579 .expect("vtxo path is valid");
580
581 Self { master, vtxo }
582 }
583
584 pub fn new_from_mnemonic(network: Network, mnemonic: &Mnemonic) -> Self {
586 Self::new_from_seed(network, &mnemonic.to_seed(""))
587 }
588
589 pub fn fingerprint(&self) -> Fingerprint {
590 self.master.fingerprint(&SECP)
591 }
592
593 fn derive_vtxo_keypair(&self, idx: u32) -> Keypair {
594 self.vtxo.derive_priv(&SECP, &[idx.into()]).unwrap().to_keypair(&SECP)
595 }
596
597 fn to_mailbox_keypair(&self) -> Keypair {
598 let mailbox_path = [ChildNumber::from_hardened_idx(MAILBOX_KEY_INDEX).unwrap()];
599 self.master.derive_priv(&SECP, &mailbox_path).unwrap().to_keypair(&SECP)
600 }
601
602 fn to_recovery_mailbox_keypair(&self) -> Keypair {
603 let mailbox_path = [ChildNumber::from_hardened_idx(RECOVERY_MAILBOX_KEY_INDEX).unwrap()];
604 self.master.derive_priv(&SECP, &mailbox_path).unwrap().to_keypair(&SECP)
605 }
606}
607
608pub struct OpenWalletArgs {
610 pub run_daemon: bool,
616
617 pub datadir: Option<PathBuf>,
627
628 pub persister: Option<Arc<dyn BarkPersister>>,
632
633 pub lock_manager: Option<Box<dyn LockManager>>,
640
641 pub onchain: Option<Arc<tokio::sync::RwLock<dyn OnchainWalletTrait>>>,
645
646 pub create_if_not_exists: bool,
650
651 pub create_without_server: bool,
655
656 pub skip_recovery: bool,
662
663 pub on_recovery_finished: Option<Box<dyn FnOnce(RecoveryReport) + Send + Sync>>,
667}
668
669impl Default for OpenWalletArgs {
670 fn default() -> Self {
671 Self {
672 run_daemon: true,
673 onchain: None,
674 datadir: None,
675 persister: None,
676 lock_manager: None,
677 create_if_not_exists: true,
678 create_without_server: false,
679 skip_recovery: false,
680 on_recovery_finished: None,
681 }
682 }
683}
684
685struct WalletInner {
686 chain: Arc<ChainSource>,
688
689 exit: Exit,
691
692 movements: Arc<MovementManager>,
694
695 notifications: NotificationDispatch,
697
698 config: Config,
700
701 db: Arc<dyn BarkPersister>,
703
704 lock_manager: Box<dyn LockManager>,
708
709 seed: WalletSeed,
711
712 server: tokio::sync::OnceCell<ServerConnection>,
719
720 onchain: Option<Arc<tokio::sync::RwLock<dyn OnchainWalletTrait>>>,
725
726 daemon: parking_lot::Mutex<Option<DaemonHandle>>,
728
729 last_force_exit_scan_tip: tokio::sync::Mutex<Option<BlockHeight>>,
733
734 pub(crate) round_secret_nonces: RoundSecretNonces,
737}
738
739#[derive(Clone)]
840pub struct Wallet {
841 inner: Arc<WalletInner>,
842}
843
844impl Wallet {
845 pub async fn network(&self) -> anyhow::Result<Network> {
846 Ok(self.properties().await?.network)
847 }
848
849 pub fn chain(&self) -> &Arc<ChainSource> {
851 &self.inner.chain
852 }
853
854 pub fn exit_mgr(&self) -> &Exit {
856 &self.inner.exit
857 }
858
859 pub fn movements_mgr(&self) -> &MovementManager {
861 &self.inner.movements
862 }
863
864 pub async fn peek_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
867 let last_revealed = self.inner.db.get_last_vtxo_key_index().await?;
868
869 let index = last_revealed.map(|i| i + 1).unwrap_or(u32::MIN);
870 let keypair = self.inner.seed.derive_vtxo_keypair(index);
871
872 Ok((keypair, index))
873 }
874
875 pub async fn derive_store_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
878 let (keypair, index) = self.peek_next_keypair().await?;
879 self.inner.db.store_vtxo_key(index, keypair.public_key()).await?;
880 Ok((keypair, index))
881 }
882
883 #[deprecated(note = "use peek_keypair instead")]
884 pub async fn peak_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
885 self.peek_keypair(index).await
886 }
887
888 pub async fn peek_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
902 let keypair = self.inner.seed.derive_vtxo_keypair(index);
903 if self.inner.db.get_public_key_idx(&keypair.public_key()).await?.is_some() {
904 Ok(keypair)
905 } else {
906 bail!("VTXO key {} does not exist, please derive it first", index)
907 }
908 }
909
910
911 pub async fn pubkey_keypair(&self, public_key: &PublicKey) -> anyhow::Result<Option<(u32, Keypair)>> {
923 if let Some(index) = self.inner.db.get_public_key_idx(&public_key).await? {
924 Ok(Some((index, self.inner.seed.derive_vtxo_keypair(index))))
925 } else {
926 Ok(None)
927 }
928 }
929
930 pub async fn get_vtxo_key(&self, vtxo: impl VtxoRef) -> anyhow::Result<Keypair> {
941 let bare_vtxo = match vtxo.as_bare_vtxo() {
942 Some(bare) => bare,
943 None => Cow::Owned(self.get_vtxo_by_id(vtxo.vtxo_id()).await?.vtxo),
944 };
945 let pubkey = self.find_signable_clause(&bare_vtxo).await
946 .context("VTXO is not signable by wallet")?
947 .pubkey();
948 let idx = self.inner.db.get_public_key_idx(&pubkey).await?
949 .context("VTXO key not found")?;
950 Ok(self.inner.seed.derive_vtxo_keypair(idx))
951 }
952
953 #[deprecated(note = "use peek_address instead")]
954 pub async fn peak_address(&self, index: u32) -> anyhow::Result<ark::Address> {
955 self.peek_address(index).await
956 }
957
958 pub async fn peek_address(&self, index: u32) -> anyhow::Result<ark::Address> {
962 let properties = self.properties().await?;
963 let network = properties.network;
964 let keypair = self.peek_keypair(index).await?;
965 let mailbox = self.mailbox_identifier();
966
967
968 let (server_pubkey, mailbox_pubkey) =
969 if let (Some(spk), Some(mpk)) = (properties.server_pubkey, properties.server_mailbox_pubkey) {
970 (spk, mpk)
971 } else {
972 let (_, ark_info) = self.require_server().await?;
973 (ark_info.server_pubkey, ark_info.mailbox_pubkey)
974 };
975
976 Ok(ark::Address::builder()
977 .testnet(network != bitcoin::Network::Bitcoin)
978 .server_pubkey(server_pubkey)
979 .pubkey_policy(keypair.public_key())
980 .mailbox(mailbox_pubkey, mailbox, &keypair)
981 .context("failed to assign mailbox")?
982 .into_address()
983 .context("failed to build address")?)
984 }
985
986 pub async fn new_address_with_index(&self) -> anyhow::Result<(ark::Address, u32)> {
990 let (_, index) = self.derive_store_next_keypair().await?;
991 let addr = self.peek_address(index).await?;
992 Ok((addr, index))
993 }
994
995 pub async fn new_address(&self) -> anyhow::Result<ark::Address> {
997 let (addr, _) = self.new_address_with_index().await?;
998 Ok(addr)
999 }
1000
1001 pub async fn create(
1010 network: Network,
1011 seed: &WalletSeed,
1012 config: &Config,
1013 db: &dyn BarkPersister,
1014 lock_manager: &dyn LockManager,
1015 allow_unreachable_server: bool,
1016 ) -> anyhow::Result<()> {
1017 trace!("Config: {:?}", config);
1018
1019 let wallet_fingerprint = seed.fingerprint();
1020
1021 let create_guard = lock_manager.lock(
1026 &format!("{}.create", wallet_fingerprint),
1027 Duration::from_secs(5),
1028 ).await.context("wallet initialization already in progress")?;
1029
1030 if let Some(existing) = db.read_properties().await? {
1031 trace!("Existing config: {:?}", existing);
1032 bail!("cannot overwrite already existing config")
1033 }
1034
1035 let (server_pubkey, mailbox_pubkey) = match Self::connect_to_server(&config, network).await {
1037 Ok(conn) => {
1038 let ark_info = conn.ark_info().await;
1039 (Some(ark_info.server_pubkey), Some(ark_info.mailbox_pubkey))
1040 },
1041 Err(_) if allow_unreachable_server => (None, None),
1042 Err(err) => {
1043 bail!("Failed to connect to provided server: {:#}", err);
1044 },
1045 };
1046
1047 let properties = WalletProperties {
1048 network,
1049 fingerprint: wallet_fingerprint,
1050 server_pubkey,
1051 server_mailbox_pubkey: mailbox_pubkey,
1052 };
1053
1054 db.init_wallet(&properties).await.context("cannot init wallet in the database")?;
1056 info!("Created wallet with fingerprint: {}", wallet_fingerprint);
1057 if let Some(pk) = server_pubkey {
1058 info!("Stored server pubkey: {}", pk);
1059 }
1060
1061 drop(create_guard);
1064
1065 Ok(())
1066 }
1067
1068 pub async fn open(
1070 network: Network,
1071 seed: WalletSeed,
1072 config: Config,
1073 args: OpenWalletArgs,
1074 ) -> anyhow::Result<Wallet> {
1075 let fingerprint = seed.fingerprint();
1076 let lock_manager = if let Some(lm) = args.lock_manager {
1077 lm
1078 } else {
1079 crate::lock_manager::platform_default(args.datadir.as_ref(), Some(fingerprint))
1080 .context("failed to instantiate platform default lock manager")?
1081 };
1082
1083 let db = if let Some(db) = args.persister {
1084 db
1085 } else {
1086 if let Some(ref datadir) = args.datadir {
1087 #[cfg(not(target_arch = "wasm32"))]
1088 if !datadir.exists() && args.create_if_not_exists {
1089 tokio::fs::create_dir_all(datadir).await.with_context(|| format!(
1090 "failed to create datadir at {}", datadir.display(),
1091 ))?;
1092 }
1093 }
1094 crate::persist::platform_default(args.datadir.as_ref(), Some(fingerprint)).await
1095 .context("failed to instantiate platform default persister")?
1096 };
1097
1098 let mut created_now = false;
1099 let properties = if let Some(p) = db.read_properties().await? {
1100 p
1101 } else if args.create_if_not_exists {
1102 Self::create(
1103 network, &seed, &config, &*db, &*lock_manager, args.create_without_server,
1104 ).await.context("error creating new wallet")?;
1105 created_now = true;
1106 db.read_properties().await?
1107 .context("create failed: no wallet properties after Wallet::create was called")?
1108 } else {
1109 bail!("wallet does not exist; use Wallet::create or \
1110 set options.create_if_not_exists to true");
1111 };
1112
1113 if properties.fingerprint != fingerprint {
1114 bail!("incorrect mnemonic")
1115 }
1116
1117 let chain_source = if let Some(ref url) = config.esplora_address {
1118 ChainSourceSpec::Esplora {
1119 url: url.clone(),
1120 }
1121 } else if let Some(ref url) = config.bitcoind_address {
1122 let auth = if let Some(ref c) = config.bitcoind_cookiefile {
1123 bitcoin_ext::rpc::Auth::CookieFile(c.clone())
1124 } else {
1125 bitcoin_ext::rpc::Auth::UserPass(
1126 config.bitcoind_user.clone().context("need bitcoind auth config")?,
1127 config.bitcoind_pass.clone().context("need bitcoind auth config")?,
1128 )
1129 };
1130 ChainSourceSpec::Bitcoind { url: url.clone(), auth }
1131 } else {
1132 bail!("Need to either provide esplora or bitcoind info");
1133 };
1134
1135 #[cfg(feature = "socks5-proxy")]
1136 let chain_proxy = proxy_for_url(&config.socks5_proxy, chain_source.url())?;
1137 let chain_source_client = ChainSource::new(
1138 chain_source, properties.network, config.fallback_fee_rate,
1139 #[cfg(feature = "socks5-proxy")] chain_proxy.as_deref(),
1140 ).await?;
1141 let chain = Arc::new(chain_source_client);
1142 chain.require_version().await
1143 .context("provided chain source doesn't meet version requirement")?;
1144
1145 let server = tokio::sync::OnceCell::new();
1146
1147 let notifications = NotificationDispatch::new();
1148 let movements = Arc::new(MovementManager::new(db.clone(), notifications.clone()));
1149 let exit = Exit::new(db.clone(), chain.clone(), movements.clone()).await?;
1150
1151 let onchain = args.onchain;
1152 let ret = Wallet { inner: Arc::new(WalletInner {
1153 config, db, lock_manager, seed, exit, movements, notifications, server, chain,
1154 onchain,
1155 daemon: parking_lot::Mutex::new(None),
1156 last_force_exit_scan_tip: tokio::sync::Mutex::new(None),
1157 round_secret_nonces: RoundSecretNonces::new(),
1158 })};
1159
1160 ret.inner.exit.load().await
1161 .context("error loading exit system after opening wallet")?;
1162
1163 if created_now {
1164 if !args.skip_recovery {
1165 match ret.recover_from_mailbox().await {
1170 Ok(report) => {
1171 if let Some(callback) = args.on_recovery_finished {
1172 callback(report);
1173 }
1174 },
1175 Err(e) => {
1176 error!("VTXO recovery from the recovery mailbox failed; funds may be \
1177 missing from this wallet until recovery succeeds: {:#}", e);
1178 },
1179 }
1180 } else {
1181 info!("Seed-based wallet recovery explicitly skipped");
1182 }
1183 }
1184
1185 if args.run_daemon {
1186 ret.start_daemon()
1187 .context("failed to start daemon after opening wallet")?;
1188 }
1189
1190 Ok(ret)
1191 }
1192
1193 pub fn config(&self) -> &Config {
1195 &self.inner.config
1196 }
1197
1198 pub async fn properties(&self) -> anyhow::Result<WalletProperties> {
1200 let properties = self.inner.db.read_properties().await?.context("Wallet is not initialised")?;
1201 Ok(properties)
1202 }
1203
1204 pub fn fingerprint(&self) -> Fingerprint {
1206 self.inner.seed.fingerprint()
1207 }
1208
1209 async fn connect_to_server(
1210 config: &Config,
1211 network: Network,
1212 ) -> anyhow::Result<ServerConnection> {
1213 let server_address = crate::utils::url_with_default_https_scheme(&config.server_address);
1214 let mut builder = ServerConnection::builder()
1215 .address(&server_address)
1216 .network(network);
1217
1218 #[cfg(feature = "socks5-proxy")]
1219 if let Some(proxy) = proxy_for_url(&config.socks5_proxy, &server_address)? {
1220 builder = builder.proxy(&proxy)
1221 }
1222
1223 #[allow(deprecated)]
1224 {
1225 if let Some(ref token) = config.server_access_token {
1226 builder = builder.access_token(token);
1227 }
1228 }
1229
1230 if let Some(ref ua) = config.user_agent {
1231 builder = builder.user_agent(ua);
1232 }
1233
1234 builder.connect().await.map_err(wrap_server_connect_error)
1235 .context("Failed to connect to Ark server")
1236 }
1237
1238 async fn require_server(&self) -> anyhow::Result<(ServerConnection, ArkInfo)> {
1239 let conn = self.inner.server.get_or_try_init(|| async {
1243 let network = self.properties().await?.network;
1244 Self::connect_to_server(&self.inner.config, network).await
1245 .context("You should be connected to Ark server to perform this action")
1246 }).await?.clone();
1247
1248 let ark_info = conn.ark_info().await;
1249 self.check_and_store_server_keys(&ark_info).await?;
1250
1251 Ok((conn, ark_info))
1252 }
1253
1254 pub async fn refresh_server(&self) -> anyhow::Result<()> {
1255 let srv = self.inner.server.get_or_try_init(|| async {
1261 let properties = self.properties().await?;
1262 Self::connect_to_server(&self.inner.config, properties.network).await
1263 .map_err(anyhow::Error::from)
1264 }).await?;
1265
1266 srv.check_connection().await?;
1267 let ark_info = srv.ark_info().await;
1268 ark_info.fees.validate().context("invalid fee schedule")?;
1269 self.check_and_store_server_keys(&ark_info).await?;
1270
1271 Ok(())
1272 }
1273
1274 pub fn onchain(&self) -> Option<Arc<tokio::sync::RwLock<dyn OnchainWalletTrait>>> {
1276 self.inner.onchain.clone()
1277 }
1278
1279 pub async fn sync_onchain(&self) -> anyhow::Result<()> {
1281 if let Some(onchain) = self.inner.onchain.as_ref() {
1282 onchain.write().await.sync(self.chain()).await?;
1283 }
1284 Ok(())
1285 }
1286
1287 async fn check_and_store_server_keys(&self, ark_info: &ArkInfo) -> anyhow::Result<()> {
1294 let properties = self.properties().await?;
1295
1296 if let Some(stored_pubkey) = properties.server_pubkey {
1297 if stored_pubkey != ark_info.server_pubkey {
1298 log_server_pubkey_changed_error(stored_pubkey, ark_info.server_pubkey);
1299 bail!("Server public key has changed. You should exit all your VTXOs!");
1300 }
1301 } else {
1302 self.inner.db.set_server_pubkey(ark_info.server_pubkey).await?;
1303 info!("Stored server pubkey for existing wallet: {}", ark_info.server_pubkey);
1304 }
1305
1306 if let Some(stored_mailbox_pubkey) = properties.server_mailbox_pubkey {
1307 if stored_mailbox_pubkey != ark_info.mailbox_pubkey {
1308 log_server_mailbox_pubkey_changed_error(stored_mailbox_pubkey, ark_info.mailbox_pubkey);
1309 bail!("Server mailbox public key has changed.");
1310 }
1311 } else {
1312 self.inner.db.set_server_mailbox_pubkey(ark_info.mailbox_pubkey).await?;
1313 info!("Stored server mailbox pubkey for existing wallet: {}", ark_info.mailbox_pubkey);
1314 }
1315
1316 Ok(())
1317 }
1318
1319 pub async fn ark_info(&self) -> anyhow::Result<Option<ArkInfo>> {
1321 match self.inner.server.get() {
1322 Some(srv) => Ok(Some(srv.ark_info().await)),
1323 None => Ok(None),
1324 }
1325 }
1326
1327 pub async fn require_ark_info(&self) -> anyhow::Result<ArkInfo> {
1333 let (_, ark_info) = self.require_server().await?;
1334 Ok(ark_info)
1335 }
1336
1337 pub async fn balance(&self) -> anyhow::Result<Balance> {
1341 let vtxos = self.vtxos().await?;
1342
1343 let spendable = {
1344 let mut v = vtxos.iter().collect();
1345 VtxoStateKind::Spendable.filter_vtxos(&mut v).await?;
1346 v.into_iter().map(|v| v.amount()).sum::<Amount>()
1347 };
1348
1349 let pending_lightning_send = self.pending_lightning_send_vtxos().await?.iter()
1350 .map(|v| v.amount())
1351 .sum::<Amount>();
1352
1353 let claimable_lightning_receive = self.claimable_lightning_receive_balance().await?;
1354
1355 let pending_board = self.pending_board_vtxos().await?.iter()
1356 .map(|v| v.amount())
1357 .sum::<Amount>();
1358
1359 let pending_in_round = self.pending_round_balance().await?;
1360
1361 let pending_exit = self.exit_mgr().try_pending_total();
1362
1363 Ok(Balance {
1364 spendable,
1365 pending_in_round,
1366 pending_lightning_send,
1367 claimable_lightning_receive,
1368 pending_exit,
1369 pending_board,
1370 })
1371 }
1372
1373 pub async fn validate_vtxo(&self, vtxo: &Vtxo<Full>) -> Result<(), VtxoValidationError> {
1375 let tx = self.inner.chain.get_tx(&vtxo.chain_anchor().txid).await
1376 .map_err(VtxoValidationError::Chain)?
1377 .ok_or(VtxoValidationError::AnchorNotFound)?;
1378
1379 vtxo.validate(&tx).map_err(VtxoValidationError::Invalid)
1380 }
1381
1382 pub async fn import_vtxo(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<()> {
1392 if self.inner.db.get_wallet_vtxo(vtxo.id()).await?.is_some() {
1393 info!("VTXO {} already exists in wallet, skipping import", vtxo.id());
1394 return Ok(());
1395 }
1396
1397 self.validate_vtxo(vtxo).await.context("VTXO validation failed")?;
1398
1399 if self.find_signable_clause(vtxo).await.is_none() {
1400 bail!("VTXO {} is not owned by this wallet (no signable clause found)", vtxo.id());
1401 }
1402
1403 let current_height = self.inner.chain.tip().await?;
1404 if vtxo.expiry_height() <= current_height {
1405 bail!("Vtxo {} has expired", vtxo.id());
1406 }
1407
1408 self.store_spendable_vtxos([vtxo]).await.context("failed to store imported VTXO")?;
1409
1410 info!("Successfully imported VTXO {}", vtxo.id());
1411 Ok(())
1412 }
1413
1414 pub async fn get_vtxo_by_id(&self, vtxo_id: VtxoId) -> anyhow::Result<WalletVtxo> {
1416 let vtxo = self.inner.db.get_wallet_vtxo(vtxo_id).await
1417 .with_context(|| format!("Error when querying vtxo {} in database", vtxo_id))?
1418 .with_context(|| format!("The VTXO with id {} cannot be found", vtxo_id))?;
1419 Ok(vtxo)
1420 }
1421
1422 pub async fn get_full_vtxo(&self, vtxo_id: VtxoId) -> anyhow::Result<Vtxo<Full>> {
1430 self.inner.db.get_full_vtxo(vtxo_id).await
1431 .with_context(|| format!("Error when querying full vtxo {} in database", vtxo_id))?
1432 .with_context(|| format!("The VTXO with id {} cannot be found", vtxo_id))
1433 }
1434
1435 pub async fn get_full_vtxos<V: VtxoRef>(
1437 &self,
1438 vtxos: impl IntoIterator<Item = V>,
1439 ) -> anyhow::Result<Vec<Vtxo<Full>>> {
1440 let ids = vtxos.into_iter().map(|v| v.vtxo_id()).collect::<Vec<_>>();
1441 self.inner.db.get_full_vtxos(&ids).await
1442 .with_context(||
1443 format!("Error when querying full vtxos in database with IDs: {:?}", ids)
1444 )
1445 }
1446
1447 #[deprecated(since="0.1.0-beta.5", note = "Use Wallet::history instead")]
1449 pub async fn movements(&self) -> anyhow::Result<Vec<Movement>> {
1450 self.history().await
1451 }
1452
1453 pub async fn history(&self) -> anyhow::Result<Vec<Movement>> {
1455 Ok(self.inner.db.get_all_movements().await?)
1456 }
1457
1458 pub async fn update_history_metadata(
1478 &self,
1479 movement_id: MovementId,
1480 patch: &serde_json::Value,
1481 ) -> anyhow::Result<()> {
1482 self.inner.movements.patch_metadata(movement_id, patch).await?;
1483 Ok(())
1484 }
1485
1486 pub async fn history_by_payment_method(
1488 &self,
1489 payment_method: &PaymentMethod,
1490 ) -> anyhow::Result<Vec<Movement>> {
1491 let mut ret = self.inner.db.get_movements_by_payment_method(payment_method).await?;
1492 ret.sort_by_key(|m| m.id);
1493 Ok(ret)
1494 }
1495
1496 pub async fn all_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1498 Ok(self.inner.db.get_all_vtxos().await?)
1499 }
1500
1501 pub async fn vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1503 Ok(self.inner.db.get_vtxos_by_state(&VtxoStateKind::UNSPENT_STATES).await?)
1504 }
1505
1506 pub async fn vtxos_with(&self, filter: &impl FilterVtxos) -> anyhow::Result<Vec<WalletVtxo>> {
1508 let mut vtxos = self.vtxos().await?;
1509 filter.filter_vtxos(&mut vtxos).await.context("error filtering vtxos")?;
1510 Ok(vtxos)
1511 }
1512
1513 pub async fn spendable_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1515 Ok(self.vtxos_with(&VtxoStateKind::Spendable).await?)
1516 }
1517
1518 pub async fn spendable_vtxos_with(
1520 &self,
1521 filter: &impl FilterVtxos,
1522 ) -> anyhow::Result<Vec<WalletVtxo>> {
1523 let mut vtxos = self.spendable_vtxos().await?;
1524 filter.filter_vtxos(&mut vtxos).await.context("error filtering vtxos")?;
1525 Ok(vtxos)
1526 }
1527
1528 pub async fn get_expiring_vtxos(
1530 &self,
1531 threshold: BlockHeight,
1532 ) -> anyhow::Result<Vec<WalletVtxo>> {
1533 let expiry = self.inner.chain.tip().await? + threshold;
1534 let filter = VtxoFilter::new(&self).expires_before(expiry);
1535 Ok(self.spendable_vtxos_with(&filter).await?)
1536 }
1537
1538 pub async fn maintenance(&self) -> anyhow::Result<()> {
1544 info!("Starting wallet maintenance in interactive mode");
1545 self.sync().await;
1546
1547 let rounds = self.progress_pending_rounds(None).await;
1549 if let Err(e) = rounds.as_ref() {
1550 warn!("Error progressing pending rounds: {:#}", e);
1551 }
1552
1553 let states = self.inner.db.get_pending_round_state_ids().await?;
1555 for id in states {
1556 debug!("Cancelling pending round participation {}", id);
1557 let mut state = match self.lock_wait_round_state(id).await {
1558 Ok(Some(s)) => s,
1559 Ok(None) => continue, Err(e) => {
1561 warn!("Failed to lock round state with id {}: {:#}", id, e);
1562 continue;
1563 }
1564 };
1565 if let Err(e) = state.state_mut().try_cancel(self).await {
1566 warn!("Error cancelling pending round: {:#}", e);
1567 }
1568 }
1569
1570 let refresh = self.maintenance_refresh().await;
1572 if let Err(e) = refresh.as_ref() {
1573 warn!("Error refreshing VTXOs: {:#}", e);
1574 }
1575
1576 if rounds.is_err() || refresh.is_err() {
1577 bail!("Maintenance encountered errors.\nprogress_rounds: {:#?}\nrefresh: {:#?}",
1578 rounds, refresh,
1579 );
1580 }
1581
1582 Ok(())
1583 }
1584
1585 pub async fn maintenance_delegated(&self) -> anyhow::Result<()> {
1592 info!("Starting wallet maintenance in delegated mode");
1593 self.sync().await;
1594 let rounds = self.progress_pending_rounds(None).await;
1595 if let Err(e) = rounds.as_ref() {
1596 warn!("Error progressing pending rounds: {:#}", e);
1597 }
1598 let refresh = self.maybe_schedule_maintenance_refresh_delegated().await;
1599 if let Err(e) = refresh.as_ref() {
1600 warn!("Error refreshing VTXOs: {:#}", e);
1601 }
1602
1603 if rounds.is_err() || refresh.is_err() {
1604 bail!("Delegated maintenance encountered errors.\n\
1605 progress_rounds: {:#?}\nrefresh: {:#?}",
1606 rounds, refresh,
1607 );
1608 }
1609
1610 Ok(())
1611 }
1612
1613 pub(crate) async fn join_round_for_maintenance_refresh(
1628 &self,
1629 attempt: &RoundAttempt,
1630 ) -> anyhow::Result<Option<RoundStateId>> {
1631 self.maintenance_refresh_retry_loop(|part| async move {
1632 info!("Joining round {} for maintenance refresh ({} vtxos)",
1633 attempt.round_seq, part.inputs.len());
1634 Ok(Some(self.join_attempt_interactive(
1635 part, attempt, Some(RoundMovement::Refresh),
1636 ).await?.id()))
1637 }).await.context("failed to join round for maintenance refresh")
1638 }
1639
1640 pub async fn maybe_schedule_maintenance_refresh_delegated(
1648 &self,
1649 ) -> anyhow::Result<Option<RoundStateId>> {
1650 self.maintenance_refresh_retry_loop(|part| async move {
1651 info!("Scheduling delegated maintenance refresh ({} vtxos)", part.inputs.len());
1652 Ok(Some(self.join_next_round_delegated(part, Some(RoundMovement::Refresh)).await?.id()))
1653 }).await.context("failed to schedule delegated maintenance refresh")
1654 }
1655
1656 async fn maintenance_refresh_retry_loop<F, Fut>(
1664 &self,
1665 attempt_refresh: F,
1666 ) -> anyhow::Result<Option<RoundStateId>>
1667 where
1668 F: Fn(RoundParticipation) -> Fut,
1669 Fut: Future<Output = anyhow::Result<Option<RoundStateId>>>,
1670 {
1671 let mut excluded = HashSet::new();
1672 for _ in 0..10 {
1673 let vtxos = self.get_vtxos_to_refresh_with_excluded(excluded.iter().copied()).await?;
1674 match (vtxos.is_empty(), excluded.is_empty()) {
1675 (true, false) => {
1679 warn!("no VTXOs to refresh after exclusions: {:?}", excluded);
1680 bail!("no VTXOs to refresh after excluding: {:?}", excluded);
1681 },
1682 (true, true) => return Ok(None),
1684 (false, _) => {},
1686 }
1687 let part = match self.build_refresh_participation(vtxos).await? {
1688 Some(participation) => participation,
1689 None => return Ok(None),
1690 };
1691
1692 match attempt_refresh(part).await {
1693 Ok(state_id) => return Ok(state_id),
1694 Err(e) => {
1695 let rejected = rejected_vtxos_from_error(&e).into_iter()
1696 .filter(|id| !excluded.contains(id))
1697 .collect::<Vec<_>>();
1698 if rejected.is_empty() {
1699 return Err(e);
1700 }
1701 warn!("Maintenance refresh rejected {} unusable input(s) ({:?}); \
1702 retrying without them", rejected.len(), rejected);
1703 excluded.extend(rejected);
1704 },
1705 }
1706 }
1707 bail!("Maintenance refresh failed after 10 retries");
1708 }
1709
1710 pub async fn maintenance_refresh(&self) -> anyhow::Result<Option<RoundStatus>> {
1722 if self.get_vtxos_to_refresh().await?.is_empty() {
1723 return Ok(None);
1724 }
1725
1726 info!("Waiting for round to perform maintenance refresh...");
1727 let mut events = self.subscribe_round_events().await?;
1728 while let Some(event) = events.next().await {
1729 let event = event.context("error on round event stream")?;
1730 if let RoundEvent::Attempt(a) = event && a.attempt_seq == 0 {
1731 debug!("Round {} started, triggering maintenance refresh", a.round_seq);
1732 let state_id = match self.join_round_for_maintenance_refresh(&a).await? {
1733 Some(id) => id,
1734 None => return Ok(None),
1735 };
1736 let state = self.lock_wait_round_state(state_id).await?
1739 .context("maintenance refresh round state vanished after joining")?;
1740 return Ok(Some(self.drive_round_state(state, &mut events).await?));
1741 }
1742 }
1743 Ok(None)
1744 }
1745
1746 pub async fn sync(&self) {
1752 self.inner.chain.invalidate_caches().await;
1753
1754 futures::join!(
1755 async {
1756 if let Err(e) = self.inner.chain.update_fee_rates(self.inner.config.fallback_fee_rate).await {
1759 warn!("Error updating fee rates: {:#}", e);
1760 }
1761 },
1762 async {
1763 if let Err(e) = self.sync_mailbox().await {
1764 warn!("Error in mailbox sync: {:#}", e);
1765 }
1766 },
1767 async {
1768 if let Err(e) = self.sync_pending_rounds().await {
1769 warn!("Error while trying to progress rounds awaiting confirmations: {:#}", e);
1770 }
1771 },
1772 async {
1773 if let Err(e) = self.sync_pending_lightning_send_vtxos().await {
1774 warn!("Error syncing pending lightning payments: {:#}", e);
1775 }
1776 },
1777 async {
1778 if let Err(e) = self.sync_pending_arkoor_sends().await {
1779 warn!("Error syncing pending arkoor sends: {:#}", e);
1780 }
1781 },
1782 async {
1783 if let Err(e) = self.try_claim_all_lightning_receives(false).await {
1784 warn!("Error claiming pending lightning receives: {:#}", e);
1785 }
1786 },
1787 async {
1788 if let Err(e) = self.sync_pending_boards().await {
1789 warn!("Error syncing pending boards: {:#}", e);
1790 }
1791 },
1792 async {
1793 if let Err(e) = self.sync_pending_offboards().await {
1794 warn!("Error syncing pending offboards: {:#}", e);
1795 }
1796 },
1797 async {
1798 if let Err(e) = self.sync_force_exited_vtxos().await {
1799 warn!("Error scanning for on-chain-exited VTXOs: {:#}", e);
1800 }
1801 },
1802 async {
1803 if let Err(e) = self.catchup_recovery_vtxos().await {
1807 warn!("Failed to catch up recovery VTXOs with server: {:#}", e);
1808 }
1809 }
1810 );
1811 }
1812
1813 pub async fn sync_exits(&self) -> anyhow::Result<()> {
1819 self.exit_mgr().sync(&self).await?;
1820 Ok(())
1821 }
1822
1823 pub async fn progress_exits(&self) -> anyhow::Result<()> {
1828 self.exit_mgr().progress_exits_with_cpfp(&self, None).await?;
1829 Ok(())
1830 }
1831
1832 pub async fn sync_force_exited_vtxos(&self) -> anyhow::Result<()> {
1845 let tip = self.inner.chain.tip().await?;
1847 let mut lock = self.inner.last_force_exit_scan_tip.lock().await;
1848 if *lock == Some(tip) {
1849 return Ok(());
1850 }
1851
1852 let exiting = self.exit_mgr().get_exit_vtxo_ids().await;
1854 let vtxos = self.inner.db.get_vtxos_by_state(&[VtxoStateKind::Spendable]).await?
1855 .into_iter()
1856 .filter(|v| !exiting.contains(&v.vtxo.id()));
1857
1858 let mut checked = FuturesUnordered::new();
1860 for wv in vtxos {
1861 let chain = self.inner.chain.clone();
1862 checked.push(async move {
1863 let txid = wv.vtxo_id().to_point().txid;
1864 let status = chain.tx_status(txid).await;
1865 (wv, status)
1866 });
1867 }
1868
1869 let mut to_exit = Vec::new();
1870 while let Some((vtxo, status)) = futures::StreamExt::next(&mut checked).await {
1871 match status {
1872 Ok(TxStatus::NotFound) => {},
1873 Ok(_) => {
1874 info!("VTXO {} was exited on-chain without us; routing it to a claimable exit",
1875 vtxo.vtxo.id(),
1876 );
1877 to_exit.push(vtxo.vtxo);
1878 },
1879 Err(e) => warn!("Could not check on-chain status of VTXO {}: {:#}",
1880 vtxo.vtxo.id(), e,
1881 ),
1882 }
1883 }
1884
1885 if !to_exit.is_empty() {
1886 self.exit_mgr().start_exit_for_vtxos(&to_exit).await
1887 .context("failed to start exit for on-chain-exited VTXOs")?;
1888
1889 *lock = Some(tip);
1890 self.sync_exits().await
1891 .context("failed to sync exits after starting new ones")?;
1892 } else {
1893 *lock = Some(tip);
1894 }
1895
1896 Ok(())
1897 }
1898
1899 pub async fn dangerous_drop_vtxo(&self, vtxo_id: VtxoId) -> anyhow::Result<()> {
1902 warn!("Drop vtxo {} from the database", vtxo_id);
1903 self.inner.db.remove_vtxo(vtxo_id).await?;
1904 Ok(())
1905 }
1906
1907 pub async fn dangerous_drop_all_vtxos(&self) -> anyhow::Result<()> {
1910 warn!("Dropping all vtxos from the db...");
1911 for vtxo in self.vtxos().await? {
1912 self.inner.db.remove_vtxo(vtxo.id()).await?;
1913 }
1914
1915 self.exit_mgr().dangerous_clear_exit().await?;
1916 Ok(())
1917 }
1918
1919 async fn has_counterparty_risk(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<bool> {
1927 for past_pks in vtxo.past_arkoor_pubkeys() {
1928 let mut owns_any = false;
1929 for past_pk in past_pks {
1930 if self.inner.db.get_public_key_idx(&past_pk).await?.is_some() {
1931 owns_any = true;
1932 break;
1933 }
1934 }
1935 if !owns_any {
1936 return Ok(true);
1937 }
1938 }
1939
1940 let my_clause = self.find_signable_clause(vtxo).await;
1941 Ok(!my_clause.is_some())
1942 }
1943
1944 pub async fn build_refresh_participation<V: VtxoRef>(
1945 &self,
1946 vtxos: impl IntoIterator<Item = V>,
1947 ) -> anyhow::Result<Option<RoundParticipation>> {
1948 self.inner_build_refresh_participation(vtxos, None).await
1949 }
1950
1951 pub async fn build_scheduled_refresh_participation<V: VtxoRef>(
1952 &self,
1953 vtxos: impl IntoIterator<Item = V>,
1954 height: BlockHeight,
1955 ) -> anyhow::Result<Option<RoundParticipation>> {
1956 self.inner_build_refresh_participation(vtxos, Some(height)).await
1957 }
1958
1959 async fn inner_build_refresh_participation<V: VtxoRef>(
1960 &self,
1961 vtxos: impl IntoIterator<Item = V>,
1962 height: Option<BlockHeight>,
1963 ) -> anyhow::Result<Option<RoundParticipation>> {
1964 let (vtxos, total_amount) = {
1965 let iter = vtxos.into_iter();
1966 let size_hint = iter.size_hint();
1967 let mut vtxos = Vec::<Vtxo<Full>>::with_capacity(size_hint.1.unwrap_or(size_hint.0));
1968 let mut amount = Amount::ZERO;
1969 for vref in iter {
1970 let id = vref.vtxo_id();
1975 if vtxos.iter().any(|v| v.id() == id) {
1976 bail!("duplicate VTXO id: {}", id);
1977 }
1978 let vtxo = if let Some(vtxo) = vref.into_full_vtxo() {
1979 vtxo
1980 } else {
1981 self.inner.db.get_full_vtxo(id).await?
1984 .with_context(|| format!("vtxo with id {} not found", id))?
1985 };
1986 amount += vtxo.amount();
1987 vtxos.push(vtxo);
1988 }
1989 (vtxos, amount)
1990 };
1991
1992 if vtxos.is_empty() {
1993 info!("Skipping refresh since no VTXOs are provided.");
1994 return Ok(None);
1995 }
1996 ensure!(total_amount >= VTXO_DUST,
1997 "vtxo amount must be at least {} to participate in a round",
1998 VTXO_DUST,
1999 );
2000
2001 let (_, ark_info) = self.require_server().await?;
2003 let refresh_height = match height {
2004 Some(height) => height,
2005 None => self.inner.chain.tip().await?,
2006 };
2007
2008 let vtxo_fee_infos = vtxos.iter()
2009 .map(|v| VtxoFeeInfo::from_vtxo_and_tip(v, refresh_height));
2010 let fee = ark_info.fees.refresh.calculate(vtxo_fee_infos).context("fee overflowed")?;
2011 let output_amount = validate_and_subtract_fee_min_dust(total_amount, fee, VTXO_DUST)?;
2012
2013 info!("Refreshing {} VTXOs (total amount = {}, fee = {}, output = {}).",
2014 vtxos.len(), total_amount, fee, output_amount,
2015 );
2016 let (user_keypair, _) = self.derive_store_next_keypair().await?;
2017 let req = VtxoRequest {
2018 policy: VtxoPolicy::Pubkey(PubkeyVtxoPolicy { user_pubkey: user_keypair.public_key() }),
2019 amount: output_amount,
2020 };
2021
2022 Ok(Some(RoundParticipation {
2023 inputs: vtxos,
2024 outputs: vec![req],
2025 unblinded_mailbox_id: None,
2026 }))
2027 }
2028
2029 pub async fn refresh_vtxos<V: VtxoRef>(
2034 &self,
2035 vtxos: impl IntoIterator<Item = V>,
2036 ) -> anyhow::Result<Option<RoundStatus>> {
2037 let participation = match self.build_refresh_participation(vtxos).await? {
2038 Some(participation) => participation,
2039 None => return Ok(None),
2040 };
2041
2042 Ok(Some(self.participate_round(participation, Some(RoundMovement::Refresh)).await?))
2043 }
2044
2045 pub async fn refresh_vtxos_delegated<V: VtxoRef>(
2051 &self,
2052 vtxos: impl IntoIterator<Item = V>,
2053 ) -> anyhow::Result<Option<StoredRoundState<Unlocked>>> {
2054 let part = match self.build_refresh_participation(vtxos).await? {
2055 Some(participation) => participation,
2056 None => return Ok(None),
2057 };
2058
2059 Ok(Some(self.join_delegated_round(
2060 part, Some(RoundMovement::Refresh), None,
2061 ).await?))
2062 }
2063
2064 pub async fn refresh_vtxos_scheduled<V: VtxoRef>(
2067 &self,
2068 vtxos: impl IntoIterator<Item = V>,
2069 scheduled_height: BlockHeight,
2070 ) -> anyhow::Result<Option<StoredRoundState<Unlocked>>> {
2071 let part = match self
2072 .build_scheduled_refresh_participation(vtxos, scheduled_height).await?
2073 {
2074 Some(participation) => participation,
2075 None => return Ok(None),
2076 };
2077
2078 Ok(Some(self.join_delegated_round(
2079 part, Some(RoundMovement::Refresh), Some(scheduled_height),
2080 ).await?))
2081 }
2082
2083 pub async fn get_vtxos_to_refresh(&self) -> anyhow::Result<Vec<WalletVtxo>> {
2086 let vtxos = self.spendable_vtxos_with(&RefreshStrategy::should_refresh_if_must(
2087 self,
2088 self.inner.chain.tip().await?,
2089 self.inner.chain.fee_rates().await.fast,
2090 )).await?;
2091 Ok(vtxos)
2092 }
2093
2094 pub async fn get_vtxos_to_refresh_with_excluded<V: VtxoRef>(
2097 &self,
2098 exclude: impl IntoIterator<Item = V>,
2099 ) -> anyhow::Result<Vec<WalletVtxo>> {
2100 let mut vtxos = self.get_vtxos_to_refresh().await?;
2101 for v in exclude.into_iter() {
2102 if let Some(index) = vtxos.iter().position(|vtxo| vtxo.id() == v.vtxo_id()) {
2103 vtxos.swap_remove(index);
2104 }
2105 }
2106 Ok(vtxos)
2107 }
2108
2109 pub async fn get_first_expiring_vtxo_blockheight(
2111 &self,
2112 ) -> anyhow::Result<Option<BlockHeight>> {
2113 Ok(self.spendable_vtxos().await?.iter().map(|v| v.expiry_height()).min())
2114 }
2115
2116 pub async fn get_next_required_refresh_blockheight(
2119 &self,
2120 ) -> anyhow::Result<Option<BlockHeight>> {
2121 let first_expiry = self.get_first_expiring_vtxo_blockheight().await?;
2122 Ok(first_expiry.map(|h| h.saturating_sub(self.inner.config.vtxo_refresh_expiry_threshold)))
2123 }
2124
2125 async fn select_any_vtxos_to_cover(
2127 &self,
2128 amount: Amount,
2129 ) -> anyhow::Result<Vec<WalletVtxo>> {
2130 InputSelection::new().select(self.spendable_vtxos().await?, amount)
2131 }
2132
2133 async fn select_any_vtxos_to_cover_with_fee<F>(
2138 &self,
2139 amount: Amount,
2140 calc_fee: F,
2141 ) -> anyhow::Result<(Vec<WalletVtxo>, Amount)>
2142 where
2143 F: for<'a> Fn(Amount, SelectedFeeInfos<'a>) -> anyhow::Result<Amount>,
2144 {
2145 let tip = self.inner.chain.tip().await?;
2146 InputSelection::new()
2147 .fee_scheme(tip, calc_fee)
2148 .select(self.spendable_vtxos().await?, amount)
2149 }
2150
2151 pub fn start_daemon(&self) -> anyhow::Result<()> {
2160 let mut daemon = self.inner.daemon.lock();
2161 if daemon.is_some() {
2162 warn!("Called Wallet::start_daemon while daemon was already running.");
2163 return Ok(());
2164 }
2165
2166 let handle = crate::daemon::start_daemon(self.clone());
2167 let _ = daemon.insert(handle);
2168
2169 Ok(())
2170 }
2171
2172 pub fn stop_daemon(&self) {
2174 let mut daemon = self.inner.daemon.lock();
2175 if let Some(handle) = daemon.take() {
2176 handle.stop();
2177 }
2178 }
2179
2180 async fn catchup_recovery_vtxos(&self) -> anyhow::Result<()> {
2198 let mut ids = self.inner.db.get_unregistered_vtxo_ids().await?;
2199 if ids.is_empty() {
2200 return Ok(());
2201 }
2202
2203 let in_progress_boards = self.boards_in_progress().await?;
2206 ids.retain(|id| !in_progress_boards.iter().any(|b| b.vtxo_id == *id));
2207 if ids.is_empty() {
2208 return Ok(());
2209 }
2210
2211 let posted = self.post_recovery_vtxo_ids(ids.iter().copied()).await
2216 .context("failed to post recovery vtxo IDs");
2217 let registered = self.register_recovery_vtxo_chains(&ids, posted.is_ok()).await;
2218
2219 match (posted, registered) {
2220 (Ok(()), registered) => registered,
2221 (posted, Ok(())) => posted,
2222 (Err(posted), Err(registered)) => {
2223 Err(registered.context(format!("mailbox post also failed: {:#}", posted)))
2224 },
2225 }
2226 }
2227
2228 async fn register_recovery_vtxo_chains(
2234 &self,
2235 ids: &[VtxoId],
2236 mark_registered: bool,
2237 ) -> anyhow::Result<()> {
2238 const CHUNK_SIZE: usize = 20;
2239 let mut failed = 0;
2240 for chunk_ids in ids.chunks(CHUNK_SIZE) {
2241 let chunk = self.inner.db.get_full_vtxos(chunk_ids).await
2244 .context("failed to load full vtxos for recovery registration")?;
2245 ensure!(chunk.len() == chunk_ids.len(),
2246 "loaded {} full vtxos for {} ids", chunk.len(), chunk_ids.len(),
2247 );
2248
2249 let mut succeeded = Vec::with_capacity(chunk.len());
2250 match self.register_vtxo_transactions_with_server(&chunk).await {
2251 Ok(()) => succeeded.extend(chunk.iter().map(|v| v.id())),
2252 Err(e) => {
2253 debug!("Failed to register chunk of {} vtxo transactions, \
2254 retrying one by one: {:#}", chunk.len(), e,
2255 );
2256 for vtxo in &chunk {
2257 match self.register_vtxo_transactions_with_server(
2258 std::slice::from_ref(vtxo),
2259 ).await {
2260 Ok(()) => succeeded.push(vtxo.id()),
2261 Err(e) => {
2262 error!("Failed to register vtxo {} transactions with server; \
2263 recovery from seed may miss it until registration succeeds: {:#}",
2264 vtxo.id(), e,
2265 );
2266 failed += 1;
2267 },
2268 }
2269 }
2270 },
2271 }
2272 if mark_registered && !succeeded.is_empty() {
2273 self.inner.db.mark_vtxos_registered(&succeeded).await
2274 .context("failed to mark vtxos as registered for recovery")?;
2275 }
2276 }
2277 if failed > 0 {
2278 bail!("failed to register {} of {} vtxo transactions", failed, ids.len());
2279 }
2280 Ok(())
2281 }
2282
2283 pub async fn register_vtxo_transactions_with_server(
2287 &self,
2288 vtxos: &[impl AsRef<Vtxo<Full>>],
2289 ) -> anyhow::Result<()> {
2290 if vtxos.is_empty() {
2291 return Ok(());
2292 }
2293
2294 let (mut srv, _) = self.require_server().await?;
2295 srv.client.register_vtxo_transactions(protos::RegisterVtxoTransactionsRequest {
2296 vtxos: vtxos.iter().map(|v| v.as_ref().serialize()).collect(),
2297 }).await.context("failed to register vtxo transactions")?;
2298
2299 Ok(())
2300 }
2301}
2302
2303fn wrap_server_connect_error(err: ConnectError) -> anyhow::Error {
2304 match err {
2305 ConnectError::CreateEndpoint(CreateEndpointError::NoTransportBackend) => {
2306 anyhow!(MISSING_SERVER_TRANSPORT_HELP)
2307 },
2308 other => anyhow::Error::from(other),
2309 }
2310}
2311
2312impl std::ops::Drop for WalletInner {
2313 fn drop(&mut self) {
2314 if let Some(handle) = self.daemon.lock().take() {
2315 handle.stop();
2316 }
2317 }
2318}
2319
2320#[cfg(test)]
2321mod tests {
2322 use server_rpc::client::CreateEndpointError;
2323
2324 use super::{wrap_server_connect_error, MISSING_SERVER_TRANSPORT_HELP};
2325
2326 #[test]
2327 fn no_transport_connect_error_is_reworded_for_wallet_users() {
2328 let err = wrap_server_connect_error(CreateEndpointError::NoTransportBackend.into());
2329 assert!(err.to_string().contains(MISSING_SERVER_TRANSPORT_HELP));
2330 assert!(err.to_string().contains("feature `bark-wallet/native` or `bark-wallet/wasm-web`"));
2331 }
2332}