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::vtxo::WalletVtxo;
419
420use std::borrow::Cow;
421use std::collections::HashSet;
422use std::path::PathBuf;
423use std::sync::Arc;
424use std::time::Duration;
425
426use anyhow::{bail, Context};
427use bip39::Mnemonic;
428use bitcoin::{Amount, Network, OutPoint};
429use bitcoin::bip32::{self, ChildNumber, Fingerprint};
430use bitcoin::secp256k1::{self, Keypair, PublicKey};
431use futures::stream::FuturesUnordered;
432use log::{debug, error, info, trace, warn};
433use tokio_stream::StreamExt;
434
435use ark::{ArkInfo, ProtocolEncoding, Vtxo, VtxoId, VtxoPolicy, VtxoRequest};
436use ark::address::VtxoDelivery;
437use ark::fees::{validate_and_subtract_fee_min_dust, VtxoFeeInfo};
438use ark::rounds::{RoundAttempt, RoundEvent};
439use ark::vtxo::{Full, PubkeyVtxoPolicy, VtxoRef, VTXO_DUST};
440use ark::vtxo::policy::signing::VtxoSigner;
441use bitcoin_ext::{BlockHeight, TxStatus};
442use server_rpc::{protos, ServerConnection};
443use server_rpc::client::{ConnectError, CreateEndpointError};
444
445use crate::chain::{ChainSource, ChainSourceSpec};
446use crate::exit::Exit;
447use crate::lock_manager::LockManager;
448use crate::movement::{Movement, MovementId, PaymentMethod};
449use crate::movement::manager::MovementManager;
450use crate::notification::NotificationDispatch;
451use crate::onchain::{OnchainWalletTrait, Utxo};
452use crate::persist::BarkPersister;
453use crate::persist::models::{RoundStateId, StoredRoundState, Unlocked};
454#[cfg(feature = "socks5-proxy")]
455use crate::proxy::proxy_for_url;
456use crate::recovery::RecoveryReport;
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
488fn log_server_pubkey_changed_error(expected: PublicKey, got: PublicKey) {
494 error!(
495 "
496Server public key has changed!
497
498The Ark server's public key is different from the one stored when this
499wallet was created. This typically happens when:
500
501 - The server operator has rotated their keys
502 - You are connecting to a different server
503 - The server has been replaced
504
505For safety, this wallet will not connect to the server until you
506resolve this. You can recover your funds on-chain by doing an emergency exit.
507
508This will exit your VTXOs to on-chain Bitcoin without needing the server's cooperation.
509
510Expected: {expected}
511Got: {got}")
512}
513
514fn log_server_mailbox_pubkey_changed_error(expected: PublicKey, got: PublicKey) {
516 error!(
517 "
518Server mailbox public key has changed!
519
520The Ark server's mailbox public key is different from the one stored when this
521wallet was created. This typically happens when:
522
523 - The server operator has rotated their keys
524 - You are connecting to a different server
525 - The server has been replaced
526
527For safety, this wallet will not connect to the server until you resolve this.
528
529Unlike a server pubkey change, your VTXOs are not at risk - the mailbox pubkey
530only affects address receive semantics. Any Ark addresses you previously
531shared will stop receiving new payments; you will need to share new addresses
532after reconnecting.
533
534Expected: {expected}
535Got: {got}")
536}
537
538#[derive(Debug, Clone)]
540pub struct LightningReceiveBalance {
541 pub total: Amount,
543 pub claimable: Amount,
545}
546
547#[derive(Debug, Clone)]
549pub struct Balance {
550 pub spendable: Amount,
552 pub pending_lightning_send: Amount,
554 pub claimable_lightning_receive: Amount,
556 pub pending_in_round: Amount,
558 pub pending_exit: Option<Amount>,
564 pub pending_board: Amount,
566}
567
568pub struct UtxoInfo {
569 pub outpoint: OutPoint,
570 pub amount: Amount,
571 pub confirmation_height: Option<u32>,
572}
573
574impl From<Utxo> for UtxoInfo {
575 fn from(value: Utxo) -> Self {
576 match value {
577 Utxo::Local(o) => UtxoInfo {
578 outpoint: o.outpoint,
579 amount: o.amount,
580 confirmation_height: o.confirmation_height,
581 },
582 Utxo::Exit(e) => UtxoInfo {
583 outpoint: e.vtxo.point(),
584 amount: e.vtxo.amount(),
585 confirmation_height: Some(e.height),
586 },
587 }
588 }
589}
590
591pub struct OffchainBalance {
594 pub available: Amount,
596 pub pending_in_round: Amount,
598 pub pending_exit: Amount,
601}
602
603#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
605pub struct WalletProperties {
606 pub network: Network,
610
611 pub fingerprint: Fingerprint,
615
616 pub server_pubkey: Option<PublicKey>,
623
624 pub server_mailbox_pubkey: Option<PublicKey>,
632}
633
634pub struct WalletSeed {
640 master: bip32::Xpriv,
641 vtxo: bip32::Xpriv,
642}
643
644impl WalletSeed {
645 pub fn new_from_seed(network: Network, seed: &[u8; 64]) -> Self {
647 let bark_path = [ChildNumber::from_hardened_idx(BARK_PURPOSE_INDEX).unwrap()];
648 let master = bip32::Xpriv::new_master(network, seed)
649 .expect("invalid seed")
650 .derive_priv(&SECP, &bark_path)
651 .expect("purpose is valid");
652
653 let vtxo_path = [ChildNumber::from_hardened_idx(VTXO_KEYS_INDEX).unwrap()];
654 let vtxo = master.derive_priv(&SECP, &vtxo_path)
655 .expect("vtxo path is valid");
656
657 Self { master, vtxo }
658 }
659
660 pub fn new_from_mnemonic(network: Network, mnemonic: &Mnemonic) -> Self {
662 Self::new_from_seed(network, &mnemonic.to_seed(""))
663 }
664
665 pub fn fingerprint(&self) -> Fingerprint {
666 self.master.fingerprint(&SECP)
667 }
668
669 fn derive_vtxo_keypair(&self, idx: u32) -> Keypair {
670 self.vtxo.derive_priv(&SECP, &[idx.into()]).unwrap().to_keypair(&SECP)
671 }
672
673 fn to_mailbox_keypair(&self) -> Keypair {
674 let mailbox_path = [ChildNumber::from_hardened_idx(MAILBOX_KEY_INDEX).unwrap()];
675 self.master.derive_priv(&SECP, &mailbox_path).unwrap().to_keypair(&SECP)
676 }
677
678 fn to_recovery_mailbox_keypair(&self) -> Keypair {
679 let mailbox_path = [ChildNumber::from_hardened_idx(RECOVERY_MAILBOX_KEY_INDEX).unwrap()];
680 self.master.derive_priv(&SECP, &mailbox_path).unwrap().to_keypair(&SECP)
681 }
682}
683
684pub struct OpenWalletArgs {
686 pub run_daemon: bool,
692
693 pub datadir: Option<PathBuf>,
703
704 pub persister: Option<Arc<dyn BarkPersister>>,
708
709 pub lock_manager: Option<Box<dyn LockManager>>,
716
717 pub onchain: Option<Arc<tokio::sync::RwLock<dyn OnchainWalletTrait>>>,
721
722 pub create_if_not_exists: bool,
726
727 pub create_without_server: bool,
731
732 pub skip_recovery: bool,
738
739 pub on_recovery_finished: Option<Box<dyn FnOnce(RecoveryReport) + Send + Sync>>,
743}
744
745impl Default for OpenWalletArgs {
746 fn default() -> Self {
747 Self {
748 run_daemon: true,
749 onchain: None,
750 datadir: None,
751 persister: None,
752 lock_manager: None,
753 create_if_not_exists: true,
754 create_without_server: false,
755 skip_recovery: false,
756 on_recovery_finished: None,
757 }
758 }
759}
760
761struct WalletInner {
762 chain: Arc<ChainSource>,
764
765 exit: Exit,
767
768 movements: Arc<MovementManager>,
770
771 notifications: NotificationDispatch,
773
774 config: Config,
776
777 db: Arc<dyn BarkPersister>,
779
780 lock_manager: Box<dyn LockManager>,
784
785 seed: WalletSeed,
787
788 server: tokio::sync::OnceCell<ServerConnection>,
795
796 onchain: Option<Arc<tokio::sync::RwLock<dyn OnchainWalletTrait>>>,
801
802 daemon: parking_lot::Mutex<Option<DaemonHandle>>,
804
805 last_force_exit_scan_tip: tokio::sync::Mutex<Option<BlockHeight>>,
809
810 pub(crate) round_secret_nonces: RoundSecretNonces,
813}
814
815#[derive(Clone)]
916pub struct Wallet {
917 inner: Arc<WalletInner>,
918}
919
920impl Wallet {
921 pub async fn network(&self) -> anyhow::Result<Network> {
922 Ok(self.properties().await?.network)
923 }
924
925 pub fn chain(&self) -> &Arc<ChainSource> {
927 &self.inner.chain
928 }
929
930 pub fn exit_mgr(&self) -> &Exit {
932 &self.inner.exit
933 }
934
935 pub fn movements_mgr(&self) -> &MovementManager {
937 &self.inner.movements
938 }
939
940 pub async fn peek_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
943 let last_revealed = self.inner.db.get_last_vtxo_key_index().await?;
944
945 let index = last_revealed.map(|i| i + 1).unwrap_or(u32::MIN);
946 let keypair = self.inner.seed.derive_vtxo_keypair(index);
947
948 Ok((keypair, index))
949 }
950
951 pub async fn derive_store_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
954 let (keypair, index) = self.peek_next_keypair().await?;
955 self.inner.db.store_vtxo_key(index, keypair.public_key()).await?;
956 Ok((keypair, index))
957 }
958
959 #[deprecated(note = "use peek_keypair instead")]
960 pub async fn peak_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
961 self.peek_keypair(index).await
962 }
963
964 pub async fn peek_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
978 let keypair = self.inner.seed.derive_vtxo_keypair(index);
979 if self.inner.db.get_public_key_idx(&keypair.public_key()).await?.is_some() {
980 Ok(keypair)
981 } else {
982 bail!("VTXO key {} does not exist, please derive it first", index)
983 }
984 }
985
986
987 pub async fn pubkey_keypair(&self, public_key: &PublicKey) -> anyhow::Result<Option<(u32, Keypair)>> {
999 if let Some(index) = self.inner.db.get_public_key_idx(&public_key).await? {
1000 Ok(Some((index, self.inner.seed.derive_vtxo_keypair(index))))
1001 } else {
1002 Ok(None)
1003 }
1004 }
1005
1006 pub async fn get_vtxo_key(&self, vtxo: impl VtxoRef) -> anyhow::Result<Keypair> {
1017 let bare_vtxo = match vtxo.as_bare_vtxo() {
1018 Some(bare) => bare,
1019 None => Cow::Owned(self.get_vtxo_by_id(vtxo.vtxo_id()).await?.vtxo),
1020 };
1021 let pubkey = self.find_signable_clause(&bare_vtxo).await
1022 .context("VTXO is not signable by wallet")?
1023 .pubkey();
1024 let idx = self.inner.db.get_public_key_idx(&pubkey).await?
1025 .context("VTXO key not found")?;
1026 Ok(self.inner.seed.derive_vtxo_keypair(idx))
1027 }
1028
1029 #[deprecated(note = "use peek_address instead")]
1030 pub async fn peak_address(&self, index: u32) -> anyhow::Result<ark::Address> {
1031 self.peek_address(index).await
1032 }
1033
1034 pub async fn peek_address(&self, index: u32) -> anyhow::Result<ark::Address> {
1038 let properties = self.properties().await?;
1039 let network = properties.network;
1040 let keypair = self.peek_keypair(index).await?;
1041 let mailbox = self.mailbox_identifier();
1042
1043
1044 let (server_pubkey, mailbox_pubkey) =
1045 if let (Some(spk), Some(mpk)) = (properties.server_pubkey, properties.server_mailbox_pubkey) {
1046 (spk, mpk)
1047 } else {
1048 let (_, ark_info) = self.require_server().await?;
1049 (ark_info.server_pubkey, ark_info.mailbox_pubkey)
1050 };
1051
1052 Ok(ark::Address::builder()
1053 .testnet(network != bitcoin::Network::Bitcoin)
1054 .server_pubkey(server_pubkey)
1055 .pubkey_policy(keypair.public_key())
1056 .mailbox(mailbox_pubkey, mailbox, &keypair)
1057 .context("failed to assign mailbox")?
1058 .into_address()
1059 .context("failed to build address")?)
1060 }
1061
1062 pub async fn new_address_with_index(&self) -> anyhow::Result<(ark::Address, u32)> {
1066 let (_, index) = self.derive_store_next_keypair().await?;
1067 let addr = self.peek_address(index).await?;
1068 Ok((addr, index))
1069 }
1070
1071 pub async fn new_address(&self) -> anyhow::Result<ark::Address> {
1073 let (addr, _) = self.new_address_with_index().await?;
1074 Ok(addr)
1075 }
1076
1077 pub async fn sign_message(
1085 &self,
1086 message: &[u8],
1087 address: &ark::Address,
1088 ) -> anyhow::Result<Option<secp256k1::schnorr::Signature>> {
1089 let pubkey = address.policy().user_pubkey();
1090 let Some((_, keypair)) = self.pubkey_keypair(&pubkey).await? else {
1091 return Ok(None);
1092 };
1093 Ok(Some(ark::message::sign(&keypair, message)))
1094 }
1095
1096 pub async fn create(
1105 network: Network,
1106 seed: &WalletSeed,
1107 config: &Config,
1108 db: &dyn BarkPersister,
1109 lock_manager: &dyn LockManager,
1110 allow_unreachable_server: bool,
1111 ) -> anyhow::Result<()> {
1112 trace!("Config: {:?}", config);
1113
1114 let wallet_fingerprint = seed.fingerprint();
1115
1116 let create_guard = lock_manager.lock(
1121 &format!("{}.create", wallet_fingerprint),
1122 Duration::from_secs(5),
1123 ).await.context("wallet initialization already in progress")?;
1124
1125 if let Some(existing) = db.read_properties().await? {
1126 trace!("Existing config: {:?}", existing);
1127 bail!("cannot overwrite already existing config")
1128 }
1129
1130 let (server_pubkey, mailbox_pubkey) = match Self::connect_to_server(&config, network).await {
1132 Ok(conn) => {
1133 let ark_info = conn.ark_info().await;
1134 (Some(ark_info.server_pubkey), Some(ark_info.mailbox_pubkey))
1135 },
1136 Err(_) if allow_unreachable_server => (None, None),
1137 Err(err) => {
1138 bail!("Failed to connect to provided server: {:#}", err);
1139 },
1140 };
1141
1142 let properties = WalletProperties {
1143 network,
1144 fingerprint: wallet_fingerprint,
1145 server_pubkey,
1146 server_mailbox_pubkey: mailbox_pubkey,
1147 };
1148
1149 db.init_wallet(&properties).await.context("cannot init wallet in the database")?;
1151 info!("Created wallet with fingerprint: {}", wallet_fingerprint);
1152 if let Some(pk) = server_pubkey {
1153 info!("Stored server pubkey: {}", pk);
1154 }
1155
1156 drop(create_guard);
1159
1160 Ok(())
1161 }
1162
1163 pub async fn open(
1165 network: Network,
1166 seed: WalletSeed,
1167 config: Config,
1168 args: OpenWalletArgs,
1169 ) -> anyhow::Result<Wallet> {
1170 if !(1..=3).contains(&config.change_vtxo_split_factor) {
1171 bail!("change_vtxo_split_factor must be 1, 2 or 3, got {}",
1172 config.change_vtxo_split_factor,
1173 );
1174 }
1175
1176 let fingerprint = seed.fingerprint();
1177 let lock_manager = if let Some(lm) = args.lock_manager {
1178 lm
1179 } else {
1180 crate::lock_manager::platform_default(args.datadir.as_ref(), Some(fingerprint))
1181 .context("failed to instantiate platform default lock manager")?
1182 };
1183
1184 let db = if let Some(db) = args.persister {
1185 db
1186 } else {
1187 if let Some(ref datadir) = args.datadir {
1188 #[cfg(not(target_arch = "wasm32"))]
1189 if !datadir.exists() && args.create_if_not_exists {
1190 tokio::fs::create_dir_all(datadir).await.with_context(|| format!(
1191 "failed to create datadir at {}", datadir.display(),
1192 ))?;
1193 }
1194 }
1195 crate::persist::platform_default(args.datadir.as_ref(), Some(fingerprint)).await
1196 .context("failed to instantiate platform default persister")?
1197 };
1198
1199 let mut created_now = false;
1200 let properties = if let Some(p) = db.read_properties().await? {
1201 p
1202 } else if args.create_if_not_exists {
1203 Self::create(
1204 network, &seed, &config, &*db, &*lock_manager, args.create_without_server,
1205 ).await.context("error creating new wallet")?;
1206 created_now = true;
1207 db.read_properties().await?
1208 .context("create failed: no wallet properties after Wallet::create was called")?
1209 } else {
1210 bail!("wallet does not exist; use Wallet::create or \
1211 set options.create_if_not_exists to true");
1212 };
1213
1214 if properties.fingerprint != fingerprint {
1215 bail!("incorrect mnemonic")
1216 }
1217
1218 let chain_source = if let Some(ref url) = config.esplora_address {
1219 ChainSourceSpec::Esplora {
1220 url: url.clone(),
1221 }
1222 } else if let Some(ref url) = config.bitcoind_address {
1223 let auth = if let Some(ref c) = config.bitcoind_cookiefile {
1224 bitcoin_ext::rpc::Auth::CookieFile(c.clone())
1225 } else {
1226 bitcoin_ext::rpc::Auth::UserPass(
1227 config.bitcoind_user.clone().context("need bitcoind auth config")?,
1228 config.bitcoind_pass.as_ref().context("need bitcoind auth config")?
1229 .leak_ref().clone(),
1230 )
1231 };
1232 ChainSourceSpec::Bitcoind {
1233 url: url.clone(),
1234 auth,
1235 zmq: config.bitcoind_zmq_address.clone(),
1236 }
1237 } else {
1238 bail!("Need to either provide esplora or bitcoind info");
1239 };
1240
1241 #[cfg(feature = "socks5-proxy")]
1242 let chain_proxy = proxy_for_url(&config.socks5_proxy, chain_source.url())?;
1243 let chain_source_client = ChainSource::new(
1244 chain_source, properties.network, config.fallback_fee_rate,
1245 #[cfg(feature = "socks5-proxy")] chain_proxy.as_deref(),
1246 ).await?;
1247 let chain = Arc::new(chain_source_client);
1248 chain.require_version().await
1249 .context("provided chain source doesn't meet version requirement")?;
1250
1251 let server = tokio::sync::OnceCell::new();
1252
1253 let notifications = NotificationDispatch::new();
1254 let movements = Arc::new(MovementManager::new(db.clone(), notifications.clone()));
1255 let exit = Exit::new(db.clone(), chain.clone(), movements.clone()).await?;
1256
1257 let onchain = args.onchain;
1258 let ret = Wallet { inner: Arc::new(WalletInner {
1259 config, db, lock_manager, seed, exit, movements, notifications, server, chain,
1260 onchain,
1261 daemon: parking_lot::Mutex::new(None),
1262 last_force_exit_scan_tip: tokio::sync::Mutex::new(None),
1263 round_secret_nonces: RoundSecretNonces::new(),
1264 })};
1265
1266 ret.inner.exit.load().await
1267 .context("error loading exit system after opening wallet")?;
1268
1269 if created_now {
1270 if !args.skip_recovery {
1271 match ret.recover_from_mailbox().await {
1276 Ok(report) => {
1277 if let Some(callback) = args.on_recovery_finished {
1278 callback(report);
1279 }
1280 },
1281 Err(e) => {
1282 error!("VTXO recovery from the recovery mailbox failed; funds may be \
1283 missing from this wallet until recovery succeeds: {:#}", e);
1284 },
1285 }
1286 } else {
1287 info!("Seed-based wallet recovery explicitly skipped");
1288 }
1289 }
1290
1291 if args.run_daemon {
1292 ret.start_daemon()
1293 .context("failed to start daemon after opening wallet")?;
1294 }
1295
1296 Ok(ret)
1297 }
1298
1299 pub fn config(&self) -> &Config {
1301 &self.inner.config
1302 }
1303
1304 pub async fn properties(&self) -> anyhow::Result<WalletProperties> {
1306 let properties = self.inner.db.read_properties().await?.context("Wallet is not initialised")?;
1307 Ok(properties)
1308 }
1309
1310 pub fn fingerprint(&self) -> Fingerprint {
1312 self.inner.seed.fingerprint()
1313 }
1314
1315 async fn connect_to_server(
1316 config: &Config,
1317 network: Network,
1318 ) -> anyhow::Result<ServerConnection> {
1319 let server_address = crate::utils::url_with_default_https_scheme(&config.server_address);
1320 let mut builder = ServerConnection::builder()
1321 .address(&server_address)
1322 .network(network);
1323
1324 #[cfg(feature = "socks5-proxy")]
1325 if let Some(proxy) = proxy_for_url(&config.socks5_proxy, &server_address)? {
1326 builder = builder.proxy(&proxy)
1327 }
1328
1329 #[allow(deprecated)]
1330 {
1331 if let Some(ref token) = config.server_access_token {
1332 builder = builder.access_token(token);
1333 }
1334 }
1335
1336 if let Some(ref ua) = config.user_agent {
1337 builder = builder.user_agent(ua);
1338 }
1339
1340 builder.connect().await.map_err(wrap_server_connect_error)
1341 .context("Failed to connect to Ark server")
1342 }
1343
1344 async fn require_server(&self) -> anyhow::Result<(ServerConnection, ArkInfo)> {
1345 let conn = self.inner.server.get_or_try_init(|| async {
1349 let network = self.properties().await?.network;
1350 Self::connect_to_server(&self.inner.config, network).await
1351 .context("You should be connected to Ark server to perform this action")
1352 }).await?.clone();
1353
1354 let ark_info = conn.ark_info().await;
1355 self.check_and_store_server_keys(&ark_info).await?;
1356
1357 Ok((conn, ark_info))
1358 }
1359
1360 pub async fn refresh_server(&self) -> anyhow::Result<()> {
1361 let srv = self.inner.server.get_or_try_init(|| async {
1367 let properties = self.properties().await?;
1368 Self::connect_to_server(&self.inner.config, properties.network).await
1369 .map_err(anyhow::Error::from)
1370 }).await?;
1371
1372 srv.check_connection().await?;
1373 let ark_info = srv.ark_info().await;
1374 ark_info.fees.validate().context("invalid fee schedule")?;
1375 self.check_and_store_server_keys(&ark_info).await?;
1376
1377 Ok(())
1378 }
1379
1380 pub fn onchain(&self) -> Option<Arc<tokio::sync::RwLock<dyn OnchainWalletTrait>>> {
1382 self.inner.onchain.clone()
1383 }
1384
1385 pub async fn sync_onchain(&self) -> anyhow::Result<()> {
1387 if let Some(onchain) = self.inner.onchain.as_ref() {
1388 onchain.write().await.sync(self.chain()).await?;
1389 }
1390 Ok(())
1391 }
1392
1393 async fn check_and_store_server_keys(&self, ark_info: &ArkInfo) -> anyhow::Result<()> {
1400 let properties = self.properties().await?;
1401
1402 if let Some(stored_pubkey) = properties.server_pubkey {
1403 if stored_pubkey != ark_info.server_pubkey {
1404 log_server_pubkey_changed_error(stored_pubkey, ark_info.server_pubkey);
1405 bail!("Server public key has changed. You should exit all your VTXOs!");
1406 }
1407 } else {
1408 self.inner.db.set_server_pubkey(ark_info.server_pubkey).await?;
1409 info!("Stored server pubkey for existing wallet: {}", ark_info.server_pubkey);
1410 }
1411
1412 if let Some(stored_mailbox_pubkey) = properties.server_mailbox_pubkey {
1413 if stored_mailbox_pubkey != ark_info.mailbox_pubkey {
1414 log_server_mailbox_pubkey_changed_error(stored_mailbox_pubkey, ark_info.mailbox_pubkey);
1415 bail!("Server mailbox public key has changed.");
1416 }
1417 } else {
1418 self.inner.db.set_server_mailbox_pubkey(ark_info.mailbox_pubkey).await?;
1419 info!("Stored server mailbox pubkey for existing wallet: {}", ark_info.mailbox_pubkey);
1420 }
1421
1422 Ok(())
1423 }
1424
1425 pub async fn ark_info(&self) -> anyhow::Result<Option<ArkInfo>> {
1427 match self.inner.server.get() {
1428 Some(srv) => Ok(Some(srv.ark_info().await)),
1429 None => Ok(None),
1430 }
1431 }
1432
1433 pub async fn require_ark_info(&self) -> anyhow::Result<ArkInfo> {
1439 let (_, ark_info) = self.require_server().await?;
1440 Ok(ark_info)
1441 }
1442
1443 pub async fn balance(&self) -> anyhow::Result<Balance> {
1447 let vtxos = self.vtxos().await?;
1448
1449 let spendable = {
1450 let mut v = vtxos.iter().collect();
1451 VtxoStateKind::Spendable.filter_vtxos(&mut v).await?;
1452 v.into_iter().map(|v| v.amount()).sum::<Amount>()
1453 };
1454
1455 let pending_lightning_send = self.pending_lightning_send_vtxos().await?.iter()
1456 .map(|v| v.amount())
1457 .sum::<Amount>();
1458
1459 let claimable_lightning_receive = self.claimable_lightning_receive_balance().await?;
1460
1461 let pending_board = self.pending_board_vtxos().await?.iter()
1462 .map(|v| v.amount())
1463 .sum::<Amount>();
1464
1465 let pending_in_round = self.pending_round_balance().await?;
1466
1467 let pending_exit = self.exit_mgr().try_pending_total();
1468
1469 Ok(Balance {
1470 spendable,
1471 pending_in_round,
1472 pending_lightning_send,
1473 claimable_lightning_receive,
1474 pending_exit,
1475 pending_board,
1476 })
1477 }
1478
1479 pub async fn validate_vtxo(&self, vtxo: &Vtxo<Full>) -> Result<(), VtxoValidationError> {
1481 let tx = self.inner.chain.get_tx(&vtxo.chain_anchor().txid).await
1482 .map_err(VtxoValidationError::Chain)?
1483 .ok_or(VtxoValidationError::AnchorNotFound)?;
1484
1485 vtxo.validate(&tx).map_err(VtxoValidationError::Invalid)
1486 }
1487
1488 pub async fn import_vtxo(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<()> {
1498 if self.inner.db.get_wallet_vtxo(vtxo.id()).await?.is_some() {
1499 info!("VTXO {} already exists in wallet, skipping import", vtxo.id());
1500 return Ok(());
1501 }
1502
1503 self.validate_vtxo(vtxo).await.context("VTXO validation failed")?;
1504
1505 if self.find_signable_clause(vtxo).await.is_none() {
1506 bail!("VTXO {} is not owned by this wallet (no signable clause found)", vtxo.id());
1507 }
1508
1509 let current_height = self.inner.chain.tip().await?;
1510 if vtxo.expiry_height() <= current_height {
1511 bail!("Vtxo {} has expired", vtxo.id());
1512 }
1513
1514 self.store_spendable_vtxos([vtxo]).await.context("failed to store imported VTXO")?;
1515
1516 info!("Successfully imported VTXO {}", vtxo.id());
1517 Ok(())
1518 }
1519
1520 pub async fn get_vtxo_by_id(&self, vtxo_id: VtxoId) -> anyhow::Result<WalletVtxo> {
1522 let vtxo = self.inner.db.get_wallet_vtxo(vtxo_id).await
1523 .with_context(|| format!("Error when querying vtxo {} in database", vtxo_id))?
1524 .with_context(|| format!("The VTXO with id {} cannot be found", vtxo_id))?;
1525 Ok(vtxo)
1526 }
1527
1528 pub async fn get_full_vtxo(&self, vtxo_id: VtxoId) -> anyhow::Result<Vtxo<Full>> {
1536 self.inner.db.get_full_vtxo(vtxo_id).await
1537 .with_context(|| format!("Error when querying full vtxo {} in database", vtxo_id))?
1538 .with_context(|| format!("The VTXO with id {} cannot be found", vtxo_id))
1539 }
1540
1541 pub async fn get_full_vtxos<V: VtxoRef>(
1543 &self,
1544 vtxos: impl IntoIterator<Item = V>,
1545 ) -> anyhow::Result<Vec<Vtxo<Full>>> {
1546 let ids = vtxos.into_iter().map(|v| v.vtxo_id()).collect::<Vec<_>>();
1547 self.inner.db.get_full_vtxos(&ids).await
1548 .with_context(||
1549 format!("Error when querying full vtxos in database with IDs: {:?}", ids)
1550 )
1551 }
1552
1553 #[deprecated(since="0.1.0-beta.5", note = "Use Wallet::history instead")]
1555 pub async fn movements(&self) -> anyhow::Result<Vec<Movement>> {
1556 self.history().await
1557 }
1558
1559 pub async fn history(&self) -> anyhow::Result<Vec<Movement>> {
1561 Ok(self.inner.db.get_all_movements().await?)
1562 }
1563
1564 pub async fn update_history_metadata(
1584 &self,
1585 movement_id: MovementId,
1586 patch: &serde_json::Value,
1587 ) -> anyhow::Result<()> {
1588 self.inner.movements.patch_metadata(movement_id, patch).await?;
1589 Ok(())
1590 }
1591
1592 pub async fn history_by_payment_method(
1594 &self,
1595 payment_method: &PaymentMethod,
1596 ) -> anyhow::Result<Vec<Movement>> {
1597 let mut ret = self.inner.db.get_movements_by_payment_method(payment_method).await?;
1598 ret.sort_by_key(|m| m.id);
1599 Ok(ret)
1600 }
1601
1602 pub async fn all_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1604 Ok(self.inner.db.get_all_vtxos().await?)
1605 }
1606
1607 pub async fn vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1609 Ok(self.inner.db.get_vtxos_by_state(&VtxoStateKind::UNSPENT_STATES).await?)
1610 }
1611
1612 pub async fn vtxos_with(&self, filter: &impl FilterVtxos) -> anyhow::Result<Vec<WalletVtxo>> {
1614 let mut vtxos = self.vtxos().await?;
1615 filter.filter_vtxos(&mut vtxos).await.context("error filtering vtxos")?;
1616 Ok(vtxos)
1617 }
1618
1619 pub async fn spendable_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1621 Ok(self.vtxos_with(&VtxoStateKind::Spendable).await?)
1622 }
1623
1624 pub async fn spendable_vtxos_with(
1626 &self,
1627 filter: &impl FilterVtxos,
1628 ) -> anyhow::Result<Vec<WalletVtxo>> {
1629 let mut vtxos = self.spendable_vtxos().await?;
1630 filter.filter_vtxos(&mut vtxos).await.context("error filtering vtxos")?;
1631 Ok(vtxos)
1632 }
1633
1634 pub async fn get_expiring_vtxos(
1636 &self,
1637 threshold: BlockHeight,
1638 ) -> anyhow::Result<Vec<WalletVtxo>> {
1639 let expiry = self.inner.chain.tip().await? + threshold;
1640 let filter = VtxoFilter::new(&self).expires_before(expiry);
1641 Ok(self.spendable_vtxos_with(&filter).await?)
1642 }
1643
1644 pub async fn maintenance(&self) -> anyhow::Result<()> {
1650 info!("Starting wallet maintenance in interactive mode");
1651 self.sync().await;
1652
1653 let rounds = self.progress_pending_rounds(None).await;
1655 if let Err(e) = rounds.as_ref() {
1656 warn!("Error progressing pending rounds: {:#}", e);
1657 }
1658
1659 let states = self.inner.db.get_pending_round_state_ids().await?;
1661 for id in states {
1662 debug!("Cancelling pending round participation {}", id);
1663 let mut state = match self.lock_wait_round_state(id).await {
1664 Ok(Some(s)) => s,
1665 Ok(None) => continue, Err(e) => {
1667 warn!("Failed to lock round state with id {}: {:#}", id, e);
1668 continue;
1669 }
1670 };
1671 if let Err(e) = state.state_mut().try_cancel(self).await {
1672 warn!("Error cancelling pending round: {:#}", e);
1673 }
1674 }
1675
1676 let refresh = self.maintenance_refresh().await;
1678 if let Err(e) = refresh.as_ref() {
1679 warn!("Error refreshing VTXOs: {:#}", e);
1680 }
1681
1682 if rounds.is_err() || refresh.is_err() {
1683 bail!("Maintenance encountered errors.\nprogress_rounds: {:#?}\nrefresh: {:#?}",
1684 rounds, refresh,
1685 );
1686 }
1687
1688 Ok(())
1689 }
1690
1691 pub async fn maintenance_delegated(&self) -> anyhow::Result<()> {
1698 info!("Starting wallet maintenance in delegated mode");
1699 self.sync().await;
1700 let rounds = self.progress_pending_rounds(None).await;
1701 if let Err(e) = rounds.as_ref() {
1702 warn!("Error progressing pending rounds: {:#}", e);
1703 }
1704 let refresh = self.maybe_schedule_maintenance_refresh_delegated().await;
1705 if let Err(e) = refresh.as_ref() {
1706 warn!("Error refreshing VTXOs: {:#}", e);
1707 }
1708
1709 if rounds.is_err() || refresh.is_err() {
1710 bail!("Delegated maintenance encountered errors.\n\
1711 progress_rounds: {:#?}\nrefresh: {:#?}",
1712 rounds, refresh,
1713 );
1714 }
1715
1716 Ok(())
1717 }
1718
1719 pub(crate) async fn join_round_for_maintenance_refresh(
1734 &self,
1735 attempt: &RoundAttempt,
1736 ) -> anyhow::Result<Option<RoundStateId>> {
1737 self.maintenance_refresh_retry_loop(|part| async move {
1738 info!("Joining round {} for maintenance refresh ({} vtxos)",
1739 attempt.round_seq, part.inputs.len());
1740 Ok(Some(self.join_attempt_interactive(
1741 part, attempt, Some(RoundMovement::Refresh),
1742 ).await?.id()))
1743 }).await.context("failed to join round for maintenance refresh")
1744 }
1745
1746 pub async fn maybe_schedule_maintenance_refresh_delegated(
1754 &self,
1755 ) -> anyhow::Result<Option<RoundStateId>> {
1756 self.maintenance_refresh_retry_loop(|part| async move {
1757 info!("Scheduling delegated maintenance refresh ({} vtxos)", part.inputs.len());
1758 Ok(Some(self.join_next_round_delegated(part, Some(RoundMovement::Refresh)).await?.id()))
1759 }).await.context("failed to schedule delegated maintenance refresh")
1760 }
1761
1762 async fn maintenance_refresh_retry_loop<F, Fut>(
1770 &self,
1771 attempt_refresh: F,
1772 ) -> anyhow::Result<Option<RoundStateId>>
1773 where
1774 F: Fn(RoundParticipation) -> Fut,
1775 Fut: Future<Output = anyhow::Result<Option<RoundStateId>>>,
1776 {
1777 let mut excluded = HashSet::new();
1778 for _ in 0..10 {
1779 let vtxos = self.get_vtxos_to_refresh_with_excluded(excluded.iter().copied()).await?;
1780 match (vtxos.is_empty(), excluded.is_empty()) {
1781 (true, false) => {
1785 warn!("no VTXOs to refresh after exclusions: {:?}", excluded);
1786 bail!("no VTXOs to refresh after excluding: {:?}", excluded);
1787 },
1788 (true, true) => return Ok(None),
1790 (false, _) => {},
1792 }
1793 let part = match self.build_refresh_participation(vtxos).await? {
1794 Some(participation) => participation,
1795 None => return Ok(None),
1796 };
1797
1798 match attempt_refresh(part).await {
1799 Ok(state_id) => return Ok(state_id),
1800 Err(e) => {
1801 let rejected = rejected_vtxos_from_error(&e).into_iter()
1802 .filter(|id| !excluded.contains(id))
1803 .collect::<Vec<_>>();
1804 if rejected.is_empty() {
1805 return Err(e);
1806 }
1807 warn!("Maintenance refresh rejected {} unusable input(s) ({:?}); \
1808 retrying without them", rejected.len(), rejected);
1809 excluded.extend(rejected);
1810 },
1811 }
1812 }
1813 bail!("Maintenance refresh failed after 10 retries");
1814 }
1815
1816 pub async fn maintenance_refresh(&self) -> anyhow::Result<Option<RoundStatus>> {
1828 if self.get_vtxos_to_refresh().await?.is_empty() {
1829 return Ok(None);
1830 }
1831
1832 info!("Waiting for round to perform maintenance refresh...");
1833 let mut events = self.subscribe_round_events().await?;
1834 while let Some(event) = events.next().await {
1835 let event = event.context("error on round event stream")?;
1836 if let RoundEvent::Attempt(a) = event && a.attempt_seq == 0 {
1837 debug!("Round {} started, triggering maintenance refresh", a.round_seq);
1838 let state_id = match self.join_round_for_maintenance_refresh(&a).await? {
1839 Some(id) => id,
1840 None => return Ok(None),
1841 };
1842 let state = self.lock_wait_round_state(state_id).await?
1845 .context("maintenance refresh round state vanished after joining")?;
1846 return Ok(Some(self.drive_round_state(state, &mut events).await?));
1847 }
1848 }
1849 Ok(None)
1850 }
1851
1852 pub async fn sync(&self) {
1858 self.inner.chain.invalidate_caches().await;
1859
1860 futures::join!(
1861 async {
1862 if let Err(e) = self.inner.chain.update_fee_rates(self.inner.config.fallback_fee_rate).await {
1865 warn!("Error updating fee rates: {:#}", e);
1866 }
1867 },
1868 async {
1869 if let Err(e) = self.sync_mailbox().await {
1870 warn!("Error in mailbox sync: {:#}", e);
1871 }
1872 },
1873 async {
1874 if let Err(e) = self.sync_pending_rounds().await {
1875 warn!("Error while trying to progress rounds awaiting confirmations: {:#}", e);
1876 }
1877 },
1878 async {
1879 if let Err(e) = self.sync_pending_lightning_send_vtxos().await {
1880 warn!("Error syncing pending lightning payments: {:#}", e);
1881 }
1882 },
1883 async {
1884 if let Err(e) = self.sync_pending_arkoor_sends().await {
1885 warn!("Error syncing pending arkoor sends: {:#}", e);
1886 }
1887 },
1888 async {
1889 if let Err(e) = self.try_claim_all_lightning_receives(false).await {
1890 warn!("Error claiming pending lightning receives: {:#}", e);
1891 }
1892 },
1893 async {
1894 if let Err(e) = self.sync_pending_boards().await {
1895 warn!("Error syncing pending boards: {:#}", e);
1896 }
1897 },
1898 async {
1899 if let Err(e) = self.sync_pending_offboards().await {
1900 warn!("Error syncing pending offboards: {:#}", e);
1901 }
1902 },
1903 async {
1904 if let Err(e) = self.sync_force_exited_vtxos().await {
1905 warn!("Error scanning for on-chain-exited VTXOs: {:#}", e);
1906 }
1907 },
1908 async {
1909 if let Err(e) = self.catchup_recovery_vtxos().await {
1913 warn!("Failed to catch up recovery VTXOs with server: {:#}", e);
1914 }
1915 }
1916 );
1917 }
1918
1919 pub async fn sync_exits(&self) -> anyhow::Result<()> {
1925 self.exit_mgr().sync(&self).await?;
1926 Ok(())
1927 }
1928
1929 pub async fn progress_exits(&self) -> anyhow::Result<()> {
1934 self.exit_mgr().progress_exits_with_cpfp(&self, None).await?;
1935 Ok(())
1936 }
1937
1938 pub async fn sync_force_exited_vtxos(&self) -> anyhow::Result<()> {
1951 let tip = self.inner.chain.tip().await?;
1953 let mut lock = self.inner.last_force_exit_scan_tip.lock().await;
1954 if *lock == Some(tip) {
1955 return Ok(());
1956 }
1957
1958 let exiting = self.exit_mgr().get_exit_vtxo_ids().await;
1960 let vtxos = self.inner.db.get_vtxos_by_state(&[VtxoStateKind::Spendable]).await?
1961 .into_iter()
1962 .filter(|v| !exiting.contains(&v.vtxo.id()));
1963
1964 let mut checked = FuturesUnordered::new();
1966 for wv in vtxos {
1967 let chain = self.inner.chain.clone();
1968 checked.push(async move {
1969 let txid = wv.vtxo_id().to_point().txid;
1970 let status = chain.tx_status(txid).await;
1971 (wv, status)
1972 });
1973 }
1974
1975 let mut to_exit = Vec::new();
1976 while let Some((vtxo, status)) = futures::StreamExt::next(&mut checked).await {
1977 match status {
1978 Ok(TxStatus::NotFound) => {},
1979 Ok(_) => {
1980 info!("VTXO {} was exited on-chain without us; routing it to a claimable exit",
1981 vtxo.vtxo.id(),
1982 );
1983 to_exit.push(vtxo.vtxo);
1984 },
1985 Err(e) => warn!("Could not check on-chain status of VTXO {}: {:#}",
1986 vtxo.vtxo.id(), e,
1987 ),
1988 }
1989 }
1990
1991 if !to_exit.is_empty() {
1992 self.exit_mgr().start_exit_for_vtxos(&to_exit).await
1993 .context("failed to start exit for on-chain-exited VTXOs")?;
1994
1995 *lock = Some(tip);
1996 self.sync_exits().await
1997 .context("failed to sync exits after starting new ones")?;
1998 } else {
1999 *lock = Some(tip);
2000 }
2001
2002 Ok(())
2003 }
2004
2005 pub async fn dangerous_drop_vtxo(&self, vtxo_id: VtxoId) -> anyhow::Result<()> {
2008 warn!("Drop vtxo {} from the database", vtxo_id);
2009 self.inner.db.remove_vtxo(vtxo_id).await?;
2010 Ok(())
2011 }
2012
2013 pub async fn dangerous_drop_all_vtxos(&self) -> anyhow::Result<()> {
2016 warn!("Dropping all vtxos from the db...");
2017 for vtxo in self.vtxos().await? {
2018 self.inner.db.remove_vtxo(vtxo.id()).await?;
2019 }
2020
2021 self.exit_mgr().dangerous_clear_exit().await?;
2022 Ok(())
2023 }
2024
2025 async fn has_counterparty_risk(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<bool> {
2033 for past_pks in vtxo.past_arkoor_pubkeys() {
2034 let mut owns_any = false;
2035 for past_pk in past_pks {
2036 if self.inner.db.get_public_key_idx(&past_pk).await?.is_some() {
2037 owns_any = true;
2038 break;
2039 }
2040 }
2041 if !owns_any {
2042 return Ok(true);
2043 }
2044 }
2045
2046 let my_clause = self.find_signable_clause(vtxo).await;
2047 Ok(!my_clause.is_some())
2048 }
2049
2050 pub async fn build_refresh_participation<V: VtxoRef>(
2051 &self,
2052 vtxos: impl IntoIterator<Item = V>,
2053 ) -> anyhow::Result<Option<RoundParticipation>> {
2054 self.inner_build_refresh_participation(vtxos, None).await
2055 }
2056
2057 pub async fn build_scheduled_refresh_participation<V: VtxoRef>(
2058 &self,
2059 vtxos: impl IntoIterator<Item = V>,
2060 height: BlockHeight,
2061 ) -> anyhow::Result<Option<RoundParticipation>> {
2062 self.inner_build_refresh_participation(vtxos, Some(height)).await
2063 }
2064
2065 async fn inner_build_refresh_participation<V: VtxoRef>(
2066 &self,
2067 vtxos: impl IntoIterator<Item = V>,
2068 height: Option<BlockHeight>,
2069 ) -> anyhow::Result<Option<RoundParticipation>> {
2070 let (vtxos, total_amount) = {
2071 let iter = vtxos.into_iter();
2072 let size_hint = iter.size_hint();
2073 let mut vtxos = Vec::<Vtxo<Full>>::with_capacity(size_hint.1.unwrap_or(size_hint.0));
2074 let mut amount = Amount::ZERO;
2075 for vref in iter {
2076 let id = vref.vtxo_id();
2081 if vtxos.iter().any(|v| v.id() == id) {
2082 bail!("duplicate VTXO id: {}", id);
2083 }
2084 let vtxo = if let Some(vtxo) = vref.into_full_vtxo() {
2085 vtxo
2086 } else {
2087 self.inner.db.get_full_vtxo(id).await?
2090 .with_context(|| format!("vtxo with id {} not found", id))?
2091 };
2092 amount += vtxo.amount();
2093 vtxos.push(vtxo);
2094 }
2095 (vtxos, amount)
2096 };
2097
2098 if vtxos.is_empty() {
2099 info!("Skipping refresh since no VTXOs are provided.");
2100 return Ok(None);
2101 }
2102 ensure!(total_amount >= VTXO_DUST,
2103 "vtxo amount must be at least {} to participate in a round",
2104 VTXO_DUST,
2105 );
2106
2107 let (_, ark_info) = self.require_server().await?;
2109 let refresh_height = match height {
2110 Some(height) => height,
2111 None => self.inner.chain.tip().await?,
2112 };
2113
2114 let vtxo_fee_infos = vtxos.iter()
2115 .map(|v| VtxoFeeInfo::from_vtxo_and_tip(v, refresh_height));
2116 let fee = ark_info.fees.refresh.calculate(vtxo_fee_infos).context("fee overflowed")?;
2117 let output_amount = validate_and_subtract_fee_min_dust(total_amount, fee, VTXO_DUST)?;
2118
2119 info!("Refreshing {} VTXOs (total amount = {}, fee = {}, output = {}).",
2120 vtxos.len(), total_amount, fee, output_amount,
2121 );
2122 let (user_keypair, _) = self.derive_store_next_keypair().await?;
2123 let req = VtxoRequest {
2124 policy: VtxoPolicy::Pubkey(PubkeyVtxoPolicy { user_pubkey: user_keypair.public_key() }),
2125 amount: output_amount,
2126 };
2127
2128 Ok(Some(RoundParticipation {
2129 inputs: vtxos,
2130 outputs: vec![req],
2131 unblinded_mailbox_id: None,
2132 }))
2133 }
2134
2135 pub async fn refresh_vtxos<V: VtxoRef>(
2140 &self,
2141 vtxos: impl IntoIterator<Item = V>,
2142 ) -> anyhow::Result<Option<RoundStatus>> {
2143 let participation = match self.build_refresh_participation(vtxos).await? {
2144 Some(participation) => participation,
2145 None => return Ok(None),
2146 };
2147
2148 Ok(Some(self.participate_round(participation, Some(RoundMovement::Refresh)).await?))
2149 }
2150
2151 pub async fn refresh_vtxos_delegated<V: VtxoRef>(
2157 &self,
2158 vtxos: impl IntoIterator<Item = V>,
2159 ) -> anyhow::Result<Option<StoredRoundState<Unlocked>>> {
2160 let part = match self.build_refresh_participation(vtxos).await? {
2161 Some(participation) => participation,
2162 None => return Ok(None),
2163 };
2164
2165 Ok(Some(self.join_delegated_round(
2166 part, Some(RoundMovement::Refresh), None,
2167 ).await?))
2168 }
2169
2170 pub async fn refresh_vtxos_scheduled<V: VtxoRef>(
2173 &self,
2174 vtxos: impl IntoIterator<Item = V>,
2175 scheduled_height: BlockHeight,
2176 ) -> anyhow::Result<Option<StoredRoundState<Unlocked>>> {
2177 let part = match self
2178 .build_scheduled_refresh_participation(vtxos, scheduled_height).await?
2179 {
2180 Some(participation) => participation,
2181 None => return Ok(None),
2182 };
2183
2184 Ok(Some(self.join_delegated_round(
2185 part, Some(RoundMovement::Refresh), Some(scheduled_height),
2186 ).await?))
2187 }
2188
2189 pub async fn get_vtxos_to_refresh(&self) -> anyhow::Result<Vec<WalletVtxo>> {
2192 let vtxos = self.spendable_vtxos_with(&RefreshStrategy::should_refresh_if_must(
2193 self,
2194 self.inner.chain.tip().await?,
2195 self.inner.chain.fee_rates().await.fast,
2196 )).await?;
2197 Ok(vtxos)
2198 }
2199
2200 pub async fn get_vtxos_to_refresh_with_excluded<V: VtxoRef>(
2203 &self,
2204 exclude: impl IntoIterator<Item = V>,
2205 ) -> anyhow::Result<Vec<WalletVtxo>> {
2206 let mut vtxos = self.get_vtxos_to_refresh().await?;
2207 for v in exclude.into_iter() {
2208 if let Some(index) = vtxos.iter().position(|vtxo| vtxo.id() == v.vtxo_id()) {
2209 vtxos.swap_remove(index);
2210 }
2211 }
2212 Ok(vtxos)
2213 }
2214
2215 pub async fn get_first_expiring_vtxo_blockheight(
2217 &self,
2218 ) -> anyhow::Result<Option<BlockHeight>> {
2219 Ok(self.spendable_vtxos().await?.iter().map(|v| v.expiry_height()).min())
2220 }
2221
2222 pub async fn get_next_required_refresh_blockheight(
2225 &self,
2226 ) -> anyhow::Result<Option<BlockHeight>> {
2227 let first_expiry = self.get_first_expiring_vtxo_blockheight().await?;
2228 Ok(first_expiry.map(|h| h.saturating_sub(self.inner.config.vtxo_refresh_expiry_threshold)))
2229 }
2230
2231 async fn spend_input_selection(&self) -> anyhow::Result<InputSelection> {
2235 let mut selection = InputSelection::new();
2236 if let Some(info) = self.ark_info().await? {
2237 selection = selection.max_exit_depth(info.max_vtxo_exit_depth);
2238 }
2239 Ok(selection)
2240 }
2241
2242 async fn select_any_vtxos_to_cover(
2244 &self,
2245 amount: Amount,
2246 ) -> anyhow::Result<Vec<WalletVtxo>> {
2247 self.spend_input_selection().await?.select(self.spendable_vtxos().await?, amount)
2248 }
2249
2250 async fn select_any_vtxos_to_cover_with_fee<F>(
2255 &self,
2256 amount: Amount,
2257 calc_fee: F,
2258 ) -> anyhow::Result<(Vec<WalletVtxo>, Amount)>
2259 where
2260 F: for<'a> Fn(Amount, SelectedFeeInfos<'a>) -> anyhow::Result<Amount>,
2261 {
2262 let tip = self.inner.chain.tip().await?;
2263 self.spend_input_selection().await?
2264 .fee_scheme(tip, calc_fee)
2265 .select(self.spendable_vtxos().await?, amount)
2266 }
2267
2268 pub fn start_daemon(&self) -> anyhow::Result<()> {
2278 let mut daemon = self.inner.daemon.lock();
2279 if daemon.is_some() {
2280 warn!("Called Wallet::start_daemon while daemon was already running.");
2281 return Ok(());
2282 }
2283
2284 let handle = crate::daemon::start_daemon(self);
2285 let _ = daemon.insert(handle);
2286
2287 Ok(())
2288 }
2289
2290 pub fn stop_daemon(&self) {
2292 let mut daemon = self.inner.daemon.lock();
2293 if let Some(handle) = daemon.take() {
2294 handle.stop();
2295 }
2296 }
2297
2298 async fn catchup_recovery_vtxos(&self) -> anyhow::Result<()> {
2316 let mut ids = self.inner.db.get_unregistered_vtxo_ids().await?;
2317 if ids.is_empty() {
2318 return Ok(());
2319 }
2320
2321 let in_progress_boards = self.boards_in_progress().await?;
2324 ids.retain(|id| !in_progress_boards.iter().any(|b| b.vtxo_id == *id));
2325 if ids.is_empty() {
2326 return Ok(());
2327 }
2328
2329 let posted = self.post_recovery_vtxo_ids(ids.iter().copied()).await
2334 .context("failed to post recovery vtxo IDs");
2335 let registered = self.register_recovery_vtxo_chains(&ids, posted.is_ok()).await;
2336
2337 match (posted, registered) {
2338 (Ok(()), registered) => registered,
2339 (posted, Ok(())) => posted,
2340 (Err(posted), Err(registered)) => {
2341 Err(registered.context(format!("mailbox post also failed: {:#}", posted)))
2342 },
2343 }
2344 }
2345
2346 async fn register_recovery_vtxo_chains(
2352 &self,
2353 ids: &[VtxoId],
2354 mark_registered: bool,
2355 ) -> anyhow::Result<()> {
2356 const CHUNK_SIZE: usize = 20;
2357 let mut failed = 0;
2358 for chunk_ids in ids.chunks(CHUNK_SIZE) {
2359 let chunk = self.inner.db.get_full_vtxos(chunk_ids).await
2362 .context("failed to load full vtxos for recovery registration")?;
2363 ensure!(chunk.len() == chunk_ids.len(),
2364 "loaded {} full vtxos for {} ids", chunk.len(), chunk_ids.len(),
2365 );
2366
2367 let mut succeeded = Vec::with_capacity(chunk.len());
2368 match self.register_vtxo_transactions_with_server(&chunk).await {
2369 Ok(()) => succeeded.extend(chunk.iter().map(|v| v.id())),
2370 Err(e) => {
2371 debug!("Failed to register chunk of {} vtxo transactions, \
2372 retrying one by one: {:#}", chunk.len(), e,
2373 );
2374 for vtxo in &chunk {
2375 match self.register_vtxo_transactions_with_server(
2376 std::slice::from_ref(vtxo),
2377 ).await {
2378 Ok(()) => succeeded.push(vtxo.id()),
2379 Err(e) => {
2380 error!("Failed to register vtxo {} transactions with server; \
2381 recovery from seed may miss it until registration succeeds: {:#}",
2382 vtxo.id(), e,
2383 );
2384 failed += 1;
2385 },
2386 }
2387 }
2388 },
2389 }
2390 if mark_registered && !succeeded.is_empty() {
2391 self.inner.db.mark_vtxos_registered(&succeeded).await
2392 .context("failed to mark vtxos as registered for recovery")?;
2393 }
2394 }
2395 if failed > 0 {
2396 bail!("failed to register {} of {} vtxo transactions", failed, ids.len());
2397 }
2398 Ok(())
2399 }
2400
2401 pub async fn register_vtxo_transactions_with_server(
2405 &self,
2406 vtxos: &[impl AsRef<Vtxo<Full>>],
2407 ) -> anyhow::Result<()> {
2408 if vtxos.is_empty() {
2409 return Ok(());
2410 }
2411
2412 let (mut srv, _) = self.require_server().await?;
2413 srv.client.register_vtxo_transactions(protos::RegisterVtxoTransactionsRequest {
2414 vtxos: vtxos.iter().map(|v| v.as_ref().serialize()).collect(),
2415 }).await.context("failed to register vtxo transactions")?;
2416
2417 Ok(())
2418 }
2419}
2420
2421fn wrap_server_connect_error(err: ConnectError) -> anyhow::Error {
2422 match err {
2423 ConnectError::CreateEndpoint(CreateEndpointError::NoTransportBackend) => {
2424 anyhow!(MISSING_SERVER_TRANSPORT_HELP)
2425 },
2426 other => anyhow::Error::from(other),
2427 }
2428}
2429
2430impl std::ops::Drop for WalletInner {
2431 fn drop(&mut self) {
2432 if let Some(handle) = self.daemon.lock().take() {
2439 handle.stop();
2440 }
2441 }
2442}
2443
2444#[cfg(test)]
2445mod tests {
2446 use server_rpc::client::CreateEndpointError;
2447
2448 use super::{wrap_server_connect_error, MISSING_SERVER_TRANSPORT_HELP};
2449
2450 #[test]
2451 fn no_transport_connect_error_is_reworded_for_wallet_users() {
2452 let err = wrap_server_connect_error(CreateEndpointError::NoTransportBackend.into());
2453 assert!(err.to_string().contains(MISSING_SERVER_TRANSPORT_HELP));
2454 assert!(err.to_string().contains("feature `bark-wallet/native` or `bark-wallet/wasm-web`"));
2455 }
2456}