-
Notifications
You must be signed in to change notification settings - Fork 3
Implement parsing & resolving lnurls #2
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 |
---|---|---|
|
@@ -111,10 +111,14 @@ pub trait HrnResolver { | |
/// can be further parsed as payment instructions. | ||
fn resolve_hrn<'a>(&'a self, hrn: &'a HumanReadableName) -> HrnResolutionFuture<'a>; | ||
|
||
/// Resolves the given Lnurl into a [`HrnResolution`] containing a result which | ||
/// can be further parsed as payment instructions. | ||
fn resolve_lnurl<'a>(&'a self, url: &'a str) -> HrnResolutionFuture<'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. Should we rename the trait now that its resolving more than HRNs? I think I'm fine with no, because we really want this to be about HRNs, we just happen to support LNURL too, but still. |
||
|
||
/// Resolves the LNURL callback (from a [`HrnResolution::LNURLPay`]) into a [`Bolt11Invoice`]. | ||
/// | ||
/// This shall only be called if [`Self::resolve_hrn`] returns an [`HrnResolution::LNURLPay`]. | ||
fn resolve_lnurl<'a>( | ||
fn resolve_lnurl_to_invoice<'a>( | ||
&'a self, callback_url: String, amount: Amount, expected_description_hash: [u8; 32], | ||
) -> LNURLResolutionFuture<'a>; | ||
} | ||
|
@@ -128,7 +132,13 @@ impl HrnResolver for DummyHrnResolver { | |
Box::pin(async { Err("Human Readable Name resolution not supported") }) | ||
} | ||
|
||
fn resolve_lnurl<'a>(&'a self, _: String, _: Amount, _: [u8; 32]) -> LNURLResolutionFuture<'a> { | ||
fn resolve_lnurl<'a>(&'a self, _lnurl: &'a str) -> HrnResolutionFuture<'a> { | ||
Box::pin(async { Err("LNURL resolution not supported") }) | ||
} | ||
|
||
fn resolve_lnurl_to_invoice<'a>( | ||
&'a self, _: String, _: Amount, _: [u8; 32], | ||
) -> LNURLResolutionFuture<'a> { | ||
Box::pin(async { Err("LNURL resolution not supported") }) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -134,17 +134,16 @@ impl HTTPHrnResolver { | |
resolve_proof(&dns_name, proof) | ||
} | ||
|
||
async fn resolve_lnurl(&self, hrn: &HumanReadableName) -> Result<HrnResolution, &'static str> { | ||
let init_url = format!("https://{}/.well-known/lnurlp/{}", hrn.domain(), hrn.user()); | ||
async fn resolve_lnurl_impl(&self, lnurl_url: &str) -> Result<HrnResolution, &'static str> { | ||
let err = "Failed to fetch LN-Address initial well-known endpoint"; | ||
let init: LNURLInitResponse = | ||
reqwest::get(init_url).await.map_err(|_| err)?.json().await.map_err(|_| err)?; | ||
reqwest::get(lnurl_url).await.map_err(|_| err)?.json().await.map_err(|_| err)?; | ||
|
||
if init.tag != "payRequest" { | ||
return Err("LNURL initial init_responseponse had an incorrect tag value"); | ||
return Err("LNURL initial init_response had an incorrect tag value"); | ||
} | ||
if init.min_sendable > init.max_sendable { | ||
return Err("LNURL initial init_responseponse had no sendable amounts"); | ||
return Err("LNURL initial init_response had no sendable amounts"); | ||
} | ||
|
||
let err = "LNURL metadata was not in the correct format"; | ||
|
@@ -176,14 +175,20 @@ impl HrnResolver for HTTPHrnResolver { | |
Err(e) if e == DNS_ERR => { | ||
// If we got an error that might indicate the recipient doesn't support BIP | ||
// 353, try LN-Address via LNURL | ||
self.resolve_lnurl(hrn).await | ||
let init_url = | ||
format!("https://{}/.well-known/lnurlp/{}", hrn.domain(), hrn.user()); | ||
self.resolve_lnurl(&init_url).await | ||
}, | ||
Err(e) => Err(e), | ||
} | ||
}) | ||
} | ||
|
||
fn resolve_lnurl<'a>( | ||
fn resolve_lnurl<'a>(&'a self, url: &'a str) -> HrnResolutionFuture<'a> { | ||
Box::pin(async move { self.resolve_lnurl_impl(url).await }) | ||
} | ||
|
||
fn resolve_lnurl_to_invoice<'a>( | ||
&'a self, mut callback: String, amt: Amount, expected_description_hash: [u8; 32], | ||
) -> LNURLResolutionFuture<'a> { | ||
Box::pin(async move { | ||
|
@@ -308,7 +313,8 @@ mod tests { | |
.unwrap(); | ||
|
||
let resolved = if let PaymentInstructions::ConfigurableAmount(instr) = instructions { | ||
// min_amt and max_amt may or may not be set by the LNURL server | ||
assert!(instr.min_amt().is_some()); | ||
assert!(instr.max_amt().is_some()); | ||
|
||
assert_eq!(instr.pop_callback(), None); | ||
assert!(instr.bip_353_dnssec_proof().is_none()); | ||
|
@@ -339,4 +345,42 @@ mod tests { | |
} | ||
} | ||
} | ||
|
||
#[tokio::test] | ||
async fn test_http_lnurl_resolver() { | ||
let instructions = PaymentInstructions::parse( | ||
// lnurl encoding for [email protected] | ||
"lnurl1dp68gurn8ghj7cnfw33k76tw9ehxjmn2vyhjuam9d3kz66mwdamkutmvde6hymrs9akxuatjd36x2um5ahcq39", | ||
Network::Bitcoin, | ||
&HTTPHrnResolver, | ||
true, | ||
) | ||
.await | ||
.unwrap(); | ||
|
||
let resolved = if let PaymentInstructions::ConfigurableAmount(instr) = instructions { | ||
assert!(instr.min_amt().is_some()); | ||
assert!(instr.max_amt().is_some()); | ||
|
||
assert_eq!(instr.pop_callback(), None); | ||
assert!(instr.bip_353_dnssec_proof().is_none()); | ||
|
||
instr.set_amount(Amount::from_sats(100_000).unwrap(), &HTTPHrnResolver).await.unwrap() | ||
} else { | ||
panic!(); | ||
}; | ||
|
||
assert_eq!(resolved.pop_callback(), None); | ||
assert!(resolved.bip_353_dnssec_proof().is_none()); | ||
|
||
for method in resolved.methods() { | ||
match method { | ||
PaymentMethod::LightningBolt11(invoice) => { | ||
assert_eq!(invoice.amount_milli_satoshis(), Some(100_000_000)); | ||
}, | ||
PaymentMethod::LightningBolt12(_) => panic!("Should only resolve to BOLT 11"), | ||
PaymentMethod::OnChain(_) => panic!("Should only resolve to BOLT 11"), | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -347,7 +347,8 @@ impl ConfigurableAmountPaymentInstructions { | |
debug_assert!(inner.onchain_amt.is_none()); | ||
debug_assert!(inner.pop_callback.is_none()); | ||
debug_assert!(inner.hrn_proof.is_none()); | ||
let bolt11 = resolver.resolve_lnurl(callback, amount, expected_desc_hash).await?; | ||
let bolt11 = | ||
resolver.resolve_lnurl_to_invoice(callback, amount, expected_desc_hash).await?; | ||
if bolt11.amount_milli_satoshis() != Some(amount.milli_sats()) { | ||
return Err("LNURL resolution resulted in a BOLT 11 invoice with the wrong amount"); | ||
} | ||
|
@@ -428,6 +429,8 @@ pub enum ParseError { | |
InvalidBolt12(Bolt12ParseError), | ||
/// An invalid on-chain address was encountered | ||
InvalidOnChain(address::ParseError), | ||
/// An invalid lnurl was encountered | ||
InvalidLnurl(&'static str), | ||
/// The payment instructions encoded instructions for a network other than the one specified. | ||
WrongNetwork, | ||
/// Different parts of the payment instructions were inconsistent. | ||
|
@@ -944,6 +947,38 @@ impl PaymentInstructions { | |
)) | ||
}, | ||
} | ||
} else if let Some((_, data)) = | ||
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. Isn't this supposed to support decoding any part of the string that contains |
||
bitcoin::bech32::decode(instructions).ok().filter(|(hrp, _)| hrp.as_str() == "lnurl") | ||
{ | ||
let url = String::from_utf8(data).map_err(|_| ParseError::InvalidLnurl(""))?; | ||
let resolution = hrn_resolver.resolve_lnurl(&url).await; | ||
let resolution = resolution.map_err(ParseError::HrnResolutionError)?; | ||
match resolution { | ||
HrnResolution::DNSSEC { .. } => { | ||
Err(ParseError::HrnResolutionError("Unexpected return when resolving lnurl")) | ||
}, | ||
HrnResolution::LNURLPay { | ||
min_value, | ||
max_value, | ||
expected_description_hash, | ||
recipient_description, | ||
callback, | ||
} => { | ||
let inner = PaymentInstructionsImpl { | ||
description: recipient_description, | ||
methods: Vec::new(), | ||
lnurl: Some((callback, expected_description_hash, min_value, max_value)), | ||
onchain_amt: None, | ||
ln_amt: None, | ||
pop_callback: None, | ||
hrn: None, | ||
hrn_proof: None, | ||
}; | ||
Ok(PaymentInstructions::ConfigurableAmount( | ||
ConfigurableAmountPaymentInstructions { inner }, | ||
)) | ||
}, | ||
} | ||
} else { | ||
parse_resolved_instructions(instructions, network, supports_pops, None, None) | ||
} | ||
|
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.
An
Lnurl
isn't a well-defined thing, I believe? This needs to specify what, exactly, theurl
parameter is (a URL for an LNURL endpoint, and not the encoded LNURL...... strings).