Skip to content

Commit

Permalink
offers: send invoice request
Browse files Browse the repository at this point in the history
  • Loading branch information
orbitalturtle committed Feb 7, 2024
1 parent f82d925 commit 68ed737
Show file tree
Hide file tree
Showing 6 changed files with 380 additions and 15 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 31 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ mod rate_limit;
use crate::lnd::{
features_support_onion_messages, get_lnd_client, string_to_network, LndCfg, LndNodeSigner,
};

use crate::lndk_offers::OfferError;
use crate::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,
};
Expand All @@ -24,8 +27,9 @@ use log4rs::encode::pattern::PatternEncoder;
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::{Mutex, Once};
use tokio::sync::mpsc::Sender;
use tokio::sync::mpsc::{Receiver, Sender};
use tonic_lnd::lnrpc::GetInfoRequest;
use tonic_lnd::Client;
use triggered::{Listener, Trigger};

static INIT: Once = Once::new();
Expand Down Expand Up @@ -175,19 +179,41 @@ enum OfferState {
}

pub struct OfferHandler {
_active_offers: Mutex<HashMap<String, OfferState>>,
active_offers: Mutex<HashMap<String, OfferState>>,
pending_messages: Mutex<Vec<PendingOnionMessage<OffersMessage>>>,
messenger_utils: MessengerUtilities,
}

#[derive(Clone)]
pub struct PayOfferParams {
pub offer: Offer,
pub amount: Option<u64>,
pub network: Network,
pub client: Client,
/// The blinded path the offer creator provided, which we will use to send the invoice request.
pub blinded_path: BlindedPath,
/// The path we will send back to the offer creator, so it knows where to send back the invoice.
pub reply_path: Option<BlindedPath>,
}

impl OfferHandler {
pub fn new() -> Self {
OfferHandler {
_active_offers: Mutex::new(HashMap::new()),
active_offers: Mutex::new(HashMap::new()),
pending_messages: Mutex::new(Vec::new()),
messenger_utils: MessengerUtilities::new(),
}
}

/// Adds an offer to be paid with the amount specified. May only be called once for a single offer.
pub async fn pay_offer(
&self,
cfg: PayOfferParams,
started: Receiver<u32>,
) -> Result<(), OfferError<Secp256k1Error>> {
self.send_invoice_request(cfg, started).await?;
Ok(())
}
}

