lib.rs 9.5 KB

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