lib.rs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. //! CDK lightning backend for lnbits
  2. #![warn(missing_docs)]
  3. #![warn(rustdoc::bare_urls)]
  4. use std::pin::Pin;
  5. use std::sync::atomic::{AtomicBool, Ordering};
  6. use std::sync::Arc;
  7. use anyhow::anyhow;
  8. use async_trait::async_trait;
  9. use axum::Router;
  10. use cdk::amount::{to_unit, Amount, MSAT_IN_SAT};
  11. use cdk::cdk_lightning::{
  12. self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings,
  13. };
  14. use cdk::mint::FeeReserve;
  15. use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState};
  16. use cdk::util::unix_time;
  17. use cdk::{mint, Bolt11Invoice};
  18. use error::Error;
  19. use futures::stream::StreamExt;
  20. use futures::Stream;
  21. use lnbits_rs::api::invoice::CreateInvoiceRequest;
  22. use lnbits_rs::LNBitsClient;
  23. use tokio::sync::Mutex;
  24. use tokio_util::sync::CancellationToken;
  25. pub mod error;
  26. /// LNbits
  27. #[derive(Clone)]
  28. pub struct LNbits {
  29. lnbits_api: LNBitsClient,
  30. fee_reserve: FeeReserve,
  31. receiver: Arc<Mutex<Option<tokio::sync::mpsc::Receiver<String>>>>,
  32. webhook_url: String,
  33. wait_invoice_cancel_token: CancellationToken,
  34. wait_invoice_is_active: Arc<AtomicBool>,
  35. }
  36. impl LNbits {
  37. /// Create new [`LNbits`] wallet
  38. #[allow(clippy::too_many_arguments)]
  39. pub async fn new(
  40. admin_api_key: String,
  41. invoice_api_key: String,
  42. api_url: String,
  43. fee_reserve: FeeReserve,
  44. receiver: Arc<Mutex<Option<tokio::sync::mpsc::Receiver<String>>>>,
  45. webhook_url: String,
  46. ) -> Result<Self, Error> {
  47. let lnbits_api = LNBitsClient::new("", &admin_api_key, &invoice_api_key, &api_url, None)?;
  48. Ok(Self {
  49. lnbits_api,
  50. receiver,
  51. fee_reserve,
  52. webhook_url,
  53. wait_invoice_cancel_token: CancellationToken::new(),
  54. wait_invoice_is_active: Arc::new(AtomicBool::new(false)),
  55. })
  56. }
  57. }
  58. #[async_trait]
  59. impl MintLightning for LNbits {
  60. type Err = cdk_lightning::Error;
  61. fn get_settings(&self) -> Settings {
  62. Settings {
  63. mpp: false,
  64. unit: CurrencyUnit::Sat,
  65. invoice_description: true,
  66. }
  67. }
  68. fn is_wait_invoice_active(&self) -> bool {
  69. self.wait_invoice_is_active.load(Ordering::SeqCst)
  70. }
  71. fn cancel_wait_invoice(&self) {
  72. self.wait_invoice_cancel_token.cancel()
  73. }
  74. #[allow(clippy::incompatible_msrv)]
  75. async fn wait_any_invoice(
  76. &self,
  77. ) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> {
  78. let receiver = self
  79. .receiver
  80. .lock()
  81. .await
  82. .take()
  83. .ok_or(anyhow!("No receiver"))?;
  84. let lnbits_api = self.lnbits_api.clone();
  85. let cancel_token = self.wait_invoice_cancel_token.clone();
  86. Ok(futures::stream::unfold(
  87. (
  88. receiver,
  89. lnbits_api,
  90. cancel_token,
  91. Arc::clone(&self.wait_invoice_is_active),
  92. ),
  93. |(mut receiver, lnbits_api, cancel_token, is_active)| async move {
  94. is_active.store(true, Ordering::SeqCst);
  95. tokio::select! {
  96. _ = cancel_token.cancelled() => {
  97. // Stream is cancelled
  98. is_active.store(false, Ordering::SeqCst);
  99. tracing::info!("Waiting for phonixd invoice ending");
  100. None
  101. }
  102. msg_option = receiver.recv() => {
  103. match msg_option {
  104. Some(msg) => {
  105. let check = lnbits_api.is_invoice_paid(&msg).await;
  106. match check {
  107. Ok(state) => {
  108. if state {
  109. Some((msg, (receiver, lnbits_api, cancel_token, is_active)))
  110. } else {
  111. None
  112. }
  113. }
  114. _ => None,
  115. }
  116. }
  117. None => {
  118. is_active.store(true, Ordering::SeqCst);
  119. None
  120. },
  121. }
  122. }
  123. }
  124. },
  125. )
  126. .boxed())
  127. }
  128. async fn get_payment_quote(
  129. &self,
  130. melt_quote_request: &MeltQuoteBolt11Request,
  131. ) -> Result<PaymentQuoteResponse, Self::Err> {
  132. if melt_quote_request.unit != CurrencyUnit::Sat {
  133. return Err(Self::Err::Anyhow(anyhow!("Unsupported unit")));
  134. }
  135. let amount = melt_quote_request.amount_msat()?;
  136. let amount = amount / MSAT_IN_SAT.into();
  137. let relative_fee_reserve =
  138. (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
  139. let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
  140. let fee = match relative_fee_reserve > absolute_fee_reserve {
  141. true => relative_fee_reserve,
  142. false => absolute_fee_reserve,
  143. };
  144. Ok(PaymentQuoteResponse {
  145. request_lookup_id: melt_quote_request.request.payment_hash().to_string(),
  146. amount,
  147. fee: fee.into(),
  148. state: MeltQuoteState::Unpaid,
  149. })
  150. }
  151. async fn pay_invoice(
  152. &self,
  153. melt_quote: mint::MeltQuote,
  154. _partial_msats: Option<Amount>,
  155. _max_fee_msats: Option<Amount>,
  156. ) -> Result<PayInvoiceResponse, Self::Err> {
  157. let pay_response = self
  158. .lnbits_api
  159. .pay_invoice(&melt_quote.request)
  160. .await
  161. .map_err(|err| {
  162. tracing::error!("Could not pay invoice");
  163. tracing::error!("{}", err.to_string());
  164. Self::Err::Anyhow(anyhow!("Could not pay invoice"))
  165. })?;
  166. let invoice_info = self
  167. .lnbits_api
  168. .find_invoice(&pay_response.payment_hash)
  169. .await
  170. .map_err(|err| {
  171. tracing::error!("Could not find invoice");
  172. tracing::error!("{}", err.to_string());
  173. Self::Err::Anyhow(anyhow!("Could not find invoice"))
  174. })?;
  175. let status = match invoice_info.pending {
  176. true => MeltQuoteState::Unpaid,
  177. false => MeltQuoteState::Paid,
  178. };
  179. let total_spent = Amount::from(
  180. (invoice_info
  181. .amount
  182. .checked_add(invoice_info.fee)
  183. .ok_or(Error::AmountOverflow)?)
  184. .unsigned_abs(),
  185. );
  186. Ok(PayInvoiceResponse {
  187. payment_lookup_id: pay_response.payment_hash,
  188. payment_preimage: Some(invoice_info.payment_hash),
  189. status,
  190. total_spent,
  191. unit: CurrencyUnit::Sat,
  192. })
  193. }
  194. async fn create_invoice(
  195. &self,
  196. amount: Amount,
  197. unit: &CurrencyUnit,
  198. description: String,
  199. unix_expiry: u64,
  200. ) -> Result<CreateInvoiceResponse, Self::Err> {
  201. if unit != &CurrencyUnit::Sat {
  202. return Err(Self::Err::Anyhow(anyhow!("Unsupported unit")));
  203. }
  204. let time_now = unix_time();
  205. assert!(unix_expiry > time_now);
  206. let expiry = unix_expiry - time_now;
  207. let invoice_request = CreateInvoiceRequest {
  208. amount: to_unit(amount, unit, &CurrencyUnit::Sat)?.into(),
  209. memo: Some(description),
  210. unit: unit.to_string(),
  211. expiry: Some(expiry),
  212. webhook: Some(self.webhook_url.clone()),
  213. internal: None,
  214. out: false,
  215. };
  216. let create_invoice_response = self
  217. .lnbits_api
  218. .create_invoice(&invoice_request)
  219. .await
  220. .map_err(|err| {
  221. tracing::error!("Could not create invoice");
  222. tracing::error!("{}", err.to_string());
  223. Self::Err::Anyhow(anyhow!("Could not create invoice"))
  224. })?;
  225. let request: Bolt11Invoice = create_invoice_response.payment_request.parse()?;
  226. let expiry = request.expires_at().map(|t| t.as_secs());
  227. Ok(CreateInvoiceResponse {
  228. request_lookup_id: create_invoice_response.payment_hash,
  229. request,
  230. expiry,
  231. })
  232. }
  233. async fn check_incoming_invoice_status(
  234. &self,
  235. payment_hash: &str,
  236. ) -> Result<MintQuoteState, Self::Err> {
  237. let paid = self
  238. .lnbits_api
  239. .is_invoice_paid(payment_hash)
  240. .await
  241. .map_err(|err| {
  242. tracing::error!("Could not check invoice status");
  243. tracing::error!("{}", err.to_string());
  244. Self::Err::Anyhow(anyhow!("Could not check invoice status"))
  245. })?;
  246. let state = match paid {
  247. true => MintQuoteState::Paid,
  248. false => MintQuoteState::Unpaid,
  249. };
  250. Ok(state)
  251. }
  252. async fn check_outgoing_payment(
  253. &self,
  254. payment_hash: &str,
  255. ) -> Result<PayInvoiceResponse, Self::Err> {
  256. let payment = self
  257. .lnbits_api
  258. .get_payment_info(payment_hash)
  259. .await
  260. .map_err(|err| {
  261. tracing::error!("Could not check invoice status");
  262. tracing::error!("{}", err.to_string());
  263. Self::Err::Anyhow(anyhow!("Could not check invoice status"))
  264. })?;
  265. let pay_response = PayInvoiceResponse {
  266. payment_lookup_id: payment.details.payment_hash,
  267. payment_preimage: Some(payment.preimage),
  268. status: lnbits_to_melt_status(&payment.details.status, payment.details.pending),
  269. total_spent: Amount::from(
  270. payment.details.amount.unsigned_abs()
  271. + payment.details.fee.unsigned_abs() / MSAT_IN_SAT,
  272. ),
  273. unit: self.get_settings().unit,
  274. };
  275. Ok(pay_response)
  276. }
  277. }
  278. fn lnbits_to_melt_status(status: &str, pending: bool) -> MeltQuoteState {
  279. match (status, pending) {
  280. ("success", false) => MeltQuoteState::Paid,
  281. ("failed", false) => MeltQuoteState::Unpaid,
  282. (_, false) => MeltQuoteState::Unknown,
  283. (_, true) => MeltQuoteState::Pending,
  284. }
  285. }
  286. impl LNbits {
  287. /// Create invoice webhook
  288. pub async fn create_invoice_webhook_router(
  289. &self,
  290. webhook_endpoint: &str,
  291. sender: tokio::sync::mpsc::Sender<String>,
  292. ) -> anyhow::Result<Router> {
  293. self.lnbits_api
  294. .create_invoice_webhook_router(webhook_endpoint, sender)
  295. .await
  296. }
  297. }