bright_lightning/
ln_address.rs1use base64::prelude::*;
2use lightning_invoice::Bolt11Invoice;
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
6pub struct LnAddressPaymentRequest {
7 pub pr: String,
8}
9impl LnAddressPaymentRequest {
10 #[cfg(not(target_arch = "wasm32"))]
11 pub async fn new(
12 address: &LightningAddress,
13 millisatoshis: u64,
14 client: &reqwest::Client,
15 ) -> anyhow::Result<Self> {
16 let confirmation = LnAddressConfirmation::new(address, client).await?;
17 tracing::info!("Confirmation: {:?}", confirmation);
18 if millisatoshis < confirmation.min_sendable {
19 return Err(anyhow::anyhow!("Amount too low"));
20 }
21 let pr_url = format!("{}?amount={}", confirmation.callback, millisatoshis);
22 let pay_request_fetch = client.get(&pr_url).send().await?.text().await?;
23 tracing::debug!("Pay request: {}", pay_request_fetch);
24 Self::try_from(pay_request_fetch)
25 }
26 pub fn r_hash(&self) -> anyhow::Result<String> {
27 let r_hash_b = self
28 .pr
29 .parse::<Bolt11Invoice>()
30 .map_err(|e| anyhow::anyhow!(e.to_string()))?;
31 let r_hash = BASE64_STANDARD.encode(r_hash_b.payment_hash());
32 Ok(r_hash)
33 }
34 pub fn r_hash_url_safe(&self) -> anyhow::Result<String> {
35 let r_hash = self
36 .pr
37 .parse::<Bolt11Invoice>()
38 .map_err(|e| anyhow::anyhow!(e.to_string()))?;
39 let url_safe = BASE64_URL_SAFE.encode(r_hash.payment_hash());
40 Ok(url_safe)
41 }
42}
43impl std::fmt::Display for LnAddressPaymentRequest {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 write!(f, "{}", serde_json::to_string(self).unwrap_or_default())
46 }
47}
48impl TryFrom<String> for LnAddressPaymentRequest {
49 type Error = anyhow::Error;
50 fn try_from(value: String) -> Result<Self, Self::Error> {
51 Ok(serde_json::from_str(&value)?)
52 }
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct LnAddressConfirmation {
57 pub callback: String,
58 #[serde(rename = "minSendable")]
59 pub min_sendable: u64,
60 #[serde(rename = "maxSendable")]
61 pub max_sendable: u64,
62}
63impl LnAddressConfirmation {
64 #[cfg(not(target_arch = "wasm32"))]
65 pub async fn new(address: &LightningAddress, client: &reqwest::Client) -> anyhow::Result<Self> {
66 let (user, domain) = address
67 .0
68 .split_once('@')
69 .ok_or_else(|| anyhow::anyhow!("Invalid address"))?;
70 let url = format!("https://{domain}/.well-known/lnurlp/{user}");
71 let response = client.get(&url).send().await?.text().await?;
72 Self::try_from(response)
73 }
74}
75impl std::fmt::Display for LnAddressConfirmation {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 write!(f, "{}", serde_json::to_string(self).unwrap_or_default())
78 }
79}
80impl TryFrom<String> for LnAddressConfirmation {
81 type Error = anyhow::Error;
82 fn try_from(value: String) -> Result<Self, Self::Error> {
83 Ok(serde_json::from_str(&value)?)
84 }
85}
86
87pub struct LightningAddress(pub &'static str);
88impl LightningAddress {
89 #[cfg(not(target_arch = "wasm32"))]
90 pub async fn get_invoice(
91 &self,
92 client: &reqwest::Client,
93 millisatoshis: u64,
94 ) -> anyhow::Result<LnAddressPaymentRequest> {
95 LnAddressPaymentRequest::new(self, millisatoshis, client).await
96 }
97}
98
99#[cfg(test)]
100mod tests {
101
102 use super::*;
103
104 #[tokio::test]
105 #[tracing_test::traced_test]
106 pub async fn get_ln_url_invoice() -> Result<(), anyhow::Error> {
107 let client = reqwest::Client::new();
108 let address = LightningAddress("42pupusas@blink.sv");
109 let invoice = address.get_invoice(&client, 1000).await?;
110 tracing::info!("Invoice: {:?}", invoice);
111 Ok(())
112 }
113}