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, 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
632#[derive(Clone)]
773pub struct Wallet {
774 inner: Arc<WalletInner>,
775}
776
777impl Wallet {
778 pub async fn require_chainsource_version(&self) -> anyhow::Result<()> {
782 self.inner.chain.require_version().await
783 }
784
785 pub async fn network(&self) -> anyhow::Result<Network> {
786 Ok(self.properties().await?.network)
787 }
788
789 pub fn chain(&self) -> &Arc<ChainSource> {
791 &self.inner.chain
792 }
793
794 pub fn exit_mgr(&self) -> &Exit {
796 &self.inner.exit
797 }
798
799 pub fn movements_mgr(&self) -> &MovementManager {
801 &self.inner.movements
802 }
803
804 pub async fn peek_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
807 let last_revealed = self.inner.db.get_last_vtxo_key_index().await?;
808
809 let index = last_revealed.map(|i| i + 1).unwrap_or(u32::MIN);
810 let keypair = self.inner.seed.derive_vtxo_keypair(index);
811
812 Ok((keypair, index))
813 }
814
815 pub async fn derive_store_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
818 let (keypair, index) = self.peek_next_keypair().await?;
819 self.inner.db.store_vtxo_key(index, keypair.public_key()).await?;
820 Ok((keypair, index))
821 }
822
823 #[deprecated(note = "use peek_keypair instead")]
824 pub async fn peak_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
825 self.peek_keypair(index).await
826 }
827
828 pub async fn peek_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
842 let keypair = self.inner.seed.derive_vtxo_keypair(index);
843 if self.inner.db.get_public_key_idx(&keypair.public_key()).await?.is_some() {
844 Ok(keypair)
845 } else {
846 bail!("VTXO key {} does not exist, please derive it first", index)
847 }
848 }
849
850
851 pub async fn pubkey_keypair(&self, public_key: &PublicKey) -> anyhow::Result<Option<(u32, Keypair)>> {
863 if let Some(index) = self.inner.db.get_public_key_idx(&public_key).await? {
864 Ok(Some((index, self.inner.seed.derive_vtxo_keypair(index))))
865 } else {
866 Ok(None)
867 }
868 }
869
870 pub async fn get_vtxo_key(&self, vtxo: impl VtxoRef) -> anyhow::Result<Keypair> {
881 let bare_vtxo = match vtxo.as_bare_vtxo() {
882 Some(bare) => bare,
883 None => Cow::Owned(self.get_vtxo_by_id(vtxo.vtxo_id()).await?.vtxo),
884 };
885 let pubkey = self.find_signable_clause(&bare_vtxo).await
886 .context("VTXO is not signable by wallet")?
887 .pubkey();
888 let idx = self.inner.db.get_public_key_idx(&pubkey).await?
889 .context("VTXO key not found")?;
890 Ok(self.inner.seed.derive_vtxo_keypair(idx))
891 }
892
893 #[deprecated(note = "use peek_address instead")]
894 pub async fn peak_address(&self, index: u32) -> anyhow::Result<ark::Address> {
895 self.peek_address(index).await
896 }
897
898 pub async fn peek_address(&self, index: u32) -> anyhow::Result<ark::Address> {
902 let properties = self.properties().await?;
903 let network = properties.network;
904 let keypair = self.peek_keypair(index).await?;
905 let mailbox = self.mailbox_identifier();
906
907
908 let (server_pubkey, mailbox_pubkey) =
909 if let (Some(spk), Some(mpk)) = (properties.server_pubkey, properties.server_mailbox_pubkey) {
910 (spk, mpk)
911 } else {
912 let (_, ark_info) = self.require_server().await?;
913 (ark_info.server_pubkey, ark_info.mailbox_pubkey)
914 };
915
916 Ok(ark::Address::builder()
917 .testnet(network != bitcoin::Network::Bitcoin)
918 .server_pubkey(server_pubkey)
919 .pubkey_policy(keypair.public_key())
920 .mailbox(mailbox_pubkey, mailbox, &keypair)
921 .expect("Failed to assign mailbox")
922 .into_address().unwrap())
923 }
924
925 pub async fn new_address_with_index(&self) -> anyhow::Result<(ark::Address, u32)> {
929 let (_, index) = self.derive_store_next_keypair().await?;
930 let addr = self.peek_address(index).await?;
931 Ok((addr, index))
932 }
933
934 pub async fn new_address(&self) -> anyhow::Result<ark::Address> {
936 let (addr, _) = self.new_address_with_index().await?;
937 Ok(addr)
938 }
939
940 pub async fn create(
949 mnemonic: &Mnemonic,
950 network: Network,
951 config: Config,
952 db: Arc<dyn BarkPersister>,
953 lock_manager: Box<dyn LockManager>,
954 force: bool,
955 ) -> anyhow::Result<Wallet> {
956 trace!("Config: {:?}", config);
957
958 let wallet_fingerprint = WalletSeed::new(network, &mnemonic.to_seed("")).fingerprint();
959
960 let create_guard = lock_manager.lock(
965 &format!("{}.create", wallet_fingerprint),
966 Duration::from_secs(5),
967 ).await.context("wallet initialization already in progress")?;
968
969 if let Some(existing) = db.read_properties().await? {
970 trace!("Existing config: {:?}", existing);
971 bail!("cannot overwrite already existing config")
972 }
973
974 let (server_pubkey, mailbox_pubkey) = if !force {
976 match Self::connect_to_server(&config, network).await {
977 Ok(conn) => {
978 let ark_info = conn.ark_info().await;
979 (Some(ark_info.server_pubkey), Some(ark_info.mailbox_pubkey))
980 }
981 Err(err) => {
982 bail!("Failed to connect to provided server (if you are sure use the --force flag): {:#}", err);
983 }
984 }
985 } else {
986 (None, None)
987 };
988
989 let properties = WalletProperties {
990 network,
991 fingerprint: wallet_fingerprint,
992 server_pubkey,
993 server_mailbox_pubkey: mailbox_pubkey,
994 };
995
996 db.init_wallet(&properties).await.context("cannot init wallet in the database")?;
998 info!("Created wallet with fingerprint: {}", wallet_fingerprint);
999 if let Some(pk) = server_pubkey {
1000 info!("Stored server pubkey: {}", pk);
1001 }
1002
1003 drop(create_guard);
1006
1007 let wallet = Wallet::open(&mnemonic, db, config, lock_manager).await.context("failed to open wallet")?;
1009 wallet.require_chainsource_version().await?;
1010
1011 Ok(wallet)
1012 }
1013
1014 pub async fn create_with_exits(
1021 mnemonic: &Mnemonic,
1022 network: Network,
1023 config: Config,
1024 db: Arc<dyn BarkPersister>,
1025 lock_manager: Box<dyn LockManager>,
1026 force: bool,
1027 ) -> anyhow::Result<Wallet> {
1028 let wallet = Wallet::create(mnemonic, network, config, db, lock_manager, force).await?;
1029 wallet.inner.exit.load().await?;
1030 Ok(wallet)
1031 }
1032
1033 pub async fn open(
1038 mnemonic: &Mnemonic,
1039 db: Arc<dyn BarkPersister>,
1040 config: Config,
1041 lock_manager: Box<dyn LockManager>,
1042 ) -> anyhow::Result<Wallet> {
1043 let properties = db.read_properties().await?.context("Wallet is not initialised")?;
1044
1045 let seed = {
1046 let seed = mnemonic.to_seed("");
1047 WalletSeed::new(properties.network, &seed)
1048 };
1049
1050 if properties.fingerprint != seed.fingerprint() {
1051 bail!("incorrect mnemonic")
1052 }
1053
1054 let chain_source = if let Some(ref url) = config.esplora_address {
1055 ChainSourceSpec::Esplora {
1056 url: url.clone(),
1057 }
1058 } else if let Some(ref url) = config.bitcoind_address {
1059 let auth = if let Some(ref c) = config.bitcoind_cookiefile {
1060 bitcoin_ext::rpc::Auth::CookieFile(c.clone())
1061 } else {
1062 bitcoin_ext::rpc::Auth::UserPass(
1063 config.bitcoind_user.clone().context("need bitcoind auth config")?,
1064 config.bitcoind_pass.clone().context("need bitcoind auth config")?,
1065 )
1066 };
1067 ChainSourceSpec::Bitcoind { url: url.clone(), auth }
1068 } else {
1069 bail!("Need to either provide esplora or bitcoind info");
1070 };
1071
1072 #[cfg(feature = "socks5-proxy")]
1073 let chain_proxy = proxy_for_url(&config.socks5_proxy, chain_source.url())?;
1074 let chain_source_client = ChainSource::new(
1075 chain_source, properties.network, config.fallback_fee_rate,
1076 #[cfg(feature = "socks5-proxy")] chain_proxy.as_deref(),
1077 ).await?;
1078 let chain = Arc::new(chain_source_client);
1079
1080 let server = tokio::sync::OnceCell::new();
1081
1082 let notifications = NotificationDispatch::new();
1083 let movements = Arc::new(MovementManager::new(db.clone(), notifications.clone()));
1084 let exit = Exit::new(db.clone(), chain.clone(), movements.clone()).await?;
1085
1086 Ok(Wallet { inner: Arc::new(WalletInner {
1087 config, db, lock_manager, seed, exit, movements, notifications, server, chain,
1088 daemon: parking_lot::Mutex::new(None),
1089 })})
1090 }
1091
1092 pub async fn open_with_exits(
1096 mnemonic: &Mnemonic,
1097 db: Arc<dyn BarkPersister>,
1098 cfg: Config,
1099 lock_manager: Box<dyn LockManager>,
1100 ) -> anyhow::Result<Wallet> {
1101 let wallet = Wallet::open(mnemonic, db, cfg, lock_manager).await?;
1102 wallet.inner.exit.load().await?;
1103 Ok(wallet)
1104 }
1105
1106 pub async fn open_with_daemon(
1109 mnemonic: &Mnemonic,
1110 db: Arc<dyn BarkPersister>,
1111 cfg: Config,
1112 onchain: Option<Arc<tokio::sync::RwLock<dyn DaemonizableOnchainWallet>>>,
1113 lock_manager: Box<dyn LockManager>,
1114 ) -> anyhow::Result<Wallet> {
1115 let wallet = Wallet::open(mnemonic, db, cfg, lock_manager).await?;
1116 if onchain.is_some() {
1117 wallet.inner.exit.load().await?;
1118 }
1119
1120 wallet.start_daemon(onchain)?;
1121
1122 Ok(wallet)
1123 }
1124
1125 pub fn config(&self) -> &Config {
1127 &self.inner.config
1128 }
1129
1130 pub async fn properties(&self) -> anyhow::Result<WalletProperties> {
1132 let properties = self.inner.db.read_properties().await?.context("Wallet is not initialised")?;
1133 Ok(properties)
1134 }
1135
1136 pub fn fingerprint(&self) -> Fingerprint {
1138 self.inner.seed.fingerprint()
1139 }
1140
1141 async fn connect_to_server(
1142 config: &Config,
1143 network: Network,
1144 ) -> anyhow::Result<ServerConnection> {
1145 let server_address = crate::utils::url_with_default_https_scheme(&config.server_address);
1146 let mut builder = ServerConnection::builder()
1147 .address(&server_address)
1148 .network(network);
1149
1150 #[cfg(feature = "socks5-proxy")]
1151 if let Some(proxy) = proxy_for_url(&config.socks5_proxy, &server_address)? {
1152 builder = builder.proxy(&proxy)
1153 }
1154
1155 if let Some(ref token) = config.server_access_token {
1156 builder = builder.access_token(token);
1157 }
1158
1159 builder.connect().await.map_err(wrap_server_connect_error)
1160 .context("Failed to connect to Ark server")
1161 }
1162
1163 async fn require_server(&self) -> anyhow::Result<(ServerConnection, ArkInfo)> {
1164 let conn = self.inner.server.get_or_try_init(|| async {
1168 let network = self.properties().await?.network;
1169 Self::connect_to_server(&self.inner.config, network).await
1170 .context("You should be connected to Ark server to perform this action")
1171 }).await?.clone();
1172
1173 let ark_info = conn.ark_info().await;
1174 self.check_and_store_server_keys(&ark_info).await?;
1175
1176 Ok((conn, ark_info))
1177 }
1178
1179 pub async fn refresh_server(&self) -> anyhow::Result<()> {
1180 let srv = self.inner.server.get_or_try_init(|| async {
1186 let properties = self.properties().await?;
1187 Self::connect_to_server(&self.inner.config, properties.network).await
1188 .map_err(anyhow::Error::from)
1189 }).await?;
1190
1191 srv.check_connection().await?;
1192 let ark_info = srv.ark_info().await;
1193 ark_info.fees.validate().context("invalid fee schedule")?;
1194 self.check_and_store_server_keys(&ark_info).await?;
1195
1196 Ok(())
1197 }
1198
1199 async fn check_and_store_server_keys(&self, ark_info: &ArkInfo) -> anyhow::Result<()> {
1206 let properties = self.properties().await?;
1207
1208 if let Some(stored_pubkey) = properties.server_pubkey {
1209 if stored_pubkey != ark_info.server_pubkey {
1210 log_server_pubkey_changed_error(stored_pubkey, ark_info.server_pubkey);
1211 bail!("Server public key has changed. You should exit all your VTXOs!");
1212 }
1213 } else {
1214 self.inner.db.set_server_pubkey(ark_info.server_pubkey).await?;
1215 info!("Stored server pubkey for existing wallet: {}", ark_info.server_pubkey);
1216 }
1217
1218 if let Some(stored_mailbox_pubkey) = properties.server_mailbox_pubkey {
1219 if stored_mailbox_pubkey != ark_info.mailbox_pubkey {
1220 log_server_mailbox_pubkey_changed_error(stored_mailbox_pubkey, ark_info.mailbox_pubkey);
1221 bail!("Server mailbox public key has changed.");
1222 }
1223 } else {
1224 self.inner.db.set_server_mailbox_pubkey(ark_info.mailbox_pubkey).await?;
1225 info!("Stored server mailbox pubkey for existing wallet: {}", ark_info.mailbox_pubkey);
1226 }
1227
1228 Ok(())
1229 }
1230
1231 pub async fn ark_info(&self) -> anyhow::Result<Option<ArkInfo>> {
1233 match self.inner.server.get() {
1234 Some(srv) => Ok(Some(srv.ark_info().await)),
1235 None => Ok(None),
1236 }
1237 }
1238
1239 pub async fn require_ark_info(&self) -> anyhow::Result<ArkInfo> {
1245 let (_, ark_info) = self.require_server().await?;
1246 Ok(ark_info)
1247 }
1248
1249 pub async fn balance(&self) -> anyhow::Result<Balance> {
1253 let vtxos = self.vtxos().await?;
1254
1255 let spendable = {
1256 let mut v = vtxos.iter().collect();
1257 VtxoStateKind::Spendable.filter_vtxos(&mut v).await?;
1258 v.into_iter().map(|v| v.amount()).sum::<Amount>()
1259 };
1260
1261 let pending_lightning_send = self.pending_lightning_send_vtxos().await?.iter()
1262 .map(|v| v.amount())
1263 .sum::<Amount>();
1264
1265 let claimable_lightning_receive = self.claimable_lightning_receive_balance().await?;
1266
1267 let pending_board = self.pending_board_vtxos().await?.iter()
1268 .map(|v| v.amount())
1269 .sum::<Amount>();
1270
1271 let pending_in_round = self.pending_round_balance().await?;
1272
1273 let pending_exit = self.exit_mgr().try_pending_total();
1274
1275 Ok(Balance {
1276 spendable,
1277 pending_in_round,
1278 pending_lightning_send,
1279 claimable_lightning_receive,
1280 pending_exit,
1281 pending_board,
1282 })
1283 }
1284
1285 pub async fn validate_vtxo(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<()> {
1287 let tx = self.inner.chain.get_tx(&vtxo.chain_anchor().txid).await
1288 .context("could not fetch chain tx")?;
1289
1290 let tx = tx.with_context(|| {
1291 format!("vtxo chain anchor not found for vtxo: {}", vtxo.chain_anchor().txid)
1292 })?;
1293
1294 vtxo.validate(&tx)?;
1295
1296 Ok(())
1297 }
1298
1299 pub async fn import_vtxo(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<()> {
1309 if self.inner.db.get_wallet_vtxo(vtxo.id()).await?.is_some() {
1310 info!("VTXO {} already exists in wallet, skipping import", vtxo.id());
1311 return Ok(());
1312 }
1313
1314 self.validate_vtxo(vtxo).await.context("VTXO validation failed")?;
1315
1316 if self.find_signable_clause(vtxo).await.is_none() {
1317 bail!("VTXO {} is not owned by this wallet (no signable clause found)", vtxo.id());
1318 }
1319
1320 let current_height = self.inner.chain.tip().await?;
1321 if vtxo.expiry_height() <= current_height {
1322 bail!("Vtxo {} has expired", vtxo.id());
1323 }
1324
1325 self.store_spendable_vtxos([vtxo]).await.context("failed to store imported VTXO")?;
1326
1327 info!("Successfully imported VTXO {}", vtxo.id());
1328 Ok(())
1329 }
1330
1331 pub async fn get_vtxo_by_id(&self, vtxo_id: VtxoId) -> anyhow::Result<WalletVtxo> {
1333 let vtxo = self.inner.db.get_wallet_vtxo(vtxo_id).await
1334 .with_context(|| format!("Error when querying vtxo {} in database", vtxo_id))?
1335 .with_context(|| format!("The VTXO with id {} cannot be found", vtxo_id))?;
1336 Ok(vtxo)
1337 }
1338
1339 pub async fn get_full_vtxo(&self, vtxo_id: VtxoId) -> anyhow::Result<Vtxo<Full>> {
1347 self.inner.db.get_full_vtxo(vtxo_id).await
1348 .with_context(|| format!("Error when querying full vtxo {} in database", vtxo_id))?
1349 .with_context(|| format!("The VTXO with id {} cannot be found", vtxo_id))
1350 }
1351
1352 pub async fn get_full_vtxos<V: VtxoRef>(
1354 &self,
1355 vtxos: impl IntoIterator<Item = V>,
1356 ) -> anyhow::Result<Vec<Vtxo<Full>>> {
1357 let ids = vtxos.into_iter().map(|v| v.vtxo_id()).collect::<Vec<_>>();
1358 self.inner.db.get_full_vtxos(&ids).await
1359 .with_context(||
1360 format!("Error when querying full vtxos in database with IDs: {:?}", ids)
1361 )
1362 }
1363
1364 #[deprecated(since="0.1.0-beta.5", note = "Use Wallet::history instead")]
1366 pub async fn movements(&self) -> anyhow::Result<Vec<Movement>> {
1367 self.history().await
1368 }
1369
1370 pub async fn history(&self) -> anyhow::Result<Vec<Movement>> {
1372 Ok(self.inner.db.get_all_movements().await?)
1373 }
1374
1375 pub async fn update_history_metadata(
1395 &self,
1396 movement_id: MovementId,
1397 patch: &serde_json::Value,
1398 ) -> anyhow::Result<()> {
1399 self.inner.movements.patch_metadata(movement_id, patch).await?;
1400 Ok(())
1401 }
1402
1403 pub async fn history_by_payment_method(
1405 &self,
1406 payment_method: &PaymentMethod,
1407 ) -> anyhow::Result<Vec<Movement>> {
1408 let mut ret = self.inner.db.get_movements_by_payment_method(payment_method).await?;
1409 ret.sort_by_key(|m| m.id);
1410 Ok(ret)
1411 }
1412
1413 pub async fn all_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1415 Ok(self.inner.db.get_all_vtxos().await?)
1416 }
1417
1418 pub async fn vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1420 Ok(self.inner.db.get_vtxos_by_state(&VtxoStateKind::UNSPENT_STATES).await?)
1421 }
1422
1423 pub async fn vtxos_with(&self, filter: &impl FilterVtxos) -> anyhow::Result<Vec<WalletVtxo>> {
1425 let mut vtxos = self.vtxos().await?;
1426 filter.filter_vtxos(&mut vtxos).await.context("error filtering vtxos")?;
1427 Ok(vtxos)
1428 }
1429
1430 pub async fn spendable_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1432 Ok(self.vtxos_with(&VtxoStateKind::Spendable).await?)
1433 }
1434
1435 pub async fn spendable_vtxos_with(
1437 &self,
1438 filter: &impl FilterVtxos,
1439 ) -> anyhow::Result<Vec<WalletVtxo>> {
1440 let mut vtxos = self.spendable_vtxos().await?;
1441 filter.filter_vtxos(&mut vtxos).await.context("error filtering vtxos")?;
1442 Ok(vtxos)
1443 }
1444
1445 pub async fn get_expiring_vtxos(
1447 &self,
1448 threshold: BlockHeight,
1449 ) -> anyhow::Result<Vec<WalletVtxo>> {
1450 let expiry = self.inner.chain.tip().await? + threshold;
1451 let filter = VtxoFilter::new(&self).expires_before(expiry);
1452 Ok(self.spendable_vtxos_with(&filter).await?)
1453 }
1454
1455 pub async fn maintenance(&self) -> anyhow::Result<()> {
1461 info!("Starting wallet maintenance in interactive mode");
1462 self.sync().await;
1463
1464 let rounds = self.progress_pending_rounds(None).await;
1466 if let Err(e) = rounds.as_ref() {
1467 warn!("Error progressing pending rounds: {:#}", e);
1468 }
1469
1470 let states = self.inner.db.get_pending_round_state_ids().await?;
1472 for id in states {
1473 debug!("Cancelling pending round participation {}", id);
1474 let mut state = match self.lock_wait_round_state(id).await {
1475 Ok(Some(s)) => s,
1476 Ok(None) => continue, Err(e) => {
1478 warn!("Failed to lock round state with id {}: {:#}", id, e);
1479 continue;
1480 }
1481 };
1482 if let Err(e) = state.state_mut().try_cancel(self).await {
1483 warn!("Error cancelling pending round: {:#}", e);
1484 }
1485 }
1486
1487 let refresh = self.maintenance_refresh().await;
1489 if let Err(e) = refresh.as_ref() {
1490 warn!("Error refreshing VTXOs: {:#}", e);
1491 }
1492 if rounds.is_err() || refresh.is_err() {
1493 bail!("Maintenance encountered errors.\nprogress_rounds: {:#?}\nrefresh: {:#?}", rounds, refresh);
1494 }
1495 Ok(())
1496 }
1497
1498 pub async fn maintenance_delegated(&self) -> anyhow::Result<()> {
1504 info!("Starting wallet maintenance in delegated mode");
1505 self.sync().await;
1506 let rounds = self.progress_pending_rounds(None).await;
1507 if let Err(e) = rounds.as_ref() {
1508 warn!("Error progressing pending rounds: {:#}", e);
1509 }
1510 let refresh = self.maybe_schedule_maintenance_refresh_delegated().await;
1511 if let Err(e) = refresh.as_ref() {
1512 warn!("Error refreshing VTXOs: {:#}", e);
1513 }
1514 if rounds.is_err() || refresh.is_err() {
1515 bail!("Delegated maintenance encountered errors.\nprogress_rounds: {:#?}\nrefresh: {:#?}", rounds, refresh);
1516 }
1517 Ok(())
1518 }
1519
1520 pub async fn maintenance_with_onchain<W: PreparePsbt + SignPsbt + ExitUnilaterally>(
1528 &self,
1529 onchain: &mut W,
1530 ) -> anyhow::Result<()> {
1531 info!("Starting wallet maintenance in interactive mode with onchain wallet");
1532
1533 let maintenance = self.maintenance().await;
1535
1536 let exit_sync = self.sync_exits().await;
1538 if let Err(e) = exit_sync.as_ref() {
1539 warn!("Error syncing exits: {:#}", e);
1540 }
1541 let exit_progress = self.exit_mgr().progress_exits_with_bdk(self, onchain, None).await;
1542 if let Err(e) = exit_progress.as_ref() {
1543 warn!("Error progressing exits: {:#}", e);
1544 }
1545 if maintenance.is_err() || exit_sync.is_err() || exit_progress.is_err() {
1546 bail!("Maintenance encountered errors.\nmaintenance: {:#?}\nexit_sync: {:#?}\nexit_progress: {:#?}", maintenance, exit_sync, exit_progress);
1547 }
1548 Ok(())
1549 }
1550
1551 pub async fn maintenance_with_onchain_delegated<W: PreparePsbt + SignPsbt + ExitUnilaterally>(
1558 &self,
1559 onchain: &mut W,
1560 ) -> anyhow::Result<()> {
1561 info!("Starting wallet maintenance in delegated mode with onchain wallet");
1562
1563 let maintenance = self.maintenance_delegated().await;
1565
1566 let exit_sync = self.sync_exits().await;
1568 if let Err(e) = exit_sync.as_ref() {
1569 warn!("Error syncing exits: {:#}", e);
1570 }
1571 let exit_progress = self.exit_mgr().progress_exits_with_bdk(self, onchain, None).await;
1572 if let Err(e) = exit_progress.as_ref() {
1573 warn!("Error progressing exits: {:#}", e);
1574 }
1575 if maintenance.is_err() || exit_sync.is_err() || exit_progress.is_err() {
1576 bail!("Delegated maintenance encountered errors.\nmaintenance: {:#?}\nexit_sync: {:#?}\nexit_progress: {:#?}", maintenance, exit_sync, exit_progress);
1577 }
1578 Ok(())
1579 }
1580
1581 pub async fn maybe_schedule_maintenance_refresh(&self) -> anyhow::Result<Option<RoundStateId>> {
1589 let vtxos = self.get_vtxos_to_refresh().await?;
1590 if vtxos.len() == 0 {
1591 return Ok(None);
1592 }
1593
1594 let participation = match self.build_refresh_participation(vtxos).await? {
1595 Some(participation) => participation,
1596 None => return Ok(None),
1597 };
1598
1599 info!("Scheduling maintenance refresh ({} vtxos)", participation.inputs.len());
1600 let state = self.join_next_round(participation, Some(RoundMovement::Refresh)).await?;
1601 Ok(Some(state.id()))
1602 }
1603
1604 pub async fn maybe_schedule_maintenance_refresh_delegated(
1612 &self,
1613 ) -> anyhow::Result<Option<RoundStateId>> {
1614 let vtxos = self.get_vtxos_to_refresh().await?;
1615 if vtxos.len() == 0 {
1616 return Ok(None);
1617 }
1618
1619 let participation = match self.build_refresh_participation(vtxos).await? {
1620 Some(participation) => participation,
1621 None => return Ok(None),
1622 };
1623
1624 info!("Scheduling delegated maintenance refresh ({} vtxos)", participation.inputs.len());
1625 let state = self.join_next_round_delegated(participation, Some(RoundMovement::Refresh)).await?;
1626 Ok(Some(state.id()))
1627 }
1628
1629 pub async fn maintenance_refresh(&self) -> anyhow::Result<Option<RoundStatus>> {
1637 let vtxos = self.get_vtxos_to_refresh().await?;
1638 if vtxos.len() == 0 {
1639 return Ok(None);
1640 }
1641
1642 info!("Waiting for round to perform maintenance refresh...");
1643 let mut events = self.subscribe_round_events().await?;
1644 while let Some(event) = events.next().await {
1645 match event {
1646 Ok(RoundEvent::Attempt(a)) if a.attempt_seq == 0 => {
1647 let vtxos = self.get_vtxos_to_refresh().await?;
1648 if vtxos.len() == 0 {
1649 return Ok(None);
1650 }
1651 debug!("Round {} started, triggering refresh", a.round_seq);
1652 return self.refresh_vtxos(vtxos).await;
1653 },
1654 _ => {},
1655 }
1656 }
1657 Ok(None)
1658 }
1659
1660 pub async fn sync(&self) {
1667 futures::join!(
1668 async {
1669 if let Err(e) = self.inner.chain.update_fee_rates(self.inner.config.fallback_fee_rate).await {
1672 warn!("Error updating fee rates: {:#}", e);
1673 }
1674 },
1675 async {
1676 if let Err(e) = self.sync_mailbox().await {
1677 warn!("Error in mailbox sync: {:#}", e);
1678 }
1679 },
1680 async {
1681 if let Err(e) = self.sync_pending_rounds().await {
1682 warn!("Error while trying to progress rounds awaiting confirmations: {:#}", e);
1683 }
1684 },
1685 async {
1686 if let Err(e) = self.sync_pending_lightning_send_vtxos().await {
1687 warn!("Error syncing pending lightning payments: {:#}", e);
1688 }
1689 },
1690 async {
1691 if let Err(e) = self.try_claim_all_lightning_receives(false).await {
1692 warn!("Error claiming pending lightning receives: {:#}", e);
1693 }
1694 },
1695 async {
1696 if let Err(e) = self.sync_pending_boards().await {
1697 warn!("Error syncing pending boards: {:#}", e);
1698 }
1699 },
1700 async {
1701 if let Err(e) = self.sync_pending_offboards().await {
1702 warn!("Error syncing pending offboards: {:#}", e);
1703 }
1704 }
1705 );
1706 }
1707
1708 pub async fn sync_exits(&self) -> anyhow::Result<()> {
1714 self.exit_mgr().sync(&self).await?;
1715 Ok(())
1716 }
1717
1718 pub async fn dangerous_drop_vtxo(&self, vtxo_id: VtxoId) -> anyhow::Result<()> {
1721 warn!("Drop vtxo {} from the database", vtxo_id);
1722 self.inner.db.remove_vtxo(vtxo_id).await?;
1723 Ok(())
1724 }
1725
1726 pub async fn dangerous_drop_all_vtxos(&self) -> anyhow::Result<()> {
1729 warn!("Dropping all vtxos from the db...");
1730 for vtxo in self.vtxos().await? {
1731 self.inner.db.remove_vtxo(vtxo.id()).await?;
1732 }
1733
1734 self.exit_mgr().dangerous_clear_exit().await?;
1735 Ok(())
1736 }
1737
1738 async fn has_counterparty_risk(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<bool> {
1746 for past_pks in vtxo.past_arkoor_pubkeys() {
1747 let mut owns_any = false;
1748 for past_pk in past_pks {
1749 if self.inner.db.get_public_key_idx(&past_pk).await?.is_some() {
1750 owns_any = true;
1751 break;
1752 }
1753 }
1754 if !owns_any {
1755 return Ok(true);
1756 }
1757 }
1758
1759 let my_clause = self.find_signable_clause(vtxo).await;
1760 Ok(!my_clause.is_some())
1761 }
1762
1763 async fn add_should_refresh_vtxos(
1769 &self,
1770 participation: &mut RoundParticipation,
1771 ) -> anyhow::Result<()> {
1772 let tip = self.inner.chain.tip().await?;
1775 let mut vtxos_to_refresh = self.spendable_vtxos_with(
1776 &RefreshStrategy::should_refresh(self, tip, self.inner.chain.fee_rates().await.fast),
1777 ).await?;
1778 if vtxos_to_refresh.is_empty() {
1779 return Ok(());
1780 }
1781
1782 let excluded_ids = participation.inputs.iter().map(|v| v.vtxo_id())
1783 .collect::<HashSet<_>>();
1784 let mut total_amount = Amount::ZERO;
1785 for i in (0..vtxos_to_refresh.len()).rev() {
1786 let vtxo = &vtxos_to_refresh[i];
1787 if excluded_ids.contains(&vtxo.id()) {
1788 vtxos_to_refresh.swap_remove(i);
1789 continue;
1790 }
1791 total_amount += vtxo.amount();
1792 }
1793 if vtxos_to_refresh.is_empty() {
1794 return Ok(());
1796 }
1797
1798 let (_, ark_info) = self.require_server().await?;
1801 let fee = ark_info.fees.refresh.calculate_no_base_fee(
1802 vtxos_to_refresh.iter().map(|wv| VtxoFeeInfo::from_vtxo_and_tip(&wv.vtxo, tip)),
1803 ).context("fee overflowed")?;
1804
1805 let output_amount = match validate_and_subtract_fee_min_dust(total_amount, fee) {
1807 Ok(amount) => amount,
1808 Err(e) => {
1809 trace!("Cannot add should-refresh VTXOs: {}", e);
1810 return Ok(());
1811 },
1812 };
1813 info!(
1814 "Adding {} extra VTXOs to round participation total = {}, fee = {}, output = {}",
1815 vtxos_to_refresh.len(), total_amount, fee, output_amount,
1816 );
1817 let (user_keypair, _) = self.derive_store_next_keypair().await?;
1818 let req = VtxoRequest {
1819 policy: VtxoPolicy::new_pubkey(user_keypair.public_key()),
1820 amount: output_amount,
1821 };
1822 let extra_ids = vtxos_to_refresh.into_iter().map(|wv| wv.id()).collect::<Vec<_>>();
1823 let extra_full = self.inner.db.get_full_vtxos(&extra_ids).await
1824 .context("failed to hydrate refresh candidates")?;
1825 participation.inputs.reserve(extra_full.len());
1826 participation.inputs.extend(extra_full);
1827 participation.outputs.push(req);
1828
1829 Ok(())
1830 }
1831
1832 pub async fn build_refresh_participation<V: VtxoRef>(
1833 &self,
1834 vtxos: impl IntoIterator<Item = V>,
1835 ) -> anyhow::Result<Option<RoundParticipation>> {
1836 let (vtxos, total_amount) = {
1837 let iter = vtxos.into_iter();
1838 let size_hint = iter.size_hint();
1839 let mut vtxos = Vec::<Vtxo<Full>>::with_capacity(size_hint.1.unwrap_or(size_hint.0));
1840 let mut amount = Amount::ZERO;
1841 for vref in iter {
1842 let id = vref.vtxo_id();
1847 if vtxos.iter().any(|v| v.id() == id) {
1848 bail!("duplicate VTXO id: {}", id);
1849 }
1850 let vtxo = if let Some(vtxo) = vref.into_full_vtxo() {
1851 vtxo
1852 } else {
1853 self.inner.db.get_full_vtxo(id).await?
1856 .with_context(|| format!("vtxo with id {} not found", id))?
1857 };
1858 amount += vtxo.amount();
1859 vtxos.push(vtxo);
1860 }
1861 (vtxos, amount)
1862 };
1863
1864 if vtxos.is_empty() {
1865 info!("Skipping refresh since no VTXOs are provided.");
1866 return Ok(None);
1867 }
1868 ensure!(total_amount >= P2TR_DUST,
1869 "vtxo amount must be at least {} to participate in a round",
1870 P2TR_DUST,
1871 );
1872
1873 let (_, ark_info) = self.require_server().await?;
1875 let current_height = self.inner.chain.tip().await?;
1876 let vtxo_fee_infos = vtxos.iter()
1877 .map(|v| VtxoFeeInfo::from_vtxo_and_tip(v, current_height));
1878 let fee = ark_info.fees.refresh.calculate(vtxo_fee_infos).context("fee overflowed")?;
1879 let output_amount = validate_and_subtract_fee_min_dust(total_amount, fee)?;
1880
1881 info!("Refreshing {} VTXOs (total amount = {}, fee = {}, output = {}).",
1882 vtxos.len(), total_amount, fee, output_amount,
1883 );
1884 let (user_keypair, _) = self.derive_store_next_keypair().await?;
1885 let req = VtxoRequest {
1886 policy: VtxoPolicy::Pubkey(PubkeyVtxoPolicy { user_pubkey: user_keypair.public_key() }),
1887 amount: output_amount,
1888 };
1889
1890 Ok(Some(RoundParticipation {
1891 inputs: vtxos,
1892 outputs: vec![req],
1893 unblinded_mailbox_id: None,
1894 }))
1895 }
1896
1897 pub async fn refresh_vtxos<V: VtxoRef>(
1902 &self,
1903 vtxos: impl IntoIterator<Item = V>,
1904 ) -> anyhow::Result<Option<RoundStatus>> {
1905 let mut participation = match self.build_refresh_participation(vtxos).await? {
1906 Some(participation) => participation,
1907 None => return Ok(None),
1908 };
1909
1910 if let Err(e) = self.add_should_refresh_vtxos(&mut participation).await {
1911 warn!("Error trying to add additional VTXOs that should be refreshed: {:#}", e);
1912 }
1913
1914 Ok(Some(self.participate_round(participation, Some(RoundMovement::Refresh)).await?))
1915 }
1916
1917 pub async fn refresh_vtxos_delegated<V: VtxoRef>(
1923 &self,
1924 vtxos: impl IntoIterator<Item = V>,
1925 ) -> anyhow::Result<Option<StoredRoundState<Unlocked>>> {
1926 let mut part = match self.build_refresh_participation(vtxos).await? {
1927 Some(participation) => participation,
1928 None => return Ok(None),
1929 };
1930
1931 if let Err(e) = self.add_should_refresh_vtxos(&mut part).await {
1932 warn!("Error trying to add additional VTXOs that should be refreshed: {:#}", e);
1933 }
1934
1935 Ok(Some(self.join_next_round_delegated(part, Some(RoundMovement::Refresh)).await?))
1936 }
1937
1938 pub async fn get_vtxos_to_refresh(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1941 let vtxos = self.spendable_vtxos_with(&RefreshStrategy::should_refresh_if_must(
1942 self,
1943 self.inner.chain.tip().await?,
1944 self.inner.chain.fee_rates().await.fast,
1945 )).await?;
1946 Ok(vtxos)
1947 }
1948
1949 pub async fn get_first_expiring_vtxo_blockheight(
1951 &self,
1952 ) -> anyhow::Result<Option<BlockHeight>> {
1953 Ok(self.spendable_vtxos().await?.iter().map(|v| v.expiry_height()).min())
1954 }
1955
1956 pub async fn get_next_required_refresh_blockheight(
1959 &self,
1960 ) -> anyhow::Result<Option<BlockHeight>> {
1961 let first_expiry = self.get_first_expiring_vtxo_blockheight().await?;
1962 Ok(first_expiry.map(|h| h.saturating_sub(self.inner.config.vtxo_refresh_expiry_threshold)))
1963 }
1964
1965 async fn select_vtxos_to_cover(
1971 &self,
1972 amount: Amount,
1973 ) -> anyhow::Result<Vec<WalletVtxo>> {
1974 let mut vtxos = self.spendable_vtxos().await?;
1975 self.sort_vtxos_for_selection(&mut vtxos);
1976
1977 let (last, _total_amount) = self.select_vtxos_inner(amount, &vtxos)?;
1978 vtxos.truncate(last+1);
1979 Ok(vtxos)
1980 }
1981
1982 async fn select_vtxos_to_cover_with_fee<F>(
1988 &self,
1989 amount: Amount,
1990 calc_fee: F,
1991 ) -> anyhow::Result<(Vec<WalletVtxo>, Amount)>
1992 where
1993 F: for<'a> Fn(
1994 Amount, std::iter::Copied<std::slice::Iter<'a, VtxoFeeInfo>>,
1995 ) -> anyhow::Result<Amount>,
1996 {
1997 let tip = self.inner.chain.tip().await?;
1998 let mut vtxos = self.spendable_vtxos().await?;
1999 self.sort_vtxos_for_selection(&mut vtxos);
2000
2001 let fee_info = vtxos.iter()
2002 .map(|v| VtxoFeeInfo::from_vtxo_and_tip(v, tip))
2003 .collect::<Vec<_>>();
2004
2005 const MAX_ITERATIONS: usize = 100;
2008 let mut fee = Amount::ZERO;
2009 for _ in 0..MAX_ITERATIONS {
2010 let required = amount.checked_add(fee)
2011 .context("Amount + fee overflow")?;
2012
2013 let (last, vtxo_amount) = self.select_vtxos_inner(required, &vtxos)
2014 .context("Could not find enough suitable VTXOs to cover payment + fees")?;
2015 fee = calc_fee(amount, fee_info[..=last].iter().copied())?;
2016
2017 if amount + fee <= vtxo_amount {
2018 trace!("Selected vtxos to cover amount + fee: amount = {}, fee = {}, total inputs = {}",
2019 amount, fee, vtxo_amount,
2020 );
2021 vtxos.truncate(last+1);
2022 return Ok((vtxos, fee));
2023 }
2024 trace!("VTXO sum of {} did not exceed amount {} and fee {}, iterating again",
2025 vtxo_amount, amount, fee,
2026 );
2027 }
2028 bail!("Fee calculation did not converge after maximum iterations")
2029 }
2030
2031 fn sort_vtxos_for_selection(&self, vtxos: &mut Vec<WalletVtxo>) {
2033 vtxos.sort_by_key(|v| v.expiry_height());
2034 }
2035
2036 fn select_vtxos_inner(
2042 &self,
2043 amount: Amount,
2044 vtxos: &Vec<WalletVtxo>,
2045 ) -> anyhow::Result<(usize, Amount)> {
2046 let mut total_amount = Amount::ZERO;
2048 for (i, vtxo) in vtxos.iter().enumerate() {
2049 total_amount += vtxo.amount();
2050
2051 if total_amount >= amount {
2052 return Ok((i, total_amount))
2053 }
2054 }
2055
2056 bail!("Insufficient money available. Needed {} but {} is available",
2057 amount, total_amount,
2058 );
2059 }
2060
2061 pub fn start_daemon(
2067 &self,
2068 onchain: Option<Arc<tokio::sync::RwLock<dyn DaemonizableOnchainWallet>>>,
2069 ) -> anyhow::Result<()> {
2070 let mut daemon = self.inner.daemon.lock();
2071 if daemon.is_some() {
2072 warn!("Called Wallet::start_daemon while daemon was already running.");
2073 return Ok(());
2074 }
2075
2076 let handle = crate::daemon::start_daemon(self.clone(), onchain);
2079 let _ = daemon.insert(handle);
2080
2081 Ok(())
2082 }
2083
2084 #[deprecated(since = "0.1.4", note = "use start_daemon instead")]
2086 pub fn run_daemon(
2087 &self,
2088 onchain: Option<Arc<tokio::sync::RwLock<dyn DaemonizableOnchainWallet>>>,
2089 ) -> anyhow::Result<()> {
2090 self.start_daemon(onchain)
2091 }
2092
2093 pub fn stop_daemon(&self) {
2095 let mut daemon = self.inner.daemon.lock();
2096 if let Some(handle) = daemon.take() {
2097 handle.stop();
2098 }
2099 }
2100
2101 pub async fn register_vtxo_transactions_with_server(
2105 &self,
2106 vtxos: &[impl AsRef<Vtxo<Full>>],
2107 ) -> anyhow::Result<()> {
2108 if vtxos.is_empty() {
2109 return Ok(());
2110 }
2111
2112 let (mut srv, _) = self.require_server().await?;
2113 srv.client.register_vtxo_transactions(protos::RegisterVtxoTransactionsRequest {
2114 vtxos: vtxos.iter().map(|v| v.as_ref().serialize()).collect(),
2115 }).await.context("failed to register vtxo transactions")?;
2116
2117 Ok(())
2118 }
2119}
2120
2121fn wrap_server_connect_error(err: ConnectError) -> anyhow::Error {
2122 match err {
2123 ConnectError::CreateEndpoint(CreateEndpointError::NoTransportBackend) => {
2124 anyhow!(MISSING_SERVER_TRANSPORT_HELP)
2125 },
2126 other => anyhow::Error::from(other),
2127 }
2128}
2129
2130impl std::ops::Drop for WalletInner {
2131 fn drop(&mut self) {
2132 if let Some(handle) = self.daemon.lock().take() {
2133 handle.stop();
2134 }
2135 }
2136}
2137
2138#[cfg(test)]
2139mod tests {
2140 use server_rpc::client::CreateEndpointError;
2141
2142 use super::{wrap_server_connect_error, MISSING_SERVER_TRANSPORT_HELP};
2143
2144 #[test]
2145 fn no_transport_connect_error_is_reworded_for_wallet_users() {
2146 let err = wrap_server_connect_error(CreateEndpointError::NoTransportBackend.into());
2147 assert!(err.to_string().contains(MISSING_SERVER_TRANSPORT_HELP));
2148 assert!(err.to_string().contains("feature `bark-wallet/native` or `bark-wallet/wasm-web`"));
2149 }
2150}