lib.rs 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. //! CDK lightning backend for lnbits
  2. #![warn(missing_docs)]
  3. #![warn(rustdoc::bare_urls)]
  4. use std::pin::Pin;
  5. use std::sync::Arc;
  6. use anyhow::anyhow;
  7. use async_trait::async_trait;
  8. use axum::Router;
  9. use cdk::amount::Amount;
  10. use cdk::cdk_lightning::{
  11. self, to_unit, CreateInvoiceResponse, MintLightning, MintMeltSettings, PayInvoiceResponse,
  12. 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. pub mod error;
  25. /// LNbits
  26. #[derive(Clone)]
  27. pub struct LNbits {
  28. lnbits_api: LNBitsClient,
  29. mint_settings: MintMeltSettings,
  30. melt_settings: MintMeltSettings,
  31. fee_reserve: FeeReserve,
  32. receiver: Arc<Mutex<Option<tokio::sync::mpsc::Receiver<String>>>>,
  33. webhook_url: String,
  34. }
  35. impl LNbits {
  36. /// Create new [`LNbits`] wallet
  37. #[allow(clippy::too_many_arguments)]
  38. pub async fn new(
  39. admin_api_key: String,
  40. invoice_api_key: String,
  41. api_url: String,
  42. mint_settings: MintMeltSettings,
  43. melt_settings: MintMeltSettings,
  44. fee_reserve: FeeReserve,
  45. receiver: Arc<Mutex<Option<tokio::sync::mpsc::Receiver<String>>>>,
  46. webhook_url: String,
  47. ) -> Result<Self, Error> {
  48. let lnbits_api = LNBitsClient::new("", &admin_api_key, &invoice_api_key, &api_url, None)?;
  49. Ok(Self {
  50. lnbits_api,
  51. mint_settings,
  52. melt_settings,
  53. receiver,
  54. fee_reserve,
  55. webhook_url,
  56. })
  57. }
  58. }
  59. #[async_trait]
  60. impl MintLightning for LNbits {
  61. type Err = cdk_lightning::Error;
  62. fn get_settings(&self) -> Settings {
  63. Settings {
  64. mpp: false,
  65. unit: CurrencyUnit::Sat,
  66. mint_settings: self.mint_settings,
  67. melt_settings: self.melt_settings,
  68. }
  69. }
  70. async fn wait_any_invoice(
  71. &self,
  72. ) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> {
  73. let receiver = self
  74. .receiver
  75. .lock()
  76. .await
  77. .take()
  78. .ok_or(anyhow!("No receiver"))?;
  79. let lnbits_api = self.lnbits_api.clone();
  80. Ok(futures::stream::unfold(
  81. (receiver, lnbits_api),
  82. |(mut receiver, lnbits_api)| async move {
  83. match receiver.recv().await {
  84. Some(msg) => {
  85. let check = lnbits_api.is_invoice_paid(&msg).await;
  86. match check {
  87. Ok(state) => {
  88. if state {
  89. Some((msg, (receiver, lnbits_api)))
  90. } else {
  91. None
  92. }
  93. }
  94. _ => None,
  95. }
  96. }
  97. None => None,
  98. }
  99. },
  100. )
  101. .boxed())
  102. }
  103. async fn get_payment_quote(
  104. &self,
  105. melt_quote_request: &MeltQuoteBolt11Request,
  106. ) -> Result<PaymentQuoteResponse, Self::Err> {
  107. if melt_quote_request.unit != CurrencyUnit::Sat {
  108. return Err(Self::Err::Anyhow(anyhow!("Unsupported unit")));
  109. }
  110. let invoice_amount_msat = melt_quote_request
  111. .request
  112. .amount_milli_satoshis()
  113. .ok_or(Error::UnknownInvoiceAmount)?;
  114. let amount = to_unit(
  115. invoice_amount_msat,
  116. &CurrencyUnit::Msat,
  117. &melt_quote_request.unit,
  118. )?;
  119. let relative_fee_reserve =
  120. (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
  121. let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
  122. let fee = match relative_fee_reserve > absolute_fee_reserve {
  123. true => relative_fee_reserve,
  124. false => absolute_fee_reserve,
  125. };
  126. Ok(PaymentQuoteResponse {
  127. request_lookup_id: melt_quote_request.request.payment_hash().to_string(),
  128. amount,
  129. fee: fee.into(),
  130. state: MeltQuoteState::Unpaid,
  131. })
  132. }
  133. async fn pay_invoice(
  134. &self,
  135. melt_quote: mint::MeltQuote,
  136. _partial_msats: Option<Amount>,
  137. _max_fee_msats: Option<Amount>,
  138. ) -> Result<PayInvoiceResponse, Self::Err> {
  139. let pay_response = self
  140. .lnbits_api
  141. .pay_invoice(&melt_quote.request)
  142. .await
  143. .map_err(|err| {
  144. tracing::error!("Could not pay invoice");
  145. tracing::error!("{}", err.to_string());
  146. Self::Err::Anyhow(anyhow!("Could not pay invoice"))
  147. })?;
  148. let invoice_info = self
  149. .lnbits_api
  150. .find_invoice(&pay_response.payment_hash)
  151. .await
  152. .map_err(|err| {
  153. tracing::error!("Could not find invoice");
  154. tracing::error!("{}", err.to_string());
  155. Self::Err::Anyhow(anyhow!("Could not find invoice"))
  156. })?;
  157. let status = match invoice_info.pending {
  158. true => MeltQuoteState::Unpaid,
  159. false => MeltQuoteState::Paid,
  160. };
  161. let total_spent = Amount::from((invoice_info.amount + invoice_info.fee).unsigned_abs());
  162. Ok(PayInvoiceResponse {
  163. payment_hash: pay_response.payment_hash,
  164. payment_preimage: Some(invoice_info.payment_hash),
  165. status,
  166. total_spent,
  167. })
  168. }
  169. async fn create_invoice(
  170. &self,
  171. amount: Amount,
  172. unit: &CurrencyUnit,
  173. description: String,
  174. unix_expiry: u64,
  175. ) -> Result<CreateInvoiceResponse, Self::Err> {
  176. if unit != &CurrencyUnit::Sat {
  177. return Err(Self::Err::Anyhow(anyhow!("Unsupported unit")));
  178. }
  179. let time_now = unix_time();
  180. assert!(unix_expiry > time_now);
  181. let expiry = unix_expiry - time_now;
  182. let invoice_request = CreateInvoiceRequest {
  183. amount: to_unit(amount, unit, &CurrencyUnit::Sat)?.into(),
  184. memo: Some(description),
  185. unit: unit.to_string(),
  186. expiry: Some(expiry),
  187. webhook: Some(self.webhook_url.clone()),
  188. internal: None,
  189. out: false,
  190. };
  191. let create_invoice_response = self
  192. .lnbits_api
  193. .create_invoice(&invoice_request)
  194. .await
  195. .map_err(|err| {
  196. tracing::error!("Could not create invoice");
  197. tracing::error!("{}", err.to_string());
  198. Self::Err::Anyhow(anyhow!("Could not create invoice"))
  199. })?;
  200. let request: Bolt11Invoice = create_invoice_response.payment_request.parse()?;
  201. let expiry = request.expires_at().map(|t| t.as_secs());
  202. Ok(CreateInvoiceResponse {
  203. request_lookup_id: create_invoice_response.payment_hash,
  204. request,
  205. expiry,
  206. })
  207. }
  208. async fn check_invoice_status(
  209. &self,
  210. request_lookup_id: &str,
  211. ) -> Result<MintQuoteState, Self::Err> {
  212. let paid = self
  213. .lnbits_api
  214. .is_invoice_paid(request_lookup_id)
  215. .await
  216. .map_err(|err| {
  217. tracing::error!("Could not check invoice status");
  218. tracing::error!("{}", err.to_string());
  219. Self::Err::Anyhow(anyhow!("Could not check invoice status"))
  220. })?;
  221. let state = match paid {
  222. true => MintQuoteState::Paid,
  223. false => MintQuoteState::Unpaid,
  224. };
  225. Ok(state)
  226. }
  227. }
  228. impl LNbits {
  229. /// Create invoice webhook
  230. pub async fn create_invoice_webhook_router(
  231. &self,
  232. webhook_endpoint: &str,
  233. sender: tokio::sync::mpsc::Sender<String>,
  234. ) -> anyhow::Result<Router> {
  235. self.lnbits_api
  236. .create_invoice_webhook_router(webhook_endpoint, sender)
  237. .await
  238. }
  239. }