Skip to content

Commit

Permalink
Implement and test Refund flow
Browse files Browse the repository at this point in the history
  • Loading branch information
tnull committed May 29, 2024
1 parent aebe9bf commit da32263
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 36 deletions.
12 changes: 12 additions & 0 deletions bindings/ldk_node.udl
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ interface Bolt12Payment {
Offer receive(u64 amount_msat, [ByRef]string description);
[Throws=NodeError]
Offer receive_variable_amount([ByRef]string description);
[Throws=NodeError]
Bolt12Invoice request_refund([ByRef]Refund refund);
[Throws=NodeError]
Refund offer_refund(u64 amount_msat, u32 expiry_secs);
};

interface SpontaneousPayment {
Expand Down Expand Up @@ -136,6 +140,7 @@ enum NodeError {
"InvoiceCreationFailed",
"InvoiceRequestCreationFailed",
"OfferCreationFailed",
"RefundCreationFailed",
"PaymentSendingFailed",
"ProbeSendingFailed",
"ChannelCreationFailed",
Expand All @@ -161,6 +166,7 @@ enum NodeError {
"InvalidAmount",
"InvalidInvoice",
"InvalidOffer",
"InvalidRefund",
"InvalidChannelId",
"InvalidNetwork",
"DuplicatePayment",
Expand Down Expand Up @@ -393,6 +399,12 @@ typedef string Bolt11Invoice;
[Custom]
typedef string Offer;

[Custom]
typedef string Refund;

[Custom]
typedef string Bolt12Invoice;

[Custom]
typedef string OfferId;

Expand Down
6 changes: 6 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ pub enum Error {
InvoiceRequestCreationFailed,
/// Offer creation failed.
OfferCreationFailed,
/// Refund creation failed.
RefundCreationFailed,
/// Sending a payment has failed.
PaymentSendingFailed,
/// Sending a payment probe has failed.
Expand Down Expand Up @@ -67,6 +69,8 @@ pub enum Error {
InvalidInvoice,
/// The given offer is invalid.
InvalidOffer,
/// The given refund is invalid.
InvalidRefund,
/// The given channel ID is invalid.
InvalidChannelId,
/// The given network is invalid.
Expand Down Expand Up @@ -95,6 +99,7 @@ impl fmt::Display for Error {
Self::InvoiceCreationFailed => write!(f, "Failed to create invoice."),
Self::InvoiceRequestCreationFailed => write!(f, "Failed to create invoice request."),
Self::OfferCreationFailed => write!(f, "Failed to create offer."),
Self::RefundCreationFailed => write!(f, "Failed to create refund."),
Self::PaymentSendingFailed => write!(f, "Failed to send the given payment."),
Self::ProbeSendingFailed => write!(f, "Failed to send the given payment probe."),
Self::ChannelCreationFailed => write!(f, "Failed to create channel."),
Expand Down Expand Up @@ -122,6 +127,7 @@ impl fmt::Display for Error {
Self::InvalidAmount => write!(f, "The given amount is invalid."),
Self::InvalidInvoice => write!(f, "The given invoice is invalid."),
Self::InvalidOffer => write!(f, "The given offer is invalid."),
Self::InvalidRefund => write!(f, "The given refund is invalid."),
Self::InvalidChannelId => write!(f, "The given channel ID is invalid."),
Self::InvalidNetwork => write!(f, "The given network is invalid."),
Self::DuplicatePayment => {
Expand Down
37 changes: 1 addition & 36 deletions src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -549,42 +549,7 @@ where
}
payment_preimage
},
PaymentPurpose::Bolt12RefundPayment {
payment_preimage,
payment_secret,
..
} => {
let payment = PaymentDetails {
id: payment_id,
kind: PaymentKind::Bolt12Refund {
hash: Some(payment_hash),
preimage: payment_preimage,
secret: Some(payment_secret),
},
amount_msat: Some(amount_msat),
direction: PaymentDirection::Inbound,
status: PaymentStatus::Pending,
};
match self.payment_store.insert(payment) {
Ok(false) => (),
Ok(true) => {
log_error!(
self.logger,
"Bolt12RefundPayment with ID {} was previously known",
payment_id,
);
debug_assert!(false);
},
Err(e) => {
log_error!(
self.logger,
"Failed to insert payment with ID {}: {}",
payment_id,
e
);
debug_assert!(false);
},
}
PaymentPurpose::Bolt12RefundPayment { payment_preimage, .. } => {
payment_preimage
},
PaymentPurpose::SpontaneousPayment(preimage) => {
Expand Down
81 changes: 81 additions & 0 deletions src/payment/bolt12.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ use crate::payment::store::{
use crate::types::ChannelManager;

use lightning::ln::channelmanager::{PaymentId, Retry};
use lightning::offers::invoice::Bolt12Invoice;
use lightning::offers::offer::{Amount, Offer};
use lightning::offers::parse::Bolt12SemanticError;
use lightning::offers::refund::Refund;

use rand::RngCore;

use std::sync::{Arc, RwLock};
use std::time::{Duration, SystemTime, UNIX_EPOCH};

/// A payment handler allowing to create and pay [BOLT 12] offers and refunds.
///
Expand Down Expand Up @@ -264,4 +267,82 @@ impl Bolt12Payment {

Ok(offer)
}

/// Requests a refund payment for the given [`Refund`].
///
/// The returned [`Bolt12Invoice`] is for informational purposes only (i.e., isn't needed to
/// retrieve the refund).
pub fn request_refund(&self, refund: &Refund) -> Result<Bolt12Invoice, Error> {
let invoice = self.channel_manager.request_refund_payment(refund).map_err(|e| {
log_error!(self.logger, "Failed to request refund payment: {:?}", e);
Error::InvoiceRequestCreationFailed
})?;

let payment_hash = invoice.payment_hash();
let payment_id = PaymentId(payment_hash.0);

let payment = PaymentDetails {
id: payment_id,
kind: PaymentKind::Bolt12Refund {
hash: Some(payment_hash),
preimage: None,
secret: None,
},
amount_msat: Some(refund.amount_msats()),
direction: PaymentDirection::Inbound,
status: PaymentStatus::Pending,
};

self.payment_store.insert(payment)?;

Ok(invoice)
}

/// Returns a [`Refund`] that can be used to offer a refund payment of the amount given.
pub fn offer_refund(&self, amount_msat: u64, expiry_secs: u32) -> Result<Refund, Error> {
let mut random_bytes = [0u8; 32];
rand::thread_rng().fill_bytes(&mut random_bytes);
let payment_id = PaymentId(random_bytes);

let expiration = (SystemTime::now() + Duration::from_secs(expiry_secs as u64))
.duration_since(UNIX_EPOCH)
.unwrap();
let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT);
let max_total_routing_fee_msat = None;

let refund = self
.channel_manager
.create_refund_builder(
amount_msat,
expiration,
payment_id,
retry_strategy,
max_total_routing_fee_msat,
)
.map_err(|e| {
log_error!(self.logger, "Failed to create refund builder: {:?}", e);
Error::RefundCreationFailed
})?
.build()
.map_err(|e| {
log_error!(self.logger, "Failed to create refund: {:?}", e);
Error::RefundCreationFailed
})?;

log_info!(self.logger, "Offering refund of {}msat", amount_msat);

let kind = PaymentKind::Bolt12Refund { hash: None, preimage: None, secret: None };

let payment = PaymentDetails {
id: payment_id,
kind,
amount_msat: Some(amount_msat),
direction: PaymentDirection::Outbound,
status: PaymentStatus::Pending,
};

self.payment_store.insert(payment)?;

Ok(refund)
}
}
32 changes: 32 additions & 0 deletions src/uniffi_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ pub use crate::payment::store::{LSPFeeLimits, PaymentDirection, PaymentKind, Pay

pub use lightning::events::{ClosureReason, PaymentFailureReason};
pub use lightning::ln::{ChannelId, PaymentHash, PaymentPreimage, PaymentSecret};
pub use lightning::offers::invoice::Bolt12Invoice;
pub use lightning::offers::offer::{Offer, OfferId};
pub use lightning::offers::refund::Refund;
pub use lightning::util::string::UntrustedString;

pub use lightning_invoice::Bolt11Invoice;
Expand All @@ -21,6 +23,7 @@ use bitcoin::hashes::sha256::Hash as Sha256;
use bitcoin::hashes::Hash;
use bitcoin::secp256k1::PublicKey;
use lightning::ln::channelmanager::PaymentId;
use lightning::util::ser::Writeable;
use lightning_invoice::SignedRawBolt11Invoice;

use std::convert::TryInto;
Expand Down Expand Up @@ -88,6 +91,35 @@ impl UniffiCustomTypeConverter for Offer {
}
}

impl UniffiCustomTypeConverter for Refund {
type Builtin = String;

fn into_custom(val: Self::Builtin) -> uniffi::Result<Self> {
Refund::from_str(&val).map_err(|_| Error::InvalidRefund.into())
}

fn from_custom(obj: Self) -> Self::Builtin {
obj.to_string()
}
}

impl UniffiCustomTypeConverter for Bolt12Invoice {
type Builtin = String;

fn into_custom(val: Self::Builtin) -> uniffi::Result<Self> {
if let Some(bytes_vec) = hex_utils::to_vec(&val) {
if let Ok(invoice) = Bolt12Invoice::try_from(bytes_vec) {
return Ok(invoice);
}
}
Err(Error::InvalidInvoice.into())
}

fn from_custom(obj: Self) -> Self::Builtin {
hex_utils::to_string(&obj.encode())
}
}

impl UniffiCustomTypeConverter for OfferId {
type Builtin = String;

Expand Down
43 changes: 43 additions & 0 deletions tests/integration_tests_rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -487,4 +487,47 @@ fn simple_bolt12_send_receive() {
},
}
assert_eq!(node_b_payments.first().unwrap().amount_msat, Some(expected_amount_msat));

// Now node_b refunds the amount node_a just overpaid.
let overpaid_amount = expected_amount_msat - offer_amount_msat;
let refund = node_b.bolt12_payment().offer_refund(overpaid_amount, 3600).unwrap();
let invoice = node_a.bolt12_payment().request_refund(&refund).unwrap();
expect_payment_received_event!(node_a, overpaid_amount);

let node_b_payment_id = node_b
.list_payments_with_filter(|p| p.amount_msat == Some(overpaid_amount))
.first()
.unwrap()
.id;
expect_payment_successful_event!(node_b, Some(node_b_payment_id), None);

let node_b_payments = node_b.list_payments_with_filter(|p| p.id == node_b_payment_id);
assert_eq!(node_b_payments.len(), 1);
match node_b_payments.first().unwrap().kind {
PaymentKind::Bolt12Refund { hash, preimage, secret: _ } => {
assert!(hash.is_some());
assert!(preimage.is_some());
//TODO: We should eventually set and assert the secret sender-side, too, but the BOLT12
//API currently doesn't allow to do that.
},
_ => {
panic!("Unexpected payment kind");
},
}
assert_eq!(node_b_payments.first().unwrap().amount_msat, Some(overpaid_amount));

let node_a_payment_id = PaymentId(invoice.payment_hash().0);
let node_a_payments = node_a.list_payments_with_filter(|p| p.id == node_a_payment_id);
assert_eq!(node_a_payments.len(), 1);
match node_a_payments.first().unwrap().kind {
PaymentKind::Bolt12Refund { hash, preimage, secret } => {
assert!(hash.is_some());
assert!(preimage.is_some());
assert!(secret.is_some());
},
_ => {
panic!("Unexpected payment kind");
},
}
assert_eq!(node_a_payments.first().unwrap().amount_msat, Some(overpaid_amount));
}

0 comments on commit da32263

Please sign in to comment.