#![allow(clippy::result_large_err)]
pub mod api;
mod auth;
pub mod channel;
pub mod lightning_address;
pub mod lnurl;
pub mod pay;
pub mod withdraw;
#[cfg(any(feature = "async", feature = "async-https"))]
pub mod r#async;
#[cfg(feature = "blocking")]
pub mod blocking;
pub use auth::get_derivation_path;
pub use api::*;
#[cfg(feature = "blocking")]
pub use blocking::BlockingClient;
#[cfg(any(feature = "async", feature = "async-https"))]
pub use r#async::AsyncClient;
use std::{fmt, io};
#[derive(Debug, Clone, Default)]
pub struct Builder {
pub proxy: Option<String>,
pub timeout: Option<u64>,
}
impl Builder {
pub fn proxy(mut self, proxy: &str) -> Self {
self.proxy = Some(proxy.to_string());
self
}
pub fn timeout(mut self, timeout: u64) -> Self {
self.timeout = Some(timeout);
self
}
#[cfg(feature = "blocking")]
pub fn build_blocking(self) -> Result<BlockingClient, Error> {
BlockingClient::from_builder(self)
}
#[cfg(feature = "async")]
pub fn build_async(self) -> Result<AsyncClient, Error> {
AsyncClient::from_builder(self)
}
}
#[derive(Debug)]
pub enum Error {
InvalidLnUrl,
InvalidLightningAddress,
InvalidComment,
InvalidAmount,
#[cfg(feature = "blocking")]
Ureq(ureq::Error),
#[cfg(feature = "blocking")]
UreqTransport(ureq::Transport),
#[cfg(any(feature = "async", feature = "async-https"))]
Reqwest(reqwest::Error),
HttpResponse(u16),
Io(io::Error),
NoHeader,
Json(serde_json::Error),
InvalidResponse,
Parsing(std::num::ParseIntError),
BitcoinEncoding(bitcoin::consensus::encode::Error),
Hex(bitcoin::hashes::hex::Error),
Other(String),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}
macro_rules! impl_error {
( $from:ty, $to:ident ) => {
impl_error!($from, $to, Error);
};
( $from:ty, $to:ident, $impl_for:ty ) => {
impl std::convert::From<$from> for $impl_for {
fn from(err: $from) -> Self {
<$impl_for>::$to(err)
}
}
};
}
impl std::error::Error for Error {}
#[cfg(feature = "blocking")]
impl_error!(::ureq::Transport, UreqTransport, Error);
#[cfg(any(feature = "async", feature = "async-https"))]
impl_error!(::reqwest::Error, Reqwest, Error);
impl_error!(io::Error, Io, Error);
impl_error!(serde_json::Error, Json, Error);
impl_error!(std::num::ParseIntError, Parsing, Error);
impl_error!(bitcoin::hashes::hex::Error, Hex, Error);
#[cfg(all(feature = "blocking", any(feature = "async", feature = "async-https")))]
#[cfg(test)]
mod tests {
use crate::lightning_address::LightningAddress;
use crate::lnurl::LnUrl;
use crate::LnUrlResponse::{LnUrlChannelResponse, LnUrlPayResponse, LnUrlWithdrawResponse};
use crate::{AsyncClient, BlockingClient, Builder, Response};
use bitcoin::secp256k1::PublicKey;
use lightning_invoice::Bolt11Invoice;
use nostr::{EventBuilder, Keys};
use std::str::FromStr;
#[cfg(all(feature = "blocking", any(feature = "async", feature = "async-https")))]
async fn setup_clients() -> (BlockingClient, AsyncClient) {
let blocking_client = Builder::default().build_blocking().unwrap();
let async_client = Builder::default().build_async().unwrap();
(blocking_client, async_client)
}
#[cfg(all(feature = "blocking", any(feature = "async", feature = "async-https")))]
#[tokio::test]
async fn test_get_invoice() {
let url = "https://benthecarman.com/.well-known/lnurlp/ben";
let (blocking_client, async_client) = setup_clients().await;
let res = blocking_client.make_request(url).unwrap();
let res_async = async_client.make_request(url).await.unwrap();
match res_async {
LnUrlPayResponse(_) => {}
_ => panic!("Wrong response type"),
}
if let LnUrlPayResponse(pay) = res {
let msats = 1_000_000;
let invoice = blocking_client
.get_invoice(&pay, msats, None, None)
.unwrap();
let invoice_async = async_client
.get_invoice(&pay, msats, None, None)
.await
.unwrap();
let invoice = Bolt11Invoice::from_str(invoice.invoice()).unwrap();
let invoice_async = Bolt11Invoice::from_str(invoice_async.invoice()).unwrap();
assert_eq!(invoice.amount_milli_satoshis(), Some(msats));
assert_eq!(invoice_async.amount_milli_satoshis(), Some(msats));
} else {
panic!("Wrong response type");
}
}
#[cfg(all(feature = "blocking", any(feature = "async", feature = "async-https")))]
#[tokio::test]
async fn test_get_zap_invoice() {
let url = "https://benthecarman.com/.well-known/lnurlp/ben";
let (blocking_client, async_client) = setup_clients().await;
let res = blocking_client.make_request(url).unwrap();
let res_async = async_client.make_request(url).await.unwrap();
match res_async {
LnUrlPayResponse(_) => {}
_ => panic!("Wrong response type"),
}
if let LnUrlPayResponse(pay) = res {
let msats = 1_000_000;
let keys = Keys::generate();
let event = {
EventBuilder::new_zap_request::<String>(keys.public_key(), None, Some(msats), None)
.to_event(&keys)
.unwrap()
};
let invoice = blocking_client
.get_invoice(&pay, msats, Some(event.as_json()), None)
.unwrap();
let invoice_async = async_client
.get_invoice(&pay, msats, Some(event.as_json()), None)
.await
.unwrap();
let invoice = Bolt11Invoice::from_str(invoice.invoice()).unwrap();
let invoice_async = Bolt11Invoice::from_str(invoice_async.invoice()).unwrap();
assert_eq!(invoice.amount_milli_satoshis(), Some(msats));
assert_eq!(invoice_async.amount_milli_satoshis(), Some(msats));
} else {
panic!("Wrong response type");
}
}
#[cfg(all(feature = "blocking", any(feature = "async", feature = "async-https")))]
#[tokio::test]
async fn test_get_invoice_with_comment() {
let url = "https://getalby.com/.well-known/lnurlp/nvk";
let (blocking_client, async_client) = setup_clients().await;
let res = blocking_client.make_request(url).unwrap();
let res_async = async_client.make_request(url).await.unwrap();
match res_async {
LnUrlPayResponse(_) => {}
_ => panic!("Wrong response type"),
}
if let LnUrlPayResponse(pay) = res {
let msats = 1_000_000;
let comment = "test comment".to_string();
let invoice = blocking_client
.get_invoice(&pay, msats, None, Some(&comment))
.unwrap();
let invoice_async = async_client
.get_invoice(&pay, msats, None, Some(&comment))
.await
.unwrap();
let invoice = Bolt11Invoice::from_str(invoice.invoice()).unwrap();
let invoice_async = Bolt11Invoice::from_str(invoice_async.invoice()).unwrap();
assert_eq!(invoice.amount_milli_satoshis(), Some(msats));
assert_eq!(invoice_async.amount_milli_satoshis(), Some(msats));
} else {
panic!("Wrong response type");
}
}
#[cfg(all(feature = "blocking", any(feature = "async", feature = "async-https")))]
#[tokio::test]
async fn test_get_invoice_ln_addr() {
let ln_addr = LightningAddress::from_str("ben@opreturnbot.com").unwrap();
let (blocking_client, async_client) = setup_clients().await;
let res = blocking_client
.make_request(ln_addr.lnurlp_url().as_str())
.unwrap();
let res_async = async_client
.make_request(ln_addr.lnurlp_url().as_str())
.await
.unwrap();
match res_async {
LnUrlPayResponse(_) => {}
_ => panic!("Wrong response type"),
}
if let LnUrlPayResponse(pay) = res {
let msats = 1_000_000;
let invoice = blocking_client
.get_invoice(&pay, msats, None, None)
.unwrap();
let invoice_async = async_client
.get_invoice(&pay, msats, None, None)
.await
.unwrap();
let invoice = Bolt11Invoice::from_str(invoice.invoice()).unwrap();
let invoice_async = Bolt11Invoice::from_str(invoice_async.invoice()).unwrap();
assert_eq!(invoice.amount_milli_satoshis(), Some(msats));
assert_eq!(invoice_async.amount_milli_satoshis(), Some(msats));
} else {
panic!("Wrong response type");
}
}
#[cfg(all(feature = "blocking", any(feature = "async", feature = "async-https")))]
#[tokio::test]
async fn test_do_withdrawal() {
let lnurl = LnUrl::from_str("LNURL1DP68GURN8GHJ7MRWW4EXCTNXD9SHG6NPVCHXXMMD9AKXUATJDSKHW6T5DPJ8YCTH8AEK2UMND9HKU0FJVSCNZDPHVYENVDTPVYCRSVMPXVMRSCEEXGERQVPSXV6X2C3KX9JXZVMZ8PNXZDR9VY6N2DRZVG6RWEPCVYMRZDMRV9SK2D3KV43XVCF58DT").unwrap();
let url = lnurl.url.as_str();
let (blocking_client, async_client) = setup_clients().await;
let res = blocking_client.make_request(url).unwrap();
let res_async = async_client.make_request(url).await.unwrap();
match res_async {
LnUrlWithdrawResponse(_) => {}
_ => panic!("Wrong response type"),
}
if let LnUrlWithdrawResponse(w) = res {
let invoice = "lnbc1302470n1p3x3ssapp5axqf6dsusf98895vdhw97rn0szk4z6cxa5hfw3s2q5ksn3575qssdzz2pskjepqw3hjqnmsv4h9xct5wvszsnmjv3jhygzfgsazqem9dejhyctvtan82mny9ycqzpgxqzuysp5q97feeev2tnjsc0qn9kezqlgs8eekwfkxsc28uwxp9elnzkj2n0s9qyyssq02hkrz7dr0adx09t6w2tr9k8nczvq094r7qx297tsdupgeg5t3m8hvmkl7mqhtvx94he3swlg2qzhqk2j39wehcmv9awc06gex82e8qq0u0pm6";
let response = blocking_client.do_withdrawal(&w, invoice).unwrap();
let response_async = async_client.do_withdrawal(&w, invoice).await.unwrap();
assert_eq!(response, Response::Ok { event: None });
assert_eq!(response_async, Response::Ok { event: None });
} else {
panic!("Wrong response type");
}
}
#[cfg(all(feature = "blocking", any(feature = "async", feature = "async-https")))]
#[tokio::test]
async fn test_open_channel() {
let lnurl = LnUrl::from_str("LNURL1DP68GURN8GHJ7MRWW4EXCTNXD9SHG6NPVCHXXMMD9AKXUATJDSKKX6RPDEHX2MPLWDJHXUMFDAHR6ERR8YCNZEF3XYUNXENRVENXYDF3XQ6XGVEKXGMRQC3CX33N2ERXVC6KZCE38YCNQDF5VDJR2VPEVV6KVC3SV4JRYENX8YUXGEFEX4SSQ7L4MQ").unwrap();
let url = lnurl.url.as_str();
let (blocking_client, async_client) = setup_clients().await;
let res = blocking_client.make_request(url).unwrap();
let res_async = async_client.make_request(url).await.unwrap();
match res_async {
LnUrlChannelResponse(_) => {}
_ => panic!("Wrong response type"),
}
if let LnUrlChannelResponse(chan) = res {
let node_id = PublicKey::from_str(
"02f7467f4de732f3b3cffc8d5e007aecdf6e58878edb6e46a8e80164421c1b90aa",
)
.unwrap();
let response = blocking_client.open_channel(&chan, node_id, true).unwrap();
let response_async = async_client
.open_channel(&chan, node_id, true)
.await
.unwrap();
assert_eq!(response, Response::Ok { event: None });
assert_eq!(response_async, Response::Ok { event: None });
} else {
panic!("Wrong response type");
}
}
}