Skip to content

Commit 3acd077

Browse files
committed
Implement parsing & resolving lnurls
1 parent dfa2b00 commit 3acd077

File tree

4 files changed

+112
-13
lines changed

4 files changed

+112
-13
lines changed

src/dns_resolver.rs

+10-2
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,16 @@ impl HrnResolver for DNSHrnResolver {
3939
Box::pin(async move { self.resolve_dns(hrn).await })
4040
}
4141

42-
fn resolve_lnurl<'a>(&'a self, _: String, _: Amount, _: [u8; 32]) -> LNURLResolutionFuture<'a> {
43-
let err = "resolve_lnurl shouldn't be called when we don't reoslve LNURL";
42+
fn resolve_lnurl<'a>(&'a self, _url: &'a str) -> HrnResolutionFuture<'a> {
43+
let err = "resolve_lnurl shouldn't be called when we don't resolve LNURL";
44+
debug_assert!(false, "{}", err);
45+
Box::pin(async move { Err(err) })
46+
}
47+
48+
fn resolve_lnurl_to_invoice<'a>(
49+
&'a self, _: String, _: Amount, _: [u8; 32],
50+
) -> LNURLResolutionFuture<'a> {
51+
let err = "resolve_lnurl shouldn't be called when we don't resolve LNURL";
4452
debug_assert!(false, "{}", err);
4553
Box::pin(async move { Err(err) })
4654
}

src/hrn_resolution.rs

+12-2
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,14 @@ pub trait HrnResolver {
111111
/// can be further parsed as payment instructions.
112112
fn resolve_hrn<'a>(&'a self, hrn: &'a HumanReadableName) -> HrnResolutionFuture<'a>;
113113

114+
/// Resolves the given Lnurl into a [`HrnResolution`] containing a result which
115+
/// can be further parsed as payment instructions.
116+
fn resolve_lnurl<'a>(&'a self, url: &'a str) -> HrnResolutionFuture<'a>;
117+
114118
/// Resolves the LNURL callback (from a [`HrnResolution::LNURLPay`]) into a [`Bolt11Invoice`].
115119
///
116120
/// This shall only be called if [`Self::resolve_hrn`] returns an [`HrnResolution::LNURLPay`].
117-
fn resolve_lnurl<'a>(
121+
fn resolve_lnurl_to_invoice<'a>(
118122
&'a self, callback_url: String, amount: Amount, expected_description_hash: [u8; 32],
119123
) -> LNURLResolutionFuture<'a>;
120124
}
@@ -128,7 +132,13 @@ impl HrnResolver for DummyHrnResolver {
128132
Box::pin(async { Err("Human Readable Name resolution not supported") })
129133
}
130134

131-
fn resolve_lnurl<'a>(&'a self, _: String, _: Amount, _: [u8; 32]) -> LNURLResolutionFuture<'a> {
135+
fn resolve_lnurl<'a>(&'a self, _lnurl: &'a str) -> HrnResolutionFuture<'a> {
136+
Box::pin(async { Err("LNURL resolution not supported") })
137+
}
138+
139+
fn resolve_lnurl_to_invoice<'a>(
140+
&'a self, _: String, _: Amount, _: [u8; 32],
141+
) -> LNURLResolutionFuture<'a> {
132142
Box::pin(async { Err("LNURL resolution not supported") })
133143
}
134144
}

src/http_resolver.rs

+52-8
Original file line numberDiff line numberDiff line change
@@ -134,17 +134,16 @@ impl HTTPHrnResolver {
134134
resolve_proof(&dns_name, proof)
135135
}
136136

137-
async fn resolve_lnurl(&self, hrn: &HumanReadableName) -> Result<HrnResolution, &'static str> {
138-
let init_url = format!("https://{}/.well-known/lnurlp/{}", hrn.domain(), hrn.user());
137+
async fn resolve_lnurl_impl(&self, lnurl_url: &str) -> Result<HrnResolution, &'static str> {
139138
let err = "Failed to fetch LN-Address initial well-known endpoint";
140139
let init: LNURLInitResponse =
141-
reqwest::get(init_url).await.map_err(|_| err)?.json().await.map_err(|_| err)?;
140+
reqwest::get(lnurl_url).await.map_err(|_| err)?.json().await.map_err(|_| err)?;
142141

143142
if init.tag != "payRequest" {
144-
return Err("LNURL initial init_responseponse had an incorrect tag value");
143+
return Err("LNURL initial init_response had an incorrect tag value");
145144
}
146145
if init.min_sendable > init.max_sendable {
147-
return Err("LNURL initial init_responseponse had no sendable amounts");
146+
return Err("LNURL initial init_response had no sendable amounts");
148147
}
149148

150149
let err = "LNURL metadata was not in the correct format";
@@ -176,14 +175,20 @@ impl HrnResolver for HTTPHrnResolver {
176175
Err(e) if e == DNS_ERR => {
177176
// If we got an error that might indicate the recipient doesn't support BIP
178177
// 353, try LN-Address via LNURL
179-
self.resolve_lnurl(hrn).await
178+
let init_url =
179+
format!("https://{}/.well-known/lnurlp/{}", hrn.domain(), hrn.user());
180+
self.resolve_lnurl(&init_url).await
180181
},
181182
Err(e) => Err(e),
182183
}
183184
})
184185
}
185186

