1pub extern crate ark;
283
284pub extern crate bip39;
285pub extern crate lightning_invoice;
286pub extern crate lnurl as lnurllib;
287
288#[macro_use] extern crate anyhow;
289#[macro_use] extern crate async_trait;
290#[macro_use] extern crate serde;
291
292pub mod chain;
293pub mod exit;
294pub mod movement;
295pub mod onchain;
296pub mod persist;
297pub mod round;
298pub mod subsystem;
299pub mod vtxo;
300
301mod arkoor;
302mod config;
303mod daemon;
304mod lightning;
305mod offboard;
306mod psbtext;
307mod server;
308
309pub use self::arkoor::ArkoorCreateResult;
310pub use self::config::{BarkNetwork, Config};
311pub use self::daemon::DaemonHandle;
312pub use self::persist::sqlite::SqliteClient;
313pub use self::vtxo::WalletVtxo;
314
315use std::collections::{HashMap, HashSet};
316use std::sync::Arc;
317
318use anyhow::{bail, Context};
319use bip39::Mnemonic;
320use bitcoin::{Amount, Network, OutPoint};
321use bitcoin::bip32::{self, ChildNumber, Fingerprint};
322use bitcoin::secp256k1::{self, Keypair, PublicKey};
323use log::{trace, debug, info, warn, error};
324use tokio::sync::RwLock;
325
326use ark::{ArkInfo, ProtocolEncoding, Vtxo, VtxoId, VtxoPolicy, VtxoRequest};
327use ark::address::VtxoDelivery;
328use ark::board::{BoardBuilder, BOARD_FUNDING_TX_VTXO_VOUT};
329use ark::vtxo::{PubkeyVtxoPolicy, VtxoRef};
330use ark::vtxo::policy::signing::VtxoSigner;
331use bitcoin_ext::{BlockHeight, P2TR_DUST, TxStatus};
332use server_rpc::{self as rpc, protos, ServerConnection};
333
334use crate::chain::{ChainSource, ChainSourceSpec};
335use crate::exit::Exit;
336use crate::movement::{Movement, MovementStatus};
337use crate::movement::manager::MovementManager;
338use crate::movement::update::MovementUpdate;
339use crate::onchain::{DaemonizableOnchainWallet, ExitUnilaterally, PreparePsbt, SignPsbt, Utxo};
340use crate::persist::{BarkPersister, RoundStateId};
341use crate::persist::models::{LightningReceive, LightningSend, PendingBoard};
342use crate::round::{RoundParticipation, RoundStatus};
343use crate::subsystem::{ArkoorMovement, BoardMovement, RoundMovement, Subsystem};
344use crate::vtxo::{FilterVtxos, RefreshStrategy, VtxoFilter, VtxoState, VtxoStateKind};
345
346const BARK_PURPOSE_INDEX: u32 = 350;
348const VTXO_KEYS_INDEX: u32 = 0;
350const MAILBOX_KEY_INDEX: u32 = 1;
352
353lazy_static::lazy_static! {
354 static ref SECP: secp256k1::Secp256k1<secp256k1::All> = secp256k1::Secp256k1::new();
356}
357
358#[derive(Debug, Clone)]
360pub struct LightningReceiveBalance {
361 pub total: Amount,
363 pub claimable: Amount,
365}
366
367#[derive(Debug, Clone)]
369pub struct Balance {
370 pub spendable: Amount,
372 pub pending_lightning_send: Amount,
374 pub claimable_lightning_receive: Amount,
376 pub pending_in_round: Amount,
378 pub pending_exit: Option<Amount>,
381 pub pending_board: Amount,
383}
384
385pub struct UtxoInfo {
386 pub outpoint: OutPoint,
387 pub amount: Amount,
388 pub confirmation_height: Option<u32>,
389}
390
391impl From<Utxo> for UtxoInfo {
392 fn from(value: Utxo) -> Self {
393 match value {
394 Utxo::Local(o) => UtxoInfo {
395 outpoint: o.outpoint,
396 amount: o.amount,
397 confirmation_height: o.confirmation_height,
398 },
399 Utxo::Exit(e) => UtxoInfo {
400 outpoint: e.vtxo.point(),
401 amount: e.vtxo.amount(),
402 confirmation_height: Some(e.height),
403 },
404 }
405 }
406}
407
408pub struct OffchainBalance {
411 pub available: Amount,
413 pub pending_in_round: Amount,
415 pub pending_exit: Amount,
418}
419
420#[derive(Debug, Clone)]
422pub struct WalletProperties {
423 pub network: Network,
427
428 pub fingerprint: Fingerprint,
432}
433
434pub struct WalletSeed {
440 master: bip32::Xpriv,
441 vtxo: bip32::Xpriv,
442}
443
444impl WalletSeed {
445 fn new(network: Network, seed: &[u8; 64]) -> Self {
446 let bark_path = [ChildNumber::from_hardened_idx(BARK_PURPOSE_INDEX).unwrap()];
447 let master = bip32::Xpriv::new_master(network, seed)
448 .expect("invalid seed")
449 .derive_priv(&SECP, &bark_path)
450 .expect("purpose is valid");
451
452 let vtxo_path = [ChildNumber::from_hardened_idx(VTXO_KEYS_INDEX).unwrap()];
453 let vtxo = master.derive_priv(&SECP, &vtxo_path)
454 .expect("vtxo path is valid");
455
456 Self { master, vtxo }
457 }
458
459 fn fingerprint(&self) -> Fingerprint {
460 self.master.fingerprint(&SECP)
461 }
462
463 fn derive_vtxo_keypair(&self, idx: u32) -> Keypair {
464 self.vtxo.derive_priv(&SECP, &[idx.into()]).unwrap().to_keypair(&SECP)
465 }
466
467 #[allow(unused)]
468 fn to_mailbox_keypair(&self) -> Keypair {
469 let mailbox_path = [ChildNumber::from_hardened_idx(MAILBOX_KEY_INDEX).unwrap()];
470 self.master.derive_priv(&SECP, &mailbox_path).unwrap().to_keypair(&SECP)
471 }
472}
473
474pub struct Wallet {
607 pub chain: Arc<ChainSource>,
609
610 pub exit: RwLock<Exit>,
612
613 pub movements: Arc<MovementManager>,
615
616 config: Config,
618
619 db: Arc<dyn BarkPersister>,
621
622 seed: WalletSeed,
624
625 server: parking_lot::RwLock<Option<ServerConnection>>,
627}
628
629impl Wallet {
630 pub fn chain_source(
633 config: &Config,
634 ) -> anyhow::Result<ChainSourceSpec> {
635 if let Some(ref url) = config.esplora_address {
636 Ok(ChainSourceSpec::Esplora {
637 url: url.clone(),
638 })
639 } else if let Some(ref url) = config.bitcoind_address {
640 let auth = if let Some(ref c) = config.bitcoind_cookiefile {
641 bitcoin_ext::rpc::Auth::CookieFile(c.clone())
642 } else {
643 bitcoin_ext::rpc::Auth::UserPass(
644 config.bitcoind_user.clone().context("need bitcoind auth config")?,
645 config.bitcoind_pass.clone().context("need bitcoind auth config")?,
646 )
647 };
648 Ok(ChainSourceSpec::Bitcoind {
649 url: url.clone(),
650 auth,
651 })
652 } else {
653 bail!("Need to either provide esplora or bitcoind info");
654 }
655 }
656
657 pub fn require_chainsource_version(&self) -> anyhow::Result<()> {
661 self.chain.require_version()
662 }
663
664 pub async fn network(&self) -> anyhow::Result<Network> {
665 Ok(self.properties().await?.network)
666 }
667
668 pub async fn derive_store_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
671 let last_revealed = self.db.get_last_vtxo_key_index().await?;
672
673 let index = last_revealed.map(|i| i + 1).unwrap_or(u32::MIN);
674 let keypair = self.seed.derive_vtxo_keypair(index);
675
676 self.db.store_vtxo_key(index, keypair.public_key()).await?;
677 Ok((keypair, index))
678 }
679
680 pub async fn peak_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
694 let keypair = self.seed.derive_vtxo_keypair(index);
695 if self.db.get_public_key_idx(&keypair.public_key()).await?.is_some() {
696 Ok(keypair)
697 } else {
698 bail!("VTXO key {} does not exist, please derive it first", index)
699 }
700 }
701
702
703 pub async fn pubkey_keypair(&self, public_key: &PublicKey) -> anyhow::Result<Option<(u32, Keypair)>> {
715 if let Some(index) = self.db.get_public_key_idx(&public_key).await? {
716 Ok(Some((index, self.seed.derive_vtxo_keypair(index))))
717 } else {
718 Ok(None)
719 }
720 }
721
722 pub async fn get_vtxo_key(&self, vtxo: impl VtxoRef) -> anyhow::Result<Keypair> {
733 let vtxo = match vtxo.vtxo() {
734 Some(v) => v,
735 None => &self.get_vtxo_by_id(vtxo.vtxo_id()).await?,
736 };
737 let pubkey = self.find_signable_clause(vtxo).await
738 .context("VTXO is not signable by wallet")?
739 .pubkey();
740 let idx = self.db.get_public_key_idx(&pubkey).await?
741 .context("VTXO key not found")?;
742 Ok(self.seed.derive_vtxo_keypair(idx))
743 }
744
745 pub async fn new_address(&self) -> anyhow::Result<ark::Address> {
747 let srv = &self.require_server()?;
748 let network = self.properties().await?.network;
749 let (keypair, _) = self.derive_store_next_keypair().await?;
750
751 Ok(ark::Address::builder()
752 .testnet(network != bitcoin::Network::Bitcoin)
753 .server_pubkey(srv.ark_info().await?.server_pubkey)
754 .pubkey_policy(keypair.public_key())
755 .into_address().unwrap())
756 }
757
758 pub async fn peak_address(&self, index: u32) -> anyhow::Result<ark::Address> {
762 let srv = &self.require_server()?;
763 let network = self.properties().await?.network;
764 let keypair = self.peak_keypair(index).await?;
765
766 Ok(ark::Address::builder()
767 .testnet(network != Network::Bitcoin)
768 .server_pubkey(srv.ark_info().await?.server_pubkey)
769 .pubkey_policy(keypair.public_key())
770 .into_address().unwrap())
771 }
772
773 pub async fn new_address_with_index(&self) -> anyhow::Result<(ark::Address, u32)> {
777 let srv = &self.require_server()?;
778 let network = self.properties().await?.network;
779 let (keypair, index) = self.derive_store_next_keypair().await?;
780 let pubkey = keypair.public_key();
781 let addr = ark::Address::builder()
782 .testnet(network != bitcoin::Network::Bitcoin)
783 .server_pubkey(srv.ark_info().await?.server_pubkey)
784 .pubkey_policy(pubkey)
785 .into_address()?;
786 Ok((addr, index))
787 }
788
789 pub async fn create(
795 mnemonic: &Mnemonic,
796 network: Network,
797 config: Config,
798 db: Arc<dyn BarkPersister>,
799 force: bool,
800 ) -> anyhow::Result<Wallet> {
801 trace!("Config: {:?}", config);
802 if let Some(existing) = db.read_properties().await? {
803 trace!("Existing config: {:?}", existing);
804 bail!("cannot overwrite already existing config")
805 }
806
807 if !force {
808 if let Err(err) = ServerConnection::connect(&config.server_address, network).await {
809 bail!("Failed to connect to provided server (if you are sure use the --force flag): {}", err);
810 }
811 }
812
813 let wallet_fingerprint = WalletSeed::new(network, &mnemonic.to_seed("")).fingerprint();
814 let properties = WalletProperties {
815 network: network,
816 fingerprint: wallet_fingerprint,
817 };
818
819 db.init_wallet(&properties).await.context("cannot init wallet in the database")?;
821 info!("Created wallet with fingerprint: {}", wallet_fingerprint);
822
823 let wallet = Wallet::open(&mnemonic, db, config).await.context("failed to open wallet")?;
825 wallet.require_chainsource_version()?;
826
827 Ok(wallet)
828 }
829
830 pub async fn create_with_onchain(
838 mnemonic: &Mnemonic,
839 network: Network,
840 config: Config,
841 db: Arc<dyn BarkPersister>,
842 onchain: &dyn ExitUnilaterally,
843 force: bool,
844 ) -> anyhow::Result<Wallet> {
845 let mut wallet = Wallet::create(mnemonic, network, config, db, force).await?;
846 wallet.exit.get_mut().load(onchain).await?;
847 Ok(wallet)
848 }
849
850 pub async fn open(
852 mnemonic: &Mnemonic,
853 db: Arc<dyn BarkPersister>,
854 config: Config,
855 ) -> anyhow::Result<Wallet> {
856 let properties = db.read_properties().await?.context("Wallet is not initialised")?;
857
858 let seed = {
859 let seed = mnemonic.to_seed("");
860 WalletSeed::new(properties.network, &seed)
861 };
862
863 if properties.fingerprint != seed.fingerprint() {
864 bail!("incorrect mnemonic")
865 }
866
867 let chain_source = if let Some(ref url) = config.esplora_address {
868 ChainSourceSpec::Esplora {
869 url: url.clone(),
870 }
871 } else if let Some(ref url) = config.bitcoind_address {
872 let auth = if let Some(ref c) = config.bitcoind_cookiefile {
873 bitcoin_ext::rpc::Auth::CookieFile(c.clone())
874 } else {
875 bitcoin_ext::rpc::Auth::UserPass(
876 config.bitcoind_user.clone().context("need bitcoind auth config")?,
877 config.bitcoind_pass.clone().context("need bitcoind auth config")?,
878 )
879 };
880 ChainSourceSpec::Bitcoind { url: url.clone(), auth }
881 } else {
882 bail!("Need to either provide esplora or bitcoind info");
883 };
884
885 let chain_source_client = ChainSource::new(
886 chain_source, properties.network, config.fallback_fee_rate,
887 ).await?;
888 let chain = Arc::new(chain_source_client);
889
890 let server = match ServerConnection::connect(
891 &config.server_address, properties.network,
892 ).await {
893 Ok(s) => Some(s),
894 Err(e) => {
895 warn!("Ark server handshake failed: {}", e);
896 None
897 }
898 };
899 let server = parking_lot::RwLock::new(server);
900
901 let movements = Arc::new(MovementManager::new(db.clone()));
902 let exit = RwLock::new(Exit::new(db.clone(), chain.clone(), movements.clone()).await?);
903
904 Ok(Wallet { config, db, seed, exit, movements, server, chain })
905 }
906
907 pub async fn open_with_onchain(
910 mnemonic: &Mnemonic,
911 db: Arc<dyn BarkPersister>,
912 onchain: &dyn ExitUnilaterally,
913 cfg: Config,
914 ) -> anyhow::Result<Wallet> {
915 let mut wallet = Wallet::open(mnemonic, db, cfg).await?;
916 wallet.exit.get_mut().load(onchain).await?;
917 Ok(wallet)
918 }
919
920 pub fn config(&self) -> &Config {
922 &self.config
923 }
924
925 pub async fn properties(&self) -> anyhow::Result<WalletProperties> {
927 let properties = self.db.read_properties().await?.context("Wallet is not initialised")?;
928 Ok(properties)
929 }
930
931 pub fn fingerprint(&self) -> Fingerprint {
933 self.seed.fingerprint()
934 }
935
936 fn require_server(&self) -> anyhow::Result<ServerConnection> {
937 self.server.read().clone()
938 .context("You should be connected to Ark server to perform this action")
939 }
940
941 pub async fn refresh_server(&self) -> anyhow::Result<()> {
942 let server = self.server.read().clone();
943
944 let srv = if let Some(srv) = server {
945 srv.check_connection().await?;
946 srv.ark_info().await?;
947 srv
948 } else {
949 let srv_address = &self.config.server_address;
950 let network = self.properties().await?.network;
951
952 ServerConnection::connect(srv_address, network).await?
953 };
954
955 let _ = self.server.write().insert(srv);
956
957 Ok(())
958 }
959
960 pub async fn ark_info(&self) -> anyhow::Result<Option<ArkInfo>> {
962 let server = self.server.read().clone();
963 match server.as_ref() {
964 Some(srv) => Ok(Some(srv.ark_info().await?)),
965 _ => Ok(None),
966 }
967 }
968
969 pub async fn balance(&self) -> anyhow::Result<Balance> {
973 let vtxos = self.vtxos().await?;
974
975 let spendable = {
976 let mut v = vtxos.iter().collect();
977 VtxoStateKind::Spendable.filter_vtxos(&mut v).await?;
978 v.into_iter().map(|v| v.amount()).sum::<Amount>()
979 };
980
981 let pending_lightning_send = self.pending_lightning_send_vtxos().await?.iter().map(|v| v.amount())
982 .sum::<Amount>();
983
984 let claimable_lightning_receive = self.claimable_lightning_receive_balance().await?;
985
986 let pending_board = self.pending_board_vtxos().await?.iter().map(|v| v.amount()).sum::<Amount>();
987
988 let pending_in_round = self.pending_round_input_vtxos().await?.iter().map(|v| v.amount()).sum();
989
990 let pending_exit = self.exit.try_read().ok().map(|e| e.pending_total());
991
992 Ok(Balance {
993 spendable,
994 pending_in_round,
995 pending_lightning_send,
996 claimable_lightning_receive,
997 pending_exit,
998 pending_board,
999 })
1000 }
1001
1002 pub async fn validate_vtxo(&self, vtxo: &Vtxo) -> anyhow::Result<()> {
1004 let tx = self.chain.get_tx(&vtxo.chain_anchor().txid).await
1005 .context("could not fetch chain tx")?;
1006
1007 let tx = tx.with_context(|| {
1008 format!("vtxo chain anchor not found for vtxo: {}", vtxo.chain_anchor().txid)
1009 })?;
1010
1011 vtxo.validate(&tx)?;
1012
1013 Ok(())
1014 }
1015
1016 pub async fn get_vtxo_by_id(&self, vtxo_id: VtxoId) -> anyhow::Result<WalletVtxo> {
1018 let vtxo = self.db.get_wallet_vtxo(vtxo_id).await
1019 .with_context(|| format!("Error when querying vtxo {} in database", vtxo_id))?
1020 .with_context(|| format!("The VTXO with id {} cannot be found", vtxo_id))?;
1021 Ok(vtxo)
1022 }
1023
1024 #[deprecated(since="0.1.0-beta.5", note = "Use Wallet::history instead")]
1026 pub async fn movements(&self) -> anyhow::Result<Vec<Movement>> {
1027 self.history().await
1028 }
1029
1030 pub async fn history(&self) -> anyhow::Result<Vec<Movement>> {
1032 Ok(self.db.get_all_movements().await?)
1033 }
1034
1035 pub async fn all_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1037 Ok(self.db.get_all_vtxos().await?)
1038 }
1039
1040 pub async fn vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1042 Ok(self.db.get_vtxos_by_state(&VtxoStateKind::UNSPENT_STATES).await?)
1043 }
1044
1045 pub async fn vtxos_with(&self, filter: &impl FilterVtxos) -> anyhow::Result<Vec<WalletVtxo>> {
1047 let mut vtxos = self.vtxos().await?;
1048 filter.filter_vtxos(&mut vtxos).await.context("error filtering vtxos")?;
1049 Ok(vtxos)
1050 }
1051
1052 pub async fn spendable_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1054 Ok(self.vtxos_with(&VtxoStateKind::Spendable).await?)
1055 }
1056
1057 pub async fn spendable_vtxos_with(
1059 &self,
1060 filter: &impl FilterVtxos,
1061 ) -> anyhow::Result<Vec<WalletVtxo>> {
1062 let mut vtxos = self.spendable_vtxos().await?;
1063 filter.filter_vtxos(&mut vtxos).await.context("error filtering vtxos")?;
1064 Ok(vtxos)
1065 }
1066
1067 pub async fn pending_boards(&self) -> anyhow::Result<Vec<PendingBoard>> {
1068 let boarding_vtxo_ids = self.db.get_all_pending_board_ids().await?;
1069 let mut boards = Vec::with_capacity(boarding_vtxo_ids.len());
1070 for vtxo_id in boarding_vtxo_ids {
1071 let board = self.db.get_pending_board_by_vtxo_id(vtxo_id).await?
1072 .expect("id just retrieved from db");
1073 boards.push(board);
1074 }
1075 Ok(boards)
1076 }
1077
1078 pub async fn pending_board_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1083 let vtxo_ids = self.pending_boards().await?.into_iter()
1084 .flat_map(|b| b.vtxos.into_iter())
1085 .collect::<Vec<_>>();
1086
1087 let mut vtxos = Vec::with_capacity(vtxo_ids.len());
1088 for vtxo_id in vtxo_ids {
1089 let vtxo = self.get_vtxo_by_id(vtxo_id).await
1090 .expect("vtxo id just got retrieved from db");
1091 vtxos.push(vtxo);
1092 }
1093
1094 debug_assert!(vtxos.iter().all(|v| matches!(v.state.kind(), VtxoStateKind::Locked)),
1095 "all pending board vtxos should be locked"
1096 );
1097
1098 Ok(vtxos)
1099 }
1100
1101 pub async fn pending_round_input_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1106 let mut ret = Vec::new();
1107 for round in self.db.load_round_states().await? {
1108 let inputs = round.state.locked_pending_inputs();
1109 ret.reserve(inputs.len());
1110 for input in inputs {
1111 let v = self.get_vtxo_by_id(input.id()).await
1112 .context("unknown round input VTXO")?;
1113 ret.push(v);
1114 }
1115 }
1116 Ok(ret)
1117 }
1118
1119 pub async fn pending_lightning_send_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1121 let vtxos = self.db.get_all_pending_lightning_send().await?.into_iter()
1122 .flat_map(|pending_lightning_send| pending_lightning_send.htlc_vtxos)
1123 .collect::<Vec<_>>();
1124
1125 Ok(vtxos)
1126 }
1127
1128 pub async fn get_expiring_vtxos(
1130 &self,
1131 threshold: BlockHeight,
1132 ) -> anyhow::Result<Vec<WalletVtxo>> {
1133 let expiry = self.chain.tip().await? + threshold;
1134 let filter = VtxoFilter::new(&self).expires_before(expiry);
1135 Ok(self.spendable_vtxos_with(&filter).await?)
1136 }
1137
1138 pub async fn sync_pending_boards(&self) -> anyhow::Result<()> {
1142 let ark_info = self.require_server()?.ark_info().await?;
1143 let current_height = self.chain.tip().await?;
1144 let unregistered_boards = self.pending_boards().await?;
1145 let mut registered_boards = 0;
1146
1147 if unregistered_boards.is_empty() {
1148 return Ok(());
1149 }
1150
1151 trace!("Attempting registration of sufficiently confirmed boards");
1152
1153 for board in unregistered_boards {
1154 let [vtxo_id] = board.vtxos.try_into()
1155 .map_err(|_| anyhow!("multiple board vtxos is not supported yet"))?;
1156
1157 let vtxo = self.get_vtxo_by_id(vtxo_id).await?;
1158
1159 let anchor = vtxo.chain_anchor();
1160 let confs = match self.chain.tx_status(anchor.txid).await {
1161 Ok(TxStatus::Confirmed(block_ref)) => Some(current_height - (block_ref.height - 1)),
1162 Ok(TxStatus::Mempool) => Some(0),
1163 Ok(TxStatus::NotFound) => None,
1164 Err(_) => None,
1165 };
1166
1167 if let Some(confs) = confs {
1168 if confs >= ark_info.required_board_confirmations as BlockHeight {
1169 if let Err(e) = self.register_board(vtxo.id()).await {
1170 warn!("Failed to register board {}: {}", vtxo.id(), e);
1171 } else {
1172 info!("Registered board {}", vtxo.id());
1173 registered_boards += 1;
1174 }
1175 }
1176
1177 continue;
1178 }
1179
1180 if vtxo.expiry_height() < current_height + ark_info.required_board_confirmations as BlockHeight {
1181 warn!("VTXO {} expired before its board was confirmed, removing board", vtxo.id());
1182 self.movements.finish_movement(board.movement_id, MovementStatus::Failed).await?;
1183 self.mark_vtxos_as_spent(&[vtxo]).await?;
1184 self.db.remove_pending_board(&vtxo_id).await?;
1185 }
1186 };
1187
1188 if registered_boards > 0 {
1189 info!("Registered {registered_boards} sufficiently confirmed boards");
1190 }
1191 Ok(())
1192 }
1193
1194 pub async fn maintenance(&self) -> anyhow::Result<()> {
1199 info!("Starting wallet maintenance");
1200 self.sync().await;
1201 self.progress_pending_rounds(None).await?;
1202 self.maintenance_refresh().await?;
1203 Ok(())
1204 }
1205
1206 pub async fn maintenance_with_onchain<W: PreparePsbt + SignPsbt + ExitUnilaterally>(
1212 &self,
1213 onchain: &mut W,
1214 ) -> anyhow::Result<()> {
1215 info!("Starting wallet maintenance with onchain wallet");
1216 self.sync().await;
1217 self.maintenance_refresh().await?;
1218
1219 self.sync_exits(onchain).await?;
1221
1222 Ok(())
1223 }
1224
1225 pub async fn maybe_schedule_maintenance_refresh(&self) -> anyhow::Result<Option<RoundStateId>> {
1233 let vtxos = self.get_vtxos_to_refresh().await?.into_iter()
1234 .map(|v| v.id())
1235 .collect::<Vec<_>>();
1236 if vtxos.len() == 0 {
1237 return Ok(None);
1238 }
1239
1240 info!("Scheduling maintenance refresh");
1241 let mut participation = match self.build_refresh_participation(vtxos).await? {
1242 Some(participation) => participation,
1243 None => return Ok(None),
1244 };
1245
1246 if let Err(e) = self.add_should_refresh_vtxos(&mut participation).await {
1247 warn!("Error trying to add additional VTXOs that should be refreshed: {:#}", e);
1248 }
1249
1250 let state = self.join_next_round(participation, Some(RoundMovement::Refresh)).await?;
1251 Ok(Some(state.id))
1252 }
1253
1254 pub async fn maintenance_refresh(&self) -> anyhow::Result<Option<RoundStatus>> {
1260 let vtxos = self.get_vtxos_to_refresh().await?.into_iter()
1261 .map(|v| v.id())
1262 .collect::<Vec<_>>();
1263 if vtxos.len() == 0 {
1264 return Ok(None);
1265 }
1266
1267 info!("Performing maintenance refresh");
1268 self.refresh_vtxos(vtxos).await
1269 }
1270
1271 pub async fn sync(&self) {
1277 tokio::join!(
1278 async {
1279 if let Err(e) = self.chain.update_fee_rates(self.config.fallback_fee_rate).await {
1282 warn!("Error updating fee rates: {:#}", e);
1283 }
1284 },
1285 async {
1286 if let Err(e) = self.sync_oors().await {
1287 warn!("Error in arkoor sync: {:#}", e);
1288 }
1289 },
1290 async {
1291 if let Err(e) = self.sync_pending_rounds().await {
1292 warn!("Error while trying to progress rounds awaiting confirmations: {:#}", e);
1293 }
1294 },
1295 async {
1296 if let Err(e) = self.sync_pending_lightning_send_vtxos().await {
1297 warn!("Error syncing pending lightning payments: {:#}", e);
1298 }
1299 },
1300 async {
1301 if let Err(e) = self.try_claim_all_lightning_receives(false).await {
1302 warn!("Error claiming pending lightning receives: {:#}", e);
1303 }
1304 },
1305 async {
1306 if let Err(e) = self.sync_pending_boards().await {
1307 warn!("Error syncing pending boards: {:#}", e);
1308 }
1309 }
1310 );
1311 }
1312
1313 pub async fn sync_exits(
1319 &self,
1320 onchain: &mut dyn ExitUnilaterally,
1321 ) -> anyhow::Result<()> {
1322 self.exit.write().await.sync(&self, onchain).await?;
1323 Ok(())
1324 }
1325
1326 pub async fn pending_lightning_sends(&self) -> anyhow::Result<Vec<LightningSend>> {
1327 Ok(self.db.get_all_pending_lightning_send().await?)
1328 }
1329
1330 pub async fn sync_pending_lightning_send_vtxos(&self) -> anyhow::Result<()> {
1333 let pending_payments = self.pending_lightning_sends().await?;
1334
1335 if pending_payments.is_empty() {
1336 return Ok(());
1337 }
1338
1339 info!("Syncing {} pending lightning sends", pending_payments.len());
1340
1341 for payment in pending_payments {
1342 let payment_hash = payment.invoice.payment_hash();
1343 self.check_lightning_payment(payment_hash, false).await?;
1344 }
1345
1346 Ok(())
1347 }
1348
1349 pub async fn dangerous_drop_vtxo(&self, vtxo_id: VtxoId) -> anyhow::Result<()> {
1352 warn!("Drop vtxo {} from the database", vtxo_id);
1353 self.db.remove_vtxo(vtxo_id).await?;
1354 Ok(())
1355 }
1356
1357 pub async fn dangerous_drop_all_vtxos(&self) -> anyhow::Result<()> {
1360 warn!("Dropping all vtxos from the db...");
1361 for vtxo in self.vtxos().await? {
1362 self.db.remove_vtxo(vtxo.id()).await?;
1363 }
1364
1365 self.exit.write().await.dangerous_clear_exit().await?;
1366 Ok(())
1367 }
1368
1369 pub async fn board_amount(
1373 &self,
1374 onchain: &mut dyn onchain::Board,
1375 amount: Amount,
1376 ) -> anyhow::Result<PendingBoard> {
1377 let (user_keypair, _) = self.derive_store_next_keypair().await?;
1378 self.board(onchain, Some(amount), user_keypair).await
1379 }
1380
1381 pub async fn board_all(
1383 &self,
1384 onchain: &mut dyn onchain::Board,
1385 ) -> anyhow::Result<PendingBoard> {
1386 let (user_keypair, _) = self.derive_store_next_keypair().await?;
1387 self.board(onchain, None, user_keypair).await
1388 }
1389
1390 async fn board(
1391 &self,
1392 wallet: &mut dyn onchain::Board,
1393 amount: Option<Amount>,
1394 user_keypair: Keypair,
1395 ) -> anyhow::Result<PendingBoard> {
1396 let mut srv = self.require_server()?;
1397 let ark_info = srv.ark_info().await?;
1398
1399 let properties = self.db.read_properties().await?.context("Missing config")?;
1400 let current_height = self.chain.tip().await?;
1401
1402 let expiry_height = current_height + ark_info.vtxo_expiry_delta as BlockHeight;
1403 let builder = BoardBuilder::new(
1404 user_keypair.public_key(),
1405 expiry_height,
1406 ark_info.server_pubkey,
1407 ark_info.vtxo_exit_delta,
1408 );
1409
1410 let addr = bitcoin::Address::from_script(
1411 &builder.funding_script_pubkey(),
1412 properties.network,
1413 )?;
1414
1415 let fee_rate = self.chain.fee_rates().await.regular;
1417 let (board_psbt, amount) = if let Some(amount) = amount {
1418 let psbt = wallet.prepare_tx(&[(addr, amount)], fee_rate)?;
1419 (psbt, amount)
1420 } else {
1421 let psbt = wallet.prepare_drain_tx(addr, fee_rate)?;
1422 assert_eq!(psbt.unsigned_tx.output.len(), 1);
1423 let amount = psbt.unsigned_tx.output[0].value;
1424 (psbt, amount)
1425 };
1426
1427 ensure!(amount >= ark_info.min_board_amount,
1428 "board amount of {amount} is less than minimum board amount required by server ({})",
1429 ark_info.min_board_amount,
1430 );
1431
1432 let utxo = OutPoint::new(board_psbt.unsigned_tx.compute_txid(), BOARD_FUNDING_TX_VTXO_VOUT);
1433 let builder = builder
1434 .set_funding_details(amount, utxo)
1435 .generate_user_nonces();
1436
1437 let cosign_resp = srv.client.request_board_cosign(protos::BoardCosignRequest {
1438 amount: amount.to_sat(),
1439 utxo: bitcoin::consensus::serialize(&utxo), expiry_height,
1441 user_pubkey: user_keypair.public_key().serialize().to_vec(),
1442 pub_nonce: builder.user_pub_nonce().serialize().to_vec(),
1443 }).await.context("error requesting board cosign")?
1444 .into_inner().try_into().context("invalid cosign response from server")?;
1445
1446 ensure!(builder.verify_cosign_response(&cosign_resp),
1447 "invalid board cosignature received from server",
1448 );
1449
1450 let vtxo = builder.build_vtxo(&cosign_resp, &user_keypair)?;
1452
1453 let onchain_fee = board_psbt.fee()?;
1454 let movement_id = self.movements.new_movement_with_update(
1455 Subsystem::BOARD,
1456 BoardMovement::Board.to_string(),
1457 MovementUpdate::new()
1458 .produced_vtxo(&vtxo)
1459 .intended_and_effective_balance(vtxo.amount().to_signed()?)
1460 .metadata(BoardMovement::metadata(utxo, onchain_fee)),
1461 ).await?;
1462 self.store_locked_vtxos([&vtxo], Some(movement_id)).await?;
1463
1464 let tx = wallet.finish_tx(board_psbt).await?;
1465 self.db.store_pending_board(&vtxo, &tx, movement_id).await?;
1466
1467 trace!("Broadcasting board tx: {}", bitcoin::consensus::encode::serialize_hex(&tx));
1468 self.chain.broadcast_tx(&tx).await?;
1469
1470 info!("Board broadcasted");
1471 Ok(self.db.get_pending_board_by_vtxo_id(vtxo.id()).await?.expect("board should be stored"))
1472 }
1473
1474 async fn register_board(&self, vtxo: impl VtxoRef) -> anyhow::Result<()> {
1476 trace!("Attempting to register board {} to server", vtxo.vtxo_id());
1477 let mut srv = self.require_server()?;
1478
1479 let vtxo = match vtxo.vtxo() {
1481 Some(v) => v,
1482 None => {
1483 &self.db.get_wallet_vtxo(vtxo.vtxo_id()).await?
1484 .with_context(|| format!("VTXO doesn't exist: {}", vtxo.vtxo_id()))?
1485 },
1486 };
1487
1488 srv.client.register_board_vtxo(protos::BoardVtxoRequest {
1490 board_vtxo: vtxo.serialize(),
1491 }).await.context("error registering board with the Ark server")?;
1492
1493 self.db.update_vtxo_state_checked(
1496 vtxo.id(), VtxoState::Spendable, &VtxoStateKind::UNSPENT_STATES,
1497 ).await?;
1498
1499 let board = self.db.get_pending_board_by_vtxo_id(vtxo.id()).await?
1500 .context("pending board not found")?;
1501
1502 self.movements.finish_movement(board.movement_id, MovementStatus::Successful).await?;
1503 self.db.remove_pending_board(&vtxo.id()).await?;
1504
1505 Ok(())
1506 }
1507
1508 async fn has_counterparty_risk(&self, vtxo: &Vtxo) -> anyhow::Result<bool> {
1513 for past_pk in vtxo.past_arkoor_pubkeys() {
1514 if !self.db.get_public_key_idx(&past_pk).await?.is_some() {
1515 return Ok(true);
1516 }
1517 }
1518
1519 let my_clause = self.find_signable_clause(vtxo).await;
1520 Ok(!my_clause.is_some())
1521 }
1522
1523 pub async fn sync_oors(&self) -> anyhow::Result<()> {
1524 let last_pk_index = self.db.get_last_vtxo_key_index().await?.unwrap_or_default();
1525 let pubkeys = (0..=last_pk_index).map(|idx| {
1526 self.seed.derive_vtxo_keypair(idx).public_key()
1527 }).collect::<Vec<_>>();
1528
1529 self.sync_arkoor_for_pubkeys(&pubkeys).await?;
1530
1531 Ok(())
1532 }
1533
1534 async fn sync_arkoor_for_pubkeys(
1536 &self,
1537 public_keys: &[PublicKey],
1538 ) -> anyhow::Result<()> {
1539 let mut srv = self.require_server()?;
1540
1541 for pubkeys in public_keys.chunks(rpc::MAX_NB_MAILBOX_PUBKEYS) {
1542 debug!("Emptying OOR mailbox at Ark server...");
1544 let req = protos::ArkoorVtxosRequest {
1545 pubkeys: pubkeys.iter().map(|pk| pk.serialize().to_vec()).collect(),
1546 };
1547
1548 #[allow(deprecated)]
1549 let packages = srv.client.empty_arkoor_mailbox(req).await
1550 .context("error fetching oors")?.into_inner().packages;
1551 debug!("Ark server has {} arkoor packages for us", packages.len());
1552
1553 for package in packages {
1554 let mut vtxos = Vec::with_capacity(package.vtxos.len());
1555 for vtxo in package.vtxos {
1556 let vtxo = match Vtxo::deserialize(&vtxo) {
1557 Ok(vtxo) => vtxo,
1558 Err(e) => {
1559 warn!("Invalid vtxo from Ark server: {}", e);
1560 continue;
1561 }
1562 };
1563
1564 if let Err(e) = self.validate_vtxo(&vtxo).await {
1565 error!("Received invalid arkoor VTXO from server: {}", e);
1566 continue;
1567 }
1568
1569 match self.db.has_spent_vtxo(vtxo.id()).await {
1570 Ok(spent) if spent => {
1571 debug!("Not adding OOR vtxo {} because it is considered spent", vtxo.id());
1572 continue;
1573 },
1574 _ => {}
1575 }
1576
1577 if let Ok(Some(_)) = self.db.get_wallet_vtxo(vtxo.id()).await {
1578 debug!("Not adding OOR vtxo {} because it already exists", vtxo.id());
1579 continue;
1580 }
1581
1582 vtxos.push(vtxo);
1583 }
1584
1585 self.store_spendable_vtxos(&vtxos).await?;
1586 self.movements.new_finished_movement(
1587 Subsystem::ARKOOR,
1588 ArkoorMovement::Receive.to_string(),
1589 MovementStatus::Successful,
1590 MovementUpdate::new()
1591 .produced_vtxos(&vtxos)
1592 .intended_and_effective_balance(
1593 vtxos
1594 .iter()
1595 .map(|vtxo| vtxo.amount()).sum::<Amount>()
1596 .to_signed()?,
1597 ),
1598 ).await?;
1599 }
1600 }
1601
1602 Ok(())
1603 }
1604
1605 async fn add_should_refresh_vtxos(
1609 &self,
1610 participation: &mut RoundParticipation,
1611 ) -> anyhow::Result<()> {
1612 let excluded_ids = participation.inputs.iter().map(|v| v.vtxo_id())
1613 .collect::<HashSet<_>>();
1614
1615 let should_refresh_vtxos = self.get_vtxos_to_refresh().await?.into_iter()
1616 .filter(|v| !excluded_ids.contains(&v.id()))
1617 .map(|v| v.vtxo).collect::<Vec<_>>();
1618
1619
1620 let total_amount = should_refresh_vtxos.iter().map(|v| v.amount()).sum::<Amount>();
1621
1622 if total_amount > P2TR_DUST {
1623 let (user_keypair, _) = self.derive_store_next_keypair().await?;
1624 let req = VtxoRequest {
1625 policy: VtxoPolicy::new_pubkey(user_keypair.public_key()),
1626 amount: total_amount,
1627 };
1628
1629 participation.inputs.extend(should_refresh_vtxos);
1630 participation.outputs.push(req);
1631 }
1632
1633 Ok(())
1634 }
1635
1636 pub async fn build_refresh_participation<V: VtxoRef>(
1637 &self,
1638 vtxos: impl IntoIterator<Item = V>,
1639 ) -> anyhow::Result<Option<RoundParticipation>> {
1640 let vtxos = {
1641 let mut ret = HashMap::new();
1642 for v in vtxos {
1643 let id = v.vtxo_id();
1644 let vtxo = self.get_vtxo_by_id(id).await
1645 .with_context(|| format!("vtxo with id {} not found", id))?;
1646 if !ret.insert(id, vtxo).is_none() {
1647 bail!("duplicate VTXO id: {}", id);
1648 }
1649 }
1650 ret
1651 };
1652
1653 if vtxos.is_empty() {
1654 info!("Skipping refresh since no VTXOs are provided.");
1655 return Ok(None);
1656 }
1657
1658 let total_amount = vtxos.values().map(|v| v.vtxo.amount()).sum();
1659
1660 info!("Refreshing {} VTXOs (total amount = {}).", vtxos.len(), total_amount);
1661
1662 let (user_keypair, _) = self.derive_store_next_keypair().await?;
1663 let req = VtxoRequest {
1664 policy: VtxoPolicy::Pubkey(PubkeyVtxoPolicy { user_pubkey: user_keypair.public_key() }),
1665 amount: total_amount,
1666 };
1667
1668 Ok(Some(RoundParticipation {
1669 inputs: vtxos.into_values().map(|v| v.vtxo).collect(),
1670 outputs: vec![req],
1671 }))
1672 }
1673
1674 pub async fn refresh_vtxos<V: VtxoRef>(
1680 &self,
1681 vtxos: impl IntoIterator<Item = V>,
1682 ) -> anyhow::Result<Option<RoundStatus>> {
1683 let mut participation = match self.build_refresh_participation(vtxos).await? {
1684 Some(participation) => participation,
1685 None => return Ok(None),
1686 };
1687
1688 if let Err(e) = self.add_should_refresh_vtxos(&mut participation).await {
1689 warn!("Error trying to add additional VTXOs that should be refreshed: {:#}", e);
1690 }
1691
1692 Ok(Some(self.participate_round(participation, Some(RoundMovement::Refresh)).await?))
1693 }
1694
1695 pub async fn get_vtxos_to_refresh(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1699 let tip = self.chain.tip().await?;
1700 let fee_rate = self.chain.fee_rates().await.fast;
1701
1702 let must_refresh_vtxos = self.spendable_vtxos_with(
1704 &RefreshStrategy::must_refresh(self, tip, fee_rate),
1705 ).await?;
1706 if must_refresh_vtxos.is_empty() {
1707 return Ok(vec![]);
1708 } else {
1709 let should_refresh_vtxos = self.spendable_vtxos_with(
1712 &RefreshStrategy::should_refresh(self, tip, fee_rate),
1713 ).await?;
1714 Ok(should_refresh_vtxos)
1715 }
1716 }
1717
1718 pub async fn get_first_expiring_vtxo_blockheight(
1720 &self,
1721 ) -> anyhow::Result<Option<BlockHeight>> {
1722 Ok(self.spendable_vtxos().await?.iter().map(|v| v.expiry_height()).min())
1723 }
1724
1725 pub async fn get_next_required_refresh_blockheight(
1728 &self,
1729 ) -> anyhow::Result<Option<BlockHeight>> {
1730 let first_expiry = self.get_first_expiring_vtxo_blockheight().await?;
1731 Ok(first_expiry.map(|h| h.saturating_sub(self.config.vtxo_refresh_expiry_threshold)))
1732 }
1733
1734 async fn select_vtxos_to_cover(
1740 &self,
1741 amount: Amount,
1742 ) -> anyhow::Result<Vec<WalletVtxo>> {
1743 let mut vtxos = self.spendable_vtxos().await?;
1744 vtxos.sort_by_key(|v| v.expiry_height());
1745
1746 let mut result = Vec::new();
1748 let mut total_amount = Amount::ZERO;
1749 for input in vtxos {
1750 total_amount += input.amount();
1751 result.push(input);
1752
1753 if total_amount >= amount {
1754 return Ok(result)
1755 }
1756 }
1757
1758 bail!("Insufficient money available. Needed {} but {} is available",
1759 amount, total_amount,
1760 );
1761 }
1762
1763 pub async fn pending_lightning_receives(&self) -> anyhow::Result<Vec<LightningReceive>> {
1765 Ok(self.db.get_all_pending_lightning_receives().await?)
1766 }
1767
1768 pub async fn claimable_lightning_receive_balance(&self) -> anyhow::Result<Amount> {
1769 let receives = self.pending_lightning_receives().await?;
1770
1771 let mut total = Amount::ZERO;
1772 for receive in receives {
1773 if let Some(htlc_vtxos) = receive.htlc_vtxos {
1774 total += htlc_vtxos.iter().map(|v| v.amount()).sum::<Amount>();
1775 }
1776 }
1777
1778 Ok(total)
1779 }
1780
1781 pub async fn run_daemon(
1787 self: &Arc<Self>,
1788 onchain: Arc<RwLock<dyn DaemonizableOnchainWallet>>,
1789 ) -> anyhow::Result<DaemonHandle> {
1790 Ok(crate::daemon::start_daemon(self.clone(), onchain))
1793 }
1794}