Skip to content

Commit cf0d482

Browse files
committed
refactor: move pay offer with name to a separate endpoint
Extract BIP-353 human-readable name payment into dedicated PayName RPC endpoint, separate from PayOffer. This improves API clarity by distinguishing direct offer payments from name-based payments that require DNS resolution.
1 parent c746a64 commit cf0d482

5 files changed

Lines changed: 181 additions & 129 deletions

File tree

proto/lndkrpc.proto

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import "external/google/rpc/error_details.proto";
55

66
service Offers {
77
rpc PayOffer (PayOfferRequest) returns (PayOfferResponse);
8+
rpc PayName (PayNameRequest) returns (PayOfferResponse);
89
rpc GetInvoice (GetInvoiceRequest) returns (GetInvoiceResponse);
910
rpc DecodeInvoice (DecodeInvoiceRequest) returns (Bolt12InvoiceContents);
1011
rpc PayInvoice (PayInvoiceRequest) returns (PayInvoiceResponse);
@@ -18,7 +19,15 @@ message PayOfferRequest {
1819
optional uint32 response_invoice_timeout = 4;
1920
optional uint32 fee_limit = 5;
2021
optional uint32 fee_limit_percent = 6;
21-
optional string name = 7;
22+
}
23+
24+
message PayNameRequest {
25+
string name = 1;
26+
optional uint64 amount = 2;
27+
optional string payer_note = 3;
28+
optional uint32 response_invoice_timeout = 4;
29+
optional uint32 fee_limit = 5;
30+
optional uint32 fee_limit_percent = 6;
2231
}
2332

2433
message PayOfferResponse {

src/cli.rs

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
use clap::{Parser, Subcommand};
22
use lightning::offers::invoice::Bolt12Invoice;
33
use lndk::lndkrpc::offers_client::OffersClient;
4-
use lndk::lndkrpc::{CreateOfferRequest, GetInvoiceRequest, PayInvoiceRequest, PayOfferRequest};
4+
use lndk::lndkrpc::{
5+
CreateOfferRequest, GetInvoiceRequest, PayInvoiceRequest, PayNameRequest, PayOfferRequest,
6+
};
57
use lndk::offers::decode;
68
use lndk::offers::handler::DEFAULT_RESPONSE_INVOICE_TIMEOUT;
79
use lndk::{
@@ -143,10 +145,34 @@ enum Commands {
143145
/// Mutually exclusive with fee_limit - only one can be set.
144146
#[arg(long, required = false, conflicts_with = "fee_limit")]
145147
fee_limit_percent: Option<u32>,
148+
},
149+
/// PayName pays a BOLT 12 offer by resolving a human-readable name (BIP-353).
150+
PayName {
151+
/// The human-readable name to resolve (e.g., "user@example.com").
152+
name: String,
146153

147-
/// The human readable name of the user to pay.
154+
/// Amount the user would like to pay. If this isn't set, we'll assume the user is paying
155+
/// whatever the offer amount is.
148156
#[arg(required = false)]
149-
name: Option<String>,
157+
amount: Option<u64>,
158+
159+
/// A payer-provided note which will be seen by the recipient.
160+
#[arg(required = false)]
161+
payer_note: Option<String>,
162+
163+
/// The amount of time in seconds that the user would like to wait for an invoice to
164+
/// arrive. If this isn't set, we'll use the default value.
165+
#[arg(long, global = false, required = false, default_value = DEFAULT_RESPONSE_INVOICE_TIMEOUT.to_string())]
166+
response_invoice_timeout: Option<u32>,
167+
168+
/// A fixed fee limit in millisatoshis.
169+
/// Mutually exclusive with fee_limit_percent - only one can be set.
170+
#[arg(long, required = false, conflicts_with = "fee_limit_percent")]
171+
fee_limit: Option<u32>,
172+
/// A percentage-based fee limit of the payment amount.
173+
/// Mutually exclusive with fee_limit - only one can be set.
174+
#[arg(long, required = false, conflicts_with = "fee_limit")]
175+
fee_limit_percent: Option<u32>,
150176
},
151177
/// GetInvoice fetch a BOLT 12 invoice, which will be returned as a hex-encoded string. It
152178
/// fetches the invoice from a BOLT 12 offer, provided as a 'lno'-prefaced offer string.
@@ -235,7 +261,6 @@ async fn main() {
235261
response_invoice_timeout,
236262
fee_limit,
237263
fee_limit_percent,
238-
name,
239264
} => {
240265
let tls = read_cert_from_args_or_exit(args.cert_pem, args.cert_path);
241266
let channel = create_grpc_channel(args.grpc_host, args.grpc_port, tls).await;
@@ -258,7 +283,6 @@ async fn main() {
258283
response_invoice_timeout,
259284
fee_limit,
260285
fee_limit_percent,
261-
name,
262286
});
263287
add_metadata(&mut request, macaroon);
264288

@@ -267,6 +291,38 @@ async fn main() {
267291
Err(err) => err.exit_gracefully(),
268292
};
269293
}
294+
Commands::PayName {
295+
ref name,
296+
amount,
297+
payer_note,
298+
response_invoice_timeout,
299+
fee_limit,
300+
fee_limit_percent,
301+
} => {
302+
let tls = read_cert_from_args_or_exit(args.cert_pem, args.cert_path);
303+
let channel = create_grpc_channel(args.grpc_host, args.grpc_port, tls).await;
304+
let (mut client, macaroon) = create_authenticated_client(
305+
channel,
306+
args.macaroon_path,
307+
args.macaroon_hex,
308+
&args.network,
309+
);
310+
311+
let mut request = Request::new(PayNameRequest {
312+
name: name.clone(),
313+
amount,
314+
payer_note,
315+
response_invoice_timeout,
316+
fee_limit,
317+
fee_limit_percent,
318+
});
319+
add_metadata(&mut request, macaroon);
320+
321+
match client.pay_name(request).await {
322+
Ok(_) => println!("Successfully paid for name {}!", name),
323+
Err(err) => err.exit_gracefully(),
324+
};
325+
}
270326
Commands::GetInvoice {
271327
ref offer_string,
272328
amount,

src/offers/handler.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ use super::lnd_requests::{
3131
send_invoice_request, LndkBolt12InvoiceInfo,
3232
};
3333
use super::OfferError;
34+
use crate::dns_resolver::LndkDNSResolverMessageHandler;
3435
use crate::offers::lnd_requests::{send_payment, track_payment, CreateOfferArgs};
36+
use crate::offers::{get_destination, parse::decode};
3537
use crate::onion_messenger::MessengerUtilities;
3638

3739
pub const DEFAULT_RESPONSE_INVOICE_TIMEOUT: u32 = 15;
@@ -77,6 +79,18 @@ pub struct PayOfferParams {
7779
pub fee_limit: Option<FeeLimit>,
7880
}
7981

82+
#[derive(Clone)]
83+
pub struct PayNameParams {
84+
pub name: String,
85+
pub amount: Option<u64>,
86+
pub payer_note: Option<String>,
87+
pub network: Network,
88+
pub client: Client,
89+
pub reply_path: Option<BlindedMessagePath>,
90+
pub response_invoice_timeout: Option<u32>,
91+
pub fee_limit: Option<FeeLimit>,
92+
}
93+
8094
#[derive(Clone)]
8195
pub struct SendPaymentParams {
8296
pub path: BlindedPaymentPath,
@@ -150,6 +164,30 @@ impl OfferHandler {
150164
.await
151165
}
152166

167+
/// Resolves a human-readable name (BIP-353) to an offer and pays it.
168+
pub async fn pay_name(&self, cfg: PayNameParams) -> Result<Payment, OfferError> {
169+
let offer_str = LndkDNSResolverMessageHandler::new()
170+
.resolve_name_to_offer(&cfg.name)
171+
.await?;
172+
173+
let offer = decode(offer_str)?;
174+
let destination = get_destination(&offer).await?;
175+
176+
let pay_offer_cfg = PayOfferParams {
177+
offer,
178+
amount: cfg.amount,
179+
payer_note: cfg.payer_note,
180+
network: cfg.network,
181+
client: cfg.client,
182+
destination,
183+
reply_path: cfg.reply_path,
184+
response_invoice_timeout: cfg.response_invoice_timeout,
185+
fee_limit: cfg.fee_limit,
186+
};
187+
188+
self.pay_offer(pay_offer_cfg).await
189+
}
190+
153191
/// Sends an invoice request and waits for an invoice to be sent back to us.
154192
/// Reminder that if this method returns an error after create_invoice_request is called, we
155193
/// *must* remove the payment_id from self.active_payments.

src/server.rs

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
use crate::dns_resolver::LndkDNSResolverMessageHandler;
21
use crate::lnd::{get_lnd_client, get_network, Creds, LndCfg, LndError};
32
use crate::lndkrpc::{CreateOfferRequest, CreateOfferResponse};
43
use crate::offers::get_destination;
5-
use crate::offers::handler::{CreateOfferParams, PayOfferParams};
4+
use crate::offers::handler::{CreateOfferParams, PayNameParams, PayOfferParams};
65
use crate::offers::parse::decode;
76
use crate::offers::validate_amount;
87
use crate::offers::OfferError;
@@ -18,8 +17,8 @@ use lightning::util::ser::Writeable;
1817
use lndkrpc::offers_server::Offers;
1918
use lndkrpc::{
2019
Bolt12InvoiceContents, DecodeInvoiceRequest, FeatureBit, GetInvoiceRequest, GetInvoiceResponse,
21-
PayInvoiceRequest, PayInvoiceResponse, PayOfferRequest, PayOfferResponse, PaymentHash,
22-
PaymentPaths,
20+
PayInvoiceRequest, PayInvoiceResponse, PayNameRequest, PayOfferRequest, PayOfferResponse,
21+
PaymentHash, PaymentPaths,
2322
};
2423
use std::num::NonZeroU64;
2524
use std::str::FromStr;
@@ -73,20 +72,7 @@ impl Offers for LNDKServer {
7372
let mut client = get_lnd_client(lnd_cfg)?;
7473

7574
let inner_request = request.get_ref();
76-
77-
let offer_str = if let Some(name_str) = &inner_request.name {
78-
if !name_str.is_empty() {
79-
LndkDNSResolverMessageHandler::new()
80-
.resolve_name_to_offer(name_str)
81-
.await?
82-
} else {
83-
inner_request.offer.clone()
84-
}
85-
} else {
86-
inner_request.offer.clone()
87-
};
88-
89-
let offer = decode(offer_str)?;
75+
let offer = decode(inner_request.offer.clone())?;
9076

9177
let destination = get_destination(&offer).await?;
9278
let reply_path = None;
@@ -125,6 +111,57 @@ impl Offers for LNDKServer {
125111
Ok(Response::new(reply))
126112
}
127113

114+
async fn pay_name(
115+
&self,
116+
request: Request<PayNameRequest>,
117+
) -> Result<Response<PayOfferResponse>, Status> {
118+
log::info!("Received a request: {:?}", request.get_ref());
119+
120+
let metadata = request.metadata();
121+
let macaroon = check_auth_metadata(metadata)?;
122+
let creds = Creds::String {
123+
cert: self.lnd_cert.clone(),
124+
macaroon,
125+
};
126+
let lnd_cfg = LndCfg::new(self.address.clone(), creds);
127+
let mut client = get_lnd_client(lnd_cfg)?;
128+
129+
let inner_request = request.get_ref();
130+
let reply_path = None;
131+
let info = client
132+
.lightning()
133+
.get_info(GetInfoRequest {})
134+
.await
135+
.map_err(|e| LndError::ServiceUnavailable(e.message().to_string()))?
136+
.into_inner();
137+
let network = get_network(info).await?;
138+
139+
let fee_limit = create_fee_limit(inner_request.fee_limit, inner_request.fee_limit_percent);
140+
141+
let cfg = PayNameParams {
142+
name: inner_request.name.clone(),
143+
amount: inner_request.amount,
144+
payer_note: inner_request.payer_note.clone(),
145+
network,
146+
client,
147+
reply_path,
148+
response_invoice_timeout: inner_request.response_invoice_timeout,
149+
fee_limit,
150+
};
151+
152+
let payment = self.offer_handler.pay_name(cfg).await?;
153+
log::info!(
154+
"Payment succeeded with preimage: {}",
155+
payment.payment_preimage
156+
);
157+
158+
let reply = PayOfferResponse {
159+
payment_preimage: payment.payment_preimage,
160+
};
161+
162+
Ok(Response::new(reply))
163+
}
164+
128165
async fn decode_invoice(
129166
&self,
130167
request: Request<DecodeInvoiceRequest>,

0 commit comments

Comments
 (0)