lib.rs 16 KB

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