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};
356
357use ark::{ArkInfo, ProtocolEncoding, Vtxo, VtxoId, VtxoPolicy, VtxoRequest};
358use ark::address::VtxoDelivery;
359use ark::fees::{validate_and_subtract_fee_min_dust, VtxoFeeInfo};
360use ark::vtxo::{Full, PubkeyVtxoPolicy, VtxoRef};
361use ark::vtxo::policy::signing::VtxoSigner;
362use bitcoin_ext::{BlockHeight, P2TR_DUST};
363use server_rpc::{protos, ServerConnection};
364use server_rpc::client::{ConnectError, CreateEndpointError};
365use tokio_stream::StreamExt;
366use crate::chain::{ChainSource, ChainSourceSpec};
367use crate::exit::Exit;
368use crate::lock_manager::LockManager;
369use crate::movement::{Movement, MovementId, PaymentMethod};
370use crate::movement::manager::MovementManager;
371use crate::movement::update::MovementUpdate;
372use crate::notification::NotificationDispatch;
373use crate::onchain::{ExitUnilaterally, PreparePsbt, SignPsbt, Utxo};
374use crate::onchain::DaemonizableOnchainWallet;
375use crate::persist::BarkPersister;
376use crate::persist::models::{RoundStateId, StoredRoundState, Unlocked};
377#[cfg(feature = "socks5-proxy")]
378use crate::proxy::proxy_for_url;
379use crate::round::{RoundParticipation, RoundStatus};
380use crate::subsystem::{ArkoorMovement, RoundMovement};
381use crate::vtxo::{FilterVtxos, RefreshStrategy, VtxoFilter, VtxoStateKind};
382
383#[cfg(all(feature = "wasm-web", feature = "socks5-proxy"))]
384compile_error!("features `wasm-web` does not support feature `socks5-proxy");
385
386#[cfg(all(feature = "wasm-web", feature = "bitcoind-rpc"))]
387compile_error!("`wasm-web` does not support the `bitcoind-rpc` feature");
388
389const BARK_PURPOSE_INDEX: u32 = 350;
391const VTXO_KEYS_INDEX: u32 = 0;
393const MAILBOX_KEY_INDEX: u32 = 1;
395const RECOVERY_MAILBOX_KEY_INDEX: u32 = 2;
397const MISSING_SERVER_TRANSPORT_HELP: &str =
398 "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.";
399
400lazy_static::lazy_static! {
401 static ref SECP: secp256k1::Secp256k1<secp256k1::All> = secp256k1::Secp256k1::new();
403}
404
405fn log_server_pubkey_changed_error(expected: PublicKey, got: PublicKey) {
411 error!(
412 "
413Server public key has changed!
414
415The Ark server's public key is different from the one stored when this
416wallet was created. This typically happens when:
417
418 - The server operator has rotated their keys
419 - You are connecting to a different server
420 - The server has been replaced
421
422For safety, this wallet will not connect to the server until you
423resolve this. You can recover your funds on-chain by doing an emergency exit.
424
425This will exit your VTXOs to on-chain Bitcoin without needing the server's cooperation.
426
427Expected: {expected}
428Got: {got}")
429}
430
431fn log_server_mailbox_pubkey_changed_error(expected: PublicKey, got: PublicKey) {
433 error!(
434 "
435Server mailbox public key has changed!
436
437The Ark server's mailbox public key is different from the one stored when this
438wallet was created. This typically happens when:
439
440 - The server operator has rotated their keys
441 - You are connecting to a different server
442 - The server has been replaced
443
444For safety, this wallet will not connect to the server until you resolve this.
445
446Unlike a server pubkey change, your VTXOs are not at risk - the mailbox pubkey
447only affects address receive semantics. Any Ark addresses you previously
448shared will stop receiving new payments; you will need to share new addresses
449after reconnecting.
450
451Expected: {expected}
452Got: {got}")
453}
454
455#[derive(Debug, Clone)]
457pub struct LightningReceiveBalance {
458 pub total: Amount,
460 pub claimable: Amount,
462}
463
464#[derive(Debug, Clone)]
466pub struct Balance {
467 pub spendable: Amount,
469 pub pending_lightning_send: Amount,
471 pub claimable_lightning_receive: Amount,
473 pub pending_in_round: Amount,
475 pub pending_exit: Option<Amount>,
478 pub pending_board: Amount,
480}
481
482pub struct UtxoInfo {
483 pub outpoint: OutPoint,
484 pub amount: Amount,
485 pub confirmation_height: Option<u32>,
486}
487
488impl From<Utxo> for UtxoInfo {
489 fn from(value: Utxo) -> Self {
490 match value {
491 Utxo::Local(o) => UtxoInfo {
492 outpoint: o.outpoint,
493 amount: o.amount,
494 confirmation_height: o.confirmation_height,
495 },
496 Utxo::Exit(e) => UtxoInfo {
497 outpoint: e.vtxo.point(),
498 amount: e.vtxo.amount(),
499 confirmation_height: Some(e.height),
500 },
501 }
502 }
503}
504
505pub struct OffchainBalance {
508 pub available: Amount,
510 pub pending_in_round: Amount,
512 pub pending_exit: Amount,
515}
516
517#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
519pub struct WalletProperties {
520 pub network: Network,
524
525 pub fingerprint: Fingerprint,
529
530 pub server_pubkey: Option<PublicKey>,
537
538 pub server_mailbox_pubkey: Option<PublicKey>,
546}
547
548pub struct WalletSeed {
554 master: bip32::Xpriv,
555 vtxo: bip32::Xpriv,
556}
557
558impl WalletSeed {
559 fn new(network: Network, seed: &[u8; 64]) -> Self {
560 let bark_path = [ChildNumber::from_hardened_idx(BARK_PURPOSE_INDEX).unwrap()];
561 let master = bip32::Xpriv::new_master(network, seed)
562 .expect("invalid seed")
563 .derive_priv(&SECP, &bark_path)
564 .expect("purpose is valid");
565
566 let vtxo_path = [ChildNumber::from_hardened_idx(VTXO_KEYS_INDEX).unwrap()];
567 let vtxo = master.derive_priv(&SECP, &vtxo_path)
568 .expect("vtxo path is valid");
569
570 Self { master, vtxo }
571 }
572
573 fn fingerprint(&self) -> Fingerprint {
574 self.master.fingerprint(&SECP)
575 }
576
577 fn derive_vtxo_keypair(&self, idx: u32) -> Keypair {
578 self.vtxo.derive_priv(&SECP, &[idx.into()]).unwrap().to_keypair(&SECP)
579 }
580
581 fn to_mailbox_keypair(&self) -> Keypair {
582 let mailbox_path = [ChildNumber::from_hardened_idx(MAILBOX_KEY_INDEX).unwrap()];
583 self.master.derive_priv(&SECP, &mailbox_path).unwrap().to_keypair(&SECP)
584 }
585
586 fn to_recovery_mailbox_keypair(&self) -> Keypair {
587 let mailbox_path = [ChildNumber::from_hardened_idx(RECOVERY_MAILBOX_KEY_INDEX).unwrap()];
588 self.master.derive_priv(&SECP, &mailbox_path).unwrap().to_keypair(&SECP)
589 }
590}
591
592struct WalletInner {
593 chain: Arc<ChainSource>,
595
596 exit: Exit,
598
599 movements: Arc<MovementManager>,
601
602 notifications: NotificationDispatch,
604
605 config: Config,
607
608 db: Arc<dyn BarkPersister>,
610
611 lock_manager: Box<dyn LockManager>,
615
616 seed: WalletSeed,
618
619 server: tokio::sync::OnceCell<ServerConnection>,
626
627 daemon: parking_lot::Mutex<Option<DaemonHandle>>,
629}
630
631#[derive(Clone)]
772pub struct Wallet {
773 inner: Arc<WalletInner>,
774}
775
776impl Wallet {
777 pub async fn require_chainsource_version(&self) -> anyhow::Result<()> {
781 self.inner.chain.require_version().await
782 }
783
784 pub async fn network(&self) -> anyhow::Result<Network> {
785 Ok(self.properties().await?.network)
786 }
787
788 pub fn chain(&self) -> &Arc<ChainSource> {
790 &self.inner.chain
791 }
792
793 pub fn exit_mgr(&self) -> &Exit {
795 &self.inner.exit
796 }
797
798 pub fn movements_mgr(&self) -> &MovementManager {
800 &self.inner.movements
801 }
802
803 pub async fn peek_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
806 let last_revealed = self.inner.db.get_last_vtxo_key_index().await?;
807
808 let index = last_revealed.map(|i| i + 1).unwrap_or(u32::MIN);
809 let keypair = self.inner.seed.derive_vtxo_keypair(index);
810
811 Ok((keypair, index))
812 }
813
814 pub async fn derive_store_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
817 let (keypair, index) = self.peek_next_keypair().await?;
818 self.inner.db.store_vtxo_key(index, keypair.public_key()).await?;
819 Ok((keypair, index))
820 }
821
822 #[deprecated(note = "use peek_keypair instead")]
823 pub async fn peak_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
824 self.peek_keypair(index).await
825 }
826
827 pub async fn peek_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
841 let keypair = self.inner.seed.derive_vtxo_keypair(index);
842 if self.inner.db.get_public_key_idx(&keypair.public_key()).await?.is_some() {
843 Ok(keypair)
844 } else {
845 bail!("VTXO key {} does not exist, please derive it first", index)
846 }
847 }
848
849
850 pub async fn pubkey_keypair(&self, public_key: &PublicKey) -> anyhow::Result<Option<(u32, Keypair)>> {
862 if let Some(index) = self.inner.db.get_public_key_idx(&public_key).await? {
863 Ok(Some((index, self.inner.seed.derive_vtxo_keypair(index))))
864 } else {
865 Ok(None)
866 }
867 }
868
869 pub async fn get_vtxo_key(&self, vtxo: impl VtxoRef) -> anyhow::Result<Keypair> {
880 let bare_vtxo = match vtxo.as_bare_vtxo() {
881 Some(bare) => bare,
882 None => Cow::Owned(self.get_vtxo_by_id(vtxo.vtxo_id()).await?.vtxo),
883 };
884 let pubkey = self.find_signable_clause(&bare_vtxo).await
885 .context("VTXO is not signable by wallet")?
886 .pubkey();
887 let idx = self.inner.db.get_public_key_idx(&pubkey).await?
888 .context("VTXO key not found")?;
889 Ok(self.inner.seed.derive_vtxo_keypair(idx))
890 }
891
892 #[deprecated(note = "use peek_address instead")]
893 pub async fn peak_address(&self, index: u32) -> anyhow::Result<ark::Address> {
894 self.peek_address(index).await
895 }
896
897 pub async fn peek_address(&self, index: u32) -> anyhow::Result<ark::Address> {
901 let properties = self.properties().await?;
902 let network = properties.network;
903 let keypair = self.peek_keypair(index).await?;
904 let mailbox = self.mailbox_identifier();
905
906
907 let (server_pubkey, mailbox_pubkey) =
908 if let (Some(spk), Some(mpk)) = (properties.server_pubkey, properties.server_mailbox_pubkey) {
909 (spk, mpk)
910 } else {
911 let (_, ark_info) = self.require_server().await?;
912 (ark_info.server_pubkey, ark_info.mailbox_pubkey)
913 };
914
915 Ok(ark::Address::builder()
916 .testnet(network != bitcoin::Network::Bitcoin)
917 .server_pubkey(server_pubkey)
918 .pubkey_policy(keypair.public_key())
919 .mailbox(mailbox_pubkey, mailbox, &keypair)
920 .expect("Failed to assign mailbox")
921 .into_address().unwrap())
922 }
923
924 pub async fn new_address_with_index(&self) -> anyhow::Result<(ark::Address, u32)> {
928 let (_, index) = self.derive_store_next_keypair().await?;
929 let addr = self.peek_address(index).await?;
930 Ok((addr, index))
931 }
932
933 pub async fn new_address(&self) -> anyhow::Result<ark::Address> {
935 let (addr, _) = self.new_address_with_index().await?;
936 Ok(addr)
937 }
938
939 pub async fn create(
948 mnemonic: &Mnemonic,
949 network: Network,
950 config: Config,
951 db: Arc<dyn BarkPersister>,
952 lock_manager: Box<dyn LockManager>,
953 force: bool,
954 ) -> anyhow::Result<Wallet> {
955 trace!("Config: {:?}", config);
956
957 let wallet_fingerprint = WalletSeed::new(network, &mnemonic.to_seed("")).fingerprint();
958
959 let create_guard = lock_manager.lock(
964 &format!("{}.create", wallet_fingerprint),
965 Duration::from_secs(5),
966 ).await.context("wallet initialization already in progress")?;
967
968 if let Some(existing) = db.read_properties().await? {
969 trace!("Existing config: {:?}", existing);
970 bail!("cannot overwrite already existing config")
971 }
972
973 let (server_pubkey, mailbox_pubkey) = if !force {
975 match Self::connect_to_server(&config, network).await {
976 Ok(conn) => {
977 let ark_info = conn.ark_info().await;
978 (Some(ark_info.server_pubkey), Some(ark_info.mailbox_pubkey))
979 }
980 Err(err) => {
981 bail!("Failed to connect to provided server (if you are sure use the --force flag): {:#}", err);
982 }
983 }
984 } else {
985 (None, None)
986 };
987
988 let properties = WalletProperties {
989 network,
990 fingerprint: wallet_fingerprint,
991 server_pubkey,
992 server_mailbox_pubkey: mailbox_pubkey,
993 };
994
995 db.init_wallet(&properties).await.context("cannot init wallet in the database")?;
997 info!("Created wallet with fingerprint: {}", wallet_fingerprint);
998 if let Some(pk) = server_pubkey {
999 info!("Stored server pubkey: {}", pk);
1000 }
1001
1002 drop(create_guard);
1005
1006 let wallet = Wallet::open(&mnemonic, db, config, lock_manager).await.context("failed to open wallet")?;
1008 wallet.require_chainsource_version().await?;
1009
1010 Ok(wallet)
1011 }
1012
1013 pub async fn create_with_exits(
1020 mnemonic: &Mnemonic,
1021 network: Network,
1022 config: Config,
1023 db: Arc<dyn BarkPersister>,
1024 lock_manager: Box<dyn LockManager>,
1025 force: bool,
1026 ) -> anyhow::Result<Wallet> {
1027 let wallet = Wallet::create(mnemonic, network, config, db, lock_manager, force).await?;
1028 wallet.inner.exit.load().await?;
1029 Ok(wallet)
1030 }
1031
1032 pub async fn open(
1037 mnemonic: &Mnemonic,
1038 db: Arc<dyn BarkPersister>,
1039 config: Config,
1040 lock_manager: Box<dyn LockManager>,
1041 ) -> anyhow::Result<Wallet> {
1042 let properties = db.read_properties().await?.context("Wallet is not initialised")?;
1043
1044 let seed = {
1045 let seed = mnemonic.to_seed("");
1046 WalletSeed::new(properties.network, &seed)
1047 };
1048
1049 if properties.fingerprint != seed.fingerprint() {
1050 bail!("incorrect mnemonic")
1051 }
1052
1053 let chain_source = if let Some(ref url) = config.esplora_address {
1054 ChainSourceSpec::Esplora {
1055 url: url.clone(),
1056 }
1057 } else if let Some(ref url) = config.bitcoind_address {
1058 let auth = if let Some(ref c) = config.bitcoind_cookiefile {
1059 bitcoin_ext::rpc::Auth::CookieFile(c.clone())
1060 } else {
1061 bitcoin_ext::rpc::Auth::UserPass(
1062 config.bitcoind_user.clone().context("need bitcoind auth config")?,
1063 config.bitcoind_pass.clone().context("need bitcoind auth config")?,
1064 )
1065 };
1066 ChainSourceSpec::Bitcoind { url: url.clone(), auth }
1067 } else {
1068 bail!("Need to either provide esplora or bitcoind info");
1069 };
1070
1071 #[cfg(feature = "socks5-proxy")]
1072 let chain_proxy = proxy_for_url(&config.socks5_proxy, chain_source.url())?;
1073 let chain_source_client = ChainSource::new(
1074 chain_source, properties.network, config.fallback_fee_rate,
1075 #[cfg(feature = "socks5-proxy")] chain_proxy.as_deref(),
1076 ).await?;
1077 let chain = Arc::new(chain_source_client);
1078
1079 let server = tokio::sync::OnceCell::new();
1080
1081 let notifications = NotificationDispatch::new();
1082 let movements = Arc::new(MovementManager::new(db.clone(), notifications.clone()));
1083 let exit = Exit::new(db.clone(), chain.clone(), movements.clone()).await?;
1084
1085 Ok(Wallet { inner: Arc::new(WalletInner {
1086 config, db, lock_manager, seed, exit, movements, notifications, server, chain,
1087 daemon: parking_lot::Mutex::new(None),
1088 })})
1089 }
1090
1091 pub async fn open_with_exits(
1095 mnemonic: &Mnemonic,
1096 db: Arc<dyn BarkPersister>,
1097 cfg: Config,
1098 lock_manager: Box<dyn LockManager>,
1099 ) -> anyhow::Result<Wallet> {
1100 let wallet = Wallet::open(mnemonic, db, cfg, lock_manager).await?;
1101 wallet.inner.exit.load().await?;
1102 Ok(wallet)
1103 }
1104
1105 pub async fn open_with_daemon(
1108 mnemonic: &Mnemonic,
1109 db: Arc<dyn BarkPersister>,
1110 cfg: Config,
1111 onchain: Option<Arc<tokio::sync::RwLock<dyn DaemonizableOnchainWallet>>>,
1112 lock_manager: Box<dyn LockManager>,
1113 ) -> anyhow::Result<Wallet> {
1114 let wallet = Wallet::open(mnemonic, db, cfg, lock_manager).await?;
1115 if onchain.is_some() {
1116 wallet.inner.exit.load().await?;
1117 }
1118
1119 wallet.start_daemon(onchain)?;
1120
1121 Ok(wallet)
1122 }
1123
1124 pub fn config(&self) -> &Config {
1126 &self.inner.config
1127 }
1128
1129 pub async fn properties(&self) -> anyhow::Result<WalletProperties> {
1131 let properties = self.inner.db.read_properties().await?.context("Wallet is not initialised")?;
1132 Ok(properties)
1133 }
1134
1135 pub fn fingerprint(&self) -> Fingerprint {
1137 self.inner.seed.fingerprint()
1138 }
1139
1140 async fn connect_to_server(
1141 config: &Config,
1142 network: Network,
1143 ) -> anyhow::Result<ServerConnection> {
1144 let server_address = crate::utils::url_with_default_https_scheme(&config.server_address);
1145 let mut builder = ServerConnection::builder()
1146 .address(&server_address)
1147 .network(network);
1148
1149 #[cfg(feature = "socks5-proxy")]
1150 if let Some(proxy) = proxy_for_url(&config.socks5_proxy, &server_address)? {
1151 builder = builder.proxy(&proxy)
1152 }
1153
1154 if let Some(ref token) = config.server_access_token {
1155 builder = builder.access_token(token);
1156 }
1157
1158 builder.connect().await.map_err(wrap_server_connect_error)
1159 .context("Failed to connect to Ark server")
1160 }
1161
1162 async fn require_server(&self) -> anyhow::Result<(ServerConnection, ArkInfo)> {
1163 let conn = self.inner.server.get_or_try_init(|| async {
1167 let network = self.properties().await?.network;
1168 Self::connect_to_server(&self.inner.config, network).await
1169 .context("You should be connected to Ark server to perform this action")
1170 }).await?.clone();
1171
1172 let ark_info = conn.ark_info().await;
1173 self.check_and_store_server_keys(&ark_info).await?;
1174
1175 Ok((conn, ark_info))
1176 }
1177
1178 pub async fn refresh_server(&self) -> anyhow::Result<()> {
1179 let srv = self.inner.server.get_or_try_init(|| async {
1185 let properties = self.properties().await?;
1186 Self::connect_to_server(&self.inner.config, properties.network).await
1187 .map_err(anyhow::Error::from)
1188 }).await?;
1189
1190 srv.check_connection().await?;
1191 let ark_info = srv.ark_info().await;
1192 ark_info.fees.validate().context("invalid fee schedule")?;
1193 self.check_and_store_server_keys(&ark_info).await?;
1194
1195 Ok(())
1196 }
1197
1198 async fn check_and_store_server_keys(&self, ark_info: &ArkInfo) -> anyhow::Result<()> {
1205 let properties = self.properties().await?;
1206
1207 if let Some(stored_pubkey) = properties.server_pubkey {
1208 if stored_pubkey != ark_info.server_pubkey {
1209 log_server_pubkey_changed_error(stored_pubkey, ark_info.server_pubkey);
1210 bail!("Server public key has changed. You should exit all your VTXOs!");
1211 }
1212 } else {
1213 self.inner.db.set_server_pubkey(ark_info.server_pubkey).await?;
1214 info!("Stored server pubkey for existing wallet: {}", ark_info.server_pubkey);
1215 }
1216
1217 if let Some(stored_mailbox_pubkey) = properties.server_mailbox_pubkey {
1218 if stored_mailbox_pubkey != ark_info.mailbox_pubkey {
1219 log_server_mailbox_pubkey_changed_error(stored_mailbox_pubkey, ark_info.mailbox_pubkey);
1220 bail!("Server mailbox public key has changed.");
1221 }
1222 } else {
1223 self.inner.db.set_server_mailbox_pubkey(ark_info.mailbox_pubkey).await?;
1224 info!("Stored server mailbox pubkey for existing wallet: {}", ark_info.mailbox_pubkey);
1225 }
1226
1227 Ok(())
1228 }
1229
1230 pub async fn ark_info(&self) -> anyhow::Result<Option<ArkInfo>> {
1232 match self.inner.server.get() {
1233 Some(srv) => Ok(Some(srv.ark_info().await)),
1234 None => Ok(None),
1235 }
1236 }
1237
1238 pub async fn require_ark_info(&self) -> anyhow::Result<ArkInfo> {
1244 let (_, ark_info) = self.require_server().await?;
1245 Ok(ark_info)
1246 }
1247
1248 pub async fn balance(&self) -> anyhow::Result<Balance> {
1252 let vtxos = self.vtxos().await?;
1253
1254 let spendable = {
1255 let mut v = vtxos.iter().collect();
1256 VtxoStateKind::Spendable.filter_vtxos(&mut v).await?;
1257 v.into_iter().map(|v| v.amount()).sum::<Amount>()
1258 };
1259
1260 let pending_lightning_send = self.pending_lightning_send_vtxos().await?.iter()
1261 .map(|v| v.amount())
1262 .sum::<Amount>();
1263
1264 let claimable_lightning_receive = self.claimable_lightning_receive_balance().await?;
1265
1266 let pending_board = self.pending_board_vtxos().await?.iter()
1267 .map(|v| v.amount())
1268 .sum::<Amount>();
1269
1270 let pending_in_round = self.pending_round_balance().await?;
1271
1272 let pending_exit = self.exit_mgr().try_pending_total();
1273
1274 Ok(Balance {
1275 spendable,
1276 pending_in_round,
1277 pending_lightning_send,
1278 claimable_lightning_receive,
1279 pending_exit,
1280 pending_board,
1281 })
1282 }
1283
1284 pub async fn validate_vtxo(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<()> {
1286 let tx = self.inner.chain.get_tx(&vtxo.chain_anchor().txid).await
1287 .context("could not fetch chain tx")?;
1288
1289 let tx = tx.with_context(|| {
1290 format!("vtxo chain anchor not found for vtxo: {}", vtxo.chain_anchor().txid)
1291 })?;
1292
1293 vtxo.validate(&tx)?;
1294
1295 Ok(())
1296 }
1297
1298 pub async fn import_vtxo(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<()> {
1308 if self.inner.db.get_wallet_vtxo(vtxo.id()).await?.is_some() {
1309 info!("VTXO {} already exists in wallet, skipping import", vtxo.id());
1310 return Ok(());
1311 }
1312
1313 self.validate_vtxo(vtxo).await.context("VTXO validation failed")?;
1314
1315 if self.find_signable_clause(vtxo).await.is_none() {
1316 bail!("VTXO {} is not owned by this wallet (no signable clause found)", vtxo.id());
1317 }
1318
1319 let current_height = self.inner.chain.tip().await?;
1320 if vtxo.expiry_height() <= current_height {
1321 bail!("Vtxo {} has expired", vtxo.id());
1322 }
1323
1324 self.store_spendable_vtxos([vtxo]).await.context("failed to store imported VTXO")?;
1325
1326 info!("Successfully imported VTXO {}", vtxo.id());
1327 Ok(())
1328 }
1329
1330 pub async fn get_vtxo_by_id(&self, vtxo_id: VtxoId) -> anyhow::Result<WalletVtxo> {
1332 let vtxo = self.inner.db.get_wallet_vtxo(vtxo_id).await
1333 .with_context(|| format!("Error when querying vtxo {} in database", vtxo_id))?
1334 .with_context(|| format!("The VTXO with id {} cannot be found", vtxo_id))?;
1335 Ok(vtxo)
1336 }
1337
1338 pub async fn get_full_vtxo(&self, vtxo_id: VtxoId) -> anyhow::Result<Vtxo<Full>> {
1346 self.inner.db.get_full_vtxo(vtxo_id).await
1347 .with_context(|| format!("Error when querying full vtxo {} in database", vtxo_id))?
1348 .with_context(|| format!("The VTXO with id {} cannot be found", vtxo_id))
1349 }
1350
1351 pub async fn get_full_vtxos<V: VtxoRef>(
1353 &self,
1354 vtxos: impl IntoIterator<Item = V>,
1355 ) -> anyhow::Result<Vec<Vtxo<Full>>> {
1356 let ids = vtxos.into_iter().map(|v| v.vtxo_id()).collect::<Vec<_>>();
1357 self.inner.db.get_full_vtxos(&ids).await
1358 .with_context(||
1359 format!("Error when querying full vtxos in database with IDs: {:?}", ids)
1360 )
1361 }
1362
1363 #[deprecated(since="0.1.0-beta.5", note = "Use Wallet::history instead")]
1365 pub async fn movements(&self) -> anyhow::Result<Vec<Movement>> {
1366 self.history().await
1367 }
1368
1369 pub async fn history(&self) -> anyhow::Result<Vec<Movement>> {
1371 Ok(self.inner.db.get_all_movements().await?)
1372 }
1373
1374 pub async fn update_history_metadata(
1394 &self,
1395 movement_id: MovementId,
1396 patch: &serde_json::Value,
1397 ) -> anyhow::Result<()> {
1398 self.inner.movements.patch_metadata(movement_id, patch).await?;
1399 Ok(())
1400 }
1401
1402 pub async fn history_by_payment_method(
1404 &self,
1405 payment_method: &PaymentMethod,
1406 ) -> anyhow::Result<Vec<Movement>> {
1407 let mut ret = self.inner.db.get_movements_by_payment_method(payment_method).await?;
1408 ret.sort_by_key(|m| m.id);
1409 Ok(ret)
1410 }
1411
1412 pub async fn all_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1414 Ok(self.inner.db.get_all_vtxos().await?)
1415 }
1416
1417 pub async fn vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1419 Ok(self.inner.db.get_vtxos_by_state(&VtxoStateKind::UNSPENT_STATES).await?)
1420 }
1421
1422 pub async fn vtxos_with(&self, filter: &impl FilterVtxos) -> anyhow::Result<Vec<WalletVtxo>> {
1424 let mut vtxos = self.vtxos().await?;
1425 filter.filter_vtxos(&mut vtxos).await.context("error filtering vtxos")?;
1426 Ok(vtxos)
1427 }
1428
1429 pub async fn spendable_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1431 Ok(self.vtxos_with(&VtxoStateKind::Spendable).await?)
1432 }
1433
1434 pub async fn spendable_vtxos_with(
1436 &self,
1437 filter: &impl FilterVtxos,
1438 ) -> anyhow::Result<Vec<WalletVtxo>> {
1439 let mut vtxos = self.spendable_vtxos().await?;
1440 filter.filter_vtxos(&mut vtxos).await.context("error filtering vtxos")?;
1441 Ok(vtxos)
1442 }
1443
1444 pub async fn get_expiring_vtxos(
1446 &self,
1447 threshold: BlockHeight,
1448 ) -> anyhow::Result<Vec<WalletVtxo>> {
1449 let expiry = self.inner.chain.tip().await? + threshold;
1450 let filter = VtxoFilter::new(&self).expires_before(expiry);
1451 Ok(self.spendable_vtxos_with(&filter).await?)
1452 }
1453
1454 pub async fn maintenance(&self) -> anyhow::Result<()> {
1460 info!("Starting wallet maintenance in interactive mode");
1461 self.sync().await;
1462
1463 let rounds = self.progress_pending_rounds(None).await;
1465 if let Err(e) = rounds.as_ref() {
1466 warn!("Error progressing pending rounds: {:#}", e);
1467 }
1468
1469 let states = self.inner.db.get_pending_round_state_ids().await?;
1471 for id in states {
1472 debug!("Cancelling pending round participation {}", id);
1473 let mut state = match self.lock_wait_round_state(id).await {
1474 Ok(Some(s)) => s,
1475 Ok(None) => continue, Err(e) => {
1477 warn!("Failed to lock round state with id {}: {:#}", id, e);
1478 continue;
1479 }
1480 };
1481 if let Err(e) = state.state_mut().try_cancel(self).await {
1482 warn!("Error cancelling pending round: {:#}", e);
1483 }
1484 }
1485
1486 let refresh = self.maintenance_refresh().await;
1488 if let Err(e) = refresh.as_ref() {
1489 warn!("Error refreshing VTXOs: {:#}", e);
1490 }
1491 if rounds.is_err() || refresh.is_err() {
1492 bail!("Maintenance encountered errors.\nprogress_rounds: {:#?}\nrefresh: {:#?}", rounds, refresh);
1493 }
1494 Ok(())
1495 }
1496
1497 pub async fn maintenance_delegated(&self) -> anyhow::Result<()> {
1503 info!("Starting wallet maintenance in delegated mode");
1504 self.sync().await;
1505 let rounds = self.progress_pending_rounds(None).await;
1506 if let Err(e) = rounds.as_ref() {
1507 warn!("Error progressing pending rounds: {:#}", e);
1508 }
1509 let refresh = self.maybe_schedule_maintenance_refresh_delegated().await;
1510 if let Err(e) = refresh.as_ref() {
1511 warn!("Error refreshing VTXOs: {:#}", e);
1512 }
1513 if rounds.is_err() || refresh.is_err() {
1514 bail!("Delegated maintenance encountered errors.\nprogress_rounds: {:#?}\nrefresh: {:#?}", rounds, refresh);
1515 }
1516 Ok(())
1517 }
1518
1519 pub async fn maintenance_with_onchain<W: PreparePsbt + SignPsbt + ExitUnilaterally>(
1527 &self,
1528 onchain: &mut W,
1529 ) -> anyhow::Result<()> {
1530 info!("Starting wallet maintenance in interactive mode with onchain wallet");
1531
1532 let maintenance = self.maintenance().await;
1534
1535 let exit_sync = self.sync_exits().await;
1537 if let Err(e) = exit_sync.as_ref() {
1538 warn!("Error syncing exits: {:#}", e);
1539 }
1540 let exit_progress = self.exit_mgr().progress_exits_with_bdk(self, onchain, None).await;
1541 if let Err(e) = exit_progress.as_ref() {
1542 warn!("Error progressing exits: {:#}", e);
1543 }
1544 if maintenance.is_err() || exit_sync.is_err() || exit_progress.is_err() {
1545 bail!("Maintenance encountered errors.\nmaintenance: {:#?}\nexit_sync: {:#?}\nexit_progress: {:#?}", maintenance, exit_sync, exit_progress);
1546 }
1547 Ok(())
1548 }
1549
1550 pub async fn maintenance_with_onchain_delegated<W: PreparePsbt + SignPsbt + ExitUnilaterally>(
1557 &self,
1558 onchain: &mut W,
1559 ) -> anyhow::Result<()> {
1560 info!("Starting wallet maintenance in delegated mode with onchain wallet");
1561
1562 let maintenance = self.maintenance_delegated().await;
1564
1565 let exit_sync = self.sync_exits().await;
1567 if let Err(e) = exit_sync.as_ref() {
1568 warn!("Error syncing exits: {:#}", e);
1569 }
1570 let exit_progress = self.exit_mgr().progress_exits_with_bdk(self, onchain, None).await;
1571 if let Err(e) = exit_progress.as_ref() {
1572 warn!("Error progressing exits: {:#}", e);
1573 }
1574 if maintenance.is_err() || exit_sync.is_err() || exit_progress.is_err() {
1575 bail!("Delegated maintenance encountered errors.\nmaintenance: {:#?}\nexit_sync: {:#?}\nexit_progress: {:#?}", maintenance, exit_sync, exit_progress);
1576 }
1577 Ok(())
1578 }
1579
1580 pub async fn maybe_schedule_maintenance_refresh(&self) -> anyhow::Result<Option<RoundStateId>> {
1588 let vtxos = self.get_vtxos_to_refresh().await?;
1589 if vtxos.len() == 0 {
1590 return Ok(None);
1591 }
1592
1593 let participation = match self.build_refresh_participation(vtxos).await? {
1594 Some(participation) => participation,
1595 None => return Ok(None),
1596 };
1597
1598 info!("Scheduling maintenance refresh ({} vtxos)", participation.inputs.len());
1599 let state = self.join_next_round(participation, Some(RoundMovement::Refresh)).await?;
1600 Ok(Some(state.id()))
1601 }
1602
1603 pub async fn maybe_schedule_maintenance_refresh_delegated(
1611 &self,
1612 ) -> anyhow::Result<Option<RoundStateId>> {
1613 let vtxos = self.get_vtxos_to_refresh().await?;
1614 if vtxos.len() == 0 {
1615 return Ok(None);
1616 }
1617
1618 let participation = match self.build_refresh_participation(vtxos).await? {
1619 Some(participation) => participation,
1620 None => return Ok(None),
1621 };
1622
1623 info!("Scheduling delegated maintenance refresh ({} vtxos)", participation.inputs.len());
1624 let state = self.join_next_round_delegated(participation, Some(RoundMovement::Refresh)).await?;
1625 Ok(Some(state.id()))
1626 }
1627
1628 pub async fn maintenance_refresh(&self) -> anyhow::Result<Option<RoundStatus>> {
1636 let vtxos = self.get_vtxos_to_refresh().await?;
1637 if vtxos.len() == 0 {
1638 return Ok(None);
1639 }
1640
1641 info!("Waiting for round to perform maintenance refresh...");
1642 let mut events = self.subscribe_round_events().await?;
1643 while let Some(event) = events.next().await {
1644 match event {
1645 Ok(RoundEvent::Attempt(a)) if a.attempt_seq == 0 => {
1646 let vtxos = self.get_vtxos_to_refresh().await?;
1647 if vtxos.len() == 0 {
1648 return Ok(None);
1649 }
1650 debug!("Round {} started, triggering refresh", a.round_seq);
1651 return self.refresh_vtxos(vtxos).await;
1652 },
1653 _ => {},
1654 }
1655 }
1656 Ok(None)
1657 }
1658
1659 pub async fn sync(&self) {
1666 futures::join!(
1667 async {
1668 if let Err(e) = self.inner.chain.update_fee_rates(self.inner.config.fallback_fee_rate).await {
1671 warn!("Error updating fee rates: {:#}", e);
1672 }
1673 },
1674 async {
1675 if let Err(e) = self.sync_mailbox().await {
1676 warn!("Error in mailbox sync: {:#}", e);
1677 }
1678 },
1679 async {
1680 if let Err(e) = self.sync_pending_rounds().await {
1681 warn!("Error while trying to progress rounds awaiting confirmations: {:#}", e);
1682 }
1683 },
1684 async {
1685 if let Err(e) = self.sync_pending_lightning_send_vtxos().await {
1686 warn!("Error syncing pending lightning payments: {:#}", e);
1687 }
1688 },
1689 async {
1690 if let Err(e) = self.try_claim_all_lightning_receives(false).await {
1691 warn!("Error claiming pending lightning receives: {:#}", e);
1692 }
1693 },
1694 async {
1695 if let Err(e) = self.sync_pending_boards().await {
1696 warn!("Error syncing pending boards: {:#}", e);
1697 }
1698 },
1699 async {
1700 if let Err(e) = self.sync_pending_offboards().await {
1701 warn!("Error syncing pending offboards: {:#}", e);
1702 }
1703 }
1704 );
1705 }
1706
1707 pub async fn sync_exits(&self) -> anyhow::Result<()> {
1713 self.exit_mgr().sync(&self).await?;
1714 Ok(())
1715 }
1716
1717 pub async fn dangerous_drop_vtxo(&self, vtxo_id: VtxoId) -> anyhow::Result<()> {
1720 warn!("Drop vtxo {} from the database", vtxo_id);
1721 self.inner.db.remove_vtxo(vtxo_id).await?;
1722 Ok(())
1723 }
1724
1725 pub async fn dangerous_drop_all_vtxos(&self) -> anyhow::Result<()> {
1728 warn!("Dropping all vtxos from the db...");
1729 for vtxo in self.vtxos().await? {
1730 self.inner.db.remove_vtxo(vtxo.id()).await?;
1731 }
1732
1733 self.exit_mgr().dangerous_clear_exit().await?;
1734 Ok(())
1735 }
1736
1737 async fn has_counterparty_risk(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<bool> {
1745 for past_pks in vtxo.past_arkoor_pubkeys() {
1746 let mut owns_any = false;
1747 for past_pk in past_pks {
1748 if self.inner.db.get_public_key_idx(&past_pk).await?.is_some() {
1749 owns_any = true;
1750 break;
1751 }
1752 }
1753 if !owns_any {
1754 return Ok(true);
1755 }
1756 }
1757
1758 let my_clause = self.find_signable_clause(vtxo).await;
1759 Ok(!my_clause.is_some())
1760 }
1761
1762 async fn add_should_refresh_vtxos(
1768 &self,
1769 participation: &mut RoundParticipation,
1770 ) -> anyhow::Result<()> {
1771 let tip = self.inner.chain.tip().await?;
1774 let mut vtxos_to_refresh = self.spendable_vtxos_with(
1775 &RefreshStrategy::should_refresh(self, tip, self.inner.chain.fee_rates().await.fast),
1776 ).await?;
1777 if vtxos_to_refresh.is_empty() {
1778 return Ok(());
1779 }
1780
1781 let excluded_ids = participation.inputs.iter().map(|v| v.vtxo_id())
1782 .collect::<HashSet<_>>();
1783 let mut total_amount = Amount::ZERO;
1784 for i in (0..vtxos_to_refresh.len()).rev() {
1785 let vtxo = &vtxos_to_refresh[i];
1786 if excluded_ids.contains(&vtxo.id()) {
1787 vtxos_to_refresh.swap_remove(i);
1788 continue;
1789 }
1790 total_amount += vtxo.amount();
1791 }
1792 if vtxos_to_refresh.is_empty() {
1793 return Ok(());
1795 }
1796
1797 let (_, ark_info) = self.require_server().await?;
1800 let fee = ark_info.fees.refresh.calculate_no_base_fee(
1801 vtxos_to_refresh.iter().map(|wv| VtxoFeeInfo::from_vtxo_and_tip(&wv.vtxo, tip)),
1802 ).context("fee overflowed")?;
1803
1804 let output_amount = match validate_and_subtract_fee_min_dust(total_amount, fee) {
1806 Ok(amount) => amount,
1807 Err(e) => {
1808 trace!("Cannot add should-refresh VTXOs: {}", e);
1809 return Ok(());
1810 },
1811 };
1812 info!(
1813 "Adding {} extra VTXOs to round participation total = {}, fee = {}, output = {}",
1814 vtxos_to_refresh.len(), total_amount, fee, output_amount,
1815 );
1816 let (user_keypair, _) = self.derive_store_next_keypair().await?;
1817 let req = VtxoRequest {
1818 policy: VtxoPolicy::new_pubkey(user_keypair.public_key()),
1819 amount: output_amount,
1820 };
1821 let extra_ids = vtxos_to_refresh.into_iter().map(|wv| wv.id()).collect::<Vec<_>>();
1822 let extra_full = self.inner.db.get_full_vtxos(&extra_ids).await
1823 .context("failed to hydrate refresh candidates")?;
1824 participation.inputs.reserve(extra_full.len());
1825 participation.inputs.extend(extra_full);
1826 participation.outputs.push(req);
1827
1828 Ok(())
1829 }
1830
1831 pub async fn build_refresh_participation<V: VtxoRef>(
1832 &self,
1833 vtxos: impl IntoIterator<Item = V>,
1834 ) -> anyhow::Result<Option<RoundParticipation>> {
1835 let (vtxos, total_amount) = {
1836 let iter = vtxos.into_iter();
1837 let size_hint = iter.size_hint();
1838 let mut vtxos = Vec::<Vtxo<Full>>::with_capacity(size_hint.1.unwrap_or(size_hint.0));
1839 let mut amount = Amount::ZERO;
1840 for vref in iter {
1841 let id = vref.vtxo_id();
1846 if vtxos.iter().any(|v| v.id() == id) {
1847 bail!("duplicate VTXO id: {}", id);
1848 }
1849 let vtxo = if let Some(vtxo) = vref.into_full_vtxo() {
1850 vtxo
1851 } else {
1852 self.inner.db.get_full_vtxo(id).await?
1855 .with_context(|| format!("vtxo with id {} not found", id))?
1856 };
1857 amount += vtxo.amount();
1858 vtxos.push(vtxo);
1859 }
1860 (vtxos, amount)
1861 };
1862
1863 if vtxos.is_empty() {
1864 info!("Skipping refresh since no VTXOs are provided.");
1865 return Ok(None);
1866 }
1867 ensure!(total_amount >= P2TR_DUST,
1868 "vtxo amount must be at least {} to participate in a round",
1869 P2TR_DUST,
1870 );
1871
1872 let (_, ark_info) = self.require_server().await?;
1874 let current_height = self.inner.chain.tip().await?;
1875 let vtxo_fee_infos = vtxos.iter()
1876 .map(|v| VtxoFeeInfo::from_vtxo_and_tip(v, current_height));
1877 let fee = ark_info.fees.refresh.calculate(vtxo_fee_infos).context("fee overflowed")?;
1878 let output_amount = validate_and_subtract_fee_min_dust(total_amount, fee)?;
1879
1880 info!("Refreshing {} VTXOs (total amount = {}, fee = {}, output = {}).",
1881 vtxos.len(), total_amount, fee, output_amount,
1882 );
1883 let (user_keypair, _) = self.derive_store_next_keypair().await?;
1884 let req = VtxoRequest {
1885 policy: VtxoPolicy::Pubkey(PubkeyVtxoPolicy { user_pubkey: user_keypair.public_key() }),
1886 amount: output_amount,
1887 };
1888
1889 Ok(Some(RoundParticipation {
1890 inputs: vtxos,
1891 outputs: vec![req],
1892 unblinded_mailbox_id: None,
1893 }))
1894 }
1895
1896 pub async fn refresh_vtxos<V: VtxoRef>(
1901 &self,
1902 vtxos: impl IntoIterator<Item = V>,
1903 ) -> anyhow::Result<Option<RoundStatus>> {
1904 let mut participation = match self.build_refresh_participation(vtxos).await? {
1905 Some(participation) => participation,
1906 None => return Ok(None),
1907 };
1908
1909 if let Err(e) = self.add_should_refresh_vtxos(&mut participation).await {
1910 warn!("Error trying to add additional VTXOs that should be refreshed: {:#}", e);
1911 }
1912
1913 Ok(Some(self.participate_round(participation, Some(RoundMovement::Refresh)).await?))
1914 }
1915
1916 pub async fn refresh_vtxos_delegated<V: VtxoRef>(
1922 &self,
1923 vtxos: impl IntoIterator<Item = V>,
1924 ) -> anyhow::Result<Option<StoredRoundState<Unlocked>>> {
1925 let mut part = match self.build_refresh_participation(vtxos).await? {
1926 Some(participation) => participation,
1927 None => return Ok(None),
1928 };
1929
1930 if let Err(e) = self.add_should_refresh_vtxos(&mut part).await {
1931 warn!("Error trying to add additional VTXOs that should be refreshed: {:#}", e);
1932 }
1933
1934 Ok(Some(self.join_next_round_delegated(part, Some(RoundMovement::Refresh)).await?))
1935 }
1936
1937 pub async fn get_vtxos_to_refresh(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1940 let vtxos = self.spendable_vtxos_with(&RefreshStrategy::should_refresh_if_must(
1941 self,
1942 self.inner.chain.tip().await?,
1943 self.inner.chain.fee_rates().await.fast,
1944 )).await?;
1945 Ok(vtxos)
1946 }
1947
1948 pub async fn get_first_expiring_vtxo_blockheight(
1950 &self,
1951 ) -> anyhow::Result<Option<BlockHeight>> {
1952 Ok(self.spendable_vtxos().await?.iter().map(|v| v.expiry_height()).min())
1953 }
1954
1955 pub async fn get_next_required_refresh_blockheight(
1958 &self,
1959 ) -> anyhow::Result<Option<BlockHeight>> {
1960 let first_expiry = self.get_first_expiring_vtxo_blockheight().await?;
1961 Ok(first_expiry.map(|h| h.saturating_sub(self.inner.config.vtxo_refresh_expiry_threshold)))
1962 }
1963
1964 async fn select_vtxos_to_cover(
1970 &self,
1971 amount: Amount,
1972 ) -> anyhow::Result<Vec<WalletVtxo>> {
1973 let mut vtxos = self.spendable_vtxos().await?;
1974 self.sort_vtxos_for_selection(&mut vtxos);
1975
1976 let (last, _total_amount) = self.select_vtxos_inner(amount, &vtxos)?;
1977 vtxos.truncate(last+1);
1978 Ok(vtxos)
1979 }
1980
1981 async fn select_vtxos_to_cover_with_fee<F>(
1987 &self,
1988 amount: Amount,
1989 calc_fee: F,
1990 ) -> anyhow::Result<(Vec<WalletVtxo>, Amount)>
1991 where
1992 F: for<'a> Fn(
1993 Amount, std::iter::Copied<std::slice::Iter<'a, VtxoFeeInfo>>,
1994 ) -> anyhow::Result<Amount>,
1995 {
1996 let tip = self.inner.chain.tip().await?;
1997 let mut vtxos = self.spendable_vtxos().await?;
1998 self.sort_vtxos_for_selection(&mut vtxos);
1999
2000 let fee_info = vtxos.iter()
2001 .map(|v| VtxoFeeInfo::from_vtxo_and_tip(v, tip))
2002 .collect::<Vec<_>>();
2003
2004 const MAX_ITERATIONS: usize = 100;
2007 let mut fee = Amount::ZERO;
2008 for _ in 0..MAX_ITERATIONS {
2009 let required = amount.checked_add(fee)
2010 .context("Amount + fee overflow")?;
2011
2012 let (last, vtxo_amount) = self.select_vtxos_inner(required, &vtxos)
2013 .context("Could not find enough suitable VTXOs to cover payment + fees")?;
2014 fee = calc_fee(amount, fee_info[..=last].iter().copied())?;
2015
2016 if amount + fee <= vtxo_amount {
2017 trace!("Selected vtxos to cover amount + fee: amount = {}, fee = {}, total inputs = {}",
2018 amount, fee, vtxo_amount,
2019 );
2020 vtxos.truncate(last+1);
2021 return Ok((vtxos, fee));
2022 }
2023 trace!("VTXO sum of {} did not exceed amount {} and fee {}, iterating again",
2024 vtxo_amount, amount, fee,
2025 );
2026 }
2027 bail!("Fee calculation did not converge after maximum iterations")
2028 }
2029
2030 fn sort_vtxos_for_selection(&self, vtxos: &mut Vec<WalletVtxo>) {
2032 vtxos.sort_by_key(|v| v.expiry_height());
2033 }
2034
2035 fn select_vtxos_inner(
2041 &self,
2042 amount: Amount,
2043 vtxos: &Vec<WalletVtxo>,
2044 ) -> anyhow::Result<(usize, Amount)> {
2045 let mut total_amount = Amount::ZERO;
2047 for (i, vtxo) in vtxos.iter().enumerate() {
2048 total_amount += vtxo.amount();
2049
2050 if total_amount >= amount {
2051 return Ok((i, total_amount))
2052 }
2053 }
2054
2055 bail!("Insufficient money available. Needed {} but {} is available",
2056 amount, total_amount,
2057 );
2058 }
2059
2060 pub fn start_daemon(
2066 &self,
2067 onchain: Option<Arc<tokio::sync::RwLock<dyn DaemonizableOnchainWallet>>>,
2068 ) -> anyhow::Result<()> {
2069 let mut daemon = self.inner.daemon.lock();
2070 if daemon.is_some() {
2071 warn!("Called Wallet::start_daemon while daemon was already running.");
2072 return Ok(());
2073 }
2074
2075 let handle = crate::daemon::start_daemon(self.clone(), onchain);
2078 let _ = daemon.insert(handle);
2079
2080 Ok(())
2081 }
2082
2083 #[deprecated(since = "0.1.4", note = "use start_daemon instead")]
2085 pub fn run_daemon(
2086 &self,
2087 onchain: Option<Arc<tokio::sync::RwLock<dyn DaemonizableOnchainWallet>>>,
2088 ) -> anyhow::Result<()> {
2089 self.start_daemon(onchain)
2090 }
2091
2092 pub fn stop_daemon(&self) {
2094 let mut daemon = self.inner.daemon.lock();
2095 if let Some(handle) = daemon.take() {
2096 handle.stop();
2097 }
2098 }
2099
2100 pub async fn register_vtxo_transactions_with_server(
2104 &self,
2105 vtxos: &[impl AsRef<Vtxo<Full>>],
2106 ) -> anyhow::Result<()> {
2107 if vtxos.is_empty() {
2108 return Ok(());
2109 }
2110
2111 let (mut srv, _) = self.require_server().await?;
2112 srv.client.register_vtxo_transactions(protos::RegisterVtxoTransactionsRequest {
2113 vtxos: vtxos.iter().map(|v| v.as_ref().serialize()).collect(),
2114 }).await.context("failed to register vtxo transactions")?;
2115
2116 Ok(())
2117 }
2118}
2119
2120fn wrap_server_connect_error(err: ConnectError) -> anyhow::Error {
2121 match err {
2122 ConnectError::CreateEndpoint(CreateEndpointError::NoTransportBackend) => {
2123 anyhow!(MISSING_SERVER_TRANSPORT_HELP)
2124 },
2125 other => anyhow::Error::from(other),
2126 }
2127}
2128
2129impl std::ops::Drop for WalletInner {
2130 fn drop(&mut self) {
2131 if let Some(handle) = self.daemon.lock().take() {
2132 handle.stop();
2133 }
2134 }
2135}
2136
2137#[cfg(test)]
2138mod tests {
2139 use server_rpc::client::CreateEndpointError;
2140
2141 use super::{wrap_server_connect_error, MISSING_SERVER_TRANSPORT_HELP};
2142
2143 #[test]
2144 fn no_transport_connect_error_is_reworded_for_wallet_users() {
2145 let err = wrap_server_connect_error(CreateEndpointError::NoTransportBackend.into());
2146 assert!(err.to_string().contains(MISSING_SERVER_TRANSPORT_HELP));
2147 assert!(err.to_string().contains("feature `bark-wallet/native` or `bark-wallet/wasm-web`"));
2148 }
2149}