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 import;
401mod lightning;
402mod mailbox;
403mod notification;
404mod offboard;
405#[cfg(feature = "socks5-proxy")]
406mod proxy;
407mod recovery;
408mod psbtext;
409mod utils;
410
411pub use self::arkoor::{ArkoorCreateResult, ArkoorAddressError};
412pub use self::payment_request::{
413 AvailablePaymentMethod, PaymentInitOutput, PaymentMethodParsingError, PaymentRequest,
414};
415pub use self::config::{
416 BarkNetwork, Config, DEFAULT_VTXO_KEY_GAP_LIMIT, MAX_VTXO_KEY_GAP_LIMIT,
417};
418pub use self::daemon::{tip_watcher, DaemonHandle};
419pub use self::fees::FeeEstimate;
420pub use self::import::{ImportVtxoArgs, ImportVtxoError};
421pub use self::notification::{WalletNotification, NotificationStream};
422pub use self::recovery::{RecoveryReport, RecoveryReportEntry, RecoveryStatus};
423pub use self::vtxo::WalletVtxo;
424
425use std::borrow::Cow;
426use std::collections::{HashMap, HashSet};
427use std::path::PathBuf;
428use std::sync::Arc;
429use std::time::Duration;
430
431use anyhow::{bail, Context};
432use bip39::Mnemonic;
433use bitcoin::{Amount, Network, OutPoint};
434use bitcoin::bip32::{self, ChildNumber, Fingerprint};
435use bitcoin::secp256k1::{self, Keypair, PublicKey};
436use futures::stream::FuturesUnordered;
437use log::{debug, error, info, trace, warn};
438use tokio_stream::StreamExt;
439
440use ark::{ArkInfo, ProtocolEncoding, Vtxo, VtxoId, VtxoPolicy, VtxoRequest};
441use ark::attestations::VtxoStatusAttestation;
442use ark::address::VtxoDelivery;
443use ark::fees::{validate_and_subtract_fee_min_dust, VtxoFeeInfo};
444use ark::rounds::{RoundAttempt, RoundEvent};
445use ark::vtxo::{Full, PubkeyVtxoPolicy, VtxoRef, VTXO_DUST};
446use ark::vtxo::policy::signing::VtxoSigner;
447use bitcoin_ext::{BlockDelta, BlockHeight, TxStatus};
448use server_rpc::{protos, ServerConnection};
449use server_rpc::protos::VtxoSpendState;
450use server_rpc::client::{ConnectError, CreateEndpointError};
451
452use crate::chain::{ChainSource, ChainSourceSpec};
453use crate::exit::Exit;
454use crate::lock_manager::LockManager;
455use crate::movement::{Movement, MovementId, PaymentMethod};
456use crate::movement::manager::MovementManager;
457use crate::notification::NotificationDispatch;
458use crate::onchain::{OnchainWalletTrait, Utxo};
459use crate::persist::BarkPersister;
460use crate::persist::models::{RoundStateId, StoredRoundState, Unlocked};
461#[cfg(feature = "socks5-proxy")]
462use crate::proxy::proxy_for_url;
463use crate::round::{RoundParticipation, RoundSecretNonces, RoundStatus};
464use crate::subsystem::RoundMovement;
465use crate::utils::rejected_vtxos_from_error;
466use crate::vtxo::{FilterVtxos, RefreshStrategy, VtxoFilter, VtxoStateKind, VtxoValidationError};
467use crate::vtxo::selection::{InputSelection, SelectedFeeInfos};
468
469#[cfg(all(feature = "wasm-web", feature = "socks5-proxy"))]
470compile_error!("features `wasm-web` does not support feature `socks5-proxy");
471
472#[cfg(all(feature = "wasm-web", feature = "bitcoind-rpc"))]
473compile_error!("`wasm-web` does not support the `bitcoind-rpc` feature");
474
475const BARK_PURPOSE_INDEX: u32 = 350;
477const VTXO_KEYS_INDEX: u32 = 0;
479const MAILBOX_KEY_INDEX: u32 = 1;
481const RECOVERY_MAILBOX_KEY_INDEX: u32 = 2;
483const MISSING_SERVER_TRANSPORT_HELP: &str =
484 "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.";
485
486const SUBSCRIBE_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 60);
488
489lazy_static::lazy_static! {
490 static ref SECP: secp256k1::Secp256k1<secp256k1::All> = secp256k1::Secp256k1::new();
492}
493
494const MAX_NB_ROUND_NONCES: usize = 16;
497
498const MIN_MAINNET_VTXO_EXIT_DELTA: BlockDelta = 96;
503
504fn check_ark_info_safe(ark_info: &ArkInfo, vtxo_exit_margin: BlockDelta) -> anyhow::Result<()> {
509 let required = ark_info.required_board_confirmations;
510 let margin = vtxo_exit_margin as usize;
511 let min_safe = required.saturating_add(margin);
512 ensure!(ark_info.vtxo_exit_delta > 0,
513 "server-advertised vtxo_exit_delta is 0; refusing to connect",
514 );
515 ensure!(ark_info.nb_round_nonces > 0,
516 "server-advertised nb_round_nonces is 0; refusing to connect",
517 );
518 if ark_info.network == Network::Bitcoin {
519 ensure!((ark_info.vtxo_lifetime as usize) > min_safe,
522 "server-advertised vtxo_lifetime {} is unsafe (minimum > {} = \
523 required_board_confirmations {} + vtxo_exit_margin {}); refusing to connect",
524 ark_info.vtxo_lifetime, min_safe, required, margin,
525 );
526 ensure!(ark_info.vtxo_exit_delta >= MIN_MAINNET_VTXO_EXIT_DELTA,
527 "server-advertised vtxo_exit_delta {} is below the mainnet minimum of {} \
528 blocks; refusing to connect",
529 ark_info.vtxo_exit_delta, MIN_MAINNET_VTXO_EXIT_DELTA,
530 );
531 ensure!(ark_info.nb_round_nonces <= MAX_NB_ROUND_NONCES,
532 "server-advertised nb_round_nonces {} exceeds cap {}; refusing to connect",
533 ark_info.nb_round_nonces, MAX_NB_ROUND_NONCES,
534 );
535 }
536 Ok(())
537}
538
539fn log_server_pubkey_changed_error(expected: PublicKey, got: PublicKey) {
545 error!(
546 "
547Server public key has changed!
548
549The Ark server's public key is different from the one stored when this
550wallet was created. This typically happens when:
551
552 - The server operator has rotated their keys
553 - You are connecting to a different server
554 - The server has been replaced
555
556For safety, this wallet will not connect to the server until you
557resolve this. You can recover your funds on-chain by doing an emergency exit.
558
559This will exit your VTXOs to on-chain Bitcoin without needing the server's cooperation.
560
561Expected: {expected}
562Got: {got}")
563}
564
565fn log_server_mailbox_pubkey_changed_error(expected: PublicKey, got: PublicKey) {
567 error!(
568 "
569Server mailbox public key has changed!
570
571The Ark server's mailbox public key is different from the one stored when this
572wallet was created. This typically happens when:
573
574 - The server operator has rotated their keys
575 - You are connecting to a different server
576 - The server has been replaced
577
578For safety, this wallet will not connect to the server until you resolve this.
579
580Unlike a server pubkey change, your VTXOs are not at risk - the mailbox pubkey
581only affects address receive semantics. Any Ark addresses you previously
582shared will stop receiving new payments; you will need to share new addresses
583after reconnecting.
584
585Expected: {expected}
586Got: {got}")
587}
588
589#[derive(Debug, Clone)]
591pub struct LightningReceiveBalance {
592 pub total: Amount,
594 pub claimable: Amount,
596}
597
598#[derive(Debug, Clone)]
600pub struct Balance {
601 pub spendable: Amount,
603 pub pending_lightning_send: Amount,
605 pub claimable_lightning_receive: Amount,
607 pub pending_in_round: Amount,
609 pub pending_exit: Option<Amount>,
615 pub pending_board: Amount,
617}
618
619pub struct UtxoInfo {
620 pub outpoint: OutPoint,
621 pub amount: Amount,
622 pub confirmation_height: Option<u32>,
623}
624
625impl From<Utxo> for UtxoInfo {
626 fn from(value: Utxo) -> Self {
627 match value {
628 Utxo::Local(o) => UtxoInfo {
629 outpoint: o.outpoint,
630 amount: o.amount,
631 confirmation_height: o.confirmation_height,
632 },
633 Utxo::Exit(e) => UtxoInfo {
634 outpoint: e.vtxo.point(),
635 amount: e.vtxo.amount(),
636 confirmation_height: Some(e.height),
637 },
638 }
639 }
640}
641
642pub struct OffchainBalance {
645 pub available: Amount,
647 pub pending_in_round: Amount,
649 pub pending_exit: Amount,
652}
653
654#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
656pub struct WalletProperties {
657 pub network: Network,
661
662 pub fingerprint: Fingerprint,
666
667 pub server_pubkey: Option<PublicKey>,
674
675 pub server_mailbox_pubkey: Option<PublicKey>,
683}
684
685pub struct WalletSeed {
691 master: bip32::Xpriv,
692 vtxo: bip32::Xpriv,
693}
694
695impl WalletSeed {
696 pub fn new_from_seed(network: Network, seed: &[u8; 64]) -> Self {
698 let bark_path = [ChildNumber::from_hardened_idx(BARK_PURPOSE_INDEX).unwrap()];
699 let master = bip32::Xpriv::new_master(network, seed)
700 .expect("invalid seed")
701 .derive_priv(&SECP, &bark_path)
702 .expect("purpose is valid");
703
704 let vtxo_path = [ChildNumber::from_hardened_idx(VTXO_KEYS_INDEX).unwrap()];
705 let vtxo = master.derive_priv(&SECP, &vtxo_path)
706 .expect("vtxo path is valid");
707
708 Self { master, vtxo }
709 }
710
711 pub fn new_from_mnemonic(network: Network, mnemonic: &Mnemonic) -> Self {
713 Self::new_from_seed(network, &mnemonic.to_seed(""))
714 }
715
716 pub fn fingerprint(&self) -> Fingerprint {
717 self.master.fingerprint(&SECP)
718 }
719
720 fn derive_vtxo_keypair(&self, idx: u32) -> Keypair {
721 self.vtxo.derive_priv(&SECP, &[idx.into()]).unwrap().to_keypair(&SECP)
722 }
723
724 fn to_mailbox_keypair(&self) -> Keypair {
725 let mailbox_path = [ChildNumber::from_hardened_idx(MAILBOX_KEY_INDEX).unwrap()];
726 self.master.derive_priv(&SECP, &mailbox_path).unwrap().to_keypair(&SECP)
727 }
728
729 fn to_recovery_mailbox_keypair(&self) -> Keypair {
730 let mailbox_path = [ChildNumber::from_hardened_idx(RECOVERY_MAILBOX_KEY_INDEX).unwrap()];
731 self.master.derive_priv(&SECP, &mailbox_path).unwrap().to_keypair(&SECP)
732 }
733}
734
735pub struct OpenWalletArgs {
737 pub run_daemon: bool,
743
744 pub datadir: Option<PathBuf>,
754
755 pub persister: Option<Arc<dyn BarkPersister>>,
759
760 pub lock_manager: Option<Box<dyn LockManager>>,
767
768 pub onchain: Option<Arc<tokio::sync::RwLock<dyn OnchainWalletTrait>>>,
772
773 pub create_if_not_exists: bool,
777
778 pub create_without_server: bool,
782
783 pub skip_recovery: bool,
789
790 pub on_recovery_finished: Option<Box<dyn FnOnce(RecoveryStatus) + Send + Sync>>,
797}
798
799impl Default for OpenWalletArgs {
800 fn default() -> Self {
801 Self {
802 run_daemon: true,
803 onchain: None,
804 datadir: None,
805 persister: None,
806 lock_manager: None,
807 create_if_not_exists: true,
808 create_without_server: false,
809 skip_recovery: false,
810 on_recovery_finished: None,
811 }
812 }
813}
814
815struct WalletInner {
816 chain: Arc<ChainSource>,
818
819 exit: Exit,
821
822 movements: Arc<MovementManager>,
824
825 notifications: NotificationDispatch,
827
828 config: Config,
830
831 db: Arc<dyn BarkPersister>,
833
834 lock_manager: Box<dyn LockManager>,
838
839 seed: WalletSeed,
841
842 server: tokio::sync::OnceCell<ServerConnection>,
849
850 onchain: Option<Arc<tokio::sync::RwLock<dyn OnchainWalletTrait>>>,
855
856 daemon: parking_lot::Mutex<Option<DaemonHandle>>,
858
859 last_force_exit_scan_tip: tokio::sync::Mutex<Option<BlockHeight>>,
863
864 pub(crate) round_secret_nonces: RoundSecretNonces,
867}
868
869#[derive(Clone)]
970pub struct Wallet {
971 inner: Arc<WalletInner>,
972}
973
974impl Wallet {
975 pub async fn network(&self) -> anyhow::Result<Network> {
976 Ok(self.properties().await?.network)
977 }
978
979 pub fn chain(&self) -> &Arc<ChainSource> {
981 &self.inner.chain
982 }
983
984 pub fn exit_mgr(&self) -> &Exit {
986 &self.inner.exit
987 }
988
989 pub fn movements_mgr(&self) -> &MovementManager {
991 &self.inner.movements
992 }
993
994 pub async fn peek_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
997 let last_revealed = self.inner.db.get_last_vtxo_key_index().await?;
998
999 let index = last_revealed.map(|i| i + 1).unwrap_or(u32::MIN);
1000 let keypair = self.inner.seed.derive_vtxo_keypair(index);
1001
1002 Ok((keypair, index))
1003 }
1004
1005 pub async fn derive_store_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
1008 let (keypair, index) = self.peek_next_keypair().await?;
1009 self.inner.db.store_vtxo_key(index, keypair.public_key()).await?;
1010 Ok((keypair, index))
1011 }
1012
1013 #[deprecated(note = "use peek_keypair instead")]
1014 pub async fn peak_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
1015 self.peek_keypair(index).await
1016 }
1017
1018 pub async fn peek_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
1032 let keypair = self.inner.seed.derive_vtxo_keypair(index);
1033 if self.inner.db.get_public_key_idx(&keypair.public_key()).await?.is_some() {
1034 Ok(keypair)
1035 } else {
1036 bail!("VTXO key {} does not exist, please derive it first", index)
1037 }
1038 }
1039
1040 pub(crate) async fn find_vtxo_keypairs(
1051 &self,
1052 wanted: impl IntoIterator<Item = PublicKey>,
1053 gap_limit: u32,
1054 ) -> anyhow::Result<HashMap<PublicKey, Keypair>> {
1055 if gap_limit > MAX_VTXO_KEY_GAP_LIMIT {
1057 bail!("vtxo key gap limit {gap_limit} is above the maximum of {}",
1058 MAX_VTXO_KEY_GAP_LIMIT);
1059 }
1060
1061 let mut found = HashMap::new();
1063 let mut unrevealed = HashSet::new();
1064 for pubkey in wanted {
1065 match self.pubkey_keypair(&pubkey).await? {
1066 Some((_idx, keypair)) => { found.insert(pubkey, keypair); },
1067 None => { unrevealed.insert(pubkey); },
1068 }
1069 }
1070 if unrevealed.is_empty() {
1071 return Ok(found);
1072 }
1073
1074 let start_idx = self.inner.db.get_last_vtxo_key_index().await?.map(|i| i + 1).unwrap_or(0);
1076 let mut frontier = start_idx.saturating_add(gap_limit);
1077 let mut idx = start_idx;
1078 let mut gap = Vec::<(u32, PublicKey)>::new();
1079 while idx <= frontier && !unrevealed.is_empty() {
1080 let keypair = self.inner.seed.derive_vtxo_keypair(idx);
1081 let pubkey = keypair.public_key();
1082 if unrevealed.remove(&pubkey) {
1083 for (i, pk) in gap.drain(..) {
1086 self.inner.db.store_vtxo_key(i, pk).await?;
1087 }
1088 self.inner.db.store_vtxo_key(idx, pubkey).await?;
1089 found.insert(pubkey, keypair);
1090
1091 frontier = idx.saturating_add(1).saturating_add(gap_limit);
1094 } else {
1095 gap.push((idx, pubkey));
1096 }
1097 let Some(next_idx) = idx.checked_add(1) else { break };
1098 idx = next_idx;
1099 }
1100
1101 Ok(found)
1102 }
1103
1104 pub async fn pubkey_keypair(&self, public_key: &PublicKey) -> anyhow::Result<Option<(u32, Keypair)>> {
1116 if let Some(index) = self.inner.db.get_public_key_idx(&public_key).await? {
1117 Ok(Some((index, self.inner.seed.derive_vtxo_keypair(index))))
1118 } else {
1119 Ok(None)
1120 }
1121 }
1122
1123 pub async fn get_vtxo_key(&self, vtxo: impl VtxoRef) -> anyhow::Result<Keypair> {
1134 let bare_vtxo = match vtxo.as_bare_vtxo() {
1135 Some(bare) => bare,
1136 None => Cow::Owned(self.get_vtxo_by_id(vtxo.vtxo_id()).await?.vtxo),
1137 };
1138 let pubkey = self.find_signable_clause(&bare_vtxo).await
1139 .context("VTXO is not signable by wallet")?
1140 .pubkey();
1141 let idx = self.inner.db.get_public_key_idx(&pubkey).await?
1142 .context("VTXO key not found")?;
1143 Ok(self.inner.seed.derive_vtxo_keypair(idx))
1144 }
1145
1146 #[deprecated(note = "use peek_address instead")]
1147 pub async fn peak_address(&self, index: u32) -> anyhow::Result<ark::Address> {
1148 self.peek_address(index).await
1149 }
1150
1151 pub async fn peek_address(&self, index: u32) -> anyhow::Result<ark::Address> {
1155 let properties = self.properties().await?;
1156 let network = properties.network;
1157 let keypair = self.peek_keypair(index).await?;
1158 let mailbox = self.mailbox_identifier();
1159
1160
1161 let (server_pubkey, mailbox_pubkey) =
1162 if let (Some(spk), Some(mpk)) = (properties.server_pubkey, properties.server_mailbox_pubkey) {
1163 (spk, mpk)
1164 } else {
1165 let (_, ark_info) = self.require_server().await?;
1166 (ark_info.server_pubkey, ark_info.mailbox_pubkey)
1167 };
1168
1169 Ok(ark::Address::builder()
1170 .testnet(network != bitcoin::Network::Bitcoin)
1171 .server_pubkey(server_pubkey)
1172 .pubkey_policy(keypair.public_key())
1173 .mailbox(mailbox_pubkey, mailbox, &keypair)
1174 .context("failed to assign mailbox")?
1175 .into_address()
1176 .context("failed to build address")?)
1177 }
1178
1179 pub async fn new_address_with_index(&self) -> anyhow::Result<(ark::Address, u32)> {
1183 let (_, index) = self.derive_store_next_keypair().await?;
1184 let addr = self.peek_address(index).await?;
1185 Ok((addr, index))
1186 }
1187
1188 pub async fn new_address(&self) -> anyhow::Result<ark::Address> {
1190 let (addr, _) = self.new_address_with_index().await?;
1191 Ok(addr)
1192 }
1193
1194 pub async fn sign_message(
1202 &self,
1203 message: &[u8],
1204 address: &ark::Address,
1205 ) -> anyhow::Result<Option<secp256k1::schnorr::Signature>> {
1206 let pubkey = address.policy().user_pubkey();
1207 let Some((_, keypair)) = self.pubkey_keypair(&pubkey).await? else {
1208 return Ok(None);
1209 };
1210 Ok(Some(ark::message::sign(&keypair, message)))
1211 }
1212
1213 pub async fn create(
1222 network: Network,
1223 seed: &WalletSeed,
1224 config: &Config,
1225 db: &dyn BarkPersister,
1226 lock_manager: &dyn LockManager,
1227 allow_unreachable_server: bool,
1228 ) -> anyhow::Result<()> {
1229 trace!("Config: {:?}", config);
1230
1231 let wallet_fingerprint = seed.fingerprint();
1232
1233 let create_guard = lock_manager.lock(
1238 &format!("{}.create", wallet_fingerprint),
1239 Duration::from_secs(5),
1240 ).await.context("wallet initialization already in progress")?;
1241
1242 if let Some(existing) = db.read_properties().await? {
1243 trace!("Existing config: {:?}", existing);
1244 bail!("cannot overwrite already existing config")
1245 }
1246
1247 let (server_pubkey, mailbox_pubkey) = match Self::connect_to_server(&config, network).await {
1253 Ok(conn) => {
1254 let ark_info = conn.ark_info().await;
1255 match check_ark_info_safe(&ark_info, config.vtxo_exit_margin) {
1256 Ok(()) => (Some(ark_info.server_pubkey), Some(ark_info.mailbox_pubkey)),
1257 Err(err) if allow_unreachable_server => {
1258 warn!("server-advertised ArkInfo is unsafe, \
1259 treating as unavailable: {:#}", err);
1260 (None, None)
1261 },
1262 Err(err) => return Err(err),
1263 }
1264 },
1265 Err(_) if allow_unreachable_server => (None, None),
1266 Err(err) => {
1267 bail!("Failed to connect to provided server: {:#}", err);
1268 },
1269 };
1270
1271 let properties = WalletProperties {
1272 network,
1273 fingerprint: wallet_fingerprint,
1274 server_pubkey,
1275 server_mailbox_pubkey: mailbox_pubkey,
1276 };
1277
1278 db.init_wallet(&properties).await.context("cannot init wallet in the database")?;
1280 info!("Created wallet with fingerprint: {}", wallet_fingerprint);
1281 if let Some(pk) = server_pubkey {
1282 info!("Stored server pubkey: {}", pk);
1283 }
1284
1285 drop(create_guard);
1288
1289 Ok(())
1290 }
1291
1292 pub async fn open(
1294 network: Network,
1295 seed: WalletSeed,
1296 config: Config,
1297 args: OpenWalletArgs,
1298 ) -> anyhow::Result<Wallet> {
1299 if !(1..=3).contains(&config.change_vtxo_split_factor) {
1300 bail!("change_vtxo_split_factor must be 1, 2 or 3, got {}",
1301 config.change_vtxo_split_factor,
1302 );
1303 }
1304
1305 let fingerprint = seed.fingerprint();
1306 let lock_manager = if let Some(lm) = args.lock_manager {
1307 lm
1308 } else {
1309 crate::lock_manager::platform_default(args.datadir.as_ref(), Some(fingerprint))
1310 .context("failed to instantiate platform default lock manager")?
1311 };
1312
1313 let db = if let Some(db) = args.persister {
1314 db
1315 } else {
1316 if let Some(ref datadir) = args.datadir {
1317 #[cfg(not(target_arch = "wasm32"))]
1318 if !datadir.exists() && args.create_if_not_exists {
1319 tokio::fs::create_dir_all(datadir).await.with_context(|| format!(
1320 "failed to create datadir at {}", datadir.display(),
1321 ))?;
1322 }
1323 }
1324 crate::persist::platform_default(args.datadir.as_ref(), Some(fingerprint)).await
1325 .context("failed to instantiate platform default persister")?
1326 };
1327
1328 let mut created_now = false;
1329 let properties = if let Some(p) = db.read_properties().await? {
1330 p
1331 } else if args.create_if_not_exists {
1332 Self::create(
1333 network, &seed, &config, &*db, &*lock_manager, args.create_without_server,
1334 ).await.context("error creating new wallet")?;
1335 created_now = true;
1336 db.read_properties().await?
1337 .context("create failed: no wallet properties after Wallet::create was called")?
1338 } else {
1339 bail!("wallet does not exist; use Wallet::create or \
1340 set options.create_if_not_exists to true");
1341 };
1342
1343 if properties.fingerprint != fingerprint {
1344 bail!("incorrect mnemonic")
1345 }
1346
1347 let chain_source = if let Some(ref url) = config.esplora_address {
1348 ChainSourceSpec::Esplora {
1349 url: url.clone(),
1350 }
1351 } else if let Some(ref url) = config.bitcoind_address {
1352 let auth = if let Some(ref c) = config.bitcoind_cookiefile {
1353 bitcoin_ext::rpc::Auth::CookieFile(c.clone())
1354 } else {
1355 bitcoin_ext::rpc::Auth::UserPass(
1356 config.bitcoind_user.clone().context("need bitcoind auth config")?,
1357 config.bitcoind_pass.as_ref().context("need bitcoind auth config")?
1358 .leak_ref().clone(),
1359 )
1360 };
1361 ChainSourceSpec::Bitcoind {
1362 url: url.clone(),
1363 auth,
1364 zmq: config.bitcoind_zmq_address.clone(),
1365 }
1366 } else {
1367 bail!("Need to either provide esplora or bitcoind info");
1368 };
1369
1370 #[cfg(feature = "socks5-proxy")]
1371 let chain_proxy = proxy_for_url(&config.socks5_proxy, chain_source.url())?;
1372 let chain_source_client = ChainSource::new(
1373 chain_source, properties.network, config.fallback_fee_rate,
1374 #[cfg(feature = "socks5-proxy")] chain_proxy.as_deref(),
1375 ).await?;
1376 let chain = Arc::new(chain_source_client);
1377 chain.require_version().await
1378 .context("provided chain source doesn't meet version requirement")?;
1379
1380 let server = tokio::sync::OnceCell::new();
1381
1382 let notifications = NotificationDispatch::new();
1383 let movements = Arc::new(MovementManager::new(db.clone(), notifications.clone()));
1384 let exit = Exit::new(db.clone(), chain.clone(), movements.clone()).await?;
1385
1386 let onchain = args.onchain;
1387 let ret = Wallet { inner: Arc::new(WalletInner {
1388 config, db, lock_manager, seed, exit, movements, notifications, server, chain,
1389 onchain,
1390 daemon: parking_lot::Mutex::new(None),
1391 last_force_exit_scan_tip: tokio::sync::Mutex::new(None),
1392 round_secret_nonces: RoundSecretNonces::new(),
1393 })};
1394
1395 ret.inner.exit.load().await
1396 .context("error loading exit system after opening wallet")?;
1397
1398 let recovery = if !created_now {
1399 RecoveryStatus::NotRun
1400 } else if args.skip_recovery {
1401 info!("Seed-based wallet recovery explicitly skipped");
1402 RecoveryStatus::NotRun
1403 } else {
1404 match ret.recover_from_mailbox().await {
1409 Ok(report) => RecoveryStatus::Completed(report),
1410 Err(e) => {
1411 error!("VTXO recovery from the recovery mailbox failed; funds may be \
1412 missing from this wallet until recovery succeeds: {:#}", e);
1413 RecoveryStatus::Failed(e)
1414 },
1415 }
1416 };
1417
1418 if args.run_daemon {
1419 ret.start_daemon()
1420 .context("failed to start daemon after opening wallet")?;
1421 }
1422
1423 if let Some(callback) = args.on_recovery_finished {
1425 callback(recovery);
1426 }
1427
1428 Ok(ret)
1429 }
1430
1431 pub fn config(&self) -> &Config {
1433 &self.inner.config
1434 }
1435
1436 pub async fn properties(&self) -> anyhow::Result<WalletProperties> {
1438 let properties = self.inner.db.read_properties().await?.context("Wallet is not initialised")?;
1439 Ok(properties)
1440 }
1441
1442 pub fn fingerprint(&self) -> Fingerprint {
1444 self.inner.seed.fingerprint()
1445 }
1446
1447 async fn connect_to_server(
1448 config: &Config,
1449 network: Network,
1450 ) -> anyhow::Result<ServerConnection> {
1451 let server_address = crate::utils::url_with_default_https_scheme(&config.server_address);
1452 let mut builder = ServerConnection::builder()
1453 .address(&server_address)
1454 .network(network);
1455
1456 #[cfg(feature = "socks5-proxy")]
1457 if let Some(proxy) = proxy_for_url(&config.socks5_proxy, &server_address)? {
1458 builder = builder.proxy(&proxy)
1459 }
1460
1461 #[allow(deprecated)]
1462 {
1463 if let Some(ref token) = config.server_access_token {
1464 builder = builder.access_token(token);
1465 }
1466 }
1467
1468 if let Some(ref ua) = config.user_agent {
1469 builder = builder.user_agent(ua);
1470 }
1471
1472 builder.connect().await.map_err(wrap_server_connect_error)
1473 .context("Failed to connect to Ark server")
1474 }
1475
1476 async fn require_server(&self) -> anyhow::Result<(ServerConnection, ArkInfo)> {
1477 let conn = self.inner.server.get_or_try_init(|| async {
1481 let network = self.properties().await?.network;
1482 Self::connect_to_server(&self.inner.config, network).await
1483 .context("You should be connected to Ark server to perform this action")
1484 }).await?.clone();
1485
1486 let ark_info = conn.ark_info().await;
1487 check_ark_info_safe(&ark_info, self.inner.config.vtxo_exit_margin)?;
1488 self.check_and_store_server_keys(&ark_info).await?;
1489
1490 Ok((conn, ark_info))
1491 }
1492
1493 pub async fn refresh_server(&self) -> anyhow::Result<()> {
1494 let srv = self.inner.server.get_or_try_init(|| async {
1500 let properties = self.properties().await?;
1501 Self::connect_to_server(&self.inner.config, properties.network).await
1502 .map_err(anyhow::Error::from)
1503 }).await?;
1504
1505 srv.check_connection().await?;
1506 let ark_info = srv.ark_info().await;
1507 ark_info.fees.validate().context("invalid fee schedule")?;
1508 check_ark_info_safe(&ark_info, self.inner.config.vtxo_exit_margin)?;
1509 self.check_and_store_server_keys(&ark_info).await?;
1510
1511 Ok(())
1512 }
1513
1514 pub fn onchain(&self) -> Option<Arc<tokio::sync::RwLock<dyn OnchainWalletTrait>>> {
1516 self.inner.onchain.clone()
1517 }
1518
1519 pub async fn sync_onchain(&self) -> anyhow::Result<()> {
1521 if let Some(onchain) = self.inner.onchain.as_ref() {
1522 onchain.write().await.sync(self.chain()).await?;
1523 }
1524 Ok(())
1525 }
1526
1527 async fn check_and_store_server_keys(&self, ark_info: &ArkInfo) -> anyhow::Result<()> {
1534 let properties = self.properties().await?;
1535
1536 if let Some(stored_pubkey) = properties.server_pubkey {
1537 if stored_pubkey != ark_info.server_pubkey {
1538 log_server_pubkey_changed_error(stored_pubkey, ark_info.server_pubkey);
1539 bail!("Server public key has changed. You should exit all your VTXOs!");
1540 }
1541 } else {
1542 self.inner.db.set_server_pubkey(ark_info.server_pubkey).await?;
1543 info!("Stored server pubkey for existing wallet: {}", ark_info.server_pubkey);
1544 }
1545
1546 if let Some(stored_mailbox_pubkey) = properties.server_mailbox_pubkey {
1547 if stored_mailbox_pubkey != ark_info.mailbox_pubkey {
1548 log_server_mailbox_pubkey_changed_error(stored_mailbox_pubkey, ark_info.mailbox_pubkey);
1549 bail!("Server mailbox public key has changed.");
1550 }
1551 } else {
1552 self.inner.db.set_server_mailbox_pubkey(ark_info.mailbox_pubkey).await?;
1553 info!("Stored server mailbox pubkey for existing wallet: {}", ark_info.mailbox_pubkey);
1554 }
1555
1556 Ok(())
1557 }
1558
1559 pub async fn ark_info(&self) -> anyhow::Result<Option<ArkInfo>> {
1561 match self.inner.server.get() {
1562 Some(srv) => Ok(Some(srv.ark_info().await)),
1563 None => Ok(None),
1564 }
1565 }
1566
1567 pub async fn require_ark_info(&self) -> anyhow::Result<ArkInfo> {
1573 let (_, ark_info) = self.require_server().await?;
1574 Ok(ark_info)
1575 }
1576
1577 pub async fn balance(&self) -> anyhow::Result<Balance> {
1581 let vtxos = self.vtxos().await?;
1582
1583 let spendable = {
1584 let mut v = vtxos.iter().collect();
1585 VtxoStateKind::Spendable.filter_vtxos(&mut v).await?;
1586 v.into_iter().map(|v| v.amount()).sum::<Amount>()
1587 };
1588
1589 let pending_lightning_send = self.pending_lightning_send_vtxos().await?.iter()
1590 .map(|v| v.amount())
1591 .sum::<Amount>();
1592
1593 let claimable_lightning_receive = self.claimable_lightning_receive_balance().await?;
1594
1595 let pending_board = self.pending_board_vtxos().await?.iter()
1596 .map(|v| v.amount())
1597 .sum::<Amount>();
1598
1599 let pending_in_round = self.pending_round_balance().await?;
1600
1601 let pending_exit = self.exit_mgr().try_pending_total();
1602
1603 Ok(Balance {
1604 spendable,
1605 pending_in_round,
1606 pending_lightning_send,
1607 claimable_lightning_receive,
1608 pending_exit,
1609 pending_board,
1610 })
1611 }
1612
1613 pub async fn validate_vtxo(&self, vtxo: &Vtxo<Full>) -> Result<(), VtxoValidationError> {
1615 let tx = self.inner.chain.get_tx(&vtxo.chain_anchor().txid).await
1616 .map_err(VtxoValidationError::Chain)?
1617 .ok_or(VtxoValidationError::AnchorNotFound)?;
1618
1619 vtxo.validate(&tx).map_err(VtxoValidationError::Invalid)
1620 }
1621
1622 pub(crate) async fn fetch_vtxo_spend_state(
1626 &self,
1627 vtxo_id: VtxoId,
1628 keypair: &Keypair,
1629 ) -> anyhow::Result<VtxoSpendState> {
1630 let (mut srv, _) = self.require_server().await?;
1631 let attestation = VtxoStatusAttestation::new(vtxo_id, keypair);
1632 let resp = srv.client.get_vtxo_status(protos::GetVtxoStatusRequest {
1633 vtxo_id: vtxo_id.to_bytes().to_vec(),
1634 attestation: attestation.serialize(),
1635 }).await.with_context(|| format!("error fetching status for vtxo {vtxo_id}"))?.into_inner();
1636
1637 VtxoSpendState::try_from(resp.spend_state).map_err(|_| anyhow::anyhow!(
1638 "server returned unknown spend state {} for vtxo {vtxo_id}; this wallet may \
1639 need updating", resp.spend_state,
1640 ))
1641 }
1642
1643 pub async fn get_vtxo_by_id(&self, vtxo_id: VtxoId) -> anyhow::Result<WalletVtxo> {
1645 let vtxo = self.inner.db.get_wallet_vtxo(vtxo_id).await
1646 .with_context(|| format!("Error when querying vtxo {} in database", vtxo_id))?
1647 .with_context(|| format!("The VTXO with id {} cannot be found", vtxo_id))?;
1648 Ok(vtxo)
1649 }
1650
1651 pub async fn get_full_vtxo(&self, vtxo_id: VtxoId) -> anyhow::Result<Vtxo<Full>> {
1659 self.inner.db.get_full_vtxo(vtxo_id).await
1660 .with_context(|| format!("Error when querying full vtxo {} in database", vtxo_id))?
1661 .with_context(|| format!("The VTXO with id {} cannot be found", vtxo_id))
1662 }
1663
1664 pub async fn get_full_vtxos<V: VtxoRef>(
1666 &self,
1667 vtxos: impl IntoIterator<Item = V>,
1668 ) -> anyhow::Result<Vec<Vtxo<Full>>> {
1669 let ids = vtxos.into_iter().map(|v| v.vtxo_id()).collect::<Vec<_>>();
1670 self.inner.db.get_full_vtxos(&ids).await
1671 .with_context(||
1672 format!("Error when querying full vtxos in database with IDs: {:?}", ids)
1673 )
1674 }
1675
1676 #[deprecated(since="0.1.0-beta.5", note = "Use Wallet::history instead")]
1678 pub async fn movements(&self) -> anyhow::Result<Vec<Movement>> {
1679 self.history().await
1680 }
1681
1682 pub async fn history(&self) -> anyhow::Result<Vec<Movement>> {
1684 Ok(self.inner.db.get_all_movements().await?)
1685 }
1686
1687 pub async fn update_history_metadata(
1707 &self,
1708 movement_id: MovementId,
1709 patch: &serde_json::Value,
1710 ) -> anyhow::Result<()> {
1711 self.inner.movements.patch_metadata(movement_id, patch).await?;
1712 Ok(())
1713 }
1714
1715 pub async fn history_by_payment_method(
1717 &self,
1718 payment_method: &PaymentMethod,
1719 ) -> anyhow::Result<Vec<Movement>> {
1720 let mut ret = self.inner.db.get_movements_by_payment_method(payment_method).await?;
1721 ret.sort_by_key(|m| m.id);
1722 Ok(ret)
1723 }
1724
1725 pub async fn all_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1727 Ok(self.inner.db.get_all_vtxos().await?)
1728 }
1729
1730 pub async fn vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1732 Ok(self.inner.db.get_vtxos_by_state(&VtxoStateKind::UNSPENT_STATES).await?)
1733 }
1734
1735 pub async fn vtxos_with(&self, filter: &impl FilterVtxos) -> anyhow::Result<Vec<WalletVtxo>> {
1737 let mut vtxos = self.vtxos().await?;
1738 filter.filter_vtxos(&mut vtxos).await.context("error filtering vtxos")?;
1739 Ok(vtxos)
1740 }
1741
1742 pub async fn spendable_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1744 Ok(self.vtxos_with(&VtxoStateKind::Spendable).await?)
1745 }
1746
1747 pub async fn spendable_vtxos_with(
1749 &self,
1750 filter: &impl FilterVtxos,
1751 ) -> anyhow::Result<Vec<WalletVtxo>> {
1752 let mut vtxos = self.spendable_vtxos().await?;
1753 filter.filter_vtxos(&mut vtxos).await.context("error filtering vtxos")?;
1754 Ok(vtxos)
1755 }
1756
1757 pub async fn get_expiring_vtxos(
1759 &self,
1760 threshold: BlockHeight,
1761 ) -> anyhow::Result<Vec<WalletVtxo>> {
1762 let expiry = self.inner.chain.tip().await? + threshold;
1763 let filter = VtxoFilter::new(&self).expires_before(expiry);
1764 Ok(self.spendable_vtxos_with(&filter).await?)
1765 }
1766
1767 pub async fn maintenance(&self) -> anyhow::Result<()> {
1773 info!("Starting wallet maintenance in interactive mode");
1774 self.sync().await;
1775
1776 let rounds = self.progress_pending_rounds(None).await;
1778 if let Err(e) = rounds.as_ref() {
1779 warn!("Error progressing pending rounds: {:#}", e);
1780 }
1781
1782 let states = self.inner.db.get_pending_round_state_ids().await?;
1784 for id in states {
1785 debug!("Cancelling pending round participation {}", id);
1786 let mut state = match self.lock_wait_round_state(id).await {
1787 Ok(Some(s)) => s,
1788 Ok(None) => continue, Err(e) => {
1790 warn!("Failed to lock round state with id {}: {:#}", id, e);
1791 continue;
1792 }
1793 };
1794 if let Err(e) = state.state_mut().try_cancel(self).await {
1795 warn!("Error cancelling pending round: {:#}", e);
1796 }
1797 }
1798
1799 let refresh = self.maintenance_refresh().await;
1801 if let Err(e) = refresh.as_ref() {
1802 warn!("Error refreshing VTXOs: {:#}", e);
1803 }
1804
1805 if rounds.is_err() || refresh.is_err() {
1806 bail!("Maintenance encountered errors.\nprogress_rounds: {:#?}\nrefresh: {:#?}",
1807 rounds, refresh,
1808 );
1809 }
1810
1811 Ok(())
1812 }
1813
1814 pub async fn maintenance_delegated(&self) -> anyhow::Result<()> {
1821 info!("Starting wallet maintenance in delegated mode");
1822 self.sync().await;
1823 let rounds = self.progress_pending_rounds(None).await;
1824 if let Err(e) = rounds.as_ref() {
1825 warn!("Error progressing pending rounds: {:#}", e);
1826 }
1827 let refresh = self.maybe_schedule_maintenance_refresh_delegated().await;
1828 if let Err(e) = refresh.as_ref() {
1829 warn!("Error refreshing VTXOs: {:#}", e);
1830 }
1831
1832 if rounds.is_err() || refresh.is_err() {
1833 bail!("Delegated maintenance encountered errors.\n\
1834 progress_rounds: {:#?}\nrefresh: {:#?}",
1835 rounds, refresh,
1836 );
1837 }
1838
1839 Ok(())
1840 }
1841
1842 pub(crate) async fn join_round_for_maintenance_refresh(
1857 &self,
1858 attempt: &RoundAttempt,
1859 ) -> anyhow::Result<Option<RoundStateId>> {
1860 self.maintenance_refresh_retry_loop(|part| async move {
1861 info!("Joining round {} for maintenance refresh ({} vtxos)",
1862 attempt.round_seq, part.inputs.len());
1863 Ok(Some(self.join_attempt_interactive(
1864 part, attempt, Some(RoundMovement::Refresh),
1865 ).await?.id()))
1866 }).await.context("failed to join round for maintenance refresh")
1867 }
1868
1869 pub async fn maybe_schedule_maintenance_refresh_delegated(
1877 &self,
1878 ) -> anyhow::Result<Option<RoundStateId>> {
1879 self.maintenance_refresh_retry_loop(|part| async move {
1880 info!("Scheduling delegated maintenance refresh ({} vtxos)", part.inputs.len());
1881 Ok(Some(self.join_next_round_delegated(part, Some(RoundMovement::Refresh)).await?.id()))
1882 }).await.context("failed to schedule delegated maintenance refresh")
1883 }
1884
1885 async fn maintenance_refresh_retry_loop<F, Fut>(
1893 &self,
1894 attempt_refresh: F,
1895 ) -> anyhow::Result<Option<RoundStateId>>
1896 where
1897 F: Fn(RoundParticipation) -> Fut,
1898 Fut: Future<Output = anyhow::Result<Option<RoundStateId>>>,
1899 {
1900 let mut excluded = HashSet::new();
1901 for _ in 0..10 {
1902 let vtxos = self.get_vtxos_to_refresh_with_excluded(excluded.iter().copied()).await?;
1903 match (vtxos.is_empty(), excluded.is_empty()) {
1904 (true, false) => {
1908 warn!("no VTXOs to refresh after exclusions: {:?}", excluded);
1909 bail!("no VTXOs to refresh after excluding: {:?}", excluded);
1910 },
1911 (true, true) => return Ok(None),
1913 (false, _) => {},
1915 }
1916 let part = match self.build_refresh_participation(vtxos).await? {
1917 Some(participation) => participation,
1918 None => return Ok(None),
1919 };
1920
1921 match attempt_refresh(part).await {
1922 Ok(state_id) => return Ok(state_id),
1923 Err(e) => {
1924 let rejected = rejected_vtxos_from_error(&e).into_iter()
1925 .filter(|id| !excluded.contains(id))
1926 .collect::<Vec<_>>();
1927 if rejected.is_empty() {
1928 return Err(e);
1929 }
1930 warn!("Maintenance refresh rejected {} unusable input(s) ({:?}); \
1931 retrying without them", rejected.len(), rejected);
1932 excluded.extend(rejected);
1933 },
1934 }
1935 }
1936 bail!("Maintenance refresh failed after 10 retries");
1937 }
1938
1939 pub async fn maintenance_refresh(&self) -> anyhow::Result<Option<RoundStatus>> {
1951 if self.get_vtxos_to_refresh().await?.is_empty() {
1952 return Ok(None);
1953 }
1954
1955 info!("Waiting for round to perform maintenance refresh...");
1956 let mut events = self.subscribe_round_events().await?;
1957 while let Some(event) = events.next().await {
1958 let event = event.context("error on round event stream")?;
1959 if let RoundEvent::Attempt(a) = event && a.attempt_seq == 0 {
1960 debug!("Round {} started, triggering maintenance refresh", a.round_seq);
1961 let state_id = match self.join_round_for_maintenance_refresh(&a).await? {
1962 Some(id) => id,
1963 None => return Ok(None),
1964 };
1965 let state = self.lock_wait_round_state(state_id).await?
1968 .context("maintenance refresh round state vanished after joining")?;
1969 return Ok(Some(self.drive_round_state(state, &mut events).await?));
1970 }
1971 }
1972 Ok(None)
1973 }
1974
1975 pub async fn sync(&self) {
1981 self.inner.chain.invalidate_caches().await;
1982
1983 futures::join!(
1984 async {
1985 if let Err(e) = self.inner.chain.update_fee_rates(self.inner.config.fallback_fee_rate).await {
1988 warn!("Error updating fee rates: {:#}", e);
1989 }
1990 },
1991 async {
1992 if let Err(e) = self.sync_mailbox().await {
1993 warn!("Error in mailbox sync: {:#}", e);
1994 }
1995 },
1996 async {
1997 if let Err(e) = self.sync_pending_rounds().await {
1998 warn!("Error while trying to progress rounds awaiting confirmations: {:#}", e);
1999 }
2000 },
2001 async {
2002 if let Err(e) = self.sync_pending_lightning_send_vtxos().await {
2003 warn!("Error syncing pending lightning payments: {:#}", e);
2004 }
2005 },
2006 async {
2007 if let Err(e) = self.sync_pending_arkoor_sends().await {
2008 warn!("Error syncing pending arkoor sends: {:#}", e);
2009 }
2010 },
2011 async {
2012 if let Err(e) = self.try_claim_all_lightning_receives(false).await {
2013 warn!("Error claiming pending lightning receives: {:#}", e);
2014 }
2015 },
2016 async {
2017 if let Err(e) = self.sync_pending_boards().await {
2018 warn!("Error syncing pending boards: {:#}", e);
2019 }
2020 },
2021 async {
2022 if let Err(e) = self.sync_pending_offboards().await {
2023 warn!("Error syncing pending offboards: {:#}", e);
2024 }
2025 },
2026 async {
2027 if let Err(e) = self.sync_force_exited_vtxos().await {
2028 warn!("Error scanning for on-chain-exited VTXOs: {:#}", e);
2029 }
2030 },
2031 async {
2032 if let Err(e) = self.catchup_recovery_vtxos().await {
2036 warn!("Failed to catch up recovery VTXOs with server: {:#}", e);
2037 }
2038 }
2039 );
2040 }
2041
2042 pub async fn sync_exits(&self) -> anyhow::Result<()> {
2048 self.exit_mgr().sync(&self).await?;
2049 Ok(())
2050 }
2051
2052 pub async fn progress_exits(&self) -> anyhow::Result<()> {
2057 self.exit_mgr().progress_exits_with_cpfp(&self, None).await?;
2058 Ok(())
2059 }
2060
2061 pub async fn sync_force_exited_vtxos(&self) -> anyhow::Result<()> {
2074 let tip = self.inner.chain.tip().await?;
2076 let mut lock = self.inner.last_force_exit_scan_tip.lock().await;
2077 if *lock == Some(tip) {
2078 return Ok(());
2079 }
2080
2081 let exiting = self.exit_mgr().get_exit_vtxo_ids().await;
2083 let vtxos = self.inner.db.get_vtxos_by_state(&[VtxoStateKind::Spendable]).await?
2084 .into_iter()
2085 .filter(|v| !exiting.contains(&v.vtxo.id()));
2086
2087 let mut checked = FuturesUnordered::new();
2089 for wv in vtxos {
2090 let chain = self.inner.chain.clone();
2091 checked.push(async move {
2092 let txid = wv.vtxo_id().to_point().txid;
2093 let status = chain.tx_status(txid).await;
2094 (wv, status)
2095 });
2096 }
2097
2098 let mut to_exit = Vec::new();
2099 while let Some((vtxo, status)) = futures::StreamExt::next(&mut checked).await {
2100 match status {
2101 Ok(TxStatus::NotFound) => {},
2102 Ok(_) => {
2103 info!("VTXO {} was exited on-chain without us; routing it to a claimable exit",
2104 vtxo.vtxo.id(),
2105 );
2106 to_exit.push(vtxo.vtxo);
2107 },
2108 Err(e) => warn!("Could not check on-chain status of VTXO {}: {:#}",
2109 vtxo.vtxo.id(), e,
2110 ),
2111 }
2112 }
2113
2114 if !to_exit.is_empty() {
2115 self.exit_mgr().start_exit_for_vtxos(&to_exit).await
2116 .context("failed to start exit for on-chain-exited VTXOs")?;
2117
2118 *lock = Some(tip);
2119 self.sync_exits().await
2120 .context("failed to sync exits after starting new ones")?;
2121 } else {
2122 *lock = Some(tip);
2123 }
2124
2125 Ok(())
2126 }
2127
2128 pub async fn dangerous_drop_vtxo(&self, vtxo_id: VtxoId) -> anyhow::Result<()> {
2131 warn!("Drop vtxo {} from the database", vtxo_id);
2132 self.inner.db.remove_vtxo(vtxo_id).await?;
2133 Ok(())
2134 }
2135
2136 pub async fn dangerous_drop_all_vtxos(&self) -> anyhow::Result<()> {
2139 warn!("Dropping all vtxos from the db...");
2140 for vtxo in self.vtxos().await? {
2141 self.inner.db.remove_vtxo(vtxo.id()).await?;
2142 }
2143
2144 self.exit_mgr().dangerous_clear_exit().await?;
2145 Ok(())
2146 }
2147
2148 async fn has_counterparty_risk(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<bool> {
2156 for past_pks in vtxo.past_arkoor_pubkeys() {
2157 let mut owns_any = false;
2158 for past_pk in past_pks {
2159 if self.inner.db.get_public_key_idx(&past_pk).await?.is_some() {
2160 owns_any = true;
2161 break;
2162 }
2163 }
2164 if !owns_any {
2165 return Ok(true);
2166 }
2167 }
2168
2169 let my_clause = self.find_signable_clause(vtxo).await;
2170 Ok(!my_clause.is_some())
2171 }
2172
2173 pub async fn build_refresh_participation<V: VtxoRef>(
2174 &self,
2175 vtxos: impl IntoIterator<Item = V>,
2176 ) -> anyhow::Result<Option<RoundParticipation>> {
2177 self.inner_build_refresh_participation(vtxos, None).await
2178 }
2179
2180 pub async fn build_scheduled_refresh_participation<V: VtxoRef>(
2181 &self,
2182 vtxos: impl IntoIterator<Item = V>,
2183 height: BlockHeight,
2184 ) -> anyhow::Result<Option<RoundParticipation>> {
2185 self.inner_build_refresh_participation(vtxos, Some(height)).await
2186 }
2187
2188 async fn inner_build_refresh_participation<V: VtxoRef>(
2189 &self,
2190 vtxos: impl IntoIterator<Item = V>,
2191 height: Option<BlockHeight>,
2192 ) -> anyhow::Result<Option<RoundParticipation>> {
2193 let (vtxos, total_amount) = {
2194 let iter = vtxos.into_iter();
2195 let size_hint = iter.size_hint();
2196 let mut vtxos = Vec::<Vtxo<Full>>::with_capacity(size_hint.1.unwrap_or(size_hint.0));
2197 let mut amount = Amount::ZERO;
2198 for vref in iter {
2199 let id = vref.vtxo_id();
2204 if vtxos.iter().any(|v| v.id() == id) {
2205 bail!("duplicate VTXO id: {}", id);
2206 }
2207 let vtxo = if let Some(vtxo) = vref.into_full_vtxo() {
2208 vtxo
2209 } else {
2210 self.inner.db.get_full_vtxo(id).await?
2213 .with_context(|| format!("vtxo with id {} not found", id))?
2214 };
2215 amount += vtxo.amount();
2216 vtxos.push(vtxo);
2217 }
2218 (vtxos, amount)
2219 };
2220
2221 if vtxos.is_empty() {
2222 info!("Skipping refresh since no VTXOs are provided.");
2223 return Ok(None);
2224 }
2225 ensure!(total_amount >= VTXO_DUST,
2226 "vtxo amount must be at least {} to participate in a round",
2227 VTXO_DUST,
2228 );
2229
2230 let (_, ark_info) = self.require_server().await?;
2232 let refresh_height = match height {
2233 Some(height) => height,
2234 None => self.inner.chain.tip().await?,
2235 };
2236
2237 let vtxo_fee_infos = vtxos.iter()
2238 .map(|v| VtxoFeeInfo::from_vtxo_and_tip(v, refresh_height));
2239 let fee = ark_info.fees.refresh.calculate(vtxo_fee_infos).context("fee overflowed")?;
2240 let output_amount = validate_and_subtract_fee_min_dust(total_amount, fee, VTXO_DUST)?;
2241
2242 info!("Refreshing {} VTXOs (total amount = {}, fee = {}, output = {}).",
2243 vtxos.len(), total_amount, fee, output_amount,
2244 );
2245 let (user_keypair, _) = self.derive_store_next_keypair().await?;
2246 let req = VtxoRequest {
2247 policy: VtxoPolicy::Pubkey(PubkeyVtxoPolicy { user_pubkey: user_keypair.public_key() }),
2248 amount: output_amount,
2249 };
2250
2251 Ok(Some(RoundParticipation {
2252 inputs: vtxos,
2253 outputs: vec![req],
2254 unblinded_mailbox_id: None,
2255 }))
2256 }
2257
2258 pub async fn refresh_vtxos<V: VtxoRef>(
2263 &self,
2264 vtxos: impl IntoIterator<Item = V>,
2265 ) -> anyhow::Result<Option<RoundStatus>> {
2266 let participation = match self.build_refresh_participation(vtxos).await? {
2267 Some(participation) => participation,
2268 None => return Ok(None),
2269 };
2270
2271 Ok(Some(self.participate_round(participation, Some(RoundMovement::Refresh)).await?))
2272 }
2273
2274 pub async fn refresh_vtxos_delegated<V: VtxoRef>(
2280 &self,
2281 vtxos: impl IntoIterator<Item = V>,
2282 ) -> anyhow::Result<Option<StoredRoundState<Unlocked>>> {
2283 let part = match self.build_refresh_participation(vtxos).await? {
2284 Some(participation) => participation,
2285 None => return Ok(None),
2286 };
2287
2288 Ok(Some(self.join_delegated_round(
2289 part, Some(RoundMovement::Refresh), None,
2290 ).await?))
2291 }
2292
2293 pub async fn refresh_vtxos_scheduled<V: VtxoRef>(
2296 &self,
2297 vtxos: impl IntoIterator<Item = V>,
2298 scheduled_height: BlockHeight,
2299 ) -> anyhow::Result<Option<StoredRoundState<Unlocked>>> {
2300 let part = match self
2301 .build_scheduled_refresh_participation(vtxos, scheduled_height).await?
2302 {
2303 Some(participation) => participation,
2304 None => return Ok(None),
2305 };
2306
2307 Ok(Some(self.join_delegated_round(
2308 part, Some(RoundMovement::Refresh), Some(scheduled_height),
2309 ).await?))
2310 }
2311
2312 pub async fn get_vtxos_to_refresh(&self) -> anyhow::Result<Vec<WalletVtxo>> {
2315 let vtxos = self.spendable_vtxos_with(&RefreshStrategy::should_refresh_if_must(
2316 self,
2317 self.inner.chain.tip().await?,
2318 self.inner.chain.fee_rates().await.fast,
2319 )).await?;
2320 Ok(vtxos)
2321 }
2322
2323 pub async fn get_vtxos_to_refresh_with_excluded<V: VtxoRef>(
2326 &self,
2327 exclude: impl IntoIterator<Item = V>,
2328 ) -> anyhow::Result<Vec<WalletVtxo>> {
2329 let mut vtxos = self.get_vtxos_to_refresh().await?;
2330 for v in exclude.into_iter() {
2331 if let Some(index) = vtxos.iter().position(|vtxo| vtxo.id() == v.vtxo_id()) {
2332 vtxos.swap_remove(index);
2333 }
2334 }
2335 Ok(vtxos)
2336 }
2337
2338 pub async fn get_first_expiring_vtxo_blockheight(
2340 &self,
2341 ) -> anyhow::Result<Option<BlockHeight>> {
2342 Ok(self.spendable_vtxos().await?.iter().map(|v| v.expiry_height()).min())
2343 }
2344
2345 pub async fn get_next_required_refresh_blockheight(
2348 &self,
2349 ) -> anyhow::Result<Option<BlockHeight>> {
2350 let first_expiry = self.get_first_expiring_vtxo_blockheight().await?;
2351 Ok(first_expiry.map(|h| {
2352 h.saturating_sub(self.inner.config.vtxo_refresh_expiry_threshold as BlockHeight)
2353 }))
2354 }
2355
2356 async fn spend_input_selection(&self) -> anyhow::Result<InputSelection> {
2360 let mut selection = InputSelection::new();
2361 if let Some(info) = self.ark_info().await? {
2362 selection = selection.max_exit_depth(info.max_vtxo_exit_depth);
2363 }
2364 Ok(selection)
2365 }
2366
2367 async fn select_any_vtxos_to_cover(
2369 &self,
2370 amount: Amount,
2371 ) -> anyhow::Result<Vec<WalletVtxo>> {
2372 self.spend_input_selection().await?.select(self.spendable_vtxos().await?, amount)
2373 }
2374
2375 async fn select_any_vtxos_to_cover_with_fee<F>(
2380 &self,
2381 amount: Amount,
2382 calc_fee: F,
2383 ) -> anyhow::Result<(Vec<WalletVtxo>, Amount)>
2384 where
2385 F: for<'a> Fn(Amount, SelectedFeeInfos<'a>) -> anyhow::Result<Amount>,
2386 {
2387 let tip = self.inner.chain.tip().await?;
2388 self.spend_input_selection().await?
2389 .fee_scheme(tip, calc_fee)
2390 .select(self.spendable_vtxos().await?, amount)
2391 }
2392
2393 pub fn start_daemon(&self) -> anyhow::Result<()> {
2403 let mut daemon = self.inner.daemon.lock();
2404 if daemon.is_some() {
2405 warn!("Called Wallet::start_daemon while daemon was already running.");
2406 return Ok(());
2407 }
2408
2409 let handle = crate::daemon::start_daemon(self);
2410 let _ = daemon.insert(handle);
2411
2412 Ok(())
2413 }
2414
2415 pub fn stop_daemon(&self) {
2417 let mut daemon = self.inner.daemon.lock();
2418 if let Some(handle) = daemon.take() {
2419 handle.stop();
2420 }
2421 }
2422
2423 pub async fn stop_daemon_wait(&self) -> anyhow::Result<()> {
2426 let handle = self.inner.daemon.lock().take();
2427 if let Some(handle) = handle {
2428 handle.stop_wait().await?;
2429 }
2430 Ok(())
2431 }
2432
2433 async fn catchup_recovery_vtxos(&self) -> anyhow::Result<()> {
2451 let mut ids = self.inner.db.get_unregistered_vtxo_ids().await?;
2452 if ids.is_empty() {
2453 return Ok(());
2454 }
2455
2456 let in_progress_boards = self.boards_in_progress().await?;
2459 ids.retain(|id| !in_progress_boards.iter().any(|b| b.vtxo_id == *id));
2460 if ids.is_empty() {
2461 return Ok(());
2462 }
2463
2464 let posted = self.post_recovery_vtxo_ids(ids.iter().copied()).await
2469 .context("failed to post recovery vtxo IDs");
2470 let registered = self.register_recovery_vtxo_chains(&ids, posted.is_ok()).await;
2471
2472 match (posted, registered) {
2473 (Ok(()), registered) => registered,
2474 (posted, Ok(())) => posted,
2475 (Err(posted), Err(registered)) => {
2476 Err(registered.context(format!("mailbox post also failed: {:#}", posted)))
2477 },
2478 }
2479 }
2480
2481 async fn register_recovery_vtxo_chains(
2487 &self,
2488 ids: &[VtxoId],
2489 mark_registered: bool,
2490 ) -> anyhow::Result<()> {
2491 const CHUNK_SIZE: usize = 20;
2492 let mut failed = 0;
2493 for chunk_ids in ids.chunks(CHUNK_SIZE) {
2494 let chunk = self.inner.db.get_full_vtxos(chunk_ids).await
2497 .context("failed to load full vtxos for recovery registration")?;
2498 ensure!(chunk.len() == chunk_ids.len(),
2499 "loaded {} full vtxos for {} ids", chunk.len(), chunk_ids.len(),
2500 );
2501
2502 let mut succeeded = Vec::with_capacity(chunk.len());
2503 match self.register_vtxo_transactions_with_server(&chunk).await {
2504 Ok(()) => succeeded.extend(chunk.iter().map(|v| v.id())),
2505 Err(e) => {
2506 debug!("Failed to register chunk of {} vtxo transactions, \
2507 retrying one by one: {:#}", chunk.len(), e,
2508 );
2509 for vtxo in &chunk {
2510 match self.register_vtxo_transactions_with_server(
2511 std::slice::from_ref(vtxo),
2512 ).await {
2513 Ok(()) => succeeded.push(vtxo.id()),
2514 Err(e) => {
2515 error!("Failed to register vtxo {} transactions with server; \
2516 recovery from seed may miss it until registration succeeds: {:#}",
2517 vtxo.id(), e,
2518 );
2519 failed += 1;
2520 },
2521 }
2522 }
2523 },
2524 }
2525 if mark_registered && !succeeded.is_empty() {
2526 self.inner.db.mark_vtxos_registered(&succeeded).await
2527 .context("failed to mark vtxos as registered for recovery")?;
2528 }
2529 }
2530 if failed > 0 {
2531 bail!("failed to register {} of {} vtxo transactions", failed, ids.len());
2532 }
2533 Ok(())
2534 }
2535
2536 pub async fn register_vtxo_transactions_with_server(
2540 &self,
2541 vtxos: &[impl AsRef<Vtxo<Full>>],
2542 ) -> anyhow::Result<()> {
2543 if vtxos.is_empty() {
2544 return Ok(());
2545 }
2546
2547 let (mut srv, _) = self.require_server().await?;
2548 srv.client.register_vtxo_transactions(protos::RegisterVtxoTransactionsRequest {
2549 vtxos: vtxos.iter().map(|v| v.as_ref().serialize()).collect(),
2550 }).await.context("failed to register vtxo transactions")?;
2551
2552 Ok(())
2553 }
2554}
2555
2556fn wrap_server_connect_error(err: ConnectError) -> anyhow::Error {
2557 match err {
2558 ConnectError::CreateEndpoint(CreateEndpointError::NoTransportBackend) => {
2559 anyhow!(MISSING_SERVER_TRANSPORT_HELP)
2560 },
2561 other => anyhow::Error::from(other),
2562 }
2563}
2564
2565impl std::ops::Drop for WalletInner {
2566 fn drop(&mut self) {
2567 if let Some(handle) = self.daemon.lock().take() {
2574 handle.stop();
2575 }
2576 }
2577}
2578
2579#[cfg(test)]
2580mod tests {
2581 use bitcoin::{Amount, FeeRate, Network};
2582 use bitcoin::secp256k1::PublicKey;
2583
2584 use ark::ArkInfo;
2585 use server_rpc::client::CreateEndpointError;
2586
2587 use super::{
2588 check_ark_info_safe, wrap_server_connect_error,
2589 MAX_NB_ROUND_NONCES, MIN_MAINNET_VTXO_EXIT_DELTA, MISSING_SERVER_TRANSPORT_HELP,
2590 };
2591
2592 #[test]
2593 fn no_transport_connect_error_is_reworded_for_wallet_users() {
2594 let err = wrap_server_connect_error(CreateEndpointError::NoTransportBackend.into());
2595 assert!(err.to_string().contains(MISSING_SERVER_TRANSPORT_HELP));
2596 assert!(err.to_string().contains("feature `bark-wallet/native` or `bark-wallet/wasm-web`"));
2597 }
2598
2599 fn ark_info_with_lifetime(vtxo_lifetime: u16, required_board_confirmations: usize) -> ArkInfo {
2600 use std::str::FromStr as _;
2601 let pk = PublicKey::from_str(
2602 "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
2603 ).unwrap();
2604 #[allow(deprecated)]
2605 ArkInfo {
2606 network: Network::Regtest,
2607 server_pubkey: pk,
2608 mailbox_pubkey: pk,
2609 round_interval: std::time::Duration::from_secs(60),
2610 nb_round_nonces: 8,
2611 vtxo_exit_delta: 48,
2612 vtxo_lifetime,
2613 htlc_send_expiry_delta: 100,
2614 htlc_expiry_delta: 100,
2615 max_vtxo_amount: None,
2616 required_board_confirmations,
2617 max_user_invoice_cltv_delta: 100,
2618 min_board_amount: Amount::from_sat(1000),
2619 vtxo_expiry_delta: vtxo_lifetime,
2620 offboard_feerate: FeeRate::ZERO,
2621 max_offboard_inputs: 1,
2622 ln_receive_anti_dos_required: false,
2623 fees: Default::default(),
2624 max_vtxo_exit_depth: 10,
2625 tos_link: None,
2626 }
2627 }
2628
2629 #[test]
2630 fn ark_info_with_zero_lifetime_is_rejected() {
2631 let mut ai = ark_info_with_lifetime(0, 6);
2632 ai.network = Network::Bitcoin;
2633 ai.vtxo_exit_delta = MIN_MAINNET_VTXO_EXIT_DELTA;
2634 let err = check_ark_info_safe(&ai, 12).unwrap_err().to_string();
2635 assert!(err.contains("unsafe"), "unexpected error: {err}");
2636 }
2637
2638 #[test]
2639 fn ark_info_at_the_boundary_is_rejected() {
2640 let mut ai = ark_info_with_lifetime(6 + 12, 6);
2643 ai.network = Network::Bitcoin;
2644 ai.vtxo_exit_delta = MIN_MAINNET_VTXO_EXIT_DELTA;
2645 assert!(check_ark_info_safe(&ai, 12).is_err());
2646 }
2647
2648 #[test]
2649 fn ark_info_one_block_over_the_boundary_is_accepted() {
2650 let ai = ark_info_with_lifetime(6 + 12 + 1, 6);
2651 check_ark_info_safe(&ai, 12).unwrap();
2652 }
2653
2654 #[test]
2655 fn ark_info_generous_lifetime_is_accepted() {
2656 let ai = ark_info_with_lifetime(4032, 6);
2657 check_ark_info_safe(&ai, 12).unwrap();
2658 }
2659
2660 #[test]
2661 fn ark_info_with_zero_vtxo_exit_delta_is_rejected() {
2662 let mut ai = ark_info_with_lifetime(4032, 6);
2663 ai.vtxo_exit_delta = 0;
2664 let err = check_ark_info_safe(&ai, 12).unwrap_err().to_string();
2665 assert!(err.contains("vtxo_exit_delta"), "unexpected error: {err}");
2666 }
2667
2668 #[test]
2669 fn ark_info_below_mainnet_vtxo_exit_delta_is_rejected() {
2670 let mut ai = ark_info_with_lifetime(4032, 6);
2671 ai.network = Network::Bitcoin;
2672 ai.vtxo_exit_delta = MIN_MAINNET_VTXO_EXIT_DELTA - 1;
2673 let err = check_ark_info_safe(&ai, 12).unwrap_err().to_string();
2674 assert!(err.contains("mainnet minimum"), "unexpected error: {err}");
2675 }
2676
2677 #[test]
2678 fn ark_info_at_mainnet_vtxo_exit_delta_is_accepted() {
2679 let mut ai = ark_info_with_lifetime(4032, 6);
2680 ai.network = Network::Bitcoin;
2681 ai.vtxo_exit_delta = MIN_MAINNET_VTXO_EXIT_DELTA;
2682 check_ark_info_safe(&ai, 12).unwrap();
2683 }
2684
2685 #[test]
2686 fn ark_info_with_zero_nb_round_nonces_is_rejected() {
2687 let mut ai = ark_info_with_lifetime(4032, 6);
2688 ai.nb_round_nonces = 0;
2689 let err = check_ark_info_safe(&ai, 12).unwrap_err().to_string();
2690 assert!(err.contains("nb_round_nonces"), "unexpected error: {err}");
2691 }
2692
2693 #[test]
2694 fn ark_info_at_nb_round_nonces_cap_is_accepted() {
2695 let mut ai = ark_info_with_lifetime(4032, 6);
2696 ai.nb_round_nonces = MAX_NB_ROUND_NONCES;
2697 check_ark_info_safe(&ai, 12).unwrap();
2698 }
2699
2700 #[test]
2701 fn ark_info_over_nb_round_nonces_cap_is_rejected() {
2702 let mut ai = ark_info_with_lifetime(4032, 6);
2703 ai.network = Network::Bitcoin;
2704 ai.vtxo_exit_delta = MIN_MAINNET_VTXO_EXIT_DELTA;
2705 ai.nb_round_nonces = MAX_NB_ROUND_NONCES + 1;
2706 let err = check_ark_info_safe(&ai, 12).unwrap_err().to_string();
2707 assert!(err.contains("nb_round_nonces"), "unexpected error: {err}");
2708 }
2709
2710}