lib.rs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. //! CDK lightning backend for Phoenixd
  2. #![warn(missing_docs)]
  3. #![warn(rustdoc::bare_urls)]
  4. use std::pin::Pin;
  5. use std::str::FromStr;
  6. use std::sync::atomic::{AtomicBool, Ordering};
  7. use std::sync::Arc;
  8. use anyhow::anyhow;
  9. use async_trait::async_trait;
  10. use axum::Router;
  11. use cdk::amount::{to_unit, Amount, MSAT_IN_SAT};
  12. use cdk::cdk_lightning::{
  13. self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings,
  14. };
  15. use cdk::mint::FeeReserve;
  16. use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState};
  17. use cdk::{mint, Bolt11Invoice};
  18. use error::Error;
  19. use futures::{Stream, StreamExt};
  20. use phoenixd_rs::webhooks::WebhookResponse;
  21. use phoenixd_rs::{InvoiceRequest, Phoenixd as PhoenixdApi};
  22. use tokio::sync::Mutex;
  23. use tokio_util::sync::CancellationToken;
  24. pub mod error;
  25. /// Phoenixd
  26. #[derive(Clone)]
  27. pub struct Phoenixd {
  28. phoenixd_api: PhoenixdApi,
  29. fee_reserve: FeeReserve,
  30. receiver: Arc<Mutex<Option<tokio::sync::mpsc::Receiver<WebhookResponse>>>>,
  31. webhook_url: String,
  32. wait_invoice_cancel_token: CancellationToken,
  33. wait_invoice_is_active: Arc<AtomicBool>,
  34. }
  35. impl Phoenixd {
  36. /// Create new [`Phoenixd`] wallet
  37. pub fn new(
  38. api_password: String,
  39. api_url: String,
  40. fee_reserve: FeeReserve,
  41. receiver: Arc<Mutex<Option<tokio::sync::mpsc::Receiver<WebhookResponse>>>>,
  42. webhook_url: String,
  43. ) -> Result<Self, Error> {
  44. let phoenixd = PhoenixdApi::new(&api_password, &api_url)?;
  45. Ok(Self {
  46. phoenixd_api: phoenixd,
  47. fee_reserve,
  48. receiver,
  49. webhook_url,
  50. wait_invoice_cancel_token: CancellationToken::new(),
  51. wait_invoice_is_active: Arc::new(AtomicBool::new(false)),
  52. })
  53. }
  54. /// Create invoice webhook
  55. pub async fn create_invoice_webhook(
  56. &self,
  57. webhook_endpoint: &str,
  58. sender: tokio::sync::mpsc::Sender<WebhookResponse>,
  59. ) -> anyhow::Result<Router> {
  60. self.phoenixd_api
  61. .create_invoice_webhook_router(webhook_endpoint, sender)
  62. .await
  63. }
  64. }
  65. #[async_trait]
  66. impl MintLightning for Phoenixd {
  67. type Err = cdk_lightning::Error;
  68. fn get_settings(&self) -> Settings {
  69. Settings {
  70. mpp: false,
  71. unit: CurrencyUnit::Sat,
  72. invoice_description: true,
  73. }
  74. }
  75. fn is_wait_invoice_active(&self) -> bool {
  76. self.wait_invoice_is_active.load(Ordering::SeqCst)
  77. }
  78. fn cancel_wait_invoice(&self) {
  79. self.wait_invoice_cancel_token.cancel()
  80. }
  81. #[allow(clippy::incompatible_msrv)]
  82. async fn wait_any_invoice(
  83. &self,
  84. ) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> {
  85. let receiver = self
  86. .receiver
  87. .lock()
  88. .await
  89. .take()
  90. .ok_or(anyhow!("No receiver"))?;
  91. let phoenixd_api = self.phoenixd_api.clone();
  92. let cancel_token = self.wait_invoice_cancel_token.clone();
  93. Ok(futures::stream::unfold(
  94. (receiver, phoenixd_api, cancel_token,
  95. Arc::clone(&self.wait_invoice_is_active),
  96. ),
  97. |(mut receiver, phoenixd_api, cancel_token, is_active)| async move {
  98. is_active.store(true, Ordering::SeqCst);
  99. tokio::select! {
  100. _ = cancel_token.cancelled() => {
  101. // Stream is cancelled
  102. is_active.store(false, Ordering::SeqCst);
  103. tracing::info!("Waiting for phonixd invoice ending");
  104. None
  105. }
  106. msg_option = receiver.recv() => {
  107. match msg_option {
  108. Some(msg) => {
  109. let check = phoenixd_api.get_incoming_invoice(&msg.payment_hash).await;
  110. match check {
  111. Ok(state) => {
  112. if state.is_paid {
  113. // Yield the payment hash and continue the stream
  114. Some((msg.payment_hash, (receiver, phoenixd_api, cancel_token, is_active)))
  115. } else {
  116. // Invoice not paid yet, continue waiting
  117. // We need to continue the stream, so we return the same state
  118. None
  119. }
  120. }
  121. Err(e) => {
  122. // Log the error and continue
  123. tracing::warn!("Error checking invoice state: {:?}", e);
  124. None
  125. }
  126. }
  127. }
  128. None => {
  129. // The receiver stream has ended
  130. None
  131. }
  132. }
  133. }
  134. }
  135. },
  136. )
  137. .boxed())
  138. }
  139. async fn get_payment_quote(
  140. &self,
  141. melt_quote_request: &MeltQuoteBolt11Request,
  142. ) -> Result<PaymentQuoteResponse, Self::Err> {
  143. if CurrencyUnit::Sat != melt_quote_request.unit {
  144. return Err(Error::UnsupportedUnit.into());
  145. }
  146. let amount = melt_quote_request.amount_msat()?;
  147. let amount = amount / MSAT_IN_SAT.into();
  148. let relative_fee_reserve =
  149. (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
  150. let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
  151. let mut fee = match relative_fee_reserve > absolute_fee_reserve {
  152. true => relative_fee_reserve,
  153. false => absolute_fee_reserve,
  154. };
  155. // Fee in phoenixd is always 0.04 + 4 sat
  156. fee = fee.checked_add(4).ok_or(Error::AmountOverflow)?;
  157. Ok(PaymentQuoteResponse {
  158. request_lookup_id: melt_quote_request.request.payment_hash().to_string(),
  159. amount,
  160. fee: fee.into(),
  161. state: MeltQuoteState::Unpaid,
  162. })
  163. }
  164. async fn pay_invoice(
  165. &self,
  166. melt_quote: mint::MeltQuote,
  167. _partial_amount: Option<Amount>,
  168. _max_fee_msats: Option<Amount>,
  169. ) -> Result<PayInvoiceResponse, Self::Err> {
  170. let msat_to_pay: Option<u64> = melt_quote
  171. .msat_to_pay
  172. .map(|a| <cdk::Amount as Into<u64>>::into(a) / MSAT_IN_SAT);
  173. let pay_response = self
  174. .phoenixd_api
  175. .pay_bolt11_invoice(&melt_quote.request, msat_to_pay)
  176. .await?;
  177. // The pay invoice response does not give the needed fee info so we have to check.
  178. let check_outgoing_response = self
  179. .check_outgoing_payment(&pay_response.payment_id)
  180. .await?;
  181. let bolt11: Bolt11Invoice = melt_quote.request.parse()?;
  182. Ok(PayInvoiceResponse {
  183. payment_lookup_id: bolt11.payment_hash().to_string(),
  184. payment_preimage: Some(pay_response.payment_preimage),
  185. status: MeltQuoteState::Paid,
  186. total_spent: check_outgoing_response.total_spent,
  187. unit: CurrencyUnit::Sat,
  188. })
  189. }
  190. async fn create_invoice(
  191. &self,
  192. amount: Amount,
  193. unit: &CurrencyUnit,
  194. description: String,
  195. _unix_expiry: u64,
  196. ) -> Result<CreateInvoiceResponse, Self::Err> {
  197. let amount_sat = to_unit(amount, unit, &CurrencyUnit::Sat)?;
  198. let invoice_request = InvoiceRequest {
  199. external_id: None,
  200. description: Some(description),
  201. description_hash: None,
  202. amount_sat: amount_sat.into(),
  203. webhook_url: Some(self.webhook_url.clone()),
  204. };
  205. let create_invoice_response = self.phoenixd_api.create_invoice(invoice_request).await?;
  206. let bolt11: Bolt11Invoice = create_invoice_response.serialized.parse()?;
  207. let expiry = bolt11.expires_at().map(|t| t.as_secs());
  208. Ok(CreateInvoiceResponse {
  209. request_lookup_id: create_invoice_response.payment_hash,
  210. request: bolt11.clone(),
  211. expiry,
  212. })
  213. }
  214. async fn check_incoming_invoice_status(
  215. &self,
  216. payment_hash: &str,
  217. ) -> Result<MintQuoteState, Self::Err> {
  218. let invoice = self.phoenixd_api.get_incoming_invoice(payment_hash).await?;
  219. let state = match invoice.is_paid {
  220. true => MintQuoteState::Paid,
  221. false => MintQuoteState::Unpaid,
  222. };
  223. Ok(state)
  224. }
  225. /// Check the status of an outgoing invoice
  226. async fn check_outgoing_payment(
  227. &self,
  228. payment_id: &str,
  229. ) -> Result<PayInvoiceResponse, Self::Err> {
  230. // We can only check the status of the payment if we have the payment id not if we only have a payment hash.
  231. // In phd this is a uuid, that we get after getting a response from the pay invoice
  232. if let Err(_err) = uuid::Uuid::from_str(payment_id) {
  233. tracing::warn!("Could not check status of payment, no payment id");
  234. return Ok(PayInvoiceResponse {
  235. payment_lookup_id: payment_id.to_string(),
  236. payment_preimage: None,
  237. status: MeltQuoteState::Unknown,
  238. total_spent: Amount::ZERO,
  239. unit: CurrencyUnit::Sat,
  240. });
  241. }
  242. let res = self.phoenixd_api.get_outgoing_invoice(payment_id).await;
  243. let state = match res {
  244. Ok(res) => {
  245. let status = match res.is_paid {
  246. true => MeltQuoteState::Paid,
  247. false => MeltQuoteState::Unpaid,
  248. };
  249. let total_spent = res.sent + (res.fees + 999) / MSAT_IN_SAT;
  250. PayInvoiceResponse {
  251. payment_lookup_id: res.payment_hash,
  252. payment_preimage: Some(res.preimage),
  253. status,
  254. total_spent: total_spent.into(),
  255. unit: CurrencyUnit::Sat,
  256. }
  257. }
  258. Err(err) => match err {
  259. phoenixd_rs::Error::NotFound => PayInvoiceResponse {
  260. payment_lookup_id: payment_id.to_string(),
  261. payment_preimage: None,
  262. status: MeltQuoteState::Unknown,
  263. total_spent: Amount::ZERO,
  264. unit: CurrencyUnit::Sat,
  265. },
  266. _ => {
  267. return Err(Error::from(err).into());
  268. }
  269. },
  270. };
  271. Ok(state)
  272. }
  273. }