Skip to content

Commit 7ceaf5d

Browse files
committed
Implement and test Refund flow
1 parent 5361d9f commit 7ceaf5d

File tree

5 files changed

+174
-0
lines changed

5 files changed

+174
-0
lines changed

bindings/ldk_node.udl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ interface Bolt12Payment {
109109
Offer receive(u64 amount_msat, [ByRef]string description);
110110
[Throws=NodeError]
111111
Offer receive_variable_amount([ByRef]string description);
112+
[Throws=NodeError]
113+
Bolt12Invoice request_refund([ByRef]Refund refund);
114+
[Throws=NodeError]
115+
Refund create_refund(u64 amount_msat, u32 expiry_secs);
112116
};
113117

114118
interface SpontaneousPayment {
@@ -136,6 +140,7 @@ enum NodeError {
136140
"InvoiceCreationFailed",
137141
"InvoiceRequestCreationFailed",
138142
"OfferCreationFailed",
143+
"RefundCreationFailed",
139144
"PaymentSendingFailed",
140145
"ProbeSendingFailed",
141146
"ChannelCreationFailed",
@@ -161,6 +166,7 @@ enum NodeError {
161166
"InvalidAmount",
162167
"InvalidInvoice",
163168
"InvalidOffer",
169+
"InvalidRefund",
164170
"InvalidChannelId",
165171
"InvalidNetwork",
166172
"DuplicatePayment",
@@ -393,6 +399,12 @@ typedef string Bolt11Invoice;
393399
[Custom]
394400
typedef string Offer;
395401

402+
[Custom]
403+
typedef string Refund;
404+
405+
[Custom]
406+
typedef string Bolt12Invoice;
407+
396408
[Custom]
397409
typedef string OfferId;
398410

src/error.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ pub enum Error {
1717
InvoiceRequestCreationFailed,
1818
/// Offer creation failed.
1919
OfferCreationFailed,
20+
/// Refund creation failed.
21+
RefundCreationFailed,
2022
/// Sending a payment has failed.
2123
PaymentSendingFailed,
2224
/// Sending a payment probe has failed.
@@ -67,6 +69,8 @@ pub enum Error {
6769
InvalidInvoice,
6870
/// The given offer is invalid.
6971
InvalidOffer,
72+
/// The given refund is invalid.
73+
InvalidRefund,
7074
/// The given channel ID is invalid.
7175
InvalidChannelId,
7276
/// The given network is invalid.
@@ -95,6 +99,7 @@ impl fmt::Display for Error {
9599
Self::InvoiceCreationFailed => write!(f, "Failed to create invoice."),
96100
Self::InvoiceRequestCreationFailed => write!(f, "Failed to create invoice request."),
97101
Self::OfferCreationFailed => write!(f, "Failed to create offer."),
102+
Self::RefundCreationFailed => write!(f, "Failed to create refund."),
98103
Self::PaymentSendingFailed => write!(f, "Failed to send the given payment."),
99104
Self::ProbeSendingFailed => write!(f, "Failed to send the given payment probe."),
100105
Self::ChannelCreationFailed => write!(f, "Failed to create channel."),
@@ -122,6 +127,7 @@ impl fmt::Display for Error {
122127
Self::InvalidAmount => write!(f, "The given amount is invalid."),
123128
Self::InvalidInvoice => write!(f, "The given invoice is invalid."),
124129
Self::InvalidOffer => write!(f, "The given offer is invalid."),
130+
Self::InvalidRefund => write!(f, "The given refund is invalid."),
125131
Self::InvalidChannelId => write!(f, "The given channel ID is invalid."),
126132
Self::InvalidNetwork => write!(f, "The given network is invalid."),
127133
Self::DuplicatePayment => {

src/payment/bolt12.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@ use crate::payment::store::{
1111
use crate::types::ChannelManager;
1212

1313
use lightning::ln::channelmanager::{PaymentId, Retry};
14+
use lightning::offers::invoice::Bolt12Invoice;
1415
use lightning::offers::offer::{Amount, Offer};
1516
use lightning::offers::parse::Bolt12SemanticError;
17+
use lightning::offers::refund::Refund;
1618

1719
use rand::RngCore;
1820

1921
use std::sync::{Arc, RwLock};
22+
use std::time::{Duration, SystemTime, UNIX_EPOCH};
2023

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

265268
Ok(offer)
266269
}
270+
271+
/// Requests a refund payment for the given [`Refund`].
272+
///
273+
/// The returned [`Bolt12Invoice`] is for informational purposes only (i.e., isn't needed to
274+
/// retrieve the refund).
275+
pub fn request_refund(&self, refund: &Refund) -> Result<Bolt12Invoice, Error> {
276+
let invoice = self.channel_manager.request_refund_payment(refund).map_err(|e| {
277+
log_error!(self.logger, "Failed to request refund payment: {:?}", e);
278+
Error::InvoiceRequestCreationFailed
279+
})?;
280+
281+
let payment_hash = invoice.payment_hash();
282+
let payment_id = PaymentId(payment_hash.0);
283+
284+
let payment = PaymentDetails {
285+
id: payment_id,
286+
kind: PaymentKind::Bolt12Refund {
287+
hash: Some(payment_hash),
288+
preimage: None,
289+
secret: None,
290+
},
291+
amount_msat: Some(refund.amount_msats()),
292+
direction: PaymentDirection::Inbound,
293+
status: PaymentStatus::Pending,
294+
};
295+
296+
self.payment_store.insert(payment)?;
297+
298+
Ok(invoice)
299+
}
300+
301+
/// Returns a [`Refund`] object that can be used to offer a refund payment of the amount given.
302+
pub fn create_refund(&self, amount_msat: u64, expiry_secs: u32) -> Result<Refund, Error> {
303+
let mut random_bytes = [0u8; 32];
304+
rand::thread_rng().fill_bytes(&mut random_bytes);
305+
let payment_id = PaymentId(random_bytes);
306+
307+
let expiration = (SystemTime::now() + Duration::from_secs(expiry_secs as u64))
308+
.duration_since(UNIX_EPOCH)
309+
.unwrap();
310+
let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT);
311+
let max_total_routing_fee_msat = None;
312+
313+
let refund = self
314+
.channel_manager
315+
.create_refund_builder(
316+
amount_msat,
317+
expiration,
318+
payment_id,
319+
retry_strategy,
320+
max_total_routing_fee_msat,
321+
)
322+
.map_err(|e| {
323+
log_error!(self.logger, "Failed to create refund builder: {:?}", e);
324+
Error::RefundCreationFailed
325+
})?
326+
.build()
327+
.map_err(|e| {
328+
log_error!(self.logger, "Failed to create refund: {:?}", e);
329+
Error::RefundCreationFailed
330+
})?;
331+
332+
log_info!(self.logger, "Offering refund of {}msat", amount_msat);
333+
334+
let kind = PaymentKind::Bolt12Refund { hash: None, preimage: None, secret: None };
335+
336+
let payment = PaymentDetails {
337+
id: payment_id,
338+
kind,
339+
amount_msat: Some(amount_msat),
340+
direction: PaymentDirection::Outbound,
341+
status: PaymentStatus::Pending,
342+
};
343+
344+
self.payment_store.insert(payment)?;
345+
346+
Ok(refund)
347+
}
267348
}

src/uniffi_types.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ pub use crate::payment::store::{LSPFeeLimits, PaymentDirection, PaymentKind, Pay
22

33
pub use lightning::events::{ClosureReason, PaymentFailureReason};
44
pub use lightning::ln::{ChannelId, PaymentHash, PaymentPreimage, PaymentSecret};
5+
pub use lightning::offers::invoice::Bolt12Invoice;
56
pub use lightning::offers::offer::{Offer, OfferId};
7+
pub use lightning::offers::refund::Refund;
68
pub use lightning::util::string::UntrustedString;
79

810
pub use lightning_invoice::Bolt11Invoice;
@@ -21,6 +23,7 @@ use bitcoin::hashes::sha256::Hash as Sha256;
2123
use bitcoin::hashes::Hash;
2224
use bitcoin::secp256k1::PublicKey;
2325
use lightning::ln::channelmanager::PaymentId;
26+
use lightning::util::ser::Writeable;
2427
use lightning_invoice::SignedRawBolt11Invoice;
2528

2629
use std::convert::TryInto;
@@ -88,6 +91,35 @@ impl UniffiCustomTypeConverter for Offer {
8891
}
8992
}
9093

94+
impl UniffiCustomTypeConverter for Refund {
95+
type Builtin = String;
96+
97+
fn into_custom(val: Self::Builtin) -> uniffi::Result<Self> {
98+
Refund::from_str(&val).map_err(|_| Error::InvalidRefund.into())
99+
}
100+
101+
fn from_custom(obj: Self) -> Self::Builtin {
102+
obj.to_string()
103+
}
104+
}
105+
106+
impl UniffiCustomTypeConverter for Bolt12Invoice {
107+
type Builtin = String;
108+
109+
fn into_custom(val: Self::Builtin) -> uniffi::Result<Self> {
110+
if let Some(bytes_vec) = hex_utils::to_vec(&val) {
111+
if let Ok(invoice) = Bolt12Invoice::try_from(bytes_vec) {
112+
return Ok(invoice);
113+
}
114+
}
115+
Err(Error::InvalidInvoice.into())
116+
}
117+
118+
fn from_custom(obj: Self) -> Self::Builtin {
119+
hex_utils::to_string(&obj.encode())
120+
}
121+
}
122+
91123
impl UniffiCustomTypeConverter for OfferId {
92124
type Builtin = String;
93125

tests/integration_tests_rust.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,4 +487,47 @@ fn simple_bolt12_send_receive() {
487487
},
488488
}
489489
assert_eq!(node_b_payments.first().unwrap().amount_msat, Some(expected_amount_msat));
490+
491+
// Now node_b refunds the amount node_a just overpaid.
492+
let overpaid_amount = expected_amount_msat - offer_amount_msat;
493+
let refund = node_b.bolt12_payment().create_refund(overpaid_amount, 3600).unwrap();
494+
let invoice = node_a.bolt12_payment().request_refund(&refund).unwrap();
495+
expect_payment_received_event!(node_a, overpaid_amount);
496+
497+
let node_b_payment_id = node_b
498+
.list_payments_with_filter(|p| p.amount_msat == Some(overpaid_amount))
499+
.first()
500+
.unwrap()
501+
.id;
502+
expect_payment_successful_event!(node_b, Some(node_b_payment_id), None);
503+
504+
let node_b_payments = node_b.list_payments_with_filter(|p| p.id == node_b_payment_id);
505+
assert_eq!(node_b_payments.len(), 1);
506+
match node_b_payments.first().unwrap().kind {
507+
PaymentKind::Bolt12Refund { hash, preimage, secret: _ } => {
508+
assert!(hash.is_some());
509+
assert!(preimage.is_some());
510+
//TODO: We should eventually set and assert the secret sender-side, too, but the BOLT12
511+
//API currently doesn't allow to do that.
512+
},
513+
_ => {
514+
panic!("Unexpected payment kind");
515+
},
516+
}
517+
assert_eq!(node_b_payments.first().unwrap().amount_msat, Some(overpaid_amount));
518+
519+
let node_a_payment_id = PaymentId(invoice.payment_hash().0);
520+
let node_a_payments = node_a.list_payments_with_filter(|p| p.id == node_a_payment_id);
521+
assert_eq!(node_a_payments.len(), 1);
522+
match node_a_payments.first().unwrap().kind {
523+
PaymentKind::Bolt12Refund { hash, preimage, secret } => {
524+
assert!(hash.is_some());
525+
assert!(preimage.is_some());
526+
assert!(secret.is_some());
527+
},
528+
_ => {
529+
panic!("Unexpected payment kind");
530+
},
531+
}
532+
assert_eq!(node_a_payments.first().unwrap().amount_msat, Some(overpaid_amount));
490533
}

0 commit comments

Comments
 (0)