1#[cfg(all(any(target_os = "android", target_os = "ios"), feature = "tls-native-roots"))]
368compile_error!("feature `tls-native-roots` can't be used on Android or iOS, use `tls-webpki-roots` instead");
369
370pub extern crate ark;
371
372pub extern crate bip39;
373pub extern crate lightning_invoice;
374pub extern crate lnurl as lnurllib;
375
376#[macro_use] extern crate anyhow;
377#[macro_use] extern crate async_trait;
378#[macro_use] extern crate serde;
379
380pub mod actions;
381pub mod chain;
382pub mod exit;
383pub use bark_common::fs_perms;
384pub use bark_common::secret;
385pub mod movement;
386pub mod onchain;
387pub mod payment_request;
388pub mod persist;
389pub mod round;
390pub mod subsystem;
391pub mod vtxo;
392
393pub mod lock_manager;
394
395mod arkoor;
396mod board;
397mod config;
398mod daemon;
399mod fees;
400mod lightning;
401mod mailbox;
402mod notification;
403mod offboard;
404#[cfg(feature = "socks5-proxy")]
405mod proxy;
406mod recovery;
407mod psbtext;
408mod utils;
409
410pub use self::arkoor::{ArkoorCreateResult, ArkoorAddressError};
411pub use self::payment_request::{
412 AvailablePaymentMethod, PaymentInitOutput, PaymentMethodParsingError, PaymentRequest,
413};
414pub use self::config::{BarkNetwork, Config};
415pub use self::daemon::{tip_watcher, DaemonHandle};
416pub use self::fees::FeeEstimate;
417pub use self::notification::{WalletNotification, NotificationStream};
418pub use self::recovery::{RecoveryReport, RecoveryReportEntry, RecoveryStatus};
419pub use self::vtxo::WalletVtxo;
420
421use std::borrow::Cow;
422use std::collections::HashSet;
423use std::path::PathBuf;
424use std::sync::Arc;
425use std::time::Duration;
426
427use anyhow::{bail, Context};
428use bip39::Mnemonic;
429use bitcoin::{Amount, Network, OutPoint};
430use bitcoin::bip32::{self, ChildNumber, Fingerprint};
431use bitcoin::secp256k1::{self, Keypair, PublicKey};
432use futures::stream::FuturesUnordered;
433use log::{debug, error, info, trace, warn};
434use tokio_stream::StreamExt;
435
436use ark::{ArkInfo, ProtocolEncoding, Vtxo, VtxoId, VtxoPolicy, VtxoRequest};
437use ark::address::VtxoDelivery;
438use ark::fees::{validate_and_subtract_fee_min_dust, VtxoFeeInfo};
439use ark::rounds::{RoundAttempt, RoundEvent};
440use ark::vtxo::{Full, PubkeyVtxoPolicy, VtxoRef, VTXO_DUST};
441use ark::vtxo::policy::signing::VtxoSigner;
442use bitcoin_ext::{BlockDelta, BlockHeight, TxStatus};
443use server_rpc::{protos, ServerConnection};
444use server_rpc::client::{ConnectError, CreateEndpointError};
445
446use crate::chain::{ChainSource, ChainSourceSpec};
447use crate::exit::Exit;
448use crate::lock_manager::LockManager;
449use crate::movement::{Movement, MovementId, PaymentMethod};
450use crate::movement::manager::MovementManager;
451use crate::notification::NotificationDispatch;
452use crate::onchain::{OnchainWalletTrait, Utxo};
453use crate::persist::BarkPersister;
454use crate::persist::models::{RoundStateId, StoredRoundState, Unlocked};
455#[cfg(feature = "socks5-proxy")]
456use crate::proxy::proxy_for_url;
457use crate::round::{RoundParticipation, RoundSecretNonces, RoundStatus};
458use crate::subsystem::RoundMovement;
459use crate::utils::rejected_vtxos_from_error;
460use crate::vtxo::{FilterVtxos, RefreshStrategy, VtxoFilter, VtxoStateKind, VtxoValidationError};
461use crate::vtxo::selection::{InputSelection, SelectedFeeInfos};
462
463#[cfg(all(feature = "wasm-web", feature = "socks5-proxy"))]
464compile_error!("features `wasm-web` does not support feature `socks5-proxy");
465
466#[cfg(all(feature = "wasm-web", feature = "bitcoind-rpc"))]
467compile_error!("`wasm-web` does not support the `bitcoind-rpc` feature");
468
469const BARK_PURPOSE_INDEX: u32 = 350;
471const VTXO_KEYS_INDEX: u32 = 0;
473const MAILBOX_KEY_INDEX: u32 = 1;
475const RECOVERY_MAILBOX_KEY_INDEX: u32 = 2;
477const MISSING_SERVER_TRANSPORT_HELP: &str =
478 "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.";
479
480const SUBSCRIBE_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 60);
482
483lazy_static::lazy_static! {
484 static ref SECP: secp256k1::Secp256k1<secp256k1::All> = secp256k1::Secp256k1::new();
486}
487
488const MAX_NB_ROUND_NONCES: usize = 16;
491
492const MIN_MAINNET_VTXO_EXIT_DELTA: BlockDelta = 96;
497
498fn check_ark_info_safe(ark_info: &ArkInfo, vtxo_exit_margin: BlockDelta) -> anyhow::Result<()> {
503 let required = ark_info.required_board_confirmations;
504 let margin = vtxo_exit_margin as usize;
505 let min_safe = required.saturating_add(margin);
506 ensure!(ark_info.vtxo_exit_delta > 0,
507 "server-advertised vtxo_exit_delta is 0; refusing to connect",
508 );
509 ensure!(ark_info.nb_round_nonces > 0,
510 "server-advertised nb_round_nonces is 0; refusing to connect",
511 );
512 if ark_info.network == Network::Bitcoin {
513 ensure!((ark_info.vtxo_lifetime as usize) > min_safe,
516 "server-advertised vtxo_lifetime {} is unsafe (minimum > {} = \
517 required_board_confirmations {} + vtxo_exit_margin {}); refusing to connect",
518 ark_info.vtxo_lifetime, min_safe, required, margin,
519 );
520 ensure!(ark_info.vtxo_exit_delta >= MIN_MAINNET_VTXO_EXIT_DELTA,
521 "server-advertised vtxo_exit_delta {} is below the mainnet minimum of {} \
522 blocks; refusing to connect",
523 ark_info.vtxo_exit_delta, MIN_MAINNET_VTXO_EXIT_DELTA,
524 );
525 ensure!(ark_info.nb_round_nonces <= MAX_NB_ROUND_NONCES,
526 "server-advertised nb_round_nonces {} exceeds cap {}; refusing to connect",
527 ark_info.nb_round_nonces, MAX_NB_ROUND_NONCES,
528 );
529 }
530 Ok(())
531}
532
533fn log_server_pubkey_changed_error(expected: PublicKey, got: PublicKey) {
539 error!(
540 "
541Server public key has changed!
542
543The Ark server's public key is different from the one stored when this
544wallet was created. This typically happens when:
545
546 - The server operator has rotated their keys
547 - You are connecting to a different server
548 - The server has been replaced
549
550For safety, this wallet will not connect to the server until you
551resolve this. You can recover your funds on-chain by doing an emergency exit.
552
553This will exit your VTXOs to on-chain Bitcoin without needing the server's cooperation.
554
555Expected: {expected}
556Got: {got}")
557}
558
559fn log_server_mailbox_pubkey_changed_error(expected: PublicKey, got: PublicKey) {
561 error!(
562 "
563Server mailbox public key has changed!
564
565The Ark server's mailbox public key is different from the one stored when this
566wallet was created. This typically happens when:
567
568 - The server operator has rotated their keys
569 - You are connecting to a different server
570 - The server has been replaced
571
572For safety, this wallet will not connect to the server until you resolve this.
573
574Unlike a server pubkey change, your VTXOs are not at risk - the mailbox pubkey
575only affects address receive semantics. Any Ark addresses you previously
576shared will stop receiving new payments; you will need to share new addresses
577after reconnecting.
578
579Expected: {expected}
580Got: {got}")
581}
582
583#[derive(Debug, Clone)]
585pub struct LightningReceiveBalance {
586 pub total: Amount,
588 pub claimable: Amount,
590}
591
592#[derive(Debug, Clone)]
594pub struct Balance {
595 pub spendable: Amount,
597 pub pending_lightning_send: Amount,
599 pub claimable_lightning_receive: Amount,
601 pub pending_in_round: Amount,
603 pub pending_exit: Option<Amount>,
609 pub pending_board: Amount,
611}
612
613pub struct UtxoInfo {
614 pub outpoint: OutPoint,
615 pub amount: Amount,
616 pub confirmation_height: Option<u32>,
617}
618
619impl From<Utxo> for UtxoInfo {
620 fn from(value: Utxo) -> Self {
621 match value {
622 Utxo::Local(o) => UtxoInfo {
623 outpoint: o.outpoint,
624 amount: o.amount,
625 confirmation_height: o.confirmation_height,
626 },
627 Utxo::Exit(e) => UtxoInfo {
628 outpoint: e.vtxo.point(),
629 amount: e.vtxo.amount(),
630 confirmation_height: Some(e.height),
631 },
632 }
633 }
634}
635
636pub struct OffchainBalance {
639 pub available: Amount,
641 pub pending_in_round: Amount,
643 pub pending_exit: Amount,
646}
647
648#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
650pub struct WalletProperties {
651 pub network: Network,
655
656 pub fingerprint: Fingerprint,
660
661 pub server_pubkey: Option<PublicKey>,
668
669 pub server_mailbox_pubkey: Option<PublicKey>,
677}
678
679pub struct WalletSeed {
685 master: bip32::Xpriv,
686 vtxo: bip32::Xpriv,
687}
688
689impl WalletSeed {
690 pub fn new_from_seed(network: Network, seed: &[u8; 64]) -> Self {
692 let bark_path = [ChildNumber::from_hardened_idx(BARK_PURPOSE_INDEX).unwrap()];
693 let master = bip32::Xpriv::new_master(network, seed)
694 .expect("invalid seed")
695 .derive_priv(&SECP, &bark_path)
696 .expect("purpose is valid");
697
698 let vtxo_path = [ChildNumber::from_hardened_idx(VTXO_KEYS_INDEX).unwrap()];
699 let vtxo = master.derive_priv(&SECP, &vtxo_path)
700 .expect("vtxo path is valid");
701
702 Self { master, vtxo }
703 }
704
705 pub fn new_from_mnemonic(network: Network, mnemonic: &Mnemonic) -> Self {
707 Self::new_from_seed(network, &mnemonic.to_seed(""))
708 }
709
710 pub fn fingerprint(&self) -> Fingerprint {
711 self.master.fingerprint(&SECP)
712 }
713
714 fn derive_vtxo_keypair(&self, idx: u32) -> Keypair {
715 self.vtxo.derive_priv(&SECP, &[idx.into()]).unwrap().to_keypair(&SECP)
716 }
717
718 fn to_mailbox_keypair(&self) -> Keypair {
719 let mailbox_path = [ChildNumber::from_hardened_idx(MAILBOX_KEY_INDEX).unwrap()];
720 self.master.derive_priv(&SECP, &mailbox_path).unwrap().to_keypair(&SECP)
721 }
722
723 fn to_recovery_mailbox_keypair(&self) -> Keypair {
724 let mailbox_path = [ChildNumber::from_hardened_idx(RECOVERY_MAILBOX_KEY_INDEX).unwrap()];
725 self.master.derive_priv(&SECP, &mailbox_path).unwrap().to_keypair(&SECP)
726 }
727}
728
729pub struct OpenWalletArgs {
731 pub run_daemon: bool,
737
738 pub datadir: Option<PathBuf>,
748
749 pub persister: Option<Arc<dyn BarkPersister>>,
753
754 pub lock_manager: Option<Box<dyn LockManager>>,
761
762 pub onchain: Option<Arc<tokio::sync::RwLock<dyn OnchainWalletTrait>>>,
766
767 pub create_if_not_exists: bool,
771
772 pub create_without_server: bool,
776
777 pub skip_recovery: bool,
783
784 pub on_recovery_finished: Option<Box<dyn FnOnce(RecoveryStatus) + Send + Sync>>,
791}
792
793impl Default for OpenWalletArgs {
794 fn default() -> Self {
795 Self {
796 run_daemon: true,
797 onchain: None,
798 datadir: None,
799 persister: None,
800 lock_manager: None,
801 create_if_not_exists: true,
802 create_without_server: false,
803 skip_recovery: false,
804 on_recovery_finished: None,
805 }
806 }
807}
808
809struct WalletInner {
810 chain: Arc<ChainSource>,
812
813 exit: Exit,
815
816 movements: Arc<MovementManager>,
818
819 notifications: NotificationDispatch,
821
822 config: Config,
824
825 db: Arc<dyn BarkPersister>,
827
828 lock_manager: Box<dyn LockManager>,
832
833 seed: WalletSeed,
835
836 server: tokio::sync::OnceCell<ServerConnection>,
843
844 onchain: Option<Arc<tokio::sync::RwLock<dyn OnchainWalletTrait>>>,
849
850 daemon: parking_lot::Mutex<Option<DaemonHandle>>,
852
853 last_force_exit_scan_tip: tokio::sync::Mutex<Option<BlockHeight>>,
857
858 pub(crate) round_secret_nonces: RoundSecretNonces,
861}
862
863#[derive(Clone)]
964pub struct Wallet {
965 inner: Arc<WalletInner>,
966}
967
968impl Wallet {
969 pub async fn network(&self) -> anyhow::Result<Network> {
970 Ok(self.properties().await?.network)
971 }
972
973 pub fn chain(&self) -> &Arc<ChainSource> {
975 &self.inner.chain
976 }
977
978 pub fn exit_mgr(&self) -> &Exit {
980 &self.inner.exit
981 }
982
983 pub fn movements_mgr(&self) -> &MovementManager {
985 &self.inner.movements
986 }
987
988 pub async fn peek_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
991 let last_revealed = self.inner.db.get_last_vtxo_key_index().await?;
992
993 let index = last_revealed.map(|i| i + 1).unwrap_or(u32::MIN);
994 let keypair = self.inner.seed.derive_vtxo_keypair(index);
995
996 Ok((keypair, index))
997 }
998
999 pub async fn derive_store_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
1002 let (keypair, index) = self.peek_next_keypair().await?;
1003 self.inner.db.store_vtxo_key(index, keypair.public_key()).await?;
1004 Ok((keypair, index))
1005 }
1006
1007 #[deprecated(note = "use peek_keypair instead")]
1008 pub async fn peak_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
1009 self.peek_keypair(index).await
1010 }
1011
1012 pub async fn peek_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
1026 let keypair = self.inner.seed.derive_vtxo_keypair(index);
1027 if self.inner.db.get_public_key_idx(&keypair.public_key()).await?.is_some() {
1028 Ok(keypair)
1029 } else {
1030 bail!("VTXO key {} does not exist, please derive it first", index)
1031 }
1032 }
1033
1034
1035 pub async fn pubkey_keypair(&self, public_key: &PublicKey) -> anyhow::Result<Option<(u32, Keypair)>> {
1047 if let Some(index) = self.inner.db.get_public_key_idx(&public_key).await? {
1048 Ok(Some((index, self.inner.seed.derive_vtxo_keypair(index))))
1049 } else {
1050 Ok(None)
1051 }
1052 }
1053
1054 pub async fn get_vtxo_key(&self, vtxo: impl VtxoRef) -> anyhow::Result<Keypair> {
1065 let bare_vtxo = match vtxo.as_bare_vtxo() {
1066 Some(bare) => bare,
1067 None => Cow::Owned(self.get_vtxo_by_id(vtxo.vtxo_id()).await?.vtxo),
1068 };
1069 let pubkey = self.find_signable_clause(&bare_vtxo).await
1070 .context("VTXO is not signable by wallet")?
1071 .pubkey();
1072 let idx = self.inner.db.get_public_key_idx(&pubkey).await?
1073 .context("VTXO key not found")?;
1074 Ok(self.inner.seed.derive_vtxo_keypair(idx))
1075 }
1076
1077 #[deprecated(note = "use peek_address instead")]
1078 pub async fn peak_address(&self, index: u32) -> anyhow::Result<ark::Address> {
1079 self.peek_address(index).await
1080 }
1081
1082 pub async fn peek_address(&self, index: u32) -> anyhow::Result<ark::Address> {
1086 let properties = self.properties().await?;
1087 let network = properties.network;
1088 let keypair = self.peek_keypair(index).await?;
1089 let mailbox = self.mailbox_identifier();
1090
1091
1092 let (server_pubkey, mailbox_pubkey) =
1093 if let (Some(spk), Some(mpk)) = (properties.server_pubkey, properties.server_mailbox_pubkey) {
1094 (spk, mpk)
1095 } else {
1096 let (_, ark_info) = self.require_server().await?;
1097 (ark_info.server_pubkey, ark_info.mailbox_pubkey)
1098 };
1099
1100 Ok(ark::Address::builder()
1101 .testnet(network != bitcoin::Network::Bitcoin)
1102 .server_pubkey(server_pubkey)
1103 .pubkey_policy(keypair.public_key())
1104 .mailbox(mailbox_pubkey, mailbox, &keypair)
1105 .context("failed to assign mailbox")?
1106 .into_address()
1107 .context("failed to build address")?)
1108 }
1109
1110 pub async fn new_address_with_index(&self) -> anyhow::Result<(ark::Address, u32)> {
1114 let (_, index) = self.derive_store_next_keypair().await?;
1115 let addr = self.peek_address(index).await?;
1116 Ok((addr, index))
1117 }
1118
1119 pub async fn new_address(&self) -> anyhow::Result<ark::Address> {
1121 let (addr, _) = self.new_address_with_index().await?;
1122 Ok(addr)
1123 }
1124
1125 pub async fn sign_message(
1133 &self,
1134 message: &[u8],
1135 address: &ark::Address,
1136 ) -> anyhow::Result<Option<secp256k1::schnorr::Signature>> {
1137 let pubkey = address.policy().user_pubkey();
1138 let Some((_, keypair)) = self.pubkey_keypair(&pubkey).await? else {
1139 return Ok(None);
1140 };
1141 Ok(Some(ark::message::sign(&keypair, message)))
1142 }
1143
1144 pub async fn create(
1153 network: Network,
1154 seed: &WalletSeed,
1155 config: &Config,
1156 db: &dyn BarkPersister,
1157 lock_manager: &dyn LockManager,
1158 allow_unreachable_server: bool,
1159 ) -> anyhow::Result<()> {
1160 trace!("Config: {:?}", config);
1161
1162 let wallet_fingerprint = seed.fingerprint();
1163
1164 let create_guard = lock_manager.lock(
1169 &format!("{}.create", wallet_fingerprint),
1170 Duration::from_secs(5),
1171 ).await.context("wallet initialization already in progress")?;
1172
1173 if let Some(existing) = db.read_properties().await? {
1174 trace!("Existing config: {:?}", existing);
1175 bail!("cannot overwrite already existing config")
1176 }
1177
1178 let (server_pubkey, mailbox_pubkey) = match Self::connect_to_server(&config, network).await {
1184 Ok(conn) => {
1185 let ark_info = conn.ark_info().await;
1186 match check_ark_info_safe(&ark_info, config.vtxo_exit_margin) {
1187 Ok(()) => (Some(ark_info.server_pubkey), Some(ark_info.mailbox_pubkey)),
1188 Err(err) if allow_unreachable_server => {
1189 warn!("server-advertised ArkInfo is unsafe, \
1190 treating as unavailable: {:#}", err);
1191 (None, None)
1192 },
1193 Err(err) => return Err(err),
1194 }
1195 },
1196 Err(_) if allow_unreachable_server => (None, None),
1197 Err(err) => {
1198 bail!("Failed to connect to provided server: {:#}", err);
1199 },
1200 };
1201
1202 let properties = WalletProperties {
1203 network,
1204 fingerprint: wallet_fingerprint,
1205 server_pubkey,
1206 server_mailbox_pubkey: mailbox_pubkey,
1207 };
1208
1209 db.init_wallet(&properties).await.context("cannot init wallet in the database")?;
1211 info!("Created wallet with fingerprint: {}", wallet_fingerprint);
1212 if let Some(pk) = server_pubkey {
1213 info!("Stored server pubkey: {}", pk);
1214 }
1215
1216 drop(create_guard);
1219
1220 Ok(())
1221 }
1222
1223 pub async fn open(
1225 network: Network,
1226 seed: WalletSeed,
1227 config: Config,
1228 args: OpenWalletArgs,
1229 ) -> anyhow::Result<Wallet> {
1230 if !(1..=3).contains(&config.change_vtxo_split_factor) {
1231 bail!("change_vtxo_split_factor must be 1, 2 or 3, got {}",
1232 config.change_vtxo_split_factor,
1233 );
1234 }
1235
1236 let fingerprint = seed.fingerprint();
1237 let lock_manager = if let Some(lm) = args.lock_manager {
1238 lm
1239 } else {
1240 crate::lock_manager::platform_default(args.datadir.as_ref(), Some(fingerprint))
1241 .context("failed to instantiate platform default lock manager")?
1242 };
1243
1244 let db = if let Some(db) = args.persister {
1245 db
1246 } else {
1247 if let Some(ref datadir) = args.datadir {
1248 #[cfg(not(target_arch = "wasm32"))]
1249 if !datadir.exists() && args.create_if_not_exists {
1250 tokio::fs::create_dir_all(datadir).await.with_context(|| format!(
1251 "failed to create datadir at {}", datadir.display(),
1252 ))?;
1253 }
1254 }
1255 crate::persist::platform_default(args.datadir.as_ref(), Some(fingerprint)).await
1256 .context("failed to instantiate platform default persister")?
1257 };
1258
1259 let mut created_now = false;
1260 let properties = if let Some(p) = db.read_properties().await? {
1261 p
1262 } else if args.create_if_not_exists {
1263 Self::create(
1264 network, &seed, &config, &*db, &*lock_manager, args.create_without_server,
1265 ).await.context("error creating new wallet")?;
1266 created_now = true;
1267 db.read_properties().await?
1268 .context("create failed: no wallet properties after Wallet::create was called")?
1269 } else {
1270 bail!("wallet does not exist; use Wallet::create or \
1271 set options.create_if_not_exists to true");
1272 };
1273
1274 if properties.fingerprint != fingerprint {
1275 bail!("incorrect mnemonic")
1276 }
1277
1278 let chain_source = if let Some(ref url) = config.esplora_address {
1279 ChainSourceSpec::Esplora {
1280 url: url.clone(),
1281 }
1282 } else if let Some(ref url) = config.bitcoind_address {
1283 let auth = if let Some(ref c) = config.bitcoind_cookiefile {
1284 bitcoin_ext::rpc::Auth::CookieFile(c.clone())
1285 } else {
1286 bitcoin_ext::rpc::Auth::UserPass(
1287 config.bitcoind_user.clone().context("need bitcoind auth config")?,
1288 config.bitcoind_pass.as_ref().context("need bitcoind auth config")?
1289 .leak_ref().clone(),
1290 )
1291 };
1292 ChainSourceSpec::Bitcoind {
1293 url: url.clone(),
1294 auth,
1295 zmq: config.bitcoind_zmq_address.clone(),
1296 }
1297 } else {
1298 bail!("Need to either provide esplora or bitcoind info");
1299 };
1300
1301 #[cfg(feature = "socks5-proxy")]
1302 let chain_proxy = proxy_for_url(&config.socks5_proxy, chain_source.url())?;
1303 let chain_source_client = ChainSource::new(
1304 chain_source, properties.network, config.fallback_fee_rate,
1305 #[cfg(feature = "socks5-proxy")] chain_proxy.as_deref(),
1306 ).await?;
1307 let chain = Arc::new(chain_source_client);
1308 chain.require_version().await
1309 .context("provided chain source doesn't meet version requirement")?;
1310
1311 let server = tokio::sync::OnceCell::new();
1312
1313 let notifications = NotificationDispatch::new();
1314 let movements = Arc::new(MovementManager::new(db.clone(), notifications.clone()));
1315 let exit = Exit::new(db.clone(), chain.clone(), movements.clone()).await?;
1316
1317 let onchain = args.onchain;
1318 let ret = Wallet { inner: Arc::new(WalletInner {
1319 config, db, lock_manager, seed, exit, movements, notifications, server, chain,
1320 onchain,
1321 daemon: parking_lot::Mutex::new(None),
1322 last_force_exit_scan_tip: tokio::sync::Mutex::new(None),
1323 round_secret_nonces: RoundSecretNonces::new(),
1324 })};
1325
1326 ret.inner.exit.load().await
1327 .context("error loading exit system after opening wallet")?;
1328
1329 let recovery = if !created_now {
1330 RecoveryStatus::NotRun
1331 } else if args.skip_recovery {
1332 info!("Seed-based wallet recovery explicitly skipped");
1333 RecoveryStatus::NotRun
1334 } else {
1335 match ret.recover_from_mailbox().await {
1340 Ok(report) => RecoveryStatus::Completed(report),
1341 Err(e) => {
1342 error!("VTXO recovery from the recovery mailbox failed; funds may be \
1343 missing from this wallet until recovery succeeds: {:#}", e);
1344 RecoveryStatus::Failed(e)
1345 },
1346 }
1347 };
1348
1349 if args.run_daemon {
1350 ret.start_daemon()
1351 .context("failed to start daemon after opening wallet")?;
1352 }
1353
1354 if let Some(callback) = args.on_recovery_finished {
1356 callback(recovery);
1357 }
1358
1359 Ok(ret)
1360 }
1361
1362 pub fn config(&self) -> &Config {
1364 &self.inner.config
1365 }
1366
1367 pub async fn properties(&self) -> anyhow::Result<WalletProperties> {
1369 let properties = self.inner.db.read_properties().await?.context("Wallet is not initialised")?;
1370 Ok(properties)
1371 }
1372
1373 pub fn fingerprint(&self) -> Fingerprint {
1375 self.inner.seed.fingerprint()
1376 }
1377
1378 async fn connect_to_server(
1379 config: &Config,
1380 network: Network,
1381 ) -> anyhow::Result<ServerConnection> {
1382 let server_address = crate::utils::url_with_default_https_scheme(&config.server_address);
1383 let mut builder = ServerConnection::builder()
1384 .address(&server_address)
1385 .network(network);
1386
1387 #[cfg(feature = "socks5-proxy")]
1388 if let Some(proxy) = proxy_for_url(&config.socks5_proxy, &server_address)? {
1389 builder = builder.proxy(&proxy)
1390 }
1391
1392 #[allow(deprecated)]
1393 {
1394 if let Some(ref token) = config.server_access_token {
1395 builder = builder.access_token(token);
1396 }
1397 }
1398
1399 if let Some(ref ua) = config.user_agent {
1400 builder = builder.user_agent(ua);
1401 }
1402
1403 builder.connect().await.map_err(wrap_server_connect_error)
1404 .context("Failed to connect to Ark server")
1405 }
1406
1407 async fn require_server(&self) -> anyhow::Result<(ServerConnection, ArkInfo)> {
1408 let conn = self.inner.server.get_or_try_init(|| async {
1412 let network = self.properties().await?.network;
1413 Self::connect_to_server(&self.inner.config, network).await
1414 .context("You should be connected to Ark server to perform this action")
1415 }).await?.clone();
1416
1417 let ark_info = conn.ark_info().await;
1418 check_ark_info_safe(&ark_info, self.inner.config.vtxo_exit_margin)?;
1419 self.check_and_store_server_keys(&ark_info).await?;
1420
1421 Ok((conn, ark_info))
1422 }
1423
1424 pub async fn refresh_server(&self) -> anyhow::Result<()> {
1425 let srv = self.inner.server.get_or_try_init(|| async {
1431 let properties = self.properties().await?;
1432 Self::connect_to_server(&self.inner.config, properties.network).await
1433 .map_err(anyhow::Error::from)
1434 }).await?;
1435
1436 srv.check_connection().await?;
1437 let ark_info = srv.ark_info().await;
1438 ark_info.fees.validate().context("invalid fee schedule")?;
1439 check_ark_info_safe(&ark_info, self.inner.config.vtxo_exit_margin)?;
1440 self.check_and_store_server_keys(&ark_info).await?;
1441
1442 Ok(())
1443 }
1444
1445 pub fn onchain(&self) -> Option<Arc<tokio::sync::RwLock<dyn OnchainWalletTrait>>> {
1447 self.inner.onchain.clone()
1448 }
1449
1450 pub async fn sync_onchain(&self) -> anyhow::Result<()> {
1452 if let Some(onchain) = self.inner.onchain.as_ref() {
1453 onchain.write().await.sync(self.chain()).await?;
1454 }
1455 Ok(())
1456 }
1457
1458 async fn check_and_store_server_keys(&self, ark_info: &ArkInfo) -> anyhow::Result<()> {
1465 let properties = self.properties().await?;
1466
1467 if let Some(stored_pubkey) = properties.server_pubkey {
1468 if stored_pubkey != ark_info.server_pubkey {
1469 log_server_pubkey_changed_error(stored_pubkey, ark_info.server_pubkey);
1470 bail!("Server public key has changed. You should exit all your VTXOs!");
1471 }
1472 } else {
1473 self.inner.db.set_server_pubkey(ark_info.server_pubkey).await?;
1474 info!("Stored server pubkey for existing wallet: {}", ark_info.server_pubkey);
1475 }
1476
1477 if let Some(stored_mailbox_pubkey) = properties.server_mailbox_pubkey {
1478 if stored_mailbox_pubkey != ark_info.mailbox_pubkey {
1479 log_server_mailbox_pubkey_changed_error(stored_mailbox_pubkey, ark_info.mailbox_pubkey);
1480 bail!("Server mailbox public key has changed.");
1481 }
1482 } else {
1483 self.inner.db.set_server_mailbox_pubkey(ark_info.mailbox_pubkey).await?;
1484 info!("Stored server mailbox pubkey for existing wallet: {}", ark_info.mailbox_pubkey);
1485 }
1486
1487 Ok(())
1488 }
1489
1490 pub async fn ark_info(&self) -> anyhow::Result<Option<ArkInfo>> {
1492 match self.inner.server.get() {
1493 Some(srv) => Ok(Some(srv.ark_info().await)),
1494 None => Ok(None),
1495 }
1496 }
1497
1498 pub async fn require_ark_info(&self) -> anyhow::Result<ArkInfo> {
1504 let (_, ark_info) = self.require_server().await?;
1505 Ok(ark_info)
1506 }
1507
1508 pub async fn balance(&self) -> anyhow::Result<Balance> {
1512 let vtxos = self.vtxos().await?;
1513
1514 let spendable = {
1515 let mut v = vtxos.iter().collect();
1516 VtxoStateKind::Spendable.filter_vtxos(&mut v).await?;
1517 v.into_iter().map(|v| v.amount()).sum::<Amount>()
1518 };
1519
1520 let pending_lightning_send = self.pending_lightning_send_vtxos().await?.iter()
1521 .map(|v| v.amount())
1522 .sum::<Amount>();
1523
1524 let claimable_lightning_receive = self.claimable_lightning_receive_balance().await?;
1525
1526 let pending_board = self.pending_board_vtxos().await?.iter()
1527 .map(|v| v.amount())
1528 .sum::<Amount>();
1529
1530 let pending_in_round = self.pending_round_balance().await?;
1531
1532 let pending_exit = self.exit_mgr().try_pending_total();
1533
1534 Ok(Balance {
1535 spendable,
1536 pending_in_round,
1537 pending_lightning_send,
1538 claimable_lightning_receive,
1539 pending_exit,
1540 pending_board,
1541 })
1542 }
1543
1544 pub async fn validate_vtxo(&self, vtxo: &Vtxo<Full>) -> Result<(), VtxoValidationError> {
1546 let tx = self.inner.chain.get_tx(&vtxo.chain_anchor().txid).await
1547 .map_err(VtxoValidationError::Chain)?
1548 .ok_or(VtxoValidationError::AnchorNotFound)?;
1549
1550 vtxo.validate(&tx).map_err(VtxoValidationError::Invalid)
1551 }
1552
1553 pub async fn import_vtxo(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<()> {
1563 if self.inner.db.get_wallet_vtxo(vtxo.id()).await?.is_some() {
1564 info!("VTXO {} already exists in wallet, skipping import", vtxo.id());
1565 return Ok(());
1566 }
1567
1568 self.validate_vtxo(vtxo).await.context("VTXO validation failed")?;
1569
1570 if self.find_signable_clause(vtxo).await.is_none() {
1571 bail!("VTXO {} is not owned by this wallet (no signable clause found)", vtxo.id());
1572 }
1573
1574 let current_height = self.inner.chain.tip().await?;
1575 if vtxo.expiry_height() <= current_height {
1576 bail!("Vtxo {} has expired", vtxo.id());
1577 }
1578
1579 self.store_spendable_vtxos([vtxo]).await.context("failed to store imported VTXO")?;
1580
1581 info!("Successfully imported VTXO {}", vtxo.id());
1582 Ok(())
1583 }
1584
1585 pub async fn get_vtxo_by_id(&self, vtxo_id: VtxoId) -> anyhow::Result<WalletVtxo> {
1587 let vtxo = self.inner.db.get_wallet_vtxo(vtxo_id).await
1588 .with_context(|| format!("Error when querying vtxo {} in database", vtxo_id))?
1589 .with_context(|| format!("The VTXO with id {} cannot be found", vtxo_id))?;
1590 Ok(vtxo)
1591 }
1592
1593 pub async fn get_full_vtxo(&self, vtxo_id: VtxoId) -> anyhow::Result<Vtxo<Full>> {
1601 self.inner.db.get_full_vtxo(vtxo_id).await
1602 .with_context(|| format!("Error when querying full vtxo {} in database", vtxo_id))?
1603 .with_context(|| format!("The VTXO with id {} cannot be found", vtxo_id))
1604 }
1605
1606 pub async fn get_full_vtxos<V: VtxoRef>(
1608 &self,
1609 vtxos: impl IntoIterator<Item = V>,
1610 ) -> anyhow::Result<Vec<Vtxo<Full>>> {
1611 let ids = vtxos.into_iter().map(|v| v.vtxo_id()).collect::<Vec<_>>();
1612 self.inner.db.get_full_vtxos(&ids).await
1613 .with_context(||
1614 format!("Error when querying full vtxos in database with IDs: {:?}", ids)
1615 )
1616 }
1617
1618 #[deprecated(since="0.1.0-beta.5", note = "Use Wallet::history instead")]
1620 pub async fn movements(&self) -> anyhow::Result<Vec<Movement>> {
1621 self.history().await
1622 }
1623
1624 pub async fn history(&self) -> anyhow::Result<Vec<Movement>> {
1626 Ok(self.inner.db.get_all_movements().await?)
1627 }
1628
1629 pub async fn update_history_metadata(
1649 &self,
1650 movement_id: MovementId,
1651 patch: &serde_json::Value,
1652 ) -> anyhow::Result<()> {
1653 self.inner.movements.patch_metadata(movement_id, patch).await?;
1654 Ok(())
1655 }
1656
1657 pub async fn history_by_payment_method(
1659 &self,
1660 payment_method: &PaymentMethod,
1661 ) -> anyhow::Result<Vec<Movement>> {
1662 let mut ret = self.inner.db.get_movements_by_payment_method(payment_method).await?;
1663 ret.sort_by_key(|m| m.id);
1664 Ok(ret)
1665 }
1666
1667 pub async fn all_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1669 Ok(self.inner.db.get_all_vtxos().await?)
1670 }
1671
1672 pub async fn vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1674 Ok(self.inner.db.get_vtxos_by_state(&VtxoStateKind::UNSPENT_STATES).await?)
1675 }
1676
1677 pub async fn vtxos_with(&self, filter: &impl FilterVtxos) -> anyhow::Result<Vec<WalletVtxo>> {
1679 let mut vtxos = self.vtxos().await?;
1680 filter.filter_vtxos(&mut vtxos).await.context("error filtering vtxos")?;
1681 Ok(vtxos)
1682 }
1683
1684 pub async fn spendable_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1686 Ok(self.vtxos_with(&VtxoStateKind::Spendable).await?)
1687 }
1688
1689 pub async fn spendable_vtxos_with(
1691 &self,
1692 filter: &impl FilterVtxos,
1693 ) -> anyhow::Result<Vec<WalletVtxo>> {
1694 let mut vtxos = self.spendable_vtxos().await?;
1695 filter.filter_vtxos(&mut vtxos).await.context("error filtering vtxos")?;
1696 Ok(vtxos)
1697 }
1698
1699 pub async fn get_expiring_vtxos(
1701 &self,
1702 threshold: BlockHeight,
1703 ) -> anyhow::Result<Vec<WalletVtxo>> {
1704 let expiry = self.inner.chain.tip().await? + threshold;
1705 let filter = VtxoFilter::new(&self).expires_before(expiry);
1706 Ok(self.spendable_vtxos_with(&filter).await?)
1707 }
1708
1709 pub async fn maintenance(&self) -> anyhow::Result<()> {
1715 info!("Starting wallet maintenance in interactive mode");
1716 self.sync().await;
1717
1718 let rounds = self.progress_pending_rounds(None).await;
1720 if let Err(e) = rounds.as_ref() {
1721 warn!("Error progressing pending rounds: {:#}", e);
1722 }
1723
1724 let states = self.inner.db.get_pending_round_state_ids().await?;
1726 for id in states {
1727 debug!("Cancelling pending round participation {}", id);
1728 let mut state = match self.lock_wait_round_state(id).await {
1729 Ok(Some(s)) => s,
1730 Ok(None) => continue, Err(e) => {
1732 warn!("Failed to lock round state with id {}: {:#}", id, e);
1733 continue;
1734 }
1735 };
1736 if let Err(e) = state.state_mut().try_cancel(self).await {
1737 warn!("Error cancelling pending round: {:#}", e);
1738 }
1739 }
1740
1741 let refresh = self.maintenance_refresh().await;
1743 if let Err(e) = refresh.as_ref() {
1744 warn!("Error refreshing VTXOs: {:#}", e);
1745 }
1746
1747 if rounds.is_err() || refresh.is_err() {
1748 bail!("Maintenance encountered errors.\nprogress_rounds: {:#?}\nrefresh: {:#?}",
1749 rounds, refresh,
1750 );
1751 }
1752
1753 Ok(())
1754 }
1755
1756 pub async fn maintenance_delegated(&self) -> anyhow::Result<()> {
1763 info!("Starting wallet maintenance in delegated mode");
1764 self.sync().await;
1765 let rounds = self.progress_pending_rounds(None).await;
1766 if let Err(e) = rounds.as_ref() {
1767 warn!("Error progressing pending rounds: {:#}", e);
1768 }
1769 let refresh = self.maybe_schedule_maintenance_refresh_delegated().await;
1770 if let Err(e) = refresh.as_ref() {
1771 warn!("Error refreshing VTXOs: {:#}", e);
1772 }
1773
1774 if rounds.is_err() || refresh.is_err() {
1775 bail!("Delegated maintenance encountered errors.\n\
1776 progress_rounds: {:#?}\nrefresh: {:#?}",
1777 rounds, refresh,
1778 );
1779 }
1780
1781 Ok(())
1782 }
1783
1784 pub(crate) async fn join_round_for_maintenance_refresh(
1799 &self,
1800 attempt: &RoundAttempt,
1801 ) -> anyhow::Result<Option<RoundStateId>> {
1802 self.maintenance_refresh_retry_loop(|part| async move {
1803 info!("Joining round {} for maintenance refresh ({} vtxos)",
1804 attempt.round_seq, part.inputs.len());
1805 Ok(Some(self.join_attempt_interactive(
1806 part, attempt, Some(RoundMovement::Refresh),
1807 ).await?.id()))
1808 }).await.context("failed to join round for maintenance refresh")
1809 }
1810
1811 pub async fn maybe_schedule_maintenance_refresh_delegated(
1819 &self,
1820 ) -> anyhow::Result<Option<RoundStateId>> {
1821 self.maintenance_refresh_retry_loop(|part| async move {
1822 info!("Scheduling delegated maintenance refresh ({} vtxos)", part.inputs.len());
1823 Ok(Some(self.join_next_round_delegated(part, Some(RoundMovement::Refresh)).await?.id()))
1824 }).await.context("failed to schedule delegated maintenance refresh")
1825 }
1826
1827 async fn maintenance_refresh_retry_loop<F, Fut>(
1835 &self,
1836 attempt_refresh: F,
1837 ) -> anyhow::Result<Option<RoundStateId>>
1838 where
1839 F: Fn(RoundParticipation) -> Fut,
1840 Fut: Future<Output = anyhow::Result<Option<RoundStateId>>>,
1841 {
1842 let mut excluded = HashSet::new();
1843 for _ in 0..10 {
1844 let vtxos = self.get_vtxos_to_refresh_with_excluded(excluded.iter().copied()).await?;
1845 match (vtxos.is_empty(), excluded.is_empty()) {
1846 (true, false) => {
1850 warn!("no VTXOs to refresh after exclusions: {:?}", excluded);
1851 bail!("no VTXOs to refresh after excluding: {:?}", excluded);
1852 },
1853 (true, true) => return Ok(None),
1855 (false, _) => {},
1857 }
1858 let part = match self.build_refresh_participation(vtxos).await? {
1859 Some(participation) => participation,
1860 None => return Ok(None),
1861 };
1862
1863 match attempt_refresh(part).await {
1864 Ok(state_id) => return Ok(state_id),
1865 Err(e) => {
1866 let rejected = rejected_vtxos_from_error(&e).into_iter()
1867 .filter(|id| !excluded.contains(id))
1868 .collect::<Vec<_>>();
1869 if rejected.is_empty() {
1870 return Err(e);
1871 }
1872 warn!("Maintenance refresh rejected {} unusable input(s) ({:?}); \
1873 retrying without them", rejected.len(), rejected);
1874 excluded.extend(rejected);
1875 },
1876 }
1877 }
1878 bail!("Maintenance refresh failed after 10 retries");
1879 }
1880
1881 pub async fn maintenance_refresh(&self) -> anyhow::Result<Option<RoundStatus>> {
1893 if self.get_vtxos_to_refresh().await?.is_empty() {
1894 return Ok(None);
1895 }
1896
1897 info!("Waiting for round to perform maintenance refresh...");
1898 let mut events = self.subscribe_round_events().await?;
1899 while let Some(event) = events.next().await {
1900 let event = event.context("error on round event stream")?;
1901 if let RoundEvent::Attempt(a) = event && a.attempt_seq == 0 {
1902 debug!("Round {} started, triggering maintenance refresh", a.round_seq);
1903 let state_id = match self.join_round_for_maintenance_refresh(&a).await? {
1904 Some(id) => id,
1905 None => return Ok(None),
1906 };
1907 let state = self.lock_wait_round_state(state_id).await?
1910 .context("maintenance refresh round state vanished after joining")?;
1911 return Ok(Some(self.drive_round_state(state, &mut events).await?));
1912 }
1913 }
1914 Ok(None)
1915 }
1916
1917 pub async fn sync(&self) {
1923 self.inner.chain.invalidate_caches().await;
1924
1925 futures::join!(
1926 async {
1927 if let Err(e) = self.inner.chain.update_fee_rates(self.inner.config.fallback_fee_rate).await {
1930 warn!("Error updating fee rates: {:#}", e);
1931 }
1932 },
1933 async {
1934 if let Err(e) = self.sync_mailbox().await {
1935 warn!("Error in mailbox sync: {:#}", e);
1936 }
1937 },
1938 async {
1939 if let Err(e) = self.sync_pending_rounds().await {
1940 warn!("Error while trying to progress rounds awaiting confirmations: {:#}", e);
1941 }
1942 },
1943 async {
1944 if let Err(e) = self.sync_pending_lightning_send_vtxos().await {
1945 warn!("Error syncing pending lightning payments: {:#}", e);
1946 }
1947 },
1948 async {
1949 if let Err(e) = self.sync_pending_arkoor_sends().await {
1950 warn!("Error syncing pending arkoor sends: {:#}", e);
1951 }
1952 },
1953 async {
1954 if let Err(e) = self.try_claim_all_lightning_receives(false).await {
1955 warn!("Error claiming pending lightning receives: {:#}", e);
1956 }
1957 },
1958 async {
1959 if let Err(e) = self.sync_pending_boards().await {
1960 warn!("Error syncing pending boards: {:#}", e);
1961 }
1962 },
1963 async {
1964 if let Err(e) = self.sync_pending_offboards().await {
1965 warn!("Error syncing pending offboards: {:#}", e);
1966 }
1967 },
1968 async {
1969 if let Err(e) = self.sync_force_exited_vtxos().await {
1970 warn!("Error scanning for on-chain-exited VTXOs: {:#}", e);
1971 }
1972 },
1973 async {
1974 if let Err(e) = self.catchup_recovery_vtxos().await {
1978 warn!("Failed to catch up recovery VTXOs with server: {:#}", e);
1979 }
1980 }
1981 );
1982 }
1983
1984 pub async fn sync_exits(&self) -> anyhow::Result<()> {
1990 self.exit_mgr().sync(&self).await?;
1991 Ok(())
1992 }
1993
1994 pub async fn progress_exits(&self) -> anyhow::Result<()> {
1999 self.exit_mgr().progress_exits_with_cpfp(&self, None).await?;
2000 Ok(())
2001 }
2002
2003 pub async fn sync_force_exited_vtxos(&self) -> anyhow::Result<()> {
2016 let tip = self.inner.chain.tip().await?;
2018 let mut lock = self.inner.last_force_exit_scan_tip.lock().await;
2019 if *lock == Some(tip) {
2020 return Ok(());
2021 }
2022
2023 let exiting = self.exit_mgr().get_exit_vtxo_ids().await;
2025 let vtxos = self.inner.db.get_vtxos_by_state(&[VtxoStateKind::Spendable]).await?
2026 .into_iter()
2027 .filter(|v| !exiting.contains(&v.vtxo.id()));
2028
2029 let mut checked = FuturesUnordered::new();
2031 for wv in vtxos {
2032 let chain = self.inner.chain.clone();
2033 checked.push(async move {
2034 let txid = wv.vtxo_id().to_point().txid;
2035 let status = chain.tx_status(txid).await;
2036 (wv, status)
2037 });
2038 }
2039
2040 let mut to_exit = Vec::new();
2041 while let Some((vtxo, status)) = futures::StreamExt::next(&mut checked).await {
2042 match status {
2043 Ok(TxStatus::NotFound) => {},
2044 Ok(_) => {
2045 info!("VTXO {} was exited on-chain without us; routing it to a claimable exit",
2046 vtxo.vtxo.id(),
2047 );
2048 to_exit.push(vtxo.vtxo);
2049 },
2050 Err(e) => warn!("Could not check on-chain status of VTXO {}: {:#}",
2051 vtxo.vtxo.id(), e,
2052 ),
2053 }
2054 }
2055
2056 if !to_exit.is_empty() {
2057 self.exit_mgr().start_exit_for_vtxos(&to_exit).await
2058 .context("failed to start exit for on-chain-exited VTXOs")?;
2059
2060 *lock = Some(tip);
2061 self.sync_exits().await
2062 .context("failed to sync exits after starting new ones")?;
2063 } else {
2064 *lock = Some(tip);
2065 }
2066
2067 Ok(())
2068 }
2069
2070 pub async fn dangerous_drop_vtxo(&self, vtxo_id: VtxoId) -> anyhow::Result<()> {
2073 warn!("Drop vtxo {} from the database", vtxo_id);
2074 self.inner.db.remove_vtxo(vtxo_id).await?;
2075 Ok(())
2076 }
2077
2078 pub async fn dangerous_drop_all_vtxos(&self) -> anyhow::Result<()> {
2081 warn!("Dropping all vtxos from the db...");
2082 for vtxo in self.vtxos().await? {
2083 self.inner.db.remove_vtxo(vtxo.id()).await?;
2084 }
2085
2086 self.exit_mgr().dangerous_clear_exit().await?;
2087 Ok(())
2088 }
2089
2090 async fn has_counterparty_risk(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<bool> {
2098 for past_pks in vtxo.past_arkoor_pubkeys() {
2099 let mut owns_any = false;
2100 for past_pk in past_pks {
2101 if self.inner.db.get_public_key_idx(&past_pk).await?.is_some() {
2102 owns_any = true;
2103 break;
2104 }
2105 }
2106 if !owns_any {
2107 return Ok(true);
2108 }
2109 }
2110
2111 let my_clause = self.find_signable_clause(vtxo).await;
2112 Ok(!my_clause.is_some())
2113 }
2114
2115 pub async fn build_refresh_participation<V: VtxoRef>(
2116 &self,
2117 vtxos: impl IntoIterator<Item = V>,
2118 ) -> anyhow::Result<Option<RoundParticipation>> {
2119 self.inner_build_refresh_participation(vtxos, None).await
2120 }
2121
2122 pub async fn build_scheduled_refresh_participation<V: VtxoRef>(
2123 &self,
2124 vtxos: impl IntoIterator<Item = V>,
2125 height: BlockHeight,
2126 ) -> anyhow::Result<Option<RoundParticipation>> {
2127 self.inner_build_refresh_participation(vtxos, Some(height)).await
2128 }
2129
2130 async fn inner_build_refresh_participation<V: VtxoRef>(
2131 &self,
2132 vtxos: impl IntoIterator<Item = V>,
2133 height: Option<BlockHeight>,
2134 ) -> anyhow::Result<Option<RoundParticipation>> {
2135 let (vtxos, total_amount) = {
2136 let iter = vtxos.into_iter();
2137 let size_hint = iter.size_hint();
2138 let mut vtxos = Vec::<Vtxo<Full>>::with_capacity(size_hint.1.unwrap_or(size_hint.0));
2139 let mut amount = Amount::ZERO;
2140 for vref in iter {
2141 let id = vref.vtxo_id();
2146 if vtxos.iter().any(|v| v.id() == id) {
2147 bail!("duplicate VTXO id: {}", id);
2148 }
2149 let vtxo = if let Some(vtxo) = vref.into_full_vtxo() {
2150 vtxo
2151 } else {
2152 self.inner.db.get_full_vtxo(id).await?
2155 .with_context(|| format!("vtxo with id {} not found", id))?
2156 };
2157 amount += vtxo.amount();
2158 vtxos.push(vtxo);
2159 }
2160 (vtxos, amount)
2161 };
2162
2163 if vtxos.is_empty() {
2164 info!("Skipping refresh since no VTXOs are provided.");
2165 return Ok(None);
2166 }
2167 ensure!(total_amount >= VTXO_DUST,
2168 "vtxo amount must be at least {} to participate in a round",
2169 VTXO_DUST,
2170 );
2171
2172 let (_, ark_info) = self.require_server().await?;
2174 let refresh_height = match height {
2175 Some(height) => height,
2176 None => self.inner.chain.tip().await?,
2177 };
2178
2179 let vtxo_fee_infos = vtxos.iter()
2180 .map(|v| VtxoFeeInfo::from_vtxo_and_tip(v, refresh_height));
2181 let fee = ark_info.fees.refresh.calculate(vtxo_fee_infos).context("fee overflowed")?;
2182 let output_amount = validate_and_subtract_fee_min_dust(total_amount, fee, VTXO_DUST)?;
2183
2184 info!("Refreshing {} VTXOs (total amount = {}, fee = {}, output = {}).",
2185 vtxos.len(), total_amount, fee, output_amount,
2186 );
2187 let (user_keypair, _) = self.derive_store_next_keypair().await?;
2188 let req = VtxoRequest {
2189 policy: VtxoPolicy::Pubkey(PubkeyVtxoPolicy { user_pubkey: user_keypair.public_key() }),
2190 amount: output_amount,
2191 };
2192
2193 Ok(Some(RoundParticipation {
2194 inputs: vtxos,
2195 outputs: vec![req],
2196 unblinded_mailbox_id: None,
2197 }))
2198 }
2199
2200 pub async fn refresh_vtxos<V: VtxoRef>(
2205 &self,
2206 vtxos: impl IntoIterator<Item = V>,
2207 ) -> anyhow::Result<Option<RoundStatus>> {
2208 let participation = match self.build_refresh_participation(vtxos).await? {
2209 Some(participation) => participation,
2210 None => return Ok(None),
2211 };
2212
2213 Ok(Some(self.participate_round(participation, Some(RoundMovement::Refresh)).await?))
2214 }
2215
2216 pub async fn refresh_vtxos_delegated<V: VtxoRef>(
2222 &self,
2223 vtxos: impl IntoIterator<Item = V>,
2224 ) -> anyhow::Result<Option<StoredRoundState<Unlocked>>> {
2225 let part = match self.build_refresh_participation(vtxos).await? {
2226 Some(participation) => participation,
2227 None => return Ok(None),
2228 };
2229
2230 Ok(Some(self.join_delegated_round(
2231 part, Some(RoundMovement::Refresh), None,
2232 ).await?))
2233 }
2234
2235 pub async fn refresh_vtxos_scheduled<V: VtxoRef>(
2238 &self,
2239 vtxos: impl IntoIterator<Item = V>,
2240 scheduled_height: BlockHeight,
2241 ) -> anyhow::Result<Option<StoredRoundState<Unlocked>>> {
2242 let part = match self
2243 .build_scheduled_refresh_participation(vtxos, scheduled_height).await?
2244 {
2245 Some(participation) => participation,
2246 None => return Ok(None),
2247 };
2248
2249 Ok(Some(self.join_delegated_round(
2250 part, Some(RoundMovement::Refresh), Some(scheduled_height),
2251 ).await?))
2252 }
2253
2254 pub async fn get_vtxos_to_refresh(&self) -> anyhow::Result<Vec<WalletVtxo>> {
2257 let vtxos = self.spendable_vtxos_with(&RefreshStrategy::should_refresh_if_must(
2258 self,
2259 self.inner.chain.tip().await?,
2260 self.inner.chain.fee_rates().await.fast,
2261 )).await?;
2262 Ok(vtxos)
2263 }
2264
2265 pub async fn get_vtxos_to_refresh_with_excluded<V: VtxoRef>(
2268 &self,
2269 exclude: impl IntoIterator<Item = V>,
2270 ) -> anyhow::Result<Vec<WalletVtxo>> {
2271 let mut vtxos = self.get_vtxos_to_refresh().await?;
2272 for v in exclude.into_iter() {
2273 if let Some(index) = vtxos.iter().position(|vtxo| vtxo.id() == v.vtxo_id()) {
2274 vtxos.swap_remove(index);
2275 }
2276 }
2277 Ok(vtxos)
2278 }
2279
2280 pub async fn get_first_expiring_vtxo_blockheight(
2282 &self,
2283 ) -> anyhow::Result<Option<BlockHeight>> {
2284 Ok(self.spendable_vtxos().await?.iter().map(|v| v.expiry_height()).min())
2285 }
2286
2287 pub async fn get_next_required_refresh_blockheight(
2290 &self,
2291 ) -> anyhow::Result<Option<BlockHeight>> {
2292 let first_expiry = self.get_first_expiring_vtxo_blockheight().await?;
2293 Ok(first_expiry.map(|h| {
2294 h.saturating_sub(self.inner.config.vtxo_refresh_expiry_threshold as BlockHeight)
2295 }))
2296 }
2297
2298 async fn spend_input_selection(&self) -> anyhow::Result<InputSelection> {
2302 let mut selection = InputSelection::new();
2303 if let Some(info) = self.ark_info().await? {
2304 selection = selection.max_exit_depth(info.max_vtxo_exit_depth);
2305 }
2306 Ok(selection)
2307 }
2308
2309 async fn select_any_vtxos_to_cover(
2311 &self,
2312 amount: Amount,
2313 ) -> anyhow::Result<Vec<WalletVtxo>> {
2314 self.spend_input_selection().await?.select(self.spendable_vtxos().await?, amount)
2315 }
2316
2317 async fn select_any_vtxos_to_cover_with_fee<F>(
2322 &self,
2323 amount: Amount,
2324 calc_fee: F,
2325 ) -> anyhow::Result<(Vec<WalletVtxo>, Amount)>
2326 where
2327 F: for<'a> Fn(Amount, SelectedFeeInfos<'a>) -> anyhow::Result<Amount>,
2328 {
2329 let tip = self.inner.chain.tip().await?;
2330 self.spend_input_selection().await?
2331 .fee_scheme(tip, calc_fee)
2332 .select(self.spendable_vtxos().await?, amount)
2333 }
2334
2335 pub fn start_daemon(&self) -> anyhow::Result<()> {
2345 let mut daemon = self.inner.daemon.lock();
2346 if daemon.is_some() {
2347 warn!("Called Wallet::start_daemon while daemon was already running.");
2348 return Ok(());
2349 }
2350
2351 let handle = crate::daemon::start_daemon(self);
2352 let _ = daemon.insert(handle);
2353
2354 Ok(())
2355 }
2356
2357 pub fn stop_daemon(&self) {
2359 let mut daemon = self.inner.daemon.lock();
2360 if let Some(handle) = daemon.take() {
2361 handle.stop();
2362 }
2363 }
2364
2365 pub async fn stop_daemon_wait(&self) -> anyhow::Result<()> {
2368 let handle = self.inner.daemon.lock().take();
2369 if let Some(handle) = handle {
2370 handle.stop_wait().await?;
2371 }
2372 Ok(())
2373 }
2374
2375 async fn catchup_recovery_vtxos(&self) -> anyhow::Result<()> {
2393 let mut ids = self.inner.db.get_unregistered_vtxo_ids().await?;
2394 if ids.is_empty() {
2395 return Ok(());
2396 }
2397
2398 let in_progress_boards = self.boards_in_progress().await?;
2401 ids.retain(|id| !in_progress_boards.iter().any(|b| b.vtxo_id == *id));
2402 if ids.is_empty() {
2403 return Ok(());
2404 }
2405
2406 let posted = self.post_recovery_vtxo_ids(ids.iter().copied()).await
2411 .context("failed to post recovery vtxo IDs");
2412 let registered = self.register_recovery_vtxo_chains(&ids, posted.is_ok()).await;
2413
2414 match (posted, registered) {
2415 (Ok(()), registered) => registered,
2416 (posted, Ok(())) => posted,
2417 (Err(posted), Err(registered)) => {
2418 Err(registered.context(format!("mailbox post also failed: {:#}", posted)))
2419 },
2420 }
2421 }
2422
2423 async fn register_recovery_vtxo_chains(
2429 &self,
2430 ids: &[VtxoId],
2431 mark_registered: bool,
2432 ) -> anyhow::Result<()> {
2433 const CHUNK_SIZE: usize = 20;
2434 let mut failed = 0;
2435 for chunk_ids in ids.chunks(CHUNK_SIZE) {
2436 let chunk = self.inner.db.get_full_vtxos(chunk_ids).await
2439 .context("failed to load full vtxos for recovery registration")?;
2440 ensure!(chunk.len() == chunk_ids.len(),
2441 "loaded {} full vtxos for {} ids", chunk.len(), chunk_ids.len(),
2442 );
2443
2444 let mut succeeded = Vec::with_capacity(chunk.len());
2445 match self.register_vtxo_transactions_with_server(&chunk).await {
2446 Ok(()) => succeeded.extend(chunk.iter().map(|v| v.id())),
2447 Err(e) => {
2448 debug!("Failed to register chunk of {} vtxo transactions, \
2449 retrying one by one: {:#}", chunk.len(), e,
2450 );
2451 for vtxo in &chunk {
2452 match self.register_vtxo_transactions_with_server(
2453 std::slice::from_ref(vtxo),
2454 ).await {
2455 Ok(()) => succeeded.push(vtxo.id()),
2456 Err(e) => {
2457 error!("Failed to register vtxo {} transactions with server; \
2458 recovery from seed may miss it until registration succeeds: {:#}",
2459 vtxo.id(), e,
2460 );
2461 failed += 1;
2462 },
2463 }
2464 }
2465 },
2466 }
2467 if mark_registered && !succeeded.is_empty() {
2468 self.inner.db.mark_vtxos_registered(&succeeded).await
2469 .context("failed to mark vtxos as registered for recovery")?;
2470 }
2471 }
2472 if failed > 0 {
2473 bail!("failed to register {} of {} vtxo transactions", failed, ids.len());
2474 }
2475 Ok(())
2476 }
2477
2478 pub async fn register_vtxo_transactions_with_server(
2482 &self,
2483 vtxos: &[impl AsRef<Vtxo<Full>>],
2484 ) -> anyhow::Result<()> {
2485 if vtxos.is_empty() {
2486 return Ok(());
2487 }
2488
2489 let (mut srv, _) = self.require_server().await?;
2490 srv.client.register_vtxo_transactions(protos::RegisterVtxoTransactionsRequest {
2491 vtxos: vtxos.iter().map(|v| v.as_ref().serialize()).collect(),
2492 }).await.context("failed to register vtxo transactions")?;
2493
2494 Ok(())
2495 }
2496}
2497
2498fn wrap_server_connect_error(err: ConnectError) -> anyhow::Error {
2499 match err {
2500 ConnectError::CreateEndpoint(CreateEndpointError::NoTransportBackend) => {
2501 anyhow!(MISSING_SERVER_TRANSPORT_HELP)
2502 },
2503 other => anyhow::Error::from(other),
2504 }
2505}
2506
2507impl std::ops::Drop for WalletInner {
2508 fn drop(&mut self) {
2509 if let Some(handle) = self.daemon.lock().take() {
2516 handle.stop();
2517 }
2518 }
2519}
2520
2521#[cfg(test)]
2522mod tests {
2523 use bitcoin::{Amount, FeeRate, Network};
2524 use bitcoin::secp256k1::PublicKey;
2525
2526 use ark::ArkInfo;
2527 use server_rpc::client::CreateEndpointError;
2528
2529 use super::{
2530 check_ark_info_safe, wrap_server_connect_error,
2531 MAX_NB_ROUND_NONCES, MIN_MAINNET_VTXO_EXIT_DELTA, MISSING_SERVER_TRANSPORT_HELP,
2532 };
2533
2534 #[test]
2535 fn no_transport_connect_error_is_reworded_for_wallet_users() {
2536 let err = wrap_server_connect_error(CreateEndpointError::NoTransportBackend.into());
2537 assert!(err.to_string().contains(MISSING_SERVER_TRANSPORT_HELP));
2538 assert!(err.to_string().contains("feature `bark-wallet/native` or `bark-wallet/wasm-web`"));
2539 }
2540
2541 fn ark_info_with_lifetime(vtxo_lifetime: u16, required_board_confirmations: usize) -> ArkInfo {
2542 use std::str::FromStr as _;
2543 let pk = PublicKey::from_str(
2544 "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
2545 ).unwrap();
2546 #[allow(deprecated)]
2547 ArkInfo {
2548 network: Network::Regtest,
2549 server_pubkey: pk,
2550 mailbox_pubkey: pk,
2551 round_interval: std::time::Duration::from_secs(60),
2552 nb_round_nonces: 8,
2553 vtxo_exit_delta: 48,
2554 vtxo_lifetime,
2555 htlc_send_expiry_delta: 100,
2556 htlc_expiry_delta: 100,
2557 max_vtxo_amount: None,
2558 required_board_confirmations,
2559 max_user_invoice_cltv_delta: 100,
2560 min_board_amount: Amount::from_sat(1000),
2561 vtxo_expiry_delta: vtxo_lifetime,
2562 offboard_feerate: FeeRate::ZERO,
2563 max_offboard_inputs: 1,
2564 ln_receive_anti_dos_required: false,
2565 fees: Default::default(),
2566 max_vtxo_exit_depth: 10,
2567 tos_link: None,
2568 }
2569 }
2570
2571 #[test]
2572 fn ark_info_with_zero_lifetime_is_rejected() {
2573 let mut ai = ark_info_with_lifetime(0, 6);
2574 ai.network = Network::Bitcoin;
2575 ai.vtxo_exit_delta = MIN_MAINNET_VTXO_EXIT_DELTA;
2576 let err = check_ark_info_safe(&ai, 12).unwrap_err().to_string();
2577 assert!(err.contains("unsafe"), "unexpected error: {err}");
2578 }
2579
2580 #[test]
2581 fn ark_info_at_the_boundary_is_rejected() {
2582 let mut ai = ark_info_with_lifetime(6 + 12, 6);
2585 ai.network = Network::Bitcoin;
2586 ai.vtxo_exit_delta = MIN_MAINNET_VTXO_EXIT_DELTA;
2587 assert!(check_ark_info_safe(&ai, 12).is_err());
2588 }
2589
2590 #[test]
2591 fn ark_info_one_block_over_the_boundary_is_accepted() {
2592 let ai = ark_info_with_lifetime(6 + 12 + 1, 6);
2593 check_ark_info_safe(&ai, 12).unwrap();
2594 }
2595
2596 #[test]
2597 fn ark_info_generous_lifetime_is_accepted() {
2598 let ai = ark_info_with_lifetime(4032, 6);
2599 check_ark_info_safe(&ai, 12).unwrap();
2600 }
2601
2602 #[test]
2603 fn ark_info_with_zero_vtxo_exit_delta_is_rejected() {
2604 let mut ai = ark_info_with_lifetime(4032, 6);
2605 ai.vtxo_exit_delta = 0;
2606 let err = check_ark_info_safe(&ai, 12).unwrap_err().to_string();
2607 assert!(err.contains("vtxo_exit_delta"), "unexpected error: {err}");
2608 }
2609
2610 #[test]
2611 fn ark_info_below_mainnet_vtxo_exit_delta_is_rejected() {
2612 let mut ai = ark_info_with_lifetime(4032, 6);
2613 ai.network = Network::Bitcoin;
2614 ai.vtxo_exit_delta = MIN_MAINNET_VTXO_EXIT_DELTA - 1;
2615 let err = check_ark_info_safe(&ai, 12).unwrap_err().to_string();
2616 assert!(err.contains("mainnet minimum"), "unexpected error: {err}");
2617 }
2618
2619 #[test]
2620 fn ark_info_at_mainnet_vtxo_exit_delta_is_accepted() {
2621 let mut ai = ark_info_with_lifetime(4032, 6);
2622 ai.network = Network::Bitcoin;
2623 ai.vtxo_exit_delta = MIN_MAINNET_VTXO_EXIT_DELTA;
2624 check_ark_info_safe(&ai, 12).unwrap();
2625 }
2626
2627 #[test]
2628 fn ark_info_with_zero_nb_round_nonces_is_rejected() {
2629 let mut ai = ark_info_with_lifetime(4032, 6);
2630 ai.nb_round_nonces = 0;
2631 let err = check_ark_info_safe(&ai, 12).unwrap_err().to_string();
2632 assert!(err.contains("nb_round_nonces"), "unexpected error: {err}");
2633 }
2634
2635 #[test]
2636 fn ark_info_at_nb_round_nonces_cap_is_accepted() {
2637 let mut ai = ark_info_with_lifetime(4032, 6);
2638 ai.nb_round_nonces = MAX_NB_ROUND_NONCES;
2639 check_ark_info_safe(&ai, 12).unwrap();
2640 }
2641
2642 #[test]
2643 fn ark_info_over_nb_round_nonces_cap_is_rejected() {
2644 let mut ai = ark_info_with_lifetime(4032, 6);
2645 ai.network = Network::Bitcoin;
2646 ai.vtxo_exit_delta = MIN_MAINNET_VTXO_EXIT_DELTA;
2647 ai.nb_round_nonces = MAX_NB_ROUND_NONCES + 1;
2648 let err = check_ark_info_safe(&ai, 12).unwrap_err().to_string();
2649 assert!(err.contains("nb_round_nonces"), "unexpected error: {err}");
2650 }
2651
2652}