Skip to content
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

Add/implement HRN (Human-Readable Name) support using a sub-module #437

Open
wants to merge 1 commit 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
4 changes: 4 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ pub enum Error {
LiquiditySourceUnavailable,
/// The given operation failed due to the LSP's required opening fee being too high.
LiquidityFeeTooHigh,
/// Failed to resolve the HRN to an offer.
HrnResolutionFailed,
/// The provided HRN is invalid or malformed.
InvalidHrn,
}

impl fmt::Display for Error {
Expand Down
160 changes: 160 additions & 0 deletions src/payment/hrn.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// src/payment/hrn.rs

use crate::error::Error;
use crate::logger::{log_error, log_info, Logger};
use crate::payment::store::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, PaymentStore};
use crate::types::ChannelManager;

use lightning::ln::channelmanager::{PaymentId, Retry};
use lightning::offers::offer::{Amount, Offer, Quantity};
use lightning::offers::parse::Bolt12SemanticError;
use lightning::util::string::UntrustedString;

use std::sync::{Arc, RwLock};

/// A payment handler for sending payments to Human-Readable Names (HRNs).
pub struct HrnPayment {
runtime: Arc<RwLock<Option<Arc<tokio::runtime::Runtime>>>>,
channel_manager: Arc<ChannelManager>,
payment_store: Arc<PaymentStore<Arc<Logger>>>,
logger: Arc<Logger>,
}

impl HrnPayment {
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>,
) -> Self {
Self { runtime, channel_manager, payment_store, logger }
}

/// Send a payment to a Human-Readable Name (HRN).
///
/// This method resolves the HRN to an offer and sends the payment.
///
/// If `payer_note` is `Some`, it will be seen by the recipient and reflected back in the invoice.
/// If `quantity` is `Some`, it represents the number of items requested.
pub fn send_to_hrn(
&self, hrn: &str, quantity: Option<u64>, payer_note: Option<String>,
) -> Result<PaymentId, Error> {
let rt_lock = self.runtime.read().unwrap();
if rt_lock.is_none() {
return Err(Error::NotRunning);
}

// Resolve the HRN to an offer
let offer = self.resolve_hrn_to_offer(hrn)?;

// Use the existing payment logic to send the payment
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;

match self.channel_manager.pay_for_offer(
&offer,
quantity,
None,
payer_note.clone(),
payment_id,
retry_strategy,
max_total_routing_fee_msat,
) {
Ok(()) => {
let payee_pubkey = offer.issuer_signing_pubkey();
log_info!(
self.logger,
"Initiated sending payment to HRN: {} (payee: {:?})",
hrn,
payee_pubkey
);

let kind = PaymentKind::Bolt12Offer {
hash: None,
preimage: None,
secret: None,
offer_id: offer.id(),
payer_note: payer_note.map(UntrustedString),
quantity,
};
let payment = PaymentDetails::new(
payment_id,
kind,
None, // Amount will be set by the offer
PaymentDirection::Outbound,
PaymentStatus::Pending,
);
self.payment_store.insert(payment)?;

Ok(payment_id)
}
Err(e) => {
log_error!(self.logger, "Failed to send payment to HRN: {:?}", e);
match e {
Bolt12SemanticError::DuplicatePaymentId => Err(Error::DuplicatePayment),
_ => Err(Error::PaymentSendingFailed),
}
}
}
}

/// Resolves a Human-Readable Name (HRN) to an offer.
///
/// This is a placeholder for actual HRN resolution logic.
fn resolve_hrn_to_offer(&self, hrn: &str) -> Result<Offer, Error> {
// Placeholder logic for resolving HRN to an offer
log_info!(self.logger, "Resolving HRN: {}", hrn);

// For now, return a mock offer
let offer_builder = self.channel_manager.create_offer_builder(None).map_err(|e| {
log_error!(self.logger, "Failed to create offer builder: {:?}", e);
Error::OfferCreationFailed
})?;

let offer = offer_builder
Copy link

@TheBlueMatt TheBlueMatt Jan 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're building an offer which we can use to receive payment to ourselves and then trying to pay it, paying ourselves. Instead, you need to look at the lightning-dns-resolver crate and its contained resolver to resolve the HRN to a URI.

.amount_msats(1000) // Example amount
.description(hrn.to_string())
.build()
.map_err(|e| {
log_error!(self.logger, "Failed to create offer: {:?}", e);
Error::OfferCreationFailed
})?;

Ok(offer)
}
}


#[cfg(test)]
mod tests {
use super::*;
use crate::logger::TestLogger;
use crate::types::TestChannelManager;

#[test]
fn test_send_to_hrn() {
let runtime = Arc::new(RwLock::new(Some(Arc::new(tokio::runtime::Runtime::new().unwrap()))));
let channel_manager = Arc::new(TestChannelManager::new());
let payment_store = Arc::new(PaymentStore::new(Vec::new(), Arc::new(TestStore::new(false)), Arc::new(TestLogger::new())));
let logger = Arc::new(TestLogger::new());

let hrn_payment = HrnPayment::new(runtime, channel_manager, payment_store, logger);
let result = hrn_payment.send_to_hrn("example.hrn", None, None);
assert!(result.is_ok());
}

#[test]
fn test_resolve_hrn_to_offer() {
let runtime = Arc::new(RwLock::new(Some(Arc::new(tokio::runtime::Runtime::new().unwrap()))));
let channel_manager = Arc::new(TestChannelManager::new());
let payment_store = Arc::new(PaymentStore::new(Vec::new(), Arc::new(TestStore::new(false)), Arc::new(TestLogger::new())));
let logger = Arc::new(TestLogger::new());

let hrn_payment = HrnPayment::new(runtime, channel_manager, payment_store, logger);
let result = hrn_payment.resolve_hrn_to_offer("example.hrn");
assert!(result.is_ok());
}
}
2 changes: 2 additions & 0 deletions src/payment/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ mod onchain;
mod spontaneous;
pub(crate) mod store;
mod unified_qr;
pub mod hrn;

pub use hrn::HrnPayment;
pub use bolt11::Bolt11Payment;
pub use bolt12::Bolt12Payment;
pub use onchain::OnchainPayment;
Expand Down
2 changes: 2 additions & 0 deletions src/payment/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ pub struct PaymentDetails {
pub status: PaymentStatus,
/// The timestamp, in seconds since start of the UNIX epoch, when this entry was last updated.
pub latest_update_timestamp: u64,
/// The HRN associated with this payment, if applicable.
pub hrn: Option<String>,
}

impl PaymentDetails {
Expand Down
Loading