186-
fn resolve_lnurl<'a>(
187+
fn resolve_lnurl<'a>(&'a self, url: &'a str) -> HrnResolutionFuture<'a> {
188+
Box::pin(async move { self.resolve_lnurl_impl(url).await })
189+
}
190+
191+
fn resolve_lnurl_to_invoice<'a>(
187192
&'a self, mut callback: String, amt: Amount, expected_description_hash: [u8; 32],
188193
) -> LNURLResolutionFuture<'a> {
189194
Box::pin(async move {
@@ -308,7 +313,8 @@ mod tests {
308313
.unwrap();
309314

310315
let resolved = if let PaymentInstructions::ConfigurableAmount(instr) = instructions {
311-
// min_amt and max_amt may or may not be set by the LNURL server
316+
assert!(instr.min_amt().is_some());
317+
assert!(instr.max_amt().is_some());
312318

313319
assert_eq!(instr.pop_callback(), None);
314320
assert!(instr.bip_353_dnssec_proof().is_none());
@@ -339,4 +345,42 @@ mod tests {
339345
}
340346
}
341347
}
348+
349+
#[tokio::test]
350+
async fn test_http_lnurl_resolver() {
351+
let instructions = PaymentInstructions::parse(
352+
// lnurl encoding for [email protected]
353+
"lnurl1dp68gurn8ghj7cnfw33k76tw9ehxjmn2vyhjuam9d3kz66mwdamkutmvde6hymrs9akxuatjd36x2um5ahcq39",
354+
Network::Bitcoin,
355+
&HTTPHrnResolver,
356+
true,
357+
)
358+
.await
359+
.unwrap();
360+
361+
let resolved = if let PaymentInstructions::ConfigurableAmount(instr) = instructions {
362+
assert!(instr.min_amt().is_some());
363+
assert!(instr.max_amt().is_some());
364+
365+
assert_eq!(instr.pop_callback(), None);
366+
assert!(instr.bip_353_dnssec_proof().is_none());
367+
368+
instr.set_amount(Amount::from_sats(100_000).unwrap(), &HTTPHrnResolver).await.unwrap()
369+
} else {
370+
panic!();
371+
};
372+
373+
assert_eq!(resolved.pop_callback(), None);
374+
assert!(resolved.bip_353_dnssec_proof().is_none());
375+
376+
for method in resolved.methods() {
377+
match method {
378+
PaymentMethod::LightningBolt11(invoice) => {
379+
assert_eq!(invoice.amount_milli_satoshis(), Some(100_000_000));
380+
},
381+
PaymentMethod::LightningBolt12(_) => panic!("Should only resolve to BOLT 11"),
382+
PaymentMethod::OnChain(_) => panic!("Should only resolve to BOLT 11"),
383+
}
384+
}
385+
}
342386
}

src/lib.rs

+38-1
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,8 @@ impl ConfigurableAmountPaymentInstructions {
347347
debug_assert!(inner.onchain_amt.is_none());
348348
debug_assert!(inner.pop_callback.is_none());
349349
debug_assert!(inner.hrn_proof.is_none());
350-
let bolt11 = resolver.resolve_lnurl(callback, amount, expected_desc_hash).await?;
350+
let bolt11 =
351+
resolver.resolve_lnurl_to_invoice(callback, amount, expected_desc_hash).await?;
351352
if bolt11.amount_milli_satoshis() != Some(amount.milli_sats()) {
352353
return Err("LNURL resolution resulted in a BOLT 11 invoice with the wrong amount");
353354
}
@@ -428,6 +429,8 @@ pub enum ParseError {
428429
InvalidBolt12(Bolt12ParseError),
429430
/// An invalid on-chain address was encountered
430431
InvalidOnChain(address::ParseError),
432+
/// An invalid lnurl was encountered
433+
InvalidLnurl(&'static str),
431434
/// The payment instructions encoded instructions for a network other than the one specified.
432435
WrongNetwork,
433436
/// Different parts of the payment instructions were inconsistent.
@@ -944,6 +947,40 @@ impl PaymentInstructions {
944947
))
945948
},
946949
}
950+
} else if let Some((_, data)) =
951+
bitcoin::bech32::decode(instructions).ok().filter(|(hrp, _)| hrp.as_str() == "lnurl")
952+
{
953+
let url = String::from_utf8(data).map_err(|_| ParseError::InvalidLnurl(""))?;
954+
let resolution = hrn_resolver.resolve_lnurl(&url).await;
955+
let resolution = resolution.map_err(ParseError::HrnResolutionError)?;
956+
match resolution {
957+
HrnResolution::DNSSEC { .. } => {
958+
return Err(ParseError::HrnResolutionError(
959+
"Unexpected return when resolving lnurl",
960+
));
961+
},
962+
HrnResolution::LNURLPay {
963+
min_value,
964+
max_value,
965+
expected_description_hash,
966+
recipient_description,
967+
callback,
968+
} => {
969+
let inner = PaymentInstructionsImpl {
970+
description: recipient_description,
971+
methods: Vec::new(),
972+
lnurl: Some((callback, expected_description_hash, min_value, max_value)),
973+
onchain_amt: None,
974+
ln_amt: None,
975+
pop_callback: None,
976+
hrn: None,
977+
hrn_proof: None,
978+
};
979+
Ok(PaymentInstructions::ConfigurableAmount(
980+
ConfigurableAmountPaymentInstructions { inner },
981+
))
982+
},
983+
}
947984
} else {
948985
parse_resolved_instructions(instructions, network, supports_pops, None, None)
949986
}

0 commit comments

Comments
 (0)