Skip to content

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

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ panic = 'abort' # Abort on panic
default = []

[dependencies]
lightning = { version = "0.1.0", features = ["std"] }
lightning = { version = "0.1.0", features = ["std", "dnssec"] }
lightning-types = { version = "0.2.0" }
lightning-invoice = { version = "0.33.0", features = ["std"] }
lightning-net-tokio = { version = "0.1.0" }
Expand Down
12 changes: 10 additions & 2 deletions bindings/ldk_node.udl
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ dictionary Config {
u64 probing_liquidity_limit_multiplier;
AnchorChannelsConfig? anchor_channels_config;
SendingParameters? sending_parameters;
sequence<PublicKey>? dns_resolvers_node_ids;
};

dictionary AnchorChannelsConfig {
Expand Down Expand Up @@ -94,6 +95,8 @@ interface Builder {
[Throws=BuildError]
void set_node_alias(string node_alias);
[Throws=BuildError]
void set_dns_resolvers(sequence<PublicKey> dns_resolvers_node_ids);
[Throws=BuildError]
Node build();
[Throws=BuildError]
Node build_with_fs_store();
Expand Down Expand Up @@ -197,6 +200,8 @@ interface Bolt12Payment {
[Throws=NodeError]
PaymentId send_using_amount([ByRef]Offer offer, u64 amount_msat, u64? quantity, string? payer_note);
[Throws=NodeError]
PaymentId send_to_human_readable_name([ByRef]string name, u64 amount_msat);
[Throws=NodeError]
Offer receive(u64 amount_msat, [ByRef]string description, u32? expiry_secs, u64? quantity);
[Throws=NodeError]
Offer receive_variable_amount([ByRef]string description, u32? expiry_secs);
Expand Down Expand Up @@ -302,6 +307,8 @@ enum NodeError {
"InsufficientFunds",
"LiquiditySourceUnavailable",
"LiquidityFeeTooHigh",
"HrnParsingFailed",
"DnsResolversNotConfigured",
};

dictionary NodeStatus {
Expand Down Expand Up @@ -337,6 +344,7 @@ enum BuildError {
"WalletSetupFailed",
"LoggerSetupFailed",
"NetworkMismatch",
"DnsResolversEmpty",
};

[Trait]
Expand Down Expand Up @@ -402,7 +410,7 @@ interface PaymentKind {
Onchain(Txid txid, ConfirmationStatus status);
Bolt11(PaymentHash hash, PaymentPreimage? preimage, PaymentSecret? secret);
Bolt11Jit(PaymentHash hash, PaymentPreimage? preimage, PaymentSecret? secret, u64? counterparty_skimmed_fee_msat, LSPFeeLimits lsp_fee_limits);
Bolt12Offer(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, OfferId offer_id, UntrustedString? payer_note, u64? quantity);
Bolt12Offer(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, OfferId? offer_id, UntrustedString? payer_note, u64? quantity);
Bolt12Refund(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, UntrustedString? payer_note, u64? quantity);
Spontaneous(PaymentHash hash, PaymentPreimage? preimage);
};
Expand Down Expand Up @@ -797,4 +805,4 @@ typedef string NodeAlias;
typedef string OrderId;

[Custom]
typedef string DateTime;
typedef string DateTime;
23 changes: 23 additions & 0 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ pub enum BuildError {
LoggerSetupFailed,
/// The given network does not match the node's previously configured network.
NetworkMismatch,
/// The dns_resolvers list provided for HRN resolution is empty
DnsResolversEmpty,
}

impl fmt::Display for BuildError {
Expand Down Expand Up @@ -199,6 +201,9 @@ impl fmt::Display for BuildError {
Self::NetworkMismatch => {
write!(f, "Given network does not match the node's previously configured network.")
},
Self::DnsResolversEmpty => {
write!(f, "The dns_resolvers list provided for HRN resolution is empty.")
},
}
}
}
Expand Down Expand Up @@ -457,6 +462,17 @@ impl NodeBuilder {
Ok(self)
}

/// Sets the default dns_resolvers to be used when sending payments to HRNs.
pub fn set_dns_resolvers(
&mut self, dns_resolvers_node_ids: Vec<PublicKey>,
) -> Result<&mut Self, BuildError> {
if dns_resolvers_node_ids.is_empty() {
return Err(BuildError::DnsResolversEmpty);
}
self.config.dns_resolvers_node_ids = Some(dns_resolvers_node_ids);
Ok(self)
}

/// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options
/// previously configured.
pub fn build(&self) -> Result<Node, BuildError> {
Expand Down Expand Up @@ -838,6 +854,13 @@ impl ArcedNodeBuilder {
self.inner.write().unwrap().set_node_alias(node_alias).map(|_| ())
}

/// Sets the default dns_resolvers to be used when sending payments to HRNs.
pub fn set_dns_resolvers(
&self, dns_resolvers_node_ids: Vec<PublicKey>,
) -> Result<(), BuildError> {
self.inner.write().unwrap().set_dns_resolvers(dns_resolvers_node_ids).map(|_| ())
}

/// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options
/// previously configured.
pub fn build(&self) -> Result<Arc<Node>, BuildError> {
Expand Down
7 changes: 7 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ pub const WALLET_KEYS_SEED_LEN: usize = 64;
/// | `log_level` | Debug |
/// | `anchor_channels_config` | Some(..) |
/// | `sending_parameters` | None |
/// | `dns_resolvers` | None |
///
/// See [`AnchorChannelsConfig`] and [`SendingParameters`] for more information regarding their
/// respective default values.
Expand Down Expand Up @@ -167,6 +168,11 @@ pub struct Config {
/// **Note:** If unset, default parameters will be used, and you will be able to override the
/// parameters on a per-payment basis in the corresponding method calls.
pub sending_parameters: Option<SendingParameters>,
/// The dns_resolvers node_ids to be used for resolving Human-readable Names.
///
/// If set to `Some`, the values set will be used as dns_resolvers when sending to HRNs.
/// **Note:** If set to `None`, payments to HRNs will fail.
pub dns_resolvers_node_ids: Option<Vec<PublicKey>>,
}

impl Default for Config {
Expand All @@ -181,6 +187,7 @@ impl Default for Config {
anchor_channels_config: Some(AnchorChannelsConfig::default()),
sending_parameters: None,
node_alias: None,
dns_resolvers_node_ids: None,
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ pub enum Error {
LiquiditySourceUnavailable,
/// The given operation failed due to the LSP's required opening fee being too high.
LiquidityFeeTooHigh,
/// Parsing a Human-Readable Name has failed
HrnParsingFailed,
/// The given operation failed due to `dns-resolvers` not being configured in builder.
DnsResolversNotConfigured,
}

impl fmt::Display for Error {
Expand Down Expand Up @@ -193,6 +197,12 @@ impl fmt::Display for Error {
Self::LiquidityFeeTooHigh => {
write!(f, "The given operation failed due to the LSP's required opening fee being too high.")
},
Self::HrnParsingFailed => {
write!(f, "Failed to parse a human-readable name.")
},
Self::DnsResolversNotConfigured => {
write!(f, "The given operation failed due to `dns-resolvers` not being configured in builder.")
},
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down
7 changes: 7 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -829,6 +829,11 @@ impl Node {
self.config.node_alias
}

/// Returns the list of dns_resolvers that will be used to resolve HRNs.
pub fn dns_resolvers(&self) -> Option<Vec<PublicKey>> {
self.config.dns_resolvers_node_ids.clone()
}

/// Returns a payment handler allowing to create and pay [BOLT 11] invoices.
///
/// [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md
Expand Down Expand Up @@ -873,6 +878,7 @@ impl Node {
Arc::clone(&self.channel_manager),
Arc::clone(&self.payment_store),
Arc::clone(&self.logger),
Arc::clone(&self.config),
)
}

Expand All @@ -886,6 +892,7 @@ impl Node {
Arc::clone(&self.channel_manager),
Arc::clone(&self.payment_store),
Arc::clone(&self.logger),
Arc::clone(&self.config),
))
}

Expand Down
103 changes: 96 additions & 7 deletions src/payment/bolt12.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::logger::{log_error, log_info, LdkLogger, Logger};
use crate::payment::store::{
Expand All @@ -22,6 +22,8 @@ use lightning::offers::invoice::Bolt12Invoice;
use lightning::offers::offer::{Amount, Offer, Quantity};
use lightning::offers::parse::Bolt12SemanticError;
use lightning::offers::refund::Refund;
use lightning::onion_message::dns_resolution::HumanReadableName;
use lightning::onion_message::messenger::Destination;
use lightning::util::string::UntrustedString;

use rand::RngCore;
Expand All @@ -41,15 +43,16 @@ pub struct Bolt12Payment {
channel_manager: Arc<ChannelManager>,
payment_store: Arc<PaymentStore<Arc<Logger>>>,
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<Arc<Logger>>>,
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.
Expand Down Expand Up @@ -105,7 +108,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,
};
Expand All @@ -130,7 +133,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,
};
Expand Down Expand Up @@ -211,7 +214,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,
};
Expand All @@ -236,7 +239,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,
};
Expand All @@ -256,6 +259,92 @@ 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 `[email protected]`.
///
/// 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 is set to `None`, this operation will fail.
pub fn send_to_human_readable_name(
&self, name: &str, amount_msat: u64,
) -> Result<PaymentId, Error> {
let rt_lock = self.runtime.read().unwrap();
if rt_lock.is_none() {
return Err(Error::NotRunning);
}

let hrn = HumanReadableName::from_encoded(&name).map_err(|_| Error::HrnParsingFailed)?;

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 dns_resolvers = match &self.config.dns_resolvers_node_ids {
Some(dns_resolvers) => Ok(dns_resolvers.clone()),
None => Err(Error::DnsResolversNotConfigured),
}?;

let destinations: Vec<Destination> =
dns_resolvers.into_iter().map(|public_key| Destination::Node(public_key)).collect();

match self.channel_manager.pay_for_offer_from_human_readable_name(
hrn.clone(),
amount_msat,
payment_id,
retry_strategy,
max_total_routing_fee_msat,
destinations,
) {
Ok(()) => {
log_info!(self.logger, "Initiated sending {} msats to {}", amount_msat, name);
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::Pending,
);
self.payment_store.insert(payment)?;
Ok(payment_id)
},
Err(()) => {
log_error!(self.logger, "Failed to send payment to {}", name);
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::Pending,
);
self.payment_store.insert(payment)?;
Err(Error::PaymentSendingFailed)
},
}
}

/// Returns a payable offer that can be used to request and receive a payment of the amount
/// given.
pub fn receive(
Expand Down
2 changes: 1 addition & 1 deletion src/payment/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ pub enum PaymentKind {
/// The secret used by the payment.
secret: Option<PaymentSecret>,
/// The ID of the offer this payment is for.
offer_id: OfferId,
offer_id: Option<OfferId>,
/// The payer note for the payment.
///
/// Truncated to [`PAYER_NOTE_LIMIT`] characters.
Expand Down
Loading
Loading