Skip to content

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

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
11 changes: 10 additions & 1 deletion fuzz/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,16 @@ impl HrnResolver for Resolver<'_> {
})
}

fn resolve_lnurl<'a>(&'a self, _: String, _: Amount, _: [u8; 32]) -> LNURLResolutionFuture<'a> {
fn resolve_lnurl<'a>(&'a self, _: &'a str) -> HrnResolutionFuture<'a> {
Box::pin(async {
let mut us = self.0.lock().unwrap();
us.0.take().unwrap()
})
}

fn resolve_lnurl_to_invoice<'a>(
&'a self, _: String, _: Amount, _: [u8; 32],
) -> LNURLResolutionFuture<'a> {
Box::pin(async {
let mut us = self.0.lock().unwrap();
if let Ok(s) = std::str::from_utf8(us.1.take().unwrap()) {
Expand Down
12 changes: 10 additions & 2 deletions src/dns_resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,16 @@ impl HrnResolver for DNSHrnResolver {
Box::pin(async move { self.resolve_dns(hrn).await })
}

fn resolve_lnurl<'a>(&'a self, _: String, _: Amount, _: [u8; 32]) -> LNURLResolutionFuture<'a> {
let err = "resolve_lnurl shouldn't be called when we don't reoslve LNURL";
fn resolve_lnurl<'a>(&'a self, _url: &'a str) -> HrnResolutionFuture<'a> {
let err = "resolve_lnurl shouldn't be called when we don't resolve LNURL";
debug_assert!(false, "{}", err);
Box::pin(async move { Err(err) })
}

fn resolve_lnurl_to_invoice<'a>(
&'a self, _: String, _: Amount, _: [u8; 32],
) -> LNURLResolutionFuture<'a> {
let err = "resolve_lnurl shouldn't be called when we don't resolve LNURL";
debug_assert!(false, "{}", err);
Box::pin(async move { Err(err) })
}
Expand Down
14 changes: 12 additions & 2 deletions src/hrn_resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>;

/// 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>;
}
Expand All @@ -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") })
}
}
60 changes: 52 additions & 8 deletions src/http_resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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"),
}
}
}
}
37 changes: 36 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -944,6 +947,38 @@ impl PaymentInstructions {
))
},
}
} else if let Some((_, data)) =
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)
}
Expand Down