lib.rs 8.4 KB

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