lib.rs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. //! CDK lightning backend for lnbits
  2. #![doc = include_str!("../README.md")]
  3. #![warn(missing_docs)]
  4. #![warn(rustdoc::bare_urls)]
  5. use std::cmp::max;
  6. use std::pin::Pin;
  7. use std::sync::atomic::{AtomicBool, Ordering};
  8. use std::sync::Arc;
  9. use anyhow::anyhow;
  10. use async_trait::async_trait;
  11. use axum::Router;
  12. use cdk_common::amount::{to_unit, Amount, MSAT_IN_SAT};
  13. use cdk_common::common::FeeReserve;
  14. use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
  15. use cdk_common::payment::{
  16. self, Bolt11Settings, CreateIncomingPaymentResponse, IncomingPaymentOptions,
  17. MakePaymentResponse, MintPayment, OutgoingPaymentOptions, PaymentIdentifier,
  18. PaymentQuoteResponse, WaitPaymentResponse,
  19. };
  20. use cdk_common::util::{hex, unix_time};
  21. use cdk_common::Bolt11Invoice;
  22. use error::Error;
  23. use futures::Stream;
  24. use lnbits_rs::api::invoice::CreateInvoiceRequest;
  25. use lnbits_rs::LNBitsClient;
  26. use serde_json::Value;
  27. use tokio_util::sync::CancellationToken;
  28. pub mod error;
  29. /// LNbits
  30. #[derive(Clone)]
  31. pub struct LNbits {
  32. lnbits_api: LNBitsClient,
  33. fee_reserve: FeeReserve,
  34. webhook_url: Option<String>,
  35. wait_invoice_cancel_token: CancellationToken,
  36. wait_invoice_is_active: Arc<AtomicBool>,
  37. settings: Bolt11Settings,
  38. }
  39. impl LNbits {
  40. /// Create new [`LNbits`] wallet
  41. #[allow(clippy::too_many_arguments)]
  42. pub async fn new(
  43. admin_api_key: String,
  44. invoice_api_key: String,
  45. api_url: String,
  46. fee_reserve: FeeReserve,
  47. webhook_url: Option<String>,
  48. ) -> Result<Self, Error> {
  49. let lnbits_api = LNBitsClient::new("", &admin_api_key, &invoice_api_key, &api_url, None)?;
  50. Ok(Self {
  51. lnbits_api,
  52. fee_reserve,
  53. webhook_url,
  54. wait_invoice_cancel_token: CancellationToken::new(),
  55. wait_invoice_is_active: Arc::new(AtomicBool::new(false)),
  56. settings: Bolt11Settings {
  57. mpp: false,
  58. unit: CurrencyUnit::Sat,
  59. invoice_description: true,
  60. amountless: false,
  61. bolt12: false,
  62. },
  63. })
  64. }
  65. /// Subscribe to lnbits ws
  66. pub async fn subscribe_ws(&self) -> Result<(), Error> {
  67. if rustls::crypto::CryptoProvider::get_default().is_none() {
  68. let _ = rustls::crypto::ring::default_provider().install_default();
  69. }
  70. self.lnbits_api
  71. .subscribe_to_websocket()
  72. .await
  73. .map_err(|err| {
  74. tracing::error!("Could not subscribe to lnbits ws");
  75. Error::Anyhow(err)
  76. })
  77. }
  78. }
  79. #[async_trait]
  80. impl MintPayment for LNbits {
  81. type Err = payment::Error;
  82. async fn get_settings(&self) -> Result<Value, Self::Err> {
  83. Ok(serde_json::to_value(&self.settings)?)
  84. }
  85. fn is_wait_invoice_active(&self) -> bool {
  86. self.wait_invoice_is_active.load(Ordering::SeqCst)
  87. }
  88. fn cancel_wait_invoice(&self) {
  89. self.wait_invoice_cancel_token.cancel()
  90. }
  91. async fn wait_any_incoming_payment(
  92. &self,
  93. ) -> Result<Pin<Box<dyn Stream<Item = WaitPaymentResponse> + Send>>, Self::Err> {
  94. let api = self.lnbits_api.clone();
  95. let cancel_token = self.wait_invoice_cancel_token.clone();
  96. let is_active = Arc::clone(&self.wait_invoice_is_active);
  97. Ok(Box::pin(futures::stream::unfold(
  98. (api, cancel_token, is_active),
  99. |(api, cancel_token, is_active)| async move {
  100. is_active.store(true, Ordering::SeqCst);
  101. let receiver = api.receiver();
  102. let mut receiver = receiver.lock().await;
  103. tokio::select! {
  104. _ = cancel_token.cancelled() => {
  105. // Stream is cancelled
  106. is_active.store(false, Ordering::SeqCst);
  107. tracing::info!("Waiting for lnbits invoice ending");
  108. None
  109. }
  110. msg_option = receiver.recv() => {
  111. match msg_option {
  112. Some(msg) => {
  113. let check = api.get_payment_info(&msg).await;
  114. match check {
  115. Ok(payment) => {
  116. if payment.paid {
  117. match hex::decode(msg.clone()) {
  118. Ok(decoded) => {
  119. match decoded.try_into() {
  120. Ok(hash) => {
  121. let response = WaitPaymentResponse {
  122. payment_identifier: PaymentIdentifier::PaymentHash(hash),
  123. payment_amount: Amount::from(payment.details.amount as u64),
  124. unit: CurrencyUnit::Msat,
  125. payment_id: msg.clone()
  126. };
  127. Some((response, (api, cancel_token, is_active)))
  128. },
  129. Err(e) => {
  130. tracing::error!("Failed to convert payment hash bytes to array: {:?}", e);
  131. None
  132. }
  133. }
  134. },
  135. Err(e) => {
  136. tracing::error!("Failed to decode payment hash hex string: {}", e);
  137. None
  138. }
  139. }
  140. } else {
  141. tracing::warn!("Received payment notification but could not check payment for {}", msg);
  142. None
  143. }
  144. },
  145. Err(_) => None
  146. }
  147. },
  148. None => {
  149. is_active.store(false, Ordering::SeqCst);
  150. None
  151. }
  152. }
  153. }
  154. }
  155. },
  156. )))
  157. }
  158. async fn get_payment_quote(
  159. &self,
  160. unit: &CurrencyUnit,
  161. options: OutgoingPaymentOptions,
  162. ) -> Result<PaymentQuoteResponse, Self::Err> {
  163. if unit != &CurrencyUnit::Sat {
  164. return Err(Self::Err::Anyhow(anyhow!("Unsupported unit")));
  165. }
  166. match options {
  167. OutgoingPaymentOptions::Bolt11(bolt11_options) => {
  168. let amount_msat = match bolt11_options.melt_options {
  169. Some(amount) => {
  170. if matches!(amount, MeltOptions::Mpp { mpp: _ }) {
  171. return Err(payment::Error::UnsupportedPaymentOption);
  172. }
  173. amount.amount_msat()
  174. }
  175. None => bolt11_options
  176. .bolt11
  177. .amount_milli_satoshis()
  178. .ok_or(Error::UnknownInvoiceAmount)?
  179. .into(),
  180. };
  181. let amount = amount_msat / MSAT_IN_SAT.into();
  182. let relative_fee_reserve =
  183. (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
  184. let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
  185. let fee = max(relative_fee_reserve, absolute_fee_reserve);
  186. Ok(PaymentQuoteResponse {
  187. request_lookup_id: Some(PaymentIdentifier::PaymentHash(
  188. *bolt11_options.bolt11.payment_hash().as_ref(),
  189. )),
  190. amount,
  191. fee: fee.into(),
  192. state: MeltQuoteState::Unpaid,
  193. unit: unit.clone(),
  194. })
  195. }
  196. OutgoingPaymentOptions::Bolt12(_bolt12_options) => {
  197. Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LNbits")))
  198. }
  199. }
  200. }
  201. async fn make_payment(
  202. &self,
  203. _unit: &CurrencyUnit,
  204. options: OutgoingPaymentOptions,
  205. ) -> Result<MakePaymentResponse, Self::Err> {
  206. match options {
  207. OutgoingPaymentOptions::Bolt11(bolt11_options) => {
  208. let pay_response = self
  209. .lnbits_api
  210. .pay_invoice(&bolt11_options.bolt11.to_string(), None)
  211. .await
  212. .map_err(|err| {
  213. tracing::error!("Could not pay invoice");
  214. tracing::error!("{}", err.to_string());
  215. Self::Err::Anyhow(anyhow!("Could not pay invoice"))
  216. })?;
  217. let invoice_info = self
  218. .lnbits_api
  219. .get_payment_info(&pay_response.payment_hash)
  220. .await
  221. .map_err(|err| {
  222. tracing::error!("Could not find invoice");
  223. tracing::error!("{}", err.to_string());
  224. Self::Err::Anyhow(anyhow!("Could not find invoice"))
  225. })?;
  226. let status = if invoice_info.paid {
  227. MeltQuoteState::Paid
  228. } else {
  229. MeltQuoteState::Unpaid
  230. };
  231. let total_spent = Amount::from(
  232. (invoice_info
  233. .details
  234. .amount
  235. .checked_add(invoice_info.details.fee)
  236. .ok_or(Error::AmountOverflow)?)
  237. .unsigned_abs(),
  238. );
  239. Ok(MakePaymentResponse {
  240. payment_lookup_id: PaymentIdentifier::PaymentHash(
  241. hex::decode(pay_response.payment_hash)
  242. .map_err(|_| Error::InvalidPaymentHash)?
  243. .try_into()
  244. .map_err(|_| Error::InvalidPaymentHash)?,
  245. ),
  246. payment_proof: Some(invoice_info.details.payment_hash),
  247. status,
  248. total_spent,
  249. unit: CurrencyUnit::Msat,
  250. })
  251. }
  252. OutgoingPaymentOptions::Bolt12(_) => {
  253. Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LNbits")))
  254. }
  255. }
  256. }
  257. async fn create_incoming_payment_request(
  258. &self,
  259. unit: &CurrencyUnit,
  260. options: IncomingPaymentOptions,
  261. ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
  262. if unit != &CurrencyUnit::Sat {
  263. return Err(Self::Err::Anyhow(anyhow!("Unsupported unit")));
  264. }
  265. match options {
  266. IncomingPaymentOptions::Bolt11(bolt11_options) => {
  267. let description = bolt11_options.description.unwrap_or_default();
  268. let amount = bolt11_options.amount;
  269. let unix_expiry = bolt11_options.unix_expiry;
  270. let time_now = unix_time();
  271. let expiry = unix_expiry.map(|t| t - time_now);
  272. let invoice_request = CreateInvoiceRequest {
  273. amount: to_unit(amount, unit, &CurrencyUnit::Sat)?.into(),
  274. memo: Some(description),
  275. unit: unit.to_string(),
  276. expiry,
  277. webhook: self.webhook_url.clone(),
  278. internal: None,
  279. out: false,
  280. };
  281. let create_invoice_response = self
  282. .lnbits_api
  283. .create_invoice(&invoice_request)
  284. .await
  285. .map_err(|err| {
  286. tracing::error!("Could not create invoice");
  287. tracing::error!("{}", err.to_string());
  288. Self::Err::Anyhow(anyhow!("Could not create invoice"))
  289. })?;
  290. let request: Bolt11Invoice = create_invoice_response
  291. .bolt11()
  292. .ok_or_else(|| Self::Err::Anyhow(anyhow!("Missing bolt11 invoice")))?
  293. .parse()?;
  294. let expiry = request.expires_at().map(|t| t.as_secs());
  295. Ok(CreateIncomingPaymentResponse {
  296. request_lookup_id: PaymentIdentifier::PaymentHash(
  297. *request.payment_hash().as_ref(),
  298. ),
  299. request: request.to_string(),
  300. expiry,
  301. })
  302. }
  303. IncomingPaymentOptions::Bolt12(_) => {
  304. Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LNbits")))
  305. }
  306. }
  307. }
  308. async fn check_incoming_payment_status(
  309. &self,
  310. payment_identifier: &PaymentIdentifier,
  311. ) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
  312. let payment = self
  313. .lnbits_api
  314. .get_payment_info(&payment_identifier.to_string())
  315. .await
  316. .map_err(|err| {
  317. tracing::error!("Could not check invoice status");
  318. tracing::error!("{}", err.to_string());
  319. Self::Err::Anyhow(anyhow!("Could not check invoice status"))
  320. })?;
  321. let amount = payment.details.amount;
  322. if amount == i64::MIN {
  323. return Err(Error::AmountOverflow.into());
  324. }
  325. match payment.paid {
  326. true => Ok(vec![WaitPaymentResponse {
  327. payment_identifier: payment_identifier.clone(),
  328. payment_amount: Amount::from(amount.unsigned_abs()),
  329. unit: CurrencyUnit::Msat,
  330. payment_id: payment.details.payment_hash,
  331. }]),
  332. false => Ok(vec![]),
  333. }
  334. }
  335. async fn check_outgoing_payment(
  336. &self,
  337. payment_identifier: &PaymentIdentifier,
  338. ) -> Result<MakePaymentResponse, Self::Err> {
  339. let payment = self
  340. .lnbits_api
  341. .get_payment_info(&payment_identifier.to_string())
  342. .await
  343. .map_err(|err| {
  344. tracing::error!("Could not check invoice status");
  345. tracing::error!("{}", err.to_string());
  346. Self::Err::Anyhow(anyhow!("Could not check invoice status"))
  347. })?;
  348. let pay_response = MakePaymentResponse {
  349. payment_lookup_id: payment_identifier.clone(),
  350. payment_proof: payment.preimage,
  351. status: lnbits_to_melt_status(&payment.details.status, payment.details.pending),
  352. total_spent: Amount::from(
  353. payment.details.amount.unsigned_abs() + payment.details.fee.unsigned_abs(),
  354. ),
  355. unit: CurrencyUnit::Msat,
  356. };
  357. Ok(pay_response)
  358. }
  359. }
  360. fn lnbits_to_melt_status(status: &str, pending: Option<bool>) -> MeltQuoteState {
  361. if pending.unwrap_or_default() {
  362. return MeltQuoteState::Pending;
  363. }
  364. match status {
  365. "success" => MeltQuoteState::Paid,
  366. "failed" => MeltQuoteState::Unpaid,
  367. "pending" => MeltQuoteState::Pending,
  368. _ => MeltQuoteState::Unknown,
  369. }
  370. }
  371. impl LNbits {
  372. /// Create invoice webhook
  373. pub async fn create_invoice_webhook_router(
  374. &self,
  375. webhook_endpoint: &str,
  376. ) -> anyhow::Result<Router> {
  377. self.lnbits_api
  378. .create_invoice_webhook_router(webhook_endpoint)
  379. .await
  380. }
  381. }