1pub mod bip353;
7pub mod lnurl;
9
10use anyhow::{Context, anyhow, ensure};
11use lexe_common::ln::network::Network;
12pub use lexe_payment_uri_core::*;
13
14pub async fn resolve_best(
23 bip353_client: &bip353::Bip353Client,
24 lnurl_client: &lnurl::LnurlClient,
25 network: Network,
26 payment_uri: PaymentUri,
27) -> anyhow::Result<PaymentMethod> {
28 let mut payment_methods =
31 resolve_payment_methods(bip353_client, lnurl_client, payment_uri)
32 .await
33 .context("Failed to resolve payment URI into payment methods")?;
34
35 payment_methods.retain(|method| method.supports_network(network));
38 ensure!(
39 !payment_methods.is_empty(),
40 "Payment code is not valid for {network}"
41 );
42
43 let best = payment_methods
45 .into_iter()
46 .max_by_key(|x| match x {
47 PaymentMethod::Invoice(_) => 40,
48 PaymentMethod::Offer(_) => 30,
49 PaymentMethod::LnurlPayRequest(_) => 20,
50 PaymentMethod::Onchain(o) => 10 + o.relative_priority(),
51 })
52 .expect("We just checked there's at least one method");
53
54 Ok(best)
55}
56
57async fn resolve_payment_methods(
59 bip353_client: &bip353::Bip353Client,
60 lnurl_client: &lnurl::LnurlClient,
61 payment_uri: PaymentUri,
62) -> anyhow::Result<Vec<PaymentMethod>> {
63 let payment_methods = match payment_uri {
64 PaymentUri::Bip321Uri(bip321) => bip321.flatten(),
65
66 PaymentUri::LightningUri(lnuri) => lnuri.flatten(),
67
68 PaymentUri::Invoice(invoice) =>
69 lexe_payment_uri_core::helpers::flatten_invoice(invoice),
70
71 PaymentUri::Offer(offer) => vec![PaymentMethod::Offer(
72 OfferWithAmount::no_bip321_amount(offer),
73 )],
74
75 PaymentUri::Address(address) =>
76 vec![PaymentMethod::Onchain(Onchain::from(address))],
77
78 PaymentUri::EmailLikeAddress(email_like) => {
79 let mut methods = Vec::with_capacity(3);
80 let mut errors = Vec::with_capacity(2);
81
82 if let Some(bip353_fqdn) = email_like.bip353_fqdn {
84 let bip353_result = bip353_client
85 .resolve_bip353_fqdn(bip353_fqdn)
86 .await
87 .context("Failed to resolve BIP353 address");
88 match bip353_result {
89 Ok(bip353_methods) => {
90 if bip353_methods.iter().any(|m| !m.is_onchain()) {
94 return Ok(bip353_methods);
95 } else {
96 methods.extend(bip353_methods);
97 }
98 }
99 Err(e) => errors.push(format!("{e:#}")),
100 }
101 }
102
103 let ln_address_result = lnurl_client
105 .get_pay_request(&email_like.lightning_address_url)
106 .await
107 .context("Failed to resolve Lightning Address url");
108 match ln_address_result {
109 Ok(pay_request) =>
110 methods.push(PaymentMethod::LnurlPayRequest(pay_request)),
111 Err(e) => errors.push(format!("{e:#}")),
112 }
113
114 if !methods.is_empty() {
118 methods
119 } else {
120 debug_assert!(!errors.is_empty());
121 let joined_errs = errors.join("; ");
122 return Err(anyhow!("{joined_errs}"));
123 }
124 }
125
126 PaymentUri::Lnurl(lnurl) => {
127 let pay_request = lnurl_client
128 .get_pay_request(&lnurl.http_url)
129 .await
130 .context("Failed to resolve LNURL-pay url")?;
131
132 vec![PaymentMethod::LnurlPayRequest(pay_request)]
133 }
134 };
135
136 Ok(payment_methods)
137}
138
139#[cfg(test)]
140mod test {
141 use std::time::Duration;
142
143 use lexe_common::{env::DeployEnv, ln::network::Network};
144 use lexe_std::Apply;
145 use tracing::info;
146
147 use super::*;
148
149 #[tokio::test]
162 #[ignore]
163 async fn test_resolve_best_bluematt() {
164 const RESOLVE_BEST_TIMEOUT: Duration = Duration::from_secs(5);
166 lexe_std::const_assert!(
167 lnurl::LNURL_HTTP_TIMEOUT.as_secs()
168 > RESOLVE_BEST_TIMEOUT.as_secs()
169 );
170
171 lexe_logger::init_for_testing();
172
173 let payment_uri = PaymentUri::parse("matt@mattcorallo.com").unwrap();
174 info!("Resolving best payment method for matt@mattcorallo.com");
175
176 let bip353_client =
177 bip353::Bip353Client::new(bip353::GOOGLE_DOH_ENDPOINT).unwrap();
178 let lnurl_client = lnurl::LnurlClient::new(DeployEnv::Prod).unwrap();
179
180 let payment_method = resolve_best(
181 &bip353_client,
182 &lnurl_client,
183 Network::Mainnet,
184 payment_uri,
185 )
186 .apply(|fut| tokio::time::timeout(RESOLVE_BEST_TIMEOUT, fut))
187 .await
188 .expect("Timed out")
189 .unwrap();
190
191 assert!(matches!(payment_method, PaymentMethod::Offer(_)));
193 assert!(payment_method.supports_network(Network::Mainnet));
194
195 info!("Successfully resolved BlueMatt's payment methods");
196 }
197}