lib.rs 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. //! CDK lightning backend for Phoenixd
  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::{mint, Bolt11Invoice};
  20. use error::Error;
  21. use futures::{Stream, StreamExt};
  22. use phoenixd_rs::webhooks::WebhookResponse;
  23. use phoenixd_rs::{InvoiceRequest, Phoenixd as PhoenixdApi};
  24. use tokio::sync::Mutex;
  25. pub mod error;
  26. /// Phoenixd
  27. #[derive(Clone)]
  28. pub struct Phoenixd {
  29. mint_settings: MintMethodSettings,
  30. melt_settings: MeltMethodSettings,
  31. phoenixd_api: PhoenixdApi,
  32. fee_reserve: FeeReserve,
  33. receiver: Arc<Mutex<Option<tokio::sync::mpsc::Receiver<WebhookResponse>>>>,
  34. webhook_url: String,
  35. }
  36. impl Phoenixd {
  37. /// Create new [`Phoenixd`] wallet
  38. pub fn new(
  39. api_password: String,
  40. api_url: String,
  41. mint_settings: MintMethodSettings,
  42. melt_settings: MeltMethodSettings,
  43. fee_reserve: FeeReserve,
  44. receiver: Arc<Mutex<Option<tokio::sync::mpsc::Receiver<WebhookResponse>>>>,
  45. webhook_url: String,
  46. ) -> Result<Self, Error> {
  47. let phoenixd = PhoenixdApi::new(&api_password, &api_url)?;
  48. Ok(Self {
  49. mint_settings,
  50. melt_settings,
  51. phoenixd_api: phoenixd,
  52. fee_reserve,
  53. receiver,
  54. webhook_url,
  55. })
  56. }
  57. /// Create invoice webhook
  58. pub async fn create_invoice_webhook(
  59. &self,
  60. webhook_endpoint: &str,
  61. sender: tokio::sync::mpsc::Sender<WebhookResponse>,
  62. ) -> anyhow::Result<Router> {
  63. self.phoenixd_api
  64. .create_invoice_webhook_router(webhook_endpoint, sender)
  65. .await
  66. }
  67. }
  68. #[async_trait]
  69. impl MintLightning for Phoenixd {
  70. type Err = cdk_lightning::Error;
  71. fn get_settings(&self) -> Settings {
  72. Settings {
  73. mpp: false,
  74. unit: CurrencyUnit::Sat,
  75. mint_settings: self.mint_settings.clone(),
  76. melt_settings: self.melt_settings.clone(),
  77. invoice_description: true,
  78. }
  79. }
  80. async fn wait_any_invoice(
  81. &self,
  82. ) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> {
  83. let receiver = self
  84. .receiver
  85. .lock()
  86. .await
  87. .take()
  88. .ok_or(anyhow!("No receiver"))?;
  89. let phoenixd_api = self.phoenixd_api.clone();
  90. Ok(futures::stream::unfold(
  91. (receiver, phoenixd_api),
  92. |(mut receiver, phoenixd_api)| async move {
  93. match receiver.recv().await {
  94. Some(msg) => {
  95. let check = phoenixd_api.get_incoming_invoice(&msg.payment_hash).await;
  96. match check {
  97. Ok(state) => {
  98. if state.is_paid {
  99. Some((msg.payment_hash, (receiver, phoenixd_api)))
  100. } else {
  101. None
  102. }
  103. }
  104. _ => None,
  105. }
  106. }
  107. None => None,
  108. }
  109. },
  110. )
  111. .boxed())
  112. }
  113. async fn get_payment_quote(
  114. &self,
  115. melt_quote_request: &MeltQuoteBolt11Request,
  116. ) -> Result<PaymentQuoteResponse, Self::Err> {
  117. if CurrencyUnit::Sat != melt_quote_request.unit {
  118. return Err(Error::UnsupportedUnit.into());
  119. }
  120. let invoice_amount_msat = melt_quote_request
  121. .request
  122. .amount_milli_satoshis()
  123. .ok_or(Error::UnknownInvoiceAmount)?;
  124. let amount = to_unit(
  125. invoice_amount_msat,
  126. &CurrencyUnit::Msat,
  127. &melt_quote_request.unit,
  128. )?;
  129. let relative_fee_reserve =
  130. (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
  131. let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
  132. let mut fee = match relative_fee_reserve > absolute_fee_reserve {
  133. true => relative_fee_reserve,
  134. false => absolute_fee_reserve,
  135. };
  136. // Fee in phoenixd is always 0.04 + 4 sat
  137. fee += 4;
  138. Ok(PaymentQuoteResponse {
  139. request_lookup_id: melt_quote_request.request.payment_hash().to_string(),
  140. amount,
  141. fee: fee.into(),
  142. state: MeltQuoteState::Unpaid,
  143. })
  144. }
  145. async fn pay_invoice(
  146. &self,
  147. melt_quote: mint::MeltQuote,
  148. partial_amount: Option<Amount>,
  149. _max_fee_msats: Option<Amount>,
  150. ) -> Result<PayInvoiceResponse, Self::Err> {
  151. let pay_response = self
  152. .phoenixd_api
  153. .pay_bolt11_invoice(&melt_quote.request, partial_amount.map(|a| a.into()))
  154. .await?;
  155. // The pay response does not include the fee paided to Aciq so we check it here
  156. let check_outgoing_response = self
  157. .check_outgoing_invoice(&pay_response.payment_id)
  158. .await?;
  159. if check_outgoing_response.state != MeltQuoteState::Paid {
  160. return Err(anyhow!("Invoice is not paid").into());
  161. }
  162. let total_spent_sats = check_outgoing_response.fee + check_outgoing_response.amount;
  163. let bolt11: Bolt11Invoice = melt_quote.request.parse()?;
  164. Ok(PayInvoiceResponse {
  165. payment_hash: bolt11.payment_hash().to_string(),
  166. payment_preimage: Some(pay_response.payment_preimage),
  167. status: MeltQuoteState::Paid,
  168. total_spent: total_spent_sats,
  169. unit: CurrencyUnit::Sat,
  170. })
  171. }
  172. async fn create_invoice(
  173. &self,
  174. amount: Amount,
  175. unit: &CurrencyUnit,
  176. description: String,
  177. _unix_expiry: u64,
  178. ) -> Result<CreateInvoiceResponse, Self::Err> {
  179. let amount_sat = to_unit(amount, unit, &CurrencyUnit::Sat)?;
  180. let invoice_request = InvoiceRequest {
  181. external_id: None,
  182. description: Some(description),
  183. description_hash: None,
  184. amount_sat: amount_sat.into(),
  185. webhook_url: Some(self.webhook_url.clone()),
  186. };
  187. let create_invoice_response = self.phoenixd_api.create_invoice(invoice_request).await?;
  188. let bolt11: Bolt11Invoice = create_invoice_response.serialized.parse()?;
  189. let expiry = bolt11.expires_at().map(|t| t.as_secs());
  190. Ok(CreateInvoiceResponse {
  191. request_lookup_id: create_invoice_response.payment_hash,
  192. request: bolt11.clone(),
  193. expiry,
  194. })
  195. }
  196. async fn check_invoice_status(&self, payment_hash: &str) -> Result<MintQuoteState, Self::Err> {
  197. let invoice = self.phoenixd_api.get_incoming_invoice(payment_hash).await?;
  198. let state = match invoice.is_paid {
  199. true => MintQuoteState::Paid,
  200. false => MintQuoteState::Unpaid,
  201. };
  202. Ok(state)
  203. }
  204. }
  205. impl Phoenixd {
  206. /// Check the status of an outgooing invoice
  207. // TODO: This should likely bee added to the trait. Both CLN and PhD use a form
  208. // of it
  209. async fn check_outgoing_invoice(
  210. &self,
  211. payment_hash: &str,
  212. ) -> Result<PaymentQuoteResponse, Error> {
  213. let res = self.phoenixd_api.get_outgoing_invoice(payment_hash).await?;
  214. // Phenixd gives fees in msats so we need to round up to the nearst sat
  215. let fee_sats = (res.fees + 999) / MSAT_IN_SAT;
  216. let state = match res.is_paid {
  217. true => MeltQuoteState::Paid,
  218. false => MeltQuoteState::Unpaid,
  219. };
  220. let quote_response = PaymentQuoteResponse {
  221. request_lookup_id: res.payment_hash,
  222. amount: res.sent.into(),
  223. fee: fee_sats.into(),
  224. state,
  225. };
  226. Ok(quote_response)
  227. }
  228. }