1#[cfg(all(any(target_os = "android", target_os = "ios"), feature = "tls-native-roots"))]
285compile_error!("feature `tls-native-roots` can't be used on Android or iOS, use `tls-webpki-roots` instead");
286
287pub extern crate ark;
288
289pub extern crate bip39;
290pub extern crate lightning_invoice;
291pub extern crate lnurl as lnurllib;
292
293#[macro_use] extern crate anyhow;
294#[macro_use] extern crate async_trait;
295#[macro_use] extern crate serde;
296
297pub mod chain;
298pub mod exit;
299pub mod movement;
300pub mod onchain;
301pub mod persist;
302pub mod round;
303pub mod subsystem;
304pub mod vtxo;
305
306#[cfg(feature = "pid-lock")]
307pub mod pid_lock;
308
309mod arkoor;
310mod board;
311mod config;
312mod daemon;
313mod fees;
314mod lightning;
315mod mailbox;
316mod notification;
317mod offboard;
318#[cfg(feature = "socks5-proxy")]
319mod proxy;
320mod psbtext;
321mod utils;
322
323pub use self::arkoor::ArkoorCreateResult;
324pub use self::config::{BarkNetwork, Config};
325pub use self::daemon::DaemonHandle;
326pub use self::fees::FeeEstimate;
327pub use self::notification::{WalletNotification, NotificationStream};
328pub use self::vtxo::WalletVtxo;
329
330use std::collections::HashSet;
331use std::sync::Arc;
332
333use anyhow::{bail, Context};
334use bip39::Mnemonic;
335use bitcoin::{Amount, Network, OutPoint};
336use bitcoin::bip32::{self, ChildNumber, Fingerprint};
337use bitcoin::secp256k1::{self, Keypair, PublicKey};
338use log::{trace, info, warn, error};
339use tokio::sync::{Mutex, RwLock};
340
341use ark::lightning::PaymentHash;
342
343use ark::{ArkInfo, ProtocolEncoding, Vtxo, VtxoId, VtxoPolicy, VtxoRequest};
344use ark::address::VtxoDelivery;
345use ark::fees::{validate_and_subtract_fee_min_dust, VtxoFeeInfo};
346use ark::vtxo::{Full, PubkeyVtxoPolicy, VtxoRef};
347use ark::vtxo::policy::signing::VtxoSigner;
348use bitcoin_ext::{BlockHeight, P2TR_DUST, TxStatus};
349use server_rpc::{protos, ServerConnection};
350use server_rpc::client::{ConnectError, CreateEndpointError};
351
352use crate::chain::{ChainSource, ChainSourceSpec};
353use crate::exit::Exit;
354use crate::movement::{Movement, MovementStatus, PaymentMethod};
355use crate::movement::manager::MovementManager;
356use crate::movement::update::MovementUpdate;
357use crate::notification::NotificationDispatch;
358use crate::onchain::{ExitUnilaterally, PreparePsbt, SignPsbt, Utxo};
359use crate::onchain::DaemonizableOnchainWallet;
360use crate::persist::BarkPersister;
361use crate::persist::models::{PendingOffboard, RoundStateId, StoredRoundState, Unlocked};
362#[cfg(feature = "socks5-proxy")]
363use crate::proxy::proxy_for_url;
364use crate::round::{RoundParticipation, RoundStateLockIndex, RoundStatus};
365use crate::subsystem::{ArkoorMovement, RoundMovement};
366use crate::vtxo::{FilterVtxos, RefreshStrategy, VtxoFilter, VtxoState, VtxoStateKind};
367
368#[cfg(all(feature = "wasm-web", feature = "socks5-proxy"))]
369compile_error!("features `wasm-web` does not support feature `socks5-proxy");
370
371const BARK_PURPOSE_INDEX: u32 = 350;
373const VTXO_KEYS_INDEX: u32 = 0;
375const MAILBOX_KEY_INDEX: u32 = 1;
377const RECOVERY_MAILBOX_KEY_INDEX: u32 = 2;
379const MISSING_SERVER_TRANSPORT_HELP: &str =
380 "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.";
381
382lazy_static::lazy_static! {
383 static ref SECP: secp256k1::Secp256k1<secp256k1::All> = secp256k1::Secp256k1::new();
385}
386
387fn log_server_pubkey_changed_error(expected: PublicKey, got: PublicKey) {
393 error!(
394 "
395Server public key has changed!
396
397The Ark server's public key is different from the one stored when this
398wallet was created. This typically happens when:
399
400 - The server operator has rotated their keys
401 - You are connecting to a different server
402 - The server has been replaced
403
404For safety, this wallet will not connect to the server until you
405resolve this. You can recover your funds on-chain by doing an emergency exit.
406
407This will exit your VTXOs to on-chain Bitcoin without needing the server's cooperation.
408
409Expected: {expected}
410Got: {got}")
411}
412
413fn log_server_mailbox_pubkey_changed_error(expected: PublicKey, got: PublicKey) {
415 error!(
416 "
417Server mailbox public key has changed!
418
419The Ark server's mailbox public key is different from the one stored when this
420wallet was created. This typically happens when:
421
422 - The server operator has rotated their keys
423 - You are connecting to a different server
424 - The server has been replaced
425
426For safety, this wallet will not connect to the server until you resolve this.
427
428Unlike a server pubkey change, your VTXOs are not at risk - the mailbox pubkey
429only affects address receive semantics. Any Ark addresses you previously
430shared will stop receiving new payments; you will need to share new addresses
431after reconnecting.
432
433Expected: {expected}
434Got: {got}")
435}
436
437#[derive(Debug, Clone)]
439pub struct LightningReceiveBalance {
440 pub total: Amount,
442 pub claimable: Amount,
444}
445
446#[derive(Debug, Clone)]
448pub struct Balance {
449 pub spendable: Amount,
451 pub pending_lightning_send: Amount,
453 pub claimable_lightning_receive: Amount,
455 pub pending_in_round: Amount,
457 pub pending_exit: Option<Amount>,
460 pub pending_board: Amount,
462}
463
464pub struct UtxoInfo {
465 pub outpoint: OutPoint,
466 pub amount: Amount,
467 pub confirmation_height: Option<u32>,
468}
469
470impl From<Utxo> for UtxoInfo {
471 fn from(value: Utxo) -> Self {
472 match value {
473 Utxo::Local(o) => UtxoInfo {
474 outpoint: o.outpoint,
475 amount: o.amount,
476 confirmation_height: o.confirmation_height,
477 },
478 Utxo::Exit(e) => UtxoInfo {
479 outpoint: e.vtxo.point(),
480 amount: e.vtxo.amount(),
481 confirmation_height: Some(e.height),
482 },
483 }
484 }
485}
486
487pub struct OffchainBalance {
490 pub available: Amount,
492 pub pending_in_round: Amount,
494 pub pending_exit: Amount,
497}
498
499#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
501pub struct WalletProperties {
502 pub network: Network,
506
507 pub fingerprint: Fingerprint,
511
512 pub server_pubkey: Option<PublicKey>,
519
520 pub server_mailbox_pubkey: Option<PublicKey>,
528}
529
530pub struct WalletSeed {
536 master: bip32::Xpriv,
537 vtxo: bip32::Xpriv,
538}
539
540impl WalletSeed {
541 fn new(network: Network, seed: &[u8; 64]) -> Self {
542 let bark_path = [ChildNumber::from_hardened_idx(BARK_PURPOSE_INDEX).unwrap()];
543 let master = bip32::Xpriv::new_master(network, seed)
544 .expect("invalid seed")
545 .derive_priv(&SECP, &bark_path)
546 .expect("purpose is valid");
547
548 let vtxo_path = [ChildNumber::from_hardened_idx(VTXO_KEYS_INDEX).unwrap()];
549 let vtxo = master.derive_priv(&SECP, &vtxo_path)
550 .expect("vtxo path is valid");
551
552 Self { master, vtxo }
553 }
554
555 fn fingerprint(&self) -> Fingerprint {
556 self.master.fingerprint(&SECP)
557 }
558
559 fn derive_vtxo_keypair(&self, idx: u32) -> Keypair {
560 self.vtxo.derive_priv(&SECP, &[idx.into()]).unwrap().to_keypair(&SECP)
561 }
562
563 fn to_mailbox_keypair(&self) -> Keypair {
564 let mailbox_path = [ChildNumber::from_hardened_idx(MAILBOX_KEY_INDEX).unwrap()];
565 self.master.derive_priv(&SECP, &mailbox_path).unwrap().to_keypair(&SECP)
566 }
567
568 fn to_recovery_mailbox_keypair(&self) -> Keypair {
569 let mailbox_path = [ChildNumber::from_hardened_idx(RECOVERY_MAILBOX_KEY_INDEX).unwrap()];
570 self.master.derive_priv(&SECP, &mailbox_path).unwrap().to_keypair(&SECP)
571 }
572}
573
574pub struct Wallet {
707 pub chain: Arc<ChainSource>,
709
710 pub exit: RwLock<Exit>,
712
713 pub movements: Arc<MovementManager>,
715
716 notifications: NotificationDispatch,
718
719 config: Config,
721
722 db: Arc<dyn BarkPersister>,
724
725 seed: WalletSeed,
727
728 server: parking_lot::RwLock<Option<ServerConnection>>,
730
731 inflight_lightning_payments: Mutex<HashSet<PaymentHash>>,
734
735 round_state_lock_index: RoundStateLockIndex,
737
738 daemon: parking_lot::Mutex<Option<DaemonHandle>>,
740}
741
742impl Wallet {
743 pub fn chain_source(
746 config: &Config,
747 ) -> anyhow::Result<ChainSourceSpec> {
748 if let Some(ref url) = config.esplora_address {
749 Ok(ChainSourceSpec::Esplora {
750 url: url.clone(),
751 })
752 } else if let Some(ref url) = config.bitcoind_address {
753 let auth = if let Some(ref c) = config.bitcoind_cookiefile {
754 bitcoin_ext::rpc::Auth::CookieFile(c.clone())
755 } else {
756 bitcoin_ext::rpc::Auth::UserPass(
757 config.bitcoind_user.clone().context("need bitcoind auth config")?,
758 config.bitcoind_pass.clone().context("need bitcoind auth config")?,
759 )
760 };
761 Ok(ChainSourceSpec::Bitcoind {
762 url: url.clone(),
763 auth,
764 })
765 } else {
766 bail!("Need to either provide esplora or bitcoind info");
767 }
768 }
769
770 pub fn require_chainsource_version(&self) -> anyhow::Result<()> {
774 self.chain.require_version()
775 }
776
777 pub async fn network(&self) -> anyhow::Result<Network> {
778 Ok(self.properties().await?.network)
779 }
780
781 pub async fn peek_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
784 let last_revealed = self.db.get_last_vtxo_key_index().await?;
785
786 let index = last_revealed.map(|i| i + 1).unwrap_or(u32::MIN);
787 let keypair = self.seed.derive_vtxo_keypair(index);
788
789 Ok((keypair, index))
790 }
791
792 pub async fn derive_store_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
795 let (keypair, index) = self.peek_next_keypair().await?;
796 self.db.store_vtxo_key(index, keypair.public_key()).await?;
797 Ok((keypair, index))
798 }
799
800 #[deprecated(note = "use peek_keypair instead")]
801 pub async fn peak_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
802 self.peek_keypair(index).await
803 }
804
805 pub async fn peek_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
819 let keypair = self.seed.derive_vtxo_keypair(index);
820 if self.db.get_public_key_idx(&keypair.public_key()).await?.is_some() {
821 Ok(keypair)
822 } else {
823 bail!("VTXO key {} does not exist, please derive it first", index)
824 }
825 }
826
827
828 pub async fn pubkey_keypair(&self, public_key: &PublicKey) -> anyhow::Result<Option<(u32, Keypair)>> {
840 if let Some(index) = self.db.get_public_key_idx(&public_key).await? {
841 Ok(Some((index, self.seed.derive_vtxo_keypair(index))))
842 } else {
843 Ok(None)
844 }
845 }
846
847 pub async fn get_vtxo_key(&self, vtxo: impl VtxoRef) -> anyhow::Result<Keypair> {
858 let wallet_vtxo = self.get_vtxo_by_id(vtxo.vtxo_id()).await?;
859 let pubkey = self.find_signable_clause(&wallet_vtxo.vtxo).await
860 .context("VTXO is not signable by wallet")?
861 .pubkey();
862 let idx = self.db.get_public_key_idx(&pubkey).await?
863 .context("VTXO key not found")?;
864 Ok(self.seed.derive_vtxo_keypair(idx))
865 }
866
867 #[deprecated(note = "use peek_address instead")]
868 pub async fn peak_address(&self, index: u32) -> anyhow::Result<ark::Address> {
869 self.peek_address(index).await
870 }
871
872 pub async fn peek_address(&self, index: u32) -> anyhow::Result<ark::Address> {
876 let properties = self.properties().await?;
877 let network = properties.network;
878 let keypair = self.peek_keypair(index).await?;
879 let mailbox = self.mailbox_identifier();
880
881 let (server_pubkey, mailbox_pubkey) =
882 if let (Some(spk), Some(mpk)) = (properties.server_pubkey, properties.server_mailbox_pubkey) {
883 (spk, mpk)
884 } else {
885 let (_, ark_info) = self.require_server().await?;
886 (ark_info.server_pubkey, ark_info.mailbox_pubkey)
887 };
888
889 Ok(ark::Address::builder()
890 .testnet(network != bitcoin::Network::Bitcoin)
891 .server_pubkey(server_pubkey)
892 .pubkey_policy(keypair.public_key())
893 .mailbox(mailbox_pubkey, mailbox, &keypair)
894 .expect("Failed to assign mailbox")
895 .into_address().unwrap())
896 }
897
898 pub async fn new_address_with_index(&self) -> anyhow::Result<(ark::Address, u32)> {
902 let (_, index) = self.derive_store_next_keypair().await?;
903 let addr = self.peek_address(index).await?;
904 Ok((addr, index))
905 }
906
907 pub async fn new_address(&self) -> anyhow::Result<ark::Address> {
909 let (addr, _) = self.new_address_with_index().await?;
910 Ok(addr)
911 }
912
913 pub async fn create(
919 mnemonic: &Mnemonic,
920 network: Network,
921 config: Config,
922 db: Arc<dyn BarkPersister>,
923 force: bool,
924 ) -> anyhow::Result<Wallet> {
925 trace!("Config: {:?}", config);
926 if let Some(existing) = db.read_properties().await? {
927 trace!("Existing config: {:?}", existing);
928 bail!("cannot overwrite already existing config")
929 }
930
931 let (server_pubkey, mailbox_pubkey) = if !force {
933 match Self::connect_to_server(&config, network).await {
934 Ok(conn) => {
935 let ark_info = conn.ark_info().await?;
936 (Some(ark_info.server_pubkey), Some(ark_info.mailbox_pubkey))
937 }
938 Err(err) => {
939 bail!("Failed to connect to provided server (if you are sure use the --force flag): {:#}", err);
940 }
941 }
942 } else {
943 (None, None)
944 };
945
946 let wallet_fingerprint = WalletSeed::new(network, &mnemonic.to_seed("")).fingerprint();
947 let properties = WalletProperties {
948 network,
949 fingerprint: wallet_fingerprint,
950 server_pubkey,
951 server_mailbox_pubkey: mailbox_pubkey,
952 };
953
954 db.init_wallet(&properties).await.context("cannot init wallet in the database")?;
956 info!("Created wallet with fingerprint: {}", wallet_fingerprint);
957 if let Some(pk) = server_pubkey {
958 info!("Stored server pubkey: {}", pk);
959 }
960
961 let wallet = Wallet::open(&mnemonic, db, config).await.context("failed to open wallet")?;
963 wallet.require_chainsource_version()?;
964
965 Ok(wallet)
966 }
967
968 pub async fn create_with_onchain(
976 mnemonic: &Mnemonic,
977 network: Network,
978 config: Config,
979 db: Arc<dyn BarkPersister>,
980 onchain: &dyn ExitUnilaterally,
981 force: bool,
982 ) -> anyhow::Result<Wallet> {
983 let mut wallet = Wallet::create(mnemonic, network, config, db, force).await?;
984 wallet.exit.get_mut().load(onchain).await?;
985 Ok(wallet)
986 }
987
988 pub async fn open(
990 mnemonic: &Mnemonic,
991 db: Arc<dyn BarkPersister>,
992 config: Config,
993 ) -> anyhow::Result<Wallet> {
994 let properties = db.read_properties().await?.context("Wallet is not initialised")?;
995
996 let seed = {
997 let seed = mnemonic.to_seed("");
998 WalletSeed::new(properties.network, &seed)
999 };
1000
1001 if properties.fingerprint != seed.fingerprint() {
1002 bail!("incorrect mnemonic")
1003 }
1004
1005 let chain_source = if let Some(ref url) = config.esplora_address {
1006 ChainSourceSpec::Esplora {
1007 url: url.clone(),
1008 }
1009 } else if let Some(ref url) = config.bitcoind_address {
1010 let auth = if let Some(ref c) = config.bitcoind_cookiefile {
1011 bitcoin_ext::rpc::Auth::CookieFile(c.clone())
1012 } else {
1013 bitcoin_ext::rpc::Auth::UserPass(
1014 config.bitcoind_user.clone().context("need bitcoind auth config")?,
1015 config.bitcoind_pass.clone().context("need bitcoind auth config")?,
1016 )
1017 };
1018 ChainSourceSpec::Bitcoind { url: url.clone(), auth }
1019 } else {
1020 bail!("Need to either provide esplora or bitcoind info");
1021 };
1022
1023 #[cfg(feature = "socks5-proxy")]
1024 let chain_proxy = proxy_for_url(&config.socks5_proxy, chain_source.url())?;
1025 let chain_source_client = ChainSource::new(
1026 chain_source, properties.network, config.fallback_fee_rate,
1027 #[cfg(feature = "socks5-proxy")] chain_proxy.as_deref(),
1028 ).await?;
1029 let chain = Arc::new(chain_source_client);
1030
1031 let server = parking_lot::RwLock::new(None);
1032
1033 let notifications = NotificationDispatch::new();
1034 let movements = Arc::new(MovementManager::new(db.clone(), notifications.clone()));
1035 let exit = RwLock::new(Exit::new(db.clone(), chain.clone(), movements.clone()).await?);
1036
1037 Ok(Wallet {
1038 config, db, seed, exit, movements, notifications, server, chain,
1039 inflight_lightning_payments: Mutex::new(HashSet::new()),
1040 round_state_lock_index: RoundStateLockIndex::new(),
1041 daemon: parking_lot::Mutex::new(None),
1042 })
1043 }
1044
1045 pub async fn open_with_onchain(
1048 mnemonic: &Mnemonic,
1049 db: Arc<dyn BarkPersister>,
1050 onchain: &dyn ExitUnilaterally,
1051 cfg: Config,
1052 ) -> anyhow::Result<Wallet> {
1053 let mut wallet = Wallet::open(mnemonic, db, cfg).await?;
1054 wallet.exit.get_mut().load(onchain).await?;
1055 Ok(wallet)
1056 }
1057
1058 pub async fn open_with_daemon(
1061 mnemonic: &Mnemonic,
1062 db: Arc<dyn BarkPersister>,
1063 cfg: Config,
1064 onchain: Option<Arc<RwLock<dyn DaemonizableOnchainWallet>>>,
1065 ) -> anyhow::Result<Arc<Wallet>> {
1066 let wallet = Arc::new(Wallet::open(mnemonic, db, cfg).await?);
1067 if let Some(onchain) = onchain.as_ref() {
1068 let mut onchain = onchain.write().await;
1069 wallet.exit.write().await.load(&mut *onchain).await?;
1070 }
1071
1072 wallet.clone().start_daemon(onchain)?;
1073
1074 Ok(wallet)
1075 }
1076
1077 pub fn config(&self) -> &Config {
1079 &self.config
1080 }
1081
1082 pub async fn properties(&self) -> anyhow::Result<WalletProperties> {
1084 let properties = self.db.read_properties().await?.context("Wallet is not initialised")?;
1085 Ok(properties)
1086 }
1087
1088 pub fn fingerprint(&self) -> Fingerprint {
1090 self.seed.fingerprint()
1091 }
1092
1093 async fn connect_to_server(
1094 config: &Config,
1095 network: Network,
1096 ) -> anyhow::Result<ServerConnection> {
1097 let mut builder = ServerConnection::builder()
1098 .address(&config.server_address)
1099 .network(network);
1100
1101 #[cfg(feature = "socks5-proxy")]
1102 if let Some(proxy) = proxy_for_url(&config.socks5_proxy, &config.server_address)? {
1103 builder = builder.proxy(&proxy)
1104 }
1105
1106 if let Some(ref token) = config.server_access_token {
1107 builder = builder.access_token(token);
1108 }
1109
1110 builder.connect().await.map_err(wrap_server_connect_error)
1111 .context("Failed to connect to Ark server")
1112 }
1113
1114 async fn require_server(&self) -> anyhow::Result<(ServerConnection, ArkInfo)> {
1115 if self.server.read().is_none() {
1117 let network = self.properties().await?.network;
1118 let conn = Self::connect_to_server(&self.config, network).await
1119 .context("You should be connected to Ark server to perform this action")?;
1120 let _ = self.server.write().insert(conn);
1121 }
1122
1123 let conn = self.server.read().clone()
1124 .context("You should be connected to Ark server to perform this action")?;
1125 let ark_info = conn.ark_info().await?;
1126
1127 self.check_and_store_server_keys(&ark_info).await?;
1128
1129 Ok((conn, ark_info))
1130 }
1131
1132 pub async fn refresh_server(&self) -> anyhow::Result<()> {
1133 let server = self.server.read().clone();
1134
1135 let srv = if let Some(srv) = server {
1136 srv.check_connection().await?;
1137 let ark_info = srv.ark_info().await?;
1138 ark_info.fees.validate().context("invalid fee schedule")?;
1139 self.check_and_store_server_keys(&ark_info).await?;
1140 srv
1141 } else {
1142 let properties = self.properties().await?;
1143 let conn = Self::connect_to_server(&self.config, properties.network).await?;
1144 let ark_info = conn.ark_info().await?;
1145 ark_info.fees.validate().context("invalid fee schedule")?;
1146 self.check_and_store_server_keys(&ark_info).await?;
1147 conn
1148 };
1149
1150 let _ = self.server.write().insert(srv);
1151
1152 Ok(())
1153 }
1154
1155 async fn check_and_store_server_keys(&self, ark_info: &ArkInfo) -> anyhow::Result<()> {
1162 let properties = self.properties().await?;
1163
1164 if let Some(stored_pubkey) = properties.server_pubkey {
1165 if stored_pubkey != ark_info.server_pubkey {
1166 log_server_pubkey_changed_error(stored_pubkey, ark_info.server_pubkey);
1167 bail!("Server public key has changed. You should exit all your VTXOs!");
1168 }
1169 } else {
1170 self.db.set_server_pubkey(ark_info.server_pubkey).await?;
1171 info!("Stored server pubkey for existing wallet: {}", ark_info.server_pubkey);
1172 }
1173
1174 if let Some(stored_mailbox_pubkey) = properties.server_mailbox_pubkey {
1175 if stored_mailbox_pubkey != ark_info.mailbox_pubkey {
1176 log_server_mailbox_pubkey_changed_error(stored_mailbox_pubkey, ark_info.mailbox_pubkey);
1177 bail!("Server mailbox public key has changed.");
1178 }
1179 } else {
1180 self.db.set_server_mailbox_pubkey(ark_info.mailbox_pubkey).await?;
1181 info!("Stored server mailbox pubkey for existing wallet: {}", ark_info.mailbox_pubkey);
1182 }
1183
1184 Ok(())
1185 }
1186
1187 pub async fn ark_info(&self) -> anyhow::Result<Option<ArkInfo>> {
1189 let server = self.server.read().clone();
1190 match server.as_ref() {
1191 Some(srv) => Ok(Some(srv.ark_info().await?)),
1192 _ => Ok(None),
1193 }
1194 }
1195
1196 pub async fn require_ark_info(&self) -> anyhow::Result<ArkInfo> {
1202 let (_, ark_info) = self.require_server().await?;
1203 Ok(ark_info)
1204 }
1205
1206 pub async fn balance(&self) -> anyhow::Result<Balance> {
1210 let vtxos = self.vtxos().await?;
1211
1212 let spendable = {
1213 let mut v = vtxos.iter().collect();
1214 VtxoStateKind::Spendable.filter_vtxos(&mut v).await?;
1215 v.into_iter().map(|v| v.amount()).sum::<Amount>()
1216 };
1217
1218 let pending_lightning_send = self.pending_lightning_send_vtxos().await?.iter()
1219 .map(|v| v.amount())
1220 .sum::<Amount>();
1221
1222 let claimable_lightning_receive = self.claimable_lightning_receive_balance().await?;
1223
1224 let pending_board = self.pending_board_vtxos().await?.iter()
1225 .map(|v| v.amount())
1226 .sum::<Amount>();
1227
1228 let pending_in_round = self.pending_round_balance().await?;
1229
1230 let pending_exit = self.exit.try_read().ok().map(|e| e.pending_total());
1231
1232 Ok(Balance {
1233 spendable,
1234 pending_in_round,
1235 pending_lightning_send,
1236 claimable_lightning_receive,
1237 pending_exit,
1238 pending_board,
1239 })
1240 }
1241
1242 pub async fn validate_vtxo(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<()> {
1244 let tx = self.chain.get_tx(&vtxo.chain_anchor().txid).await
1245 .context("could not fetch chain tx")?;
1246
1247 let tx = tx.with_context(|| {
1248 format!("vtxo chain anchor not found for vtxo: {}", vtxo.chain_anchor().txid)
1249 })?;
1250
1251 vtxo.validate(&tx)?;
1252
1253 Ok(())
1254 }
1255
1256 pub async fn import_vtxo(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<()> {
1266 if self.db.get_wallet_vtxo(vtxo.id()).await?.is_some() {
1267 info!("VTXO {} already exists in wallet, skipping import", vtxo.id());
1268 return Ok(());
1269 }
1270
1271 self.validate_vtxo(vtxo).await.context("VTXO validation failed")?;
1272
1273 if self.find_signable_clause(vtxo).await.is_none() {
1274 bail!("VTXO {} is not owned by this wallet (no signable clause found)", vtxo.id());
1275 }
1276
1277 let current_height = self.chain.tip().await?;
1278 if vtxo.expiry_height() <= current_height {
1279 bail!("Vtxo {} has expired", vtxo.id());
1280 }
1281
1282 self.store_spendable_vtxos([vtxo]).await.context("failed to store imported VTXO")?;
1283
1284 info!("Successfully imported VTXO {}", vtxo.id());
1285 Ok(())
1286 }
1287
1288 pub async fn get_vtxo_by_id(&self, vtxo_id: VtxoId) -> anyhow::Result<WalletVtxo> {
1290 let vtxo = self.db.get_wallet_vtxo(vtxo_id).await
1291 .with_context(|| format!("Error when querying vtxo {} in database", vtxo_id))?
1292 .with_context(|| format!("The VTXO with id {} cannot be found", vtxo_id))?;
1293 Ok(vtxo)
1294 }
1295
1296 #[deprecated(since="0.1.0-beta.5", note = "Use Wallet::history instead")]
1298 pub async fn movements(&self) -> anyhow::Result<Vec<Movement>> {
1299 self.history().await
1300 }
1301
1302 pub async fn history(&self) -> anyhow::Result<Vec<Movement>> {
1304 Ok(self.db.get_all_movements().await?)
1305 }
1306
1307 pub async fn history_by_payment_method(
1309 &self,
1310 payment_method: &PaymentMethod,
1311 ) -> anyhow::Result<Vec<Movement>> {
1312 let mut ret = self.db.get_movements_by_payment_method(payment_method).await?;
1313 ret.sort_by_key(|m| m.id);
1314 Ok(ret)
1315 }
1316
1317 pub async fn all_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1319 Ok(self.db.get_all_vtxos().await?)
1320 }
1321
1322 pub async fn vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1324 Ok(self.db.get_vtxos_by_state(&VtxoStateKind::UNSPENT_STATES).await?)
1325 }
1326
1327 pub async fn vtxos_with(&self, filter: &impl FilterVtxos) -> anyhow::Result<Vec<WalletVtxo>> {
1329 let mut vtxos = self.vtxos().await?;
1330 filter.filter_vtxos(&mut vtxos).await.context("error filtering vtxos")?;
1331 Ok(vtxos)
1332 }
1333
1334 pub async fn spendable_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1336 Ok(self.vtxos_with(&VtxoStateKind::Spendable).await?)
1337 }
1338
1339 pub async fn spendable_vtxos_with(
1341 &self,
1342 filter: &impl FilterVtxos,
1343 ) -> anyhow::Result<Vec<WalletVtxo>> {
1344 let mut vtxos = self.spendable_vtxos().await?;
1345 filter.filter_vtxos(&mut vtxos).await.context("error filtering vtxos")?;
1346 Ok(vtxos)
1347 }
1348
1349 pub async fn get_expiring_vtxos(
1351 &self,
1352 threshold: BlockHeight,
1353 ) -> anyhow::Result<Vec<WalletVtxo>> {
1354 let expiry = self.chain.tip().await? + threshold;
1355 let filter = VtxoFilter::new(&self).expires_before(expiry);
1356 Ok(self.spendable_vtxos_with(&filter).await?)
1357 }
1358
1359 pub async fn sync_pending_offboards(&self) -> anyhow::Result<()> {
1365 let pending_offboards: Vec<PendingOffboard> = self.db.get_pending_offboards().await?;
1366
1367 if pending_offboards.is_empty() {
1368 return Ok(());
1369 }
1370
1371 let current_height = self.chain.tip().await?;
1372 let required_confs = self.config.offboard_required_confirmations;
1373
1374 trace!("Checking {} pending offboard transaction(s)", pending_offboards.len());
1375
1376 for pending in pending_offboards {
1377 let status = self.chain.tx_status(pending.offboard_txid).await;
1378
1379 match status {
1380 Ok(TxStatus::Confirmed(block_ref)) => {
1381 let confs = current_height - (block_ref.height - 1);
1382 if confs < required_confs as BlockHeight {
1383 trace!(
1384 "Offboard tx {} has {}/{} confirmations, waiting...",
1385 pending.offboard_txid, confs, required_confs,
1386 );
1387 continue;
1388 }
1389
1390 info!(
1391 "Offboard tx {} confirmed, finalizing movement {}",
1392 pending.offboard_txid, pending.movement_id,
1393 );
1394
1395 for vtxo_id in &pending.vtxo_ids {
1397 if let Err(e) = self.db.update_vtxo_state_checked(
1398 *vtxo_id,
1399 VtxoState::Spent,
1400 &[VtxoStateKind::Locked],
1401 ).await {
1402 warn!("Failed to mark vtxo {} as spent: {:#}", vtxo_id, e);
1403 }
1404 }
1405
1406 if let Err(e) = self.movements.finish_movement(
1408 pending.movement_id,
1409 MovementStatus::Successful,
1410 ).await {
1411 warn!("Failed to finish movement {}: {:#}", pending.movement_id, e);
1412 }
1413
1414 self.db.remove_pending_offboard(pending.movement_id).await?;
1415 }
1416 Ok(TxStatus::Mempool) => {
1417 if required_confs == 0 {
1418 info!(
1419 "Offboard tx {} in mempool with 0 required confirmations, \
1420 finalizing movement {}",
1421 pending.offboard_txid, pending.movement_id,
1422 );
1423
1424 for vtxo_id in &pending.vtxo_ids {
1426 if let Err(e) = self.db.update_vtxo_state_checked(
1427 *vtxo_id,
1428 VtxoState::Spent,
1429 &[VtxoStateKind::Locked],
1430 ).await {
1431 warn!("Failed to mark vtxo {} as spent: {:#}", vtxo_id, e);
1432 }
1433 }
1434
1435 if let Err(e) = self.movements.finish_movement(
1437 pending.movement_id,
1438 MovementStatus::Successful,
1439 ).await {
1440 warn!("Failed to finish movement {}: {:#}", pending.movement_id, e);
1441 }
1442
1443 self.db.remove_pending_offboard(pending.movement_id).await?;
1444 } else {
1445 trace!(
1446 "Offboard tx {} still in mempool, waiting...",
1447 pending.offboard_txid,
1448 );
1449 }
1450 }
1451 Ok(TxStatus::NotFound) => {
1452 let age = chrono::Local::now() - pending.created_at;
1456 if age < chrono::Duration::hours(1) {
1457 trace!(
1458 "Offboard tx {} not found, but only {} minutes old — waiting...",
1459 pending.offboard_txid, age.num_minutes(),
1460 );
1461 continue;
1462 }
1463
1464 warn!(
1465 "Offboard tx {} not found after {} minutes, canceling movement {}",
1466 pending.offboard_txid, age.num_minutes(), pending.movement_id,
1467 );
1468
1469 for vtxo_id in &pending.vtxo_ids {
1471 if let Err(e) = self.db.update_vtxo_state_checked(
1472 *vtxo_id,
1473 VtxoState::Spendable,
1474 &[VtxoStateKind::Locked],
1475 ).await {
1476 warn!("Failed to restore vtxo {} to spendable: {:#}", vtxo_id, e);
1477 }
1478 }
1479
1480 if let Err(e) = self.movements.finish_movement(
1482 pending.movement_id,
1483 MovementStatus::Failed,
1484 ).await {
1485 warn!("Failed to fail movement {}: {:#}", pending.movement_id, e);
1486 }
1487
1488 self.db.remove_pending_offboard(pending.movement_id).await?;
1489 }
1490 Err(e) => {
1491 warn!(
1492 "Failed to check status of offboard tx {}: {:#}",
1493 pending.offboard_txid, e,
1494 );
1495 }
1496 }
1497 }
1498
1499 Ok(())
1500 }
1501
1502 pub async fn maintenance(&self) -> anyhow::Result<()> {
1508 info!("Starting wallet maintenance in interactive mode");
1509 self.sync().await;
1510
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.maintenance_refresh().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!("Maintenance encountered errors.\nprogress_rounds: {:#?}\nrefresh: {:#?}", rounds, refresh);
1521 }
1522 Ok(())
1523 }
1524
1525 pub async fn maintenance_delegated(&self) -> anyhow::Result<()> {
1531 info!("Starting wallet maintenance in delegated mode");
1532 self.sync().await;
1533 let rounds = self.progress_pending_rounds(None).await;
1534 if let Err(e) = rounds.as_ref() {
1535 warn!("Error progressing pending rounds: {:#}", e);
1536 }
1537 let refresh = self.maybe_schedule_maintenance_refresh_delegated().await;
1538 if let Err(e) = refresh.as_ref() {
1539 warn!("Error refreshing VTXOs: {:#}", e);
1540 }
1541 if rounds.is_err() || refresh.is_err() {
1542 bail!("Delegated maintenance encountered errors.\nprogress_rounds: {:#?}\nrefresh: {:#?}", rounds, refresh);
1543 }
1544 Ok(())
1545 }
1546
1547 pub async fn maintenance_with_onchain<W: PreparePsbt + SignPsbt + ExitUnilaterally>(
1555 &self,
1556 onchain: &mut W,
1557 ) -> anyhow::Result<()> {
1558 info!("Starting wallet maintenance in interactive mode with onchain wallet");
1559
1560 let maintenance = self.maintenance().await;
1562
1563 let exit_sync = self.sync_exits(onchain).await;
1565 if let Err(e) = exit_sync.as_ref() {
1566 warn!("Error syncing exits: {:#}", e);
1567 }
1568 let exit_progress = self.exit.write().await.progress_exits(&self, onchain, None).await;
1569 if let Err(e) = exit_progress.as_ref() {
1570 warn!("Error progressing exits: {:#}", e);
1571 }
1572 if maintenance.is_err() || exit_sync.is_err() || exit_progress.is_err() {
1573 bail!("Maintenance encountered errors.\nmaintenance: {:#?}\nexit_sync: {:#?}\nexit_progress: {:#?}", maintenance, exit_sync, exit_progress);
1574 }
1575 Ok(())
1576 }
1577
1578 pub async fn maintenance_with_onchain_delegated<W: PreparePsbt + SignPsbt + ExitUnilaterally>(
1585 &self,
1586 onchain: &mut W,
1587 ) -> anyhow::Result<()> {
1588 info!("Starting wallet maintenance in delegated mode with onchain wallet");
1589
1590 let maintenance = self.maintenance_delegated().await;
1592
1593 let exit_sync = self.sync_exits(onchain).await;
1595 if let Err(e) = exit_sync.as_ref() {
1596 warn!("Error syncing exits: {:#}", e);
1597 }
1598 let exit_progress = self.exit.write().await.progress_exits(&self, onchain, None).await;
1599 if let Err(e) = exit_progress.as_ref() {
1600 warn!("Error progressing exits: {:#}", e);
1601 }
1602 if maintenance.is_err() || exit_sync.is_err() || exit_progress.is_err() {
1603 bail!("Delegated maintenance encountered errors.\nmaintenance: {:#?}\nexit_sync: {:#?}\nexit_progress: {:#?}", maintenance, exit_sync, exit_progress);
1604 }
1605 Ok(())
1606 }
1607
1608 pub async fn maybe_schedule_maintenance_refresh(&self) -> anyhow::Result<Option<RoundStateId>> {
1616 let vtxos = self.get_vtxos_to_refresh().await?;
1617 if vtxos.len() == 0 {
1618 return Ok(None);
1619 }
1620
1621 let participation = match self.build_refresh_participation(vtxos).await? {
1622 Some(participation) => participation,
1623 None => return Ok(None),
1624 };
1625
1626 info!("Scheduling maintenance refresh ({} vtxos)", participation.inputs.len());
1627 let state = self.join_next_round(participation, Some(RoundMovement::Refresh)).await?;
1628 Ok(Some(state.id()))
1629 }
1630
1631 pub async fn maybe_schedule_maintenance_refresh_delegated(
1639 &self,
1640 ) -> anyhow::Result<Option<RoundStateId>> {
1641 let vtxos = self.get_vtxos_to_refresh().await?;
1642 if vtxos.len() == 0 {
1643 return Ok(None);
1644 }
1645
1646 let participation = match self.build_refresh_participation(vtxos).await? {
1647 Some(participation) => participation,
1648 None => return Ok(None),
1649 };
1650
1651 info!("Scheduling delegated maintenance refresh ({} vtxos)", participation.inputs.len());
1652 let state = self.join_next_round_delegated(participation, Some(RoundMovement::Refresh)).await?;
1653 Ok(Some(state.id()))
1654 }
1655
1656 pub async fn maintenance_refresh(&self) -> anyhow::Result<Option<RoundStatus>> {
1664 let vtxos = self.get_vtxos_to_refresh().await?;
1665 if vtxos.len() == 0 {
1666 return Ok(None);
1667 }
1668
1669 info!("Performing maintenance refresh");
1670 self.refresh_vtxos(vtxos).await
1671 }
1672
1673 pub async fn sync(&self) {
1679 futures::join!(
1680 async {
1681 if let Err(e) = self.chain.update_fee_rates(self.config.fallback_fee_rate).await {
1684 warn!("Error updating fee rates: {:#}", e);
1685 }
1686 },
1687 async {
1688 if let Err(e) = self.sync_mailbox().await {
1689 warn!("Error in mailbox sync: {:#}", e);
1690 }
1691 },
1692 async {
1693 if let Err(e) = self.sync_pending_rounds().await {
1694 warn!("Error while trying to progress rounds awaiting confirmations: {:#}", e);
1695 }
1696 },
1697 async {
1698 if let Err(e) = self.sync_pending_lightning_send_vtxos().await {
1699 warn!("Error syncing pending lightning payments: {:#}", e);
1700 }
1701 },
1702 async {
1703 if let Err(e) = self.try_claim_all_lightning_receives(false).await {
1704 warn!("Error claiming pending lightning receives: {:#}", e);
1705 }
1706 },
1707 async {
1708 if let Err(e) = self.sync_pending_boards().await {
1709 warn!("Error syncing pending boards: {:#}", e);
1710 }
1711 },
1712 async {
1713 if let Err(e) = self.sync_pending_offboards().await {
1714 warn!("Error syncing pending offboards: {:#}", e);
1715 }
1716 }
1717 );
1718 }
1719
1720 pub async fn sync_exits(
1726 &self,
1727 onchain: &mut dyn ExitUnilaterally,
1728 ) -> anyhow::Result<()> {
1729 self.exit.write().await.sync(&self, onchain).await?;
1730 Ok(())
1731 }
1732
1733 pub async fn dangerous_drop_vtxo(&self, vtxo_id: VtxoId) -> anyhow::Result<()> {
1736 warn!("Drop vtxo {} from the database", vtxo_id);
1737 self.db.remove_vtxo(vtxo_id).await?;
1738 Ok(())
1739 }
1740
1741 pub async fn dangerous_drop_all_vtxos(&self) -> anyhow::Result<()> {
1744 warn!("Dropping all vtxos from the db...");
1745 for vtxo in self.vtxos().await? {
1746 self.db.remove_vtxo(vtxo.id()).await?;
1747 }
1748
1749 self.exit.write().await.dangerous_clear_exit().await?;
1750 Ok(())
1751 }
1752
1753 async fn has_counterparty_risk(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<bool> {
1758 for past_pks in vtxo.past_arkoor_pubkeys() {
1759 let mut owns_any = false;
1760 for past_pk in past_pks {
1761 if self.db.get_public_key_idx(&past_pk).await?.is_some() {
1762 owns_any = true;
1763 break;
1764 }
1765 }
1766 if !owns_any {
1767 return Ok(true);
1768 }
1769 }
1770
1771 let my_clause = self.find_signable_clause(vtxo).await;
1772 Ok(!my_clause.is_some())
1773 }
1774
1775 async fn add_should_refresh_vtxos(
1781 &self,
1782 participation: &mut RoundParticipation,
1783 ) -> anyhow::Result<()> {
1784 let tip = self.chain.tip().await?;
1787 let mut vtxos_to_refresh = self.spendable_vtxos_with(
1788 &RefreshStrategy::should_refresh(self, tip, self.chain.fee_rates().await.fast),
1789 ).await?;
1790 if vtxos_to_refresh.is_empty() {
1791 return Ok(());
1792 }
1793
1794 let excluded_ids = participation.inputs.iter().map(|v| v.vtxo_id())
1795 .collect::<HashSet<_>>();
1796 let mut total_amount = Amount::ZERO;
1797 for i in (0..vtxos_to_refresh.len()).rev() {
1798 let vtxo = &vtxos_to_refresh[i];
1799 if excluded_ids.contains(&vtxo.id()) {
1800 vtxos_to_refresh.swap_remove(i);
1801 continue;
1802 }
1803 total_amount += vtxo.amount();
1804 }
1805 if vtxos_to_refresh.is_empty() {
1806 return Ok(());
1808 }
1809
1810 let (_, ark_info) = self.require_server().await?;
1813 let fee = ark_info.fees.refresh.calculate_no_base_fee(
1814 vtxos_to_refresh.iter().map(|wv| VtxoFeeInfo::from_vtxo_and_tip(&wv.vtxo, tip)),
1815 ).context("fee overflowed")?;
1816
1817 let output_amount = match validate_and_subtract_fee_min_dust(total_amount, fee) {
1819 Ok(amount) => amount,
1820 Err(e) => {
1821 trace!("Cannot add should-refresh VTXOs: {}", e);
1822 return Ok(());
1823 },
1824 };
1825 info!(
1826 "Adding {} extra VTXOs to round participation total = {}, fee = {}, output = {}",
1827 vtxos_to_refresh.len(), total_amount, fee, output_amount,
1828 );
1829 let (user_keypair, _) = self.derive_store_next_keypair().await?;
1830 let req = VtxoRequest {
1831 policy: VtxoPolicy::new_pubkey(user_keypair.public_key()),
1832 amount: output_amount,
1833 };
1834 participation.inputs.reserve(vtxos_to_refresh.len());
1835 participation.inputs.extend(vtxos_to_refresh.into_iter().map(|wv| wv.vtxo));
1836 participation.outputs.push(req);
1837
1838 Ok(())
1839 }
1840
1841 pub async fn build_refresh_participation<V: VtxoRef>(
1842 &self,
1843 vtxos: impl IntoIterator<Item = V>,
1844 ) -> anyhow::Result<Option<RoundParticipation>> {
1845 let (vtxos, total_amount) = {
1846 let iter = vtxos.into_iter();
1847 let size_hint = iter.size_hint();
1848 let mut vtxos = Vec::<Vtxo<Full>>::with_capacity(size_hint.1.unwrap_or(size_hint.0));
1849 let mut amount = Amount::ZERO;
1850 for vref in iter {
1851 let id = vref.vtxo_id();
1856 if vtxos.iter().any(|v| v.id() == id) {
1857 bail!("duplicate VTXO id: {}", id);
1858 }
1859 let vtxo = if let Some(vtxo) = vref.into_full_vtxo() {
1860 vtxo
1861 } else {
1862 self.get_vtxo_by_id(id).await
1863 .with_context(|| format!("vtxo with id {} not found", id))?.vtxo
1864 };
1865 amount += vtxo.amount();
1866 vtxos.push(vtxo);
1867 }
1868 (vtxos, amount)
1869 };
1870
1871 if vtxos.is_empty() {
1872 info!("Skipping refresh since no VTXOs are provided.");
1873 return Ok(None);
1874 }
1875 ensure!(total_amount >= P2TR_DUST,
1876 "vtxo amount must be at least {} to participate in a round",
1877 P2TR_DUST,
1878 );
1879
1880 let (_, ark_info) = self.require_server().await?;
1882 let current_height = self.chain.tip().await?;
1883 let vtxo_fee_infos = vtxos.iter()
1884 .map(|v| VtxoFeeInfo::from_vtxo_and_tip(v, current_height));
1885 let fee = ark_info.fees.refresh.calculate(vtxo_fee_infos).context("fee overflowed")?;
1886 let output_amount = validate_and_subtract_fee_min_dust(total_amount, fee)?;
1887
1888 info!("Refreshing {} VTXOs (total amount = {}, fee = {}, output = {}).",
1889 vtxos.len(), total_amount, fee, output_amount,
1890 );
1891 let (user_keypair, _) = self.derive_store_next_keypair().await?;
1892 let req = VtxoRequest {
1893 policy: VtxoPolicy::Pubkey(PubkeyVtxoPolicy { user_pubkey: user_keypair.public_key() }),
1894 amount: output_amount,
1895 };
1896
1897 Ok(Some(RoundParticipation {
1898 inputs: vtxos,
1899 outputs: vec![req],
1900 unblinded_mailbox_id: None,
1901 }))
1902 }
1903
1904 pub async fn refresh_vtxos<V: VtxoRef>(
1909 &self,
1910 vtxos: impl IntoIterator<Item = V>,
1911 ) -> anyhow::Result<Option<RoundStatus>> {
1912 let mut participation = match self.build_refresh_participation(vtxos).await? {
1913 Some(participation) => participation,
1914 None => return Ok(None),
1915 };
1916
1917 if let Err(e) = self.add_should_refresh_vtxos(&mut participation).await {
1918 warn!("Error trying to add additional VTXOs that should be refreshed: {:#}", e);
1919 }
1920
1921 Ok(Some(self.participate_round(participation, Some(RoundMovement::Refresh)).await?))
1922 }
1923
1924 pub async fn refresh_vtxos_delegated<V: VtxoRef>(
1930 &self,
1931 vtxos: impl IntoIterator<Item = V>,
1932 ) -> anyhow::Result<Option<StoredRoundState<Unlocked>>> {
1933 let mut part = match self.build_refresh_participation(vtxos).await? {
1934 Some(participation) => participation,
1935 None => return Ok(None),
1936 };
1937
1938 if let Err(e) = self.add_should_refresh_vtxos(&mut part).await {
1939 warn!("Error trying to add additional VTXOs that should be refreshed: {:#}", e);
1940 }
1941
1942 Ok(Some(self.join_next_round_delegated(part, Some(RoundMovement::Refresh)).await?))
1943 }
1944
1945 pub async fn get_vtxos_to_refresh(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1948 let vtxos = self.spendable_vtxos_with(&RefreshStrategy::should_refresh_if_must(
1949 self,
1950 self.chain.tip().await?,
1951 self.chain.fee_rates().await.fast,
1952 )).await?;
1953 Ok(vtxos)
1954 }
1955
1956 pub async fn get_first_expiring_vtxo_blockheight(
1958 &self,
1959 ) -> anyhow::Result<Option<BlockHeight>> {
1960 Ok(self.spendable_vtxos().await?.iter().map(|v| v.expiry_height()).min())
1961 }
1962
1963 pub async fn get_next_required_refresh_blockheight(
1966 &self,
1967 ) -> anyhow::Result<Option<BlockHeight>> {
1968 let first_expiry = self.get_first_expiring_vtxo_blockheight().await?;
1969 Ok(first_expiry.map(|h| h.saturating_sub(self.config.vtxo_refresh_expiry_threshold)))
1970 }
1971
1972 async fn select_vtxos_to_cover(
1978 &self,
1979 amount: Amount,
1980 ) -> anyhow::Result<Vec<WalletVtxo>> {
1981 let mut vtxos = self.spendable_vtxos().await?;
1982 vtxos.sort_by_key(|v| v.expiry_height());
1983
1984 let mut result = Vec::new();
1986 let mut total_amount = Amount::ZERO;
1987 for input in vtxos {
1988 total_amount += input.amount();
1989 result.push(input);
1990
1991 if total_amount >= amount {
1992 return Ok(result)
1993 }
1994 }
1995
1996 bail!("Insufficient money available. Needed {} but {} is available",
1997 amount, total_amount,
1998 );
1999 }
2000
2001 async fn select_vtxos_to_cover_with_fee<F>(
2007 &self,
2008 amount: Amount,
2009 calc_fee: F,
2010 ) -> anyhow::Result<(Vec<WalletVtxo>, Amount)>
2011 where
2012 F: for<'a> Fn(
2013 Amount, std::iter::Copied<std::slice::Iter<'a, VtxoFeeInfo>>,
2014 ) -> anyhow::Result<Amount>,
2015 {
2016 let tip = self.chain.tip().await?;
2017
2018 const MAX_ITERATIONS: usize = 100;
2021 let mut fee = Amount::ZERO;
2022 let mut fee_info = Vec::new();
2023 for _ in 0..MAX_ITERATIONS {
2024 let required = amount.checked_add(fee)
2025 .context("Amount + fee overflow")?;
2026
2027 let vtxos = self.select_vtxos_to_cover(required).await
2028 .context("Could not find enough suitable VTXOs to cover payment + fees")?;
2029
2030 fee_info.reserve(vtxos.len());
2031 let mut vtxo_amount = Amount::ZERO;
2032 for vtxo in &vtxos {
2033 vtxo_amount += vtxo.amount();
2034 fee_info.push(VtxoFeeInfo::from_vtxo_and_tip(vtxo, tip));
2035 }
2036
2037 fee = calc_fee(amount, fee_info.iter().copied())?;
2038 if amount + fee <= vtxo_amount {
2039 trace!("Selected vtxos to cover amount + fee: amount = {}, fee = {}, total inputs = {}",
2040 amount, fee, vtxo_amount,
2041 );
2042 return Ok((vtxos, fee));
2043 }
2044 trace!("VTXO sum of {} did not exceed amount {} and fee {}, iterating again",
2045 vtxo_amount, amount, fee,
2046 );
2047 fee_info.clear();
2048 }
2049 bail!("Fee calculation did not converge after maximum iterations")
2050 }
2051
2052 pub fn start_daemon(
2058 self: &Arc<Self>,
2059 onchain: Option<Arc<RwLock<dyn DaemonizableOnchainWallet>>>,
2060 ) -> anyhow::Result<()> {
2061 let mut daemon = self.daemon.lock();
2062 if daemon.is_some() {
2063 warn!("Called Wallet::start_daemon while daemon was already running.");
2064 return Ok(());
2065 }
2066
2067 let handle = crate::daemon::start_daemon(self.clone(), onchain);
2070 let _ = daemon.insert(handle);
2071
2072 Ok(())
2073 }
2074
2075 #[deprecated(since = "0.1.4", note = "use start_daemon instead")]
2077 pub fn run_daemon(
2078 self: &Arc<Self>,
2079 onchain: Option<Arc<RwLock<dyn DaemonizableOnchainWallet>>>,
2080 ) -> anyhow::Result<()> {
2081 self.start_daemon(onchain)
2082 }
2083
2084 pub fn stop_daemon(&self) {
2086 let mut daemon = self.daemon.lock();
2087 if let Some(handle) = daemon.take() {
2088 handle.stop();
2089 }
2090 }
2091
2092 pub async fn register_vtxo_transactions_with_server(
2096 &self,
2097 vtxos: &[impl AsRef<Vtxo<Full>>],
2098 ) -> anyhow::Result<()> {
2099 if vtxos.is_empty() {
2100 return Ok(());
2101 }
2102
2103 let (mut srv, _) = self.require_server().await?;
2104 srv.client.register_vtxo_transactions(protos::RegisterVtxoTransactionsRequest {
2105 vtxos: vtxos.iter().map(|v| v.as_ref().serialize()).collect(),
2106 }).await.context("failed to register vtxo transactions")?;
2107
2108 Ok(())
2109 }
2110}
2111
2112fn wrap_server_connect_error(err: ConnectError) -> anyhow::Error {
2113 match err {
2114 ConnectError::CreateEndpoint(CreateEndpointError::NoTransportBackend) => {
2115 anyhow!(MISSING_SERVER_TRANSPORT_HELP)
2116 },
2117 other => anyhow::Error::from(other),
2118 }
2119}
2120
2121impl std::ops::Drop for Wallet {
2122 fn drop(&mut self) {
2123 self.stop_daemon();
2124 }
2125}
2126
2127#[cfg(test)]
2128mod tests {
2129 use server_rpc::client::CreateEndpointError;
2130
2131 use super::{wrap_server_connect_error, MISSING_SERVER_TRANSPORT_HELP};
2132
2133 #[test]
2134 fn no_transport_connect_error_is_reworded_for_wallet_users() {
2135 let err = wrap_server_connect_error(CreateEndpointError::NoTransportBackend.into());
2136 assert!(err.to_string().contains(MISSING_SERVER_TRANSPORT_HELP));
2137 assert!(err.to_string().contains("feature `bark-wallet/native` or `bark-wallet/wasm-web`"));
2138 }
2139}