1#[cfg(all(any(target_os = "android", target_os = "ios"), feature = "tls-native-roots"))]
297compile_error!("feature `tls-native-roots` can't be used on Android or iOS, use `tls-webpki-roots` instead");
298
299pub extern crate ark;
300
301pub extern crate bip39;
302pub extern crate lightning_invoice;
303pub extern crate lnurl as lnurllib;
304
305#[macro_use] extern crate anyhow;
306#[macro_use] extern crate async_trait;
307#[macro_use] extern crate serde;
308
309pub mod actions;
310pub mod chain;
311pub mod exit;
312pub mod movement;
313pub mod onchain;
314pub mod payment_request;
315pub mod persist;
316pub mod round;
317pub mod subsystem;
318pub mod vtxo;
319
320pub mod lock_manager;
321
322mod arkoor;
323mod board;
324mod config;
325mod daemon;
326mod fees;
327mod lightning;
328mod mailbox;
329mod notification;
330mod offboard;
331#[cfg(feature = "socks5-proxy")]
332mod proxy;
333mod psbtext;
334mod utils;
335
336pub use self::arkoor::{ArkoorCreateResult, ArkoorAddressError};
337pub use self::config::{BarkNetwork, Config};
338pub use self::daemon::DaemonHandle;
339pub use self::fees::FeeEstimate;
340pub use self::notification::{WalletNotification, NotificationStream};
341pub use self::vtxo::WalletVtxo;
342pub use self::utils::time;
343
344use std::borrow::Cow;
345use std::collections::HashSet;
346use std::sync::Arc;
347use std::time::Duration;
348
349use anyhow::{bail, Context};
350use ark::rounds::RoundEvent;
351use bip39::Mnemonic;
352use bitcoin::{Amount, Network, OutPoint};
353use bitcoin::bip32::{self, ChildNumber, Fingerprint};
354use bitcoin::secp256k1::{self, Keypair, PublicKey};
355use log::{debug, error, info, trace, warn};
356use tokio_stream::StreamExt;
357
358use ark::{ArkInfo, ProtocolEncoding, Vtxo, VtxoId, VtxoPolicy, VtxoRequest};
359use ark::address::VtxoDelivery;
360use ark::fees::{validate_and_subtract_fee_min_dust, VtxoFeeInfo};
361use ark::vtxo::{Full, PubkeyVtxoPolicy, VtxoRef};
362use ark::vtxo::policy::signing::VtxoSigner;
363use bitcoin_ext::{BlockHeight, P2TR_DUST};
364use server_rpc::{protos, ServerConnection};
365use server_rpc::client::{ConnectError, CreateEndpointError};
366
367use crate::chain::{ChainSource, ChainSourceSpec};
368use crate::exit::Exit;
369use crate::lock_manager::LockManager;
370use crate::movement::{Movement, MovementId, PaymentMethod};
371use crate::movement::manager::MovementManager;
372use crate::movement::update::MovementUpdate;
373use crate::notification::NotificationDispatch;
374use crate::onchain::{ExitUnilaterally, PreparePsbt, SignPsbt, Utxo};
375use crate::onchain::DaemonizableOnchainWallet;
376use crate::persist::BarkPersister;
377use crate::persist::models::{RoundStateId, StoredRoundState, Unlocked};
378#[cfg(feature = "socks5-proxy")]
379use crate::proxy::proxy_for_url;
380use crate::round::{RoundParticipation, RoundSecretNonces, RoundStatus};
381use crate::subsystem::{ArkoorMovement, RoundMovement};
382use crate::vtxo::{FilterVtxos, RefreshStrategy, VtxoFilter, VtxoStateKind};
383
384#[cfg(all(feature = "wasm-web", feature = "socks5-proxy"))]
385compile_error!("features `wasm-web` does not support feature `socks5-proxy");
386
387#[cfg(all(feature = "wasm-web", feature = "bitcoind-rpc"))]
388compile_error!("`wasm-web` does not support the `bitcoind-rpc` feature");
389
390const BARK_PURPOSE_INDEX: u32 = 350;
392const VTXO_KEYS_INDEX: u32 = 0;
394const MAILBOX_KEY_INDEX: u32 = 1;
396const RECOVERY_MAILBOX_KEY_INDEX: u32 = 2;
398const MISSING_SERVER_TRANSPORT_HELP: &str =
399 "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.";
400
401lazy_static::lazy_static! {
402 static ref SECP: secp256k1::Secp256k1<secp256k1::All> = secp256k1::Secp256k1::new();
404}
405
406fn log_server_pubkey_changed_error(expected: PublicKey, got: PublicKey) {
412 error!(
413 "
414Server public key has changed!
415
416The Ark server's public key is different from the one stored when this
417wallet was created. This typically happens when:
418
419 - The server operator has rotated their keys
420 - You are connecting to a different server
421 - The server has been replaced
422
423For safety, this wallet will not connect to the server until you
424resolve this. You can recover your funds on-chain by doing an emergency exit.
425
426This will exit your VTXOs to on-chain Bitcoin without needing the server's cooperation.
427
428Expected: {expected}
429Got: {got}")
430}
431
432fn log_server_mailbox_pubkey_changed_error(expected: PublicKey, got: PublicKey) {
434 error!(
435 "
436Server mailbox public key has changed!
437
438The Ark server's mailbox public key is different from the one stored when this
439wallet was created. This typically happens when:
440
441 - The server operator has rotated their keys
442 - You are connecting to a different server
443 - The server has been replaced
444
445For safety, this wallet will not connect to the server until you resolve this.
446
447Unlike a server pubkey change, your VTXOs are not at risk - the mailbox pubkey
448only affects address receive semantics. Any Ark addresses you previously
449shared will stop receiving new payments; you will need to share new addresses
450after reconnecting.
451
452Expected: {expected}
453Got: {got}")
454}
455
456#[derive(Debug, Clone)]
458pub struct LightningReceiveBalance {
459 pub total: Amount,
461 pub claimable: Amount,
463}
464
465#[derive(Debug, Clone)]
467pub struct Balance {
468 pub spendable: Amount,
470 pub pending_lightning_send: Amount,
472 pub claimable_lightning_receive: Amount,
474 pub pending_in_round: Amount,
476 pub pending_exit: Option<Amount>,
479 pub pending_board: Amount,
481}
482
483pub struct UtxoInfo {
484 pub outpoint: OutPoint,
485 pub amount: Amount,
486 pub confirmation_height: Option<u32>,
487}
488
489impl From<Utxo> for UtxoInfo {
490 fn from(value: Utxo) -> Self {
491 match value {
492 Utxo::Local(o) => UtxoInfo {
493 outpoint: o.outpoint,
494 amount: o.amount,
495 confirmation_height: o.confirmation_height,
496 },
497 Utxo::Exit(e) => UtxoInfo {
498 outpoint: e.vtxo.point(),
499 amount: e.vtxo.amount(),
500 confirmation_height: Some(e.height),
501 },
502 }
503 }
504}
505
506pub struct OffchainBalance {
509 pub available: Amount,
511 pub pending_in_round: Amount,
513 pub pending_exit: Amount,
516}
517
518#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
520pub struct WalletProperties {
521 pub network: Network,
525
526 pub fingerprint: Fingerprint,
530
531 pub server_pubkey: Option<PublicKey>,
538
539 pub server_mailbox_pubkey: Option<PublicKey>,
547}
548
549pub struct WalletSeed {
555 master: bip32::Xpriv,
556 vtxo: bip32::Xpriv,
557}
558
559impl WalletSeed {
560 fn new(network: Network, seed: &[u8; 64]) -> Self {
561 let bark_path = [ChildNumber::from_hardened_idx(BARK_PURPOSE_INDEX).unwrap()];
562 let master = bip32::Xpriv::new_master(network, seed)
563 .expect("invalid seed")
564 .derive_priv(&SECP, &bark_path)
565 .expect("purpose is valid");
566
567 let vtxo_path = [ChildNumber::from_hardened_idx(VTXO_KEYS_INDEX).unwrap()];
568 let vtxo = master.derive_priv(&SECP, &vtxo_path)
569 .expect("vtxo path is valid");
570
571 Self { master, vtxo }
572 }
573
574 fn fingerprint(&self) -> Fingerprint {
575 self.master.fingerprint(&SECP)
576 }
577
578 fn derive_vtxo_keypair(&self, idx: u32) -> Keypair {
579 self.vtxo.derive_priv(&SECP, &[idx.into()]).unwrap().to_keypair(&SECP)
580 }
581
582 fn to_mailbox_keypair(&self) -> Keypair {
583 let mailbox_path = [ChildNumber::from_hardened_idx(MAILBOX_KEY_INDEX).unwrap()];
584 self.master.derive_priv(&SECP, &mailbox_path).unwrap().to_keypair(&SECP)
585 }
586
587 fn to_recovery_mailbox_keypair(&self) -> Keypair {
588 let mailbox_path = [ChildNumber::from_hardened_idx(RECOVERY_MAILBOX_KEY_INDEX).unwrap()];
589 self.master.derive_priv(&SECP, &mailbox_path).unwrap().to_keypair(&SECP)
590 }
591}
592
593struct WalletInner {
594 chain: Arc<ChainSource>,
596
597 exit: Exit,
599
600 movements: Arc<MovementManager>,
602
603 notifications: NotificationDispatch,
605
606 config: Config,
608
609 db: Arc<dyn BarkPersister>,
611
612 lock_manager: Box<dyn LockManager>,
616
617 seed: WalletSeed,
619
620 server: tokio::sync::OnceCell<ServerConnection>,
627
628 daemon: parking_lot::Mutex<Option<DaemonHandle>>,
630
631 pub(crate) round_secret_nonces: RoundSecretNonces,
634}
635
636#[derive(Clone)]
777pub struct Wallet {
778 inner: Arc<WalletInner>,
779}
780
781impl Wallet {
782 pub async fn require_chainsource_version(&self) -> anyhow::Result<()> {
786 self.inner.chain.require_version().await
787 }
788
789 pub async fn network(&self) -> anyhow::Result<Network> {
790 Ok(self.properties().await?.network)
791 }
792
793 pub fn chain(&self) -> &Arc<ChainSource> {
795 &self.inner.chain
796 }
797
798 pub fn exit_mgr(&self) -> &Exit {
800 &self.inner.exit
801 }
802
803 pub fn movements_mgr(&self) -> &MovementManager {
805 &self.inner.movements
806 }
807
808 pub async fn peek_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
811 let last_revealed = self.inner.db.get_last_vtxo_key_index().await?;
812
813 let index = last_revealed.map(|i| i + 1).unwrap_or(u32::MIN);
814 let keypair = self.inner.seed.derive_vtxo_keypair(index);
815
816 Ok((keypair, index))
817 }
818
819 pub async fn derive_store_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
822 let (keypair, index) = self.peek_next_keypair().await?;
823 self.inner.db.store_vtxo_key(index, keypair.public_key()).await?;
824 Ok((keypair, index))
825 }
826
827 #[deprecated(note = "use peek_keypair instead")]
828 pub async fn peak_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
829 self.peek_keypair(index).await
830 }
831
832 pub async fn peek_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
846 let keypair = self.inner.seed.derive_vtxo_keypair(index);
847 if self.inner.db.get_public_key_idx(&keypair.public_key()).await?.is_some() {
848 Ok(keypair)
849 } else {
850 bail!("VTXO key {} does not exist, please derive it first", index)
851 }
852 }
853
854
855 pub async fn pubkey_keypair(&self, public_key: &PublicKey) -> anyhow::Result<Option<(u32, Keypair)>> {
867 if let Some(index) = self.inner.db.get_public_key_idx(&public_key).await? {
868 Ok(Some((index, self.inner.seed.derive_vtxo_keypair(index))))
869 } else {
870 Ok(None)
871 }
872 }
873
874 pub async fn get_vtxo_key(&self, vtxo: impl VtxoRef) -> anyhow::Result<Keypair> {
885 let bare_vtxo = match vtxo.as_bare_vtxo() {
886 Some(bare) => bare,
887 None => Cow::Owned(self.get_vtxo_by_id(vtxo.vtxo_id()).await?.vtxo),
888 };
889 let pubkey = self.find_signable_clause(&bare_vtxo).await
890 .context("VTXO is not signable by wallet")?
891 .pubkey();
892 let idx = self.inner.db.get_public_key_idx(&pubkey).await?
893 .context("VTXO key not found")?;
894 Ok(self.inner.seed.derive_vtxo_keypair(idx))
895 }
896
897 #[deprecated(note = "use peek_address instead")]
898 pub async fn peak_address(&self, index: u32) -> anyhow::Result<ark::Address> {
899 self.peek_address(index).await
900 }
901
902 pub async fn peek_address(&self, index: u32) -> anyhow::Result<ark::Address> {
906 let properties = self.properties().await?;
907 let network = properties.network;
908 let keypair = self.peek_keypair(index).await?;
909 let mailbox = self.mailbox_identifier();
910
911
912 let (server_pubkey, mailbox_pubkey) =
913 if let (Some(spk), Some(mpk)) = (properties.server_pubkey, properties.server_mailbox_pubkey) {
914 (spk, mpk)
915 } else {
916 let (_, ark_info) = self.require_server().await?;
917 (ark_info.server_pubkey, ark_info.mailbox_pubkey)
918 };
919
920 Ok(ark::Address::builder()
921 .testnet(network != bitcoin::Network::Bitcoin)
922 .server_pubkey(server_pubkey)
923 .pubkey_policy(keypair.public_key())
924 .mailbox(mailbox_pubkey, mailbox, &keypair)
925 .expect("Failed to assign mailbox")
926 .into_address().unwrap())
927 }
928
929 pub async fn new_address_with_index(&self) -> anyhow::Result<(ark::Address, u32)> {
933 let (_, index) = self.derive_store_next_keypair().await?;
934 let addr = self.peek_address(index).await?;
935 Ok((addr, index))
936 }
937
938 pub async fn new_address(&self) -> anyhow::Result<ark::Address> {
940 let (addr, _) = self.new_address_with_index().await?;
941 Ok(addr)
942 }
943
944 pub async fn create(
953 mnemonic: &Mnemonic,
954 network: Network,
955 config: Config,
956 db: Arc<dyn BarkPersister>,
957 lock_manager: Box<dyn LockManager>,
958 force: bool,
959 ) -> anyhow::Result<Wallet> {
960 trace!("Config: {:?}", config);
961
962 let wallet_fingerprint = WalletSeed::new(network, &mnemonic.to_seed("")).fingerprint();
963
964 let create_guard = lock_manager.lock(
969 &format!("{}.create", wallet_fingerprint),
970 Duration::from_secs(5),
971 ).await.context("wallet initialization already in progress")?;
972
973 if let Some(existing) = db.read_properties().await? {
974 trace!("Existing config: {:?}", existing);
975 bail!("cannot overwrite already existing config")
976 }
977
978 let (server_pubkey, mailbox_pubkey) = if !force {
980 match Self::connect_to_server(&config, network).await {
981 Ok(conn) => {
982 let ark_info = conn.ark_info().await;
983 (Some(ark_info.server_pubkey), Some(ark_info.mailbox_pubkey))
984 }
985 Err(err) => {
986 bail!("Failed to connect to provided server (if you are sure use the --force flag): {:#}", err);
987 }
988 }
989 } else {
990 (None, None)
991 };
992
993 let properties = WalletProperties {
994 network,
995 fingerprint: wallet_fingerprint,
996 server_pubkey,
997 server_mailbox_pubkey: mailbox_pubkey,
998 };
999
1000 db.init_wallet(&properties).await.context("cannot init wallet in the database")?;
1002 info!("Created wallet with fingerprint: {}", wallet_fingerprint);
1003 if let Some(pk) = server_pubkey {
1004 info!("Stored server pubkey: {}", pk);
1005 }
1006
1007 drop(create_guard);
1010
1011 let wallet = Wallet::open(&mnemonic, db, config, lock_manager).await.context("failed to open wallet")?;
1013 wallet.require_chainsource_version().await?;
1014
1015 Ok(wallet)
1016 }
1017
1018 pub async fn create_with_exits(
1025 mnemonic: &Mnemonic,
1026 network: Network,
1027 config: Config,
1028 db: Arc<dyn BarkPersister>,
1029 lock_manager: Box<dyn LockManager>,
1030 force: bool,
1031 ) -> anyhow::Result<Wallet> {
1032 let wallet = Wallet::create(mnemonic, network, config, db, lock_manager, force).await?;
1033 wallet.inner.exit.load().await?;
1034 Ok(wallet)
1035 }
1036
1037 pub async fn open(
1042 mnemonic: &Mnemonic,
1043 db: Arc<dyn BarkPersister>,
1044 config: Config,
1045 lock_manager: Box<dyn LockManager>,
1046 ) -> anyhow::Result<Wallet> {
1047 let properties = db.read_properties().await?.context("Wallet is not initialised")?;
1048
1049 let seed = {
1050 let seed = mnemonic.to_seed("");
1051 WalletSeed::new(properties.network, &seed)
1052 };
1053
1054 if properties.fingerprint != seed.fingerprint() {
1055 bail!("incorrect mnemonic")
1056 }
1057
1058 let chain_source = if let Some(ref url) = config.esplora_address {
1059 ChainSourceSpec::Esplora {
1060 url: url.clone(),
1061 }
1062 } else if let Some(ref url) = config.bitcoind_address {
1063 let auth = if let Some(ref c) = config.bitcoind_cookiefile {
1064 bitcoin_ext::rpc::Auth::CookieFile(c.clone())
1065 } else {
1066 bitcoin_ext::rpc::Auth::UserPass(
1067 config.bitcoind_user.clone().context("need bitcoind auth config")?,
1068 config.bitcoind_pass.clone().context("need bitcoind auth config")?,
1069 )
1070 };
1071 ChainSourceSpec::Bitcoind { url: url.clone(), auth }
1072 } else {
1073 bail!("Need to either provide esplora or bitcoind info");
1074 };
1075
1076 #[cfg(feature = "socks5-proxy")]
1077 let chain_proxy = proxy_for_url(&config.socks5_proxy, chain_source.url())?;
1078 let chain_source_client = ChainSource::new(
1079 chain_source, properties.network, config.fallback_fee_rate,
1080 #[cfg(feature = "socks5-proxy")] chain_proxy.as_deref(),
1081 ).await?;
1082 let chain = Arc::new(chain_source_client);
1083
1084 let server = tokio::sync::OnceCell::new();
1085
1086 let notifications = NotificationDispatch::new();
1087 let movements = Arc::new(MovementManager::new(db.clone(), notifications.clone()));
1088 let exit = Exit::new(db.clone(), chain.clone(), movements.clone()).await?;
1089
1090 Ok(Wallet { inner: Arc::new(WalletInner {
1091 config, db, lock_manager, seed, exit, movements, notifications, server, chain,
1092 daemon: parking_lot::Mutex::new(None),
1093 round_secret_nonces: RoundSecretNonces::new(),
1094 })})
1095 }
1096
1097 pub async fn open_with_exits(
1101 mnemonic: &Mnemonic,
1102 db: Arc<dyn BarkPersister>,
1103 cfg: Config,
1104 lock_manager: Box<dyn LockManager>,
1105 ) -> anyhow::Result<Wallet> {
1106 let wallet = Wallet::open(mnemonic, db, cfg, lock_manager).await?;
1107 wallet.inner.exit.load().await?;
1108 Ok(wallet)
1109 }
1110
1111 pub async fn open_with_daemon(
1114 mnemonic: &Mnemonic,
1115 db: Arc<dyn BarkPersister>,
1116 cfg: Config,
1117 onchain: Option<Arc<tokio::sync::RwLock<dyn DaemonizableOnchainWallet>>>,
1118 lock_manager: Box<dyn LockManager>,
1119 ) -> anyhow::Result<Wallet> {
1120 let wallet = Wallet::open(mnemonic, db, cfg, lock_manager).await?;
1121 if onchain.is_some() {
1122 wallet.inner.exit.load().await?;
1123 }
1124
1125 wallet.start_daemon(onchain)?;
1126
1127 Ok(wallet)
1128 }
1129
1130 pub fn config(&self) -> &Config {
1132 &self.inner.config
1133 }
1134
1135 pub async fn properties(&self) -> anyhow::Result<WalletProperties> {
1137 let properties = self.inner.db.read_properties().await?.context("Wallet is not initialised")?;
1138 Ok(properties)
1139 }
1140
1141 pub fn fingerprint(&self) -> Fingerprint {
1143 self.inner.seed.fingerprint()
1144 }
1145
1146 async fn connect_to_server(
1147 config: &Config,
1148 network: Network,
1149 ) -> anyhow::Result<ServerConnection> {
1150 let server_address = crate::utils::url_with_default_https_scheme(&config.server_address);
1151 let mut builder = ServerConnection::builder()
1152 .address(&server_address)
1153 .network(network);
1154
1155 #[cfg(feature = "socks5-proxy")]
1156 if let Some(proxy) = proxy_for_url(&config.socks5_proxy, &server_address)? {
1157 builder = builder.proxy(&proxy)
1158 }
1159
1160 if let Some(ref token) = config.server_access_token {
1161 builder = builder.access_token(token);
1162 }
1163
1164 builder.connect().await.map_err(wrap_server_connect_error)
1165 .context("Failed to connect to Ark server")
1166 }
1167
1168 async fn require_server(&self) -> anyhow::Result<(ServerConnection, ArkInfo)> {
1169 let conn = self.inner.server.get_or_try_init(|| async {
1173 let network = self.properties().await?.network;
1174 Self::connect_to_server(&self.inner.config, network).await
1175 .context("You should be connected to Ark server to perform this action")
1176 }).await?.clone();
1177
1178 let ark_info = conn.ark_info().await;
1179 self.check_and_store_server_keys(&ark_info).await?;
1180
1181 Ok((conn, ark_info))
1182 }
1183
1184 pub async fn refresh_server(&self) -> anyhow::Result<()> {
1185 let srv = self.inner.server.get_or_try_init(|| async {
1191 let properties = self.properties().await?;
1192 Self::connect_to_server(&self.inner.config, properties.network).await
1193 .map_err(anyhow::Error::from)
1194 }).await?;
1195
1196 srv.check_connection().await?;
1197 let ark_info = srv.ark_info().await;
1198 ark_info.fees.validate().context("invalid fee schedule")?;
1199 self.check_and_store_server_keys(&ark_info).await?;
1200
1201 Ok(())
1202 }
1203
1204 async fn check_and_store_server_keys(&self, ark_info: &ArkInfo) -> anyhow::Result<()> {
1211 let properties = self.properties().await?;
1212
1213 if let Some(stored_pubkey) = properties.server_pubkey {
1214 if stored_pubkey != ark_info.server_pubkey {
1215 log_server_pubkey_changed_error(stored_pubkey, ark_info.server_pubkey);
1216 bail!("Server public key has changed. You should exit all your VTXOs!");
1217 }
1218 } else {
1219 self.inner.db.set_server_pubkey(ark_info.server_pubkey).await?;
1220 info!("Stored server pubkey for existing wallet: {}", ark_info.server_pubkey);
1221 }
1222
1223 if let Some(stored_mailbox_pubkey) = properties.server_mailbox_pubkey {
1224 if stored_mailbox_pubkey != ark_info.mailbox_pubkey {
1225 log_server_mailbox_pubkey_changed_error(stored_mailbox_pubkey, ark_info.mailbox_pubkey);
1226 bail!("Server mailbox public key has changed.");
1227 }
1228 } else {
1229 self.inner.db.set_server_mailbox_pubkey(ark_info.mailbox_pubkey).await?;
1230 info!("Stored server mailbox pubkey for existing wallet: {}", ark_info.mailbox_pubkey);
1231 }
1232
1233 Ok(())
1234 }
1235
1236 pub async fn ark_info(&self) -> anyhow::Result<Option<ArkInfo>> {
1238 match self.inner.server.get() {
1239 Some(srv) => Ok(Some(srv.ark_info().await)),
1240 None => Ok(None),
1241 }
1242 }
1243
1244 pub async fn require_ark_info(&self) -> anyhow::Result<ArkInfo> {
1250 let (_, ark_info) = self.require_server().await?;
1251 Ok(ark_info)
1252 }
1253
1254 pub async fn balance(&self) -> anyhow::Result<Balance> {
1258 let vtxos = self.vtxos().await?;
1259
1260 let spendable = {
1261 let mut v = vtxos.iter().collect();
1262 VtxoStateKind::Spendable.filter_vtxos(&mut v).await?;
1263 v.into_iter().map(|v| v.amount()).sum::<Amount>()
1264 };
1265
1266 let pending_lightning_send = self.pending_lightning_send_vtxos().await?.iter()
1267 .map(|v| v.amount())
1268 .sum::<Amount>();
1269
1270 let claimable_lightning_receive = self.claimable_lightning_receive_balance().await?;
1271
1272 let pending_board = self.pending_board_vtxos().await?.iter()
1273 .map(|v| v.amount())
1274 .sum::<Amount>();
1275
1276 let pending_in_round = self.pending_round_balance().await?;
1277
1278 let pending_exit = self.exit_mgr().try_pending_total();
1279
1280 Ok(Balance {
1281 spendable,
1282 pending_in_round,
1283 pending_lightning_send,
1284 claimable_lightning_receive,
1285 pending_exit,
1286 pending_board,
1287 })
1288 }
1289
1290 pub async fn validate_vtxo(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<()> {
1292 let tx = self.inner.chain.get_tx(&vtxo.chain_anchor().txid).await
1293 .context("could not fetch chain tx")?;
1294
1295 let tx = tx.with_context(|| {
1296 format!("vtxo chain anchor not found for vtxo: {}", vtxo.chain_anchor().txid)
1297 })?;
1298
1299 vtxo.validate(&tx)?;
1300
1301 Ok(())
1302 }
1303
1304 pub async fn import_vtxo(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<()> {
1314 if self.inner.db.get_wallet_vtxo(vtxo.id()).await?.is_some() {
1315 info!("VTXO {} already exists in wallet, skipping import", vtxo.id());
1316 return Ok(());
1317 }
1318
1319 self.validate_vtxo(vtxo).await.context("VTXO validation failed")?;
1320
1321 if self.find_signable_clause(vtxo).await.is_none() {
1322 bail!("VTXO {} is not owned by this wallet (no signable clause found)", vtxo.id());
1323 }
1324
1325 let current_height = self.inner.chain.tip().await?;
1326 if vtxo.expiry_height() <= current_height {
1327 bail!("Vtxo {} has expired", vtxo.id());
1328 }
1329
1330 self.store_spendable_vtxos([vtxo]).await.context("failed to store imported VTXO")?;
1331
1332 info!("Successfully imported VTXO {}", vtxo.id());
1333 Ok(())
1334 }
1335
1336 pub async fn get_vtxo_by_id(&self, vtxo_id: VtxoId) -> anyhow::Result<WalletVtxo> {
1338 let vtxo = self.inner.db.get_wallet_vtxo(vtxo_id).await
1339 .with_context(|| format!("Error when querying vtxo {} in database", vtxo_id))?
1340 .with_context(|| format!("The VTXO with id {} cannot be found", vtxo_id))?;
1341 Ok(vtxo)
1342 }
1343
1344 pub async fn get_full_vtxo(&self, vtxo_id: VtxoId) -> anyhow::Result<Vtxo<Full>> {
1352 self.inner.db.get_full_vtxo(vtxo_id).await
1353 .with_context(|| format!("Error when querying full vtxo {} in database", vtxo_id))?
1354 .with_context(|| format!("The VTXO with id {} cannot be found", vtxo_id))
1355 }
1356
1357 pub async fn get_full_vtxos<V: VtxoRef>(
1359 &self,
1360 vtxos: impl IntoIterator<Item = V>,
1361 ) -> anyhow::Result<Vec<Vtxo<Full>>> {
1362 let ids = vtxos.into_iter().map(|v| v.vtxo_id()).collect::<Vec<_>>();
1363 self.inner.db.get_full_vtxos(&ids).await
1364 .with_context(||
1365 format!("Error when querying full vtxos in database with IDs: {:?}", ids)
1366 )
1367 }
1368
1369 #[deprecated(since="0.1.0-beta.5", note = "Use Wallet::history instead")]
1371 pub async fn movements(&self) -> anyhow::Result<Vec<Movement>> {
1372 self.history().await
1373 }
1374
1375 pub async fn history(&self) -> anyhow::Result<Vec<Movement>> {
1377 Ok(self.inner.db.get_all_movements().await?)
1378 }
1379
1380 pub async fn update_history_metadata(
1400 &self,
1401 movement_id: MovementId,
1402 patch: &serde_json::Value,
1403 ) -> anyhow::Result<()> {
1404 self.inner.movements.patch_metadata(movement_id, patch).await?;
1405 Ok(())
1406 }
1407
1408 pub async fn history_by_payment_method(
1410 &self,
1411 payment_method: &PaymentMethod,
1412 ) -> anyhow::Result<Vec<Movement>> {
1413 let mut ret = self.inner.db.get_movements_by_payment_method(payment_method).await?;
1414 ret.sort_by_key(|m| m.id);
1415 Ok(ret)
1416 }
1417
1418 pub async fn all_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1420 Ok(self.inner.db.get_all_vtxos().await?)
1421 }
1422
1423 pub async fn vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1425 Ok(self.inner.db.get_vtxos_by_state(&VtxoStateKind::UNSPENT_STATES).await?)
1426 }
1427
1428 pub async fn vtxos_with(&self, filter: &impl FilterVtxos) -> anyhow::Result<Vec<WalletVtxo>> {
1430 let mut vtxos = self.vtxos().await?;
1431 filter.filter_vtxos(&mut vtxos).await.context("error filtering vtxos")?;
1432 Ok(vtxos)
1433 }
1434
1435 pub async fn spendable_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1437 Ok(self.vtxos_with(&VtxoStateKind::Spendable).await?)
1438 }
1439
1440 pub async fn spendable_vtxos_with(
1442 &self,
1443 filter: &impl FilterVtxos,
1444 ) -> anyhow::Result<Vec<WalletVtxo>> {
1445 let mut vtxos = self.spendable_vtxos().await?;
1446 filter.filter_vtxos(&mut vtxos).await.context("error filtering vtxos")?;
1447 Ok(vtxos)
1448 }
1449
1450 pub async fn get_expiring_vtxos(
1452 &self,
1453 threshold: BlockHeight,
1454 ) -> anyhow::Result<Vec<WalletVtxo>> {
1455 let expiry = self.inner.chain.tip().await? + threshold;
1456 let filter = VtxoFilter::new(&self).expires_before(expiry);
1457 Ok(self.spendable_vtxos_with(&filter).await?)
1458 }
1459
1460 pub async fn maintenance(&self) -> anyhow::Result<()> {
1466 info!("Starting wallet maintenance in interactive mode");
1467 self.sync().await;
1468
1469 let rounds = self.progress_pending_rounds(None).await;
1471 if let Err(e) = rounds.as_ref() {
1472 warn!("Error progressing pending rounds: {:#}", e);
1473 }
1474
1475 let states = self.inner.db.get_pending_round_state_ids().await?;
1477 for id in states {
1478 debug!("Cancelling pending round participation {}", id);
1479 let mut state = match self.lock_wait_round_state(id).await {
1480 Ok(Some(s)) => s,
1481 Ok(None) => continue, Err(e) => {
1483 warn!("Failed to lock round state with id {}: {:#}", id, e);
1484 continue;
1485 }
1486 };
1487 if let Err(e) = state.state_mut().try_cancel(self).await {
1488 warn!("Error cancelling pending round: {:#}", e);
1489 }
1490 }
1491
1492 let refresh = self.maintenance_refresh().await;
1494 if let Err(e) = refresh.as_ref() {
1495 warn!("Error refreshing VTXOs: {:#}", e);
1496 }
1497 if rounds.is_err() || refresh.is_err() {
1498 bail!("Maintenance encountered errors.\nprogress_rounds: {:#?}\nrefresh: {:#?}", rounds, refresh);
1499 }
1500 Ok(())
1501 }
1502
1503 pub async fn maintenance_delegated(&self) -> anyhow::Result<()> {
1509 info!("Starting wallet maintenance in delegated mode");
1510 self.sync().await;
1511 let rounds = self.progress_pending_rounds(None).await;
1512 if let Err(e) = rounds.as_ref() {
1513 warn!("Error progressing pending rounds: {:#}", e);
1514 }
1515 let refresh = self.maybe_schedule_maintenance_refresh_delegated().await;
1516 if let Err(e) = refresh.as_ref() {
1517 warn!("Error refreshing VTXOs: {:#}", e);
1518 }
1519 if rounds.is_err() || refresh.is_err() {
1520 bail!("Delegated maintenance encountered errors.\nprogress_rounds: {:#?}\nrefresh: {:#?}", rounds, refresh);
1521 }
1522 Ok(())
1523 }
1524
1525 pub async fn maintenance_with_onchain<W: PreparePsbt + SignPsbt + ExitUnilaterally>(
1533 &self,
1534 onchain: &mut W,
1535 ) -> anyhow::Result<()> {
1536 info!("Starting wallet maintenance in interactive mode with onchain wallet");
1537
1538 let maintenance = self.maintenance().await;
1540
1541 let exit_sync = self.sync_exits().await;
1543 if let Err(e) = exit_sync.as_ref() {
1544 warn!("Error syncing exits: {:#}", e);
1545 }
1546 let exit_progress = self.exit_mgr().progress_exits_with_bdk(self, onchain, None).await;
1547 if let Err(e) = exit_progress.as_ref() {
1548 warn!("Error progressing exits: {:#}", e);
1549 }
1550 if maintenance.is_err() || exit_sync.is_err() || exit_progress.is_err() {
1551 bail!("Maintenance encountered errors.\nmaintenance: {:#?}\nexit_sync: {:#?}\nexit_progress: {:#?}", maintenance, exit_sync, exit_progress);
1552 }
1553 Ok(())
1554 }
1555
1556 pub async fn maintenance_with_onchain_delegated<W: PreparePsbt + SignPsbt + ExitUnilaterally>(
1563 &self,
1564 onchain: &mut W,
1565 ) -> anyhow::Result<()> {
1566 info!("Starting wallet maintenance in delegated mode with onchain wallet");
1567
1568 let maintenance = self.maintenance_delegated().await;
1570
1571 let exit_sync = self.sync_exits().await;
1573 if let Err(e) = exit_sync.as_ref() {
1574 warn!("Error syncing exits: {:#}", e);
1575 }
1576 let exit_progress = self.exit_mgr().progress_exits_with_bdk(self, onchain, None).await;
1577 if let Err(e) = exit_progress.as_ref() {
1578 warn!("Error progressing exits: {:#}", e);
1579 }
1580 if maintenance.is_err() || exit_sync.is_err() || exit_progress.is_err() {
1581 bail!("Delegated maintenance encountered errors.\nmaintenance: {:#?}\nexit_sync: {:#?}\nexit_progress: {:#?}", maintenance, exit_sync, exit_progress);
1582 }
1583 Ok(())
1584 }
1585
1586 pub async fn maybe_schedule_maintenance_refresh(&self) -> anyhow::Result<Option<RoundStateId>> {
1594 let vtxos = self.get_vtxos_to_refresh().await?;
1595 if vtxos.len() == 0 {
1596 return Ok(None);
1597 }
1598
1599 let participation = match self.build_refresh_participation(vtxos).await? {
1600 Some(participation) => participation,
1601 None => return Ok(None),
1602 };
1603
1604 info!("Scheduling maintenance refresh ({} vtxos)", participation.inputs.len());
1605 let state = self.join_next_round(participation, Some(RoundMovement::Refresh)).await?;
1606 Ok(Some(state.id()))
1607 }
1608
1609 pub async fn maybe_schedule_maintenance_refresh_delegated(
1617 &self,
1618 ) -> anyhow::Result<Option<RoundStateId>> {
1619 let vtxos = self.get_vtxos_to_refresh().await?;
1620 if vtxos.len() == 0 {
1621 return Ok(None);
1622 }
1623
1624 let participation = match self.build_refresh_participation(vtxos).await? {
1625 Some(participation) => participation,
1626 None => return Ok(None),
1627 };
1628
1629 info!("Scheduling delegated maintenance refresh ({} vtxos)", participation.inputs.len());
1630 let state = self.join_next_round_delegated(participation, Some(RoundMovement::Refresh)).await?;
1631 Ok(Some(state.id()))
1632 }
1633
1634 pub async fn maintenance_refresh(&self) -> anyhow::Result<Option<RoundStatus>> {
1642 let vtxos = self.get_vtxos_to_refresh().await?;
1643 if vtxos.len() == 0 {
1644 return Ok(None);
1645 }
1646
1647 info!("Waiting for round to perform maintenance refresh...");
1648 let mut events = self.subscribe_round_events().await?;
1649 while let Some(event) = events.next().await {
1650 match event {
1651 Ok(RoundEvent::Attempt(a)) if a.attempt_seq == 0 => {
1652 let vtxos = self.get_vtxos_to_refresh().await?;
1653 if vtxos.len() == 0 {
1654 return Ok(None);
1655 }
1656 debug!("Round {} started, triggering refresh", a.round_seq);
1657 return self.refresh_vtxos(vtxos).await;
1658 },
1659 _ => {},
1660 }
1661 }
1662 Ok(None)
1663 }
1664
1665 pub async fn sync(&self) {
1672 futures::join!(
1673 async {
1674 if let Err(e) = self.inner.chain.update_fee_rates(self.inner.config.fallback_fee_rate).await {
1677 warn!("Error updating fee rates: {:#}", e);
1678 }
1679 },
1680 async {
1681 if let Err(e) = self.sync_mailbox().await {
1682 warn!("Error in mailbox sync: {:#}", e);
1683 }
1684 },
1685 async {
1686 if let Err(e) = self.sync_pending_rounds().await {
1687 warn!("Error while trying to progress rounds awaiting confirmations: {:#}", e);
1688 }
1689 },
1690 async {
1691 if let Err(e) = self.sync_pending_lightning_send_vtxos().await {
1692 warn!("Error syncing pending lightning payments: {:#}", e);
1693 }
1694 },
1695 async {
1696 if let Err(e) = self.try_claim_all_lightning_receives(false).await {
1697 warn!("Error claiming pending lightning receives: {:#}", e);
1698 }
1699 },
1700 async {
1701 if let Err(e) = self.sync_pending_boards().await {
1702 warn!("Error syncing pending boards: {:#}", e);
1703 }
1704 },
1705 async {
1706 if let Err(e) = self.sync_pending_offboards().await {
1707 warn!("Error syncing pending offboards: {:#}", e);
1708 }
1709 }
1710 );
1711 }
1712
1713 pub async fn sync_exits(&self) -> anyhow::Result<()> {
1719 self.exit_mgr().sync(&self).await?;
1720 Ok(())
1721 }
1722
1723 pub async fn dangerous_drop_vtxo(&self, vtxo_id: VtxoId) -> anyhow::Result<()> {
1726 warn!("Drop vtxo {} from the database", vtxo_id);
1727 self.inner.db.remove_vtxo(vtxo_id).await?;
1728 Ok(())
1729 }
1730
1731 pub async fn dangerous_drop_all_vtxos(&self) -> anyhow::Result<()> {
1734 warn!("Dropping all vtxos from the db...");
1735 for vtxo in self.vtxos().await? {
1736 self.inner.db.remove_vtxo(vtxo.id()).await?;
1737 }
1738
1739 self.exit_mgr().dangerous_clear_exit().await?;
1740 Ok(())
1741 }
1742
1743 async fn has_counterparty_risk(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<bool> {
1751 for past_pks in vtxo.past_arkoor_pubkeys() {
1752 let mut owns_any = false;
1753 for past_pk in past_pks {
1754 if self.inner.db.get_public_key_idx(&past_pk).await?.is_some() {
1755 owns_any = true;
1756 break;
1757 }
1758 }
1759 if !owns_any {
1760 return Ok(true);
1761 }
1762 }
1763
1764 let my_clause = self.find_signable_clause(vtxo).await;
1765 Ok(!my_clause.is_some())
1766 }
1767
1768 async fn add_should_refresh_vtxos(
1774 &self,
1775 participation: &mut RoundParticipation,
1776 ) -> anyhow::Result<()> {
1777 let tip = self.inner.chain.tip().await?;
1780 let mut vtxos_to_refresh = self.spendable_vtxos_with(
1781 &RefreshStrategy::should_refresh(self, tip, self.inner.chain.fee_rates().await.fast),
1782 ).await?;
1783 if vtxos_to_refresh.is_empty() {
1784 return Ok(());
1785 }
1786
1787 let excluded_ids = participation.inputs.iter().map(|v| v.vtxo_id())
1788 .collect::<HashSet<_>>();
1789 let mut total_amount = Amount::ZERO;
1790 for i in (0..vtxos_to_refresh.len()).rev() {
1791 let vtxo = &vtxos_to_refresh[i];
1792 if excluded_ids.contains(&vtxo.id()) {
1793 vtxos_to_refresh.swap_remove(i);
1794 continue;
1795 }
1796 total_amount += vtxo.amount();
1797 }
1798 if vtxos_to_refresh.is_empty() {
1799 return Ok(());
1801 }
1802
1803 let (_, ark_info) = self.require_server().await?;
1806 let fee = ark_info.fees.refresh.calculate_no_base_fee(
1807 vtxos_to_refresh.iter().map(|wv| VtxoFeeInfo::from_vtxo_and_tip(&wv.vtxo, tip)),
1808 ).context("fee overflowed")?;
1809
1810 let output_amount = match validate_and_subtract_fee_min_dust(total_amount, fee) {
1812 Ok(amount) => amount,
1813 Err(e) => {
1814 trace!("Cannot add should-refresh VTXOs: {}", e);
1815 return Ok(());
1816 },
1817 };
1818 info!(
1819 "Adding {} extra VTXOs to round participation total = {}, fee = {}, output = {}",
1820 vtxos_to_refresh.len(), total_amount, fee, output_amount,
1821 );
1822 let (user_keypair, _) = self.derive_store_next_keypair().await?;
1823 let req = VtxoRequest {
1824 policy: VtxoPolicy::new_pubkey(user_keypair.public_key()),
1825 amount: output_amount,
1826 };
1827 let extra_ids = vtxos_to_refresh.into_iter().map(|wv| wv.id()).collect::<Vec<_>>();
1828 let extra_full = self.inner.db.get_full_vtxos(&extra_ids).await
1829 .context("failed to hydrate refresh candidates")?;
1830 participation.inputs.reserve(extra_full.len());
1831 participation.inputs.extend(extra_full);
1832 participation.outputs.push(req);
1833
1834 Ok(())
1835 }
1836
1837 pub async fn build_refresh_participation<V: VtxoRef>(
1838 &self,
1839 vtxos: impl IntoIterator<Item = V>,
1840 ) -> anyhow::Result<Option<RoundParticipation>> {
1841 let (vtxos, total_amount) = {
1842 let iter = vtxos.into_iter();
1843 let size_hint = iter.size_hint();
1844 let mut vtxos = Vec::<Vtxo<Full>>::with_capacity(size_hint.1.unwrap_or(size_hint.0));
1845 let mut amount = Amount::ZERO;
1846 for vref in iter {
1847 let id = vref.vtxo_id();
1852 if vtxos.iter().any(|v| v.id() == id) {
1853 bail!("duplicate VTXO id: {}", id);
1854 }
1855 let vtxo = if let Some(vtxo) = vref.into_full_vtxo() {
1856 vtxo
1857 } else {
1858 self.inner.db.get_full_vtxo(id).await?
1861 .with_context(|| format!("vtxo with id {} not found", id))?
1862 };
1863 amount += vtxo.amount();
1864 vtxos.push(vtxo);
1865 }
1866 (vtxos, amount)
1867 };
1868
1869 if vtxos.is_empty() {
1870 info!("Skipping refresh since no VTXOs are provided.");
1871 return Ok(None);
1872 }
1873 ensure!(total_amount >= P2TR_DUST,
1874 "vtxo amount must be at least {} to participate in a round",
1875 P2TR_DUST,
1876 );
1877
1878 let (_, ark_info) = self.require_server().await?;
1880 let current_height = self.inner.chain.tip().await?;
1881 let vtxo_fee_infos = vtxos.iter()
1882 .map(|v| VtxoFeeInfo::from_vtxo_and_tip(v, current_height));
1883 let fee = ark_info.fees.refresh.calculate(vtxo_fee_infos).context("fee overflowed")?;
1884 let output_amount = validate_and_subtract_fee_min_dust(total_amount, fee)?;
1885
1886 info!("Refreshing {} VTXOs (total amount = {}, fee = {}, output = {}).",
1887 vtxos.len(), total_amount, fee, output_amount,
1888 );
1889 let (user_keypair, _) = self.derive_store_next_keypair().await?;
1890 let req = VtxoRequest {
1891 policy: VtxoPolicy::Pubkey(PubkeyVtxoPolicy { user_pubkey: user_keypair.public_key() }),
1892 amount: output_amount,
1893 };
1894
1895 Ok(Some(RoundParticipation {
1896 inputs: vtxos,
1897 outputs: vec![req],
1898 unblinded_mailbox_id: None,
1899 }))
1900 }
1901
1902 pub async fn refresh_vtxos<V: VtxoRef>(
1907 &self,
1908 vtxos: impl IntoIterator<Item = V>,
1909 ) -> anyhow::Result<Option<RoundStatus>> {
1910 let mut participation = match self.build_refresh_participation(vtxos).await? {
1911 Some(participation) => participation,
1912 None => return Ok(None),
1913 };
1914
1915 if let Err(e) = self.add_should_refresh_vtxos(&mut participation).await {
1916 warn!("Error trying to add additional VTXOs that should be refreshed: {:#}", e);
1917 }
1918
1919 Ok(Some(self.participate_round(participation, Some(RoundMovement::Refresh)).await?))
1920 }
1921
1922 pub async fn refresh_vtxos_delegated<V: VtxoRef>(
1928 &self,
1929 vtxos: impl IntoIterator<Item = V>,
1930 ) -> anyhow::Result<Option<StoredRoundState<Unlocked>>> {
1931 let mut part = match self.build_refresh_participation(vtxos).await? {
1932 Some(participation) => participation,
1933 None => return Ok(None),
1934 };
1935
1936 if let Err(e) = self.add_should_refresh_vtxos(&mut part).await {
1937 warn!("Error trying to add additional VTXOs that should be refreshed: {:#}", e);
1938 }
1939
1940 Ok(Some(self.join_next_round_delegated(part, Some(RoundMovement::Refresh)).await?))
1941 }
1942
1943 pub async fn get_vtxos_to_refresh(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1946 let vtxos = self.spendable_vtxos_with(&RefreshStrategy::should_refresh_if_must(
1947 self,
1948 self.inner.chain.tip().await?,
1949 self.inner.chain.fee_rates().await.fast,
1950 )).await?;
1951 Ok(vtxos)
1952 }
1953
1954 pub async fn get_first_expiring_vtxo_blockheight(
1956 &self,
1957 ) -> anyhow::Result<Option<BlockHeight>> {
1958 Ok(self.spendable_vtxos().await?.iter().map(|v| v.expiry_height()).min())
1959 }
1960
1961 pub async fn get_next_required_refresh_blockheight(
1964 &self,
1965 ) -> anyhow::Result<Option<BlockHeight>> {
1966 let first_expiry = self.get_first_expiring_vtxo_blockheight().await?;
1967 Ok(first_expiry.map(|h| h.saturating_sub(self.inner.config.vtxo_refresh_expiry_threshold)))
1968 }
1969
1970 async fn select_vtxos_to_cover(
1976 &self,
1977 amount: Amount,
1978 ) -> anyhow::Result<Vec<WalletVtxo>> {
1979 let mut vtxos = self.spendable_vtxos().await?;
1980 self.sort_vtxos_for_selection(&mut vtxos);
1981
1982 let (last, _total_amount) = self.select_vtxos_inner(amount, &vtxos)?;
1983 vtxos.truncate(last+1);
1984 Ok(vtxos)
1985 }
1986
1987 async fn select_vtxos_to_cover_with_fee<F>(
1993 &self,
1994 amount: Amount,
1995 calc_fee: F,
1996 ) -> anyhow::Result<(Vec<WalletVtxo>, Amount)>
1997 where
1998 F: for<'a> Fn(
1999 Amount, std::iter::Copied<std::slice::Iter<'a, VtxoFeeInfo>>,
2000 ) -> anyhow::Result<Amount>,
2001 {
2002 let tip = self.inner.chain.tip().await?;
2003 let mut vtxos = self.spendable_vtxos().await?;
2004 self.sort_vtxos_for_selection(&mut vtxos);
2005
2006 let fee_info = vtxos.iter()
2007 .map(|v| VtxoFeeInfo::from_vtxo_and_tip(v, tip))
2008 .collect::<Vec<_>>();
2009
2010 const MAX_ITERATIONS: usize = 100;
2013 let mut fee = Amount::ZERO;
2014 for _ in 0..MAX_ITERATIONS {
2015 let required = amount.checked_add(fee)
2016 .context("Amount + fee overflow")?;
2017
2018 let (last, vtxo_amount) = self.select_vtxos_inner(required, &vtxos)
2019 .context("Could not find enough suitable VTXOs to cover payment + fees")?;
2020 fee = calc_fee(amount, fee_info[..=last].iter().copied())?;
2021
2022 if amount + fee <= vtxo_amount {
2023 trace!("Selected vtxos to cover amount + fee: amount = {}, fee = {}, total inputs = {}",
2024 amount, fee, vtxo_amount,
2025 );
2026 vtxos.truncate(last+1);
2027 return Ok((vtxos, fee));
2028 }
2029 trace!("VTXO sum of {} did not exceed amount {} and fee {}, iterating again",
2030 vtxo_amount, amount, fee,
2031 );
2032 }
2033 bail!("Fee calculation did not converge after maximum iterations")
2034 }
2035
2036 fn sort_vtxos_for_selection(&self, vtxos: &mut Vec<WalletVtxo>) {
2038 vtxos.sort_by_key(|v| v.expiry_height());
2039 }
2040
2041 fn select_vtxos_inner(
2047 &self,
2048 amount: Amount,
2049 vtxos: &Vec<WalletVtxo>,
2050 ) -> anyhow::Result<(usize, Amount)> {
2051 let mut total_amount = Amount::ZERO;
2053 for (i, vtxo) in vtxos.iter().enumerate() {
2054 total_amount += vtxo.amount();
2055
2056 if total_amount >= amount {
2057 return Ok((i, total_amount))
2058 }
2059 }
2060
2061 bail!("Insufficient money available. Needed {} but {} is available",
2062 amount, total_amount,
2063 );
2064 }
2065
2066 pub fn start_daemon(
2072 &self,
2073 onchain: Option<Arc<tokio::sync::RwLock<dyn DaemonizableOnchainWallet>>>,
2074 ) -> anyhow::Result<()> {
2075 let mut daemon = self.inner.daemon.lock();
2076 if daemon.is_some() {
2077 warn!("Called Wallet::start_daemon while daemon was already running.");
2078 return Ok(());
2079 }
2080
2081 let handle = crate::daemon::start_daemon(self.clone(), onchain);
2084 let _ = daemon.insert(handle);
2085
2086 Ok(())
2087 }
2088
2089 #[deprecated(since = "0.1.4", note = "use start_daemon instead")]
2091 pub fn run_daemon(
2092 &self,
2093 onchain: Option<Arc<tokio::sync::RwLock<dyn DaemonizableOnchainWallet>>>,
2094 ) -> anyhow::Result<()> {
2095 self.start_daemon(onchain)
2096 }
2097
2098 pub fn stop_daemon(&self) {
2100 let mut daemon = self.inner.daemon.lock();
2101 if let Some(handle) = daemon.take() {
2102 handle.stop();
2103 }
2104 }
2105
2106 pub async fn register_vtxo_transactions_with_server(
2110 &self,
2111 vtxos: &[impl AsRef<Vtxo<Full>>],
2112 ) -> anyhow::Result<()> {
2113 if vtxos.is_empty() {
2114 return Ok(());
2115 }
2116
2117 let (mut srv, _) = self.require_server().await?;
2118 srv.client.register_vtxo_transactions(protos::RegisterVtxoTransactionsRequest {
2119 vtxos: vtxos.iter().map(|v| v.as_ref().serialize()).collect(),
2120 }).await.context("failed to register vtxo transactions")?;
2121
2122 Ok(())
2123 }
2124}
2125
2126fn wrap_server_connect_error(err: ConnectError) -> anyhow::Error {
2127 match err {
2128 ConnectError::CreateEndpoint(CreateEndpointError::NoTransportBackend) => {
2129 anyhow!(MISSING_SERVER_TRANSPORT_HELP)
2130 },
2131 other => anyhow::Error::from(other),
2132 }
2133}
2134
2135impl std::ops::Drop for WalletInner {
2136 fn drop(&mut self) {
2137 if let Some(handle) = self.daemon.lock().take() {
2138 handle.stop();
2139 }
2140 }
2141}
2142
2143#[cfg(test)]
2144mod tests {
2145 use server_rpc::client::CreateEndpointError;
2146
2147 use super::{wrap_server_connect_error, MISSING_SERVER_TRANSPORT_HELP};
2148
2149 #[test]
2150 fn no_transport_connect_error_is_reworded_for_wallet_users() {
2151 let err = wrap_server_connect_error(CreateEndpointError::NoTransportBackend.into());
2152 assert!(err.to_string().contains(MISSING_SERVER_TRANSPORT_HELP));
2153 assert!(err.to_string().contains("feature `bark-wallet/native` or `bark-wallet/wasm-web`"));
2154 }
2155}