Skip to content

Commit

Permalink
Merge pull request #334 from getlipa/feature/enable-paying-open-invoices
Browse files Browse the repository at this point in the history
Enable paying open invoices
  • Loading branch information
gcomte authored Apr 28, 2023
2 parents 4d64b31 + 859fa30 commit ed6c0ea
Show file tree
Hide file tree
Showing 6 changed files with 271 additions and 65 deletions.
40 changes: 37 additions & 3 deletions eel/examples/eel-node/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ pub(crate) fn poll_for_user_input(node: &LightningNode, log_file_path: &str) {
println!("{}", message.red());
}
}
"payopeninvoice" => {
if let Err(message) = pay_open_invoice(node, &mut words) {
println!("{}", message.red());
}
}
"listpayments" => {
if let Err(message) = list_payments(node) {
println!("{}", message.red());
Expand Down Expand Up @@ -96,6 +101,7 @@ fn help() {
println!(" invoice <amount in millisats> [description]");
println!(" decodeinvoice <invoice>");
println!(" payinvoice <invoice>");
println!(" payopeninvoice <invoice> <amount in millisats>");
println!("");
println!(" listpayments");
println!("");
Expand Down Expand Up @@ -204,15 +210,43 @@ fn decode_invoice<'a>(
Ok(())
}

fn pay_invoice<'a>(
fn pay_invoice(node: &LightningNode, words: &mut dyn Iterator<Item = &str>) -> Result<(), String> {
let invoice = words
.next()
.ok_or_else(|| "invoice is required".to_string())?;

if words.next().is_some() {
return Err("To many arguments. Specifying an amount is only allowed for open invoices. To pay an open invoice use 'payopeninvoice'.".to_string());
}

match node.pay_invoice(invoice.to_string(), String::new()) {
Ok(_) => {}
Err(e) => return Err(e.to_string()),
};

Ok(())
}

fn pay_open_invoice(
node: &LightningNode,
words: &mut dyn Iterator<Item = &'a str>,
words: &mut dyn Iterator<Item = &str>,
) -> Result<(), String> {
let invoice = words
.next()
.ok_or_else(|| "invoice is required".to_string())?;

match node.pay_invoice(invoice.to_string(), String::new()) {
let amount_argument = match words.next() {
Some(amount) => match amount.parse::<u64>() {
Ok(parsed) => Ok(parsed),
Err(_) => return Err("Error: millisat amount must be an integer".to_string()),
},
None => Err(
"Open amount invoices require an amount in millisats as an additional argument"
.to_string(),
),
}?;

match node.pay_open_invoice(invoice.to_string(), amount_argument, String::new()) {
Ok(_) => {}
Err(e) => return Err(e.to_string()),
};
Expand Down
154 changes: 100 additions & 54 deletions eel/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ use lightning::ln::channelmanager::{ChainParameters, Retry, RetryableSendFailure
use lightning::ln::peer_handler::IgnoringMessageHandler;
use lightning::util::config::UserConfig;
use lightning_background_processor::{BackgroundProcessor, GossipSync};
use lightning_invoice::payment::{pay_invoice, PaymentError};
use lightning_invoice::payment::{pay_invoice, pay_zero_value_invoice, PaymentError};
use lightning_invoice::{Currency, Invoice, InvoiceDescription};
use log::error;
pub use log::Level as LogLevel;
Expand Down Expand Up @@ -404,75 +404,121 @@ impl LightningNode {
}

pub fn pay_invoice(&self, invoice: String, metadata: String) -> Result<()> {
let invoice_struct =
self.validate_persist_new_outgoing_payment_attempt(&invoice, &metadata)?;
let invoice = invoice::parse_invoice(&invoice)?;
let amount_msat = invoice.amount_milli_satoshis().unwrap_or(0);

if amount_msat == 0 {
return Err(invalid_input(
"Expected invoice with a specified amount, but an open invoice was provided",
));
}

match pay_invoice(
&invoice_struct,
self.validate_persist_new_outgoing_payment_attempt(&invoice, amount_msat, &metadata)?;

let payment_result = pay_invoice(
&invoice,
Retry::Timeout(Duration::from_secs(10)),
&self.channel_manager,
) {
);

match payment_result {
Ok(_payment_id) => {
info!(
"Initiated payment of {:?} msats",
invoice_struct.amount_milli_satoshis()
);
info!("Initiated payment of {amount_msat} msats");
Ok(())
}
Err(e) => {
return match e {
PaymentError::Invoice(e) => {
self.payment_store.lock().unwrap().new_payment_state(
&invoice_struct.payment_hash().to_string(),
PaymentState::Failed,
)?;
Err(invalid_input(format!("Invalid invoice - {e}")))
}
PaymentError::Sending(e) => {
self.payment_store.lock().unwrap().new_payment_state(
&invoice_struct.payment_hash().to_string(),
PaymentState::Failed,
)?;
match e {
RetryableSendFailure::PaymentExpired => Err(runtime_error(
RuntimeErrorCode::SendFailure,
format!("Failed to send payment - {e:?}"),
)),
RetryableSendFailure::RouteNotFound => Err(runtime_error(
RuntimeErrorCode::NoRouteFound,
"Failed to find a route",
)),
RetryableSendFailure::DuplicatePayment => Err(runtime_error(
RuntimeErrorCode::SendFailure,
format!("Failed to send payment - {e:?}"),
)),
}
}
Err(e) => self.process_failed_payment_attempts(e, &invoice.payment_hash().to_string()),
}
}

pub fn pay_open_invoice(
&self,
invoice: String,
amount_msat: u64,
metadata: String,
) -> Result<()> {
let invoice = invoice::parse_invoice(&invoice)?;
let invoice_amount_msat = invoice.amount_milli_satoshis().unwrap_or(0);

if invoice_amount_msat != 0 {
return Err(invalid_input(
"Expected open invoice, but an invoice with a specified amount was provided",
));
} else if amount_msat == 0 {
return Err(invalid_input(
"Invoice does not specify an amount and no amount was specified manually",
));
}

self.validate_persist_new_outgoing_payment_attempt(&invoice, amount_msat, &metadata)?;

let payment_result = pay_zero_value_invoice(
&invoice,
amount_msat,
Retry::Timeout(Duration::from_secs(10)),
&self.channel_manager,
);

match payment_result {
Ok(_payment_id) => {
info!("Initiated payment of {amount_msat} msats (open amount invoice)");
Ok(())
}
Err(e) => self.process_failed_payment_attempts(e, &invoice.payment_hash().to_string()),
}
}

fn process_failed_payment_attempts(
&self,
error: PaymentError,
payment_hash: &str,
) -> Result<()> {
match error {
PaymentError::Invoice(e) => {
self.payment_store
.lock()
.unwrap()
.new_payment_state(payment_hash, PaymentState::Failed)?;
Err(invalid_input(format!("Invalid invoice - {e}")))
}
PaymentError::Sending(e) => {
self.payment_store
.lock()
.unwrap()
.new_payment_state(payment_hash, PaymentState::Failed)?;
match e {
RetryableSendFailure::PaymentExpired => Err(runtime_error(
RuntimeErrorCode::SendFailure,
format!("Failed to send payment - {e:?}"),
)),
RetryableSendFailure::RouteNotFound => Err(runtime_error(
RuntimeErrorCode::NoRouteFound,
"Failed to find a route",
)),
RetryableSendFailure::DuplicatePayment => Err(runtime_error(
RuntimeErrorCode::SendFailure,
format!("Failed to send payment - {e:?}"),
)),
}
}
}
Ok(())
}

fn validate_persist_new_outgoing_payment_attempt(
&self,
invoice: &str,
invoice: &Invoice,
amount_msat: u64,
metadata: &str,
) -> Result<Invoice> {
let invoice_struct = invoice::parse_invoice(invoice)?;
) -> Result<()> {
validate_invoice(self.config.network, invoice)?;

validate_invoice(self.config.network, &invoice_struct)?;

let amount_msat = invoice_struct
.amount_milli_satoshis()
.ok_or_invalid_input("Invalid invoice - invoice is a zero value invoice and paying such invoice is not supported yet")?;
let description = match invoice_struct.description() {
let description = match invoice.description() {
InvoiceDescription::Direct(d) => d.clone().into_inner(),
InvoiceDescription::Hash(h) => h.0.to_hex(),
};
let fiat_values = self.get_fiat_values(amount_msat);

let mut payment_store = self.payment_store.lock().unwrap();
if let Ok(payment) = payment_store.get_payment(&invoice_struct.payment_hash().to_string()) {
if let Ok(payment) = payment_store.get_payment(&invoice.payment_hash().to_string()) {
match payment.payment_type {
PaymentType::Receiving => return Err(runtime_error(
RuntimeErrorCode::PayingToSelf,
Expand All @@ -486,22 +532,22 @@ impl LightningNode {
));
}
payment_store.new_payment_state(
&invoice_struct.payment_hash().to_string(),
&invoice.payment_hash().to_string(),
PaymentState::Retried,
)?;
}
}
} else {
payment_store.new_outgoing_payment(
&invoice_struct.payment_hash().to_string(),
&invoice.payment_hash().to_string(),
amount_msat,
&description,
invoice,
&invoice.to_string(),
metadata,
fiat_values,
)?;
}
Ok(invoice_struct)
Ok(())
}

pub fn get_latest_payments(&self, number_of_payments: u32) -> Result<Vec<Payment>> {
Expand Down
76 changes: 73 additions & 3 deletions eel/tests/sending_payments_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ mod sending_payments_test {
wait_for_eq!(node.get_node_info().num_peers, 1);
nigiri::initiate_channel_from_remote(node.get_node_info().node_pubkey, LspdLnd);

// Test hardcoded invoices here to avoid an additional test env set up
invoice_decode_test(&node);

assert!(node.get_node_info().channels_info.num_channels > 0);
Expand All @@ -50,16 +49,87 @@ mod sending_payments_test {
< CHANNEL_SIZE - REBALANCE_AMOUNT
); // smaller instead of equal because of channel reserves

// Test vanilla payment
let invoice = nigiri::issue_invoice(LspdLnd, "test", PAYMENT_AMOUNT, 3600).unwrap();

let initial_balance = nigiri::query_node_balance(LspdLnd).unwrap();

node.pay_invoice(invoice, String::new()).unwrap();

wait_for_eq!(
nigiri::query_node_balance(LspdLnd).unwrap() - initial_balance,
PAYMENT_AMOUNT
);

// Test a regular payment but using an invoice that has no amount specified
let invoice =
nigiri::lnd_issue_invoice(LspdLnd, "open amount invoice", None, 3600).unwrap();

let initial_balance = nigiri::query_node_balance(LspdLnd).unwrap();

let payment_result = node.pay_invoice(invoice, String::new());
assert!(matches!(
payment_result,
Err(perro::Error::InvalidInput { .. })
));

// no payment took place
sleep(Duration::from_secs(2));
assert_eq!(
initial_balance,
nigiri::query_node_balance(LspdLnd).unwrap()
);

// Test paying open invoices
let invoice =
nigiri::lnd_issue_invoice(LspdLnd, "open amount invoice", None, 3600).unwrap();

let initial_balance = nigiri::query_node_balance(LspdLnd).unwrap();

node.pay_open_invoice(invoice, PAYMENT_AMOUNT, String::new())
.unwrap();

wait_for_eq!(
nigiri::query_node_balance(LspdLnd).unwrap() - initial_balance,
PAYMENT_AMOUNT
);

// Test paying open invoices specifying 0 as the payment amount
let invoice =
nigiri::lnd_issue_invoice(LspdLnd, "open amount invoice", None, 3600).unwrap();

let initial_balance = nigiri::query_node_balance(LspdLnd).unwrap();

let payment_result = node.pay_open_invoice(invoice, 0, String::new());
assert!(matches!(
payment_result,
Err(perro::Error::InvalidInput { .. })
));

// no payment took place
sleep(Duration::from_secs(2));
assert_eq!(
initial_balance,
nigiri::query_node_balance(LspdLnd).unwrap()
);

// Test paying open invoices using an invoice with a specified amount
let invoice = nigiri::issue_invoice(LspdLnd, "test", PAYMENT_AMOUNT, 3600).unwrap();

let initial_balance = nigiri::query_node_balance(LspdLnd).unwrap();

let final_balance = nigiri::query_node_balance(LspdLnd).unwrap();
let payment_result = node.pay_open_invoice(invoice, PAYMENT_AMOUNT, String::new());
assert!(matches!(
payment_result,
Err(perro::Error::InvalidInput { .. })
));

assert_eq!(final_balance - initial_balance, PAYMENT_AMOUNT);
// no payment took place
sleep(Duration::from_secs(2));
assert_eq!(
initial_balance,
nigiri::query_node_balance(LspdLnd).unwrap()
);
}

const THOUSAND_SATS: u64 = 1_000_000;
Expand Down
Loading

0 comments on commit ed6c0ea

Please sign in to comment.