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 serde;
290
291pub mod daemon;
292pub mod exit;
293pub mod lightning;
294pub mod movement;
295pub mod onchain;
296pub mod persist;
297pub mod round;
298pub mod subsystem;
299pub mod vtxo;
300
301pub use self::config::{BarkNetwork, Config};
302pub use self::persist::sqlite::SqliteClient;
303pub use self::vtxo::state::WalletVtxo;
304
305mod config;
306mod psbtext;
307
308use std::collections::{HashMap, HashSet};
309
310use std::sync::Arc;
311
312use anyhow::{bail, Context};
313use bip39::Mnemonic;
314use bitcoin::{Amount, Network, OutPoint, ScriptBuf};
315use bitcoin::bip32::{self, Fingerprint};
316use bitcoin::hex::DisplayHex;
317use bitcoin::secp256k1::{self, Keypair, PublicKey};
318use log::{trace, debug, info, warn, error};
319use tokio::sync::RwLock;
320use tokio_util::sync::CancellationToken;
321
322use ark::{ArkInfo, OffboardRequest, ProtocolEncoding, Vtxo, VtxoId, VtxoPolicy, VtxoRequest};
323use ark::address::VtxoDelivery;
324use ark::arkoor::ArkoorPackageBuilder;
325use ark::board::{BoardBuilder, BOARD_FUNDING_TX_VTXO_VOUT};
326use ark::musig;
327use ark::rounds::RoundId;
328use ark::vtxo::{VtxoRef, PubkeyVtxoPolicy, VtxoPolicyKind};
329use bitcoin_ext::{BlockHeight, P2TR_DUST, TxStatus};
330use server_rpc::{self as rpc, protos, ServerConnection};
331
332use crate::daemon::Daemon;
333use crate::exit::Exit;
334use crate::movement::{Movement, MovementDestination, MovementStatus};
335use crate::movement::manager::{MovementGuard, MovementManager};
336use crate::movement::update::MovementUpdate;
337use crate::onchain::{ChainSource, PreparePsbt, ExitUnilaterally, Utxo, SignPsbt};
338use crate::persist::{BarkPersister, RoundStateId};
339use crate::persist::models::{LightningReceive, LightningSend, PendingBoard};
340use crate::round::{RoundParticipation, RoundStatus};
341use crate::subsystem::{ArkoorMovement, BarkSubsystem, BoardMovement, RoundMovement, SubsystemId};
342use crate::vtxo::selection::{FilterVtxos, VtxoFilter, RefreshStrategy};
343use crate::vtxo::state::{VtxoState, VtxoStateKind, UNSPENT_STATES};
344
345const ARK_PURPOSE_INDEX: u32 = 350;
346
347lazy_static::lazy_static! {
348 static ref SECP: secp256k1::Secp256k1<secp256k1::All> = secp256k1::Secp256k1::new();
350}
351
352#[derive(Debug, Clone)]
354pub struct LightningReceiveBalance {
355 pub total: Amount,
357 pub claimable: Amount,
359}
360
361#[derive(Debug, Clone)]
363pub struct Balance {
364 pub spendable: Amount,
366 pub pending_lightning_send: Amount,
368 pub claimable_lightning_receive: Amount,
370 pub pending_in_round: Amount,
372 pub pending_exit: Option<Amount>,
375 pub pending_board: Amount,
377}
378
379struct ArkoorCreateResult {
380 input: Vec<Vtxo>,
381 created: Vec<Vtxo>,
382 change: Option<Vtxo>,
383}
384
385impl ArkoorCreateResult {
386 pub fn to_movement_update(&self) -> anyhow::Result<MovementUpdate> {
387 Ok(MovementUpdate::new()
388 .consumed_vtxos(self.input.iter())
389 .produced_vtxo_if_some(self.change.as_ref())
390 )
391 }
392}
393
394pub struct UtxoInfo {
395 pub outpoint: OutPoint,
396 pub amount: Amount,
397 pub confirmation_height: Option<u32>,
398}
399
400impl From<Utxo> for UtxoInfo {
401 fn from(value: Utxo) -> Self {
402 match value {
403 Utxo::Local(o) => UtxoInfo {
404 outpoint: o.outpoint,
405 amount: o.amount,
406 confirmation_height: o.confirmation_height,
407 },
408 Utxo::Exit(e) => UtxoInfo {
409 outpoint: e.vtxo.point(),
410 amount: e.vtxo.amount(),
411 confirmation_height: Some(e.height),
412 },
413 }
414 }
415}
416
417#[derive(Debug, Clone, PartialEq, Eq, Hash)]
420pub struct Offboard {
421 pub round: RoundId,
423}
424
425pub struct OffchainBalance {
428 pub available: Amount,
430 pub pending_in_round: Amount,
432 pub pending_exit: Amount,
435}
436
437#[derive(Debug, Clone)]
439pub struct WalletProperties {
440 pub network: Network,
444
445 pub fingerprint: Fingerprint,
449}
450
451pub struct VtxoSeed(bip32::Xpriv);
457
458impl VtxoSeed {
459 fn new(network: Network, seed: &[u8; 64]) -> Self {
460 let master = bip32::Xpriv::new_master(network, seed).unwrap();
461
462 Self(master.derive_priv(&SECP, &[ARK_PURPOSE_INDEX.into()]).unwrap())
463 }
464
465 fn fingerprint(&self) -> Fingerprint {
466 self.0.fingerprint(&SECP)
467 }
468
469 fn derive_keypair(&self, idx: u32) -> Keypair {
470 self.0.derive_priv(&SECP, &[idx.into()]).unwrap().to_keypair(&SECP)
471 }
472}
473
474pub struct Wallet {
608 pub chain: Arc<ChainSource>,
610
611 pub exit: RwLock<Exit>,
613
614 pub movements: Arc<MovementManager>,
616
617 config: Config,
619
620 db: Arc<dyn BarkPersister>,
622
623 vtxo_seed: VtxoSeed,
625
626 server: parking_lot::RwLock<Option<ServerConnection>>,
628
629 subsystem_ids: HashMap<BarkSubsystem, SubsystemId>,
631}
632
633impl Wallet {
634 pub fn chain_source(
637 config: &Config,
638 ) -> anyhow::Result<onchain::ChainSourceSpec> {
639 if let Some(ref url) = config.esplora_address {
640 Ok(onchain::ChainSourceSpec::Esplora {
641 url: url.clone(),
642 })
643 } else if let Some(ref url) = config.bitcoind_address {
644 let auth = if let Some(ref c) = config.bitcoind_cookiefile {
645 bitcoin_ext::rpc::Auth::CookieFile(c.clone())
646 } else {
647 bitcoin_ext::rpc::Auth::UserPass(
648 config.bitcoind_user.clone().context("need bitcoind auth config")?,
649 config.bitcoind_pass.clone().context("need bitcoind auth config")?,
650 )
651 };
652 Ok(onchain::ChainSourceSpec::Bitcoind {
653 url: url.clone(),
654 auth,
655 })
656 } else {
657 bail!("Need to either provide esplora or bitcoind info");
658 }
659 }
660
661 pub fn require_chainsource_version(&self) -> anyhow::Result<()> {
665 self.chain.require_version()
666 }
667
668 pub fn derive_store_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
671 let last_revealed = self.db.get_last_vtxo_key_index()?;
672
673 let index = last_revealed.map(|i| i + 1).unwrap_or(u32::MIN);
674 let keypair = self.vtxo_seed.derive_keypair(index);
675
676 self.db.store_vtxo_key(index, keypair.public_key())?;
677 Ok((keypair, index))
678 }
679
680 pub fn peak_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
694 let keypair = self.vtxo_seed.derive_keypair(index);
695 if self.db.get_public_key_idx(&keypair.public_key())?.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 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)? {
716 Ok(Some((index, self.vtxo_seed.derive_keypair(index))))
717 } else {
718 Ok(None)
719 }
720 }
721
722 pub fn get_vtxo_key(&self, vtxo: &Vtxo) -> anyhow::Result<Keypair> {
733 let idx = self.db.get_public_key_idx(&vtxo.user_pubkey())?
734 .context("VTXO key not found")?;
735 Ok(self.vtxo_seed.derive_keypair(idx))
736 }
737
738 pub async fn new_address(&self) -> anyhow::Result<ark::Address> {
740 let srv = &self.require_server()?;
741 let network = self.properties()?.network;
742 let pubkey = self.derive_store_next_keypair()?.0.public_key();
743
744 Ok(ark::Address::builder()
745 .testnet(network != bitcoin::Network::Bitcoin)
746 .server_pubkey(srv.ark_info().await?.server_pubkey)
747 .pubkey_policy(pubkey)
748 .into_address().unwrap())
749 }
750
751 pub async fn peak_address(&self, index: u32) -> anyhow::Result<ark::Address> {
755 let srv = &self.require_server()?;
756 let network = self.properties()?.network;
757 let pubkey = self.peak_keypair(index)?.public_key();
758
759 Ok(ark::Address::builder()
760 .testnet(network != Network::Bitcoin)
761 .server_pubkey(srv.ark_info().await?.server_pubkey)
762 .pubkey_policy(pubkey)
763 .into_address().unwrap())
764 }
765
766 pub async fn new_address_with_index(&self) -> anyhow::Result<(ark::Address, u32)> {
770 let srv = &self.require_server()?;
771 let network = self.properties()?.network;
772 let (keypair, index) = self.derive_store_next_keypair()?;
773 let pubkey = keypair.public_key();
774 let addr = ark::Address::builder()
775 .testnet(network != bitcoin::Network::Bitcoin)
776 .server_pubkey(srv.ark_info().await?.server_pubkey)
777 .pubkey_policy(pubkey)
778 .into_address()?;
779 Ok((addr, index))
780 }
781
782 pub async fn create(
788 mnemonic: &Mnemonic,
789 network: Network,
790 config: Config,
791 db: Arc<dyn BarkPersister>,
792 force: bool,
793 ) -> anyhow::Result<Wallet> {
794 trace!("Config: {:?}", config);
795 if let Some(existing) = db.read_properties()? {
796 trace!("Existing config: {:?}", existing);
797 bail!("cannot overwrite already existing config")
798 }
799
800 if !force {
801 if let Err(err) = ServerConnection::connect(&config.server_address, network).await {
802 bail!("Failed to connect to provided server (if you are sure use the --force flag): {}", err);
803 }
804 }
805
806 let wallet_fingerprint = VtxoSeed::new(network, &mnemonic.to_seed("")).fingerprint();
807 let properties = WalletProperties {
808 network: network,
809 fingerprint: wallet_fingerprint,
810 };
811
812 db.init_wallet(&properties).context("cannot init wallet in the database")?;
814 info!("Created wallet with fingerprint: {}", wallet_fingerprint);
815
816 let wallet = Wallet::open(&mnemonic, db, config).await.context("failed to open wallet")?;
818 wallet.require_chainsource_version()?;
819
820 Ok(wallet)
821 }
822
823 pub async fn create_with_onchain(
831 mnemonic: &Mnemonic,
832 network: Network,
833 config: Config,
834 db: Arc<dyn BarkPersister>,
835 onchain: &dyn ExitUnilaterally,
836 force: bool,
837 ) -> anyhow::Result<Wallet> {
838 let mut wallet = Wallet::create(mnemonic, network, config, db, force).await?;
839 wallet.exit.get_mut().load(onchain).await?;
840 Ok(wallet)
841 }
842
843 pub async fn open(
845 mnemonic: &Mnemonic,
846 db: Arc<dyn BarkPersister>,
847 config: Config,
848 ) -> anyhow::Result<Wallet> {
849 let properties = db.read_properties()?.context("Wallet is not initialised")?;
850
851 let seed = mnemonic.to_seed("");
852 let vtxo_seed = VtxoSeed::new(properties.network, &seed);
853
854 if properties.fingerprint != vtxo_seed.fingerprint() {
855 bail!("incorrect mnemonic")
856 }
857
858 let chain_source = if let Some(ref url) = config.esplora_address {
859 onchain::ChainSourceSpec::Esplora {
860 url: url.clone(),
861 }
862 } else if let Some(ref url) = config.bitcoind_address {
863 let auth = if let Some(ref c) = config.bitcoind_cookiefile {
864 bitcoin_ext::rpc::Auth::CookieFile(c.clone())
865 } else {
866 bitcoin_ext::rpc::Auth::UserPass(
867 config.bitcoind_user.clone().context("need bitcoind auth config")?,
868 config.bitcoind_pass.clone().context("need bitcoind auth config")?,
869 )
870 };
871 onchain::ChainSourceSpec::Bitcoind { url: url.clone(), auth }
872 } else {
873 bail!("Need to either provide esplora or bitcoind info");
874 };
875
876 let chain_source_client = ChainSource::new(
877 chain_source, properties.network, config.fallback_fee_rate,
878 ).await?;
879 let chain = Arc::new(chain_source_client);
880
881 let server = match ServerConnection::connect(
882 &config.server_address, properties.network,
883 ).await {
884 Ok(s) => Some(s),
885 Err(e) => {
886 warn!("Ark server handshake failed: {}", e);
887 None
888 }
889 };
890 let server = parking_lot::RwLock::new(server);
891
892 let movements = Arc::new(MovementManager::new(db.clone()));
893 let exit = RwLock::new(Exit::new(db.clone(), chain.clone(), movements.clone()).await?);
894 let mut subsystem_ids = HashMap::new();
895 {
896 let subsystems = [
897 BarkSubsystem::Arkoor,
898 BarkSubsystem::Board,
899 BarkSubsystem::LightningReceive,
900 BarkSubsystem::LightningSend,
901 BarkSubsystem::Round,
902 ];
903 for subsystem in subsystems {
904 let id = movements.register_subsystem(subsystem.as_str().into()).await?;
905 subsystem_ids.insert(subsystem, id);
906 }
907 };
908
909 Ok(Wallet { config, db, vtxo_seed, exit, movements, server, chain, subsystem_ids })
910 }
911
912 pub async fn open_with_onchain(
915 mnemonic: &Mnemonic,
916 db: Arc<dyn BarkPersister>,
917 onchain: &dyn ExitUnilaterally,
918 cfg: Config,
919 ) -> anyhow::Result<Wallet> {
920 let mut wallet = Wallet::open(mnemonic, db, cfg).await?;
921 wallet.exit.get_mut().load(onchain).await?;
922 Ok(wallet)
923 }
924
925 pub fn config(&self) -> &Config {
927 &self.config
928 }
929
930 pub fn properties(&self) -> anyhow::Result<WalletProperties> {
932 let properties = self.db.read_properties()?.context("Wallet is not initialised")?;
933 Ok(properties)
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()?.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 fn balance(&self) -> anyhow::Result<Balance> {
973 let vtxos = self.vtxos()?;
974
975 let spendable = {
976 let mut v = vtxos.iter().collect();
977 VtxoStateKind::Spendable.filter_vtxos(&mut v)?;
978 v.into_iter().map(|v| v.amount()).sum::<Amount>()
979 };
980
981 let pending_lightning_send = self.pending_lightning_send_vtxos()?.iter().map(|v| v.amount())
982 .sum::<Amount>();
983
984 let claimable_lightning_receive = self.claimable_lightning_receive_balance()?;
985
986 let pending_board = self.pending_board_vtxos()?.iter().map(|v| v.amount()).sum::<Amount>();
987
988 let pending_in_round = self.pending_round_input_vtxos()?.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 fn get_vtxo_by_id(&self, vtxo_id: VtxoId) -> anyhow::Result<WalletVtxo> {
1018 let vtxo = self.db.get_wallet_vtxo(vtxo_id)
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 pub fn movements(&self) -> anyhow::Result<Vec<Movement>> {
1026 Ok(self.db.get_all_movements()?)
1027 }
1028
1029 pub fn all_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1031 Ok(self.db.get_all_vtxos()?)
1032 }
1033
1034 pub fn vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1036 Ok(self.db.get_vtxos_by_state(&UNSPENT_STATES)?)
1037 }
1038
1039 pub fn vtxos_with(&self, filter: &impl FilterVtxos) -> anyhow::Result<Vec<WalletVtxo>> {
1041 let mut vtxos = self.vtxos()?;
1042 filter.filter_vtxos(&mut vtxos).context("error filtering vtxos")?;
1043 Ok(vtxos)
1044 }
1045
1046 pub fn spendable_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1048 Ok(self.vtxos_with(&VtxoStateKind::Spendable)?)
1049 }
1050
1051 pub fn spendable_vtxos_with(
1053 &self,
1054 filter: &impl FilterVtxos,
1055 ) -> anyhow::Result<Vec<WalletVtxo>> {
1056 let mut vtxos = self.spendable_vtxos()?;
1057 filter.filter_vtxos(&mut vtxos).context("error filtering vtxos")?;
1058 Ok(vtxos)
1059 }
1060
1061 pub fn pending_boards(&self) -> anyhow::Result<Vec<PendingBoard>> {
1062 let boarding_vtxo_ids = self.db.get_all_pending_board_ids()?;
1063 let mut boards = Vec::with_capacity(boarding_vtxo_ids.len());
1064 for vtxo_id in boarding_vtxo_ids {
1065 let board = self.db.get_pending_board_by_vtxo_id(vtxo_id)?
1066 .expect("id just retrieved from db");
1067 boards.push(board);
1068 }
1069 Ok(boards)
1070 }
1071
1072 pub fn pending_board_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1077 let vtxo_ids = self.pending_boards()?.into_iter()
1078 .flat_map(|b| b.vtxos.into_iter())
1079 .collect::<Vec<_>>();
1080
1081 let mut vtxos = Vec::with_capacity(vtxo_ids.len());
1082 for vtxo_id in vtxo_ids {
1083 let vtxo = self.get_vtxo_by_id(vtxo_id)
1084 .expect("vtxo id just got retrieved from db");
1085 vtxos.push(vtxo);
1086 }
1087
1088 debug_assert!(vtxos.iter().all(|v| matches!(v.state.kind(), VtxoStateKind::Locked)),
1089 "all pending board vtxos should be locked"
1090 );
1091
1092 Ok(vtxos)
1093 }
1094
1095 pub fn pending_round_input_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1100 let mut ret = Vec::new();
1101 for round in self.db.load_round_states()? {
1102 let inputs = round.state.locked_pending_inputs();
1103 ret.reserve(inputs.len());
1104 for input in inputs {
1105 ret.push(self.get_vtxo_by_id(input.id()).context("unknown round input VTXO")?);
1106 }
1107 }
1108 Ok(ret)
1109 }
1110
1111 pub fn pending_lightning_send_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1113 let vtxos = self.db.get_all_pending_lightning_send()?.into_iter()
1114 .flat_map(|pending_lightning_send| pending_lightning_send.htlc_vtxos)
1115 .collect::<Vec<_>>();
1116
1117 Ok(vtxos)
1118 }
1119
1120 pub async fn get_expiring_vtxos(
1122 &self,
1123 threshold: BlockHeight,
1124 ) -> anyhow::Result<Vec<WalletVtxo>> {
1125 let expiry = self.chain.tip().await? + threshold;
1126 let filter = VtxoFilter::new(&self).expires_before(expiry);
1127 Ok(self.spendable_vtxos_with(&filter)?)
1128 }
1129
1130 pub async fn sync_pending_boards(&self) -> anyhow::Result<()> {
1134 let ark_info = self.require_server()?.ark_info().await?;
1135 let current_height = self.chain.tip().await?;
1136 let unregistered_boards = self.pending_board_vtxos()?;
1137 let mut registered_boards = 0;
1138
1139 if unregistered_boards.is_empty() {
1140 return Ok(());
1141 }
1142
1143 trace!("Attempting registration of sufficiently confirmed boards");
1144
1145 for board in unregistered_boards {
1146 let anchor = board.vtxo.chain_anchor();
1147 let confs = match self.chain.tx_status(anchor.txid).await {
1148 Ok(TxStatus::Confirmed(block_ref)) => Some(current_height - (block_ref.height - 1)),
1149 Ok(TxStatus::Mempool) => Some(0),
1150 Ok(TxStatus::NotFound) => None,
1151 Err(_) => None,
1152 };
1153
1154 if let Some(confs) = confs {
1155 if confs >= ark_info.required_board_confirmations as BlockHeight {
1156 if let Err(e) = self.register_board(board.vtxo.id()).await {
1157 warn!("Failed to register board {}: {}", board.vtxo.id(), e);
1158 } else {
1159 info!("Registered board {}", board.vtxo.id());
1160 registered_boards += 1;
1161 }
1162 }
1163 }
1164 };
1165
1166 if registered_boards > 0 {
1167 info!("Registered {registered_boards} sufficiently confirmed boards");
1168 }
1169 Ok(())
1170 }
1171
1172 pub async fn maintenance(&self) -> anyhow::Result<()> {
1177 info!("Starting wallet maintenance");
1178 self.sync().await;
1179 self.progress_pending_rounds(None).await?;
1180 self.maintenance_refresh().await?;
1181 Ok(())
1182 }
1183
1184 pub async fn maintenance_with_onchain<W: PreparePsbt + SignPsbt + ExitUnilaterally>(
1190 &self,
1191 onchain: &mut W,
1192 ) -> anyhow::Result<()> {
1193 info!("Starting wallet maintenance with onchain wallet");
1194 self.sync().await;
1195 self.maintenance_refresh().await?;
1196
1197 self.sync_exits(onchain).await?;
1199
1200 Ok(())
1201 }
1202
1203 pub async fn maybe_schedule_maintenance_refresh(&self) -> anyhow::Result<Option<RoundStateId>> {
1211 let vtxos = self.get_vtxos_to_refresh().await?.into_iter()
1212 .map(|v| v.id())
1213 .collect::<Vec<_>>();
1214 if vtxos.len() == 0 {
1215 return Ok(None);
1216 }
1217
1218 info!("Scheduling maintenance refresh");
1219 let mut participation = match self.build_refresh_participation(vtxos)? {
1220 Some(participation) => participation,
1221 None => return Ok(None),
1222 };
1223
1224 if let Err(e) = self.add_should_refresh_vtxos(&mut participation).await {
1225 warn!("Error trying to add additional VTXOs that should be refreshed: {:#}", e);
1226 }
1227
1228 let state = self.join_next_round(participation, Some(RoundMovement::Refresh)).await?;
1229 Ok(Some(state.id))
1230 }
1231
1232 pub async fn maintenance_refresh(&self) -> anyhow::Result<Option<RoundStatus>> {
1238 let vtxos = self.get_vtxos_to_refresh().await?.into_iter()
1239 .map(|v| v.id())
1240 .collect::<Vec<_>>();
1241 if vtxos.len() == 0 {
1242 return Ok(None);
1243 }
1244
1245 info!("Performing maintenance refresh");
1246 self.refresh_vtxos(vtxos).await
1247 }
1248
1249 pub async fn sync(&self) {
1255 tokio::join!(
1256 async {
1257 if let Err(e) = self.chain.update_fee_rates(self.config.fallback_fee_rate).await {
1260 warn!("Error updating fee rates: {:#}", e);
1261 }
1262 },
1263 async {
1264 if let Err(e) = self.sync_oors().await {
1265 warn!("Error in arkoor sync: {:#}", e);
1266 }
1267 },
1268 async {
1269 if let Err(e) = self.sync_pending_rounds().await {
1270 warn!("Error while trying to progress rounds awaiting confirmations: {:#}", e);
1271 }
1272 },
1273 async {
1274 if let Err(e) = self.sync_pending_lightning_send_vtxos().await {
1275 warn!("Error syncing pending lightning payments: {:#}", e);
1276 }
1277 },
1278 async {
1279 if let Err(e) = self.try_claim_all_lightning_receives(false).await {
1280 warn!("Error claiming pending lightning receives: {:#}", e);
1281 }
1282 },
1283 async {
1284 if let Err(e) = self.sync_pending_boards().await {
1285 warn!("Error syncing pending boards: {:#}", e);
1286 }
1287 }
1288 );
1289 }
1290
1291 pub async fn sync_exits(
1297 &self,
1298 onchain: &mut dyn ExitUnilaterally,
1299 ) -> anyhow::Result<()> {
1300 self.exit.write().await.sync_exit(onchain).await?;
1301 Ok(())
1302 }
1303
1304 pub fn pending_lightning_sends(&self) -> anyhow::Result<Vec<LightningSend>> {
1305 Ok(self.db.get_all_pending_lightning_send()?)
1306 }
1307
1308 pub async fn sync_pending_lightning_send_vtxos(&self) -> anyhow::Result<()> {
1311 let pending_payments = self.pending_lightning_sends()?;
1312
1313 if pending_payments.is_empty() {
1314 return Ok(());
1315 }
1316
1317 info!("Syncing {} pending lightning sends", pending_payments.len());
1318
1319 for payment in pending_payments {
1320 self.check_lightning_payment(&payment).await?;
1321 }
1322
1323 Ok(())
1324 }
1325
1326 pub async fn dangerous_drop_vtxo(&self, vtxo_id: VtxoId) -> anyhow::Result<()> {
1329 warn!("Drop vtxo {} from the database", vtxo_id);
1330 self.db.remove_vtxo(vtxo_id)?;
1331 Ok(())
1332 }
1333
1334 pub async fn dangerous_drop_all_vtxos(&self) -> anyhow::Result<()> {
1337 warn!("Dropping all vtxos from the db...");
1338 for vtxo in self.vtxos()? {
1339 self.db.remove_vtxo(vtxo.id())?;
1340 }
1341
1342 self.exit.write().await.clear_exit()?;
1343 Ok(())
1344 }
1345
1346 pub async fn board_amount(
1350 &self,
1351 onchain: &mut dyn onchain::Board,
1352 amount: Amount,
1353 ) -> anyhow::Result<PendingBoard> {
1354 let (user_keypair, _) = self.derive_store_next_keypair()?;
1355 self.board(onchain, Some(amount), user_keypair).await
1356 }
1357
1358 pub async fn board_all(
1360 &self,
1361 onchain: &mut dyn onchain::Board,
1362 ) -> anyhow::Result<PendingBoard> {
1363 let (user_keypair, _) = self.derive_store_next_keypair()?;
1364 self.board(onchain, None, user_keypair).await
1365 }
1366
1367 async fn board(
1368 &self,
1369 wallet: &mut dyn onchain::Board,
1370 amount: Option<Amount>,
1371 user_keypair: Keypair,
1372 ) -> anyhow::Result<PendingBoard> {
1373 let mut srv = self.require_server()?;
1374 let ark_info = srv.ark_info().await?;
1375
1376 let properties = self.db.read_properties()?.context("Missing config")?;
1377 let current_height = self.chain.tip().await?;
1378
1379 let expiry_height = current_height + ark_info.vtxo_expiry_delta as BlockHeight;
1380 let builder = BoardBuilder::new(
1381 user_keypair.public_key(),
1382 expiry_height,
1383 ark_info.server_pubkey,
1384 ark_info.vtxo_exit_delta,
1385 );
1386
1387 let addr = bitcoin::Address::from_script(
1388 &builder.funding_script_pubkey(),
1389 properties.network,
1390 )?;
1391
1392 let fee_rate = self.chain.fee_rates().await.regular;
1394 let (board_psbt, amount) = if let Some(amount) = amount {
1395 let psbt = wallet.prepare_tx(&[(addr, amount)], fee_rate)?;
1396 (psbt, amount)
1397 } else {
1398 let psbt = wallet.prepare_drain_tx(addr, fee_rate)?;
1399 assert_eq!(psbt.unsigned_tx.output.len(), 1);
1400 let amount = psbt.unsigned_tx.output[0].value;
1401 (psbt, amount)
1402 };
1403
1404 ensure!(amount >= ark_info.min_board_amount,
1405 "board amount of {amount} is less than minimum board amount required by server ({})",
1406 ark_info.min_board_amount,
1407 );
1408
1409 let utxo = OutPoint::new(board_psbt.unsigned_tx.compute_txid(), BOARD_FUNDING_TX_VTXO_VOUT);
1410 let builder = builder
1411 .set_funding_details(amount, utxo)
1412 .generate_user_nonces();
1413
1414 let cosign_resp = srv.client.request_board_cosign(protos::BoardCosignRequest {
1415 amount: amount.to_sat(),
1416 utxo: bitcoin::consensus::serialize(&utxo), expiry_height,
1418 user_pubkey: user_keypair.public_key().serialize().to_vec(),
1419 pub_nonce: builder.user_pub_nonce().serialize().to_vec(),
1420 }).await.context("error requesting board cosign")?
1421 .into_inner().try_into().context("invalid cosign response from server")?;
1422
1423 ensure!(builder.verify_cosign_response(&cosign_resp),
1424 "invalid board cosignature received from server",
1425 );
1426
1427 let vtxo = builder.build_vtxo(&cosign_resp, &user_keypair)?;
1429
1430 let onchain_fee = board_psbt.fee()?;
1431 let movement_id = self.movements.new_movement(
1432 self.subsystem_ids[&BarkSubsystem::Board],
1433 BoardMovement::Board.to_string(),
1434 ).await?;
1435 self.movements.update_movement(
1436 movement_id,
1437 MovementUpdate::new()
1438 .produced_vtxo(&vtxo)
1439 .intended_and_effective_balance(vtxo.amount().to_signed()?)
1440 .metadata(BoardMovement::metadata(utxo, onchain_fee)?),
1441 ).await?;
1442 self.store_locked_vtxos([&vtxo], Some(movement_id))?;
1443
1444 let tx = wallet.finish_tx(board_psbt)?;
1445 self.db.store_pending_board(&vtxo, &tx, movement_id)?;
1446
1447 trace!("Broadcasting board tx: {}", bitcoin::consensus::encode::serialize_hex(&tx));
1448 self.chain.broadcast_tx(&tx).await?;
1449
1450 info!("Board broadcasted");
1451 Ok(self.db.get_pending_board_by_vtxo_id(vtxo.id())?.expect("board should be stored"))
1452 }
1453
1454 async fn register_board(&self, vtxo: impl VtxoRef) -> anyhow::Result<()> {
1456 trace!("Attempting to register board {} to server", vtxo.vtxo_id());
1457 let mut srv = self.require_server()?;
1458
1459 let vtxo = match vtxo.vtxo() {
1461 Some(v) => v,
1462 None => {
1463 &self.db.get_wallet_vtxo(vtxo.vtxo_id())?
1464 .with_context(|| format!("VTXO doesn't exist: {}", vtxo.vtxo_id()))?
1465 },
1466 };
1467
1468 srv.client.register_board_vtxo(protos::BoardVtxoRequest {
1470 board_vtxo: vtxo.serialize(),
1471 }).await.context("error registering board with the Ark server")?;
1472
1473 self.db.update_vtxo_state_checked(vtxo.id(), VtxoState::Spendable, &UNSPENT_STATES)?;
1476
1477 let board = self.db.get_pending_board_by_vtxo_id(vtxo.id())?
1478 .context("pending board not found")?;
1479
1480 self.movements.finish_movement(board.movement_id, MovementStatus::Finished).await?;
1481 self.db.remove_pending_board(&vtxo.id())?;
1482
1483 Ok(())
1484 }
1485
1486 fn has_counterparty_risk(&self, vtxo: &Vtxo) -> anyhow::Result<bool> {
1491 for past_pk in vtxo.past_arkoor_pubkeys() {
1492 if !self.db.get_public_key_idx(&past_pk)?.is_some() {
1493 return Ok(true);
1494 }
1495 }
1496 Ok(!self.db.get_public_key_idx(&vtxo.user_pubkey())?.is_some())
1497 }
1498
1499 pub async fn sync_oors(&self) -> anyhow::Result<()> {
1500 let last_pk_index = self.db.get_last_vtxo_key_index()?.unwrap_or_default();
1501 let pubkeys = (0..=last_pk_index).map(|idx| {
1502 self.vtxo_seed.derive_keypair(idx).public_key()
1503 }).collect::<Vec<_>>();
1504
1505 self.sync_arkoor_for_pubkeys(&pubkeys).await?;
1506
1507 Ok(())
1508 }
1509
1510 async fn sync_arkoor_for_pubkeys(
1512 &self,
1513 public_keys: &[PublicKey],
1514 ) -> anyhow::Result<()> {
1515 let mut srv = self.require_server()?;
1516
1517 for pubkeys in public_keys.chunks(rpc::MAX_NB_MAILBOX_PUBKEYS) {
1518 debug!("Emptying OOR mailbox at Ark server...");
1520 let req = protos::ArkoorVtxosRequest {
1521 pubkeys: pubkeys.iter().map(|pk| pk.serialize().to_vec()).collect(),
1522 };
1523 let packages = srv.client.empty_arkoor_mailbox(req).await
1524 .context("error fetching oors")?.into_inner().packages;
1525 debug!("Ark server has {} arkoor packages for us", packages.len());
1526
1527 for package in packages {
1528 let mut vtxos = Vec::with_capacity(package.vtxos.len());
1529 for vtxo in package.vtxos {
1530 let vtxo = match Vtxo::deserialize(&vtxo) {
1531 Ok(vtxo) => vtxo,
1532 Err(e) => {
1533 warn!("Invalid vtxo from Ark server: {}", e);
1534 continue;
1535 }
1536 };
1537
1538 if let Err(e) = self.validate_vtxo(&vtxo).await {
1539 error!("Received invalid arkoor VTXO from server: {}", e);
1540 continue;
1541 }
1542
1543 match self.db.has_spent_vtxo(vtxo.id()) {
1544 Ok(spent) if spent => {
1545 debug!("Not adding OOR vtxo {} because it is considered spent", vtxo.id());
1546 continue;
1547 },
1548 _ => {}
1549 }
1550
1551 if let Ok(Some(_)) = self.db.get_wallet_vtxo(vtxo.id()) {
1552 debug!("Not adding OOR vtxo {} because it already exists", vtxo.id());
1553 continue;
1554 }
1555
1556 vtxos.push(vtxo);
1557 }
1558
1559 self.store_spendable_vtxos(&vtxos)?;
1560 self.movements.new_finished_movement(
1561 self.subsystem_ids[&BarkSubsystem::Arkoor],
1562 ArkoorMovement::Receive.to_string(),
1563 MovementStatus::Finished,
1564 MovementUpdate::new()
1565 .produced_vtxos(&vtxos)
1566 .intended_and_effective_balance(
1567 vtxos
1568 .iter()
1569 .map(|vtxo| vtxo.amount()).sum::<Amount>()
1570 .to_signed()?,
1571 ),
1572 ).await?;
1573 }
1574 }
1575
1576 Ok(())
1577 }
1578
1579 async fn add_should_refresh_vtxos(
1583 &self,
1584 participation: &mut RoundParticipation,
1585 ) -> anyhow::Result<()> {
1586 let excluded_ids = participation.inputs.iter().map(|v| v.vtxo_id())
1587 .collect::<HashSet<_>>();
1588
1589 let should_refresh_vtxos = self.get_vtxos_to_refresh().await?.into_iter()
1590 .filter(|v| !excluded_ids.contains(&v.id()))
1591 .map(|v| v.vtxo).collect::<Vec<_>>();
1592
1593
1594 let total_amount = should_refresh_vtxos.iter().map(|v| v.amount()).sum::<Amount>();
1595
1596 if total_amount > P2TR_DUST {
1597 let (user_keypair, _) = self.derive_store_next_keypair()?;
1598 let req = VtxoRequest {
1599 policy: VtxoPolicy::new_pubkey(user_keypair.public_key()),
1600 amount: total_amount,
1601 };
1602
1603 participation.inputs.extend(should_refresh_vtxos);
1604 participation.outputs.push(req);
1605 }
1606
1607 Ok(())
1608 }
1609
1610 pub async fn build_offboard_participation<V: VtxoRef>(
1611 &self,
1612 vtxos: impl IntoIterator<Item = V>,
1613 destination: ScriptBuf,
1614 ) -> anyhow::Result<RoundParticipation> {
1615 let srv = self.require_server()?;
1616 let ark_info = srv.ark_info().await?;
1617
1618 let vtxos = {
1619 let vtxos = vtxos.into_iter();
1620 let mut ret = Vec::with_capacity(vtxos.size_hint().0);
1621 for v in vtxos {
1622 let vtxo = match v.vtxo() {
1623 Some(v) => v.clone(),
1624 None => self.get_vtxo_by_id(v.vtxo_id()).context("vtxo not found")?.vtxo,
1625 };
1626 ret.push(vtxo);
1627 }
1628 ret
1629 };
1630
1631 if vtxos.is_empty() {
1632 bail!("no VTXO to offboard");
1633 }
1634
1635 let fee = OffboardRequest::calculate_fee(&destination, ark_info.offboard_feerate)
1636 .expect("bdk created invalid scriptPubkey");
1637
1638 let vtxo_sum = vtxos.iter().map(|v| v.amount()).sum::<Amount>();
1639
1640 if fee > vtxo_sum {
1641 bail!("offboarded amount is lower than fees. Need {fee}, got: {vtxo_sum}");
1642 }
1643
1644 let offb = OffboardRequest {
1645 amount: vtxo_sum - fee,
1646 script_pubkey: destination.clone(),
1647 };
1648
1649 Ok(RoundParticipation {
1650 inputs: vtxos.clone(),
1651 outputs: Vec::new(),
1652 offboards: vec![offb],
1653 })
1654 }
1655
1656 pub async fn offboard<V: VtxoRef>(
1657 &self,
1658 vtxos: impl IntoIterator<Item = V>,
1659 destination: ScriptBuf,
1660 ) -> anyhow::Result<RoundStatus> {
1661 let mut participation = self.build_offboard_participation(vtxos, destination.clone()).await?;
1662
1663 if let Err(e) = self.add_should_refresh_vtxos(&mut participation).await {
1664 warn!("Error trying to add additional VTXOs that should be refreshed: {:#}", e);
1665 }
1666
1667 Ok(self.participate_round(participation, Some(RoundMovement::Offboard)).await?)
1668 }
1669
1670 pub async fn offboard_all(&self, address: bitcoin::Address) -> anyhow::Result<RoundStatus> {
1672 let input_vtxos = self.spendable_vtxos()?;
1673 Ok(self.offboard(input_vtxos, address.script_pubkey()).await?)
1674 }
1675
1676 pub async fn offboard_vtxos<V: VtxoRef>(
1678 &self,
1679 vtxos: impl IntoIterator<Item = V>,
1680 address: bitcoin::Address,
1681 ) -> anyhow::Result<RoundStatus> {
1682 let input_vtxos = vtxos
1683 .into_iter()
1684 .map(|v| {
1685 let id = v.vtxo_id();
1686 match self.db.get_wallet_vtxo(id)? {
1687 Some(vtxo) => Ok(vtxo.vtxo),
1688 _ => bail!("cannot find requested vtxo: {}", id),
1689 }
1690 })
1691 .collect::<anyhow::Result<Vec<_>>>()?;
1692
1693 Ok(self.offboard(input_vtxos, address.script_pubkey()).await?)
1694 }
1695
1696 pub fn build_refresh_participation<V: VtxoRef>(
1697 &self,
1698 vtxos: impl IntoIterator<Item = V>,
1699 ) -> anyhow::Result<Option<RoundParticipation>> {
1700 let vtxos = {
1701 let mut ret = HashMap::new();
1702 for v in vtxos {
1703 let id = v.vtxo_id();
1704 let vtxo = self.get_vtxo_by_id(id)
1705 .with_context(|| format!("vtxo with id {} not found", id))?;
1706 if !ret.insert(id, vtxo).is_none() {
1707 bail!("duplicate VTXO id: {}", id);
1708 }
1709 }
1710 ret
1711 };
1712
1713 if vtxos.is_empty() {
1714 info!("Skipping refresh since no VTXOs are provided.");
1715 return Ok(None);
1716 }
1717
1718 let total_amount = vtxos.values().map(|v| v.vtxo.amount()).sum();
1719
1720 info!("Refreshing {} VTXOs (total amount = {}).", vtxos.len(), total_amount);
1721
1722 let (user_keypair, _) = self.derive_store_next_keypair()?;
1723 let req = VtxoRequest {
1724 policy: VtxoPolicy::Pubkey(PubkeyVtxoPolicy { user_pubkey: user_keypair.public_key() }),
1725 amount: total_amount,
1726 };
1727
1728 Ok(Some(RoundParticipation {
1729 inputs: vtxos.into_values().map(|v| v.vtxo).collect(),
1730 outputs: vec![req],
1731 offboards: Vec::new(),
1732 }))
1733 }
1734
1735 pub async fn refresh_vtxos<V: VtxoRef>(
1741 &self,
1742 vtxos: impl IntoIterator<Item = V>,
1743 ) -> anyhow::Result<Option<RoundStatus>> {
1744 let mut participation = match self.build_refresh_participation(vtxos)? {
1745 Some(participation) => participation,
1746 None => return Ok(None),
1747 };
1748
1749 if let Err(e) = self.add_should_refresh_vtxos(&mut participation).await {
1750 warn!("Error trying to add additional VTXOs that should be refreshed: {:#}", e);
1751 }
1752
1753 Ok(Some(self.participate_round(participation, Some(RoundMovement::Refresh)).await?))
1754 }
1755
1756 pub async fn get_vtxos_to_refresh(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1760 let tip = self.chain.tip().await?;
1761 let fee_rate = self.chain.fee_rates().await.fast;
1762
1763 let must_refresh_vtxos = self.spendable_vtxos_with(
1765 &RefreshStrategy::must_refresh(self, tip, fee_rate),
1766 )?;
1767 if must_refresh_vtxos.is_empty() {
1768 return Ok(vec![]);
1769 } else {
1770 let should_refresh_vtxos = self.spendable_vtxos_with(
1773 &RefreshStrategy::should_refresh(self, tip, fee_rate),
1774 )?;
1775 Ok(should_refresh_vtxos)
1776 }
1777 }
1778
1779 pub fn get_first_expiring_vtxo_blockheight(
1781 &self,
1782 ) -> anyhow::Result<Option<BlockHeight>> {
1783 Ok(self.spendable_vtxos()?.iter().map(|v| v.expiry_height()).min())
1784 }
1785
1786 pub fn get_next_required_refresh_blockheight(
1789 &self,
1790 ) -> anyhow::Result<Option<BlockHeight>> {
1791 let first_expiry = self.get_first_expiring_vtxo_blockheight()?;
1792 Ok(first_expiry.map(|h| h.saturating_sub(self.config.vtxo_refresh_expiry_threshold)))
1793 }
1794
1795 fn select_vtxos_to_cover(
1801 &self,
1802 amount: Amount,
1803 expiry_threshold: Option<BlockHeight>,
1804 ) -> anyhow::Result<Vec<Vtxo>> {
1805 let inputs = self.spendable_vtxos()?;
1806
1807 let mut result = Vec::new();
1809 let mut total_amount = bitcoin::Amount::ZERO;
1810 for input in inputs {
1811 if let Some(threshold) = expiry_threshold {
1813 if input.expiry_height() < threshold {
1814 warn!("VTXO {} is expiring soon (expires at {}, threshold {}), \
1815 skipping for arkoor payment",
1816 input.id(), input.expiry_height(), threshold,
1817 );
1818 continue;
1819 }
1820 }
1821
1822 total_amount += input.amount();
1823 result.push(input.vtxo);
1824
1825 if total_amount >= amount {
1826 return Ok(result)
1827 }
1828 }
1829
1830 bail!("Insufficient money available. Needed {} but {} is available",
1831 amount, total_amount,
1832 );
1833 }
1834
1835 async fn create_arkoor_vtxos(
1841 &self,
1842 destination_policy: VtxoPolicy,
1843 amount: Amount,
1844 ) -> anyhow::Result<ArkoorCreateResult> {
1845 let mut srv = self.require_server()?;
1846
1847 let change_pubkey = self.derive_store_next_keypair()?.0.public_key();
1848
1849 let req = VtxoRequest {
1850 amount,
1851 policy: destination_policy,
1852 };
1853
1854 let tip = self.chain.tip().await?;
1856 let inputs = self.select_vtxos_to_cover(
1857 req.amount,
1858 Some(tip + self.config.vtxo_refresh_expiry_threshold),
1859 )?;
1860
1861 let mut secs = Vec::with_capacity(inputs.len());
1862 let mut pubs = Vec::with_capacity(inputs.len());
1863 let mut keypairs = Vec::with_capacity(inputs.len());
1864 for input in inputs.iter() {
1865 let keypair = self.get_vtxo_key(&input)?;
1866 let (s, p) = musig::nonce_pair(&keypair);
1867 secs.push(s);
1868 pubs.push(p);
1869 keypairs.push(keypair);
1870 }
1871
1872 let builder = ArkoorPackageBuilder::new(&inputs, &pubs, req, Some(change_pubkey))?;
1873
1874 let req = protos::ArkoorPackageCosignRequest {
1875 arkoors: builder.arkoors.iter().map(|a| a.into()).collect(),
1876 };
1877 let cosign_resp: Vec<_> = srv.client.request_arkoor_package_cosign(req).await?
1878 .into_inner().try_into().context("invalid server cosign response")?;
1879 ensure!(builder.verify_cosign_response(&cosign_resp),
1880 "invalid arkoor cosignature received from server",
1881 );
1882
1883 let (sent, change) = builder.build_vtxos(&cosign_resp, &keypairs, secs)?;
1884
1885 if let Some(change) = change.as_ref() {
1886 info!("Added change VTXO of {}", change.amount());
1887 }
1888
1889 Ok(ArkoorCreateResult {
1890 input: inputs,
1891 created: sent,
1892 change,
1893 })
1894 }
1895
1896 pub async fn validate_arkoor_address(&self, address: &ark::Address) -> anyhow::Result<()> {
1900 let srv = self.require_server()?;
1901
1902 if !address.ark_id().is_for_server(srv.ark_info().await?.server_pubkey) {
1903 bail!("Ark address is for different server");
1904 }
1905
1906 match address.policy().policy_type() {
1908 VtxoPolicyKind::Pubkey => {},
1909 VtxoPolicyKind::Checkpoint | VtxoPolicyKind::ServerHtlcRecv | VtxoPolicyKind::ServerHtlcSend => {
1910 bail!("VTXO policy in address cannot be used for arkoor payment: {}",
1911 address.policy().policy_type(),
1912 );
1913 }
1914 }
1915
1916 if address.delivery().is_empty() {
1917 bail!("No VTXO delivery mechanism provided in address");
1918 }
1919 if !address.delivery().iter().any(|d| !d.is_unknown()) {
1923 for d in address.delivery() {
1924 if let VtxoDelivery::Unknown { delivery_type, data } = d {
1925 info!("Unknown delivery in address: type={:#x}, data={}",
1926 delivery_type, data.as_hex(),
1927 );
1928 }
1929 }
1930 }
1931
1932 Ok(())
1933 }
1934
1935 pub async fn send_arkoor_payment(
1945 &self,
1946 destination: &ark::Address,
1947 amount: Amount,
1948 ) -> anyhow::Result<Vec<Vtxo>> {
1949 let mut srv = self.require_server()?;
1950
1951 self.validate_arkoor_address(&destination).await
1952 .context("address validation failed")?;
1953
1954 if amount < P2TR_DUST {
1955 bail!("Sent amount must be at least {}", P2TR_DUST);
1956 }
1957
1958 let mut movement = MovementGuard::new_movement(
1959 self.movements.clone(),
1960 self.subsystem_ids[&BarkSubsystem::Arkoor],
1961 ArkoorMovement::Send.to_string(),
1962 ).await?;
1963 let arkoor = self.create_arkoor_vtxos(destination.policy().clone(), amount).await?;
1964 movement.apply_update(
1965 arkoor.to_movement_update()?
1966 .sent_to([MovementDestination::new(destination.to_string(), amount)])
1967 .intended_and_effective_balance(-amount.to_signed()?)
1968 ).await?;
1969
1970 let req = protos::ArkoorPackage {
1971 arkoors: arkoor.created.iter().map(|v| protos::ArkoorVtxo {
1972 pubkey: destination.policy().user_pubkey().serialize().to_vec(),
1973 vtxo: v.serialize().to_vec(),
1974 }).collect(),
1975 };
1976
1977 if let Err(e) = srv.client.post_arkoor_package_mailbox(req).await {
1980 error!("Failed to post the arkoor vtxo to the recipients mailbox: '{:#}'", e);
1981 }
1983 self.mark_vtxos_as_spent(&arkoor.input)?;
1984 if let Some(change) = arkoor.change {
1985 self.store_spendable_vtxos(&[change])?;
1986 }
1987 movement.finish(MovementStatus::Finished).await?;
1988 Ok(arkoor.created)
1989 }
1990
1991 pub fn pending_lightning_receives(&self) -> anyhow::Result<Vec<LightningReceive>> {
1993 Ok(self.db.get_all_pending_lightning_receives()?)
1994 }
1995
1996 pub fn claimable_lightning_receive_balance(&self) -> anyhow::Result<Amount> {
1997 let receives = self.pending_lightning_receives()?;
1998
1999 let mut total = Amount::ZERO;
2000 for receive in receives {
2001 if let Some(htlc_vtxos) = receive.htlc_vtxos {
2002 total += htlc_vtxos.iter().map(|v| v.amount()).sum::<Amount>();
2003 }
2004 }
2005
2006 Ok(total)
2007 }
2008
2009 pub async fn build_round_onchain_payment_participation(
2010 &self,
2011 addr: bitcoin::Address,
2012 amount: Amount,
2013 ) -> anyhow::Result<RoundParticipation> {
2014 let ark_info = self.require_server()?.ark_info().await?;
2015
2016 let offb = OffboardRequest {
2017 script_pubkey: addr.script_pubkey(),
2018 amount: amount,
2019 };
2020 let required_amount = offb.amount + offb.fee(ark_info.offboard_feerate)?;
2021
2022 let inputs = self.select_vtxos_to_cover(required_amount, None)?;
2023
2024 let change = {
2025 let input_sum = inputs.iter().map(|v| v.amount()).sum::<Amount>();
2026 if input_sum < offb.amount {
2027 bail!("Your balance is too low. Needed: {}, available: {}",
2028 required_amount, self.balance()?.spendable,
2029 );
2030 } else if input_sum <= required_amount + P2TR_DUST {
2031 info!("No change, emptying wallet.");
2032 None
2033 } else {
2034 let change_amount = input_sum - required_amount;
2035 let (change_keypair, _) = self.derive_store_next_keypair()?;
2036 info!("Adding change vtxo for {}", change_amount);
2037 Some(VtxoRequest {
2038 amount: change_amount,
2039 policy: VtxoPolicy::new_pubkey(change_keypair.public_key()),
2040 })
2041 }
2042 };
2043
2044 Ok(RoundParticipation {
2045 inputs: inputs,
2046 outputs: change.into_iter().collect(),
2047 offboards: vec![offb],
2048 })
2049 }
2050
2051 pub async fn send_round_onchain_payment(
2054 &self,
2055 addr: bitcoin::Address,
2056 amount: Amount,
2057 ) -> anyhow::Result<RoundStatus> {
2058 let mut participation = self.build_round_onchain_payment_participation(addr, amount).await?;
2059
2060 if let Err(e) = self.add_should_refresh_vtxos(&mut participation).await {
2061 warn!("Error trying to add additional VTXOs that should be refreshed: {:#}", e);
2062 }
2063
2064 self.participate_round(participation, Some(RoundMovement::SendOnchain)).await
2065 }
2066
2067 pub async fn run_daemon(
2073 self: &Arc<Self>,
2074 shutdown: CancellationToken,
2075 onchain: Arc<RwLock<dyn ExitUnilaterally>>,
2076 ) -> anyhow::Result<()> {
2077 let daemon = Daemon::new(shutdown, self.clone(), onchain)?;
2078
2079 tokio::spawn(async move {
2080 daemon.run().await;
2081 });
2082
2083 Ok(())
2084 }
2085}