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 exit;
292pub mod movement;
293pub mod onchain;
294pub mod persist;
295pub mod round;
296pub mod vtxo_state;
297pub mod vtxo_selection;
298
299pub use self::config::Config;
300pub use self::persist::sqlite::SqliteClient;
301pub use self::vtxo_state::WalletVtxo;
302
303mod config;
304mod lnurl;
305mod psbtext;
306
307use std::collections::{HashMap, HashSet};
308
309use std::convert::TryFrom;
310use std::str::FromStr;
311use std::sync::Arc;
312
313use anyhow::{bail, Context};
314use bip39::Mnemonic;
315use bitcoin::{Amount, FeeRate, Network, OutPoint, ScriptBuf, Transaction};
316use bitcoin::bip32::{self, Fingerprint};
317use bitcoin::consensus::deserialize;
318use bitcoin::hashes::Hash;
319use bitcoin::hex::DisplayHex;
320use bitcoin::secp256k1::{self, Keypair, PublicKey};
321use lnurllib::lightning_address::LightningAddress;
322use lightning_invoice::Bolt11Invoice;
323use lightning::util::ser::Writeable;
324use log::{trace, debug, info, warn, error};
325use futures::StreamExt;
326
327use ark::{ArkInfo, OffboardRequest, ProtocolEncoding, Vtxo, VtxoId, VtxoPolicy, VtxoRequest};
328use ark::address::VtxoDelivery;
329use ark::arkoor::ArkoorPackageBuilder;
330use ark::board::{BoardBuilder, BOARD_FUNDING_TX_VTXO_VOUT};
331use ark::lightning::{Bolt12Invoice, Bolt12InvoiceExt, Invoice, Offer, Preimage, PaymentHash};
332use ark::musig;
333use ark::rounds::RoundId;
334use ark::tree::signed::{CachedSignedVtxoTree, SignedVtxoTreeSpec};
335use ark::vtxo::{VtxoRef, PubkeyVtxoPolicy, VtxoPolicyKind};
336use server_rpc::{self as rpc, protos, ServerConnection, TryFromBytes};
337use bitcoin_ext::{AmountExt, BlockDelta, BlockHeight, P2TR_DUST, TxStatus};
338
339use crate::exit::Exit;
340use crate::movement::{Movement, MovementArgs, MovementKind};
341use crate::onchain::{ChainSource, PreparePsbt, ExitUnilaterally, Utxo, GetWalletTx, SignPsbt};
342use crate::persist::BarkPersister;
343use crate::persist::models::{LightningReceive, PendingLightningSend, StoredVtxoRequest};
344use crate::round::{DesiredRoundParticipation, RoundParticipation, RoundResult};
345use crate::vtxo_selection::{FilterVtxos, VtxoFilter};
346use crate::vtxo_state::{VtxoState, VtxoStateKind, UNSPENT_STATES};
347use crate::vtxo_selection::RefreshStrategy;
348
349const ARK_PURPOSE_INDEX: u32 = 350;
350
351const LIGHTNING_PREPARE_CLAIM_DELTA: BlockDelta = 2;
354
355lazy_static::lazy_static! {
356 static ref SECP: secp256k1::Secp256k1<secp256k1::All> = secp256k1::Secp256k1::new();
358}
359
360#[derive(Debug, Clone)]
362pub struct LightningReceiveBalance {
363 pub total: Amount,
365 pub claimable: Amount,
367}
368
369#[derive(Debug, Clone)]
371pub struct Balance {
372 pub spendable: Amount,
374 pub pending_lightning_send: Amount,
376 pub pending_lightning_receive: LightningReceiveBalance,
378 pub pending_in_round: Amount,
380 pub pending_exit: Option<Amount>,
383 pub pending_board: Amount,
385}
386
387struct ArkoorCreateResult {
388 input: Vec<Vtxo>,
389 created: Vec<Vtxo>,
390 change: Option<Vtxo>,
391}
392
393pub struct UtxoInfo {
394 pub outpoint: OutPoint,
395 pub amount: Amount,
396 pub confirmation_height: Option<u32>,
397}
398
399impl From<Utxo> for UtxoInfo {
400 fn from(value: Utxo) -> Self {
401 match value {
402 Utxo::Local(o) => UtxoInfo {
403 outpoint: o.outpoint,
404 amount: o.amount,
405 confirmation_height: o.confirmation_height,
406 },
407 Utxo::Exit(e) => UtxoInfo {
408 outpoint: e.vtxo.point(),
409 amount: e.vtxo.amount(),
410 confirmation_height: Some(e.height),
411 },
412 }
413 }
414}
415
416#[derive(Debug, Clone, PartialEq, Eq, Hash)]
418pub struct Board {
419 pub funding_txid: bitcoin::Txid,
423 pub vtxos: Vec<Vtxo>,
428}
429
430#[derive(Debug, Clone, PartialEq, Eq, Hash)]
433pub struct Offboard {
434 pub round: RoundId,
436}
437
438pub struct OffchainBalance {
441 pub available: Amount,
443 pub pending_in_round: Amount,
445 pub pending_exit: Amount,
448}
449
450#[derive(Debug, Clone)]
452pub struct WalletProperties {
453 pub network: Network,
457
458 pub fingerprint: Fingerprint,
462}
463
464pub struct VtxoSeed(bip32::Xpriv);
470
471impl VtxoSeed {
472 fn new(network: Network, seed: &[u8; 64]) -> Self {
473 let master = bip32::Xpriv::new_master(network, seed).unwrap();
474
475 Self(master.derive_priv(&SECP, &[ARK_PURPOSE_INDEX.into()]).unwrap())
476 }
477
478 fn fingerprint(&self) -> Fingerprint {
479 self.0.fingerprint(&SECP)
480 }
481
482 fn derive_keypair(&self, idx: u32) -> Keypair {
483 self.0.derive_priv(&SECP, &[idx.into()]).unwrap().to_keypair(&SECP)
484 }
485}
486
487pub struct Wallet {
621 pub chain: Arc<ChainSource>,
623
624 pub exit: tokio::sync::RwLock<Exit>,
626
627 config: Config,
629
630 db: Arc<dyn BarkPersister>,
632
633 vtxo_seed: VtxoSeed,
635
636 server: Option<ServerConnection>,
638
639}
640
641impl Wallet {
642 pub fn chain_source<P: BarkPersister>(
645 config: &Config,
646 ) -> anyhow::Result<onchain::ChainSourceSpec> {
647 if let Some(ref url) = config.esplora_address {
648 Ok(onchain::ChainSourceSpec::Esplora {
649 url: url.clone(),
650 })
651 } else if let Some(ref url) = config.bitcoind_address {
652 let auth = if let Some(ref c) = config.bitcoind_cookiefile {
653 bitcoin_ext::rpc::Auth::CookieFile(c.clone())
654 } else {
655 bitcoin_ext::rpc::Auth::UserPass(
656 config.bitcoind_user.clone().context("need bitcoind auth config")?,
657 config.bitcoind_pass.clone().context("need bitcoind auth config")?,
658 )
659 };
660 Ok(onchain::ChainSourceSpec::Bitcoind {
661 url: url.clone(),
662 auth,
663 })
664 } else {
665 bail!("Need to either provide esplora or bitcoind info");
666 }
667 }
668
669 pub fn require_chainsource_version(&self) -> anyhow::Result<()> {
673 self.chain.require_version()
674 }
675
676 pub fn derive_store_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
679 let last_revealed = self.db.get_last_vtxo_key_index()?;
680
681 let index = last_revealed.map(|i| i + 1).unwrap_or(u32::MIN);
682 let keypair = self.vtxo_seed.derive_keypair(index);
683
684 self.db.store_vtxo_key(index, keypair.public_key())?;
685 Ok((keypair, index))
686 }
687
688 pub fn peak_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
702 let keypair = self.vtxo_seed.derive_keypair(index);
703 if self.db.get_public_key_idx(&keypair.public_key())?.is_some() {
704 Ok(keypair)
705 } else {
706 bail!("VTXO key {} does not exist, please derive it first", index)
707 }
708 }
709
710
711 pub fn pubkey_keypair(&self, public_key: &PublicKey) -> anyhow::Result<Option<(u32, Keypair)>> {
723 if let Some(index) = self.db.get_public_key_idx(&public_key)? {
724 Ok(Some((index, self.vtxo_seed.derive_keypair(index))))
725 } else {
726 Ok(None)
727 }
728 }
729
730 pub fn get_vtxo_key(&self, vtxo: &Vtxo) -> anyhow::Result<Keypair> {
741 let idx = self.db.get_public_key_idx(&vtxo.user_pubkey())?
742 .context("VTXO key not found")?;
743 Ok(self.vtxo_seed.derive_keypair(idx))
744 }
745
746 pub fn new_address(&self) -> anyhow::Result<ark::Address> {
748 let ark = &self.require_server()?;
749 let network = self.properties()?.network;
750 let pubkey = self.derive_store_next_keypair()?.0.public_key();
751
752 Ok(ark::Address::builder()
753 .testnet(network != bitcoin::Network::Bitcoin)
754 .server_pubkey(ark.info.server_pubkey)
755 .pubkey_policy(pubkey)
756 .into_address().unwrap())
757 }
758
759 pub fn peak_address(&self, index: u32) -> anyhow::Result<ark::Address> {
763 let ark = &self.require_server()?;
764 let network = self.properties()?.network;
765 let pubkey = self.peak_keypair(index)?.public_key();
766
767 Ok(ark::Address::builder()
768 .testnet(network != Network::Bitcoin)
769 .server_pubkey(ark.info.server_pubkey)
770 .pubkey_policy(pubkey)
771 .into_address().unwrap())
772 }
773
774 pub fn new_address_with_index(&self) -> anyhow::Result<(ark::Address, u32)> {
778 let ark = &self.require_server()?;
779 let network = self.properties()?.network;
780 let (keypair, index) = self.derive_store_next_keypair()?;
781 let pubkey = keypair.public_key();
782 let addr = ark::Address::builder()
783 .testnet(network != bitcoin::Network::Bitcoin)
784 .server_pubkey(ark.info.server_pubkey)
785 .pubkey_policy(pubkey)
786 .into_address()?;
787 Ok((addr, index))
788 }
789
790 pub async fn create<P: BarkPersister>(
796 mnemonic: &Mnemonic,
797 network: Network,
798 config: Config,
799 db: Arc<P>,
800 force: bool,
801 ) -> anyhow::Result<Wallet> {
802 trace!("Config: {:?}", config);
803 if let Some(existing) = db.read_properties()? {
804 trace!("Existing config: {:?}", existing);
805 bail!("cannot overwrite already existing config")
806 }
807
808 if !force {
809 if let Err(_) = ServerConnection::connect(&config.server_address, network).await {
810 bail!("Not connected to a server. If you are sure use the --force flag.");
811 }
812 }
813
814 let wallet_fingerprint = VtxoSeed::new(network, &mnemonic.to_seed("")).fingerprint();
815 let properties = WalletProperties {
816 network: network,
817 fingerprint: wallet_fingerprint,
818 };
819
820 db.init_wallet(&properties).context("cannot init wallet in the database")?;
822
823 let wallet = Wallet::open(&mnemonic, db, config).await.context("failed to open wallet")?;
825 wallet.require_chainsource_version()?;
826
827 Ok(wallet)
828 }
829
830 pub async fn create_with_onchain<P: BarkPersister, W: ExitUnilaterally>(
838 mnemonic: &Mnemonic,
839 network: Network,
840 config: Config,
841 db: Arc<P>,
842 onchain: &W,
843 force: bool,
844 ) -> anyhow::Result<Wallet> {
845 let mut wallet = Wallet::create(mnemonic, network, config, db, force).await?;
846 wallet.exit.get_mut().load(onchain).await?;
847 Ok(wallet)
848 }
849
850 pub async fn open<P: BarkPersister>(
852 mnemonic: &Mnemonic,
853 db: Arc<P>,
854 config: Config,
855 ) -> anyhow::Result<Wallet> {
856 let properties = db.read_properties()?.context("Wallet is not initialised")?;
857
858 let seed = mnemonic.to_seed("");
859 let vtxo_seed = VtxoSeed::new(properties.network, &seed);
860
861 if properties.fingerprint != vtxo_seed.fingerprint() {
862 bail!("incorrect mnemonic")
863 }
864
865 let chain_source = if let Some(ref url) = config.esplora_address {
866 onchain::ChainSourceSpec::Esplora {
867 url: url.clone(),
868 }
869 } else if let Some(ref url) = config.bitcoind_address {
870 let auth = if let Some(ref c) = config.bitcoind_cookiefile {
871 bitcoin_ext::rpc::Auth::CookieFile(c.clone())
872 } else {
873 bitcoin_ext::rpc::Auth::UserPass(
874 config.bitcoind_user.clone().context("need bitcoind auth config")?,
875 config.bitcoind_pass.clone().context("need bitcoind auth config")?,
876 )
877 };
878 onchain::ChainSourceSpec::Bitcoind { url: url.clone(), auth }
879 } else {
880 bail!("Need to either provide esplora or bitcoind info");
881 };
882
883 let chain_source_client = ChainSource::new(
884 chain_source, properties.network, config.fallback_fee_rate,
885 ).await?;
886 let chain = Arc::new(chain_source_client);
887
888 let server = match ServerConnection::connect(
889 &config.server_address, properties.network,
890 ).await {
891 Ok(s) => Some(s),
892 Err(e) => {
893 warn!("Ark server handshake failed: {}", e);
894 None
895 }
896 };
897
898 let exit = tokio::sync::RwLock::new(Exit::new(db.clone(), chain.clone()).await?);
899
900 Ok(Wallet { config, db, vtxo_seed, exit, server, chain })
901 }
902
903 pub async fn open_with_onchain<P: BarkPersister, W: ExitUnilaterally>(
906 mnemonic: &Mnemonic,
907 db: Arc<P>,
908 onchain: &W,
909 cfg: Config,
910 ) -> anyhow::Result<Wallet> {
911 let mut wallet = Wallet::open(mnemonic, db, cfg).await?;
912 wallet.exit.get_mut().load(onchain).await?;
913 Ok(wallet)
914 }
915
916 pub fn config(&self) -> &Config {
918 &self.config
919 }
920
921 pub fn properties(&self) -> anyhow::Result<WalletProperties> {
923 let properties = self.db.read_properties()?.context("Wallet is not initialised")?;
924 Ok(properties)
925 }
926
927 fn require_server(&self) -> anyhow::Result<ServerConnection> {
928 self.server.clone().context("You should be connected to Ark server to perform this action")
929 }
930
931 pub fn ark_info(&self) -> Option<&ArkInfo> {
933 self.server.as_ref().map(|a| &a.info)
934 }
935
936 pub fn balance(&self) -> anyhow::Result<Balance> {
940 let vtxos = self.vtxos()?;
941
942 let spendable = {
943 let mut v = vtxos.iter().collect();
944 VtxoStateKind::Spendable.filter_vtxos(&mut v)?;
945 v.into_iter().map(|v| v.amount()).sum::<Amount>()
946 };
947
948 let pending_lightning_send = self.pending_lightning_send_vtxos()?.iter().map(|v| v.amount())
949 .sum::<Amount>();
950
951 let pending_lightning_receive = self.pending_lightning_receive_balance()?;
952
953 let pending_board = self.pending_board_vtxos()?.iter().map(|v| v.amount()).sum::<Amount>();
954
955 let pending_in_round = self.db.get_in_round_vtxos()?.iter()
956 .map(|v| v.amount()).sum();
957
958 let pending_exit = self.exit.try_read().ok().map(|e| e.pending_total());
959
960 Ok(Balance {
961 spendable,
962 pending_in_round,
963 pending_lightning_send,
964 pending_lightning_receive,
965 pending_exit,
966 pending_board,
967 })
968 }
969
970 pub fn get_vtxo_by_id(&self, vtxo_id: VtxoId) -> anyhow::Result<WalletVtxo> {
972 let vtxo = self.db.get_wallet_vtxo(vtxo_id)
973 .with_context(|| format!("Error when querying vtxo {} in database", vtxo_id))?
974 .with_context(|| format!("The VTXO with id {} cannot be found", vtxo_id))?;
975 Ok(vtxo)
976 }
977
978 pub fn movements(&self) -> anyhow::Result<Vec<Movement>> {
980 Ok(self.db.get_movements()?)
981 }
982
983 pub fn all_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
985 Ok(self.db.get_all_vtxos()?)
986 }
987
988 pub fn vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
990 Ok(self.db.get_vtxos_by_state(&UNSPENT_STATES)?)
991 }
992
993 pub fn vtxos_with(&self, filter: &impl FilterVtxos) -> anyhow::Result<Vec<WalletVtxo>> {
995 let mut vtxos = self.vtxos()?;
996 filter.filter_vtxos(&mut vtxos).context("error filtering vtxos")?;
997 Ok(vtxos)
998 }
999
1000 pub fn spendable_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1002 Ok(self.vtxos_with(&VtxoStateKind::Spendable)?)
1003 }
1004
1005 pub fn spendable_vtxos_with(
1007 &self,
1008 filter: &impl FilterVtxos,
1009 ) -> anyhow::Result<Vec<WalletVtxo>> {
1010 let mut vtxos = self.spendable_vtxos()?;
1011 filter.filter_vtxos(&mut vtxos).context("error filtering vtxos")?;
1012 Ok(vtxos)
1013 }
1014
1015 pub fn inround_vtxos_with(&self, filter: &impl FilterVtxos) -> anyhow::Result<Vec<WalletVtxo>> {
1017 let mut vtxos = self.db.get_in_round_vtxos()?;
1018 filter.filter_vtxos(&mut vtxos).context("error filtering vtxos")?;
1019 Ok(vtxos)
1020 }
1021
1022 pub fn pending_board_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1027 let vtxos = self.db.get_all_pending_boards()?.iter()
1028 .map(|vtxo_id| self.get_vtxo_by_id(*vtxo_id))
1029 .collect::<anyhow::Result<Vec<_>>>()?;
1030
1031 debug_assert!(vtxos.iter().all(|v| matches!(v.state.kind(), VtxoStateKind::Locked)),
1032 "all pending board vtxos should be locked"
1033 );
1034
1035 Ok(vtxos)
1036 }
1037
1038 pub fn pending_lightning_send_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1040 let vtxos = self.db.get_all_pending_lightning_send()?.into_iter()
1041 .flat_map(|pending_lightning_send| pending_lightning_send.htlc_vtxos)
1042 .collect::<Vec<_>>();
1043
1044 Ok(vtxos)
1045 }
1046
1047 pub async fn get_expiring_vtxos(
1049 &self,
1050 threshold: BlockHeight,
1051 ) -> anyhow::Result<Vec<WalletVtxo>> {
1052 let expiry = self.chain.tip().await? + threshold;
1053 let filter = VtxoFilter::new(&self).expires_before(expiry);
1054 Ok(self.spendable_vtxos_with(&filter)?)
1055 }
1056
1057 pub async fn sync_pending_boards(&self) -> anyhow::Result<()> {
1061 let ark_info = self.require_server()?.info;
1062 let current_height = self.chain.tip().await?;
1063 let unregistered_boards = self.pending_board_vtxos()?;
1064 let mut registered_boards = 0;
1065
1066 if unregistered_boards.is_empty() {
1067 return Ok(());
1068 }
1069
1070 trace!("Attempting registration of sufficiently confirmed boards");
1071
1072 for board in unregistered_boards {
1073 let anchor = board.vtxo.chain_anchor();
1074 let confs = match self.chain.tx_status(anchor.txid).await {
1075 Ok(TxStatus::Confirmed(block_ref)) => Some(current_height - (block_ref.height - 1)),
1076 Ok(TxStatus::Mempool) => Some(0),
1077 Ok(TxStatus::NotFound) => None,
1078 Err(_) => None,
1079 };
1080
1081 if let Some(confs) = confs {
1082 if confs >= ark_info.required_board_confirmations as BlockHeight {
1083 if let Err(e) = self.register_board(board.vtxo.id()).await {
1084 warn!("Failed to register board {}: {}", board.vtxo.id(), e);
1085 } else {
1086 info!("Registered board {}", board.vtxo.id());
1087 registered_boards += 1;
1088 }
1089 }
1090 }
1091 };
1092
1093 if registered_boards > 0 {
1094 info!("Registered {registered_boards} sufficiently confirmed boards");
1095 }
1096 Ok(())
1097 }
1098
1099 pub async fn maintenance(&self) -> anyhow::Result<()> {
1104 info!("Starting wallet maintenance");
1105 self.sync().await;
1106 self.maintenance_refresh().await?;
1107 Ok(())
1108 }
1109
1110 pub async fn maintenance_with_onchain<W: PreparePsbt + SignPsbt + ExitUnilaterally>(
1116 &self,
1117 onchain: &mut W,
1118 ) -> anyhow::Result<()> {
1119 info!("Starting wallet maintenance with onchain wallet");
1120 self.sync().await;
1121 self.maintenance_refresh().await?;
1122
1123 self.sync_exits(onchain).await?;
1125
1126 Ok(())
1127 }
1128
1129 pub async fn maintenance_refresh(&self) -> anyhow::Result<Option<RoundId>> {
1135 let vtxos = self.get_vtxos_to_refresh().await?.into_iter()
1136 .map(|v| v.id())
1137 .collect::<Vec<_>>();
1138 if vtxos.len() == 0 {
1139 return Ok(None);
1140 }
1141
1142 info!("Performing maintenance refresh");
1143 self.refresh_vtxos(vtxos).await
1144 }
1145
1146 pub async fn sync(&self) {
1154 tokio::join!(
1155 async {
1156 if let Err(e) = self.chain.update_fee_rates(self.config.fallback_fee_rate).await {
1159 warn!("Error updating fee rates: {:#}", e);
1160 }
1161 },
1162 async {
1163 if let Err(e) = self.sync_oors().await {
1164 warn!("Error in arkoor sync: {:#}", e);
1165 }
1166 },
1167 async {
1168 if let Err(e) = self.sync_pending_rounds().await {
1169 warn!("Error syncing pending rounds: {:#}", e);
1170 }
1171 },
1172 async {
1173 if let Err(e) = self.sync_pending_lightning_send_vtxos().await {
1174 warn!("Error syncing pending lightning payments: {:#}", e);
1175 }
1176 },
1177 async {
1178 if let Err(e) = self.claim_all_pending_htlc_recvs().await {
1179 warn!("Error claiming pending lightning receives: {:#}", e);
1180 }
1181 },
1182 async {
1183 if let Err(e) = self.sync_pending_boards().await {
1184 warn!("Error syncing pending boards: {:#}", e);
1185 }
1186 }
1187 );
1188 }
1189
1190 pub async fn sync_exits<W: ExitUnilaterally>(
1196 &self,
1197 onchain: &mut W,
1198 ) -> anyhow::Result<()> {
1199 self.exit.write().await.sync_exit(onchain).await?;
1200 Ok(())
1201 }
1202
1203 pub async fn sync_pending_lightning_send_vtxos(&self) -> anyhow::Result<()> {
1206 let pending_payments = self.db.get_all_pending_lightning_send()?;
1207
1208 if pending_payments.is_empty() {
1209 return Ok(());
1210 }
1211
1212 info!("Syncing {} pending lightning sends", pending_payments.len());
1213
1214 for payment in pending_payments {
1215 self.check_lightning_payment(&payment).await?;
1216 }
1217
1218 Ok(())
1219 }
1220
1221 pub async fn dangerous_drop_vtxo(&self, vtxo_id: VtxoId) -> anyhow::Result<()> {
1224 warn!("Drop vtxo {} from the database", vtxo_id);
1225 self.db.remove_vtxo(vtxo_id)?;
1226 Ok(())
1227 }
1228
1229 pub async fn dangerous_drop_all_vtxos(&self) -> anyhow::Result<()> {
1232 warn!("Dropping all vtxos from the db...");
1233 for vtxo in self.vtxos()? {
1234 self.db.remove_vtxo(vtxo.id())?;
1235 }
1236
1237 self.exit.write().await.clear_exit()?;
1238 Ok(())
1239 }
1240
1241 pub async fn board_amount<W: PreparePsbt + SignPsbt + GetWalletTx>(
1245 &self,
1246 onchain: &mut W,
1247 amount: Amount,
1248 ) -> anyhow::Result<Board> {
1249 let (user_keypair, _) = self.derive_store_next_keypair()?;
1250 self.board(onchain, Some(amount), user_keypair).await
1251 }
1252
1253 pub async fn board_all<W: PreparePsbt + SignPsbt + GetWalletTx>(
1255 &self,
1256 onchain: &mut W,
1257 ) -> anyhow::Result<Board> {
1258 let (user_keypair, _) = self.derive_store_next_keypair()?;
1259 self.board(onchain, None, user_keypair).await
1260 }
1261
1262 async fn board<W: PreparePsbt + SignPsbt + GetWalletTx>(
1263 &self,
1264 wallet: &mut W,
1265 amount: Option<Amount>,
1266 user_keypair: Keypair,
1267 ) -> anyhow::Result<Board> {
1268 let mut srv = self.require_server()?;
1269 let properties = self.db.read_properties()?.context("Missing config")?;
1270 let current_height = self.chain.tip().await?;
1271
1272 let expiry_height = current_height + srv.info.vtxo_expiry_delta as BlockHeight;
1273 let builder = BoardBuilder::new(
1274 user_keypair.public_key(),
1275 expiry_height,
1276 srv.info.server_pubkey,
1277 srv.info.vtxo_exit_delta,
1278 );
1279
1280 let addr = bitcoin::Address::from_script(
1281 &builder.funding_script_pubkey(),
1282 properties.network,
1283 ).unwrap();
1284
1285 let fee_rate = self.chain.fee_rates().await.regular;
1287 let (board_psbt, amount) = if let Some(amount) = amount {
1288 let psbt = wallet.prepare_tx([(addr, amount)], fee_rate)?;
1289 (psbt, amount)
1290 } else {
1291 let psbt = wallet.prepare_drain_tx(addr, fee_rate)?;
1292 assert_eq!(psbt.unsigned_tx.output.len(), 1);
1293 let amount = psbt.unsigned_tx.output[0].value;
1294 (psbt, amount)
1295 };
1296
1297 ensure!(amount >= srv.info.min_board_amount,
1298 "board amount of {amount} is less than minimum board amount required by server ({})",
1299 srv.info.min_board_amount,
1300 );
1301
1302 let utxo = OutPoint::new(board_psbt.unsigned_tx.compute_txid(), BOARD_FUNDING_TX_VTXO_VOUT);
1303 let builder = builder
1304 .set_funding_details(amount, utxo)
1305 .generate_user_nonces();
1306
1307 let cosign_resp = srv.client.request_board_cosign(protos::BoardCosignRequest {
1308 amount: amount.to_sat(),
1309 utxo: bitcoin::consensus::serialize(&utxo), expiry_height: expiry_height,
1311 user_pubkey: user_keypair.public_key().serialize().to_vec(),
1312 pub_nonce: builder.user_pub_nonce().serialize().to_vec(),
1313 }).await.context("error requesting board cosign")?
1314 .into_inner().try_into().context("invalid cosign response from server")?;
1315
1316 ensure!(builder.verify_cosign_response(&cosign_resp),
1317 "invalid board cosignature received from server",
1318 );
1319
1320 let vtxo = builder.build_vtxo(&cosign_resp, &user_keypair)?;
1322
1323 self.db.register_movement(MovementArgs {
1324 kind: MovementKind::Board,
1325 spends: &[],
1326 receives: &[(&vtxo, VtxoState::Locked)],
1327 recipients: &[],
1328 fees: None,
1329 }).context("db error storing vtxo")?;
1330
1331 let tx = wallet.finish_tx(board_psbt)?;
1332
1333 self.db.store_pending_board(&vtxo, &tx)?;
1334
1335 trace!("Broadcasting board tx: {}", bitcoin::consensus::encode::serialize_hex(&tx));
1336 self.chain.broadcast_tx(&tx).await?;
1337
1338 info!("Board broadcasted");
1339 Ok(Board {
1340 funding_txid: tx.compute_txid(),
1341 vtxos: vec![vtxo.into()],
1342 })
1343 }
1344
1345 async fn register_board(&self, vtxo: impl VtxoRef) -> anyhow::Result<Board> {
1347 trace!("Attempting to register board {} to server", vtxo.vtxo_id());
1348 let mut srv = self.require_server()?;
1349
1350 let vtxo = match vtxo.vtxo() {
1352 Some(v) => v,
1353 None => {
1354 &self.db.get_wallet_vtxo(vtxo.vtxo_id())?
1355 .with_context(|| format!("VTXO doesn't exist: {}", vtxo.vtxo_id()))?
1356 },
1357 };
1358
1359 srv.client.register_board_vtxo(protos::BoardVtxoRequest {
1361 board_vtxo: vtxo.serialize(),
1362 }).await.context("error registering board with the Ark server")?;
1363
1364 self.db.update_vtxo_state_checked(vtxo.vtxo_id(), VtxoState::Spendable, &UNSPENT_STATES)?;
1367
1368 self.db.remove_pending_board(&vtxo.vtxo_id())?;
1369
1370 let funding_txid = vtxo.chain_anchor().txid;
1371
1372 Ok(Board {
1373 funding_txid: funding_txid,
1374 vtxos: vec![vtxo.clone()],
1375 })
1376 }
1377
1378 fn build_vtxo(
1379 &self,
1380 vtxos: &CachedSignedVtxoTree,
1381 leaf_idx: usize,
1382 ) -> anyhow::Result<Option<Vtxo>> {
1383 let vtxo = vtxos.build_vtxo(leaf_idx).context("invalid leaf idx..")?;
1384
1385 if self.db.get_wallet_vtxo(vtxo.id())?.is_some() {
1386 debug!("Not adding vtxo {} because it already exists", vtxo.id());
1387 return Ok(None)
1388 }
1389
1390 debug!("Built new vtxo {} with value {}", vtxo.id(), vtxo.amount());
1391 Ok(Some(vtxo))
1392 }
1393
1394 fn has_counterparty_risk(&self, vtxo: &Vtxo) -> anyhow::Result<bool> {
1399 for past_pk in vtxo.past_arkoor_pubkeys() {
1400 if !self.db.get_public_key_idx(&past_pk)?.is_some() {
1401 return Ok(true);
1402 }
1403 }
1404 Ok(!self.db.get_public_key_idx(&vtxo.user_pubkey())?.is_some())
1405 }
1406
1407 pub async fn sync_past_rounds(&self) -> anyhow::Result<()> {
1411 let mut srv = self.require_server()?;
1412
1413 let fresh_rounds = srv.client.get_fresh_rounds(protos::FreshRoundsRequest {
1414 last_round_txid: None,
1415 }).await?.into_inner().txids.into_iter()
1416 .map(|txid| RoundId::from_slice(&txid))
1417 .collect::<Result<Vec<_>, _>>()?;
1418
1419 if fresh_rounds.is_empty() {
1420 debug!("No new rounds to sync");
1421 return Ok(());
1422 }
1423
1424 debug!("Received {} new rounds from ark", fresh_rounds.len());
1425
1426 let last_pk_index = self.db.get_last_vtxo_key_index()?.unwrap_or_default();
1427 let pubkeys = (0..=last_pk_index).map(|idx| {
1428 self.vtxo_seed.derive_keypair(idx).public_key()
1429 }).collect::<HashSet<_>>();
1430
1431 let results = tokio_stream::iter(fresh_rounds).map(|round_id| {
1432 let pubkeys = pubkeys.clone();
1433 let mut srv = srv.clone();
1434
1435 async move {
1436 if self.db.get_round_attempt_by_round_txid(round_id)?.is_some() {
1437 debug!("Skipping round {} because it already exists", round_id);
1438 return Ok::<_, anyhow::Error>(());
1439 }
1440
1441 let req = protos::RoundId {
1442 txid: round_id.as_round_txid().to_byte_array().to_vec(),
1443 };
1444 let round = srv.client.get_round(req).await?.into_inner();
1445
1446 let tree = SignedVtxoTreeSpec::deserialize(&round.signed_vtxos)
1447 .context("invalid signed vtxo tree from srv")?
1448 .into_cached_tree();
1449
1450 let mut reqs = Vec::new();
1451 let mut vtxos = vec![];
1452 for (idx, dest) in tree.spec.spec.vtxos.iter().enumerate() {
1453 if pubkeys.contains(&dest.vtxo.policy.user_pubkey()) {
1454 if let Some(vtxo) = self.build_vtxo(&tree, idx)? {
1455 reqs.push(StoredVtxoRequest {
1456 request_policy: dest.vtxo.policy.clone(),
1457 amount: dest.vtxo.amount,
1458 state: VtxoState::Spendable,
1459 });
1460
1461 vtxos.push(vtxo);
1462 }
1463 }
1464 }
1465
1466 let round_tx = deserialize::<Transaction>(&round.funding_tx)?;
1467 self.db.store_pending_confirmation_round(round_id, round_tx, reqs, vtxos)?;
1468
1469 Ok(())
1470 }
1471 })
1472 .buffer_unordered(10)
1473 .collect::<Vec<_>>()
1474 .await;
1475
1476 for result in results {
1477 if let Err(e) = result {
1478 return Err(e).context("failed to sync round");
1479 }
1480 }
1481
1482 Ok(())
1483 }
1484
1485 async fn sync_oors(&self) -> anyhow::Result<()> {
1486 let last_pk_index = self.db.get_last_vtxo_key_index()?.unwrap_or_default();
1487 let pubkeys = (0..=last_pk_index).map(|idx| {
1488 self.vtxo_seed.derive_keypair(idx).public_key()
1489 }).collect::<Vec<_>>();
1490
1491 self.sync_arkoor_for_pubkeys(&pubkeys).await?;
1492
1493 Ok(())
1494 }
1495
1496 async fn sync_arkoor_for_pubkeys(
1498 &self,
1499 public_keys: &[PublicKey],
1500 ) -> anyhow::Result<()> {
1501 let mut srv = self.require_server()?;
1502
1503 for pubkeys in public_keys.chunks(rpc::MAX_NB_MAILBOX_PUBKEYS) {
1504 debug!("Emptying OOR mailbox at Ark server...");
1506 let req = protos::ArkoorVtxosRequest {
1507 pubkeys: pubkeys.iter().map(|pk| pk.serialize().to_vec()).collect(),
1508 };
1509 let packages = srv.client.empty_arkoor_mailbox(req).await
1510 .context("error fetching oors")?.into_inner().packages;
1511 debug!("Ark server has {} arkoor packages for us", packages.len());
1512
1513 for package in packages {
1514 let mut vtxos = Vec::with_capacity(package.vtxos.len());
1515 for vtxo in package.vtxos {
1516 let vtxo = match Vtxo::deserialize(&vtxo) {
1517 Ok(vtxo) => vtxo,
1518 Err(e) => {
1519 warn!("Invalid vtxo from Ark server: {}", e);
1520 continue;
1521 }
1522 };
1523
1524
1525 let txid = vtxo.chain_anchor().txid;
1526 let chain_anchor = self.chain.get_tx(&txid).await?.with_context(|| {
1527 format!("received arkoor vtxo with unknown chain anchor: {}", txid)
1528 })?;
1529 if let Err(e) = vtxo.validate(&chain_anchor) {
1530 error!("Received invalid arkoor VTXO from server: {}", e);
1531 continue;
1532 }
1533
1534 match self.db.has_spent_vtxo(vtxo.id()) {
1535 Ok(spent) if spent => {
1536 debug!("Not adding OOR vtxo {} because it is considered spent", vtxo.id());
1537 continue;
1538 },
1539 _ => {}
1540 }
1541
1542 if let Ok(Some(_)) = self.db.get_wallet_vtxo(vtxo.id()) {
1543 debug!("Not adding OOR vtxo {} because it already exists", vtxo.id());
1544 continue;
1545 }
1546
1547 vtxos.push(vtxo);
1548 }
1549
1550 self.db.register_movement(MovementArgs {
1551 kind: MovementKind::ArkoorReceive,
1552 spends: &[],
1553 receives: &vtxos.iter().map(|v| (v, VtxoState::Spendable)).collect::<Vec<_>>(),
1554 recipients: &[],
1555 fees: None,
1556 }).context("failed to store OOR vtxo")?;
1557 }
1558 }
1559
1560 Ok(())
1561 }
1562
1563 async fn offboard<V: VtxoRef>(
1564 &mut self,
1565 vtxos: impl IntoIterator<Item = V>,
1566 destination: ScriptBuf,
1567 ) -> anyhow::Result<Offboard> {
1568 let vtxos = {
1569 let vtxos = vtxos.into_iter();
1570 let mut ret = Vec::with_capacity(vtxos.size_hint().0);
1571 for v in vtxos {
1572 let vtxo = match v.vtxo() {
1573 Some(v) => v.clone(),
1574 None => self.get_vtxo_by_id(v.vtxo_id()).context("vtxo not found")?.vtxo,
1575 };
1576 ret.push(vtxo);
1577 }
1578 ret
1579 };
1580
1581 if vtxos.is_empty() {
1582 bail!("no VTXO to offboard");
1583 }
1584
1585 let participation = DesiredRoundParticipation::Offboard { vtxos, destination };
1586 let RoundResult { round_id, .. } = self.participate_round(participation).await
1587 .context("round failed")?;
1588
1589 Ok(Offboard { round: round_id })
1590 }
1591
1592 pub async fn offboard_all(&mut self, address: bitcoin::Address) -> anyhow::Result<Offboard> {
1594 let input_vtxos = self.spendable_vtxos()?;
1595 Ok(self.offboard(input_vtxos, address.script_pubkey()).await?)
1596 }
1597
1598 pub async fn offboard_vtxos<V: VtxoRef>(
1600 &mut self,
1601 vtxos: impl IntoIterator<Item = V>,
1602 address: bitcoin::Address,
1603 ) -> anyhow::Result<Offboard> {
1604 let input_vtxos = vtxos
1605 .into_iter()
1606 .map(|v| {
1607 let id = v.vtxo_id();
1608 match self.db.get_wallet_vtxo(id)? {
1609 Some(vtxo) => Ok(vtxo.vtxo),
1610 _ => bail!("cannot find requested vtxo: {}", id),
1611 }
1612 })
1613 .collect::<anyhow::Result<Vec<_>>>()?;
1614
1615 Ok(self.offboard(input_vtxos, address.script_pubkey()).await?)
1616 }
1617
1618 pub async fn refresh_vtxos<V: VtxoRef>(
1624 &self,
1625 vtxos: impl IntoIterator<Item = V>,
1626 ) -> anyhow::Result<Option<RoundId>> {
1627 let vtxos = {
1628 let mut ret = HashMap::new();
1629 for v in vtxos {
1630 let id = v.vtxo_id();
1631 let vtxo = self.get_vtxo_by_id(id)
1632 .with_context(|| format!("vtxo with id {} not found", id))?;
1633 if !ret.insert(id, vtxo).is_none() {
1634 bail!("duplicate VTXO id: {}", id);
1635 }
1636 }
1637 ret
1638 };
1639
1640 if vtxos.is_empty() {
1641 info!("Skipping refresh since no VTXOs are provided.");
1642 return Ok(None);
1643 }
1644
1645 let total_amount = vtxos.values().map(|v| v.vtxo.amount()).sum();
1646
1647 info!("Refreshing {} VTXOs (total amount = {}).", vtxos.len(), total_amount);
1648
1649 let (user_keypair, _) = self.derive_store_next_keypair()?;
1650 let req = VtxoRequest {
1651 policy: VtxoPolicy::Pubkey(PubkeyVtxoPolicy { user_pubkey: user_keypair.public_key() }),
1652 amount: total_amount,
1653 };
1654
1655 let participation = DesiredRoundParticipation::Funded(RoundParticipation {
1656 inputs: vtxos.into_values().map(|v| v.vtxo).collect(),
1657 outputs: vec![StoredVtxoRequest::from_parts(req.clone(), VtxoState::Spendable)],
1658 offboards: Vec::new(),
1659 });
1660 let RoundResult { round_id, .. } = self.participate_round(participation).await
1661 .context("round failed")?;
1662
1663 Ok(Some(round_id))
1664 }
1665
1666 pub async fn get_vtxos_to_refresh(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1670 let tip = self.chain.tip().await?;
1671 let fee_rate = self.chain.fee_rates().await.fast;
1672
1673 let must_refresh_vtxos = self.spendable_vtxos_with(
1675 &RefreshStrategy::must_refresh(self, tip, fee_rate),
1676 )?;
1677 if must_refresh_vtxos.is_empty() {
1678 return Ok(vec![]);
1679 } else {
1680 let should_refresh_vtxos = self.spendable_vtxos_with(
1683 &RefreshStrategy::should_refresh(self, tip, fee_rate),
1684 )?;
1685 Ok(should_refresh_vtxos)
1686 }
1687 }
1688
1689 pub fn get_first_expiring_vtxo_blockheight(
1691 &self,
1692 ) -> anyhow::Result<Option<BlockHeight>> {
1693 Ok(self.spendable_vtxos()?.iter().map(|v| v.expiry_height()).min())
1694 }
1695
1696 pub fn get_next_required_refresh_blockheight(
1699 &self,
1700 ) -> anyhow::Result<Option<BlockHeight>> {
1701 let first_expiry = self.get_first_expiring_vtxo_blockheight()?;
1702 Ok(first_expiry.map(|h| h.saturating_sub(self.config.vtxo_refresh_expiry_threshold)))
1703 }
1704
1705 fn select_vtxos_to_cover(
1711 &self,
1712 amount: Amount,
1713 max_depth: Option<u16>,
1714 expiry_threshold: Option<BlockHeight>,
1715 ) -> anyhow::Result<Vec<Vtxo>> {
1716 let inputs = self.spendable_vtxos()?;
1717
1718 let mut result = Vec::new();
1720 let mut total_amount = bitcoin::Amount::ZERO;
1721 for input in inputs {
1722 if let Some(max_depth) = max_depth {
1723 if input.arkoor_depth() >= max_depth {
1724 warn!("VTXO {} reached max depth of {}, skipping it. \
1725 Please refresh your VTXO.", input.id(), max_depth,
1726 );
1727 continue;
1728 }
1729 }
1730
1731 if let Some(threshold) = expiry_threshold {
1733 if input.expiry_height() < threshold {
1734 warn!("VTXO {} is expiring soon (expires at {}, threshold {}), \
1735 skipping for arkoor payment",
1736 input.id(), input.expiry_height(), threshold,
1737 );
1738 continue;
1739 }
1740 }
1741
1742 total_amount += input.amount();
1743 result.push(input.vtxo);
1744
1745 if total_amount >= amount {
1746 return Ok(result)
1747 }
1748 }
1749
1750 bail!("Insufficient money available. Needed {} but {} is available",
1751 amount, total_amount,
1752 );
1753 }
1754
1755 async fn create_arkoor_vtxos(
1761 &self,
1762 destination_policy: VtxoPolicy,
1763 amount: Amount,
1764 ) -> anyhow::Result<ArkoorCreateResult> {
1765 let mut srv = self.require_server()?;
1766 let change_pubkey = self.derive_store_next_keypair()?.0.public_key();
1767
1768 let req = VtxoRequest {
1769 amount: amount,
1770 policy: destination_policy,
1771 };
1772
1773 let tip = self.chain.tip().await?;
1775 let inputs = self.select_vtxos_to_cover(
1776 req.amount,
1777 Some(srv.info.max_arkoor_depth),
1778 Some(tip + self.config.vtxo_refresh_expiry_threshold),
1779 )?;
1780
1781 let mut secs = Vec::with_capacity(inputs.len());
1782 let mut pubs = Vec::with_capacity(inputs.len());
1783 let mut keypairs = Vec::with_capacity(inputs.len());
1784 for input in inputs.iter() {
1785 let keypair = self.get_vtxo_key(&input)?;
1786 let (s, p) = musig::nonce_pair(&keypair);
1787 secs.push(s);
1788 pubs.push(p);
1789 keypairs.push(keypair);
1790 }
1791
1792 let builder = ArkoorPackageBuilder::new(&inputs, &pubs, req, Some(change_pubkey))?;
1793
1794 let req = protos::ArkoorPackageCosignRequest {
1795 arkoors: builder.arkoors.iter().map(|a| a.into()).collect(),
1796 };
1797 let cosign_resp: Vec<_> = srv.client.request_arkoor_package_cosign(req).await?
1798 .into_inner().try_into().context("invalid server cosign response")?;
1799 ensure!(builder.verify_cosign_response(&cosign_resp),
1800 "invalid arkoor cosignature received from server",
1801 );
1802
1803 let (sent, change) = builder.build_vtxos(&cosign_resp, &keypairs, secs)?;
1804
1805 if let Some(change) = change.as_ref() {
1806 info!("Added change VTXO of {}", change.amount());
1807 }
1808
1809 Ok(ArkoorCreateResult {
1810 input: inputs,
1811 created: sent,
1812 change: change,
1813 })
1814 }
1815
1816 pub fn validate_arkoor_address(&self, address: &ark::Address) -> anyhow::Result<()> {
1820 let asp = self.require_server()?;
1821
1822 if !address.ark_id().is_for_server(asp.info.server_pubkey) {
1823 bail!("Ark address is for different server");
1824 }
1825
1826 match address.policy().policy_type() {
1828 VtxoPolicyKind::Pubkey => {},
1829 VtxoPolicyKind::ServerHtlcRecv | VtxoPolicyKind::ServerHtlcSend => {
1830 bail!("VTXO policy in address cannot be used for arkoor payment: {}",
1831 address.policy().policy_type(),
1832 );
1833 }
1834 }
1835
1836 if address.delivery().is_empty() {
1837 bail!("No VTXO delivery mechanism provided in address");
1838 }
1839 if !address.delivery().iter().any(|d| !d.is_unknown()) {
1843 for d in address.delivery() {
1844 if let VtxoDelivery::Unknown { delivery_type, data } = d {
1845 info!("Unknown delivery in address: type={:#x}, data={}",
1846 delivery_type, data.as_hex(),
1847 );
1848 }
1849 }
1850 }
1851
1852 Ok(())
1853 }
1854
1855 pub async fn send_arkoor_payment(
1865 &self,
1866 destination: &ark::Address,
1867 amount: Amount,
1868 ) -> anyhow::Result<Vec<Vtxo>> {
1869 let mut srv = self.require_server()?;
1870
1871 self.validate_arkoor_address(&destination).context("cannot send to address")?;
1872
1873 if amount < P2TR_DUST {
1874 bail!("Sent amount must be at least {}", P2TR_DUST);
1875 }
1876
1877 let arkoor = self.create_arkoor_vtxos(destination.policy().clone(), amount).await?;
1878
1879 let req = protos::ArkoorPackage {
1880 arkoors: arkoor.created.iter().map(|v| protos::ArkoorVtxo {
1881 pubkey: destination.policy().user_pubkey().serialize().to_vec(),
1882 vtxo: v.serialize().to_vec(),
1883 }).collect(),
1884 };
1885
1886 if let Err(e) = srv.client.post_arkoor_package_mailbox(req).await {
1887 error!("Failed to post the arkoor vtxo to the recipients mailbox: '{}'", e);
1888 }
1890
1891 self.db.register_movement(MovementArgs {
1892 kind: MovementKind::ArkoorSend,
1893 spends: &arkoor.input.iter().collect::<Vec<_>>(),
1894 receives: &arkoor.change.as_ref()
1895 .map(|v| vec![(v, VtxoState::Spendable)])
1896 .unwrap_or(vec![]),
1897 recipients: &[(&destination.to_string(), amount)],
1898 fees: None,
1899 }).context("failed to store arkoor vtxo")?;
1900
1901 Ok(arkoor.created)
1902 }
1903
1904 async fn process_lightning_revocation(&self, payment: &PendingLightningSend) -> anyhow::Result<()> {
1905 let mut srv = self.require_server()?;
1906 let htlc_vtxos = payment.htlc_vtxos.clone().into_iter()
1907 .map(|v: WalletVtxo| v.vtxo).collect::<Vec<_>>();
1908
1909 info!("Processing {} HTLC VTXOs for revocation", htlc_vtxos.len());
1910
1911 let mut secs = Vec::with_capacity(htlc_vtxos.len());
1912 let mut pubs = Vec::with_capacity(htlc_vtxos.len());
1913 let mut keypairs = Vec::with_capacity(htlc_vtxos.len());
1914 for input in htlc_vtxos.iter() {
1915 let keypair = self.get_vtxo_key(&input)?;
1916 let (s, p) = musig::nonce_pair(&keypair);
1917 secs.push(s);
1918 pubs.push(p);
1919 keypairs.push(keypair);
1920 }
1921
1922 let revocation = ArkoorPackageBuilder::new_htlc_revocation(&htlc_vtxos, &pubs)?;
1923
1924 let req = protos::RevokeLightningPaymentRequest {
1925 htlc_vtxo_ids: revocation.arkoors.iter()
1926 .map(|i| i.input.id().to_bytes().to_vec())
1927 .collect(),
1928 user_nonces: revocation.arkoors.iter()
1929 .map(|i| i.user_nonce.serialize().to_vec())
1930 .collect(),
1931 };
1932 let cosign_resp: Vec<_> = srv.client.revoke_lightning_payment(req).await?
1933 .into_inner().try_into().context("invalid server cosign response")?;
1934 ensure!(revocation.verify_cosign_response(&cosign_resp),
1935 "invalid arkoor cosignature received from server",
1936 );
1937
1938 let (vtxos, _) = revocation.build_vtxos(&cosign_resp, &keypairs, secs)?;
1939 for vtxo in &vtxos {
1940 info!("Got revocation VTXO: {}: {}", vtxo.id(), vtxo.amount());
1941 }
1942
1943 self.db.register_movement(MovementArgs {
1944 kind: MovementKind::LightningSendRevocation,
1945 spends: &htlc_vtxos.iter().collect::<Vec<_>>(),
1946 receives: &vtxos.iter().map(|v| (v, VtxoState::Spendable)).collect::<Vec<_>>(),
1947 recipients: &[],
1948 fees: None,
1949 })?;
1950
1951 self.db.remove_pending_lightning_send(payment.invoice.payment_hash())?;
1952
1953 info!("Revoked {} HTLC VTXOs", vtxos.len());
1954
1955 Ok(())
1956 }
1957
1958 pub async fn send_lightning_payment(
1961 &self,
1962 invoice: Invoice,
1963 user_amount: Option<Amount>,
1964 ) -> anyhow::Result<Preimage> {
1965 let mut srv = self.require_server()?;
1966 let properties = self.db.read_properties()?.context("Missing config")?;
1967
1968 if invoice.network() != properties.network {
1969 bail!("Invoice is for wrong network: {}", invoice.network());
1970 }
1971
1972 if self.db.check_recipient_exists(&invoice.to_string())? {
1973 bail!("Invoice has already been paid");
1974 }
1975
1976 invoice.check_signature()?;
1977
1978 let inv_amount = invoice.amount_msat().map(|v| Amount::from_msat_ceil(v));
1979 if let (Some(_), Some(inv)) = (user_amount, inv_amount) {
1980 bail!("Invoice has amount of {} encoded. Please omit user amount argument", inv);
1981 }
1982
1983 let amount = user_amount.or(inv_amount)
1984 .context("amount required on invoice without amount")?;
1985 if amount < P2TR_DUST {
1986 bail!("Sent amount must be at least {}", P2TR_DUST);
1987 }
1988
1989 let (change_keypair, _) = self.derive_store_next_keypair()?;
1990
1991 let inputs = self.select_vtxos_to_cover(amount, Some(srv.info.max_arkoor_depth), None)
1992 .context("Could not find enough suitable VTXOs to cover lightning payment")?;
1993
1994 let mut secs = Vec::with_capacity(inputs.len());
1995 let mut pubs = Vec::with_capacity(inputs.len());
1996 let mut keypairs = Vec::with_capacity(inputs.len());
1997 for input in inputs.iter() {
1998 let keypair = self.get_vtxo_key(&input)?;
1999 let (s, p) = musig::nonce_pair(&keypair);
2000 secs.push(s);
2001 pubs.push(p);
2002 keypairs.push(keypair);
2003 }
2004
2005 let req = protos::StartLightningPaymentRequest {
2006 invoice: invoice.to_string(),
2007 user_amount_sat: user_amount.map(|a| a.to_sat()),
2008 input_vtxo_ids: inputs.iter().map(|v| v.id().to_bytes().to_vec()).collect(),
2009 user_nonces: pubs.iter().map(|p| p.serialize().to_vec()).collect(),
2010 user_pubkey: change_keypair.public_key().serialize().to_vec(),
2011 };
2012
2013 let resp = srv.client.start_lightning_payment(req).await
2014 .context("htlc request failed")?.into_inner();
2015
2016 let cosign_resp = resp.sigs.into_iter().map(|i| i.try_into())
2017 .collect::<Result<Vec<_>, _>>()?;
2018 let policy = VtxoPolicy::from_bytes(&resp.policy)?;
2019
2020 let pay_req = match policy {
2021 VtxoPolicy::ServerHtlcSend(policy) => {
2022 ensure!(policy.user_pubkey == change_keypair.public_key(), "user pubkey mismatch");
2023 ensure!(policy.payment_hash == invoice.payment_hash(), "payment hash mismatch");
2024 VtxoRequest { amount: amount, policy: policy.into() }
2026 },
2027 _ => bail!("invalid policy returned from server"),
2028 };
2029
2030 let builder = ArkoorPackageBuilder::new(
2031 &inputs, &pubs, pay_req, Some(change_keypair.public_key()),
2032 )?;
2033
2034 ensure!(builder.verify_cosign_response(&cosign_resp),
2035 "invalid arkoor cosignature received from server",
2036 );
2037
2038 let (htlc_vtxos, change_vtxo) = builder.build_vtxos(&cosign_resp, &keypairs, secs)?;
2039
2040 for (vtxo, input) in htlc_vtxos.iter().zip(inputs.iter()) {
2042 if let Ok(tx) = self.chain.get_tx(&input.chain_anchor().txid).await {
2043 let tx = tx.with_context(|| {
2044 format!("input vtxo chain anchor not found for lightning send htlc vtxo: {}", input.chain_anchor().txid)
2045 })?;
2046 vtxo.validate(&tx).context("invalid lightning htlc vtxo")?;
2047 } else {
2048 warn!("We couldn't validate the new VTXOs because of chain source error.");
2049 }
2050 }
2051
2052 if let Some(ref change) = change_vtxo {
2054 let last_input = inputs.last().context("no inputs provided")?;
2055 let tx = self.chain.get_tx(&last_input.chain_anchor().txid).await?;
2056 let tx = tx.with_context(|| {
2057 format!("input vtxo chain anchor not found for lightning change vtxo: {}", last_input.chain_anchor().txid)
2058 })?;
2059 change.validate(&tx).context("invalid lightning change vtxo")?;
2060 }
2061
2062 self.db.register_movement(MovementArgs {
2063 kind: MovementKind::LightningSend,
2064 spends: &inputs.iter().collect::<Vec<_>>(),
2065 receives: &htlc_vtxos.iter()
2066 .map(|v| (v, VtxoState::Locked))
2067 .chain(change_vtxo.as_ref().map(|c| (c, VtxoState::Spendable)))
2068 .collect::<Vec<_>>(),
2069 recipients: &[],
2070 fees: None,
2071 }).context("failed to store OOR vtxo")?;
2072
2073 let payment = self.db.store_new_pending_lightning_send(
2074 &invoice, &amount, &htlc_vtxos.iter().map(|v| v.id()).collect::<Vec<_>>(),
2075 )?;
2076
2077 let req = protos::SignedLightningPaymentDetails {
2078 invoice: invoice.to_string(),
2079 htlc_vtxo_ids: htlc_vtxos.iter().map(|v| v.id().to_bytes().to_vec()).collect(),
2080 wait: true,
2081 };
2082
2083 let res = srv.client.finish_lightning_payment(req).await?.into_inner();
2084 debug!("Progress update: {}", res.progress_message);
2085 let payment_preimage = Preimage::try_from(res.payment_preimage()).ok();
2086
2087 if let Some(preimage) = payment_preimage {
2088 info!("Payment succeeded! Preimage: {}", preimage.as_hex());
2089 self.db.register_movement(MovementArgs {
2090 kind: MovementKind::LightningSend,
2091 spends: &htlc_vtxos.iter().collect::<Vec<_>>(),
2092 receives: &[],
2093 recipients: &[(&invoice.to_string(), amount)],
2094 fees: None,
2095 }).context("failed to store OOR vtxo")?;
2096
2097 self.db.remove_pending_lightning_send(payment.invoice.payment_hash())?;
2098 Ok(preimage)
2099 } else {
2100 self.process_lightning_revocation(&payment).await?;
2101 bail!("No preimage, payment failed: {}", res.progress_message);
2102 }
2103 }
2104
2105 pub async fn check_lightning_payment(&self, payment: &PendingLightningSend)
2132 -> anyhow::Result<Option<Preimage>>
2133 {
2134 let mut srv = self.require_server()?;
2135 let tip = self.chain.tip().await?;
2136
2137 let payment_hash = payment.invoice.payment_hash();
2138
2139 let policy = payment.htlc_vtxos.first().context("no vtxo provided")?.vtxo.policy();
2140 debug_assert!(payment.htlc_vtxos.iter().all(|v| v.vtxo.policy() == policy),
2141 "All lightning htlc should have the same policy",
2142 );
2143 let policy = policy.as_server_htlc_send().context("VTXO is not an HTLC send")?;
2144 if policy.payment_hash != payment_hash {
2145 bail!("Payment hash mismatch");
2146 }
2147
2148 let req = protos::CheckLightningPaymentRequest {
2149 hash: policy.payment_hash.to_vec(),
2150 wait: false,
2151 };
2152 let res = srv.client.check_lightning_payment(req).await?.into_inner();
2153
2154 let payment_status = protos::PaymentStatus::try_from(res.status)?;
2155
2156 let should_revoke = match payment_status {
2157 protos::PaymentStatus::Failed => {
2158 info!("Payment failed ({}): revoking VTXO", res.progress_message);
2159 true
2160 },
2161 protos::PaymentStatus::Pending => {
2162 trace!("Payment is still pending, HTLC expiry: {}, tip: {}",
2163 policy.htlc_expiry, tip);
2164 if tip > policy.htlc_expiry {
2165 info!("Payment is still pending, but HTLC is expired: revoking VTXO");
2166 true
2167 } else {
2168 info!("Payment is still pending and HTLC is not expired ({}): \
2169 doing nothing for now", policy.htlc_expiry,
2170 );
2171 false
2172 }
2173 },
2174 protos::PaymentStatus::Complete => {
2175 let preimage: Preimage = res.payment_preimage
2176 .context("payment completed but no preimage")?
2177 .try_into().map_err(|_| anyhow!("preimage is not 32 bytes"))?;
2178 info!("Payment is complete, preimage, {}", preimage.as_hex());
2179
2180 self.db.register_movement(MovementArgs {
2181 kind: MovementKind::LightningSend,
2182 spends: &payment.htlc_vtxos.iter().map(|v| &v.vtxo).collect::<Vec<_>>(),
2183 receives: &[],
2184 recipients: &[(&payment.invoice.to_string(), payment.amount)],
2185 fees: None,
2186 }).context("failed to store OOR vtxo")?;
2187
2188 self.db.remove_pending_lightning_send(payment_hash)?;
2189
2190 return Ok(Some(preimage));
2191 },
2192 };
2193
2194 if should_revoke {
2195 if let Err(e) = self.process_lightning_revocation(payment).await {
2196 warn!("Failed to revoke VTXO: {}", e);
2197
2198 let min_expiry = payment.htlc_vtxos.iter()
2202 .map(|v| v.vtxo.spec().expiry_height).min().unwrap();
2203
2204 if tip > min_expiry.saturating_sub(self.config().vtxo_refresh_expiry_threshold) {
2205 warn!("Some VTXO is about to expire soon, marking to exit");
2206 let vtxos = payment.htlc_vtxos
2207 .iter()
2208 .map(|v| v.vtxo.clone())
2209 .collect::<Vec<_>>();
2210 self.exit.write().await.mark_vtxos_for_exit(&vtxos);
2211
2212 self.db.remove_pending_lightning_send(payment_hash)?;
2213 }
2214 }
2215 }
2216
2217 Ok(None)
2218 }
2219
2220 pub async fn bolt11_invoice(&self, amount: Amount) -> anyhow::Result<Bolt11Invoice> {
2222 let mut srv = self.require_server()?;
2223 let config = self.config();
2224
2225 let requested_min_cltv_delta = srv.info.vtxo_exit_delta +
2230 srv.info.htlc_expiry_delta +
2231 config.vtxo_exit_margin +
2232 config.htlc_recv_claim_delta +
2233 LIGHTNING_PREPARE_CLAIM_DELTA;
2234
2235 if requested_min_cltv_delta > srv.info.max_user_invoice_cltv_delta {
2236 bail!("HTLC CLTV delta ({}) is greater than Server's max HTLC recv CLTV delta: {}",
2237 requested_min_cltv_delta,
2238 srv.info.max_user_invoice_cltv_delta,
2239 );
2240 }
2241
2242 let preimage = Preimage::random();
2243 let payment_hash = preimage.compute_payment_hash();
2244 info!("Start bolt11 board with preimage / payment hash: {} / {}",
2245 preimage.as_hex(), payment_hash.as_hex());
2246
2247 let req = protos::StartLightningReceiveRequest {
2248 payment_hash: payment_hash.to_vec(),
2249 amount_sat: amount.to_sat(),
2250 min_cltv_delta: requested_min_cltv_delta as u32,
2251 };
2252
2253 let resp = srv.client.start_lightning_receive(req).await?.into_inner();
2254 info!("Ark Server is ready to receive LN payment to invoice: {}.", resp.bolt11);
2255
2256 let invoice = Bolt11Invoice::from_str(&resp.bolt11)
2257 .context("invalid bolt11 invoice returned by Ark server")?;
2258
2259 self.db.store_lightning_receive(
2260 payment_hash,
2261 preimage,
2262 &invoice,
2263 requested_min_cltv_delta,
2264 )?;
2265
2266 Ok(invoice)
2267 }
2268
2269 pub fn lightning_receive_status(
2271 &self,
2272 payment: impl Into<PaymentHash>,
2273 ) -> anyhow::Result<Option<LightningReceive>> {
2274 Ok(self.db.fetch_lightning_receive_by_payment_hash(payment.into())?)
2275 }
2276
2277 pub fn pending_lightning_receives(&self) -> anyhow::Result<Vec<LightningReceive>> {
2279 Ok(self.db.get_all_pending_lightning_receives()?)
2280 }
2281
2282 pub fn pending_lightning_receive_balance(&self) -> anyhow::Result<LightningReceiveBalance> {
2283 let pending_lightning_receives = self.pending_lightning_receives()?;
2284
2285 let mut total_pending_lightning_receive = Amount::ZERO;
2286 let mut claimable_pending_lightning_receive = Amount::ZERO;
2287 for receive in pending_lightning_receives {
2288 total_pending_lightning_receive += receive.invoice.amount_milli_satoshis()
2289 .map(|a| Amount::from_msat_floor(a))
2290 .expect("ln receive invoice should have amount");
2291 if let Some(htlc_vtxos) = receive.htlc_vtxos {
2292 claimable_pending_lightning_receive += htlc_vtxos.iter().map(|v| v.amount()).sum::<Amount>();
2293 }
2294 }
2295
2296 Ok(LightningReceiveBalance {
2297 total: total_pending_lightning_receive,
2298 claimable: claimable_pending_lightning_receive,
2299 })
2300 }
2301
2302 async fn claim_ln_receive(
2325 &self,
2326 lightning_receive: &LightningReceive,
2327 ) -> anyhow::Result<()> {
2328 let mut srv = self.require_server()?;
2329
2330 let inputs = {
2332 let htlc_vtxos = lightning_receive.htlc_vtxos.as_ref()
2333 .context("no HTLC VTXOs set on record yet")?;
2334 let mut ret = htlc_vtxos.iter().map(|v| &v.vtxo).collect::<Vec<_>>();
2335 ret.sort_by_key(|v| v.id());
2336 ret
2337 };
2338
2339 let (keypairs, sec_nonces, pub_nonces) = inputs.iter().map(|v| {
2340 let keypair = self.get_vtxo_key(v)?;
2341 let (sec_nonce, pub_nonce) = musig::nonce_pair(&keypair);
2342 Ok((keypair, sec_nonce, pub_nonce))
2343 }).collect::<anyhow::Result<(Vec<_>, Vec<_>, Vec<_>)>>()?;
2344
2345 let (claim_keypair, _) = self.derive_store_next_keypair()?;
2347 let receive_policy = VtxoPolicy::new_pubkey(claim_keypair.public_key());
2348
2349 let pay_req = VtxoRequest {
2350 policy: receive_policy.clone(),
2351 amount: inputs.iter().map(|v| v.amount()).sum(),
2352 };
2353 trace!("ln arkoor builder params: inputs: {:?}; user_nonces: {:?}; req: {:?}",
2354 inputs.iter().map(|v| v.id()).collect::<Vec<_>>(), pub_nonces, pay_req,
2355 );
2356 let builder = ArkoorPackageBuilder::new(
2357 inputs.iter().copied(), &pub_nonces, pay_req, None,
2358 )?;
2359
2360 info!("Claiming arkoor against payment preimage");
2361 self.db.set_preimage_revealed(lightning_receive.payment_hash)?;
2362 let resp = srv.client.claim_lightning_receive(protos::ClaimLightningReceiveRequest {
2363 payment_hash: lightning_receive.payment_hash.to_byte_array().to_vec(),
2364 payment_preimage: lightning_receive.payment_preimage.to_vec(),
2365 vtxo_policy: receive_policy.serialize(),
2366 user_pub_nonces: pub_nonces.iter().map(|n| n.serialize().to_vec()).collect(),
2367 }).await?.into_inner();
2368 let cosign_resp: Vec<_> = resp.try_into().context("invalid cosign response")?;
2369
2370 ensure!(builder.verify_cosign_response(&cosign_resp),
2371 "invalid arkoor cosignature received from server",
2372 );
2373
2374 let (outputs, change) = builder.build_vtxos(&cosign_resp, &keypairs, sec_nonces)?;
2375 if change.is_some() {
2376 bail!("shouldn't have change VTXO, this is a bug");
2377 }
2378
2379 for (vtxo, input) in outputs.iter().zip(inputs.iter()) {
2380 if let Ok(tx) = self.chain.get_tx(&input.chain_anchor().txid).await {
2381 let tx = tx.with_context(|| {
2382 format!("input vtxo chain anchor not found for lightning receive vtxo: {}", input.chain_anchor().txid)
2383 })?;
2384 vtxo.validate(&tx).context("invalid lightning receive htlc vtxo")?;
2385 } else {
2386 warn!("We couldn't validate the new VTXOs because of chain source error.");
2387 }
2388 }
2389
2390 info!("Got arkoors from lightning: {}",
2391 outputs.iter().map(|v| v.id().to_string()).collect::<Vec<_>>().join(", "));
2392 self.db.register_movement(MovementArgs {
2393 kind: MovementKind::LightningReceive,
2394 spends: &inputs,
2395 receives: &outputs.iter().map(|v| (v, VtxoState::Spendable)).collect::<Vec<_>>(),
2396 recipients: &[],
2397 fees: None,
2398 })?;
2399
2400 self.db.remove_pending_lightning_receive(lightning_receive.payment_hash)?;
2401
2402 Ok(())
2403 }
2404
2405 pub async fn check_ln_receive(
2432 &self,
2433 payment_hash: PaymentHash,
2434 wait: bool,
2435 ) -> anyhow::Result<LightningReceive> {
2436 let mut srv = self.require_server()?;
2437 let current_height = self.chain.tip().await?;
2438
2439 let mut lightning_receive = self.db.fetch_lightning_receive_by_payment_hash(payment_hash)?
2440 .context("no lightning receive found")?;
2441
2442 info!("Waiting for payment...");
2443 let sub = srv.client.check_lightning_receive(protos::CheckLightningReceiveRequest {
2444 hash: payment_hash.to_byte_array().to_vec(), wait,
2445 }).await?.into_inner();
2446
2447 let status = protos::LightningReceiveStatus::try_from(sub.status)
2448 .with_context(|| format!("unknown payment status: {}", sub.status))?;
2449 match status {
2450 protos::LightningReceiveStatus::Accepted
2452 | protos::LightningReceiveStatus::HtlcsReady => {},
2453 protos::LightningReceiveStatus::Created => bail!("sender didn't initiate payment yet"),
2454 protos::LightningReceiveStatus::Settled => bail!("payment already settled"),
2455 protos::LightningReceiveStatus::Cancelled => bail!("payment was canceled"),
2456 }
2457
2458 if let Some(vtxos) = &lightning_receive.htlc_vtxos {
2460 debug_assert!({
2461 let vtxos_by_id = vtxos.iter().map(|v| (v.id(), v)).collect::<HashMap<_, _>>();
2462 sub.htlc_vtxos.iter().all(|v| {
2463 match VtxoId::from_slice(v) {
2464 Ok(id) => vtxos_by_id.contains_key(&id),
2465 Err(_) => false,
2466 }
2467 })
2468 }, "server sent HTLC VTXOs that we don't have");
2469
2470 return Ok(lightning_receive)
2471 }
2472
2473 let htlc_recv_expiry = current_height + lightning_receive.htlc_recv_cltv_delta as BlockHeight;
2474
2475 let (keypair, _) = self.derive_store_next_keypair()?;
2476 let req = protos::PrepareLightningReceiveClaimRequest {
2477 payment_hash: lightning_receive.payment_hash.to_vec(),
2478 user_pubkey: keypair.public_key().serialize().to_vec(),
2479 htlc_recv_expiry: htlc_recv_expiry,
2480 };
2481 let res = srv.client.prepare_lightning_receive_claim(req).await
2482 .context("error preparing lightning receive claim")?.into_inner();
2483 let vtxos = res.htlc_vtxos.into_iter()
2484 .map(|b| Vtxo::deserialize(&b))
2485 .collect::<Result<Vec<_>, _>>()
2486 .context("invalid htlc vtxos from server")?;
2487
2488 for vtxo in &vtxos {
2490 if let VtxoPolicy::ServerHtlcRecv(p) = vtxo.policy() {
2491 if p.payment_hash != lightning_receive.payment_hash {
2492 bail!("invalid payment hash on HTLC VTXOs received from server: {}",
2493 p.payment_hash,
2494 );
2495 }
2496 if p.user_pubkey != keypair.public_key() {
2497 bail!("invalid pubkey on HTLC VTXOs received from server: {}", p.user_pubkey);
2498 }
2499 if p.htlc_expiry < htlc_recv_expiry {
2500 bail!("HTLC VTXO expiry height is less than requested: Requested {}, received {}", htlc_recv_expiry, p.htlc_expiry);
2501 }
2502 } else {
2503 bail!("invalid HTLC VTXO policy: {:?}", vtxo.policy());
2504 }
2505 }
2506
2507 let invoice_amount = lightning_receive.invoice.amount_milli_satoshis().map(|a| Amount::from_msat_floor(a))
2509 .expect("ln receive invoice should have amount");
2510 ensure!(vtxos.iter().map(|v| v.amount()).sum::<Amount>() >= invoice_amount,
2511 "Server didn't return enough VTXOs to cover invoice amount"
2512 );
2513
2514 let vtxos = vtxos.into_iter().map(|v| WalletVtxo {
2515 vtxo: v.clone(),
2516 state: VtxoState::Locked,
2517 }).collect::<Vec<_>>();
2518
2519 self.db.register_movement(MovementArgs {
2520 kind: MovementKind::LightningReceive,
2521 spends: &[],
2522 receives: &vtxos.iter().map(|v| (&v.vtxo, v.state.clone())).collect::<Vec<_>>(),
2523 recipients: &[],
2524 fees: None,
2525 })?;
2526
2527 let vtxo_ids = vtxos.iter().map(|v| v.id()).collect::<Vec<_>>();
2528 self.db.set_lightning_receive_vtxos(payment_hash, &vtxo_ids)?;
2529
2530 lightning_receive.htlc_vtxos = Some(vtxos);
2531
2532 Ok(lightning_receive)
2533 }
2534
2535 pub async fn check_and_claim_ln_receive(
2557 &self,
2558 payment_hash: PaymentHash,
2559 wait: bool,
2560 ) -> anyhow::Result<()> {
2561 let receive = self.check_ln_receive(payment_hash, wait).await?;
2562 self.claim_ln_receive(&receive).await
2563 }
2564
2565 pub async fn check_and_claim_all_open_ln_receives(&self, wait: bool) -> anyhow::Result<()> {
2580 tokio_stream::iter(self.pending_lightning_receives()?)
2582 .for_each_concurrent(3, |rcv| async move {
2583 if let Err(e) = self.check_and_claim_ln_receive(rcv.invoice.into(), wait).await {
2584 error!("Error claiming lightning receive: {}", e);
2585 }
2586 }).await;
2587
2588 Ok(())
2589 }
2590
2591 async fn claim_all_pending_htlc_recvs(&self) -> anyhow::Result<()> {
2602 let srv = self.require_server()?;
2603 let tip = self.chain.tip().await?;
2604 let lightning_receives = self.db.get_all_pending_lightning_receives()?;
2605 info!("Syncing {} pending lightning receives", lightning_receives.len());
2606
2607 for lightning_receive in lightning_receives {
2608 let vtxos = match &lightning_receive.htlc_vtxos {
2609 Some(vtxos) => vtxos,
2610 None => continue,
2611 };
2612
2613 if let Err(e) = self.claim_ln_receive(&lightning_receive).await {
2614 error!("Failed to claim pubkey vtxo from htlc vtxo: {}", e);
2615
2616 let first_vtxo = &vtxos.first().unwrap().vtxo;
2617 debug_assert!(vtxos.iter().all(|v| {
2618 v.vtxo.policy() == first_vtxo.policy() && v.vtxo.exit_delta() == first_vtxo.exit_delta()
2619 }), "all htlc vtxos for the same payment hash should have the same policy and exit delta");
2620
2621 let vtxo_htlc_expiry = first_vtxo.policy().as_server_htlc_recv()
2622 .expect("only server htlc recv vtxos can be pending lightning recv").htlc_expiry;
2623
2624 let safe_exit_margin = first_vtxo.exit_delta() +
2625 srv.info.htlc_expiry_delta +
2626 self.config.vtxo_exit_margin;
2627
2628 if tip > vtxo_htlc_expiry.saturating_sub(safe_exit_margin as BlockHeight) {
2629 if lightning_receive.preimage_revealed_at.is_some() {
2630 warn!("HTLC-recv VTXOs are about to expire and preimage has been disclosed, must exit");
2631 self.exit.write().await.mark_vtxos_for_exit(&vtxos.iter().map(|v| v.vtxo.clone()).collect::<Vec<_>>());
2632 } else {
2633 warn!("HTLC-recv VTXOs are about to expire, but preimage has not been disclosed yet, mark htlc as cancelled");
2634 self.db.register_movement(MovementArgs {
2635 kind: MovementKind::LightningReceive,
2636 spends: &vtxos.iter().map(|v| &v.vtxo).collect::<Vec<_>>(),
2637 receives: &[],
2638 recipients: &[],
2639 fees: None,
2640 })?;
2641 }
2642 }
2643 }
2644 }
2645
2646 Ok(())
2647 }
2648
2649 pub async fn send_lnaddr(
2651 &self,
2652 addr: &LightningAddress,
2653 amount: Amount,
2654 comment: Option<&str>,
2655 ) -> anyhow::Result<(Bolt11Invoice, Preimage)> {
2656 let invoice = lnurl::lnaddr_invoice(addr, amount, comment).await
2657 .context("lightning address error")?;
2658 info!("Attempting to pay invoice {}", invoice);
2659 let preimage = self.send_lightning_payment(Invoice::Bolt11(invoice.clone()), None).await
2660 .context("bolt11 payment error")?;
2661 Ok((invoice, preimage))
2662 }
2663
2664 pub async fn pay_offer(
2666 &self,
2667 offer: Offer,
2668 amount: Option<Amount>,
2669 ) -> anyhow::Result<(Bolt12Invoice, Preimage)> {
2670 let mut srv = self.require_server()?;
2671
2672 let offer_bytes = {
2673 let mut bytes = Vec::new();
2674 offer.write(&mut bytes).unwrap();
2675 bytes
2676 };
2677
2678 let req = protos::FetchBolt12InvoiceRequest {
2679 offer: offer_bytes,
2680 amount_sat: amount.map(|a| a.to_sat()),
2681 };
2682
2683 let resp = srv.client.fetch_bolt12_invoice(req).await?.into_inner();
2684
2685 let invoice = Bolt12Invoice::try_from(resp.invoice)
2686 .map_err(|_| anyhow::anyhow!("invalid invoice"))?;
2687
2688 invoice.validate_issuance(offer)?;
2689
2690 let preimage = self.send_lightning_payment(Invoice::Bolt12(invoice.clone()), None).await
2691 .context("bolt11 payment error")?;
2692 Ok((invoice, preimage))
2693 }
2694
2695 pub async fn send_round_onchain_payment(
2698 &self,
2699 addr: bitcoin::Address,
2700 amount: Amount,
2701 ) -> anyhow::Result<Offboard> {
2702 let balance = self.balance()?.spendable;
2703
2704 let early_fees = OffboardRequest::calculate_fee(
2706 &addr.script_pubkey(), FeeRate::BROADCAST_MIN,
2707 ).expect("script from address");
2708
2709 if balance < amount + early_fees {
2710 bail!("Your balance is too low. Needed: {}, available: {}",
2711 amount + early_fees, balance,
2712 );
2713 }
2714
2715 let participation = DesiredRoundParticipation::OnchainPayment {
2716 destination: addr.script_pubkey(),
2717 amount,
2718 };
2719 let RoundResult { round_id, .. } = self.participate_round(participation).await
2720 .context("round failed")?;
2721
2722 Ok(Offboard { round: round_id })
2723 }
2724}