impl Default for OfferHandler {
Expand Down
3 changes: 2 additions & 1 deletion src/lnd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ pub(crate) fn get_lnd_client(cfg: LndCfg) -> Result<Client, ConnectError> {
}

/// LndCfg specifies the configuration required to connect to LND's grpc client.
#[derive(Clone)]
pub struct LndCfg {
address: String,
cert: PathBuf,
Expand Down Expand Up @@ -186,7 +187,7 @@ pub(crate) fn string_to_network(network_str: &str) -> Result<Network, NetworkPar

/// MessageSigner provides a layer of abstraction over the LND API for message signing.
#[async_trait]
pub(crate) trait MessageSigner {
pub trait MessageSigner {
async fn derive_key(&mut self, key_loc: KeyLocator) -> Result<Vec<u8>, Status>;
async fn sign_message(
&mut self,
Expand Down
152 changes: 148 additions & 4 deletions src/lndk_offers.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::lnd::{features_support_onion_messages, MessageSigner, PeerConnector};
use crate::OfferHandler;
use crate::{OfferHandler, OfferState, PayOfferParams};
use async_trait::async_trait;
use bitcoin::hashes::sha256::Hash;
use bitcoin::network::constants::Network;
Expand All @@ -11,12 +11,14 @@ use lightning::offers::invoice_request::{InvoiceRequest, UnsignedInvoiceRequest}
use lightning::offers::merkle::SignError;
use lightning::offers::offer::{Amount, Offer};
use lightning::offers::parse::{Bolt12ParseError, Bolt12SemanticError};
use lightning::onion_message::{Destination, OffersMessage, PendingOnionMessage};
use log::error;
use std::error::Error;
use std::fmt::Display;
use std::str::FromStr;
use tokio::sync::mpsc::Receiver;
use tokio::task;
use tonic_lnd::lnrpc::{LightningNode, ListPeersRequest, ListPeersResponse};
use tonic_lnd::lnrpc::{GetInfoRequest, LightningNode, ListPeersRequest, ListPeersResponse};
use tonic_lnd::signrpc::{KeyLocator, SignMessageReq};
use tonic_lnd::tonic::Status;
use tonic_lnd::Client;
Expand Down Expand Up @@ -76,9 +78,71 @@ pub fn decode(offer_str: String) -> Result<Offer, Bolt12ParseError> {
}

impl OfferHandler {
#[allow(dead_code)]
pub async fn send_invoice_request(
&self,
mut cfg: PayOfferParams,
mut started: Receiver<u32>,
) -> Result<(), OfferError<bitcoin::secp256k1::Error>> {
// Wait for onion messenger to give us the signal that it's ready. Once the onion messenger drops
// the channel sender, recv will return None and we'll stop blocking here.
while (started.recv().await).is_some() {
println!("Error: we shouldn't receive any messages on this channel");
}

let validated_amount = validate_amount(&cfg.offer, cfg.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(cfg.client.clone(), cfg.blinded_path.introduction_node_id).await?;

let offer_id = cfg.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(cfg.offer.to_string().clone(), OfferState::OfferAdded);
}

let invoice_request = self
.create_invoice_request(
cfg.client.clone(),
cfg.offer,
vec![],
cfg.network,
validated_amount,
)
.await?;

if cfg.reply_path.is_none() {
let info = cfg
.client
.lightning()
.get_info(GetInfoRequest {})
.await
.expect("failed to get info")
.into_inner();

let pubkey = PublicKey::from_str(&info.identity_pubkey).unwrap();
cfg.reply_path = Some(self.create_reply_path(cfg.client.clone(), pubkey).await?)
};
let contents = OffersMessage::InvoiceRequest(invoice_request);
let pending_message = PendingOnionMessage {
contents,
destination: Destination::BlindedPath(cfg.blinded_path.clone()),
reply_path: cfg.reply_path,
};

let mut pending_messages = self.pending_messages.lock().unwrap();
pending_messages.push(pending_message);
std::mem::drop(pending_messages);

Ok(())
}

// create_invoice_request builds and signs an invoice request, the first step in the BOLT 12 process of paying an offer.
pub(crate) async fn create_invoice_request(
pub async fn create_invoice_request(
&self,
mut signer: impl MessageSigner + std::marker::Send + 'static,
offer: Offer,
Expand Down Expand Up @@ -348,6 +412,10 @@ mod tests {
use std::time::{Duration, SystemTime};
use tonic_lnd::lnrpc::NodeAddress;

fn get_offer() -> String {
"lno1qgsqvgnwgcg35z6ee2h3yczraddm72xrfua9uve2rlrm9deu7xyfzrcgqgn3qzsyvfkx26qkyypvr5hfx60h9w9k934lt8s2n6zc0wwtgqlulw7dythr83dqx8tzumg".to_string()
}

fn build_custom_offer(amount_msats: u64) -> Offer {
let secp_ctx = Secp256k1::new();
let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap());
Expand All @@ -367,6 +435,10 @@ mod tests {
"0313ba7ccbd754c117962b9afab6c2870eb3ef43f364a9f6c43d0fabb4553776ba".to_string()
}

fn get_signature() -> String {
"28b937976a29c15827433086440b36c2bec6ca5bd977557972dca8641cd59ffba50daafb8ee99a19c950976b46f47d9e7aa716652e5657dfc555b82eff467f18".to_string()
}

mock! {
TestBolt12Signer{}

Expand All @@ -388,6 +460,78 @@ mod tests {
}
}

#[tokio::test]
async fn test_request_invoice() {
let mut signer_mock = MockTestBolt12Signer::new();

signer_mock.expect_derive_key().returning(|_| {
Ok(PublicKey::from_str(&get_pubkey())
.unwrap()
.serialize()
.to_vec())
});

signer_mock.expect_sign_message().returning(|_, _, _| {
Ok(Signature::from_str(&get_signature())
.unwrap()
.as_ref()
.to_vec())
});

let offer = decode(get_offer()).unwrap();
let handler = OfferHandler::new();
assert!(handler
.create_invoice_request(signer_mock, offer, vec![], Network::Regtest, 10000)
.await
.is_ok())
}

#[tokio::test]
async fn test_request_invoice_derive_key_error() {
let mut signer_mock = MockTestBolt12Signer::new();

signer_mock
.expect_derive_key()
.returning(|_| Err(Status::unknown("error testing")));

signer_mock.expect_sign_message().returning(|_, _, _| {
Ok(Signature::from_str(&get_signature())
.unwrap()
.as_ref()
.to_vec())
});

let offer = decode(get_offer()).unwrap();
let handler = OfferHandler::new();
assert!(handler
.create_invoice_request(signer_mock, offer, vec![], Network::Regtest, 10000)
.await
.is_err())
}

#[tokio::test]
async fn test_request_invoice_signer_error() {
let mut signer_mock = MockTestBolt12Signer::new();

signer_mock.expect_derive_key().returning(|_| {
Ok(PublicKey::from_str(&get_pubkey())
.unwrap()
.serialize()
.to_vec())
});

signer_mock
.expect_sign_message()
.returning(|_, _, _| Err(Status::unknown("error testing")));

let offer = decode(get_offer()).unwrap();
let handler = OfferHandler::new();
assert!(handler
.create_invoice_request(signer_mock, offer, vec![], Network::Regtest, 10000)
.await
.is_err())
}

#[tokio::test]
async fn test_validate_amount() {
// If the amount the user provided is greater than the offer-provided amount, then
Expand Down
2 changes: 1 addition & 1 deletion tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ pub struct LndNode {
pub cert_path: String,
pub macaroon_path: String,
_handle: Child,
client: Option<Client>,
pub client: Option<Client>,
}

impl LndNode {
Expand Down
Loading

0 comments on commit 68ed737

Please sign in to comment.