bright_lightning/
ln_address.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
use base64::prelude::*;
use lightning_invoice::Bolt11Invoice;
use reqwest::Client;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct LnAddressPaymentRequest {
    pub pr: String,
}
impl LnAddressPaymentRequest {
    pub async fn new(
        address: &LightningAddress,
        millisatoshis: u64,
        client: &Client,
    ) -> anyhow::Result<Self> {
        let confirmation = LnAddressConfirmation::new(address, client).await?;
        if millisatoshis < confirmation.min_sendable {
            return Err(anyhow::anyhow!("Amount too low"));
        }
        let pr_url = format!("{}?amount={}", confirmation.callback, millisatoshis);
        let pay_request_fetch = client.get(&pr_url).send().await?.text().await?;
        Ok(LnAddressPaymentRequest::try_from(pay_request_fetch)?)
    }
    pub fn r_hash(&self) -> anyhow::Result<String> {
        let r_hash_b = self
            .pr
            .parse::<Bolt11Invoice>()
            .map_err(|e| anyhow::anyhow!(e.to_string()))?;
        let r_hash = BASE64_STANDARD.encode(r_hash_b.payment_hash());
        Ok(r_hash)
    }
    pub fn r_hash_url_safe(&self) -> anyhow::Result<String> {
        let r_hash = self
            .pr
            .parse::<Bolt11Invoice>()
            .map_err(|e| anyhow::anyhow!(e.to_string()))?;
        let url_safe = BASE64_URL_SAFE.encode(r_hash.payment_hash());
        Ok(url_safe)
    }
}
impl ToString for LnAddressPaymentRequest {
    fn to_string(&self) -> String {
        serde_json::to_string(self).unwrap()
    }
}
impl TryFrom<String> for LnAddressPaymentRequest {
    type Error = anyhow::Error;
    fn try_from(value: String) -> Result<Self, Self::Error> {
        Ok(serde_json::from_str(&value)?)
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LnAddressConfirmation {
    pub callback: String,
    #[serde(rename = "minSendable")]
    pub min_sendable: u64,
    #[serde(rename = "maxSendable")]
    pub max_sendable: u64,
}
impl LnAddressConfirmation {
    pub async fn new(address: &LightningAddress, client: &Client) -> anyhow::Result<Self> {
        let (user, domain) = address.0.split_once('@').ok_or_else(|| anyhow::anyhow!("Invalid address"))?;
        let url = format!("https://{}/.well-known/lnurlp/{}", domain, user);
        let response = client.get(&url).send().await?.text().await?;
        LnAddressConfirmation::try_from(response)
    }
}
impl ToString for LnAddressConfirmation {
    fn to_string(&self) -> String {
        serde_json::to_string(self).unwrap()
    }
}
impl TryFrom<String> for LnAddressConfirmation {
    type Error = anyhow::Error;
    fn try_from(value: String) -> Result<Self, Self::Error> {
        Ok(serde_json::from_str(&value)?)
    }
}

pub struct LightningAddress(pub &'static str);
impl LightningAddress {
    #[cfg(not(target_arch = "wasm32"))]
    pub async fn get_invoice(
        &self,
        client: &reqwest::Client,
        millisatoshis: u64,
    ) -> anyhow::Result<LnAddressPaymentRequest> {
        LnAddressPaymentRequest::new(self, millisatoshis, client).await
    }
}

#[cfg(test)]
mod tests {

    use super::*;

    #[tokio::test]
    #[tracing_test::traced_test]
    pub async fn get_ln_url_invoice() -> Result<(), anyhow::Error> {
        let client = reqwest::Client::new();
        let address = LightningAddress("42pupusas@blink.sv");
        let invoice = address.get_invoice(&client, 1000).await?;
        tracing::info!("Invoice: {:?}", invoice);
        Ok(())
    }
}