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 error;
293pub mod exit;
294pub mod lightning_utils;
295pub mod movement;
296pub mod onchain;
297pub mod persist;
298pub mod round;
299pub mod subsystem;
300pub mod vtxo;
301
302pub use self::config::{BarkNetwork, Config};
303pub use self::persist::sqlite::SqliteClient;
304pub use self::vtxo::state::WalletVtxo;
305
306mod config;
307mod lnurl;
308mod psbtext;
309
310use std::collections::{HashMap, HashSet};
311
312use std::convert::TryFrom;
313use std::fmt;
314use std::str::FromStr;
315use std::sync::Arc;
316
317use anyhow::{bail, Context};
318use bip39::Mnemonic;
319use bitcoin::{Amount, Network, OutPoint, ScriptBuf, SignedAmount};
320use bitcoin::bip32::{self, Fingerprint};
321use bitcoin::hex::DisplayHex;
322use bitcoin::secp256k1::{self, Keypair, PublicKey};
323use futures::StreamExt;
324use lightning_invoice::Bolt11Invoice;
325use lightning::util::ser::Writeable;
326use lnurllib::lightning_address::LightningAddress;
327use log::{trace, debug, info, warn, error};
328use tokio::sync::RwLock;
329use tokio_util::sync::CancellationToken;
330
331use ark::{ArkInfo, OffboardRequest, ProtocolEncoding, Vtxo, VtxoId, VtxoPolicy, VtxoRequest};
332use ark::address::VtxoDelivery;
333use ark::arkoor::ArkoorPackageBuilder;
334use ark::board::{BoardBuilder, BOARD_FUNDING_TX_VTXO_VOUT};
335use ark::lightning::{Bolt12Invoice, Bolt12InvoiceExt, Invoice, LightningReceiveChallenge, Offer, PaymentHash, Preimage};
336use ark::musig;
337use ark::rounds::RoundId;
338use ark::vtxo::{VtxoRef, PubkeyVtxoPolicy, VtxoPolicyKind};
339use bitcoin_ext::{AmountExt, BlockDelta, BlockHeight, P2TR_DUST, TxStatus};
340use server_rpc::protos::prepare_lightning_receive_claim_request::LightningReceiveAntiDos;
341use server_rpc::{self as rpc, protos, ServerConnection};
342
343use crate::daemon::Daemon;
344use crate::exit::Exit;
345use crate::movement::{Movement, MovementDestination, MovementStatus};
346use crate::movement::manager::{MovementGuard, MovementManager};
347use crate::movement::update::MovementUpdate;
348use crate::onchain::{ChainSource, PreparePsbt, ExitUnilaterally, Utxo, SignPsbt};
349use crate::persist::BarkPersister;
350use crate::persist::models::{PendingLightningSend, LightningReceive};
351use crate::round::{RoundParticipation, RoundStatus};
352use crate::subsystem::{
353 ArkoorMovement, BarkSubsystem, BoardMovement, LightningMovement, LightningReceiveMovement,
354 LightningSendMovement, RoundMovement, SubsystemId,
355};
356use crate::vtxo::selection::{FilterVtxos, VtxoFilter, RefreshStrategy};
357use crate::vtxo::state::{VtxoState, VtxoStateKind, UNSPENT_STATES};
358
359const ARK_PURPOSE_INDEX: u32 = 350;
360
361const LIGHTNING_PREPARE_CLAIM_DELTA: BlockDelta = 2;
364
365lazy_static::lazy_static! {
366 static ref SECP: secp256k1::Secp256k1<secp256k1::All> = secp256k1::Secp256k1::new();
368}
369
370#[derive(Debug, Clone)]
372pub struct LightningReceiveBalance {
373 pub total: Amount,
375 pub claimable: Amount,
377}
378
379#[derive(Debug, Clone)]
381pub struct Balance {
382 pub spendable: Amount,
384 pub pending_lightning_send: Amount,
386 pub pending_lightning_receive: LightningReceiveBalance,
388 pub pending_in_round: Amount,
390 pub pending_exit: Option<Amount>,
393 pub pending_board: Amount,
395}
396
397struct ArkoorCreateResult {
398 input: Vec<Vtxo>,
399 created: Vec<Vtxo>,
400 change: Option<Vtxo>,
401}
402
403impl ArkoorCreateResult {
404 pub fn to_movement_update(&self) -> anyhow::Result<MovementUpdate> {
405 Ok(MovementUpdate::new()
406 .consumed_vtxos(self.input.iter())
407 .produced_vtxo_if_some(self.change.as_ref())
408 )
409 }
410}
411
412pub struct UtxoInfo {
413 pub outpoint: OutPoint,
414 pub amount: Amount,
415 pub confirmation_height: Option<u32>,
416}
417
418impl From<Utxo> for UtxoInfo {
419 fn from(value: Utxo) -> Self {
420 match value {
421 Utxo::Local(o) => UtxoInfo {
422 outpoint: o.outpoint,
423 amount: o.amount,
424 confirmation_height: o.confirmation_height,
425 },
426 Utxo::Exit(e) => UtxoInfo {
427 outpoint: e.vtxo.point(),
428 amount: e.vtxo.amount(),
429 confirmation_height: Some(e.height),
430 },
431 }
432 }
433}
434
435#[derive(Debug, Clone, PartialEq, Eq, Hash)]
437pub struct Board {
438 pub funding_txid: bitcoin::Txid,
442 pub vtxos: Vec<Vtxo>,
447}
448
449#[derive(Debug, Clone, PartialEq, Eq, Hash)]
452pub struct Offboard {
453 pub round: RoundId,
455}
456
457pub struct OffchainBalance {
460 pub available: Amount,
462 pub pending_in_round: Amount,
464 pub pending_exit: Amount,
467}
468
469#[derive(Debug, Clone)]
471pub struct WalletProperties {
472 pub network: Network,
476
477 pub fingerprint: Fingerprint,
481}
482
483pub struct VtxoSeed(bip32::Xpriv);
489
490impl VtxoSeed {
491 fn new(network: Network, seed: &[u8; 64]) -> Self {
492 let master = bip32::Xpriv::new_master(network, seed).unwrap();
493
494 Self(master.derive_priv(&SECP, &[ARK_PURPOSE_INDEX.into()]).unwrap())
495 }
496
497 fn fingerprint(&self) -> Fingerprint {
498 self.0.fingerprint(&SECP)
499 }
500
501 fn derive_keypair(&self, idx: u32) -> Keypair {
502 self.0.derive_priv(&SECP, &[idx.into()]).unwrap().to_keypair(&SECP)
503 }
504}
505
506pub struct Wallet {
640 pub chain: Arc<ChainSource>,
642
643 pub exit: RwLock<Exit>,
645
646 pub movements: Arc<MovementManager>,
648
649 config: Config,
651
652 db: Arc<dyn BarkPersister>,
654
655 vtxo_seed: VtxoSeed,
657
658 server: Option<ServerConnection>,
660
661 subsystem_ids: HashMap<BarkSubsystem, SubsystemId>,
663}
664
665impl Wallet {
666 pub fn chain_source(
669 config: &Config,
670 ) -> anyhow::Result<onchain::ChainSourceSpec> {
671 if let Some(ref url) = config.esplora_address {
672 Ok(onchain::ChainSourceSpec::Esplora {
673 url: url.clone(),
674 })
675 } else if let Some(ref url) = config.bitcoind_address {
676 let auth = if let Some(ref c) = config.bitcoind_cookiefile {
677 bitcoin_ext::rpc::Auth::CookieFile(c.clone())
678 } else {
679 bitcoin_ext::rpc::Auth::UserPass(
680 config.bitcoind_user.clone().context("need bitcoind auth config")?,
681 config.bitcoind_pass.clone().context("need bitcoind auth config")?,
682 )
683 };
684 Ok(onchain::ChainSourceSpec::Bitcoind {
685 url: url.clone(),
686 auth,
687 })
688 } else {
689 bail!("Need to either provide esplora or bitcoind info");
690 }
691 }
692
693 pub fn require_chainsource_version(&self) -> anyhow::Result<()> {
697 self.chain.require_version()
698 }
699
700 pub fn derive_store_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
703 let last_revealed = self.db.get_last_vtxo_key_index()?;
704
705 let index = last_revealed.map(|i| i + 1).unwrap_or(u32::MIN);
706 let keypair = self.vtxo_seed.derive_keypair(index);
707
708 self.db.store_vtxo_key(index, keypair.public_key())?;
709 Ok((keypair, index))
710 }
711
712 pub fn peak_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
726 let keypair = self.vtxo_seed.derive_keypair(index);
727 if self.db.get_public_key_idx(&keypair.public_key())?.is_some() {
728 Ok(keypair)
729 } else {
730 bail!("VTXO key {} does not exist, please derive it first", index)
731 }
732 }
733
734
735 pub fn pubkey_keypair(&self, public_key: &PublicKey) -> anyhow::Result<Option<(u32, Keypair)>> {
747 if let Some(index) = self.db.get_public_key_idx(&public_key)? {
748 Ok(Some((index, self.vtxo_seed.derive_keypair(index))))
749 } else {
750 Ok(None)
751 }
752 }
753
754 pub fn get_vtxo_key(&self, vtxo: &Vtxo) -> anyhow::Result<Keypair> {
765 let idx = self.db.get_public_key_idx(&vtxo.user_pubkey())?
766 .context("VTXO key not found")?;
767 Ok(self.vtxo_seed.derive_keypair(idx))
768 }
769
770 pub fn new_address(&self) -> anyhow::Result<ark::Address> {
772 let ark = &self.require_server()?;
773 let network = self.properties()?.network;
774 let pubkey = self.derive_store_next_keypair()?.0.public_key();
775
776 Ok(ark::Address::builder()
777 .testnet(network != bitcoin::Network::Bitcoin)
778 .server_pubkey(ark.info.server_pubkey)
779 .pubkey_policy(pubkey)
780 .into_address().unwrap())
781 }
782
783 pub fn peak_address(&self, index: u32) -> anyhow::Result<ark::Address> {
787 let ark = &self.require_server()?;
788 let network = self.properties()?.network;
789 let pubkey = self.peak_keypair(index)?.public_key();
790
791 Ok(ark::Address::builder()
792 .testnet(network != Network::Bitcoin)
793 .server_pubkey(ark.info.server_pubkey)
794 .pubkey_policy(pubkey)
795 .into_address().unwrap())
796 }
797
798 pub fn new_address_with_index(&self) -> anyhow::Result<(ark::Address, u32)> {
802 let ark = &self.require_server()?;
803 let network = self.properties()?.network;
804 let (keypair, index) = self.derive_store_next_keypair()?;
805 let pubkey = keypair.public_key();
806 let addr = ark::Address::builder()
807 .testnet(network != bitcoin::Network::Bitcoin)
808 .server_pubkey(ark.info.server_pubkey)
809 .pubkey_policy(pubkey)
810 .into_address()?;
811 Ok((addr, index))
812 }
813
814 pub async fn create(
820 mnemonic: &Mnemonic,
821 network: Network,
822 config: Config,
823 db: Arc<dyn BarkPersister>,
824 force: bool,
825 ) -> anyhow::Result<Wallet> {
826 trace!("Config: {:?}", config);
827 if let Some(existing) = db.read_properties()? {
828 trace!("Existing config: {:?}", existing);
829 bail!("cannot overwrite already existing config")
830 }
831
832 if !force {
833 if let Err(err) = ServerConnection::connect(&config.server_address, network).await {
834 bail!("Failed to connect to provided server (if you are sure use the --force flag): {}", err);
835 }
836 }
837
838 let wallet_fingerprint = VtxoSeed::new(network, &mnemonic.to_seed("")).fingerprint();
839 let properties = WalletProperties {
840 network: network,
841 fingerprint: wallet_fingerprint,
842 };
843
844 db.init_wallet(&properties).context("cannot init wallet in the database")?;
846 info!("Created wallet with fingerprint: {}", wallet_fingerprint);
847
848 let wallet = Wallet::open(&mnemonic, db, config).await.context("failed to open wallet")?;
850 wallet.require_chainsource_version()?;
851
852 Ok(wallet)
853 }
854
855 pub async fn create_with_onchain(
863 mnemonic: &Mnemonic,
864 network: Network,
865 config: Config,
866 db: Arc<dyn BarkPersister>,
867 onchain: &dyn ExitUnilaterally,
868 force: bool,
869 ) -> anyhow::Result<Wallet> {
870 let mut wallet = Wallet::create(mnemonic, network, config, db, force).await?;
871 wallet.exit.get_mut().load(onchain).await?;
872 Ok(wallet)
873 }
874
875 pub async fn open(
877 mnemonic: &Mnemonic,
878 db: Arc<dyn BarkPersister>,
879 config: Config,
880 ) -> anyhow::Result<Wallet> {
881 let properties = db.read_properties()?.context("Wallet is not initialised")?;
882
883 let seed = mnemonic.to_seed("");
884 let vtxo_seed = VtxoSeed::new(properties.network, &seed);
885
886 if properties.fingerprint != vtxo_seed.fingerprint() {
887 bail!("incorrect mnemonic")
888 }
889
890 let chain_source = if let Some(ref url) = config.esplora_address {
891 onchain::ChainSourceSpec::Esplora {
892 url: url.clone(),
893 }
894 } else if let Some(ref url) = config.bitcoind_address {
895 let auth = if let Some(ref c) = config.bitcoind_cookiefile {
896 bitcoin_ext::rpc::Auth::CookieFile(c.clone())
897 } else {
898 bitcoin_ext::rpc::Auth::UserPass(
899 config.bitcoind_user.clone().context("need bitcoind auth config")?,
900 config.bitcoind_pass.clone().context("need bitcoind auth config")?,
901 )
902 };
903 onchain::ChainSourceSpec::Bitcoind { url: url.clone(), auth }
904 } else {
905 bail!("Need to either provide esplora or bitcoind info");
906 };
907
908 let chain_source_client = ChainSource::new(
909 chain_source, properties.network, config.fallback_fee_rate,
910 ).await?;
911 let chain = Arc::new(chain_source_client);
912
913 let server = match ServerConnection::connect(
914 &config.server_address, properties.network,
915 ).await {
916 Ok(s) => Some(s),
917 Err(e) => {
918 warn!("Ark server handshake failed: {}", e);
919 None
920 }
921 };
922
923 let movements = Arc::new(MovementManager::new(db.clone()));
924 let exit = RwLock::new(Exit::new(db.clone(), chain.clone(), movements.clone()).await?);
925 let mut subsystem_ids = HashMap::new();
926 {
927 let subsystems = [
928 BarkSubsystem::Arkoor,
929 BarkSubsystem::Board,
930 BarkSubsystem::LightningReceive,
931 BarkSubsystem::LightningSend,
932 BarkSubsystem::Round,
933 ];
934 for subsystem in subsystems.into_iter() {
935 let id = movements.register_subsystem(subsystem.as_str().into()).await?;
936 subsystem_ids.insert(subsystem, id);
937 }
938 };
939
940 Ok(Wallet { config, db, vtxo_seed, exit, movements, server, chain, subsystem_ids })
941 }
942
943 pub async fn open_with_onchain(
946 mnemonic: &Mnemonic,
947 db: Arc<dyn BarkPersister>,
948 onchain: &dyn ExitUnilaterally,
949 cfg: Config,
950 ) -> anyhow::Result<Wallet> {
951 let mut wallet = Wallet::open(mnemonic, db, cfg).await?;
952 wallet.exit.get_mut().load(onchain).await?;
953 Ok(wallet)
954 }
955
956 pub fn config(&self) -> &Config {
958 &self.config
959 }
960
961 pub fn properties(&self) -> anyhow::Result<WalletProperties> {
963 let properties = self.db.read_properties()?.context("Wallet is not initialised")?;
964 Ok(properties)
965 }
966
967 fn require_server(&self) -> anyhow::Result<ServerConnection> {
968 self.server.clone().context("You should be connected to Ark server to perform this action")
969 }
970
971 pub async fn check_connection(&self) -> anyhow::Result<()> {
972 let mut server = self.require_server()?;
973 server.client.handshake(protos::HandshakeRequest {
974 bark_version: Some(env!("CARGO_PKG_VERSION").into()),
975 }).await?;
976
977 Ok(())
978 }
979
980 pub fn ark_info(&self) -> Option<&ArkInfo> {
982 self.server.as_ref().map(|a| &a.info)
983 }
984
985 pub fn balance(&self) -> anyhow::Result<Balance> {
989 let vtxos = self.vtxos()?;
990
991 let spendable = {
992 let mut v = vtxos.iter().collect();
993 VtxoStateKind::Spendable.filter_vtxos(&mut v)?;
994 v.into_iter().map(|v| v.amount()).sum::<Amount>()
995 };
996
997 let pending_lightning_send = self.pending_lightning_send_vtxos()?.iter().map(|v| v.amount())
998 .sum::<Amount>();
999
1000 let pending_lightning_receive = self.pending_lightning_receive_balance()?;
1001
1002 let pending_board = self.pending_board_vtxos()?.iter().map(|v| v.amount()).sum::<Amount>();
1003
1004 let pending_in_round = self.pending_round_input_vtxos()?.iter().map(|v| v.amount()).sum();
1005
1006 let pending_exit = self.exit.try_read().ok().map(|e| e.pending_total());
1007
1008 Ok(Balance {
1009 spendable,
1010 pending_in_round,
1011 pending_lightning_send,
1012 pending_lightning_receive,
1013 pending_exit,
1014 pending_board,
1015 })
1016 }
1017
1018 pub async fn validate_vtxo(&self, vtxo: &Vtxo) -> anyhow::Result<()> {
1020 let tx = self.chain.get_tx(&vtxo.chain_anchor().txid).await
1021 .context("could not fetch chain tx")?;
1022
1023 let tx = tx.with_context(|| {
1024 format!("vtxo chain anchor not found for vtxo: {}", vtxo.chain_anchor().txid)
1025 })?;
1026
1027 vtxo.validate(&tx)?;
1028
1029 Ok(())
1030 }
1031
1032 pub fn get_vtxo_by_id(&self, vtxo_id: VtxoId) -> anyhow::Result<WalletVtxo> {
1034 let vtxo = self.db.get_wallet_vtxo(vtxo_id)
1035 .with_context(|| format!("Error when querying vtxo {} in database", vtxo_id))?
1036 .with_context(|| format!("The VTXO with id {} cannot be found", vtxo_id))?;
1037 Ok(vtxo)
1038 }
1039
1040 pub fn movements(&self) -> anyhow::Result<Vec<Movement>> {
1042 Ok(self.db.get_movements()?)
1043 }
1044
1045 pub fn all_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1047 Ok(self.db.get_all_vtxos()?)
1048 }
1049
1050 pub fn vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1052 Ok(self.db.get_vtxos_by_state(&UNSPENT_STATES)?)
1053 }
1054
1055 pub fn vtxos_with(&self, filter: &impl FilterVtxos) -> anyhow::Result<Vec<WalletVtxo>> {
1057 let mut vtxos = self.vtxos()?;
1058 filter.filter_vtxos(&mut vtxos).context("error filtering vtxos")?;
1059 Ok(vtxos)
1060 }
1061
1062 pub fn spendable_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1064 Ok(self.vtxos_with(&VtxoStateKind::Spendable)?)
1065 }
1066
1067 pub fn spendable_vtxos_with(
1069 &self,
1070 filter: &impl FilterVtxos,
1071 ) -> anyhow::Result<Vec<WalletVtxo>> {
1072 let mut vtxos = self.spendable_vtxos()?;
1073 filter.filter_vtxos(&mut vtxos).context("error filtering vtxos")?;
1074 Ok(vtxos)
1075 }
1076
1077 pub fn pending_board_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1082 let vtxos = self.db.get_all_pending_board_ids()?.iter()
1083 .map(|vtxo_id| self.get_vtxo_by_id(*vtxo_id))
1084 .collect::<anyhow::Result<Vec<_>>>()?;
1085
1086 debug_assert!(vtxos.iter().all(|v| matches!(v.state.kind(), VtxoStateKind::Locked)),
1087 "all pending board vtxos should be locked"
1088 );
1089
1090 Ok(vtxos)
1091 }
1092
1093 pub fn pending_round_input_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1098 let mut ret = Vec::new();
1099 for round in self.db.load_round_states()? {
1100 let inputs = round.state.locked_pending_inputs();
1101 ret.reserve(inputs.len());
1102 for input in inputs {
1103 ret.push(self.get_vtxo_by_id(input.id()).context("unknown round input VTXO")?);
1104 }
1105 }
1106 Ok(ret)
1107 }
1108
1109 pub fn pending_lightning_send_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1111 let vtxos = self.db.get_all_pending_lightning_send()?.into_iter()
1112 .flat_map(|pending_lightning_send| pending_lightning_send.htlc_vtxos)
1113 .collect::<Vec<_>>();
1114
1115 Ok(vtxos)
1116 }
1117
1118 pub async fn get_expiring_vtxos(
1120 &self,
1121 threshold: BlockHeight,
1122 ) -> anyhow::Result<Vec<WalletVtxo>> {
1123 let expiry = self.chain.tip().await? + threshold;
1124 let filter = VtxoFilter::new(&self).expires_before(expiry);
1125 Ok(self.spendable_vtxos_with(&filter)?)
1126 }
1127
1128 pub async fn sync_pending_boards(&self) -> anyhow::Result<()> {
1132 let ark_info = self.require_server()?.info;
1133 let current_height = self.chain.tip().await?;
1134 let unregistered_boards = self.pending_board_vtxos()?;
1135 let mut registered_boards = 0;
1136
1137 if unregistered_boards.is_empty() {
1138 return Ok(());
1139 }
1140
1141 trace!("Attempting registration of sufficiently confirmed boards");
1142
1143 for board in unregistered_boards {
1144 let anchor = board.vtxo.chain_anchor();
1145 let confs = match self.chain.tx_status(anchor.txid).await {
1146 Ok(TxStatus::Confirmed(block_ref)) => Some(current_height - (block_ref.height - 1)),
1147 Ok(TxStatus::Mempool) => Some(0),
1148 Ok(TxStatus::NotFound) => None,
1149 Err(_) => None,
1150 };
1151
1152 if let Some(confs) = confs {
1153 if confs >= ark_info.required_board_confirmations as BlockHeight {
1154 if let Err(e) = self.register_board(board.vtxo.id()).await {
1155 warn!("Failed to register board {}: {}", board.vtxo.id(), e);
1156 } else {
1157 info!("Registered board {}", board.vtxo.id());
1158 registered_boards += 1;
1159 }
1160 }
1161 }
1162 };
1163
1164 if registered_boards > 0 {
1165 info!("Registered {registered_boards} sufficiently confirmed boards");
1166 }
1167 Ok(())
1168 }
1169
1170 pub async fn maintenance(&self) -> anyhow::Result<()> {
1175 info!("Starting wallet maintenance");
1176 self.sync().await;
1177 self.maintenance_refresh().await?;
1178 Ok(())
1179 }
1180
1181 pub async fn maintenance_with_onchain<W: PreparePsbt + SignPsbt + ExitUnilaterally>(
1187 &self,
1188 onchain: &mut W,
1189 ) -> anyhow::Result<()> {
1190 info!("Starting wallet maintenance with onchain wallet");
1191 self.sync().await;
1192 self.maintenance_refresh().await?;
1193
1194 self.sync_exits(onchain).await?;
1196
1197 Ok(())
1198 }
1199
1200 pub async fn maintenance_refresh(&self) -> anyhow::Result<Option<RoundStatus>> {
1206 let vtxos = self.get_vtxos_to_refresh().await?.into_iter()
1207 .map(|v| v.id())
1208 .collect::<Vec<_>>();
1209 if vtxos.len() == 0 {
1210 return Ok(None);
1211 }
1212
1213 info!("Performing maintenance refresh");
1214 self.refresh_vtxos(vtxos).await
1215 }
1216
1217 pub async fn sync(&self) {
1223 tokio::join!(
1224 async {
1225 if let Err(e) = self.chain.update_fee_rates(self.config.fallback_fee_rate).await {
1228 warn!("Error updating fee rates: {:#}", e);
1229 }
1230 },
1231 async {
1232 if let Err(e) = self.sync_oors().await {
1233 warn!("Error in arkoor sync: {:#}", e);
1234 }
1235 },
1236 async {
1237 if let Err(e) = self.sync_pending_rounds().await {
1238 warn!("Error while trying to progress rounds awaiting confirmations: {:#}", e);
1239 }
1240 },
1241 async {
1242 if let Err(e) = self.sync_pending_lightning_send_vtxos().await {
1243 warn!("Error syncing pending lightning payments: {:#}", e);
1244 }
1245 },
1246 async {
1247 if let Err(e) = self.claim_all_pending_htlc_receives().await {
1248 warn!("Error claiming pending lightning receives: {:#}", e);
1249 }
1250 },
1251 async {
1252 if let Err(e) = self.sync_pending_boards().await {
1253 warn!("Error syncing pending boards: {:#}", e);
1254 }
1255 }
1256 );
1257 }
1258
1259 pub async fn sync_exits(
1265 &self,
1266 onchain: &mut dyn ExitUnilaterally,
1267 ) -> anyhow::Result<()> {
1268 self.exit.write().await.sync_exit(onchain).await?;
1269 Ok(())
1270 }
1271
1272 pub fn pending_lightning_sends(&self) -> anyhow::Result<Vec<PendingLightningSend>> {
1273 Ok(self.db.get_all_pending_lightning_send()?)
1274 }
1275
1276 pub async fn sync_pending_lightning_send_vtxos(&self) -> anyhow::Result<()> {
1279 let pending_payments = self.pending_lightning_sends()?;
1280
1281 if pending_payments.is_empty() {
1282 return Ok(());
1283 }
1284
1285 info!("Syncing {} pending lightning sends", pending_payments.len());
1286
1287 for payment in pending_payments {
1288 self.check_lightning_payment(&payment).await?;
1289 }
1290
1291 Ok(())
1292 }
1293
1294 pub async fn dangerous_drop_vtxo(&self, vtxo_id: VtxoId) -> anyhow::Result<()> {
1297 warn!("Drop vtxo {} from the database", vtxo_id);
1298 self.db.remove_vtxo(vtxo_id)?;
1299 Ok(())
1300 }
1301
1302 pub async fn dangerous_drop_all_vtxos(&self) -> anyhow::Result<()> {
1305 warn!("Dropping all vtxos from the db...");
1306 for vtxo in self.vtxos()? {
1307 self.db.remove_vtxo(vtxo.id())?;
1308 }
1309
1310 self.exit.write().await.clear_exit()?;
1311 Ok(())
1312 }
1313
1314 pub async fn board_amount(
1318 &self,
1319 onchain: &mut dyn onchain::Board,
1320 amount: Amount,
1321 ) -> anyhow::Result<Board> {
1322 let (user_keypair, _) = self.derive_store_next_keypair()?;
1323 self.board(onchain, Some(amount), user_keypair).await
1324 }
1325
1326 pub async fn board_all(
1328 &self,
1329 onchain: &mut dyn onchain::Board,
1330 ) -> anyhow::Result<Board> {
1331 let (user_keypair, _) = self.derive_store_next_keypair()?;
1332 self.board(onchain, None, user_keypair).await
1333 }
1334
1335 async fn board(
1336 &self,
1337 wallet: &mut dyn onchain::Board,
1338 amount: Option<Amount>,
1339 user_keypair: Keypair,
1340 ) -> anyhow::Result<Board> {
1341 let mut srv = self.require_server()?;
1342 let properties = self.db.read_properties()?.context("Missing config")?;
1343 let current_height = self.chain.tip().await?;
1344
1345 let expiry_height = current_height + srv.info.vtxo_expiry_delta as BlockHeight;
1346 let builder = BoardBuilder::new(
1347 user_keypair.public_key(),
1348 expiry_height,
1349 srv.info.server_pubkey,
1350 srv.info.vtxo_exit_delta,
1351 );
1352
1353 let addr = bitcoin::Address::from_script(
1354 &builder.funding_script_pubkey(),
1355 properties.network,
1356 )?;
1357
1358 let fee_rate = self.chain.fee_rates().await.regular;
1360 let (board_psbt, amount) = if let Some(amount) = amount {
1361 let psbt = wallet.prepare_tx(&[(addr, amount)], fee_rate)?;
1362 (psbt, amount)
1363 } else {
1364 let psbt = wallet.prepare_drain_tx(addr, fee_rate)?;
1365 assert_eq!(psbt.unsigned_tx.output.len(), 1);
1366 let amount = psbt.unsigned_tx.output[0].value;
1367 (psbt, amount)
1368 };
1369
1370 ensure!(amount >= srv.info.min_board_amount,
1371 "board amount of {amount} is less than minimum board amount required by server ({})",
1372 srv.info.min_board_amount,
1373 );
1374
1375 let utxo = OutPoint::new(board_psbt.unsigned_tx.compute_txid(), BOARD_FUNDING_TX_VTXO_VOUT);
1376 let builder = builder
1377 .set_funding_details(amount, utxo)
1378 .generate_user_nonces();
1379
1380 let cosign_resp = srv.client.request_board_cosign(protos::BoardCosignRequest {
1381 amount: amount.to_sat(),
1382 utxo: bitcoin::consensus::serialize(&utxo), expiry_height,
1384 user_pubkey: user_keypair.public_key().serialize().to_vec(),
1385 pub_nonce: builder.user_pub_nonce().serialize().to_vec(),
1386 }).await.context("error requesting board cosign")?
1387 .into_inner().try_into().context("invalid cosign response from server")?;
1388
1389 ensure!(builder.verify_cosign_response(&cosign_resp),
1390 "invalid board cosignature received from server",
1391 );
1392
1393 let vtxo = builder.build_vtxo(&cosign_resp, &user_keypair)?;
1395
1396 let onchain_fee = board_psbt.fee()?;
1397 let movement_id = self.movements.new_movement(
1398 self.subsystem_ids[&BarkSubsystem::Board],
1399 BoardMovement::Board.to_string(),
1400 ).await?;
1401 self.movements.update_movement(
1402 movement_id,
1403 MovementUpdate::new()
1404 .produced_vtxo(&vtxo)
1405 .intended_and_effective_balance(vtxo.amount().to_signed()?)
1406 .metadata(BoardMovement::metadata(utxo, onchain_fee)?),
1407 ).await?;
1408 self.store_locked_vtxos([&vtxo], Some(movement_id))?;
1409
1410 let tx = wallet.finish_tx(board_psbt)?;
1411 self.db.store_pending_board(vtxo.id(), &tx, movement_id)?;
1412
1413 trace!("Broadcasting board tx: {}", bitcoin::consensus::encode::serialize_hex(&tx));
1414 self.chain.broadcast_tx(&tx).await?;
1415
1416 info!("Board broadcasted");
1417 Ok(Board {
1418 funding_txid: tx.compute_txid(),
1419 vtxos: vec![vtxo.into()],
1420 })
1421 }
1422
1423 async fn register_board(&self, vtxo: impl VtxoRef) -> anyhow::Result<Board> {
1425 trace!("Attempting to register board {} to server", vtxo.vtxo_id());
1426 let mut srv = self.require_server()?;
1427
1428 let vtxo = match vtxo.vtxo() {
1430 Some(v) => v,
1431 None => {
1432 &self.db.get_wallet_vtxo(vtxo.vtxo_id())?
1433 .with_context(|| format!("VTXO doesn't exist: {}", vtxo.vtxo_id()))?
1434 },
1435 };
1436
1437 srv.client.register_board_vtxo(protos::BoardVtxoRequest {
1439 board_vtxo: vtxo.serialize(),
1440 }).await.context("error registering board with the Ark server")?;
1441
1442 self.db.update_vtxo_state_checked(vtxo.id(), VtxoState::Spendable, &UNSPENT_STATES)?;
1445
1446 let movement_id = self.db.get_pending_board_movement_id(vtxo.id())?;
1447 self.db.remove_pending_board(&vtxo.id())?;
1448
1449 self.movements.finish_movement(movement_id, MovementStatus::Finished).await?;
1450
1451 let funding_txid = vtxo.chain_anchor().txid;
1452
1453 Ok(Board {
1454 funding_txid,
1455 vtxos: vec![vtxo.clone()],
1456 })
1457 }
1458
1459 fn has_counterparty_risk(&self, vtxo: &Vtxo) -> anyhow::Result<bool> {
1464 for past_pk in vtxo.past_arkoor_pubkeys() {
1465 if !self.db.get_public_key_idx(&past_pk)?.is_some() {
1466 return Ok(true);
1467 }
1468 }
1469 Ok(!self.db.get_public_key_idx(&vtxo.user_pubkey())?.is_some())
1470 }
1471
1472 pub async fn sync_oors(&self) -> anyhow::Result<()> {
1473 let last_pk_index = self.db.get_last_vtxo_key_index()?.unwrap_or_default();
1474 let pubkeys = (0..=last_pk_index).map(|idx| {
1475 self.vtxo_seed.derive_keypair(idx).public_key()
1476 }).collect::<Vec<_>>();
1477
1478 self.sync_arkoor_for_pubkeys(&pubkeys).await?;
1479
1480 Ok(())
1481 }
1482
1483 async fn sync_arkoor_for_pubkeys(
1485 &self,
1486 public_keys: &[PublicKey],
1487 ) -> anyhow::Result<()> {
1488 let mut srv = self.require_server()?;
1489
1490 for pubkeys in public_keys.chunks(rpc::MAX_NB_MAILBOX_PUBKEYS) {
1491 debug!("Emptying OOR mailbox at Ark server...");
1493 let req = protos::ArkoorVtxosRequest {
1494 pubkeys: pubkeys.iter().map(|pk| pk.serialize().to_vec()).collect(),
1495 };
1496 let packages = srv.client.empty_arkoor_mailbox(req).await
1497 .context("error fetching oors")?.into_inner().packages;
1498 debug!("Ark server has {} arkoor packages for us", packages.len());
1499
1500 for package in packages {
1501 let mut vtxos = Vec::with_capacity(package.vtxos.len());
1502 for vtxo in package.vtxos {
1503 let vtxo = match Vtxo::deserialize(&vtxo) {
1504 Ok(vtxo) => vtxo,
1505 Err(e) => {
1506 warn!("Invalid vtxo from Ark server: {}", e);
1507 continue;
1508 }
1509 };
1510
1511 if let Err(e) = self.validate_vtxo(&vtxo).await {
1512 error!("Received invalid arkoor VTXO from server: {}", e);
1513 continue;
1514 }
1515
1516 match self.db.has_spent_vtxo(vtxo.id()) {
1517 Ok(spent) if spent => {
1518 debug!("Not adding OOR vtxo {} because it is considered spent", vtxo.id());
1519 continue;
1520 },
1521 _ => {}
1522 }
1523
1524 if let Ok(Some(_)) = self.db.get_wallet_vtxo(vtxo.id()) {
1525 debug!("Not adding OOR vtxo {} because it already exists", vtxo.id());
1526 continue;
1527 }
1528
1529 vtxos.push(vtxo);
1530 }
1531
1532 self.store_spendable_vtxos(&vtxos)?;
1533 self.movements.new_finished_movement(
1534 self.subsystem_ids[&BarkSubsystem::Arkoor],
1535 ArkoorMovement::Receive.to_string(),
1536 MovementStatus::Finished,
1537 MovementUpdate::new()
1538 .produced_vtxos(&vtxos)
1539 .intended_and_effective_balance(
1540 vtxos
1541 .iter()
1542 .map(|vtxo| vtxo.amount()).sum::<Amount>()
1543 .to_signed()?,
1544 ),
1545 ).await?;
1546 }
1547 }
1548
1549 Ok(())
1550 }
1551
1552 async fn add_should_refresh_vtxos(
1556 &self,
1557 participation: &mut RoundParticipation,
1558 ) -> anyhow::Result<()> {
1559 let excluded_ids = participation.inputs.iter().map(|v| v.vtxo_id())
1560 .collect::<HashSet<_>>();
1561
1562 let should_refresh_vtxos = self.get_vtxos_to_refresh().await?.into_iter()
1563 .filter(|v| !excluded_ids.contains(&v.id()))
1564 .map(|v| v.vtxo).collect::<Vec<_>>();
1565
1566
1567 let total_amount = should_refresh_vtxos.iter().map(|v| v.amount()).sum::<Amount>();
1568
1569 if total_amount > P2TR_DUST {
1570 let (user_keypair, _) = self.derive_store_next_keypair()?;
1571 let req = VtxoRequest {
1572 policy: VtxoPolicy::new_pubkey(user_keypair.public_key()),
1573 amount: total_amount,
1574 };
1575
1576 participation.inputs.extend(should_refresh_vtxos);
1577 participation.outputs.push(req);
1578 }
1579
1580 Ok(())
1581 }
1582
1583 pub fn build_offboard_participation<V: VtxoRef>(
1584 &self,
1585 vtxos: impl IntoIterator<Item = V>,
1586 destination: ScriptBuf,
1587 ) -> anyhow::Result<RoundParticipation> {
1588 let srv = self.require_server()?;
1589
1590 let vtxos = {
1591 let vtxos = vtxos.into_iter();
1592 let mut ret = Vec::with_capacity(vtxos.size_hint().0);
1593 for v in vtxos {
1594 let vtxo = match v.vtxo() {
1595 Some(v) => v.clone(),
1596 None => self.get_vtxo_by_id(v.vtxo_id()).context("vtxo not found")?.vtxo,
1597 };
1598 ret.push(vtxo);
1599 }
1600 ret
1601 };
1602
1603 if vtxos.is_empty() {
1604 bail!("no VTXO to offboard");
1605 }
1606
1607 let fee = OffboardRequest::calculate_fee(&destination, srv.info.offboard_feerate)
1608 .expect("bdk created invalid scriptPubkey");
1609
1610 let vtxo_sum = vtxos.iter().map(|v| v.amount()).sum::<Amount>();
1611
1612 if fee > vtxo_sum {
1613 bail!("offboarded amount is lower than fees. Need {fee}, got: {vtxo_sum}");
1614 }
1615
1616 let offb = OffboardRequest {
1617 amount: vtxo_sum - fee,
1618 script_pubkey: destination.clone(),
1619 };
1620
1621 Ok(RoundParticipation {
1622 inputs: vtxos.clone(),
1623 outputs: Vec::new(),
1624 offboards: vec![offb],
1625 })
1626 }
1627
1628 async fn offboard<V: VtxoRef>(
1629 &self,
1630 vtxos: impl IntoIterator<Item = V>,
1631 destination: ScriptBuf,
1632 ) -> anyhow::Result<RoundStatus> {
1633 let mut participation = self.build_offboard_participation(vtxos, destination.clone())?;
1634
1635 if let Err(e) = self.add_should_refresh_vtxos(&mut participation).await {
1636 warn!("Error trying to add additional VTXOs that should be refreshed: {:#}", e);
1637 }
1638
1639 Ok(self.participate_round(participation, Some(RoundMovement::Offboard)).await?)
1640 }
1641
1642 pub async fn offboard_all(&self, address: bitcoin::Address) -> anyhow::Result<RoundStatus> {
1644 let input_vtxos = self.spendable_vtxos()?;
1645 Ok(self.offboard(input_vtxos, address.script_pubkey()).await?)
1646 }
1647
1648 pub async fn offboard_vtxos<V: VtxoRef>(
1650 &self,
1651 vtxos: impl IntoIterator<Item = V>,
1652 address: bitcoin::Address,
1653 ) -> anyhow::Result<RoundStatus> {
1654 let input_vtxos = vtxos
1655 .into_iter()
1656 .map(|v| {
1657 let id = v.vtxo_id();
1658 match self.db.get_wallet_vtxo(id)? {
1659 Some(vtxo) => Ok(vtxo.vtxo),
1660 _ => bail!("cannot find requested vtxo: {}", id),
1661 }
1662 })
1663 .collect::<anyhow::Result<Vec<_>>>()?;
1664
1665 Ok(self.offboard(input_vtxos, address.script_pubkey()).await?)
1666 }
1667
1668 pub fn build_refresh_participation<V: VtxoRef>(
1669 &self,
1670 vtxos: impl IntoIterator<Item = V>,
1671 ) -> anyhow::Result<Option<RoundParticipation>> {
1672 let vtxos = {
1673 let mut ret = HashMap::new();
1674 for v in vtxos {
1675 let id = v.vtxo_id();
1676 let vtxo = self.get_vtxo_by_id(id)
1677 .with_context(|| format!("vtxo with id {} not found", id))?;
1678 if !ret.insert(id, vtxo).is_none() {
1679 bail!("duplicate VTXO id: {}", id);
1680 }
1681 }
1682 ret
1683 };
1684
1685 if vtxos.is_empty() {
1686 info!("Skipping refresh since no VTXOs are provided.");
1687 return Ok(None);
1688 }
1689
1690 let total_amount = vtxos.values().map(|v| v.vtxo.amount()).sum();
1691
1692 info!("Refreshing {} VTXOs (total amount = {}).", vtxos.len(), total_amount);
1693
1694 let (user_keypair, _) = self.derive_store_next_keypair()?;
1695 let req = VtxoRequest {
1696 policy: VtxoPolicy::Pubkey(PubkeyVtxoPolicy { user_pubkey: user_keypair.public_key() }),
1697 amount: total_amount,
1698 };
1699
1700 Ok(Some(RoundParticipation {
1701 inputs: vtxos.into_values().map(|v| v.vtxo).collect(),
1702 outputs: vec![req],
1703 offboards: Vec::new(),
1704 }))
1705 }
1706
1707 pub async fn refresh_vtxos<V: VtxoRef>(
1713 &self,
1714 vtxos: impl IntoIterator<Item = V>,
1715 ) -> anyhow::Result<Option<RoundStatus>> {
1716 let mut participation = match self.build_refresh_participation(vtxos)? {
1717 Some(participation) => participation,
1718 None => return Ok(None),
1719 };
1720
1721 if let Err(e) = self.add_should_refresh_vtxos(&mut participation).await {
1722 warn!("Error trying to add additional VTXOs that should be refreshed: {:#}", e);
1723 }
1724
1725 Ok(Some(self.participate_round(participation, Some(RoundMovement::Refresh)).await?))
1726 }
1727
1728 pub async fn get_vtxos_to_refresh(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1732 let tip = self.chain.tip().await?;
1733 let fee_rate = self.chain.fee_rates().await.fast;
1734
1735 let must_refresh_vtxos = self.spendable_vtxos_with(
1737 &RefreshStrategy::must_refresh(self, tip, fee_rate),
1738 )?;
1739 if must_refresh_vtxos.is_empty() {
1740 return Ok(vec![]);
1741 } else {
1742 let should_refresh_vtxos = self.spendable_vtxos_with(
1745 &RefreshStrategy::should_refresh(self, tip, fee_rate),
1746 )?;
1747 Ok(should_refresh_vtxos)
1748 }
1749 }
1750
1751 pub fn get_first_expiring_vtxo_blockheight(
1753 &self,
1754 ) -> anyhow::Result<Option<BlockHeight>> {
1755 Ok(self.spendable_vtxos()?.iter().map(|v| v.expiry_height()).min())
1756 }
1757
1758 pub fn get_next_required_refresh_blockheight(
1761 &self,
1762 ) -> anyhow::Result<Option<BlockHeight>> {
1763 let first_expiry = self.get_first_expiring_vtxo_blockheight()?;
1764 Ok(first_expiry.map(|h| h.saturating_sub(self.config.vtxo_refresh_expiry_threshold)))
1765 }
1766
1767 fn select_vtxos_to_cover(
1773 &self,
1774 amount: Amount,
1775 max_depth: Option<u16>,
1776 expiry_threshold: Option<BlockHeight>,
1777 ) -> anyhow::Result<Vec<Vtxo>> {
1778 let inputs = self.spendable_vtxos()?;
1779
1780 let mut result = Vec::new();
1782 let mut total_amount = bitcoin::Amount::ZERO;
1783 for input in inputs {
1784 if let Some(max_depth) = max_depth {
1785 if input.arkoor_depth() >= max_depth {
1786 warn!("VTXO {} reached max depth of {}, skipping it. \
1787 Please refresh your VTXO.", input.id(), max_depth,
1788 );
1789 continue;
1790 }
1791 }
1792
1793 if let Some(threshold) = expiry_threshold {
1795 if input.expiry_height() < threshold {
1796 warn!("VTXO {} is expiring soon (expires at {}, threshold {}), \
1797 skipping for arkoor payment",
1798 input.id(), input.expiry_height(), threshold,
1799 );
1800 continue;
1801 }
1802 }
1803
1804 total_amount += input.amount();
1805 result.push(input.vtxo);
1806
1807 if total_amount >= amount {
1808 return Ok(result)
1809 }
1810 }
1811
1812 bail!("Insufficient money available. Needed {} but {} is available",
1813 amount, total_amount,
1814 );
1815 }
1816
1817 async fn create_arkoor_vtxos(
1823 &self,
1824 destination_policy: VtxoPolicy,
1825 amount: Amount,
1826 ) -> anyhow::Result<ArkoorCreateResult> {
1827 let mut srv = self.require_server()?;
1828 let change_pubkey = self.derive_store_next_keypair()?.0.public_key();
1829
1830 let req = VtxoRequest {
1831 amount,
1832 policy: destination_policy,
1833 };
1834
1835 let tip = self.chain.tip().await?;
1837 let inputs = self.select_vtxos_to_cover(
1838 req.amount,
1839 Some(srv.info.max_arkoor_depth),
1840 Some(tip + self.config.vtxo_refresh_expiry_threshold),
1841 )?;
1842
1843 let mut secs = Vec::with_capacity(inputs.len());
1844 let mut pubs = Vec::with_capacity(inputs.len());
1845 let mut keypairs = Vec::with_capacity(inputs.len());
1846 for input in inputs.iter() {
1847 let keypair = self.get_vtxo_key(&input)?;
1848 let (s, p) = musig::nonce_pair(&keypair);
1849 secs.push(s);
1850 pubs.push(p);
1851 keypairs.push(keypair);
1852 }
1853
1854 let builder = ArkoorPackageBuilder::new(&inputs, &pubs, req, Some(change_pubkey))?;
1855
1856 let req = protos::ArkoorPackageCosignRequest {
1857 arkoors: builder.arkoors.iter().map(|a| a.into()).collect(),
1858 };
1859 let cosign_resp: Vec<_> = srv.client.request_arkoor_package_cosign(req).await?
1860 .into_inner().try_into().context("invalid server cosign response")?;
1861 ensure!(builder.verify_cosign_response(&cosign_resp),
1862 "invalid arkoor cosignature received from server",
1863 );
1864
1865 let (sent, change) = builder.build_vtxos(&cosign_resp, &keypairs, secs)?;
1866
1867 if let Some(change) = change.as_ref() {
1868 info!("Added change VTXO of {}", change.amount());
1869 }
1870
1871 Ok(ArkoorCreateResult {
1872 input: inputs,
1873 created: sent,
1874 change,
1875 })
1876 }
1877
1878 pub fn validate_arkoor_address(&self, address: &ark::Address) -> anyhow::Result<()> {
1882 let asp = self.require_server()?;
1883
1884 if !address.ark_id().is_for_server(asp.info.server_pubkey) {
1885 bail!("Ark address is for different server");
1886 }
1887
1888 match address.policy().policy_type() {
1890 VtxoPolicyKind::Pubkey => {},
1891 VtxoPolicyKind::ServerHtlcRecv | VtxoPolicyKind::ServerHtlcSend => {
1892 bail!("VTXO policy in address cannot be used for arkoor payment: {}",
1893 address.policy().policy_type(),
1894 );
1895 }
1896 }
1897
1898 if address.delivery().is_empty() {
1899 bail!("No VTXO delivery mechanism provided in address");
1900 }
1901 if !address.delivery().iter().any(|d| !d.is_unknown()) {
1905 for d in address.delivery() {
1906 if let VtxoDelivery::Unknown { delivery_type, data } = d {
1907 info!("Unknown delivery in address: type={:#x}, data={}",
1908 delivery_type, data.as_hex(),
1909 );
1910 }
1911 }
1912 }
1913
1914 Ok(())
1915 }
1916
1917 pub async fn send_arkoor_payment(
1927 &self,
1928 destination: &ark::Address,
1929 amount: Amount,
1930 ) -> anyhow::Result<Vec<Vtxo>> {
1931 let mut srv = self.require_server()?;
1932
1933 self.validate_arkoor_address(&destination).context("cannot send to address")?;
1934
1935 if amount < P2TR_DUST {
1936 bail!("Sent amount must be at least {}", P2TR_DUST);
1937 }
1938
1939 let mut movement = MovementGuard::new_movement(
1940 self.movements.clone(),
1941 self.subsystem_ids[&BarkSubsystem::Arkoor],
1942 ArkoorMovement::Send.to_string(),
1943 ).await?;
1944 let arkoor = self.create_arkoor_vtxos(destination.policy().clone(), amount).await?;
1945 movement.apply_update(
1946 arkoor.to_movement_update()?
1947 .sent_to([MovementDestination::new(destination.to_string(), amount)])
1948 .intended_and_effective_balance(-amount.to_signed()?)
1949 ).await?;
1950
1951 let req = protos::ArkoorPackage {
1952 arkoors: arkoor.created.iter().map(|v| protos::ArkoorVtxo {
1953 pubkey: destination.policy().user_pubkey().serialize().to_vec(),
1954 vtxo: v.serialize().to_vec(),
1955 }).collect(),
1956 };
1957
1958 if let Err(e) = srv.client.post_arkoor_package_mailbox(req).await {
1961 error!("Failed to post the arkoor vtxo to the recipients mailbox: '{:#}'", e);
1962 }
1964 self.mark_vtxos_as_spent(&arkoor.input)?;
1965 if let Some(change) = arkoor.change {
1966 self.store_spendable_vtxos(&[change])?;
1967 }
1968 movement.finish(MovementStatus::Finished).await?;
1969 Ok(arkoor.created)
1970 }
1971
1972 async fn process_lightning_revocation(&self, payment: &PendingLightningSend) -> anyhow::Result<()> {
1973 let mut srv = self.require_server()?;
1974 let htlc_vtxos = payment.htlc_vtxos.clone().into_iter()
1975 .map(|v: WalletVtxo| v.vtxo).collect::<Vec<_>>();
1976
1977 info!("Processing {} HTLC VTXOs for revocation", htlc_vtxos.len());
1978
1979 let mut secs = Vec::with_capacity(htlc_vtxos.len());
1980 let mut pubs = Vec::with_capacity(htlc_vtxos.len());
1981 let mut keypairs = Vec::with_capacity(htlc_vtxos.len());
1982 for input in htlc_vtxos.iter() {
1983 let keypair = self.get_vtxo_key(&input)?;
1984 let (s, p) = musig::nonce_pair(&keypair);
1985 secs.push(s);
1986 pubs.push(p);
1987 keypairs.push(keypair);
1988 }
1989
1990 let revocation = ArkoorPackageBuilder::new_htlc_revocation(&htlc_vtxos, &pubs)?;
1991
1992 let req = protos::RevokeLightningPaymentRequest {
1993 htlc_vtxo_ids: revocation.arkoors.iter()
1994 .map(|i| i.input.id().to_bytes().to_vec())
1995 .collect(),
1996 user_nonces: revocation.arkoors.iter()
1997 .map(|i| i.user_nonce.serialize().to_vec())
1998 .collect(),
1999 };
2000 let cosign_resp: Vec<_> = srv.client.revoke_lightning_payment(req).await?
2001 .into_inner().try_into().context("invalid server cosign response")?;
2002 ensure!(revocation.verify_cosign_response(&cosign_resp),
2003 "invalid arkoor cosignature received from server",
2004 );
2005
2006 let (vtxos, _) = revocation.build_vtxos(&cosign_resp, &keypairs, secs)?;
2007 let mut revoked = Amount::ZERO;
2008 for vtxo in &vtxos {
2009 info!("Got revocation VTXO: {}: {}", vtxo.id(), vtxo.amount());
2010 revoked += vtxo.amount();
2011 }
2012
2013 let count = vtxos.len();
2014 self.movements.update_movement(
2015 payment.movement_id,
2016 MovementUpdate::new()
2017 .effective_balance(-payment.amount.to_signed()? + revoked.to_signed()?)
2018 .produced_vtxos(&vtxos)
2019 ).await?;
2020 self.store_spendable_vtxos(&vtxos)?;
2021 self.mark_vtxos_as_spent(&htlc_vtxos)?;
2022 self.movements.finish_movement(payment.movement_id, MovementStatus::Failed).await?;
2023
2024 self.db.remove_pending_lightning_send(payment.invoice.payment_hash())?;
2025
2026 info!("Revoked {} HTLC VTXOs", count);
2027
2028 Ok(())
2029 }
2030
2031 pub async fn pay_lightning_invoice<T>(
2034 &self,
2035 invoice: T,
2036 user_amount: Option<Amount>,
2037 ) -> anyhow::Result<Preimage>
2038 where
2039 T: TryInto<Invoice>,
2040 T::Error: std::error::Error + fmt::Display + Send + Sync + 'static,
2041 {
2042 let mut srv = self.require_server()?;
2043 let properties = self.db.read_properties()?.context("Missing config")?;
2044
2045 let invoice = invoice.try_into().context("failed to parse invoice")?;
2046 if invoice.network() != properties.network {
2047 bail!("Invoice is for wrong network: {}", invoice.network());
2048 }
2049
2050 if self.db.check_recipient_exists(&invoice.to_string())? {
2051 bail!("Invoice has already been paid");
2052 }
2053
2054 invoice.check_signature()?;
2055
2056 let inv_amount = invoice.amount_msat().map(|v| Amount::from_msat_ceil(v));
2057 if let (Some(_), Some(inv)) = (user_amount, inv_amount) {
2058 bail!("Invoice has amount of {} encoded. Please omit user amount argument", inv);
2059 }
2060
2061 let amount = user_amount.or(inv_amount)
2062 .context("amount required on invoice without amount")?;
2063 if amount < P2TR_DUST {
2064 bail!("Sent amount must be at least {}", P2TR_DUST);
2065 }
2066
2067 let (change_keypair, _) = self.derive_store_next_keypair()?;
2068
2069 let inputs = self.select_vtxos_to_cover(amount, Some(srv.info.max_arkoor_depth), None)
2070 .context("Could not find enough suitable VTXOs to cover lightning payment")?;
2071
2072 let mut secs = Vec::with_capacity(inputs.len());
2073 let mut pubs = Vec::with_capacity(inputs.len());
2074 let mut keypairs = Vec::with_capacity(inputs.len());
2075 let mut input_ids = Vec::with_capacity(inputs.len());
2076 for input in inputs.iter() {
2077 let keypair = self.get_vtxo_key(&input)?;
2078 let (s, p) = musig::nonce_pair(&keypair);
2079 secs.push(s);
2080 pubs.push(p);
2081 keypairs.push(keypair);
2082 input_ids.push(input.id());
2083 }
2084
2085 let req = protos::StartLightningPaymentRequest {
2086 invoice: invoice.to_string(),
2087 user_amount_sat: user_amount.map(|a| a.to_sat()),
2088 input_vtxo_ids: input_ids.iter().map(|v| v.to_bytes().to_vec()).collect(),
2089 user_nonces: pubs.iter().map(|p| p.serialize().to_vec()).collect(),
2090 user_pubkey: change_keypair.public_key().serialize().to_vec(),
2091 };
2092
2093 let resp = srv.client.start_lightning_payment(req).await
2094 .context("htlc request failed")?.into_inner();
2095
2096 let cosign_resp = resp.sigs.into_iter().map(|i| i.try_into())
2097 .collect::<Result<Vec<_>, _>>()?;
2098 let policy = VtxoPolicy::deserialize(&resp.policy)?;
2099
2100 let pay_req = match policy {
2101 VtxoPolicy::ServerHtlcSend(policy) => {
2102 ensure!(policy.user_pubkey == change_keypair.public_key(), "user pubkey mismatch");
2103 ensure!(policy.payment_hash == invoice.payment_hash(), "payment hash mismatch");
2104 VtxoRequest { amount: amount, policy: policy.into() }
2106 },
2107 _ => bail!("invalid policy returned from server"),
2108 };
2109
2110 let builder = ArkoorPackageBuilder::new(
2111 &inputs, &pubs, pay_req, Some(change_keypair.public_key()),
2112 )?;
2113
2114 ensure!(builder.verify_cosign_response(&cosign_resp),
2115 "invalid arkoor cosignature received from server",
2116 );
2117
2118 let (htlc_vtxos, change_vtxo) = builder.build_vtxos(&cosign_resp, &keypairs, secs)?;
2119
2120 let mut effective_balance = Amount::ZERO;
2122 for vtxo in &htlc_vtxos {
2123 self.validate_vtxo(vtxo).await?;
2124 effective_balance += vtxo.amount();
2125 }
2126
2127 let movement_id = self.movements.new_movement(
2128 self.subsystem_ids[&BarkSubsystem::LightningSend],
2129 LightningSendMovement::Send.to_string(),
2130 ).await?;
2131 self.movements.update_movement(
2132 movement_id,
2133 MovementUpdate::new()
2134 .intended_balance(-amount.to_signed()?)
2135 .effective_balance(-effective_balance.to_signed()?)
2136 .consumed_vtxos(&inputs)
2137 .sent_to([MovementDestination::new(invoice.to_string(), amount)])
2138 ).await?;
2139 self.store_locked_vtxos(&htlc_vtxos, Some(movement_id))?;
2140 self.mark_vtxos_as_spent(&input_ids)?;
2141
2142 if let Some(ref change) = change_vtxo {
2144 let last_input = inputs.last().context("no inputs provided")?;
2145 let tx = self.chain.get_tx(&last_input.chain_anchor().txid).await?;
2146 let tx = tx.with_context(|| {
2147 format!("input vtxo chain anchor not found for lightning change vtxo: {}", last_input.chain_anchor().txid)
2148 })?;
2149 change.validate(&tx).context("invalid lightning change vtxo")?;
2150 self.store_spendable_vtxos([change])?;
2151 }
2152
2153 self.movements.update_movement(
2154 movement_id,
2155 MovementUpdate::new()
2156 .produced_vtxo_if_some(change_vtxo)
2157 .metadata(LightningMovement::htlc_metadata(&htlc_vtxos)?)
2158 ).await?;
2159
2160 let payment = self.db.store_new_pending_lightning_send(
2161 &invoice, &amount, &htlc_vtxos.iter().map(|v| v.id()).collect::<Vec<_>>(), movement_id,
2162 )?;
2163
2164 let req = protos::SignedLightningPaymentDetails {
2165 invoice: invoice.to_string(),
2166 htlc_vtxo_ids: htlc_vtxos.iter().map(|v| v.id().to_bytes().to_vec()).collect(),
2167 wait: true,
2168 };
2169
2170 let res = srv.client.finish_lightning_payment(req).await?.into_inner();
2171 debug!("Progress update: {}", res.progress_message);
2172 let payment_preimage = Preimage::try_from(res.payment_preimage()).ok();
2173
2174 if let Some(preimage) = payment_preimage {
2175 info!("Payment succeeded! Preimage: {}", preimage.as_hex());
2176
2177 self.db.remove_pending_lightning_send(payment.invoice.payment_hash())?;
2178 self.mark_vtxos_as_spent(&htlc_vtxos)?;
2179 self.movements.finish_movement(movement_id, MovementStatus::Finished).await?;
2180 Ok(preimage)
2181 } else {
2182 self.process_lightning_revocation(&payment).await?;
2183 bail!("No preimage, payment failed: {}", res.progress_message);
2184 }
2185 }
2186
2187 pub async fn check_lightning_payment(&self, payment: &PendingLightningSend)
2214 -> anyhow::Result<Option<Preimage>>
2215 {
2216 let mut srv = self.require_server()?;
2217 let tip = self.chain.tip().await?;
2218
2219 let payment_hash = payment.invoice.payment_hash();
2220
2221 let policy = payment.htlc_vtxos.first().context("no vtxo provided")?.vtxo.policy();
2222 debug_assert!(payment.htlc_vtxos.iter().all(|v| v.vtxo.policy() == policy),
2223 "All lightning htlc should have the same policy",
2224 );
2225 let policy = policy.as_server_htlc_send().context("VTXO is not an HTLC send")?;
2226 if policy.payment_hash != payment_hash {
2227 bail!("Payment hash mismatch");
2228 }
2229
2230 let req = protos::CheckLightningPaymentRequest {
2231 hash: policy.payment_hash.to_vec(),
2232 wait: false,
2233 };
2234 let res = srv.client.check_lightning_payment(req).await?.into_inner();
2235
2236 let payment_status = protos::PaymentStatus::try_from(res.status)?;
2237
2238 let should_revoke = match payment_status {
2239 protos::PaymentStatus::Failed => {
2240 info!("Payment failed ({}): revoking VTXO", res.progress_message);
2241 true
2242 },
2243 protos::PaymentStatus::Pending => {
2244 trace!("Payment is still pending, HTLC expiry: {}, tip: {}",
2245 policy.htlc_expiry, tip);
2246 if tip > policy.htlc_expiry {
2247 info!("Payment is still pending, but HTLC is expired: revoking VTXO");
2248 true
2249 } else {
2250 info!("Payment is still pending and HTLC is not expired ({}): \
2251 doing nothing for now", policy.htlc_expiry,
2252 );
2253 false
2254 }
2255 },
2256 protos::PaymentStatus::Complete => {
2257 let preimage: Preimage = res.payment_preimage
2258 .context("payment completed but no preimage")?
2259 .try_into().map_err(|_| anyhow!("preimage is not 32 bytes"))?;
2260 info!("Payment is complete, preimage, {}", preimage.as_hex());
2261
2262 self.movements.finish_movement(payment.movement_id, MovementStatus::Finished).await?;
2263
2264 self.db.remove_pending_lightning_send(payment_hash)?;
2265
2266 return Ok(Some(preimage));
2267 },
2268 };
2269
2270 if should_revoke {
2271 if let Err(e) = self.process_lightning_revocation(payment).await {
2272 warn!("Failed to revoke VTXO: {}", e);
2273
2274 let min_expiry = payment.htlc_vtxos.iter()
2278 .map(|v| v.vtxo.spec().expiry_height).min().unwrap();
2279
2280 if tip > min_expiry.saturating_sub(self.config().vtxo_refresh_expiry_threshold) {
2281 warn!("Some VTXO is about to expire soon, marking to exit");
2282 let vtxos = payment.htlc_vtxos
2283 .iter()
2284 .map(|v| v.vtxo.clone())
2285 .collect::<Vec<_>>();
2286 self.exit.write().await.mark_vtxos_for_exit(&vtxos).await?;
2287
2288 let exited = vtxos.iter().map(|v| v.amount()).sum::<Amount>();
2289 self.movements.update_movement(
2290 payment.movement_id,
2291 MovementUpdate::new()
2292 .effective_balance(-payment.amount.to_signed()? + exited.to_signed()?)
2293 .exited_vtxos(&vtxos)
2294 ).await?;
2295 self.movements.finish_movement(
2296 payment.movement_id, MovementStatus::Failed,
2297 ).await?;
2298 self.db.remove_pending_lightning_send(payment_hash)?;
2299 }
2300 }
2301 }
2302
2303 Ok(None)
2304 }
2305
2306 pub async fn bolt11_invoice(&self, amount: Amount) -> anyhow::Result<Bolt11Invoice> {
2308 let mut srv = self.require_server()?;
2309 let config = self.config();
2310
2311 let requested_min_cltv_delta = srv.info.vtxo_exit_delta +
2316 srv.info.htlc_expiry_delta +
2317 config.vtxo_exit_margin +
2318 config.htlc_recv_claim_delta +
2319 LIGHTNING_PREPARE_CLAIM_DELTA;
2320
2321 if requested_min_cltv_delta > srv.info.max_user_invoice_cltv_delta {
2322 bail!("HTLC CLTV delta ({}) is greater than Server's max HTLC recv CLTV delta: {}",
2323 requested_min_cltv_delta,
2324 srv.info.max_user_invoice_cltv_delta,
2325 );
2326 }
2327
2328 let preimage = Preimage::random();
2329 let payment_hash = preimage.compute_payment_hash();
2330 info!("Start bolt11 board with preimage / payment hash: {} / {}",
2331 preimage.as_hex(), payment_hash.as_hex());
2332
2333 let req = protos::StartLightningReceiveRequest {
2334 payment_hash: payment_hash.to_vec(),
2335 amount_sat: amount.to_sat(),
2336 min_cltv_delta: requested_min_cltv_delta as u32,
2337 };
2338
2339 let resp = srv.client.start_lightning_receive(req).await?.into_inner();
2340 info!("Ark Server is ready to receive LN payment to invoice: {}.", resp.bolt11);
2341
2342 let invoice = Bolt11Invoice::from_str(&resp.bolt11)
2343 .context("invalid bolt11 invoice returned by Ark server")?;
2344
2345 self.db.store_lightning_receive(
2346 payment_hash,
2347 preimage,
2348 &invoice,
2349 requested_min_cltv_delta,
2350 )?;
2351
2352 Ok(invoice)
2353 }
2354
2355 pub fn lightning_receive_status(
2357 &self,
2358 payment: impl Into<PaymentHash>,
2359 ) -> anyhow::Result<Option<LightningReceive>> {
2360 Ok(self.db.fetch_lightning_receive_by_payment_hash(payment.into())?)
2361 }
2362
2363 pub fn pending_lightning_receives(&self) -> anyhow::Result<Vec<LightningReceive>> {
2365 Ok(self.db.get_all_pending_lightning_receives()?)
2366 }
2367
2368 pub fn pending_lightning_receive_balance(&self) -> anyhow::Result<LightningReceiveBalance> {
2369 let pending_lightning_receives = self.pending_lightning_receives()?;
2370
2371 let mut total_pending_lightning_receive = Amount::ZERO;
2372 let mut claimable_pending_lightning_receive = Amount::ZERO;
2373 for receive in pending_lightning_receives {
2374 total_pending_lightning_receive += receive.invoice.amount_milli_satoshis()
2375 .map(|a| Amount::from_msat_floor(a))
2376 .expect("ln receive invoice should have amount");
2377 if let Some(htlc_vtxos) = receive.htlc_vtxos {
2378 claimable_pending_lightning_receive += htlc_vtxos.iter().map(|v| v.amount()).sum::<Amount>();
2379 }
2380 }
2381
2382 Ok(LightningReceiveBalance {
2383 total: total_pending_lightning_receive,
2384 claimable: claimable_pending_lightning_receive,
2385 })
2386 }
2387
2388 async fn claim_lightning_receive(
2411 &self,
2412 lightning_receive: &LightningReceive,
2413 ) -> anyhow::Result<()> {
2414 let movement_id = lightning_receive.movement_id
2415 .context("No movement created for lightning receive")?;
2416 let mut srv = self.require_server()?;
2417
2418 let inputs = {
2420 let htlc_vtxos = lightning_receive.htlc_vtxos.as_ref()
2421 .context("no HTLC VTXOs set on record yet")?;
2422 let mut ret = htlc_vtxos.iter().map(|v| &v.vtxo).collect::<Vec<_>>();
2423 ret.sort_by_key(|v| v.id());
2424 ret
2425 };
2426
2427 let (keypairs, sec_nonces, pub_nonces) = inputs.iter().map(|v| {
2428 let keypair = self.get_vtxo_key(v)?;
2429 let (sec_nonce, pub_nonce) = musig::nonce_pair(&keypair);
2430 Ok((keypair, sec_nonce, pub_nonce))
2431 }).collect::<anyhow::Result<(Vec<_>, Vec<_>, Vec<_>)>>()?;
2432
2433 let (claim_keypair, _) = self.derive_store_next_keypair()?;
2435 let receive_policy = VtxoPolicy::new_pubkey(claim_keypair.public_key());
2436
2437 let pay_req = VtxoRequest {
2438 policy: receive_policy.clone(),
2439 amount: inputs.iter().map(|v| v.amount()).sum(),
2440 };
2441 trace!("ln arkoor builder params: inputs: {:?}; user_nonces: {:?}; req: {:?}",
2442 inputs.iter().map(|v| v.id()).collect::<Vec<_>>(), pub_nonces, pay_req,
2443 );
2444 let builder = ArkoorPackageBuilder::new(
2445 inputs.iter().copied(), &pub_nonces, pay_req, None,
2446 )?;
2447
2448 info!("Claiming arkoor against payment preimage");
2449 self.db.set_preimage_revealed(lightning_receive.payment_hash)?;
2450 let resp = srv.client.claim_lightning_receive(protos::ClaimLightningReceiveRequest {
2451 payment_hash: lightning_receive.payment_hash.to_byte_array().to_vec(),
2452 payment_preimage: lightning_receive.payment_preimage.to_vec(),
2453 vtxo_policy: receive_policy.serialize(),
2454 user_pub_nonces: pub_nonces.iter().map(|n| n.serialize().to_vec()).collect(),
2455 }).await?.into_inner();
2456 let cosign_resp: Vec<_> = resp.try_into().context("invalid cosign response")?;
2457
2458 ensure!(builder.verify_cosign_response(&cosign_resp),
2459 "invalid arkoor cosignature received from server",
2460 );
2461
2462 let (outputs, change) = builder.build_vtxos(&cosign_resp, &keypairs, sec_nonces)?;
2463 if change.is_some() {
2464 bail!("shouldn't have change VTXO, this is a bug");
2465 }
2466
2467 let mut effective_balance = Amount::ZERO;
2468 for vtxo in &outputs {
2469 if let Err(e) = self.validate_vtxo(vtxo).await {
2473 bail!("invalid arkoor from lightning receive: {e}");
2474 }
2475 effective_balance += vtxo.amount();
2476 }
2477
2478 self.store_spendable_vtxos(&outputs)?;
2479 info!("Got arkoors from lightning: {}",
2480 outputs.iter().map(|v| v.id().to_string()).collect::<Vec<_>>().join(", ")
2481 );
2482
2483 self.movements.update_movement(
2484 movement_id,
2485 MovementUpdate::new()
2486 .effective_balance(effective_balance.to_signed()?)
2487 .produced_vtxos(&outputs)
2488 ).await?;
2489 self.movements.finish_movement(
2490 movement_id, MovementStatus::Finished,
2491 ).await?;
2492
2493 self.db.remove_pending_lightning_receive(lightning_receive.payment_hash)?;
2494
2495 Ok(())
2496 }
2497
2498 async fn compute_lightning_receive_anti_dos(
2499 &self,
2500 payment_hash: PaymentHash,
2501 token: Option<&str>,
2502 ) -> anyhow::Result<LightningReceiveAntiDos> {
2503 Ok(if let Some(token) = token {
2504 LightningReceiveAntiDos::Token(token.to_string())
2505 } else {
2506 let challenge = LightningReceiveChallenge::new(payment_hash);
2507 let vtxo = self.select_vtxos_to_cover(Amount::ONE_SAT, None, None)
2509 .and_then(|vtxos| vtxos.into_iter().next().ok_or_else(|| anyhow!("have no spendable vtxo to prove ownership of")))?;
2510 let vtxo_keypair = self.get_vtxo_key(&vtxo).expect("owned vtxo should be in database");
2511 LightningReceiveAntiDos::InputVtxo(protos::InputVtxo {
2512 vtxo_id: vtxo.id().to_bytes().to_vec(),
2513 ownership_proof: {
2514 let sig = challenge.sign_with(vtxo.id(), vtxo_keypair);
2515 sig.serialize().to_vec()
2516 }
2517 })
2518 })
2519 }
2520
2521 async fn check_lightning_receive(
2550 &self,
2551 payment_hash: PaymentHash,
2552 wait: bool,
2553 token: Option<&str>,
2554 ) -> anyhow::Result<LightningReceive> {
2555 let mut srv = self.require_server()?;
2556 let current_height = self.chain.tip().await?;
2557
2558 let mut lightning_receive = self.db.fetch_lightning_receive_by_payment_hash(payment_hash)?
2559 .context("no lightning receive found")?;
2560
2561 if lightning_receive.htlc_vtxos.is_some() {
2563 return Ok(lightning_receive)
2564 }
2565
2566 info!("Waiting for payment...");
2567 let sub = srv.client.check_lightning_receive(protos::CheckLightningReceiveRequest {
2568 hash: payment_hash.to_byte_array().to_vec(), wait,
2569 }).await?.into_inner();
2570
2571 let status = protos::LightningReceiveStatus::try_from(sub.status)
2572 .with_context(|| format!("unknown payment status: {}", sub.status))?;
2573 match status {
2574 protos::LightningReceiveStatus::Accepted
2576 | protos::LightningReceiveStatus::HtlcsReady => {},
2577 protos::LightningReceiveStatus::Created => bail!("sender didn't initiate payment yet"),
2578 protos::LightningReceiveStatus::Settled => bail!("payment already settled"),
2579 protos::LightningReceiveStatus::Cancelled => bail!("payment was canceled"),
2580 }
2581
2582 let lightning_receive_anti_dos = match self.compute_lightning_receive_anti_dos(payment_hash, token).await {
2583 Ok(anti_dos) => Some(anti_dos),
2584 Err(e) => {
2585 warn!("Could not compute anti-dos: {e}. Trying without");
2586 None
2587 },
2588 };
2589
2590 let htlc_recv_expiry = current_height + lightning_receive.htlc_recv_cltv_delta as BlockHeight;
2591
2592 let (next_keypair, _) = self.derive_store_next_keypair()?;
2593 let req = protos::PrepareLightningReceiveClaimRequest {
2594 payment_hash: lightning_receive.payment_hash.to_vec(),
2595 user_pubkey: next_keypair.public_key().serialize().to_vec(),
2596 htlc_recv_expiry,
2597 lightning_receive_anti_dos,
2598 };
2599 let res = srv.client.prepare_lightning_receive_claim(req).await
2600 .context("error preparing lightning receive claim")?.into_inner();
2601 let vtxos = res.htlc_vtxos.into_iter()
2602 .map(|b| Vtxo::deserialize(&b))
2603 .collect::<Result<Vec<_>, _>>()
2604 .context("invalid htlc vtxos from server")?;
2605
2606 for vtxo in &vtxos {
2608 self.validate_vtxo(vtxo).await?;
2609
2610 if let VtxoPolicy::ServerHtlcRecv(p) = vtxo.policy() {
2611 if p.payment_hash != lightning_receive.payment_hash {
2612 bail!("invalid payment hash on HTLC VTXOs received from server: {}",
2613 p.payment_hash,
2614 );
2615 }
2616 if p.user_pubkey != next_keypair.public_key() {
2617 bail!("invalid pubkey on HTLC VTXOs received from server: {}", p.user_pubkey);
2618 }
2619 if p.htlc_expiry < htlc_recv_expiry {
2620 bail!("HTLC VTXO expiry height is less than requested: Requested {}, received {}", htlc_recv_expiry, p.htlc_expiry);
2621 }
2622 } else {
2623 bail!("invalid HTLC VTXO policy: {:?}", vtxo.policy());
2624 }
2625 }
2626
2627 let invoice_amount = lightning_receive.invoice.amount_milli_satoshis().map(|a| Amount::from_msat_floor(a))
2629 .expect("ln receive invoice should have amount");
2630 let htlc_amount = vtxos.iter().map(|v| v.amount()).sum::<Amount>();
2631 ensure!(vtxos.iter().map(|v| v.amount()).sum::<Amount>() >= invoice_amount,
2632 "Server didn't return enough VTXOs to cover invoice amount"
2633 );
2634
2635 let movement_id = if let Some(movement_id) = lightning_receive.movement_id {
2636 movement_id
2637 } else {
2638 self.movements.new_movement(
2639 self.subsystem_ids[&BarkSubsystem::LightningReceive],
2640 LightningReceiveMovement::Receive.to_string(),
2641 ).await?
2642 };
2643 self.store_locked_vtxos(&vtxos, Some(movement_id))?;
2644
2645 let vtxo_ids = vtxos.iter().map(|v| v.id()).collect::<Vec<_>>();
2646 self.db.update_lightning_receive(payment_hash, &vtxo_ids, movement_id)?;
2647
2648 self.movements.update_movement(
2649 movement_id,
2650 MovementUpdate::new()
2651 .intended_balance(invoice_amount.to_signed()?)
2652 .effective_balance(htlc_amount.to_signed()?)
2653 .metadata(LightningMovement::htlc_metadata(&vtxos)?)
2654 .received_on(
2655 [MovementDestination::new(lightning_receive.invoice.to_string(), htlc_amount)],
2656 ),
2657 ).await?;
2658
2659 let vtxos = vtxos
2660 .into_iter()
2661 .map(|v| self.db
2662 .get_wallet_vtxo(v.id())
2663 .and_then(|op| op.context("Failed to get wallet VTXO for lightning receive"))
2664 ).collect::<Result<Vec<_>, _>>()?;
2665
2666 lightning_receive.htlc_vtxos = Some(vtxos);
2667 lightning_receive.movement_id = Some(movement_id);
2668
2669 Ok(lightning_receive)
2670 }
2671
2672 pub async fn try_claim_lightning_receive(
2696 &self,
2697 payment_hash: PaymentHash,
2698 wait: bool,
2699 token: Option<&str>,
2700 ) -> anyhow::Result<()> {
2701 let receive = self.check_lightning_receive(payment_hash, wait, token).await?;
2702 self.claim_lightning_receive(&receive).await
2703 }
2704
2705 pub async fn try_claim_all_lightning_receives(&self, wait: bool) -> anyhow::Result<()> {
2720 tokio_stream::iter(self.pending_lightning_receives()?)
2722 .for_each_concurrent(3, |rcv| async move {
2723 if let Err(e) = self.try_claim_lightning_receive(rcv.invoice.into(), wait, None).await {
2724 error!("Error claiming lightning receive: {}", e);
2725 }
2726 }).await;
2727
2728 Ok(())
2729 }
2730
2731 async fn claim_all_pending_htlc_receives(&self) -> anyhow::Result<()> {
2742 let srv = self.require_server()?;
2743 let tip = self.chain.tip().await?;
2744 let lightning_receives = self.db.get_all_pending_lightning_receives()?;
2745 info!("Syncing {} pending lightning receives", lightning_receives.len());
2746
2747 for lightning_receive in lightning_receives {
2748 let (vtxos, movement_id) = match (
2749 &lightning_receive.htlc_vtxos, lightning_receive.movement_id
2750 ) {
2751 (Some(vtxos), Some(movement_id)) => (vtxos, movement_id),
2752 (Some(_), None) => {
2753 error!("Movement missing for lightning receive: {}", lightning_receive.payment_hash);
2754 continue;
2755 }
2756 (None, _) => continue,
2757 };
2758
2759 if let Err(e) = self.claim_lightning_receive(&lightning_receive).await {
2760 error!("Failed to claim pubkey vtxo from htlc vtxo: {}", e);
2761
2762 let first_vtxo = &vtxos.first().unwrap().vtxo;
2763 debug_assert!(vtxos.iter().all(|v| {
2764 v.vtxo.policy() == first_vtxo.policy() && v.vtxo.exit_delta() == first_vtxo.exit_delta()
2765 }), "all htlc vtxos for the same payment hash should have the same policy and exit delta");
2766
2767 let vtxo_htlc_expiry = first_vtxo.policy().as_server_htlc_recv()
2768 .expect("only server htlc recv vtxos can be pending lightning recv").htlc_expiry;
2769
2770 let safe_exit_margin = first_vtxo.exit_delta() +
2771 srv.info.htlc_expiry_delta +
2772 self.config.vtxo_exit_margin;
2773
2774 if tip > vtxo_htlc_expiry.saturating_sub(safe_exit_margin as BlockHeight) {
2775 if lightning_receive.preimage_revealed_at.is_some() {
2776 warn!("HTLC-recv VTXOs are about to expire and preimage has been disclosed, must exit");
2777 self.exit.write().await.mark_vtxos_for_exit(
2778 &vtxos.iter().map(|v| v.vtxo.clone()).collect::<Vec<_>>(),
2779 ).await?;
2780 self.movements.update_movement(
2781 movement_id,
2782 MovementUpdate::new()
2783 .exited_vtxos(vtxos)
2784 ).await?;
2785 self.movements.finish_movement(movement_id, MovementStatus::Failed).await?;
2786 } else {
2787 warn!("HTLC-recv VTXOs are about to expire, but preimage has not been disclosed yet, mark htlc as cancelled");
2788 self.mark_vtxos_as_spent(vtxos)?;
2789 self.movements.update_movement(
2790 movement_id,
2791 MovementUpdate::new()
2792 .effective_balance(SignedAmount::ZERO)
2793 ).await?;
2794 self.movements.finish_movement(
2795 movement_id, MovementStatus::Cancelled,
2796 ).await?;
2797 }
2798 }
2799 }
2800 }
2801
2802 Ok(())
2803 }
2804
2805 pub async fn pay_lightning_address(
2807 &self,
2808 addr: &LightningAddress,
2809 amount: Amount,
2810 comment: Option<&str>,
2811 ) -> anyhow::Result<(Bolt11Invoice, Preimage)> {
2812 let invoice = lnurl::lnaddr_invoice(addr, amount, comment).await
2813 .context("lightning address error")?;
2814 info!("Attempting to pay invoice {}", invoice);
2815 let preimage = self.pay_lightning_invoice(invoice.clone(), None).await
2816 .context("bolt11 payment error")?;
2817 Ok((invoice, preimage))
2818 }
2819
2820 pub async fn pay_lightning_offer(
2822 &self,
2823 offer: Offer,
2824 amount: Option<Amount>,
2825 ) -> anyhow::Result<(Bolt12Invoice, Preimage)> {
2826 let mut srv = self.require_server()?;
2827
2828 let offer_bytes = {
2829 let mut bytes = Vec::new();
2830 offer.write(&mut bytes).unwrap();
2831 bytes
2832 };
2833
2834 let req = protos::FetchBolt12InvoiceRequest {
2835 offer: offer_bytes,
2836 amount_sat: amount.map(|a| a.to_sat()),
2837 };
2838
2839 let resp = srv.client.fetch_bolt12_invoice(req).await?.into_inner();
2840
2841 let invoice = Bolt12Invoice::try_from(resp.invoice)
2842 .map_err(|_| anyhow::anyhow!("invalid invoice"))?;
2843
2844 invoice.validate_issuance(offer)?;
2845
2846 let preimage = self.pay_lightning_invoice(invoice.clone(), None).await
2847 .context("bolt11 payment error")?;
2848 Ok((invoice, preimage))
2849 }
2850
2851 pub fn build_round_onchain_payment_participation(
2852 &self,
2853 addr: bitcoin::Address,
2854 amount: Amount,
2855 ) -> anyhow::Result<RoundParticipation> {
2856 let srv = self.require_server()?;
2857
2858 let offb = OffboardRequest {
2859 script_pubkey: addr.script_pubkey(),
2860 amount: amount,
2861 };
2862 let required_amount = offb.amount + offb.fee(srv.info.offboard_feerate)?;
2863
2864 let inputs = self.select_vtxos_to_cover(required_amount, None, None)?;
2865
2866 let change = {
2867 let input_sum = inputs.iter().map(|v| v.amount()).sum::<Amount>();
2868 if input_sum < offb.amount {
2869 bail!("Your balance is too low. Needed: {}, available: {}",
2870 required_amount, self.balance()?.spendable,
2871 );
2872 } else if input_sum <= required_amount + P2TR_DUST {
2873 info!("No change, emptying wallet.");
2874 None
2875 } else {
2876 let change_amount = input_sum - required_amount;
2877 let (change_keypair, _) = self.derive_store_next_keypair()?;
2878 info!("Adding change vtxo for {}", change_amount);
2879 Some(VtxoRequest {
2880 amount: change_amount,
2881 policy: VtxoPolicy::new_pubkey(change_keypair.public_key()),
2882 })
2883 }
2884 };
2885
2886 Ok(RoundParticipation {
2887 inputs: inputs,
2888 outputs: change.into_iter().collect(),
2889 offboards: vec![offb],
2890 })
2891 }
2892
2893 pub async fn send_round_onchain_payment(
2896 &self,
2897 addr: bitcoin::Address,
2898 amount: Amount,
2899 ) -> anyhow::Result<RoundStatus> {
2900 let mut participation = self.build_round_onchain_payment_participation(addr, amount)?;
2901
2902 if let Err(e) = self.add_should_refresh_vtxos(&mut participation).await {
2903 warn!("Error trying to add additional VTXOs that should be refreshed: {:#}", e);
2904 }
2905
2906 self.participate_round(participation, Some(RoundMovement::SendOnchain)).await
2907 }
2908
2909 pub async fn run_daemon(
2915 self: &Arc<Self>,
2916 shutdown: CancellationToken,
2917 onchain: Arc<RwLock<dyn ExitUnilaterally>>,
2918 ) -> anyhow::Result<()> {
2919 let daemon = Daemon::new(shutdown, self.clone(), onchain)?;
2920
2921 tokio::spawn(async move {
2922 let _ = daemon.run().await;
2923 });
2924
2925 Ok(())
2926 }
2927}
2928
2929#[cfg(test)]
2930mod test {
2931 use super::*;
2932
2933 #[allow(unused)] async fn pay_lightning_invoice_argument() {
2935 let db = Arc::new(SqliteClient::open("").unwrap());
2938 let w = Wallet::open(
2939 &"".parse().unwrap(), db, Config::network_default(Network::Regtest),
2940 ).await.unwrap();
2941
2942 let bolt11 = Bolt11Invoice::from_str("").unwrap();
2943 w.pay_lightning_invoice(bolt11, None).await.unwrap();
2944
2945 let bolt12 = Bolt12Invoice::from_str("").unwrap();
2946 w.pay_lightning_invoice(bolt12, None).await.unwrap();
2947
2948 let string = format!("lnbc1..");
2949 w.pay_lightning_invoice(string, None).await.unwrap();
2950
2951 let strr = "lnbc1..";
2952 w.pay_lightning_invoice(strr, None).await.unwrap();
2953
2954 let invoice = Invoice::Bolt11("".parse().unwrap());
2955 w.pay_lightning_invoice(invoice, None).await.unwrap();
2956 }
2957}