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 bip39::Mnemonic;
351use bitcoin::{Amount, Network, OutPoint};
352use bitcoin::bip32::{self, ChildNumber, Fingerprint};
353use bitcoin::secp256k1::{self, Keypair, PublicKey};
354use log::{trace, info, warn, error};
355
356use ark::{ArkInfo, ProtocolEncoding, Vtxo, VtxoId, VtxoPolicy, VtxoRequest};
357use ark::address::VtxoDelivery;
358use ark::fees::{validate_and_subtract_fee_min_dust, VtxoFeeInfo};
359use ark::vtxo::{Full, PubkeyVtxoPolicy, VtxoRef};
360use ark::vtxo::policy::signing::VtxoSigner;
361use bitcoin_ext::{BlockHeight, P2TR_DUST};
362use server_rpc::{protos, ServerConnection};
363use server_rpc::client::{ConnectError, CreateEndpointError};
364use crate::chain::{ChainSource, ChainSourceSpec};
365use crate::exit::Exit;
366use crate::lock_manager::LockManager;
367use crate::movement::{Movement, MovementId, PaymentMethod};
368use crate::movement::manager::MovementManager;
369use crate::movement::update::MovementUpdate;
370use crate::notification::NotificationDispatch;
371use crate::onchain::{ExitUnilaterally, PreparePsbt, SignPsbt, Utxo};
372use crate::onchain::DaemonizableOnchainWallet;
373use crate::persist::BarkPersister;
374use crate::persist::models::{RoundStateId, StoredRoundState, Unlocked};
375#[cfg(feature = "socks5-proxy")]
376use crate::proxy::proxy_for_url;
377use crate::round::{RoundParticipation, RoundStatus};
378use crate::subsystem::{ArkoorMovement, RoundMovement};
379use crate::vtxo::{FilterVtxos, RefreshStrategy, VtxoFilter, VtxoStateKind};
380
381#[cfg(all(feature = "wasm-web", feature = "socks5-proxy"))]
382compile_error!("features `wasm-web` does not support feature `socks5-proxy");
383
384#[cfg(all(feature = "wasm-web", feature = "bitcoind-rpc"))]
385compile_error!("`wasm-web` does not support the `bitcoind-rpc` feature");
386
387const HEALTHY_STREAM_DURATION: Duration = Duration::from_secs(59);
391
392const BARK_PURPOSE_INDEX: u32 = 350;
394const VTXO_KEYS_INDEX: u32 = 0;
396const MAILBOX_KEY_INDEX: u32 = 1;
398const RECOVERY_MAILBOX_KEY_INDEX: u32 = 2;
400const MISSING_SERVER_TRANSPORT_HELP: &str =
401 "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.";
402
403lazy_static::lazy_static! {
404 static ref SECP: secp256k1::Secp256k1<secp256k1::All> = secp256k1::Secp256k1::new();
406}
407
408fn log_server_pubkey_changed_error(expected: PublicKey, got: PublicKey) {
414 error!(
415 "
416Server public key has changed!
417
418The Ark server's public key is different from the one stored when this
419wallet was created. This typically happens when:
420
421 - The server operator has rotated their keys
422 - You are connecting to a different server
423 - The server has been replaced
424
425For safety, this wallet will not connect to the server until you
426resolve this. You can recover your funds on-chain by doing an emergency exit.
427
428This will exit your VTXOs to on-chain Bitcoin without needing the server's cooperation.
429
430Expected: {expected}
431Got: {got}")
432}
433
434fn log_server_mailbox_pubkey_changed_error(expected: PublicKey, got: PublicKey) {
436 error!(
437 "
438Server mailbox public key has changed!
439
440The Ark server's mailbox public key is different from the one stored when this
441wallet was created. This typically happens when:
442
443 - The server operator has rotated their keys
444 - You are connecting to a different server
445 - The server has been replaced
446
447For safety, this wallet will not connect to the server until you resolve this.
448
449Unlike a server pubkey change, your VTXOs are not at risk - the mailbox pubkey
450only affects address receive semantics. Any Ark addresses you previously
451shared will stop receiving new payments; you will need to share new addresses
452after reconnecting.
453
454Expected: {expected}
455Got: {got}")
456}
457
458#[derive(Debug, Clone)]
460pub struct LightningReceiveBalance {
461 pub total: Amount,
463 pub claimable: Amount,
465}
466
467#[derive(Debug, Clone)]
469pub struct Balance {
470 pub spendable: Amount,
472 pub pending_lightning_send: Amount,
474 pub claimable_lightning_receive: Amount,
476 pub pending_in_round: Amount,
478 pub pending_exit: Option<Amount>,
481 pub pending_board: Amount,
483}
484
485pub struct UtxoInfo {
486 pub outpoint: OutPoint,
487 pub amount: Amount,
488 pub confirmation_height: Option<u32>,
489}
490
491impl From<Utxo> for UtxoInfo {
492 fn from(value: Utxo) -> Self {
493 match value {
494 Utxo::Local(o) => UtxoInfo {
495 outpoint: o.outpoint,
496 amount: o.amount,
497 confirmation_height: o.confirmation_height,
498 },
499 Utxo::Exit(e) => UtxoInfo {
500 outpoint: e.vtxo.point(),
501 amount: e.vtxo.amount(),
502 confirmation_height: Some(e.height),
503 },
504 }
505 }
506}
507
508pub struct OffchainBalance {
511 pub available: Amount,
513 pub pending_in_round: Amount,
515 pub pending_exit: Amount,
518}
519
520#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
522pub struct WalletProperties {
523 pub network: Network,
527
528 pub fingerprint: Fingerprint,
532
533 pub server_pubkey: Option<PublicKey>,
540
541 pub server_mailbox_pubkey: Option<PublicKey>,
549}
550
551pub struct WalletSeed {
557 master: bip32::Xpriv,
558 vtxo: bip32::Xpriv,
559}
560
561impl WalletSeed {
562 fn new(network: Network, seed: &[u8; 64]) -> Self {
563 let bark_path = [ChildNumber::from_hardened_idx(BARK_PURPOSE_INDEX).unwrap()];
564 let master = bip32::Xpriv::new_master(network, seed)
565 .expect("invalid seed")
566 .derive_priv(&SECP, &bark_path)
567 .expect("purpose is valid");
568
569 let vtxo_path = [ChildNumber::from_hardened_idx(VTXO_KEYS_INDEX).unwrap()];
570 let vtxo = master.derive_priv(&SECP, &vtxo_path)
571 .expect("vtxo path is valid");
572
573 Self { master, vtxo }
574 }
575
576 fn fingerprint(&self) -> Fingerprint {
577 self.master.fingerprint(&SECP)
578 }
579
580 fn derive_vtxo_keypair(&self, idx: u32) -> Keypair {
581 self.vtxo.derive_priv(&SECP, &[idx.into()]).unwrap().to_keypair(&SECP)
582 }
583
584 fn to_mailbox_keypair(&self) -> Keypair {
585 let mailbox_path = [ChildNumber::from_hardened_idx(MAILBOX_KEY_INDEX).unwrap()];
586 self.master.derive_priv(&SECP, &mailbox_path).unwrap().to_keypair(&SECP)
587 }
588
589 fn to_recovery_mailbox_keypair(&self) -> Keypair {
590 let mailbox_path = [ChildNumber::from_hardened_idx(RECOVERY_MAILBOX_KEY_INDEX).unwrap()];
591 self.master.derive_priv(&SECP, &mailbox_path).unwrap().to_keypair(&SECP)
592 }
593}
594
595struct WalletInner {
596 chain: Arc<ChainSource>,
598
599 exit: Exit,
601
602 movements: Arc<MovementManager>,
604
605 notifications: NotificationDispatch,
607
608 config: Config,
610
611 db: Arc<dyn BarkPersister>,
613
614 lock_manager: Box<dyn LockManager>,
618
619 seed: WalletSeed,
621
622 server: tokio::sync::OnceCell<ServerConnection>,
629
630 daemon: parking_lot::Mutex<Option<DaemonHandle>>,
632}
633
634#[derive(Clone)]
775pub struct Wallet {
776 inner: Arc<WalletInner>,
777}
778
779impl Wallet {
780 pub async fn require_chainsource_version(&self) -> anyhow::Result<()> {
784 self.inner.chain.require_version().await
785 }
786
787 pub async fn network(&self) -> anyhow::Result<Network> {
788 Ok(self.properties().await?.network)
789 }
790
791 pub fn chain(&self) -> &Arc<ChainSource> {
793 &self.inner.chain
794 }
795
796 pub fn exit_mgr(&self) -> &Exit {
798 &self.inner.exit
799 }
800
801 pub fn movements_mgr(&self) -> &MovementManager {
803 &self.inner.movements
804 }
805
806 pub async fn peek_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
809 let last_revealed = self.inner.db.get_last_vtxo_key_index().await?;
810
811 let index = last_revealed.map(|i| i + 1).unwrap_or(u32::MIN);
812 let keypair = self.inner.seed.derive_vtxo_keypair(index);
813
814 Ok((keypair, index))
815 }
816
817 pub async fn derive_store_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
820 let (keypair, index) = self.peek_next_keypair().await?;
821 self.inner.db.store_vtxo_key(index, keypair.public_key()).await?;
822 Ok((keypair, index))
823 }
824
825 #[deprecated(note = "use peek_keypair instead")]
826 pub async fn peak_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
827 self.peek_keypair(index).await
828 }
829
830 pub async fn peek_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
844 let keypair = self.inner.seed.derive_vtxo_keypair(index);
845 if self.inner.db.get_public_key_idx(&keypair.public_key()).await?.is_some() {
846 Ok(keypair)
847 } else {
848 bail!("VTXO key {} does not exist, please derive it first", index)
849 }
850 }
851
852
853 pub async fn pubkey_keypair(&self, public_key: &PublicKey) -> anyhow::Result<Option<(u32, Keypair)>> {
865 if let Some(index) = self.inner.db.get_public_key_idx(&public_key).await? {
866 Ok(Some((index, self.inner.seed.derive_vtxo_keypair(index))))
867 } else {
868 Ok(None)
869 }
870 }
871
872 pub async fn get_vtxo_key(&self, vtxo: impl VtxoRef) -> anyhow::Result<Keypair> {
883 let bare_vtxo = match vtxo.as_bare_vtxo() {
884 Some(bare) => bare,
885 None => Cow::Owned(self.get_vtxo_by_id(vtxo.vtxo_id()).await?.vtxo),
886 };
887 let pubkey = self.find_signable_clause(&bare_vtxo).await
888 .context("VTXO is not signable by wallet")?
889 .pubkey();
890 let idx = self.inner.db.get_public_key_idx(&pubkey).await?
891 .context("VTXO key not found")?;
892 Ok(self.inner.seed.derive_vtxo_keypair(idx))
893 }
894
895 #[deprecated(note = "use peek_address instead")]
896 pub async fn peak_address(&self, index: u32) -> anyhow::Result<ark::Address> {
897 self.peek_address(index).await
898 }
899
900 pub async fn peek_address(&self, index: u32) -> anyhow::Result<ark::Address> {
904 let properties = self.properties().await?;
905 let network = properties.network;
906 let keypair = self.peek_keypair(index).await?;
907 let mailbox = self.mailbox_identifier();
908
909
910 let (server_pubkey, mailbox_pubkey) =
911 if let (Some(spk), Some(mpk)) = (properties.server_pubkey, properties.server_mailbox_pubkey) {
912 (spk, mpk)
913 } else {
914 let (_, ark_info) = self.require_server().await?;
915 (ark_info.server_pubkey, ark_info.mailbox_pubkey)
916 };
917
918 Ok(ark::Address::builder()
919 .testnet(network != bitcoin::Network::Bitcoin)
920 .server_pubkey(server_pubkey)
921 .pubkey_policy(keypair.public_key())
922 .mailbox(mailbox_pubkey, mailbox, &keypair)
923 .expect("Failed to assign mailbox")
924 .into_address().unwrap())
925 }
926
927 pub async fn new_address_with_index(&self) -> anyhow::Result<(ark::Address, u32)> {
931 let (_, index) = self.derive_store_next_keypair().await?;
932 let addr = self.peek_address(index).await?;
933 Ok((addr, index))
934 }
935
936 pub async fn new_address(&self) -> anyhow::Result<ark::Address> {
938 let (addr, _) = self.new_address_with_index().await?;
939 Ok(addr)
940 }
941
942 pub async fn create(
951 mnemonic: &Mnemonic,
952 network: Network,
953 config: Config,
954 db: Arc<dyn BarkPersister>,
955 lock_manager: Box<dyn LockManager>,
956 force: bool,
957 ) -> anyhow::Result<Wallet> {
958 trace!("Config: {:?}", config);
959
960 let wallet_fingerprint = WalletSeed::new(network, &mnemonic.to_seed("")).fingerprint();
961
962 let create_guard = lock_manager.lock(
967 &format!("{}.create", wallet_fingerprint),
968 Duration::from_secs(5),
969 ).await.context("wallet initialization already in progress")?;
970
971 if let Some(existing) = db.read_properties().await? {
972 trace!("Existing config: {:?}", existing);
973 bail!("cannot overwrite already existing config")
974 }
975
976 let (server_pubkey, mailbox_pubkey) = if !force {
978 match Self::connect_to_server(&config, network).await {
979 Ok(conn) => {
980 let ark_info = conn.ark_info().await;
981 (Some(ark_info.server_pubkey), Some(ark_info.mailbox_pubkey))
982 }
983 Err(err) => {
984 bail!("Failed to connect to provided server (if you are sure use the --force flag): {:#}", err);
985 }
986 }
987 } else {
988 (None, None)
989 };
990
991 let properties = WalletProperties {
992 network,
993 fingerprint: wallet_fingerprint,
994 server_pubkey,
995 server_mailbox_pubkey: mailbox_pubkey,
996 };
997
998 db.init_wallet(&properties).await.context("cannot init wallet in the database")?;
1000 info!("Created wallet with fingerprint: {}", wallet_fingerprint);
1001 if let Some(pk) = server_pubkey {
1002 info!("Stored server pubkey: {}", pk);
1003 }
1004
1005 drop(create_guard);
1008
1009 let wallet = Wallet::open(&mnemonic, db, config, lock_manager).await.context("failed to open wallet")?;
1011 wallet.require_chainsource_version().await?;
1012
1013 Ok(wallet)
1014 }
1015
1016 pub async fn create_with_exits(
1023 mnemonic: &Mnemonic,
1024 network: Network,
1025 config: Config,
1026 db: Arc<dyn BarkPersister>,
1027 lock_manager: Box<dyn LockManager>,
1028 force: bool,
1029 ) -> anyhow::Result<Wallet> {
1030 let wallet = Wallet::create(mnemonic, network, config, db, lock_manager, force).await?;
1031 wallet.inner.exit.load().await?;
1032 Ok(wallet)
1033 }
1034
1035 pub async fn open(
1040 mnemonic: &Mnemonic,
1041 db: Arc<dyn BarkPersister>,
1042 config: Config,
1043 lock_manager: Box<dyn LockManager>,
1044 ) -> anyhow::Result<Wallet> {
1045 let properties = db.read_properties().await?.context("Wallet is not initialised")?;
1046
1047 let seed = {
1048 let seed = mnemonic.to_seed("");
1049 WalletSeed::new(properties.network, &seed)
1050 };
1051
1052 if properties.fingerprint != seed.fingerprint() {
1053 bail!("incorrect mnemonic")
1054 }
1055
1056 let chain_source = if let Some(ref url) = config.esplora_address {
1057 ChainSourceSpec::Esplora {
1058 url: url.clone(),
1059 }
1060 } else if let Some(ref url) = config.bitcoind_address {
1061 let auth = if let Some(ref c) = config.bitcoind_cookiefile {
1062 bitcoin_ext::rpc::Auth::CookieFile(c.clone())
1063 } else {
1064 bitcoin_ext::rpc::Auth::UserPass(
1065 config.bitcoind_user.clone().context("need bitcoind auth config")?,
1066 config.bitcoind_pass.clone().context("need bitcoind auth config")?,
1067 )
1068 };
1069 ChainSourceSpec::Bitcoind { url: url.clone(), auth }
1070 } else {
1071 bail!("Need to either provide esplora or bitcoind info");
1072 };
1073
1074 #[cfg(feature = "socks5-proxy")]
1075 let chain_proxy = proxy_for_url(&config.socks5_proxy, chain_source.url())?;
1076 let chain_source_client = ChainSource::new(
1077 chain_source, properties.network, config.fallback_fee_rate,
1078 #[cfg(feature = "socks5-proxy")] chain_proxy.as_deref(),
1079 ).await?;
1080 let chain = Arc::new(chain_source_client);
1081
1082 let server = tokio::sync::OnceCell::new();
1083
1084 let notifications = NotificationDispatch::new();
1085 let movements = Arc::new(MovementManager::new(db.clone(), notifications.clone()));
1086 let exit = Exit::new(db.clone(), chain.clone(), movements.clone()).await?;
1087
1088 Ok(Wallet { inner: Arc::new(WalletInner {
1089 config, db, lock_manager, seed, exit, movements, notifications, server, chain,
1090 daemon: parking_lot::Mutex::new(None),
1091 })})
1092 }
1093
1094 pub async fn open_with_exits(
1098 mnemonic: &Mnemonic,
1099 db: Arc<dyn BarkPersister>,
1100 cfg: Config,
1101 lock_manager: Box<dyn LockManager>,
1102 ) -> anyhow::Result<Wallet> {
1103 let wallet = Wallet::open(mnemonic, db, cfg, lock_manager).await?;
1104 wallet.inner.exit.load().await?;
1105 Ok(wallet)
1106 }
1107
1108 pub async fn open_with_daemon(
1111 mnemonic: &Mnemonic,
1112 db: Arc<dyn BarkPersister>,
1113 cfg: Config,
1114 onchain: Option<Arc<tokio::sync::RwLock<dyn DaemonizableOnchainWallet>>>,
1115 lock_manager: Box<dyn LockManager>,
1116 ) -> anyhow::Result<Wallet> {
1117 let wallet = Wallet::open(mnemonic, db, cfg, lock_manager).await?;
1118 if onchain.is_some() {
1119 wallet.inner.exit.load().await?;
1120 }
1121
1122 wallet.start_daemon(onchain)?;
1123
1124 Ok(wallet)
1125 }
1126
1127 pub fn config(&self) -> &Config {
1129 &self.inner.config
1130 }
1131
1132 pub async fn properties(&self) -> anyhow::Result<WalletProperties> {
1134 let properties = self.inner.db.read_properties().await?.context("Wallet is not initialised")?;
1135 Ok(properties)
1136 }
1137
1138 pub fn fingerprint(&self) -> Fingerprint {
1140 self.inner.seed.fingerprint()
1141 }
1142
1143 async fn connect_to_server(
1144 config: &Config,
1145 network: Network,
1146 ) -> anyhow::Result<ServerConnection> {
1147 let mut builder = ServerConnection::builder()
1148 .address(&config.server_address)
1149 .network(network);
1150
1151 #[cfg(feature = "socks5-proxy")]
1152 if let Some(proxy) = proxy_for_url(&config.socks5_proxy, &config.server_address)? {
1153 builder = builder.proxy(&proxy)
1154 }
1155
1156 if let Some(ref token) = config.server_access_token {
1157 builder = builder.access_token(token);
1158 }
1159
1160 builder.connect().await.map_err(wrap_server_connect_error)
1161 .context("Failed to connect to Ark server")
1162 }
1163
1164 async fn require_server(&self) -> anyhow::Result<(ServerConnection, ArkInfo)> {
1165 let conn = self.inner.server.get_or_try_init(|| async {
1169 let network = self.properties().await?.network;
1170 Self::connect_to_server(&self.inner.config, network).await
1171 .context("You should be connected to Ark server to perform this action")
1172 }).await?.clone();
1173
1174 let ark_info = conn.ark_info().await;
1175 self.check_and_store_server_keys(&ark_info).await?;
1176
1177 Ok((conn, ark_info))
1178 }
1179
1180 pub async fn refresh_server(&self) -> anyhow::Result<()> {
1181 let srv = self.inner.server.get_or_try_init(|| async {
1187 let properties = self.properties().await?;
1188 Self::connect_to_server(&self.inner.config, properties.network).await
1189 .map_err(anyhow::Error::from)
1190 }).await?;
1191
1192 srv.check_connection().await?;
1193 let ark_info = srv.ark_info().await;
1194 ark_info.fees.validate().context("invalid fee schedule")?;
1195 self.check_and_store_server_keys(&ark_info).await?;
1196
1197 Ok(())
1198 }
1199
1200 async fn check_and_store_server_keys(&self, ark_info: &ArkInfo) -> anyhow::Result<()> {
1207 let properties = self.properties().await?;
1208
1209 if let Some(stored_pubkey) = properties.server_pubkey {
1210 if stored_pubkey != ark_info.server_pubkey {
1211 log_server_pubkey_changed_error(stored_pubkey, ark_info.server_pubkey);
1212 bail!("Server public key has changed. You should exit all your VTXOs!");
1213 }
1214 } else {
1215 self.inner.db.set_server_pubkey(ark_info.server_pubkey).await?;
1216 info!("Stored server pubkey for existing wallet: {}", ark_info.server_pubkey);
1217 }
1218
1219 if let Some(stored_mailbox_pubkey) = properties.server_mailbox_pubkey {
1220 if stored_mailbox_pubkey != ark_info.mailbox_pubkey {
1221 log_server_mailbox_pubkey_changed_error(stored_mailbox_pubkey, ark_info.mailbox_pubkey);
1222 bail!("Server mailbox public key has changed.");
1223 }
1224 } else {
1225 self.inner.db.set_server_mailbox_pubkey(ark_info.mailbox_pubkey).await?;
1226 info!("Stored server mailbox pubkey for existing wallet: {}", ark_info.mailbox_pubkey);
1227 }
1228
1229 Ok(())
1230 }
1231
1232 pub async fn ark_info(&self) -> anyhow::Result<Option<ArkInfo>> {
1234 match self.inner.server.get() {
1235 Some(srv) => Ok(Some(srv.ark_info().await)),
1236 None => Ok(None),
1237 }
1238 }
1239
1240 pub async fn require_ark_info(&self) -> anyhow::Result<ArkInfo> {
1246 let (_, ark_info) = self.require_server().await?;
1247 Ok(ark_info)
1248 }
1249
1250 pub async fn balance(&self) -> anyhow::Result<Balance> {
1254 let vtxos = self.vtxos().await?;
1255
1256 let spendable = {
1257 let mut v = vtxos.iter().collect();
1258 VtxoStateKind::Spendable.filter_vtxos(&mut v).await?;
1259 v.into_iter().map(|v| v.amount()).sum::<Amount>()
1260 };
1261
1262 let pending_lightning_send = self.pending_lightning_send_vtxos().await?.iter()
1263 .map(|v| v.amount())
1264 .sum::<Amount>();
1265
1266 let claimable_lightning_receive = self.claimable_lightning_receive_balance().await?;
1267
1268 let pending_board = self.pending_board_vtxos().await?.iter()
1269 .map(|v| v.amount())
1270 .sum::<Amount>();
1271
1272 let pending_in_round = self.pending_round_balance().await?;
1273
1274 let pending_exit = self.exit_mgr().try_pending_total();
1275
1276 Ok(Balance {
1277 spendable,
1278 pending_in_round,
1279 pending_lightning_send,
1280 claimable_lightning_receive,
1281 pending_exit,
1282 pending_board,
1283 })
1284 }
1285
1286 pub async fn validate_vtxo(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<()> {
1288 let tx = self.inner.chain.get_tx(&vtxo.chain_anchor().txid).await
1289 .context("could not fetch chain tx")?;
1290
1291 let tx = tx.with_context(|| {
1292 format!("vtxo chain anchor not found for vtxo: {}", vtxo.chain_anchor().txid)
1293 })?;
1294
1295 vtxo.validate(&tx)?;
1296
1297 Ok(())
1298 }
1299
1300 pub async fn import_vtxo(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<()> {
1310 if self.inner.db.get_wallet_vtxo(vtxo.id()).await?.is_some() {
1311 info!("VTXO {} already exists in wallet, skipping import", vtxo.id());
1312 return Ok(());
1313 }
1314
1315 self.validate_vtxo(vtxo).await.context("VTXO validation failed")?;
1316
1317 if self.find_signable_clause(vtxo).await.is_none() {
1318 bail!("VTXO {} is not owned by this wallet (no signable clause found)", vtxo.id());
1319 }
1320
1321 let current_height = self.inner.chain.tip().await?;
1322 if vtxo.expiry_height() <= current_height {
1323 bail!("Vtxo {} has expired", vtxo.id());
1324 }
1325
1326 self.store_spendable_vtxos([vtxo]).await.context("failed to store imported VTXO")?;
1327
1328 info!("Successfully imported VTXO {}", vtxo.id());
1329 Ok(())
1330 }
1331
1332 pub async fn get_vtxo_by_id(&self, vtxo_id: VtxoId) -> anyhow::Result<WalletVtxo> {
1334 let vtxo = self.inner.db.get_wallet_vtxo(vtxo_id).await
1335 .with_context(|| format!("Error when querying vtxo {} in database", vtxo_id))?
1336 .with_context(|| format!("The VTXO with id {} cannot be found", vtxo_id))?;
1337 Ok(vtxo)
1338 }
1339
1340 pub async fn get_full_vtxo(&self, vtxo_id: VtxoId) -> anyhow::Result<Vtxo<Full>> {
1348 self.inner.db.get_full_vtxo(vtxo_id).await
1349 .with_context(|| format!("Error when querying full vtxo {} in database", vtxo_id))?
1350 .with_context(|| format!("The VTXO with id {} cannot be found", vtxo_id))
1351 }
1352
1353 pub async fn get_full_vtxos<V: VtxoRef>(
1355 &self,
1356 vtxos: impl IntoIterator<Item = V>,
1357 ) -> anyhow::Result<Vec<Vtxo<Full>>> {
1358 let ids = vtxos.into_iter().map(|v| v.vtxo_id()).collect::<Vec<_>>();
1359 self.inner.db.get_full_vtxos(&ids).await
1360 .with_context(||
1361 format!("Error when querying full vtxos in database with IDs: {:?}", ids)
1362 )
1363 }
1364
1365 #[deprecated(since="0.1.0-beta.5", note = "Use Wallet::history instead")]
1367 pub async fn movements(&self) -> anyhow::Result<Vec<Movement>> {
1368 self.history().await
1369 }
1370
1371 pub async fn history(&self) -> anyhow::Result<Vec<Movement>> {
1373 Ok(self.inner.db.get_all_movements().await?)
1374 }
1375
1376 pub async fn update_history_metadata(
1396 &self,
1397 movement_id: MovementId,
1398 patch: &serde_json::Value,
1399 ) -> anyhow::Result<()> {
1400 self.inner.movements.patch_metadata(movement_id, patch).await?;
1401 Ok(())
1402 }
1403
1404 pub async fn history_by_payment_method(
1406 &self,
1407 payment_method: &PaymentMethod,
1408 ) -> anyhow::Result<Vec<Movement>> {
1409 let mut ret = self.inner.db.get_movements_by_payment_method(payment_method).await?;
1410 ret.sort_by_key(|m| m.id);
1411 Ok(ret)
1412 }
1413
1414 pub async fn all_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1416 Ok(self.inner.db.get_all_vtxos().await?)
1417 }
1418
1419 pub async fn vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1421 Ok(self.inner.db.get_vtxos_by_state(&VtxoStateKind::UNSPENT_STATES).await?)
1422 }
1423
1424 pub async fn vtxos_with(&self, filter: &impl FilterVtxos) -> anyhow::Result<Vec<WalletVtxo>> {
1426 let mut vtxos = self.vtxos().await?;
1427 filter.filter_vtxos(&mut vtxos).await.context("error filtering vtxos")?;
1428 Ok(vtxos)
1429 }
1430
1431 pub async fn spendable_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1433 Ok(self.vtxos_with(&VtxoStateKind::Spendable).await?)
1434 }
1435
1436 pub async fn spendable_vtxos_with(
1438 &self,
1439 filter: &impl FilterVtxos,
1440 ) -> anyhow::Result<Vec<WalletVtxo>> {
1441 let mut vtxos = self.spendable_vtxos().await?;
1442 filter.filter_vtxos(&mut vtxos).await.context("error filtering vtxos")?;
1443 Ok(vtxos)
1444 }
1445
1446 pub async fn get_expiring_vtxos(
1448 &self,
1449 threshold: BlockHeight,
1450 ) -> anyhow::Result<Vec<WalletVtxo>> {
1451 let expiry = self.inner.chain.tip().await? + threshold;
1452 let filter = VtxoFilter::new(&self).expires_before(expiry);
1453 Ok(self.spendable_vtxos_with(&filter).await?)
1454 }
1455
1456 pub async fn maintenance(&self) -> anyhow::Result<()> {
1462 info!("Starting wallet maintenance in interactive mode");
1463 self.sync().await;
1464
1465 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 let refresh = self.maintenance_refresh().await;
1470 if let Err(e) = refresh.as_ref() {
1471 warn!("Error refreshing VTXOs: {:#}", e);
1472 }
1473 if rounds.is_err() || refresh.is_err() {
1474 bail!("Maintenance encountered errors.\nprogress_rounds: {:#?}\nrefresh: {:#?}", rounds, refresh);
1475 }
1476 Ok(())
1477 }
1478
1479 pub async fn maintenance_delegated(&self) -> anyhow::Result<()> {
1485 info!("Starting wallet maintenance in delegated mode");
1486 self.sync().await;
1487 let rounds = self.progress_pending_rounds(None).await;
1488 if let Err(e) = rounds.as_ref() {
1489 warn!("Error progressing pending rounds: {:#}", e);
1490 }
1491 let refresh = self.maybe_schedule_maintenance_refresh_delegated().await;
1492 if let Err(e) = refresh.as_ref() {
1493 warn!("Error refreshing VTXOs: {:#}", e);
1494 }
1495 if rounds.is_err() || refresh.is_err() {
1496 bail!("Delegated maintenance encountered errors.\nprogress_rounds: {:#?}\nrefresh: {:#?}", rounds, refresh);
1497 }
1498 Ok(())
1499 }
1500
1501 pub async fn maintenance_with_onchain<W: PreparePsbt + SignPsbt + ExitUnilaterally>(
1509 &self,
1510 onchain: &mut W,
1511 ) -> anyhow::Result<()> {
1512 info!("Starting wallet maintenance in interactive mode with onchain wallet");
1513
1514 let maintenance = self.maintenance().await;
1516
1517 let exit_sync = self.sync_exits().await;
1519 if let Err(e) = exit_sync.as_ref() {
1520 warn!("Error syncing exits: {:#}", e);
1521 }
1522 let exit_progress = self.exit_mgr().progress_exits_with_bdk(self, onchain, None).await;
1523 if let Err(e) = exit_progress.as_ref() {
1524 warn!("Error progressing exits: {:#}", e);
1525 }
1526 if maintenance.is_err() || exit_sync.is_err() || exit_progress.is_err() {
1527 bail!("Maintenance encountered errors.\nmaintenance: {:#?}\nexit_sync: {:#?}\nexit_progress: {:#?}", maintenance, exit_sync, exit_progress);
1528 }
1529 Ok(())
1530 }
1531
1532 pub async fn maintenance_with_onchain_delegated<W: PreparePsbt + SignPsbt + ExitUnilaterally>(
1539 &self,
1540 onchain: &mut W,
1541 ) -> anyhow::Result<()> {
1542 info!("Starting wallet maintenance in delegated mode with onchain wallet");
1543
1544 let maintenance = self.maintenance_delegated().await;
1546
1547 let exit_sync = self.sync_exits().await;
1549 if let Err(e) = exit_sync.as_ref() {
1550 warn!("Error syncing exits: {:#}", e);
1551 }
1552 let exit_progress = self.exit_mgr().progress_exits_with_bdk(self, onchain, None).await;
1553 if let Err(e) = exit_progress.as_ref() {
1554 warn!("Error progressing exits: {:#}", e);
1555 }
1556 if maintenance.is_err() || exit_sync.is_err() || exit_progress.is_err() {
1557 bail!("Delegated maintenance encountered errors.\nmaintenance: {:#?}\nexit_sync: {:#?}\nexit_progress: {:#?}", maintenance, exit_sync, exit_progress);
1558 }
1559 Ok(())
1560 }
1561
1562 pub async fn maybe_schedule_maintenance_refresh(&self) -> anyhow::Result<Option<RoundStateId>> {
1570 let vtxos = self.get_vtxos_to_refresh().await?;
1571 if vtxos.len() == 0 {
1572 return Ok(None);
1573 }
1574
1575 let participation = match self.build_refresh_participation(vtxos).await? {
1576 Some(participation) => participation,
1577 None => return Ok(None),
1578 };
1579
1580 info!("Scheduling maintenance refresh ({} vtxos)", participation.inputs.len());
1581 let state = self.join_next_round(participation, Some(RoundMovement::Refresh)).await?;
1582 Ok(Some(state.id()))
1583 }
1584
1585 pub async fn maybe_schedule_maintenance_refresh_delegated(
1593 &self,
1594 ) -> anyhow::Result<Option<RoundStateId>> {
1595 let vtxos = self.get_vtxos_to_refresh().await?;
1596 if vtxos.len() == 0 {
1597 return Ok(None);
1598 }
1599
1600 let participation = match self.build_refresh_participation(vtxos).await? {
1601 Some(participation) => participation,
1602 None => return Ok(None),
1603 };
1604
1605 info!("Scheduling delegated maintenance refresh ({} vtxos)", participation.inputs.len());
1606 let state = self.join_next_round_delegated(participation, Some(RoundMovement::Refresh)).await?;
1607 Ok(Some(state.id()))
1608 }
1609
1610 pub async fn maintenance_refresh(&self) -> anyhow::Result<Option<RoundStatus>> {
1618 let vtxos = self.get_vtxos_to_refresh().await?;
1619 if vtxos.len() == 0 {
1620 return Ok(None);
1621 }
1622
1623 info!("Performing maintenance refresh");
1624 self.refresh_vtxos(vtxos).await
1625 }
1626
1627 pub async fn sync(&self) {
1634 futures::join!(
1635 async {
1636 if let Err(e) = self.inner.chain.update_fee_rates(self.inner.config.fallback_fee_rate).await {
1639 warn!("Error updating fee rates: {:#}", e);
1640 }
1641 },
1642 async {
1643 if let Err(e) = self.sync_mailbox().await {
1644 warn!("Error in mailbox sync: {:#}", e);
1645 }
1646 },
1647 async {
1648 if let Err(e) = self.sync_pending_rounds().await {
1649 warn!("Error while trying to progress rounds awaiting confirmations: {:#}", e);
1650 }
1651 },
1652 async {
1653 if let Err(e) = self.sync_pending_lightning_send_vtxos().await {
1654 warn!("Error syncing pending lightning payments: {:#}", e);
1655 }
1656 },
1657 async {
1658 if let Err(e) = self.try_claim_all_lightning_receives(false).await {
1659 warn!("Error claiming pending lightning receives: {:#}", e);
1660 }
1661 },
1662 async {
1663 if let Err(e) = self.sync_pending_boards().await {
1664 warn!("Error syncing pending boards: {:#}", e);
1665 }
1666 },
1667 async {
1668 if let Err(e) = self.sync_pending_offboards().await {
1669 warn!("Error syncing pending offboards: {:#}", e);
1670 }
1671 }
1672 );
1673 }
1674
1675 pub async fn sync_exits(&self) -> anyhow::Result<()> {
1681 self.exit_mgr().sync(&self).await?;
1682 Ok(())
1683 }
1684
1685 pub async fn dangerous_drop_vtxo(&self, vtxo_id: VtxoId) -> anyhow::Result<()> {
1688 warn!("Drop vtxo {} from the database", vtxo_id);
1689 self.inner.db.remove_vtxo(vtxo_id).await?;
1690 Ok(())
1691 }
1692
1693 pub async fn dangerous_drop_all_vtxos(&self) -> anyhow::Result<()> {
1696 warn!("Dropping all vtxos from the db...");
1697 for vtxo in self.vtxos().await? {
1698 self.inner.db.remove_vtxo(vtxo.id()).await?;
1699 }
1700
1701 self.exit_mgr().dangerous_clear_exit().await?;
1702 Ok(())
1703 }
1704
1705 async fn has_counterparty_risk(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<bool> {
1713 for past_pks in vtxo.past_arkoor_pubkeys() {
1714 let mut owns_any = false;
1715 for past_pk in past_pks {
1716 if self.inner.db.get_public_key_idx(&past_pk).await?.is_some() {
1717 owns_any = true;
1718 break;
1719 }
1720 }
1721 if !owns_any {
1722 return Ok(true);
1723 }
1724 }
1725
1726 let my_clause = self.find_signable_clause(vtxo).await;
1727 Ok(!my_clause.is_some())
1728 }
1729
1730 async fn add_should_refresh_vtxos(
1736 &self,
1737 participation: &mut RoundParticipation,
1738 ) -> anyhow::Result<()> {
1739 let tip = self.inner.chain.tip().await?;
1742 let mut vtxos_to_refresh = self.spendable_vtxos_with(
1743 &RefreshStrategy::should_refresh(self, tip, self.inner.chain.fee_rates().await.fast),
1744 ).await?;
1745 if vtxos_to_refresh.is_empty() {
1746 return Ok(());
1747 }
1748
1749 let excluded_ids = participation.inputs.iter().map(|v| v.vtxo_id())
1750 .collect::<HashSet<_>>();
1751 let mut total_amount = Amount::ZERO;
1752 for i in (0..vtxos_to_refresh.len()).rev() {
1753 let vtxo = &vtxos_to_refresh[i];
1754 if excluded_ids.contains(&vtxo.id()) {
1755 vtxos_to_refresh.swap_remove(i);
1756 continue;
1757 }
1758 total_amount += vtxo.amount();
1759 }
1760 if vtxos_to_refresh.is_empty() {
1761 return Ok(());
1763 }
1764
1765 let (_, ark_info) = self.require_server().await?;
1768 let fee = ark_info.fees.refresh.calculate_no_base_fee(
1769 vtxos_to_refresh.iter().map(|wv| VtxoFeeInfo::from_vtxo_and_tip(&wv.vtxo, tip)),
1770 ).context("fee overflowed")?;
1771
1772 let output_amount = match validate_and_subtract_fee_min_dust(total_amount, fee) {
1774 Ok(amount) => amount,
1775 Err(e) => {
1776 trace!("Cannot add should-refresh VTXOs: {}", e);
1777 return Ok(());
1778 },
1779 };
1780 info!(
1781 "Adding {} extra VTXOs to round participation total = {}, fee = {}, output = {}",
1782 vtxos_to_refresh.len(), total_amount, fee, output_amount,
1783 );
1784 let (user_keypair, _) = self.derive_store_next_keypair().await?;
1785 let req = VtxoRequest {
1786 policy: VtxoPolicy::new_pubkey(user_keypair.public_key()),
1787 amount: output_amount,
1788 };
1789 let extra_ids = vtxos_to_refresh.into_iter().map(|wv| wv.id()).collect::<Vec<_>>();
1790 let extra_full = self.inner.db.get_full_vtxos(&extra_ids).await
1791 .context("failed to hydrate refresh candidates")?;
1792 participation.inputs.reserve(extra_full.len());
1793 participation.inputs.extend(extra_full);
1794 participation.outputs.push(req);
1795
1796 Ok(())
1797 }
1798
1799 pub async fn build_refresh_participation<V: VtxoRef>(
1800 &self,
1801 vtxos: impl IntoIterator<Item = V>,
1802 ) -> anyhow::Result<Option<RoundParticipation>> {
1803 let (vtxos, total_amount) = {
1804 let iter = vtxos.into_iter();
1805 let size_hint = iter.size_hint();
1806 let mut vtxos = Vec::<Vtxo<Full>>::with_capacity(size_hint.1.unwrap_or(size_hint.0));
1807 let mut amount = Amount::ZERO;
1808 for vref in iter {
1809 let id = vref.vtxo_id();
1814 if vtxos.iter().any(|v| v.id() == id) {
1815 bail!("duplicate VTXO id: {}", id);
1816 }
1817 let vtxo = if let Some(vtxo) = vref.into_full_vtxo() {
1818 vtxo
1819 } else {
1820 self.inner.db.get_full_vtxo(id).await?
1823 .with_context(|| format!("vtxo with id {} not found", id))?
1824 };
1825 amount += vtxo.amount();
1826 vtxos.push(vtxo);
1827 }
1828 (vtxos, amount)
1829 };
1830
1831 if vtxos.is_empty() {
1832 info!("Skipping refresh since no VTXOs are provided.");
1833 return Ok(None);
1834 }
1835 ensure!(total_amount >= P2TR_DUST,
1836 "vtxo amount must be at least {} to participate in a round",
1837 P2TR_DUST,
1838 );
1839
1840 let (_, ark_info) = self.require_server().await?;
1842 let current_height = self.inner.chain.tip().await?;
1843 let vtxo_fee_infos = vtxos.iter()
1844 .map(|v| VtxoFeeInfo::from_vtxo_and_tip(v, current_height));
1845 let fee = ark_info.fees.refresh.calculate(vtxo_fee_infos).context("fee overflowed")?;
1846 let output_amount = validate_and_subtract_fee_min_dust(total_amount, fee)?;
1847
1848 info!("Refreshing {} VTXOs (total amount = {}, fee = {}, output = {}).",
1849 vtxos.len(), total_amount, fee, output_amount,
1850 );
1851 let (user_keypair, _) = self.derive_store_next_keypair().await?;
1852 let req = VtxoRequest {
1853 policy: VtxoPolicy::Pubkey(PubkeyVtxoPolicy { user_pubkey: user_keypair.public_key() }),
1854 amount: output_amount,
1855 };
1856
1857 Ok(Some(RoundParticipation {
1858 inputs: vtxos,
1859 outputs: vec![req],
1860 unblinded_mailbox_id: None,
1861 }))
1862 }
1863
1864 pub async fn refresh_vtxos<V: VtxoRef>(
1869 &self,
1870 vtxos: impl IntoIterator<Item = V>,
1871 ) -> anyhow::Result<Option<RoundStatus>> {
1872 let mut participation = match self.build_refresh_participation(vtxos).await? {
1873 Some(participation) => participation,
1874 None => return Ok(None),
1875 };
1876
1877 if let Err(e) = self.add_should_refresh_vtxos(&mut participation).await {
1878 warn!("Error trying to add additional VTXOs that should be refreshed: {:#}", e);
1879 }
1880
1881 Ok(Some(self.participate_round(participation, Some(RoundMovement::Refresh)).await?))
1882 }
1883
1884 pub async fn refresh_vtxos_delegated<V: VtxoRef>(
1890 &self,
1891 vtxos: impl IntoIterator<Item = V>,
1892 ) -> anyhow::Result<Option<StoredRoundState<Unlocked>>> {
1893 let mut part = match self.build_refresh_participation(vtxos).await? {
1894 Some(participation) => participation,
1895 None => return Ok(None),
1896 };
1897
1898 if let Err(e) = self.add_should_refresh_vtxos(&mut part).await {
1899 warn!("Error trying to add additional VTXOs that should be refreshed: {:#}", e);
1900 }
1901
1902 Ok(Some(self.join_next_round_delegated(part, Some(RoundMovement::Refresh)).await?))
1903 }
1904
1905 pub async fn get_vtxos_to_refresh(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1908 let vtxos = self.spendable_vtxos_with(&RefreshStrategy::should_refresh_if_must(
1909 self,
1910 self.inner.chain.tip().await?,
1911 self.inner.chain.fee_rates().await.fast,
1912 )).await?;
1913 Ok(vtxos)
1914 }
1915
1916 pub async fn get_first_expiring_vtxo_blockheight(
1918 &self,
1919 ) -> anyhow::Result<Option<BlockHeight>> {
1920 Ok(self.spendable_vtxos().await?.iter().map(|v| v.expiry_height()).min())
1921 }
1922
1923 pub async fn get_next_required_refresh_blockheight(
1926 &self,
1927 ) -> anyhow::Result<Option<BlockHeight>> {
1928 let first_expiry = self.get_first_expiring_vtxo_blockheight().await?;
1929 Ok(first_expiry.map(|h| h.saturating_sub(self.inner.config.vtxo_refresh_expiry_threshold)))
1930 }
1931
1932 async fn select_vtxos_to_cover(
1938 &self,
1939 amount: Amount,
1940 ) -> anyhow::Result<Vec<WalletVtxo>> {
1941 let mut vtxos = self.spendable_vtxos().await?;
1942 self.sort_vtxos_for_selection(&mut vtxos);
1943
1944 let (last, _total_amount) = self.select_vtxos_inner(amount, &vtxos)?;
1945 vtxos.truncate(last+1);
1946 Ok(vtxos)
1947 }
1948
1949 async fn select_vtxos_to_cover_with_fee<F>(
1955 &self,
1956 amount: Amount,
1957 calc_fee: F,
1958 ) -> anyhow::Result<(Vec<WalletVtxo>, Amount)>
1959 where
1960 F: for<'a> Fn(
1961 Amount, std::iter::Copied<std::slice::Iter<'a, VtxoFeeInfo>>,
1962 ) -> anyhow::Result<Amount>,
1963 {
1964 let tip = self.inner.chain.tip().await?;
1965 let mut vtxos = self.spendable_vtxos().await?;
1966 self.sort_vtxos_for_selection(&mut vtxos);
1967
1968 let fee_info = vtxos.iter()
1969 .map(|v| VtxoFeeInfo::from_vtxo_and_tip(v, tip))
1970 .collect::<Vec<_>>();
1971
1972 const MAX_ITERATIONS: usize = 100;
1975 let mut fee = Amount::ZERO;
1976 for _ in 0..MAX_ITERATIONS {
1977 let required = amount.checked_add(fee)
1978 .context("Amount + fee overflow")?;
1979
1980 let (last, vtxo_amount) = self.select_vtxos_inner(required, &vtxos)
1981 .context("Could not find enough suitable VTXOs to cover payment + fees")?;
1982 fee = calc_fee(amount, fee_info[..=last].iter().copied())?;
1983
1984 if amount + fee <= vtxo_amount {
1985 trace!("Selected vtxos to cover amount + fee: amount = {}, fee = {}, total inputs = {}",
1986 amount, fee, vtxo_amount,
1987 );
1988 vtxos.truncate(last+1);
1989 return Ok((vtxos, fee));
1990 }
1991 trace!("VTXO sum of {} did not exceed amount {} and fee {}, iterating again",
1992 vtxo_amount, amount, fee,
1993 );
1994 }
1995 bail!("Fee calculation did not converge after maximum iterations")
1996 }
1997
1998 fn sort_vtxos_for_selection(&self, vtxos: &mut Vec<WalletVtxo>) {
2000 vtxos.sort_by_key(|v| v.expiry_height());
2001 }
2002
2003 fn select_vtxos_inner(
2009 &self,
2010 amount: Amount,
2011 vtxos: &Vec<WalletVtxo>,
2012 ) -> anyhow::Result<(usize, Amount)> {
2013 let mut total_amount = Amount::ZERO;
2015 for (i, vtxo) in vtxos.iter().enumerate() {
2016 total_amount += vtxo.amount();
2017
2018 if total_amount >= amount {
2019 return Ok((i, total_amount))
2020 }
2021 }
2022
2023 bail!("Insufficient money available. Needed {} but {} is available",
2024 amount, total_amount,
2025 );
2026 }
2027
2028 pub fn start_daemon(
2034 &self,
2035 onchain: Option<Arc<tokio::sync::RwLock<dyn DaemonizableOnchainWallet>>>,
2036 ) -> anyhow::Result<()> {
2037 let mut daemon = self.inner.daemon.lock();
2038 if daemon.is_some() {
2039 warn!("Called Wallet::start_daemon while daemon was already running.");
2040 return Ok(());
2041 }
2042
2043 let handle = crate::daemon::start_daemon(self.clone(), onchain);
2046 let _ = daemon.insert(handle);
2047
2048 Ok(())
2049 }
2050
2051 #[deprecated(since = "0.1.4", note = "use start_daemon instead")]
2053 pub fn run_daemon(
2054 &self,
2055 onchain: Option<Arc<tokio::sync::RwLock<dyn DaemonizableOnchainWallet>>>,
2056 ) -> anyhow::Result<()> {
2057 self.start_daemon(onchain)
2058 }
2059
2060 pub fn stop_daemon(&self) {
2062 let mut daemon = self.inner.daemon.lock();
2063 if let Some(handle) = daemon.take() {
2064 handle.stop();
2065 }
2066 }
2067
2068 pub async fn register_vtxo_transactions_with_server(
2072 &self,
2073 vtxos: &[impl AsRef<Vtxo<Full>>],
2074 ) -> anyhow::Result<()> {
2075 if vtxos.is_empty() {
2076 return Ok(());
2077 }
2078
2079 let (mut srv, _) = self.require_server().await?;
2080 srv.client.register_vtxo_transactions(protos::RegisterVtxoTransactionsRequest {
2081 vtxos: vtxos.iter().map(|v| v.as_ref().serialize()).collect(),
2082 }).await.context("failed to register vtxo transactions")?;
2083
2084 Ok(())
2085 }
2086}
2087
2088fn wrap_server_connect_error(err: ConnectError) -> anyhow::Error {
2089 match err {
2090 ConnectError::CreateEndpoint(CreateEndpointError::NoTransportBackend) => {
2091 anyhow!(MISSING_SERVER_TRANSPORT_HELP)
2092 },
2093 other => anyhow::Error::from(other),
2094 }
2095}
2096
2097impl std::ops::Drop for WalletInner {
2098 fn drop(&mut self) {
2099 if let Some(handle) = self.daemon.lock().take() {
2100 handle.stop();
2101 }
2102 }
2103}
2104
2105#[cfg(test)]
2106mod tests {
2107 use server_rpc::client::CreateEndpointError;
2108
2109 use super::{wrap_server_connect_error, MISSING_SERVER_TRANSPORT_HELP};
2110
2111 #[test]
2112 fn no_transport_connect_error_is_reworded_for_wallet_users() {
2113 let err = wrap_server_connect_error(CreateEndpointError::NoTransportBackend.into());
2114 assert!(err.to_string().contains(MISSING_SERVER_TRANSPORT_HELP));
2115 assert!(err.to_string().contains("feature `bark-wallet/native` or `bark-wallet/wasm-web`"));
2116 }
2117}