Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions e2e-tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -432,13 +432,15 @@ pub async fn setup_funded_channel(
pub struct RabbitMqEventConsumer {
_connection: lapin::Connection,
channel: lapin::Channel,
queue_name: String,
consumer: lapin::Consumer,
}

impl RabbitMqEventConsumer {
/// Connect to RabbitMQ and create an exclusive queue bound to the given exchange.
pub async fn new(exchange_name: &str) -> Self {
use lapin::options::{ExchangeDeclareOptions, QueueBindOptions, QueueDeclareOptions};
use lapin::options::{
BasicConsumeOptions, ExchangeDeclareOptions, QueueBindOptions, QueueDeclareOptions,
};
use lapin::types::FieldTable;
use lapin::{ConnectionProperties, ExchangeKind};

Expand Down Expand Up @@ -484,32 +486,30 @@ impl RabbitMqEventConsumer {
.await
.expect("Failed to bind queue");

Self { _connection: connection, channel, queue_name }
let consumer = channel
.basic_consume(
&queue_name,
&format!("consumer_{}", queue_name),
BasicConsumeOptions::default(),
FieldTable::default(),
)
.await
.expect("Failed to start consumer");

Self { _connection: connection, channel, consumer }
}

/// Consume up to `count` events, waiting up to `timeout` for each.
pub async fn consume_events(
&self, count: usize, timeout: Duration,
&mut self, count: usize, timeout: Duration,
) -> Vec<ldk_server_protos::events::EventEnvelope> {
use futures_util::StreamExt;
use lapin::options::{BasicAckOptions, BasicConsumeOptions};
use lapin::types::FieldTable;
use lapin::options::BasicAckOptions;
use prost::Message;

let mut consumer = self
.channel
.basic_consume(
&self.queue_name,
&format!("consumer_{}", self.queue_name),
BasicConsumeOptions::default(),
FieldTable::default(),
)
.await
.expect("Failed to start consumer");

let mut events = Vec::new();
for _ in 0..count {
match tokio::time::timeout(timeout, consumer.next()).await {
match tokio::time::timeout(timeout, self.consumer.next()).await {
Ok(Some(Ok(delivery))) => {
let event = ldk_server_protos::events::EventEnvelope::decode(&*delivery.data)
.expect("Failed to decode event");
Expand Down
154 changes: 149 additions & 5 deletions e2e-tests/tests/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ use e2e_tests::{
find_available_port, mine_and_sync, run_cli, run_cli_raw, setup_funded_channel,
wait_for_onchain_balance, LdkServerHandle, RabbitMqEventConsumer, TestBitcoind,
};
use hex_conservative::DisplayHex;
use ldk_node::bitcoin::hashes::{sha256, Hash};
use ldk_node::lightning::ln::msgs::SocketAddress;
use ldk_server_client::ldk_server_protos::api::{
Bolt11ReceiveRequest, Bolt12ReceiveRequest, OnchainReceiveRequest,
Expand Down Expand Up @@ -271,8 +273,8 @@ async fn test_cli_bolt11_send() {
let server_b = LdkServerHandle::start(&bitcoind).await;

// Set up event consumers before any payments
let consumer_a = RabbitMqEventConsumer::new(&server_a.exchange_name).await;
let consumer_b = RabbitMqEventConsumer::new(&server_b.exchange_name).await;
let mut consumer_a = RabbitMqEventConsumer::new(&server_a.exchange_name).await;
let mut consumer_b = RabbitMqEventConsumer::new(&server_b.exchange_name).await;

setup_funded_channel(&bitcoind, &server_a, &server_b, 100_000).await;

Expand Down Expand Up @@ -339,8 +341,8 @@ async fn test_cli_spontaneous_send() {
let server_a = LdkServerHandle::start(&bitcoind).await;
let server_b = LdkServerHandle::start(&bitcoind).await;

let consumer_a = RabbitMqEventConsumer::new(&server_a.exchange_name).await;
let consumer_b = RabbitMqEventConsumer::new(&server_b.exchange_name).await;
let mut consumer_a = RabbitMqEventConsumer::new(&server_a.exchange_name).await;
let mut consumer_b = RabbitMqEventConsumer::new(&server_b.exchange_name).await;

setup_funded_channel(&bitcoind, &server_a, &server_b, 100_000).await;

Expand Down Expand Up @@ -564,7 +566,7 @@ async fn test_forwarded_payment_event() {
let server_b = LdkServerHandle::start(&bitcoind).await;

// Set up RabbitMQ consumer on B before any payments
let consumer_b = RabbitMqEventConsumer::new(&server_b.exchange_name).await;
let mut consumer_b = RabbitMqEventConsumer::new(&server_b.exchange_name).await;

// Open channel A -> B (1M sats, larger for JIT forwarding)
setup_funded_channel(&bitcoind, &server_a, &server_b, 1_000_000).await;
Expand Down Expand Up @@ -634,3 +636,145 @@ async fn test_forwarded_payment_event() {

node_c.stop().unwrap();
}

#[tokio::test]
async fn test_hodl_invoice_claim() {
let bitcoind = TestBitcoind::new();
let server_a = LdkServerHandle::start(&bitcoind).await;
let server_b = LdkServerHandle::start(&bitcoind).await;

let mut consumer_a = RabbitMqEventConsumer::new(&server_a.exchange_name).await;
let mut consumer_b = RabbitMqEventConsumer::new(&server_b.exchange_name).await;

setup_funded_channel(&bitcoind, &server_a, &server_b, 100_000).await;

// Test three claim variants: (preimage, amount, hash)
let test_cases: Vec<([u8; 32], Option<&str>, bool)> = vec![
([42u8; 32], Some("10000000msat"), true), // all args
([44u8; 32], Some("10000000msat"), false), // preimage + amount
([45u8; 32], None, true), // preimage + hash
([46u8; 32], None, false), // preimage only
];

for (preimage_bytes, amount, include_hash) in &test_cases {
let preimage_hex = preimage_bytes.to_lower_hex_string();
let payment_hash_hex =
sha256::Hash::hash(preimage_bytes).to_byte_array().to_lower_hex_string();

// Create hodl invoice on B
let invoice_resp = run_cli(
&server_b,
&[
"bolt11-receive-for-hash",
&payment_hash_hex,
"10000000msat",
"-d",
"hodl test",
"-e",
"3600",
],
);
let invoice = invoice_resp["invoice"].as_str().unwrap();

// Pay the hodl invoice from A
run_cli(&server_a, &["bolt11-send", invoice]);

// Verify PaymentClaimable event on B
let events_b = consumer_b.consume_events(1, Duration::from_secs(10)).await;
assert!(
events_b.iter().any(|e| matches!(&e.event, Some(Event::PaymentClaimable(_)))),
"Expected PaymentClaimable on receiver, got events: {:?}",
events_b.iter().map(|e| &e.event).collect::<Vec<_>>()
);

// Claim the payment on B
let mut args: Vec<&str> = vec!["bolt11-claim-for-hash", &preimage_hex];
if let Some(amt) = amount {
args.extend(["-c", amt]);
}
if *include_hash {
args.extend(["-p", &payment_hash_hex]);
}
run_cli(&server_b, &args);

// Verify PaymentReceived event on B
let events_b = consumer_b.consume_events(1, Duration::from_secs(10)).await;
assert!(
events_b.iter().any(|e| matches!(&e.event, Some(Event::PaymentReceived(_)))),
"Expected PaymentReceived on receiver after claim, got events: {:?}",
events_b.iter().map(|e| &e.event).collect::<Vec<_>>()
);

// Verify PaymentSuccessful on A
let events_a = consumer_a.consume_events(1, Duration::from_secs(10)).await;
assert!(
events_a.iter().any(|e| matches!(&e.event, Some(Event::PaymentSuccessful(_)))),
"Expected PaymentSuccessful on sender, got events: {:?}",
events_a.iter().map(|e| &e.event).collect::<Vec<_>>()
);
}
}

#[tokio::test]
async fn test_hodl_invoice_fail() {
use hex_conservative::DisplayHex;
use ldk_node::bitcoin::hashes::{sha256, Hash};

let bitcoind = TestBitcoind::new();
let server_a = LdkServerHandle::start(&bitcoind).await;
let server_b = LdkServerHandle::start(&bitcoind).await;

// Set up event consumers before any payments
let mut consumer_a = RabbitMqEventConsumer::new(&server_a.exchange_name).await;
let mut consumer_b = RabbitMqEventConsumer::new(&server_b.exchange_name).await;

setup_funded_channel(&bitcoind, &server_a, &server_b, 100_000).await;

// Generate a known preimage and compute its payment hash
let preimage_bytes = [43u8; 32];
let payment_hash = sha256::Hash::hash(&preimage_bytes);
let payment_hash_hex = payment_hash.to_byte_array().to_lower_hex_string();

// Create hodl invoice on B
let invoice_resp = run_cli(
&server_b,
&[
"bolt11-receive-for-hash",
&payment_hash_hex,
"10000000msat",
"-d",
"hodl fail test",
"-e",
"3600",
],
);
let invoice = invoice_resp["invoice"].as_str().unwrap();

// Pay the hodl invoice from A
run_cli(&server_a, &["bolt11-send", invoice]);

// Wait for payment to arrive at B
tokio::time::sleep(Duration::from_secs(5)).await;

// Verify PaymentClaimable event on B
let events_b = consumer_b.consume_events(5, Duration::from_secs(10)).await;
assert!(
events_b.iter().any(|e| matches!(&e.event, Some(Event::PaymentClaimable(_)))),
"Expected PaymentClaimable on receiver, got events: {:?}",
events_b.iter().map(|e| &e.event).collect::<Vec<_>>()
);

// Fail the payment on B using CLI
run_cli(&server_b, &["bolt11-fail-for-hash", &payment_hash_hex]);

// Wait for failure to propagate
tokio::time::sleep(Duration::from_secs(5)).await;

// Verify PaymentFailed on A
let events_a = consumer_a.consume_events(10, Duration::from_secs(10)).await;
assert!(
events_a.iter().any(|e| matches!(&e.event, Some(Event::PaymentFailed(_)))),
"Expected PaymentFailed on sender after hodl rejection, got events: {:?}",
events_a.iter().map(|e| &e.event).collect::<Vec<_>>()
);
}
96 changes: 96 additions & 0 deletions ldk-server-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ use ldk_server_client::error::LdkServerErrorCode::{
AuthError, InternalError, InternalServerError, InvalidRequestError, LightningError,
};
use ldk_server_client::ldk_server_protos::api::{
Bolt11ClaimForHashRequest, Bolt11ClaimForHashResponse, Bolt11FailForHashRequest,
Bolt11FailForHashResponse, Bolt11ReceiveForHashRequest, Bolt11ReceiveForHashResponse,
Bolt11ReceiveRequest, Bolt11ReceiveResponse, Bolt11SendRequest, Bolt11SendResponse,
Bolt12ReceiveRequest, Bolt12ReceiveResponse, Bolt12SendRequest, Bolt12SendResponse,
CloseChannelRequest, CloseChannelResponse, ConnectPeerRequest, ConnectPeerResponse,
Expand Down Expand Up @@ -134,6 +136,48 @@ enum Commands {
#[arg(short, long, help = "Invoice expiry time in seconds (default: 86400)")]
expiry_secs: Option<u32>,
},
#[command(
about = "Create a BOLT11 hodl invoice for a given payment hash (manual claim required)"
)]
Bolt11ReceiveForHash {
#[arg(help = "The hex-encoded 32-byte payment hash")]
payment_hash: String,
#[arg(
help = "Amount to request, e.g. 50sat or 50000msat. If unset, a variable-amount invoice is returned"
)]
amount: Option<Amount>,
#[arg(short, long, help = "Description to attach along with the invoice")]
description: Option<String>,
#[arg(
long,
help = "SHA-256 hash of the description (hex). Use instead of description for longer text"
)]
description_hash: Option<String>,
#[arg(short, long, help = "Invoice expiry time in seconds (default: 86400)")]
expiry_secs: Option<u32>,
},
#[command(about = "Claim a held payment by providing the preimage")]
Bolt11ClaimForHash {
#[arg(help = "The hex-encoded 32-byte payment preimage")]
preimage: String,
#[arg(
short,
long,
help = "The claimable amount, e.g. 50sat or 50000msat, only used for verifying we are claiming the expected amount"
)]
claimable_amount: Option<Amount>,
#[arg(
short,
long,
help = "The hex-encoded 32-byte payment hash, used to verify the preimage matches"
)]
payment_hash: Option<String>,
},
#[command(about = "Fail/reject a held payment")]
Bolt11FailForHash {
#[arg(help = "The hex-encoded 32-byte payment hash")]
payment_hash: String,
},
#[command(about = "Pay a BOLT11 invoice")]
Bolt11Send {
#[arg(help = "A BOLT11 invoice for a payment within the Lightning Network")]
Expand Down Expand Up @@ -551,6 +595,58 @@ async fn main() {
client.bolt11_receive(request).await,
);
},
Commands::Bolt11ReceiveForHash {
payment_hash,
amount,
description,
description_hash,
expiry_secs,
} => {
let amount_msat = amount.map(|a| a.to_msat());
let invoice_description = match (description, description_hash) {
(Some(desc), None) => Some(Bolt11InvoiceDescription {
kind: Some(bolt11_invoice_description::Kind::Direct(desc)),
}),
(None, Some(hash)) => Some(Bolt11InvoiceDescription {
kind: Some(bolt11_invoice_description::Kind::Hash(hash)),
}),
(Some(_), Some(_)) => {
handle_error(LdkServerError::new(
InternalError,
"Only one of description or description_hash can be set.".to_string(),
));
},
(None, None) => None,
};

let expiry_secs = expiry_secs.unwrap_or(DEFAULT_EXPIRY_SECS);
let request = Bolt11ReceiveForHashRequest {
description: invoice_description,
expiry_secs,
amount_msat,
payment_hash,
};

handle_response_result::<_, Bolt11ReceiveForHashResponse>(
client.bolt11_receive_for_hash(request).await,
);
},
Commands::Bolt11ClaimForHash { preimage, claimable_amount, payment_hash } => {
handle_response_result::<_, Bolt11ClaimForHashResponse>(
client
.bolt11_claim_for_hash(Bolt11ClaimForHashRequest {
payment_hash,
claimable_amount_msat: claimable_amount.map(|a| a.to_msat()),
preimage,
})
.await,
);
},
Commands::Bolt11FailForHash { payment_hash } => {
handle_response_result::<_, Bolt11FailForHashResponse>(
client.bolt11_fail_for_hash(Bolt11FailForHashRequest { payment_hash }).await,
);
},
Commands::Bolt11Send {
invoice,
amount,
Expand Down
Loading
Loading