Skip to content

Commit f3eba9a

Browse files
committed
Implement and test Refund flow
1 parent 8b2e4b6 commit f3eba9a

File tree

5 files changed

+154
-0
lines changed

5 files changed

+154
-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 offer_refund(u64 amount_msat, u32 expiry_secs);
112116
};
113117

114118
interface SpontaneousPayment {
@@ -135,6 +139,7 @@ enum NodeError {
135139
"ConnectionFailed",
136140
"InvoiceCreationFailed",
137141
"OfferCreationFailed",
142+
"RefundCreationFailed",
138143
"PaymentSendingFailed",
139144
"ProbeSendingFailed",
140145
"ChannelCreationFailed",
@@ -160,6 +165,7 @@ enum NodeError {
160165
"InvalidAmount",
161166
"InvalidInvoice",
162167
"InvalidOffer",
168+
"InvalidRefund",
163169
"InvalidChannelId",
164170
"InvalidNetwork",
165171
"DuplicatePayment",
@@ -392,6 +398,12 @@ typedef string Bolt11Invoice;
392398
[Custom]
393399
typedef string Offer;
394400

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

src/error.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ pub enum Error {
1515
InvoiceCreationFailed,
1616
/// Offer creation failed.
1717
OfferCreationFailed,
18+
/// Refund creation failed.
19+
RefundCreationFailed,
1820
/// Sending a payment has failed.
1921
PaymentSendingFailed,
2022
/// Sending a payment probe has failed.
@@ -65,6 +67,8 @@ pub enum Error {
6567
InvalidInvoice,
6668
/// The given offer is invalid.
6769
InvalidOffer,
70+
/// The given refund is invalid.
71+
InvalidRefund,
6872
/// The given channel ID is invalid.
6973
InvalidChannelId,
7074
/// The given network is invalid.
@@ -92,6 +96,7 @@ impl fmt::Display for Error {
9296
Self::ConnectionFailed => write!(f, "Network connection closed."),
9397
Self::InvoiceCreationFailed => write!(f, "Failed to create invoice."),
9498
Self::OfferCreationFailed => write!(f, "Failed to create offer."),
99+
Self::RefundCreationFailed => write!(f, "Failed to create refund."),
95100
Self::PaymentSendingFailed => write!(f, "Failed to send the given payment."),
96101
Self::ProbeSendingFailed => write!(f, "Failed to send the given payment probe."),
97102
Self::ChannelCreationFailed => write!(f, "Failed to create channel."),
@@ -119,6 +124,7 @@ impl fmt::Display for Error {
119124
Self::InvalidAmount => write!(f, "The given amount is invalid."),
120125
Self::InvalidInvoice => write!(f, "The given invoice is invalid."),
121126
Self::InvalidOffer => write!(f, "The given offer is invalid."),
127+
Self::InvalidRefund => write!(f, "The given refund is invalid."),
122128
Self::InvalidChannelId => write!(f, "The given channel ID is invalid."),
123129
Self::InvalidNetwork => write!(f, "The given network is invalid."),
124130
Self::DuplicatePayment => {

src/payment/bolt12.rs

Lines changed: 61 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,62 @@ 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+
self.channel_manager.request_refund_payment(refund).map_err(|e| {
277+
log_error!(self.logger, "Failed to request refund payment: {:?}", e);
278+
Error::InvalidRefund
279+
})
280+
}
281+
282+
/// Returns a [`Refund`] that can be used to offer a refund payment of the amount given.
283+
pub fn offer_refund(&self, amount_msat: u64, expiry_secs: u32) -> Result<Refund, Error> {
284+
let mut random_bytes = [0u8; 32];
285+
rand::thread_rng().fill_bytes(&mut random_bytes);
286+
let payment_id = PaymentId(random_bytes);
287+
288+
let expiration = (SystemTime::now() + Duration::from_secs(expiry_secs as u64))
289+
.duration_since(UNIX_EPOCH)
290+
.unwrap();
291+
let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT);
292+
let max_total_routing_fee_msat = None;
293+
294+
let refund_builder = self
295+
.channel_manager
296+
.create_refund_builder(
297+
amount_msat,
298+
expiration,
299+
payment_id,
300+
retry_strategy,
301+
max_total_routing_fee_msat,
302+
)
303+
.map_err(|e| {
304+
log_error!(self.logger, "Failed to create refund builder: {:?}", e);
305+
Error::RefundCreationFailed
306+
})?;
307+
let refund = refund_builder.build().map_err(|e| {
308+
log_error!(self.logger, "Failed to create refund: {:?}", e);
309+
Error::RefundCreationFailed
310+
})?;
311+
312+
log_info!(self.logger, "Offering refund of {}msat", amount_msat);
313+
314+
let kind = PaymentKind::Bolt12Refund { hash: None, preimage: None, secret: None };
315+
316+
let payment = PaymentDetails {
317+
id: payment_id,
318+
kind,
319+
amount_msat: Some(amount_msat),
320+
direction: PaymentDirection::Outbound,
321+
status: PaymentStatus::Pending,
322+
};
323+
324+
self.payment_store.insert(payment)?;
325+
326+
Ok(refund)
327+
}
267328
}

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().offer_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)