-
Notifications
You must be signed in to change notification settings - Fork 109
Add support for sending to human-readable names (BIP 353) #528
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -742,7 +742,7 @@ where | |
hash: Some(payment_hash), | ||
preimage: payment_preimage, | ||
secret: Some(payment_secret), | ||
offer_id, | ||
offer_id: Some(offer_id), | ||
payer_note, | ||
quantity, | ||
}; | ||
|
@@ -1417,8 +1417,24 @@ where | |
); | ||
} | ||
}, | ||
LdkEvent::InvoiceReceived { .. } => { | ||
debug_assert!(false, "We currently don't handle BOLT12 invoices manually, so this event should never be emitted."); | ||
LdkEvent::InvoiceReceived { payment_id, invoice, context, responder: _ } => { | ||
let update = PaymentDetailsUpdate { | ||
hash: Some(Some(invoice.payment_hash())), | ||
quantity: invoice.quantity(), | ||
..PaymentDetailsUpdate::new(payment_id) | ||
}; | ||
|
||
match self.payment_store.update(&update) { | ||
Ok(_) => {}, | ||
Err(e) => { | ||
log_error!(self.logger, "Failed to access payment store: {}", e); | ||
return Err(ReplayEvent()); | ||
}, | ||
}; | ||
|
||
let _ = self | ||
.channel_manager | ||
.send_payment_for_bolt12_invoice(&invoice, context.as_ref()); | ||
Comment on lines
+1435
to
+1437
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't handle the error here because I'm not sure if we want to return a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, true, but we should at the very least log the error here I think. |
||
}, | ||
LdkEvent::ConnectionNeeded { node_id, addresses } => { | ||
let runtime_lock = self.runtime.read().unwrap(); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,7 +9,7 @@ | |
//! | ||
//! [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md | ||
use crate::config::LDK_PAYMENT_RETRY_TIMEOUT; | ||
use crate::config::{Config, LDK_PAYMENT_RETRY_TIMEOUT}; | ||
use crate::error::Error; | ||
use crate::ffi::{maybe_deref, maybe_wrap}; | ||
use crate::logger::{log_error, log_info, LdkLogger, Logger}; | ||
|
@@ -19,6 +19,7 @@ use crate::types::{ChannelManager, PaymentStore}; | |
use lightning::ln::channelmanager::{PaymentId, Retry}; | ||
use lightning::offers::offer::{Amount, Offer as LdkOffer, Quantity}; | ||
use lightning::offers::parse::Bolt12SemanticError; | ||
use lightning::onion_message::messenger::Destination; | ||
use lightning::util::string::UntrustedString; | ||
|
||
use rand::RngCore; | ||
|
@@ -42,6 +43,11 @@ type Refund = lightning::offers::refund::Refund; | |
#[cfg(feature = "uniffi")] | ||
type Refund = Arc<crate::ffi::Refund>; | ||
|
||
#[cfg(not(feature = "uniffi"))] | ||
type HumanReadableName = lightning::onion_message::dns_resolution::HumanReadableName; | ||
#[cfg(feature = "uniffi")] | ||
type HumanReadableName = Arc<crate::ffi::HumanReadableName>; | ||
|
||
/// A payment handler allowing to create and pay [BOLT 12] offers and refunds. | ||
/// | ||
/// Should be retrieved by calling [`Node::bolt12_payment`]. | ||
|
@@ -53,15 +59,16 @@ pub struct Bolt12Payment { | |
channel_manager: Arc<ChannelManager>, | ||
payment_store: Arc<PaymentStore>, | ||
logger: Arc<Logger>, | ||
config: Arc<Config>, | ||
} | ||
|
||
impl Bolt12Payment { | ||
pub(crate) fn new( | ||
runtime: Arc<RwLock<Option<Arc<tokio::runtime::Runtime>>>>, | ||
channel_manager: Arc<ChannelManager>, payment_store: Arc<PaymentStore>, | ||
logger: Arc<Logger>, | ||
logger: Arc<Logger>, config: Arc<Config>, | ||
) -> Self { | ||
Self { runtime, channel_manager, payment_store, logger } | ||
Self { runtime, channel_manager, payment_store, logger, config } | ||
} | ||
|
||
/// Send a payment given an offer. | ||
|
@@ -118,7 +125,7 @@ impl Bolt12Payment { | |
hash: None, | ||
preimage: None, | ||
secret: None, | ||
offer_id: offer.id(), | ||
offer_id: Some(offer.id()), | ||
payer_note: payer_note.map(UntrustedString), | ||
quantity, | ||
}; | ||
|
@@ -143,7 +150,7 @@ impl Bolt12Payment { | |
hash: None, | ||
preimage: None, | ||
secret: None, | ||
offer_id: offer.id(), | ||
offer_id: Some(offer.id()), | ||
payer_note: payer_note.map(UntrustedString), | ||
quantity, | ||
}; | ||
|
@@ -225,7 +232,7 @@ impl Bolt12Payment { | |
hash: None, | ||
preimage: None, | ||
secret: None, | ||
offer_id: offer.id(), | ||
offer_id: Some(offer.id()), | ||
payer_note: payer_note.map(UntrustedString), | ||
quantity, | ||
}; | ||
|
@@ -250,7 +257,7 @@ impl Bolt12Payment { | |
hash: None, | ||
preimage: None, | ||
secret: None, | ||
offer_id: offer.id(), | ||
offer_id: Some(offer.id()), | ||
payer_note: payer_note.map(UntrustedString), | ||
quantity, | ||
}; | ||
|
@@ -270,6 +277,105 @@ impl Bolt12Payment { | |
} | ||
} | ||
|
||
/// Send a payment to an offer resolved from a Human-Readable Name ([BIP 353]). | ||
/// | ||
/// Paying to Human-Readable Names makes it more intuitive to make payments for offers | ||
/// as users can simply send payments to HRNs such as `user@example.com`. | ||
/// | ||
/// This can be used to pay so-called "zero-amount" offers, i.e., an offer that leaves the | ||
/// amount paid to be determined by the user. | ||
/// | ||
/// If `dns_resolvers_node_ids` in [`Config.hrn_config`] is empty, this operation will fail. | ||
/// | ||
/// [BIP 353]: https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki | ||
pub fn send_to_human_readable_name( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you think we can add a test case to check that this works somehow? Or would we need to wait for #436 to be able to create an end-to-end test? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we can intercept the I think the end-to-end test would be the ideal, eventually. |
||
&self, hrn: &HumanReadableName, amount_msat: u64, | ||
) -> Result<PaymentId, Error> { | ||
let hrn = maybe_deref(hrn); | ||
let rt_lock = self.runtime.read().unwrap(); | ||
if rt_lock.is_none() { | ||
return Err(Error::NotRunning); | ||
} | ||
|
||
let mut random_bytes = [0u8; 32]; | ||
rand::thread_rng().fill_bytes(&mut random_bytes); | ||
let payment_id = PaymentId(random_bytes); | ||
let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT); | ||
let max_total_routing_fee_msat = None; | ||
|
||
let destinations: Vec<Destination> = match &self.config.hrn_config { | ||
Some(hrn_config) => Ok(hrn_config | ||
.dns_resolvers_node_ids | ||
.iter() | ||
.map(|node_id| Destination::Node(*node_id)) | ||
.collect()), | ||
None => Err(Error::DnsResolversUnavailable), | ||
}?; | ||
|
||
match self.channel_manager.pay_for_offer_from_human_readable_name( | ||
hrn.clone(), | ||
tnull marked this conversation as resolved.
Show resolved
Hide resolved
|
||
amount_msat, | ||
payment_id, | ||
retry_strategy, | ||
max_total_routing_fee_msat, | ||
destinations, | ||
) { | ||
Ok(()) => { | ||
log_info!( | ||
self.logger, | ||
"Initiated sending {} msats to ₿{}@{}", | ||
amount_msat, | ||
hrn.user(), | ||
hrn.domain() | ||
); | ||
let kind = PaymentKind::Bolt12Offer { | ||
tnull marked this conversation as resolved.
Show resolved
Hide resolved
|
||
hash: None, | ||
preimage: None, | ||
secret: None, | ||
offer_id: None, | ||
payer_note: None, | ||
quantity: None, | ||
}; | ||
let payment = PaymentDetails::new( | ||
payment_id, | ||
kind, | ||
Some(amount_msat), | ||
None, | ||
PaymentDirection::Outbound, | ||
PaymentStatus::Pending, | ||
); | ||
self.payment_store.insert(payment)?; | ||
Ok(payment_id) | ||
}, | ||
Err(()) => { | ||
log_error!( | ||
self.logger, | ||
"Failed to send payment to ₿{}@{}", | ||
hrn.user(), | ||
hrn.domain() | ||
); | ||
let kind = PaymentKind::Bolt12Offer { | ||
hash: None, | ||
preimage: None, | ||
secret: None, | ||
offer_id: None, | ||
payer_note: None, | ||
quantity: None, | ||
}; | ||
let payment = PaymentDetails::new( | ||
payment_id, | ||
kind, | ||
Some(amount_msat), | ||
None, | ||
PaymentDirection::Outbound, | ||
PaymentStatus::Failed, | ||
); | ||
self.payment_store.insert(payment)?; | ||
Err(Error::PaymentSendingFailed) | ||
}, | ||
} | ||
} | ||
|
||
pub(crate) fn receive_inner( | ||
&self, amount_msat: u64, description: &str, expiry_secs: Option<u32>, quantity: Option<u64>, | ||
) -> Result<LdkOffer, Error> { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -404,7 +404,11 @@ pub enum PaymentKind { | |
/// The secret used by the payment. | ||
secret: Option<PaymentSecret>, | ||
/// The ID of the offer this payment is for. | ||
offer_id: OfferId, | ||
/// | ||
/// This will be set to `None` when sending payments to Human-Readable Names ([BIP 353]). | ||
/// | ||
/// [BIP 353]: https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki | ||
offer_id: Option<OfferId>, | ||
tnull marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please describe when this field would be set / unset. |
||
/// The payer note for the payment. | ||
/// | ||
/// Truncated to [`PAYER_NOTE_LIMIT`] characters. | ||
|
@@ -470,7 +474,7 @@ impl_writeable_tlv_based_enum!(PaymentKind, | |
(2, preimage, option), | ||
(3, quantity, option), | ||
(4, secret, option), | ||
(6, offer_id, required), | ||
(6, offer_id, option), | ||
}, | ||
(8, Spontaneous) => { | ||
(0, hash, required), | ||
|
@@ -542,6 +546,7 @@ pub(crate) struct PaymentDetailsUpdate { | |
pub direction: Option<PaymentDirection>, | ||
pub status: Option<PaymentStatus>, | ||
pub confirmation_status: Option<ConfirmationStatus>, | ||
pub quantity: Option<u64>, | ||
} | ||
|
||
impl PaymentDetailsUpdate { | ||
|
@@ -557,6 +562,7 @@ impl PaymentDetailsUpdate { | |
direction: None, | ||
status: None, | ||
confirmation_status: None, | ||
quantity: None, | ||
} | ||
} | ||
} | ||
|
@@ -584,6 +590,11 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate { | |
_ => None, | ||
}; | ||
|
||
let quantity = match value.kind { | ||
PaymentKind::Bolt12Offer { quantity, .. } => quantity, | ||
_ => None, | ||
}; | ||
|
||
Self { | ||
id: value.id, | ||
hash: Some(hash), | ||
|
@@ -595,6 +606,7 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate { | |
direction: Some(value.direction), | ||
status: Some(value.status), | ||
confirmation_status, | ||
quantity, | ||
} | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Not sure why this change here was included. Please revert.