diff --git a/src/lib.rs b/src/lib.rs index 9a936d9c..b2455454 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,14 +8,17 @@ mod rate_limit; use crate::lnd::{ features_support_onion_messages, get_lnd_client, string_to_network, LndCfg, LndNodeSigner, }; - +use crate::lndk_offers::{connect_to_peer, create_invoice_request, validate_amount, OfferError}; use crate::onion_messenger::{run_onion_messenger, MessengerUtilities}; -use bitcoin::secp256k1::PublicKey; +use bitcoin::network::constants::Network; +use bitcoin::secp256k1::{Error as Secp256k1Error, PublicKey}; use home::home_dir; +use lightning::blinded_path::BlindedPath; use lightning::ln::peer_handler::IgnoringMessageHandler; use lightning::offers::offer::Offer; use lightning::onion_message::{ - DefaultMessageRouter, OffersMessage, OffersMessageHandler, OnionMessenger, PendingOnionMessage, + DefaultMessageRouter, Destination, OffersMessage, OffersMessageHandler, OnionMessenger, + PendingOnionMessage, }; use log::{error, info, LevelFilter}; use log4rs::append::console::ConsoleAppender; @@ -25,9 +28,12 @@ use log4rs::encode::pattern::PatternEncoder; use std::collections::HashMap; use std::str::FromStr; use std::sync::Mutex; +use tokio::time::{sleep, Duration}; use tonic_lnd::lnrpc::GetInfoRequest; +use tonic_lnd::Client; use triggered::{Listener, Trigger}; +#[derive(Clone)] pub struct Cfg { pub lnd: LndCfg, pub log_dir: Option, @@ -37,12 +43,8 @@ pub struct Cfg { pub listener: Listener, } -pub enum OfferError { - OfferAlreadyAdded, -} - #[allow(dead_code)] -enum OfferState { +pub enum OfferState { OfferAdded, InvoiceRequestSent, InvoiceReceived, @@ -52,7 +54,7 @@ enum OfferState { pub struct OfferHandler { #[allow(dead_code)] - active_offers: Mutex>, + active_offers: Mutex>, pending_messages: Mutex>>, } @@ -65,12 +67,54 @@ impl OfferHandler { } /// Adds an offer to be paid with the amount specified. May only be called once for a single offer. - pub fn pay_offer(&mut self, _offer: Offer, _amount: u64) -> Result<(), OfferError> { - /* - Check if we've already added offer -> error. - Add offer to state map. - Create invoice request and push to offer queue. - */ + pub async fn pay_offer( + &self, + offer: Offer, + amount: Option, + network: Network, + client: Client, + blinded_path: BlindedPath, + ) -> Result<(), OfferError> { + sleep(Duration::from_secs(10)).await; + + validate_amount(&offer, amount).await?; + + // For now we connect directly to the introduction node of the blinded path so we don't need any + // intermediate nodes here. In the future we'll query for a full path to the introduction node for + // better sender privacy. + connect_to_peer( + client.clone(), + blinded_path.introduction_node_id, + String::from(""), + ) + .await?; + + let offer_id = offer.clone().to_string(); + { + let mut active_offers = self.active_offers.lock().unwrap(); + if active_offers.contains_key(&offer_id.clone()) { + return Err(OfferError::AlreadyProcessing); + } + active_offers.insert(offer.to_string().clone(), OfferState::OfferAdded); + } + + let invoice_request = + create_invoice_request(client.clone(), offer, vec![], network, 20000).await?; + + let contents = OffersMessage::InvoiceRequest(invoice_request); + let pending_message = PendingOnionMessage { + contents, + destination: Destination::BlindedPath(blinded_path), + reply_path: None, + }; + + let mut pending_messages = self.pending_messages.lock().unwrap(); + pending_messages.push(pending_message); + std::mem::drop(pending_messages); + + let mut active_offers = self.active_offers.lock().unwrap(); + active_offers.insert(offer_id, OfferState::InvoiceRequestSent); + Ok(()) } @@ -104,7 +148,7 @@ impl OfferHandler { ) .unwrap(); - let _log_handle = log4rs::init_config(config).unwrap(); + let _log_handle = log4rs::init_config(config); let mut client = get_lnd_client(args.lnd).expect("failed to connect"); @@ -121,6 +165,7 @@ impl OfferHandler { network_str = Some(chain.network.clone()) } } + if network_str.is_none() { error!("lnd node is not connected to bitcoin network as expected"); return Err(()); @@ -188,7 +233,7 @@ impl OffersMessageHandler for OfferHandler { None } OffersMessage::Invoice(_invoice) => { - // lookup corresponding invoice request / fail if not known + // TODO: lookup corresponding invoice request / fail if not known // Validate invoice for invoice request // Progress state to invoice received // Dispatch payment and update state diff --git a/src/lnd.rs b/src/lnd.rs index 989ed882..504c4b4f 100644 --- a/src/lnd.rs +++ b/src/lnd.rs @@ -29,6 +29,7 @@ pub(crate) fn get_lnd_client(cfg: LndCfg) -> Result { } /// LndCfg specifies the configuration required to connect to LND's grpc client. +#[derive(Clone)] pub struct LndCfg { address: String, cert: PathBuf, @@ -186,7 +187,7 @@ pub(crate) fn string_to_network(network_str: &str) -> Result Result, Status>; async fn sign_message( &mut self, diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 29224a43..dd13cebf 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -151,7 +151,7 @@ pub struct LndNode { pub cert_path: String, pub macaroon_path: String, _handle: Child, - client: Option, + pub client: Option, } impl LndNode { diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index af2b60a6..50ca1afc 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -2,10 +2,13 @@ mod common; use lndk; use bitcoin::secp256k1::PublicKey; +use bitcoin::Network; use chrono::Utc; use ldk_sample::node_api::Node as LdkNode; +use lightning::offers::offer::Quantity; use std::path::PathBuf; use std::str::FromStr; +use std::time::SystemTime; use tokio::select; use tokio::time::{sleep, timeout, Duration}; @@ -33,7 +36,6 @@ async fn wait_to_receive_onion_message( async fn check_for_message(ldk: LdkNode) -> LdkNode { loop { if ldk.onion_message_handler.messages.lock().unwrap().len() == 1 { - println!("MESSAGE: {:?}", ldk.onion_message_handler.messages); return ldk; } sleep(Duration::from_secs(2)).await; @@ -93,3 +95,76 @@ async fn test_lndk_forwards_onion_message() { } } } + +#[tokio::test(flavor = "multi_thread")] +// Here we test the beginning of the BOLT 12 offers flow. We show that lndk successfully builds an +// invoice_request and sends it. +async fn test_lndk_send_invoice_request() { + let test_name = "lndk_send_invoice_request"; + let (_bitcoind, mut lnd, ldk1, ldk2, lndk_dir) = + common::setup_test_infrastructure(test_name).await; + + // Here we'll produce a little network path: + // + // ldk1 <-> ldk2 <-> lnd + // + // ldk1 will be the offer creator, which will build a blinded route from ldk2 to ldk1. + let (pubkey, _) = ldk1.get_node_info(); + let (pubkey_2, addr_2) = ldk2.get_node_info(); + + println!("OTHER PUB KEY {:?}", pubkey.clone().to_string()); + + ldk1.connect_to_peer(pubkey_2, addr_2).await.unwrap(); + lnd.connect_to_peer(pubkey_2, addr_2).await; + + let path_pubkeys = vec![pubkey_2, pubkey]; + let expiration = SystemTime::now() + Duration::from_secs(24 * 60 * 60); + let offer = ldk1 + .create_offer( + &path_pubkeys, + Network::Regtest, + 20_000, + Quantity::One, + expiration, + ) + .await + .expect("should create offer"); + + // Now we'll spin up lndk, which should forward the invoice request to ldk2. + let (shutdown, listener) = triggered::trigger(); + let lnd_cfg = lndk::lnd::LndCfg::new( + lnd.address.clone(), + PathBuf::from_str(&lnd.cert_path).unwrap(), + PathBuf::from_str(&lnd.macaroon_path).unwrap(), + ); + let lndk_cfg = lndk::Cfg { + lnd: lnd_cfg, + log_dir: Some( + lndk_dir + .join(format!("lndk-logs.txt")) + .to_str() + .unwrap() + .to_string(), + ), + shutdown: shutdown.clone(), + listener, + }; + + let client = lnd.client.clone().unwrap(); + let blinded_path = offer.paths()[0].clone(); + + // Make sure lndk successfully sends the invoice_request. + let handler = &mut lndk::OfferHandler::new(); + select! { + val = handler.run(lndk_cfg.clone()) => { + panic!("lndk should not have completed first {:?}", val); + }, + // We wait for ldk2 to receive the onion message. + res = handler.pay_offer(offer.clone(), Some(20_000), Network::Regtest, client.clone(), blinded_path.clone()) => { + assert!(res.is_ok()); + shutdown.trigger(); + ldk1.stop().await; + ldk2.stop().await; + } + } +}