1pub extern crate ark;
287
288pub extern crate bip39;
289pub extern crate lightning_invoice;
290pub extern crate lnurl as lnurllib;
291
292#[macro_use] extern crate anyhow;
293#[macro_use] extern crate async_trait;
294#[macro_use] extern crate serde;
295
296pub mod chain;
297pub mod exit;
298pub mod movement;
299pub mod onchain;
300pub mod persist;
301pub mod round;
302pub mod subsystem;
303pub mod vtxo;
304
305#[cfg(feature = "pid-lock")]
306pub mod pid_lock;
307
308mod arkoor;
309mod board;
310mod config;
311mod daemon;
312mod fees;
313mod lightning;
314mod mailbox;
315mod notification;
316mod offboard;
317#[cfg(feature = "socks5-proxy")]
318mod proxy;
319mod psbtext;
320mod utils;
321
322pub use self::arkoor::ArkoorCreateResult;
323pub use self::config::{BarkNetwork, Config};
324pub use self::daemon::DaemonHandle;
325pub use self::fees::FeeEstimate;
326pub use self::notification::{WalletNotification, NotificationStream};
327pub use self::vtxo::WalletVtxo;
328
329use std::collections::HashSet;
330use std::sync::Arc;
331
332use anyhow::{bail, Context};
333use bip39::Mnemonic;
334use bitcoin::{Amount, Network, OutPoint};
335use bitcoin::bip32::{self, ChildNumber, Fingerprint};
336use bitcoin::secp256k1::{self, Keypair, PublicKey};
337use log::{trace, info, warn, error};
338use tokio::sync::{Mutex, RwLock};
339
340use ark::lightning::PaymentHash;
341
342use ark::{ArkInfo, ProtocolEncoding, Vtxo, VtxoId, VtxoPolicy, VtxoRequest};
343use ark::address::VtxoDelivery;
344use ark::fees::{validate_and_subtract_fee_min_dust, VtxoFeeInfo};
345use ark::vtxo::{Full, PubkeyVtxoPolicy, VtxoRef};
346use ark::vtxo::policy::signing::VtxoSigner;
347use bitcoin_ext::{BlockHeight, P2TR_DUST, TxStatus};
348use server_rpc::{protos, ServerConnection};
349
350use crate::chain::{ChainSource, ChainSourceSpec};
351use crate::exit::Exit;
352use crate::movement::{Movement, MovementStatus, PaymentMethod};
353use crate::movement::manager::MovementManager;
354use crate::movement::update::MovementUpdate;
355use crate::notification::NotificationDispatch;
356use crate::onchain::{ExitUnilaterally, PreparePsbt, SignPsbt, Utxo};
357use crate::onchain::DaemonizableOnchainWallet;
358use crate::persist::BarkPersister;
359use crate::persist::models::{PendingOffboard, RoundStateId, StoredRoundState, Unlocked};
360#[cfg(feature = "socks5-proxy")]
361use crate::proxy::proxy_for_url;
362use crate::round::{RoundParticipation, RoundStateLockIndex, RoundStatus};
363use crate::subsystem::{ArkoorMovement, RoundMovement};
364use crate::vtxo::{FilterVtxos, RefreshStrategy, VtxoFilter, VtxoState, VtxoStateKind};
365
366#[cfg(all(feature = "wasm-web", feature = "socks5-proxy"))]
367compile_error!("features `wasm-web` does not support feature `socks5-proxy");
368
369const BARK_PURPOSE_INDEX: u32 = 350;
371const VTXO_KEYS_INDEX: u32 = 0;
373const MAILBOX_KEY_INDEX: u32 = 1;
375const RECOVERY_MAILBOX_KEY_INDEX: u32 = 2;
377
378lazy_static::lazy_static! {
379 static ref SECP: secp256k1::Secp256k1<secp256k1::All> = secp256k1::Secp256k1::new();
381}
382
383fn log_server_pubkey_changed_error(expected: PublicKey, got: PublicKey) {
388 error!(
389 "
390Server public key has changed!
391
392The Ark server's public key is different from the one stored when this
393wallet was created. This typically happens when:
394
395 - The server operator has rotated their keys
396 - You are connecting to a different server
397 - The server has been replaced
398
399For safety, this wallet will not connect to the server until you
400resolve this. You can recover your funds on-chain by doing an emergency exit.
401
402This will exit your VTXOs to on-chain Bitcoin without needing the server's cooperation.
403
404Expected: {expected}
405Got: {got}")
406}
407
408#[derive(Debug, Clone)]
410pub struct LightningReceiveBalance {
411 pub total: Amount,
413 pub claimable: Amount,
415}
416
417#[derive(Debug, Clone)]
419pub struct Balance {
420 pub spendable: Amount,
422 pub pending_lightning_send: Amount,
424 pub claimable_lightning_receive: Amount,
426 pub pending_in_round: Amount,
428 pub pending_exit: Option<Amount>,
431 pub pending_board: Amount,
433}
434
435pub struct UtxoInfo {
436 pub outpoint: OutPoint,
437 pub amount: Amount,
438 pub confirmation_height: Option<u32>,
439}
440
441impl From<Utxo> for UtxoInfo {
442 fn from(value: Utxo) -> Self {
443 match value {
444 Utxo::Local(o) => UtxoInfo {
445 outpoint: o.outpoint,
446 amount: o.amount,
447 confirmation_height: o.confirmation_height,
448 },
449 Utxo::Exit(e) => UtxoInfo {
450 outpoint: e.vtxo.point(),
451 amount: e.vtxo.amount(),
452 confirmation_height: Some(e.height),
453 },
454 }
455 }
456}
457
458pub struct OffchainBalance {
461 pub available: Amount,
463 pub pending_in_round: Amount,
465 pub pending_exit: Amount,
468}
469
470#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
472pub struct WalletProperties {
473 pub network: Network,
477
478 pub fingerprint: Fingerprint,
482
483 pub server_pubkey: Option<PublicKey>,
490}
491
492pub struct WalletSeed {
498 master: bip32::Xpriv,
499 vtxo: bip32::Xpriv,
500}
501
502impl WalletSeed {
503 fn new(network: Network, seed: &[u8; 64]) -> Self {
504 let bark_path = [ChildNumber::from_hardened_idx(BARK_PURPOSE_INDEX).unwrap()];
505 let master = bip32::Xpriv::new_master(network, seed)
506 .expect("invalid seed")
507 .derive_priv(&SECP, &bark_path)
508 .expect("purpose is valid");
509
510 let vtxo_path = [ChildNumber::from_hardened_idx(VTXO_KEYS_INDEX).unwrap()];
511 let vtxo = master.derive_priv(&SECP, &vtxo_path)
512 .expect("vtxo path is valid");
513
514 Self { master, vtxo }
515 }
516
517 fn fingerprint(&self) -> Fingerprint {
518 self.master.fingerprint(&SECP)
519 }
520
521 fn derive_vtxo_keypair(&self, idx: u32) -> Keypair {
522 self.vtxo.derive_priv(&SECP, &[idx.into()]).unwrap().to_keypair(&SECP)
523 }
524
525 fn to_mailbox_keypair(&self) -> Keypair {
526 let mailbox_path = [ChildNumber::from_hardened_idx(MAILBOX_KEY_INDEX).unwrap()];
527 self.master.derive_priv(&SECP, &mailbox_path).unwrap().to_keypair(&SECP)
528 }
529
530 fn to_recovery_mailbox_keypair(&self) -> Keypair {
531 let mailbox_path = [ChildNumber::from_hardened_idx(RECOVERY_MAILBOX_KEY_INDEX).unwrap()];
532 self.master.derive_priv(&SECP, &mailbox_path).unwrap().to_keypair(&SECP)
533 }
534}
535
536pub struct Wallet {
669 pub chain: Arc<ChainSource>,
671
672 pub exit: RwLock<Exit>,
674
675 pub movements: Arc<MovementManager>,
677
678 notifications: NotificationDispatch,
680
681 config: Config,
683
684 db: Arc<dyn BarkPersister>,
686
687 seed: WalletSeed,
689
690 server: parking_lot::RwLock<Option<ServerConnection>>,
692
693 inflight_lightning_payments: Mutex<HashSet<PaymentHash>>,
696
697 round_state_lock_index: RoundStateLockIndex,
699}
700
701impl Wallet {
702 pub fn chain_source(
705 config: &Config,
706 ) -> anyhow::Result<ChainSourceSpec> {
707 if let Some(ref url) = config.esplora_address {
708 Ok(ChainSourceSpec::Esplora {
709 url: url.clone(),
710 })
711 } else if let Some(ref url) = config.bitcoind_address {
712 let auth = if let Some(ref c) = config.bitcoind_cookiefile {
713 bitcoin_ext::rpc::Auth::CookieFile(c.clone())
714 } else {
715 bitcoin_ext::rpc::Auth::UserPass(
716 config.bitcoind_user.clone().context("need bitcoind auth config")?,
717 config.bitcoind_pass.clone().context("need bitcoind auth config")?,
718 )
719 };
720 Ok(ChainSourceSpec::Bitcoind {
721 url: url.clone(),
722 auth,
723 })
724 } else {
725 bail!("Need to either provide esplora or bitcoind info");
726 }
727 }
728
729 pub fn require_chainsource_version(&self) -> anyhow::Result<()> {
733 self.chain.require_version()
734 }
735
736 pub async fn network(&self) -> anyhow::Result<Network> {
737 Ok(self.properties().await?.network)
738 }
739
740 pub async fn peek_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
743 let last_revealed = self.db.get_last_vtxo_key_index().await?;
744
745 let index = last_revealed.map(|i| i + 1).unwrap_or(u32::MIN);
746 let keypair = self.seed.derive_vtxo_keypair(index);
747
748 Ok((keypair, index))
749 }
750
751 pub async fn derive_store_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
754 let (keypair, index) = self.peek_next_keypair().await?;
755 self.db.store_vtxo_key(index, keypair.public_key()).await?;
756 Ok((keypair, index))
757 }
758
759 #[deprecated(note = "use peek_keypair instead")]
760 pub async fn peak_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
761 self.peek_keypair(index).await
762 }
763
764 pub async fn peek_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
778 let keypair = self.seed.derive_vtxo_keypair(index);
779 if self.db.get_public_key_idx(&keypair.public_key()).await?.is_some() {
780 Ok(keypair)
781 } else {
782 bail!("VTXO key {} does not exist, please derive it first", index)
783 }
784 }
785
786
787 pub async fn pubkey_keypair(&self, public_key: &PublicKey) -> anyhow::Result<Option<(u32, Keypair)>> {
799 if let Some(index) = self.db.get_public_key_idx(&public_key).await? {
800 Ok(Some((index, self.seed.derive_vtxo_keypair(index))))
801 } else {
802 Ok(None)
803 }
804 }
805
806 pub async fn get_vtxo_key(&self, vtxo: impl VtxoRef) -> anyhow::Result<Keypair> {
817 let wallet_vtxo = self.get_vtxo_by_id(vtxo.vtxo_id()).await?;
818 let pubkey = self.find_signable_clause(&wallet_vtxo.vtxo).await
819 .context("VTXO is not signable by wallet")?
820 .pubkey();
821 let idx = self.db.get_public_key_idx(&pubkey).await?
822 .context("VTXO key not found")?;
823 Ok(self.seed.derive_vtxo_keypair(idx))
824 }
825
826 #[deprecated(note = "use peek_address instead")]
827 pub async fn peak_address(&self, index: u32) -> anyhow::Result<ark::Address> {
828 self.peek_address(index).await
829 }
830
831 pub async fn peek_address(&self, index: u32) -> anyhow::Result<ark::Address> {
835 let (_, ark_info) = &self.require_server().await?;
836 let network = self.properties().await?.network;
837 let keypair = self.peek_keypair(index).await?;
838 let mailbox = self.mailbox_identifier();
839
840 Ok(ark::Address::builder()
841 .testnet(network != bitcoin::Network::Bitcoin)
842 .server_pubkey(ark_info.server_pubkey)
843 .pubkey_policy(keypair.public_key())
844 .mailbox(ark_info.mailbox_pubkey, mailbox, &keypair)
845 .expect("Failed to assign mailbox")
846 .into_address().unwrap())
847 }
848
849 pub async fn new_address_with_index(&self) -> anyhow::Result<(ark::Address, u32)> {
853 let (_, index) = self.derive_store_next_keypair().await?;
854 let addr = self.peek_address(index).await?;
855 Ok((addr, index))
856 }
857
858 pub async fn new_address(&self) -> anyhow::Result<ark::Address> {
860 let (addr, _) = self.new_address_with_index().await?;
861 Ok(addr)
862 }
863
864 pub async fn create(
870 mnemonic: &Mnemonic,
871 network: Network,
872 config: Config,
873 db: Arc<dyn BarkPersister>,
874 force: bool,
875 ) -> anyhow::Result<Wallet> {
876 trace!("Config: {:?}", config);
877 if let Some(existing) = db.read_properties().await? {
878 trace!("Existing config: {:?}", existing);
879 bail!("cannot overwrite already existing config")
880 }
881
882 let server_pubkey = if !force {
884 match Self::connect_to_server(&config, network).await {
885 Ok(conn) => {
886 let ark_info = conn.ark_info().await?;
887 Some(ark_info.server_pubkey)
888 }
889 Err(err) => {
890 bail!("Failed to connect to provided server (if you are sure use the --force flag): {:#}", err);
891 }
892 }
893 } else {
894 None
895 };
896
897 let wallet_fingerprint = WalletSeed::new(network, &mnemonic.to_seed("")).fingerprint();
898 let properties = WalletProperties {
899 network,
900 fingerprint: wallet_fingerprint,
901 server_pubkey,
902 };
903
904 db.init_wallet(&properties).await.context("cannot init wallet in the database")?;
906 info!("Created wallet with fingerprint: {}", wallet_fingerprint);
907 if let Some(pk) = server_pubkey {
908 info!("Stored server pubkey: {}", pk);
909 }
910
911 let wallet = Wallet::open(&mnemonic, db, config).await.context("failed to open wallet")?;
913 wallet.require_chainsource_version()?;
914
915 Ok(wallet)
916 }
917
918 pub async fn create_with_onchain(
926 mnemonic: &Mnemonic,
927 network: Network,
928 config: Config,
929 db: Arc<dyn BarkPersister>,
930 onchain: &dyn ExitUnilaterally,
931 force: bool,
932 ) -> anyhow::Result<Wallet> {
933 let mut wallet = Wallet::create(mnemonic, network, config, db, force).await?;
934 wallet.exit.get_mut().load(onchain).await?;
935 Ok(wallet)
936 }
937
938 pub async fn open(
940 mnemonic: &Mnemonic,
941 db: Arc<dyn BarkPersister>,
942 config: Config,
943 ) -> anyhow::Result<Wallet> {
944 let properties = db.read_properties().await?.context("Wallet is not initialised")?;
945
946 let seed = {
947 let seed = mnemonic.to_seed("");
948 WalletSeed::new(properties.network, &seed)
949 };
950
951 if properties.fingerprint != seed.fingerprint() {
952 bail!("incorrect mnemonic")
953 }
954
955 let chain_source = if let Some(ref url) = config.esplora_address {
956 ChainSourceSpec::Esplora {
957 url: url.clone(),
958 }
959 } else if let Some(ref url) = config.bitcoind_address {
960 let auth = if let Some(ref c) = config.bitcoind_cookiefile {
961 bitcoin_ext::rpc::Auth::CookieFile(c.clone())
962 } else {
963 bitcoin_ext::rpc::Auth::UserPass(
964 config.bitcoind_user.clone().context("need bitcoind auth config")?,
965 config.bitcoind_pass.clone().context("need bitcoind auth config")?,
966 )
967 };
968 ChainSourceSpec::Bitcoind { url: url.clone(), auth }
969 } else {
970 bail!("Need to either provide esplora or bitcoind info");
971 };
972
973 #[cfg(feature = "socks5-proxy")]
974 let chain_proxy = proxy_for_url(&config.socks5_proxy, chain_source.url())?;
975 let chain_source_client = ChainSource::new(
976 chain_source, properties.network, config.fallback_fee_rate,
977 #[cfg(feature = "socks5-proxy")] chain_proxy.as_deref(),
978 ).await?;
979 let chain = Arc::new(chain_source_client);
980
981 let server = match Self::connect_to_server(&config, properties.network).await {
982 Ok(s) => Some(s),
983 Err(e) => {
984 warn!("Ark server handshake failed: {:#}", e);
985 None
986 }
987 };
988 let server = parking_lot::RwLock::new(server);
989
990 let notifications = NotificationDispatch::new();
991 let movements = Arc::new(MovementManager::new(db.clone(), notifications.clone()));
992 let exit = RwLock::new(Exit::new(db.clone(), chain.clone(), movements.clone()).await?);
993
994 Ok(Wallet {
995 config, db, seed, exit, movements, notifications, server, chain,
996 inflight_lightning_payments: Mutex::new(HashSet::new()),
997 round_state_lock_index: RoundStateLockIndex::new(),
998 })
999 }
1000
1001 pub async fn open_with_onchain(
1004 mnemonic: &Mnemonic,
1005 db: Arc<dyn BarkPersister>,
1006 onchain: &dyn ExitUnilaterally,
1007 cfg: Config,
1008 ) -> anyhow::Result<Wallet> {
1009 let mut wallet = Wallet::open(mnemonic, db, cfg).await?;
1010 wallet.exit.get_mut().load(onchain).await?;
1011 Ok(wallet)
1012 }
1013
1014 pub async fn open_with_daemon(
1017 mnemonic: &Mnemonic,
1018 db: Arc<dyn BarkPersister>,
1019 cfg: Config,
1020 onchain: Option<Arc<RwLock<dyn DaemonizableOnchainWallet>>>,
1021 ) -> anyhow::Result<(Arc<Wallet>, DaemonHandle)> {
1022 let wallet = Arc::new(Wallet::open(mnemonic, db, cfg).await?);
1023 if let Some(onchain) = onchain.as_ref() {
1024 let mut onchain = onchain.write().await;
1025 wallet.exit.write().await.load(&mut *onchain).await?;
1026 }
1027
1028 let daemon = wallet.clone().run_daemon(onchain)?;
1029
1030 Ok((wallet, daemon))
1031 }
1032
1033 pub fn config(&self) -> &Config {
1035 &self.config
1036 }
1037
1038 pub async fn properties(&self) -> anyhow::Result<WalletProperties> {
1040 let properties = self.db.read_properties().await?.context("Wallet is not initialised")?;
1041 Ok(properties)
1042 }
1043
1044 pub fn fingerprint(&self) -> Fingerprint {
1046 self.seed.fingerprint()
1047 }
1048
1049 async fn connect_to_server(
1050 config: &Config,
1051 network: Network,
1052 ) -> anyhow::Result<ServerConnection> {
1053 let mut builder = ServerConnection::builder()
1054 .address(&config.server_address)
1055 .network(network);
1056
1057 #[cfg(feature = "socks5-proxy")]
1058 if let Some(proxy) = proxy_for_url(&config.socks5_proxy, &config.server_address)? {
1059 builder = builder.proxy(&proxy)
1060 }
1061
1062 if let Some(ref token) = config.server_access_token {
1063 builder = builder.access_token(token);
1064 }
1065
1066 builder.connect().await.context("Failed to connect to Ark server")
1067 }
1068
1069 async fn require_server(&self) -> anyhow::Result<(ServerConnection, ArkInfo)> {
1070 let conn = self.server.read().clone()
1071 .context("You should be connected to Ark server to perform this action")?;
1072 let ark_info = conn.ark_info().await?;
1073
1074 if let Some(stored_pubkey) = self.properties().await?.server_pubkey {
1076 if stored_pubkey != ark_info.server_pubkey {
1077 log_server_pubkey_changed_error(stored_pubkey, ark_info.server_pubkey);
1078 bail!("Server public key has changed. You should exit all your VTXOs!");
1079 }
1080 } else {
1081 self.db.set_server_pubkey(ark_info.server_pubkey).await?;
1083 info!("Stored server pubkey for existing wallet: {}", ark_info.server_pubkey);
1084 }
1085
1086 Ok((conn, ark_info))
1087 }
1088
1089 pub async fn refresh_server(&self) -> anyhow::Result<()> {
1090 let server = self.server.read().clone();
1091 let properties = self.properties().await?;
1092
1093 let srv = if let Some(srv) = server {
1094 srv.check_connection().await?;
1095 let ark_info = srv.ark_info().await?;
1096 ark_info.fees.validate().context("invalid fee schedule")?;
1097
1098 if let Some(stored_pubkey) = properties.server_pubkey {
1100 if stored_pubkey != ark_info.server_pubkey {
1101 log_server_pubkey_changed_error(stored_pubkey, ark_info.server_pubkey);
1102 bail!("Server public key has changed. You should exit all your VTXOs!");
1103 }
1104 } else {
1105 self.db.set_server_pubkey(ark_info.server_pubkey).await?;
1107 info!("Stored server pubkey for existing wallet: {}", ark_info.server_pubkey);
1108 }
1109
1110 srv
1111 } else {
1112 let conn = Self::connect_to_server(&self.config, properties.network).await?;
1113 let ark_info = conn.ark_info().await?;
1114 ark_info.fees.validate().context("invalid fee schedule")?;
1115
1116 if let Some(stored_pubkey) = properties.server_pubkey {
1118 if stored_pubkey != ark_info.server_pubkey {
1119 log_server_pubkey_changed_error(stored_pubkey, ark_info.server_pubkey);
1120 bail!("Server public key has changed. You should exit all your VTXOs!");
1121 }
1122 } else {
1123 self.db.set_server_pubkey(ark_info.server_pubkey).await?;
1125 info!("Stored server pubkey for existing wallet: {}", ark_info.server_pubkey);
1126 }
1127
1128 conn
1129 };
1130
1131 let _ = self.server.write().insert(srv);
1132
1133 Ok(())
1134 }
1135
1136 pub async fn ark_info(&self) -> anyhow::Result<Option<ArkInfo>> {
1138 let server = self.server.read().clone();
1139 match server.as_ref() {
1140 Some(srv) => Ok(Some(srv.ark_info().await?)),
1141 _ => Ok(None),
1142 }
1143 }
1144
1145 pub async fn balance(&self) -> anyhow::Result<Balance> {
1149 let vtxos = self.vtxos().await?;
1150
1151 let spendable = {
1152 let mut v = vtxos.iter().collect();
1153 VtxoStateKind::Spendable.filter_vtxos(&mut v).await?;
1154 v.into_iter().map(|v| v.amount()).sum::<Amount>()
1155 };
1156
1157 let pending_lightning_send = self.pending_lightning_send_vtxos().await?.iter()
1158 .map(|v| v.amount())
1159 .sum::<Amount>();
1160
1161 let claimable_lightning_receive = self.claimable_lightning_receive_balance().await?;
1162
1163 let pending_board = self.pending_board_vtxos().await?.iter()
1164 .map(|v| v.amount())
1165 .sum::<Amount>();
1166
1167 let pending_in_round = self.pending_round_balance().await?;
1168
1169 let pending_exit = self.exit.try_read().ok().map(|e| e.pending_total());
1170
1171 Ok(Balance {
1172 spendable,
1173 pending_in_round,
1174 pending_lightning_send,
1175 claimable_lightning_receive,
1176 pending_exit,
1177 pending_board,
1178 })
1179 }
1180
1181 pub async fn validate_vtxo(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<()> {
1183 let tx = self.chain.get_tx(&vtxo.chain_anchor().txid).await
1184 .context("could not fetch chain tx")?;
1185
1186 let tx = tx.with_context(|| {
1187 format!("vtxo chain anchor not found for vtxo: {}", vtxo.chain_anchor().txid)
1188 })?;
1189
1190 vtxo.validate(&tx)?;
1191
1192 Ok(())
1193 }
1194
1195 pub async fn import_vtxo(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<()> {
1205 if self.db.get_wallet_vtxo(vtxo.id()).await?.is_some() {
1206 info!("VTXO {} already exists in wallet, skipping import", vtxo.id());
1207 return Ok(());
1208 }
1209
1210 self.validate_vtxo(vtxo).await.context("VTXO validation failed")?;
1211
1212 if self.find_signable_clause(vtxo).await.is_none() {
1213 bail!("VTXO {} is not owned by this wallet (no signable clause found)", vtxo.id());
1214 }
1215
1216 let current_height = self.chain.tip().await?;
1217 if vtxo.expiry_height() <= current_height {
1218 bail!("Vtxo {} has expired", vtxo.id());
1219 }
1220
1221 self.store_spendable_vtxos([vtxo]).await.context("failed to store imported VTXO")?;
1222
1223 info!("Successfully imported VTXO {}", vtxo.id());
1224 Ok(())
1225 }
1226
1227 pub async fn get_vtxo_by_id(&self, vtxo_id: VtxoId) -> anyhow::Result<WalletVtxo> {
1229 let vtxo = self.db.get_wallet_vtxo(vtxo_id).await
1230 .with_context(|| format!("Error when querying vtxo {} in database", vtxo_id))?
1231 .with_context(|| format!("The VTXO with id {} cannot be found", vtxo_id))?;
1232 Ok(vtxo)
1233 }
1234
1235 #[deprecated(since="0.1.0-beta.5", note = "Use Wallet::history instead")]
1237 pub async fn movements(&self) -> anyhow::Result<Vec<Movement>> {
1238 self.history().await
1239 }
1240
1241 pub async fn history(&self) -> anyhow::Result<Vec<Movement>> {
1243 Ok(self.db.get_all_movements().await?)
1244 }
1245
1246 pub async fn history_by_payment_method(
1248 &self,
1249 payment_method: &PaymentMethod,
1250 ) -> anyhow::Result<Vec<Movement>> {
1251 let mut ret = self.db.get_movements_by_payment_method(payment_method).await?;
1252 ret.sort_by_key(|m| m.id);
1253 Ok(ret)
1254 }
1255
1256 pub async fn all_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1258 Ok(self.db.get_all_vtxos().await?)
1259 }
1260
1261 pub async fn vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1263 Ok(self.db.get_vtxos_by_state(&VtxoStateKind::UNSPENT_STATES).await?)
1264 }
1265
1266 pub async fn vtxos_with(&self, filter: &impl FilterVtxos) -> anyhow::Result<Vec<WalletVtxo>> {
1268 let mut vtxos = self.vtxos().await?;
1269 filter.filter_vtxos(&mut vtxos).await.context("error filtering vtxos")?;
1270 Ok(vtxos)
1271 }
1272
1273 pub async fn spendable_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1275 Ok(self.vtxos_with(&VtxoStateKind::Spendable).await?)
1276 }
1277
1278 pub async fn spendable_vtxos_with(
1280 &self,
1281 filter: &impl FilterVtxos,
1282 ) -> anyhow::Result<Vec<WalletVtxo>> {
1283 let mut vtxos = self.spendable_vtxos().await?;
1284 filter.filter_vtxos(&mut vtxos).await.context("error filtering vtxos")?;
1285 Ok(vtxos)
1286 }
1287
1288 pub async fn get_expiring_vtxos(
1290 &self,
1291 threshold: BlockHeight,
1292 ) -> anyhow::Result<Vec<WalletVtxo>> {
1293 let expiry = self.chain.tip().await? + threshold;
1294 let filter = VtxoFilter::new(&self).expires_before(expiry);
1295 Ok(self.spendable_vtxos_with(&filter).await?)
1296 }
1297
1298 pub async fn sync_pending_offboards(&self) -> anyhow::Result<()> {
1304 let pending_offboards: Vec<PendingOffboard> = self.db.get_pending_offboards().await?;
1305
1306 if pending_offboards.is_empty() {
1307 return Ok(());
1308 }
1309
1310 let current_height = self.chain.tip().await?;
1311 let required_confs = self.config.offboard_required_confirmations;
1312
1313 trace!("Checking {} pending offboard transaction(s)", pending_offboards.len());
1314
1315 for pending in pending_offboards {
1316 let status = self.chain.tx_status(pending.offboard_txid).await;
1317
1318 match status {
1319 Ok(TxStatus::Confirmed(block_ref)) => {
1320 let confs = current_height - (block_ref.height - 1);
1321 if confs < required_confs as BlockHeight {
1322 trace!(
1323 "Offboard tx {} has {}/{} confirmations, waiting...",
1324 pending.offboard_txid, confs, required_confs,
1325 );
1326 continue;
1327 }
1328
1329 info!(
1330 "Offboard tx {} confirmed, finalizing movement {}",
1331 pending.offboard_txid, pending.movement_id,
1332 );
1333
1334 for vtxo_id in &pending.vtxo_ids {
1336 if let Err(e) = self.db.update_vtxo_state_checked(
1337 *vtxo_id,
1338 VtxoState::Spent,
1339 &[VtxoStateKind::Locked],
1340 ).await {
1341 warn!("Failed to mark vtxo {} as spent: {:#}", vtxo_id, e);
1342 }
1343 }
1344
1345 if let Err(e) = self.movements.finish_movement(
1347 pending.movement_id,
1348 MovementStatus::Successful,
1349 ).await {
1350 warn!("Failed to finish movement {}: {:#}", pending.movement_id, e);
1351 }
1352
1353 self.db.remove_pending_offboard(pending.movement_id).await?;
1354 }
1355 Ok(TxStatus::Mempool) => {
1356 if required_confs == 0 {
1357 info!(
1358 "Offboard tx {} in mempool with 0 required confirmations, \
1359 finalizing movement {}",
1360 pending.offboard_txid, pending.movement_id,
1361 );
1362
1363 for vtxo_id in &pending.vtxo_ids {
1365 if let Err(e) = self.db.update_vtxo_state_checked(
1366 *vtxo_id,
1367 VtxoState::Spent,
1368 &[VtxoStateKind::Locked],
1369 ).await {
1370 warn!("Failed to mark vtxo {} as spent: {:#}", vtxo_id, e);
1371 }
1372 }
1373
1374 if let Err(e) = self.movements.finish_movement(
1376 pending.movement_id,
1377 MovementStatus::Successful,
1378 ).await {
1379 warn!("Failed to finish movement {}: {:#}", pending.movement_id, e);
1380 }
1381
1382 self.db.remove_pending_offboard(pending.movement_id).await?;
1383 } else {
1384 trace!(
1385 "Offboard tx {} still in mempool, waiting...",
1386 pending.offboard_txid,
1387 );
1388 }
1389 }
1390 Ok(TxStatus::NotFound) => {
1391 let age = chrono::Local::now() - pending.created_at;
1395 if age < chrono::Duration::hours(1) {
1396 trace!(
1397 "Offboard tx {} not found, but only {} minutes old — waiting...",
1398 pending.offboard_txid, age.num_minutes(),
1399 );
1400 continue;
1401 }
1402
1403 warn!(
1404 "Offboard tx {} not found after {} minutes, canceling movement {}",
1405 pending.offboard_txid, age.num_minutes(), pending.movement_id,
1406 );
1407
1408 for vtxo_id in &pending.vtxo_ids {
1410 if let Err(e) = self.db.update_vtxo_state_checked(
1411 *vtxo_id,
1412 VtxoState::Spendable,
1413 &[VtxoStateKind::Locked],
1414 ).await {
1415 warn!("Failed to restore vtxo {} to spendable: {:#}", vtxo_id, e);
1416 }
1417 }
1418
1419 if let Err(e) = self.movements.finish_movement(
1421 pending.movement_id,
1422 MovementStatus::Failed,
1423 ).await {
1424 warn!("Failed to fail movement {}: {:#}", pending.movement_id, e);
1425 }
1426
1427 self.db.remove_pending_offboard(pending.movement_id).await?;
1428 }
1429 Err(e) => {
1430 warn!(
1431 "Failed to check status of offboard tx {}: {:#}",
1432 pending.offboard_txid, e,
1433 );
1434 }
1435 }
1436 }
1437
1438 Ok(())
1439 }
1440
1441 pub async fn maintenance(&self) -> anyhow::Result<()> {
1447 info!("Starting wallet maintenance in interactive mode");
1448 self.sync().await;
1449
1450 let rounds = self.progress_pending_rounds(None).await;
1451 if let Err(e) = rounds.as_ref() {
1452 warn!("Error progressing pending rounds: {:#}", e);
1453 }
1454 let refresh = self.maintenance_refresh().await;
1455 if let Err(e) = refresh.as_ref() {
1456 warn!("Error refreshing VTXOs: {:#}", e);
1457 }
1458 if rounds.is_err() || refresh.is_err() {
1459 bail!("Maintenance encountered errors.\nprogress_rounds: {:#?}\nrefresh: {:#?}", rounds, refresh);
1460 }
1461 Ok(())
1462 }
1463
1464 pub async fn maintenance_delegated(&self) -> anyhow::Result<()> {
1470 info!("Starting wallet maintenance in delegated mode");
1471 self.sync().await;
1472 let rounds = self.progress_pending_rounds(None).await;
1473 if let Err(e) = rounds.as_ref() {
1474 warn!("Error progressing pending rounds: {:#}", e);
1475 }
1476 let refresh = self.maybe_schedule_maintenance_refresh_delegated().await;
1477 if let Err(e) = refresh.as_ref() {
1478 warn!("Error refreshing VTXOs: {:#}", e);
1479 }
1480 if rounds.is_err() || refresh.is_err() {
1481 bail!("Delegated maintenance encountered errors.\nprogress_rounds: {:#?}\nrefresh: {:#?}", rounds, refresh);
1482 }
1483 Ok(())
1484 }
1485
1486 pub async fn maintenance_with_onchain<W: PreparePsbt + SignPsbt + ExitUnilaterally>(
1494 &self,
1495 onchain: &mut W,
1496 ) -> anyhow::Result<()> {
1497 info!("Starting wallet maintenance in interactive mode with onchain wallet");
1498
1499 let maintenance = self.maintenance().await;
1501
1502 let exit_sync = self.sync_exits(onchain).await;
1504 if let Err(e) = exit_sync.as_ref() {
1505 warn!("Error syncing exits: {:#}", e);
1506 }
1507 let exit_progress = self.exit.write().await.progress_exits(&self, onchain, None).await;
1508 if let Err(e) = exit_progress.as_ref() {
1509 warn!("Error progressing exits: {:#}", e);
1510 }
1511 if maintenance.is_err() || exit_sync.is_err() || exit_progress.is_err() {
1512 bail!("Maintenance encountered errors.\nmaintenance: {:#?}\nexit_sync: {:#?}\nexit_progress: {:#?}", maintenance, exit_sync, exit_progress);
1513 }
1514 Ok(())
1515 }
1516
1517 pub async fn maintenance_with_onchain_delegated<W: PreparePsbt + SignPsbt + ExitUnilaterally>(
1524 &self,
1525 onchain: &mut W,
1526 ) -> anyhow::Result<()> {
1527 info!("Starting wallet maintenance in delegated mode with onchain wallet");
1528
1529 let maintenance = self.maintenance_delegated().await;
1531
1532 let exit_sync = self.sync_exits(onchain).await;
1534 if let Err(e) = exit_sync.as_ref() {
1535 warn!("Error syncing exits: {:#}", e);
1536 }
1537 let exit_progress = self.exit.write().await.progress_exits(&self, onchain, None).await;
1538 if let Err(e) = exit_progress.as_ref() {
1539 warn!("Error progressing exits: {:#}", e);
1540 }
1541 if maintenance.is_err() || exit_sync.is_err() || exit_progress.is_err() {
1542 bail!("Delegated maintenance encountered errors.\nmaintenance: {:#?}\nexit_sync: {:#?}\nexit_progress: {:#?}", maintenance, exit_sync, exit_progress);
1543 }
1544 Ok(())
1545 }
1546
1547 pub async fn maybe_schedule_maintenance_refresh(&self) -> anyhow::Result<Option<RoundStateId>> {
1555 let vtxos = self.get_vtxos_to_refresh().await?;
1556 if vtxos.len() == 0 {
1557 return Ok(None);
1558 }
1559
1560 let participation = match self.build_refresh_participation(vtxos).await? {
1561 Some(participation) => participation,
1562 None => return Ok(None),
1563 };
1564
1565 info!("Scheduling maintenance refresh ({} vtxos)", participation.inputs.len());
1566 let state = self.join_next_round(participation, Some(RoundMovement::Refresh)).await?;
1567 Ok(Some(state.id()))
1568 }
1569
1570 pub async fn maybe_schedule_maintenance_refresh_delegated(
1578 &self,
1579 ) -> anyhow::Result<Option<RoundStateId>> {
1580 let vtxos = self.get_vtxos_to_refresh().await?;
1581 if vtxos.len() == 0 {
1582 return Ok(None);
1583 }
1584
1585 let participation = match self.build_refresh_participation(vtxos).await? {
1586 Some(participation) => participation,
1587 None => return Ok(None),
1588 };
1589
1590 info!("Scheduling delegated maintenance refresh ({} vtxos)", participation.inputs.len());
1591 let state = self.join_next_round_delegated(participation, Some(RoundMovement::Refresh)).await?;
1592 Ok(Some(state.id()))
1593 }
1594
1595 pub async fn maintenance_refresh(&self) -> anyhow::Result<Option<RoundStatus>> {
1603 let vtxos = self.get_vtxos_to_refresh().await?;
1604 if vtxos.len() == 0 {
1605 return Ok(None);
1606 }
1607
1608 info!("Performing maintenance refresh");
1609 self.refresh_vtxos(vtxos).await
1610 }
1611
1612 pub async fn sync(&self) {
1618 futures::join!(
1619 async {
1620 if let Err(e) = self.chain.update_fee_rates(self.config.fallback_fee_rate).await {
1623 warn!("Error updating fee rates: {:#}", e);
1624 }
1625 },
1626 async {
1627 if let Err(e) = self.sync_mailbox().await {
1628 warn!("Error in mailbox sync: {:#}", e);
1629 }
1630 },
1631 async {
1632 if let Err(e) = self.sync_pending_rounds().await {
1633 warn!("Error while trying to progress rounds awaiting confirmations: {:#}", e);
1634 }
1635 },
1636 async {
1637 if let Err(e) = self.sync_pending_lightning_send_vtxos().await {
1638 warn!("Error syncing pending lightning payments: {:#}", e);
1639 }
1640 },
1641 async {
1642 if let Err(e) = self.try_claim_all_lightning_receives(false).await {
1643 warn!("Error claiming pending lightning receives: {:#}", e);
1644 }
1645 },
1646 async {
1647 if let Err(e) = self.sync_pending_boards().await {
1648 warn!("Error syncing pending boards: {:#}", e);
1649 }
1650 },
1651 async {
1652 if let Err(e) = self.sync_pending_offboards().await {
1653 warn!("Error syncing pending offboards: {:#}", e);
1654 }
1655 }
1656 );
1657 }
1658
1659 pub async fn sync_exits(
1665 &self,
1666 onchain: &mut dyn ExitUnilaterally,
1667 ) -> anyhow::Result<()> {
1668 self.exit.write().await.sync(&self, onchain).await?;
1669 Ok(())
1670 }
1671
1672 pub async fn dangerous_drop_vtxo(&self, vtxo_id: VtxoId) -> anyhow::Result<()> {
1675 warn!("Drop vtxo {} from the database", vtxo_id);
1676 self.db.remove_vtxo(vtxo_id).await?;
1677 Ok(())
1678 }
1679
1680 pub async fn dangerous_drop_all_vtxos(&self) -> anyhow::Result<()> {
1683 warn!("Dropping all vtxos from the db...");
1684 for vtxo in self.vtxos().await? {
1685 self.db.remove_vtxo(vtxo.id()).await?;
1686 }
1687
1688 self.exit.write().await.dangerous_clear_exit().await?;
1689 Ok(())
1690 }
1691
1692 async fn has_counterparty_risk(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<bool> {
1697 for past_pks in vtxo.past_arkoor_pubkeys() {
1698 let mut owns_any = false;
1699 for past_pk in past_pks {
1700 if self.db.get_public_key_idx(&past_pk).await?.is_some() {
1701 owns_any = true;
1702 break;
1703 }
1704 }
1705 if !owns_any {
1706 return Ok(true);
1707 }
1708 }
1709
1710 let my_clause = self.find_signable_clause(vtxo).await;
1711 Ok(!my_clause.is_some())
1712 }
1713
1714 async fn add_should_refresh_vtxos(
1720 &self,
1721 participation: &mut RoundParticipation,
1722 ) -> anyhow::Result<()> {
1723 let tip = self.chain.tip().await?;
1726 let mut vtxos_to_refresh = self.spendable_vtxos_with(
1727 &RefreshStrategy::should_refresh(self, tip, self.chain.fee_rates().await.fast),
1728 ).await?;
1729 if vtxos_to_refresh.is_empty() {
1730 return Ok(());
1731 }
1732
1733 let excluded_ids = participation.inputs.iter().map(|v| v.vtxo_id())
1734 .collect::<HashSet<_>>();
1735 let mut total_amount = Amount::ZERO;
1736 for i in (0..vtxos_to_refresh.len()).rev() {
1737 let vtxo = &vtxos_to_refresh[i];
1738 if excluded_ids.contains(&vtxo.id()) {
1739 vtxos_to_refresh.swap_remove(i);
1740 continue;
1741 }
1742 total_amount += vtxo.amount();
1743 }
1744 if vtxos_to_refresh.is_empty() {
1745 return Ok(());
1747 }
1748
1749 let (_, ark_info) = self.require_server().await?;
1752 let fee = ark_info.fees.refresh.calculate_no_base_fee(
1753 vtxos_to_refresh.iter().map(|wv| VtxoFeeInfo::from_vtxo_and_tip(&wv.vtxo, tip)),
1754 ).context("fee overflowed")?;
1755
1756 let output_amount = match validate_and_subtract_fee_min_dust(total_amount, fee) {
1758 Ok(amount) => amount,
1759 Err(e) => {
1760 trace!("Cannot add should-refresh VTXOs: {}", e);
1761 return Ok(());
1762 },
1763 };
1764 info!(
1765 "Adding {} extra VTXOs to round participation total = {}, fee = {}, output = {}",
1766 vtxos_to_refresh.len(), total_amount, fee, output_amount,
1767 );
1768 let (user_keypair, _) = self.derive_store_next_keypair().await?;
1769 let req = VtxoRequest {
1770 policy: VtxoPolicy::new_pubkey(user_keypair.public_key()),
1771 amount: output_amount,
1772 };
1773 participation.inputs.reserve(vtxos_to_refresh.len());
1774 participation.inputs.extend(vtxos_to_refresh.into_iter().map(|wv| wv.vtxo));
1775 participation.outputs.push(req);
1776
1777 Ok(())
1778 }
1779
1780 pub async fn build_refresh_participation<V: VtxoRef>(
1781 &self,
1782 vtxos: impl IntoIterator<Item = V>,
1783 ) -> anyhow::Result<Option<RoundParticipation>> {
1784 let (vtxos, total_amount) = {
1785 let iter = vtxos.into_iter();
1786 let size_hint = iter.size_hint();
1787 let mut vtxos = Vec::<Vtxo<Full>>::with_capacity(size_hint.1.unwrap_or(size_hint.0));
1788 let mut amount = Amount::ZERO;
1789 for vref in iter {
1790 let id = vref.vtxo_id();
1795 if vtxos.iter().any(|v| v.id() == id) {
1796 bail!("duplicate VTXO id: {}", id);
1797 }
1798 let vtxo = if let Some(vtxo) = vref.into_full_vtxo() {
1799 vtxo
1800 } else {
1801 self.get_vtxo_by_id(id).await
1802 .with_context(|| format!("vtxo with id {} not found", id))?.vtxo
1803 };
1804 amount += vtxo.amount();
1805 vtxos.push(vtxo);
1806 }
1807 (vtxos, amount)
1808 };
1809
1810 if vtxos.is_empty() {
1811 info!("Skipping refresh since no VTXOs are provided.");
1812 return Ok(None);
1813 }
1814 ensure!(total_amount >= P2TR_DUST,
1815 "vtxo amount must be at least {} to participate in a round",
1816 P2TR_DUST,
1817 );
1818
1819 let (_, ark_info) = self.require_server().await?;
1821 let current_height = self.chain.tip().await?;
1822 let vtxo_fee_infos = vtxos.iter()
1823 .map(|v| VtxoFeeInfo::from_vtxo_and_tip(v, current_height));
1824 let fee = ark_info.fees.refresh.calculate(vtxo_fee_infos).context("fee overflowed")?;
1825 let output_amount = validate_and_subtract_fee_min_dust(total_amount, fee)?;
1826
1827 info!("Refreshing {} VTXOs (total amount = {}, fee = {}, output = {}).",
1828 vtxos.len(), total_amount, fee, output_amount,
1829 );
1830 let (user_keypair, _) = self.derive_store_next_keypair().await?;
1831 let req = VtxoRequest {
1832 policy: VtxoPolicy::Pubkey(PubkeyVtxoPolicy { user_pubkey: user_keypair.public_key() }),
1833 amount: output_amount,
1834 };
1835
1836 Ok(Some(RoundParticipation {
1837 inputs: vtxos,
1838 outputs: vec![req],
1839 unblinded_mailbox_id: None,
1840 }))
1841 }
1842
1843 pub async fn refresh_vtxos<V: VtxoRef>(
1848 &self,
1849 vtxos: impl IntoIterator<Item = V>,
1850 ) -> anyhow::Result<Option<RoundStatus>> {
1851 let mut participation = match self.build_refresh_participation(vtxos).await? {
1852 Some(participation) => participation,
1853 None => return Ok(None),
1854 };
1855
1856 if let Err(e) = self.add_should_refresh_vtxos(&mut participation).await {
1857 warn!("Error trying to add additional VTXOs that should be refreshed: {:#}", e);
1858 }
1859
1860 Ok(Some(self.participate_round(participation, Some(RoundMovement::Refresh)).await?))
1861 }
1862
1863 pub async fn refresh_vtxos_delegated<V: VtxoRef>(
1869 &self,
1870 vtxos: impl IntoIterator<Item = V>,
1871 ) -> anyhow::Result<Option<StoredRoundState<Unlocked>>> {
1872 let mut part = 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 part).await {
1878 warn!("Error trying to add additional VTXOs that should be refreshed: {:#}", e);
1879 }
1880
1881 Ok(Some(self.join_next_round_delegated(part, Some(RoundMovement::Refresh)).await?))
1882 }
1883
1884 pub async fn get_vtxos_to_refresh(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1887 let vtxos = self.spendable_vtxos_with(&RefreshStrategy::should_refresh_if_must(
1888 self,
1889 self.chain.tip().await?,
1890 self.chain.fee_rates().await.fast,
1891 )).await?;
1892 Ok(vtxos)
1893 }
1894
1895 pub async fn get_first_expiring_vtxo_blockheight(
1897 &self,
1898 ) -> anyhow::Result<Option<BlockHeight>> {
1899 Ok(self.spendable_vtxos().await?.iter().map(|v| v.expiry_height()).min())
1900 }
1901
1902 pub async fn get_next_required_refresh_blockheight(
1905 &self,
1906 ) -> anyhow::Result<Option<BlockHeight>> {
1907 let first_expiry = self.get_first_expiring_vtxo_blockheight().await?;
1908 Ok(first_expiry.map(|h| h.saturating_sub(self.config.vtxo_refresh_expiry_threshold)))
1909 }
1910
1911 async fn select_vtxos_to_cover(
1917 &self,
1918 amount: Amount,
1919 ) -> anyhow::Result<Vec<WalletVtxo>> {
1920 let mut vtxos = self.spendable_vtxos().await?;
1921 vtxos.sort_by_key(|v| v.expiry_height());
1922
1923 let mut result = Vec::new();
1925 let mut total_amount = Amount::ZERO;
1926 for input in vtxos {
1927 total_amount += input.amount();
1928 result.push(input);
1929
1930 if total_amount >= amount {
1931 return Ok(result)
1932 }
1933 }
1934
1935 bail!("Insufficient money available. Needed {} but {} is available",
1936 amount, total_amount,
1937 );
1938 }
1939
1940 async fn select_vtxos_to_cover_with_fee<F>(
1946 &self,
1947 amount: Amount,
1948 calc_fee: F,
1949 ) -> anyhow::Result<(Vec<WalletVtxo>, Amount)>
1950 where
1951 F: for<'a> Fn(
1952 Amount, std::iter::Copied<std::slice::Iter<'a, VtxoFeeInfo>>,
1953 ) -> anyhow::Result<Amount>,
1954 {
1955 let tip = self.chain.tip().await?;
1956
1957 const MAX_ITERATIONS: usize = 100;
1960 let mut fee = Amount::ZERO;
1961 let mut fee_info = Vec::new();
1962 for _ in 0..MAX_ITERATIONS {
1963 let required = amount.checked_add(fee)
1964 .context("Amount + fee overflow")?;
1965
1966 let vtxos = self.select_vtxos_to_cover(required).await
1967 .context("Could not find enough suitable VTXOs to cover payment + fees")?;
1968
1969 fee_info.reserve(vtxos.len());
1970 let mut vtxo_amount = Amount::ZERO;
1971 for vtxo in &vtxos {
1972 vtxo_amount += vtxo.amount();
1973 fee_info.push(VtxoFeeInfo::from_vtxo_and_tip(vtxo, tip));
1974 }
1975
1976 fee = calc_fee(amount, fee_info.iter().copied())?;
1977 if amount + fee <= vtxo_amount {
1978 trace!("Selected vtxos to cover amount + fee: amount = {}, fee = {}, total inputs = {}",
1979 amount, fee, vtxo_amount,
1980 );
1981 return Ok((vtxos, fee));
1982 }
1983 trace!("VTXO sum of {} did not exceed amount {} and fee {}, iterating again",
1984 vtxo_amount, amount, fee,
1985 );
1986 fee_info.clear();
1987 }
1988 bail!("Fee calculation did not converge after maximum iterations")
1989 }
1990
1991 pub fn run_daemon(
1997 self: &Arc<Self>,
1998 onchain: Option<Arc<RwLock<dyn DaemonizableOnchainWallet>>>,
1999 ) -> anyhow::Result<DaemonHandle> {
2000 Ok(crate::daemon::start_daemon(self.clone(), onchain))
2003 }
2004
2005 pub async fn register_vtxos_with_server(
2009 &self,
2010 vtxos: &[impl AsRef<Vtxo<Full>>],
2011 ) -> anyhow::Result<()> {
2012 if vtxos.is_empty() {
2013 return Ok(());
2014 }
2015
2016 let (mut srv, _) = self.require_server().await?;
2017 srv.client.register_vtxos(protos::RegisterVtxosRequest {
2018 vtxos: vtxos.iter().map(|v| v.as_ref().serialize()).collect(),
2019 }).await.context("failed to register vtxos")?;
2020
2021 Ok(())
2022 }
2023}