lib.rs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. //! CDK lightning backend for CLN
  2. #![warn(missing_docs)]
  3. #![warn(rustdoc::bare_urls)]
  4. use std::path::PathBuf;
  5. use std::pin::Pin;
  6. use std::str::FromStr;
  7. use std::sync::Arc;
  8. use std::time::Duration;
  9. use async_trait::async_trait;
  10. use cdk::amount::Amount;
  11. use cdk::cdk_lightning::{
  12. self, to_unit, CreateInvoiceResponse, MintLightning, MintMeltSettings, PayInvoiceResponse,
  13. PaymentQuoteResponse, Settings,
  14. };
  15. use cdk::mint::FeeReserve;
  16. use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState};
  17. use cdk::util::{hex, unix_time};
  18. use cdk::{mint, Bolt11Invoice};
  19. use cln_rpc::model::requests::{
  20. InvoiceRequest, ListinvoicesRequest, ListpaysRequest, PayRequest, WaitanyinvoiceRequest,
  21. };
  22. use cln_rpc::model::responses::{
  23. ListinvoicesInvoicesStatus, ListpaysPaysStatus, PayStatus, WaitanyinvoiceResponse,
  24. };
  25. use cln_rpc::model::Request;
  26. use cln_rpc::primitives::{Amount as CLN_Amount, AmountOrAny};
  27. use error::Error;
  28. use futures::{Stream, StreamExt};
  29. use tokio::sync::Mutex;
  30. use uuid::Uuid;
  31. pub mod error;
  32. /// CLN mint backend
  33. #[derive(Clone)]
  34. pub struct Cln {
  35. rpc_socket: PathBuf,
  36. cln_client: Arc<Mutex<cln_rpc::ClnRpc>>,
  37. fee_reserve: FeeReserve,
  38. mint_settings: MintMeltSettings,
  39. melt_settings: MintMeltSettings,
  40. }
  41. impl Cln {
  42. /// Create new [`Cln`]
  43. pub async fn new(
  44. rpc_socket: PathBuf,
  45. fee_reserve: FeeReserve,
  46. mint_settings: MintMeltSettings,
  47. melt_settings: MintMeltSettings,
  48. ) -> Result<Self, Error> {
  49. let cln_client = cln_rpc::ClnRpc::new(&rpc_socket).await?;
  50. Ok(Self {
  51. rpc_socket,
  52. cln_client: Arc::new(Mutex::new(cln_client)),
  53. fee_reserve,
  54. mint_settings,
  55. melt_settings,
  56. })
  57. }
  58. }
  59. #[async_trait]
  60. impl MintLightning for Cln {
  61. type Err = cdk_lightning::Error;
  62. fn get_settings(&self) -> Settings {
  63. Settings {
  64. mpp: true,
  65. unit: CurrencyUnit::Msat,
  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 last_pay_index = self.get_last_pay_index().await?;
  74. let cln_client = cln_rpc::ClnRpc::new(&self.rpc_socket).await?;
  75. Ok(futures::stream::unfold(
  76. (cln_client, last_pay_index),
  77. |(mut cln_client, mut last_pay_idx)| async move {
  78. loop {
  79. let invoice_res = cln_client
  80. .call(cln_rpc::Request::WaitAnyInvoice(WaitanyinvoiceRequest {
  81. timeout: None,
  82. lastpay_index: last_pay_idx,
  83. }))
  84. .await;
  85. let invoice: WaitanyinvoiceResponse = match invoice_res {
  86. Ok(invoice) => invoice,
  87. Err(e) => {
  88. tracing::warn!("Error fetching invoice: {e}");
  89. // Let's not spam CLN with requests on failure
  90. tokio::time::sleep(Duration::from_secs(1)).await;
  91. // Retry same request
  92. continue;
  93. }
  94. }
  95. .try_into()
  96. .expect("Wrong response from CLN");
  97. last_pay_idx = invoice.pay_index;
  98. break Some((invoice.label, (cln_client, last_pay_idx)));
  99. }
  100. },
  101. )
  102. .boxed())
  103. }
  104. async fn get_payment_quote(
  105. &self,
  106. melt_quote_request: &MeltQuoteBolt11Request,
  107. ) -> Result<PaymentQuoteResponse, Self::Err> {
  108. let invoice_amount_msat = melt_quote_request
  109. .request
  110. .amount_milli_satoshis()
  111. .ok_or(Error::UnknownInvoiceAmount)?;
  112. let amount = to_unit(
  113. invoice_amount_msat,
  114. &CurrencyUnit::Msat,
  115. &melt_quote_request.unit,
  116. )?;
  117. let relative_fee_reserve =
  118. (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
  119. let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
  120. let fee = match relative_fee_reserve > absolute_fee_reserve {
  121. true => relative_fee_reserve,
  122. false => absolute_fee_reserve,
  123. };
  124. Ok(PaymentQuoteResponse {
  125. request_lookup_id: melt_quote_request.request.payment_hash().to_string(),
  126. amount,
  127. fee: fee.into(),
  128. state: MeltQuoteState::Unpaid,
  129. })
  130. }
  131. async fn pay_invoice(
  132. &self,
  133. melt_quote: mint::MeltQuote,
  134. partial_amount: Option<Amount>,
  135. max_fee: Option<Amount>,
  136. ) -> Result<PayInvoiceResponse, Self::Err> {
  137. let mut cln_client = self.cln_client.lock().await;
  138. let pay_state =
  139. check_pay_invoice_status(&mut cln_client, melt_quote.request.to_string()).await?;
  140. match pay_state {
  141. MeltQuoteState::Paid => {
  142. tracing::debug!("Melt attempted on invoice already paid");
  143. return Err(Self::Err::InvoiceAlreadyPaid);
  144. }
  145. MeltQuoteState::Pending => {
  146. tracing::debug!("Melt attempted on invoice already pending");
  147. return Err(Self::Err::InvoicePaymentPending);
  148. }
  149. MeltQuoteState::Unpaid => (),
  150. }
  151. let cln_response = cln_client
  152. .call(Request::Pay(PayRequest {
  153. bolt11: melt_quote.request.to_string(),
  154. amount_msat: None,
  155. label: None,
  156. riskfactor: None,
  157. maxfeepercent: None,
  158. retry_for: None,
  159. maxdelay: None,
  160. exemptfee: None,
  161. localinvreqid: None,
  162. exclude: None,
  163. maxfee: max_fee
  164. .map(|a| {
  165. let msat = to_unit(a, &melt_quote.unit, &CurrencyUnit::Msat)?;
  166. Ok::<cln_rpc::primitives::Amount, Self::Err>(CLN_Amount::from_msat(
  167. msat.into(),
  168. ))
  169. })
  170. .transpose()?,
  171. description: None,
  172. partial_msat: partial_amount
  173. .map(|a| {
  174. let msat = to_unit(a, &melt_quote.unit, &CurrencyUnit::Msat)?;
  175. Ok::<cln_rpc::primitives::Amount, Self::Err>(CLN_Amount::from_msat(
  176. msat.into(),
  177. ))
  178. })
  179. .transpose()?,
  180. }))
  181. .await
  182. .map_err(Error::from)?;
  183. let response = match cln_response {
  184. cln_rpc::Response::Pay(pay_response) => {
  185. let status = match pay_response.status {
  186. PayStatus::COMPLETE => MeltQuoteState::Paid,
  187. PayStatus::PENDING => MeltQuoteState::Pending,
  188. PayStatus::FAILED => MeltQuoteState::Unpaid,
  189. };
  190. PayInvoiceResponse {
  191. payment_preimage: Some(hex::encode(pay_response.payment_preimage.to_vec())),
  192. payment_hash: pay_response.payment_hash.to_string(),
  193. status,
  194. total_spent: to_unit(
  195. pay_response.amount_sent_msat.msat(),
  196. &CurrencyUnit::Msat,
  197. &melt_quote.unit,
  198. )?,
  199. }
  200. }
  201. _ => {
  202. tracing::warn!("CLN returned wrong response kind");
  203. return Err(cdk_lightning::Error::from(Error::WrongClnResponse));
  204. }
  205. };
  206. Ok(response)
  207. }
  208. async fn create_invoice(
  209. &self,
  210. amount: Amount,
  211. unit: &CurrencyUnit,
  212. description: String,
  213. unix_expiry: u64,
  214. ) -> Result<CreateInvoiceResponse, Self::Err> {
  215. let time_now = unix_time();
  216. assert!(unix_expiry > time_now);
  217. let mut cln_client = self.cln_client.lock().await;
  218. let label = Uuid::new_v4().to_string();
  219. let amount = to_unit(amount, unit, &CurrencyUnit::Msat)?;
  220. let amount_msat = AmountOrAny::Amount(CLN_Amount::from_msat(amount.into()));
  221. let cln_response = cln_client
  222. .call(cln_rpc::Request::Invoice(InvoiceRequest {
  223. amount_msat,
  224. description,
  225. label: label.clone(),
  226. expiry: Some(unix_expiry - time_now),
  227. fallbacks: None,
  228. preimage: None,
  229. cltv: None,
  230. deschashonly: None,
  231. exposeprivatechannels: None,
  232. }))
  233. .await
  234. .map_err(Error::from)?;
  235. match cln_response {
  236. cln_rpc::Response::Invoice(invoice_res) => {
  237. let request = Bolt11Invoice::from_str(&invoice_res.bolt11)?;
  238. let expiry = request.expires_at().map(|t| t.as_secs());
  239. Ok(CreateInvoiceResponse {
  240. request_lookup_id: label,
  241. request,
  242. expiry,
  243. })
  244. }
  245. _ => {
  246. tracing::warn!("CLN returned wrong response kind");
  247. Err(Error::WrongClnResponse.into())
  248. }
  249. }
  250. }
  251. async fn check_invoice_status(
  252. &self,
  253. request_lookup_id: &str,
  254. ) -> Result<MintQuoteState, Self::Err> {
  255. let mut cln_client = self.cln_client.lock().await;
  256. let cln_response = cln_client
  257. .call(Request::ListInvoices(ListinvoicesRequest {
  258. payment_hash: None,
  259. label: Some(request_lookup_id.to_string()),
  260. invstring: None,
  261. offer_id: None,
  262. index: None,
  263. limit: None,
  264. start: None,
  265. }))
  266. .await
  267. .map_err(Error::from)?;
  268. let status = match cln_response {
  269. cln_rpc::Response::ListInvoices(invoice_response) => {
  270. match invoice_response.invoices.first() {
  271. Some(invoice_response) => {
  272. cln_invoice_status_to_mint_state(invoice_response.status)
  273. }
  274. None => {
  275. tracing::info!(
  276. "Check invoice called on unknown look up id: {}",
  277. request_lookup_id
  278. );
  279. return Err(Error::WrongClnResponse.into());
  280. }
  281. }
  282. }
  283. _ => {
  284. tracing::warn!("CLN returned wrong response kind");
  285. return Err(Error::WrongClnResponse.into());
  286. }
  287. };
  288. Ok(status)
  289. }
  290. }
  291. impl Cln {
  292. /// Get last pay index for cln
  293. async fn get_last_pay_index(&self) -> Result<Option<u64>, Error> {
  294. let mut cln_client = self.cln_client.lock().await;
  295. let cln_response = cln_client
  296. .call(cln_rpc::Request::ListInvoices(ListinvoicesRequest {
  297. index: None,
  298. invstring: None,
  299. label: None,
  300. limit: None,
  301. offer_id: None,
  302. payment_hash: None,
  303. start: None,
  304. }))
  305. .await
  306. .map_err(Error::from)?;
  307. match cln_response {
  308. cln_rpc::Response::ListInvoices(invoice_res) => match invoice_res.invoices.last() {
  309. Some(last_invoice) => Ok(last_invoice.pay_index),
  310. None => Ok(None),
  311. },
  312. _ => {
  313. tracing::warn!("CLN returned wrong response kind");
  314. Err(Error::WrongClnResponse)
  315. }
  316. }
  317. }
  318. }
  319. fn cln_invoice_status_to_mint_state(status: ListinvoicesInvoicesStatus) -> MintQuoteState {
  320. match status {
  321. ListinvoicesInvoicesStatus::UNPAID => MintQuoteState::Unpaid,
  322. ListinvoicesInvoicesStatus::PAID => MintQuoteState::Paid,
  323. ListinvoicesInvoicesStatus::EXPIRED => MintQuoteState::Unpaid,
  324. }
  325. }
  326. async fn check_pay_invoice_status(
  327. cln_client: &mut cln_rpc::ClnRpc,
  328. bolt11: String,
  329. ) -> Result<MeltQuoteState, cdk_lightning::Error> {
  330. let cln_response = cln_client
  331. .call(Request::ListPays(ListpaysRequest {
  332. bolt11: Some(bolt11),
  333. payment_hash: None,
  334. status: None,
  335. }))
  336. .await
  337. .map_err(Error::from)?;
  338. let state = match cln_response {
  339. cln_rpc::Response::ListPays(pay_response) => {
  340. let pay = pay_response.pays.first();
  341. match pay {
  342. Some(pay) => match pay.status {
  343. ListpaysPaysStatus::COMPLETE => MeltQuoteState::Paid,
  344. ListpaysPaysStatus::PENDING => MeltQuoteState::Pending,
  345. ListpaysPaysStatus::FAILED => MeltQuoteState::Unpaid,
  346. },
  347. None => MeltQuoteState::Unpaid,
  348. }
  349. }
  350. _ => {
  351. tracing::warn!("CLN returned wrong response kind. When checking pay status");
  352. return Err(cdk_lightning::Error::from(Error::WrongClnResponse));
  353. }
  354. };
  355. Ok(state)
  356. }