1use std::path::PathBuf;
2use std::pin::Pin;
3use std::sync::atomic::{AtomicBool, Ordering};
4use std::sync::Arc;
5use std::task::{Context, Poll};
6
7use anyhow::anyhow;
8use cdk_common::grpc::{VersionInterceptor, VERSION_HEADER};
9use cdk_common::payment::{
10 CreateIncomingPaymentResponse, IncomingPaymentOptions as CdkIncomingPaymentOptions,
11 MakePaymentResponse as CdkMakePaymentResponse, MintPayment,
12 PaymentQuoteResponse as CdkPaymentQuoteResponse, WaitPaymentResponse,
13};
14use futures::{Stream, StreamExt};
15use tokio::sync::Mutex;
16use tokio_util::sync::CancellationToken;
17use tonic::codegen::InterceptedService;
18use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity};
19use tonic::{async_trait, Request};
20use tracing::instrument;
21
22use crate::proto::cdk_payment_processor_client::CdkPaymentProcessorClient;
23use crate::proto::{
24 CheckIncomingPaymentRequest, CheckOutgoingPaymentRequest, CreatePaymentRequest, EmptyRequest,
25 IncomingPaymentOptions, IntoProtoAmount, MakePaymentRequest, OutgoingPaymentRequestType,
26 PaymentQuoteRequest,
27};
28
29#[derive(Clone)]
31pub struct PaymentProcessorClient {
32 inner: CdkPaymentProcessorClient<InterceptedService<Channel, VersionInterceptor>>,
33 payment_event_stream_is_active: Arc<AtomicBool>,
34 cancel_payment_event_stream: Arc<Mutex<CancellationToken>>,
35}
36
37struct ActivePaymentEventStream {
38 inner: Pin<Box<dyn Stream<Item = cdk_common::payment::Event> + Send>>,
39 active_flag: Arc<AtomicBool>,
40}
41
42impl ActivePaymentEventStream {
43 fn new(
44 inner: Pin<Box<dyn Stream<Item = cdk_common::payment::Event> + Send>>,
45 active_flag: Arc<AtomicBool>,
46 ) -> Self {
47 Self { inner, active_flag }
48 }
49}
50
51impl Drop for ActivePaymentEventStream {
52 fn drop(&mut self) {
53 self.active_flag.store(false, Ordering::SeqCst);
54 tracing::info!("Payment event stream inactive");
55 }
56}
57
58impl Stream for ActivePaymentEventStream {
59 type Item = cdk_common::payment::Event;
60
61 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
62 let this = self.get_mut();
63 this.inner.as_mut().poll_next(cx)
64 }
65}
66
67impl std::fmt::Debug for PaymentProcessorClient {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 f.debug_struct("PaymentProcessorClient")
70 .finish_non_exhaustive()
71 }
72}
73
74impl PaymentProcessorClient {
75 pub async fn new(addr: &str, port: u16, tls_dir: Option<PathBuf>) -> anyhow::Result<Self> {
77 let scheme = if tls_dir.is_some() { "https" } else { "http" };
78 let endpoint = format!("{scheme}://{addr}:{port}");
79
80 let channel = if let Some(tls_dir) = tls_dir {
81 let ca_pem_path = tls_dir.join("ca.pem");
85 if !ca_pem_path.exists() {
86 let err_msg = format!("CA certificate file not found: {}", ca_pem_path.display());
87 tracing::error!("{}", err_msg);
88 return Err(anyhow!(err_msg));
89 }
90
91 let client_pem_path = tls_dir.join("client.pem");
93
94 let client_key_path = tls_dir.join("client.key");
96 let server_root_ca_cert = std::fs::read_to_string(&ca_pem_path)?;
98 let server_root_ca_cert = Certificate::from_pem(server_root_ca_cert);
99 let tls: ClientTlsConfig = match client_pem_path.exists() && client_key_path.exists() {
100 true => {
101 let client_cert = std::fs::read_to_string(&client_pem_path)?;
102 let client_key = std::fs::read_to_string(&client_key_path)?;
103 let client_identity = Identity::from_pem(client_cert, client_key);
104 ClientTlsConfig::new()
105 .ca_certificate(server_root_ca_cert)
106 .identity(client_identity)
107 }
108 false => ClientTlsConfig::new().ca_certificate(server_root_ca_cert),
109 };
110 Channel::from_shared(endpoint)?
111 .tls_config(tls)?
112 .connect()
113 .await?
114 } else {
115 Channel::from_shared(endpoint)?.connect().await?
117 };
118
119 let interceptor = VersionInterceptor::new(
120 VERSION_HEADER,
121 cdk_common::PAYMENT_PROCESSOR_PROTOCOL_VERSION,
122 );
123 let client = CdkPaymentProcessorClient::with_interceptor(channel, interceptor);
124
125 Ok(Self {
126 inner: client,
127 payment_event_stream_is_active: Arc::new(AtomicBool::new(false)),
128 cancel_payment_event_stream: Arc::new(Mutex::new(CancellationToken::new())),
129 })
130 }
131}
132
133#[async_trait]
134impl MintPayment for PaymentProcessorClient {
135 type Err = cdk_common::payment::Error;
136
137 async fn get_settings(&self) -> Result<cdk_common::payment::SettingsResponse, Self::Err> {
138 let mut inner = self.inner.clone();
139 let response = inner
140 .get_settings(Request::new(EmptyRequest {}))
141 .await
142 .map_err(|err| {
143 tracing::error!("Could not get settings: {}", err);
144 cdk_common::payment::Error::Custom(err.to_string())
145 })?;
146
147 let settings = response.into_inner();
148
149 Ok(cdk_common::payment::SettingsResponse {
150 unit: settings.unit,
151 bolt11: settings
152 .bolt11
153 .map(|b| cdk_common::payment::Bolt11Settings {
154 mpp: b.mpp,
155 amountless: b.amountless,
156 invoice_description: b.invoice_description,
157 }),
158 bolt12: settings
159 .bolt12
160 .map(|b| cdk_common::payment::Bolt12Settings {
161 amountless: b.amountless,
162 }),
163 onchain: settings
164 .onchain
165 .map(|o| cdk_common::payment::OnchainSettings {
166 confirmations: o.confirmations,
167 min_receive_amount_sat: o.min_receive_amount_sat,
168 min_send_amount_sat: o.min_send_amount_sat,
169 }),
170 custom: settings.custom,
171 })
172 }
173
174 async fn create_incoming_payment_request(
176 &self,
177 options: CdkIncomingPaymentOptions,
178 ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
179 let mut inner = self.inner.clone();
180
181 let proto_options = match options {
182 CdkIncomingPaymentOptions::Custom(opts) => IncomingPaymentOptions {
183 options: Some(super::incoming_payment_options::Options::Custom(
184 super::CustomIncomingPaymentOptions {
185 description: opts.description,
186 amount: opts.amount.map(Into::into),
187 unix_expiry: opts.unix_expiry,
188 extra_json: opts.extra_json,
189 quote_id: opts.quote_id.to_string(),
190 pubkey: opts.pubkey.map(|p| p.to_hex()),
191 },
192 )),
193 },
194 CdkIncomingPaymentOptions::Bolt11(opts) => IncomingPaymentOptions {
195 options: Some(super::incoming_payment_options::Options::Bolt11(
196 super::Bolt11IncomingPaymentOptions {
197 description: opts.description,
198 amount: Some(opts.amount.into()),
199 unix_expiry: opts.unix_expiry,
200 },
201 )),
202 },
203 CdkIncomingPaymentOptions::Bolt12(opts) => IncomingPaymentOptions {
204 options: Some(super::incoming_payment_options::Options::Bolt12(
205 super::Bolt12IncomingPaymentOptions {
206 description: opts.description,
207 amount: opts.amount.map(Into::into),
208 unix_expiry: opts.unix_expiry,
209 },
210 )),
211 },
212 CdkIncomingPaymentOptions::Onchain(opts) => IncomingPaymentOptions {
213 options: Some(super::incoming_payment_options::Options::Onchain(
214 super::OnchainIncomingPaymentOptions {
215 quote_id: opts.quote_id.to_string(),
216 },
217 )),
218 },
219 };
220
221 let response = inner
222 .create_payment(Request::new(CreatePaymentRequest {
223 options: Some(proto_options),
224 }))
225 .await
226 .map_err(|err| {
227 tracing::error!("Could not create payment request: {}", err);
228 cdk_common::payment::Error::Custom(err.to_string())
229 })?;
230
231 let response = response.into_inner();
232
233 Ok(response.try_into().map_err(|_| {
234 cdk_common::payment::Error::Anyhow(anyhow!("Could not create create payment response"))
235 })?)
236 }
237
238 async fn get_payment_quote(
239 &self,
240 unit: &cdk_common::CurrencyUnit,
241 options: cdk_common::payment::OutgoingPaymentOptions,
242 ) -> Result<CdkPaymentQuoteResponse, Self::Err> {
243 let mut inner = self.inner.clone();
244
245 let request_type = match &options {
246 cdk_common::payment::OutgoingPaymentOptions::Custom(_) => {
247 OutgoingPaymentRequestType::Custom
248 }
249 cdk_common::payment::OutgoingPaymentOptions::Bolt11(_) => {
250 OutgoingPaymentRequestType::Bolt11Invoice
251 }
252 cdk_common::payment::OutgoingPaymentOptions::Bolt12(_) => {
253 OutgoingPaymentRequestType::Bolt12Offer
254 }
255 cdk_common::payment::OutgoingPaymentOptions::Onchain(_) => {
256 OutgoingPaymentRequestType::Onchain
257 }
258 };
259
260 let proto_request = match &options {
261 cdk_common::payment::OutgoingPaymentOptions::Custom(opts) => opts.request.to_string(),
262 cdk_common::payment::OutgoingPaymentOptions::Bolt11(opts) => opts.bolt11.to_string(),
263 cdk_common::payment::OutgoingPaymentOptions::Bolt12(opts) => opts.offer.to_string(),
264 cdk_common::payment::OutgoingPaymentOptions::Onchain(opts) => opts.address.clone(),
265 };
266
267 let proto_options = match &options {
268 cdk_common::payment::OutgoingPaymentOptions::Custom(opts) => opts.melt_options,
269 cdk_common::payment::OutgoingPaymentOptions::Bolt11(opts) => opts.melt_options,
270 cdk_common::payment::OutgoingPaymentOptions::Bolt12(opts) => opts.melt_options,
271 cdk_common::payment::OutgoingPaymentOptions::Onchain(_) => None,
272 };
273
274 let onchain_options = match &options {
275 cdk_common::payment::OutgoingPaymentOptions::Onchain(opts) => {
276 Some(super::OnchainOutgoingPaymentOptions {
277 address: opts.address.clone(),
278 amount: Some(opts.amount.clone().into()),
279 max_fee_amount: opts.max_fee_amount.clone().into_proto(),
280 quote_id: opts.quote_id.to_string(),
281 fee_index: opts.fee_index,
282 metadata: opts.metadata.clone(),
283 })
284 }
285 _ => None,
286 };
287
288 let extra_json = match &options {
289 cdk_common::payment::OutgoingPaymentOptions::Custom(opts) => opts.extra_json.clone(),
290 _ => None,
291 };
292
293 let amount = match &options {
294 cdk_common::payment::OutgoingPaymentOptions::Custom(opts) => {
295 opts.amount.clone().into_proto()
296 }
297 _ => None,
298 };
299
300 let quote_id = match &options {
301 cdk_common::payment::OutgoingPaymentOptions::Custom(opts) => opts.quote_id.to_string(),
302 cdk_common::payment::OutgoingPaymentOptions::Bolt11(opts) => opts.quote_id.to_string(),
303 cdk_common::payment::OutgoingPaymentOptions::Bolt12(opts) => opts.quote_id.to_string(),
304 cdk_common::payment::OutgoingPaymentOptions::Onchain(opts) => opts.quote_id.to_string(),
305 };
306
307 let response = inner
308 .get_payment_quote(Request::new(PaymentQuoteRequest {
309 request: proto_request,
310 unit: unit.to_string(),
311 options: proto_options.map(Into::into),
312 request_type: request_type.into(),
313 extra_json,
314 quote_id,
315 onchain_options,
316 amount,
317 }))
318 .await
319 .map_err(|err| {
320 tracing::error!("Could not get payment quote: {}", err);
321 cdk_common::payment::Error::Custom(err.to_string())
322 })?;
323
324 let response = response.into_inner();
325
326 Ok(response.try_into().map_err(|_| {
327 cdk_common::payment::Error::Custom(
328 "Failed to convert payment quote response".to_string(),
329 )
330 })?)
331 }
332
333 async fn make_payment(
334 &self,
335 unit: &cdk_common::CurrencyUnit,
336 options: cdk_common::payment::OutgoingPaymentOptions,
337 ) -> Result<CdkMakePaymentResponse, Self::Err> {
338 let mut inner = self.inner.clone();
339 let payment_options = match options {
340 cdk_common::payment::OutgoingPaymentOptions::Custom(opts) => {
341 super::OutgoingPaymentVariant {
342 options: Some(super::outgoing_payment_variant::Options::Custom(
343 super::CustomOutgoingPaymentOptions {
344 offer: opts.request.to_string(),
345 amount: opts.amount.map(Into::into),
346 max_fee_amount: opts.max_fee_amount.into_proto(),
347 timeout_secs: opts.timeout_secs,
348 melt_options: opts.melt_options.map(Into::into),
349 extra_json: opts.extra_json.clone(),
350 quote_id: opts.quote_id.to_string(),
351 },
352 )),
353 }
354 }
355 cdk_common::payment::OutgoingPaymentOptions::Bolt11(opts) => {
356 super::OutgoingPaymentVariant {
357 options: Some(super::outgoing_payment_variant::Options::Bolt11(
358 super::Bolt11OutgoingPaymentOptions {
359 bolt11: opts.bolt11.to_string(),
360 max_fee_amount: opts.max_fee_amount.into_proto(),
361 timeout_secs: opts.timeout_secs,
362 melt_options: opts.melt_options.map(Into::into),
363 quote_id: opts.quote_id.to_string(),
364 },
365 )),
366 }
367 }
368 cdk_common::payment::OutgoingPaymentOptions::Bolt12(opts) => {
369 super::OutgoingPaymentVariant {
370 options: Some(super::outgoing_payment_variant::Options::Bolt12(
371 super::Bolt12OutgoingPaymentOptions {
372 offer: opts.offer.to_string(),
373 max_fee_amount: opts.max_fee_amount.into_proto(),
374 timeout_secs: opts.timeout_secs,
375 melt_options: opts.melt_options.map(Into::into),
376 quote_id: opts.quote_id.to_string(),
377 },
378 )),
379 }
380 }
381 cdk_common::payment::OutgoingPaymentOptions::Onchain(opts) => {
382 super::OutgoingPaymentVariant {
383 options: Some(super::outgoing_payment_variant::Options::Onchain(
384 super::OnchainOutgoingPaymentOptions {
385 address: opts.address.clone(),
386 amount: Some(opts.amount.into()),
387 max_fee_amount: opts.max_fee_amount.into_proto(),
388 quote_id: opts.quote_id.to_string(),
389 fee_index: opts.fee_index,
390 metadata: opts.metadata.clone(),
391 },
392 )),
393 }
394 }
395 };
396
397 let response = inner
398 .make_payment(Request::new(MakePaymentRequest {
399 payment_options: Some(payment_options),
400 partial_amount: None,
401 max_fee_amount: None,
402 unit: unit.to_string(),
403 }))
404 .await
405 .map_err(|err| {
406 tracing::error!("Could not pay payment request: {}", err);
407
408 if err.message().contains("already paid") {
409 cdk_common::payment::Error::InvoiceAlreadyPaid
410 } else if err.message().contains("pending") {
411 cdk_common::payment::Error::InvoicePaymentPending
412 } else {
413 cdk_common::payment::Error::Custom(err.to_string())
414 }
415 })?;
416
417 let response = response.into_inner();
418
419 Ok(response.try_into().map_err(|_err| {
420 cdk_common::payment::Error::Anyhow(anyhow!("could not make payment"))
421 })?)
422 }
423
424 #[instrument(skip_all)]
425 async fn wait_payment_event(
426 &self,
427 ) -> Result<Pin<Box<dyn Stream<Item = cdk_common::payment::Event> + Send>>, Self::Err> {
428 tracing::debug!("Client waiting for payment");
429 let mut inner = self.inner.clone();
430 let stream = inner
431 .wait_payment_event(Request::new(EmptyRequest {}))
432 .await
433 .map_err(|err| {
434 self.payment_event_stream_is_active
435 .store(false, Ordering::SeqCst);
436 tracing::error!("Could not open payment event stream: {}", err);
437 cdk_common::payment::Error::Custom(err.to_string())
438 })?
439 .into_inner();
440
441 self.payment_event_stream_is_active
442 .store(true, Ordering::SeqCst);
443
444 let cancel_token = self.cancel_payment_event_stream.lock().await.clone();
445 let cancel_fut = cancel_token.cancelled_owned();
446 let active_flag = self.payment_event_stream_is_active.clone();
447
448 let transformed_stream = stream.take_until(cancel_fut).filter_map(|item| async {
449 match item {
450 Ok(value) => match value.try_into() {
451 Ok(payment_event) => Some(payment_event),
452 Err(e) => {
453 tracing::error!("Error converting payment event: {}", e);
454 None
455 }
456 },
457 Err(e) => {
458 tracing::error!("Error in payment event stream: {}", e);
459 None
460 }
461 }
462 });
463
464 Ok(Box::pin(ActivePaymentEventStream::new(
465 Box::pin(transformed_stream),
466 active_flag,
467 )))
468 }
469
470 fn is_payment_event_stream_active(&self) -> bool {
472 self.payment_event_stream_is_active.load(Ordering::SeqCst)
473 }
474
475 fn cancel_payment_event_stream(&self) {
477 let cancel_payment_event_stream = Arc::clone(&self.cancel_payment_event_stream);
478
479 tokio::spawn(async move {
480 let mut cancel_token = cancel_payment_event_stream.lock().await;
481 cancel_token.cancel();
482 *cancel_token = CancellationToken::new();
483 });
484 }
485
486 async fn check_incoming_payment_status(
487 &self,
488 payment_identifier: &cdk_common::payment::PaymentIdentifier,
489 ) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
490 let mut inner = self.inner.clone();
491 let response = inner
492 .check_incoming_payment(Request::new(CheckIncomingPaymentRequest {
493 request_identifier: Some(payment_identifier.clone().into()),
494 }))
495 .await
496 .map_err(|err| {
497 tracing::error!("Could not check incoming payment: {}", err);
498 cdk_common::payment::Error::Custom(err.to_string())
499 })?;
500
501 let check_incoming = response.into_inner();
502 check_incoming
503 .payments
504 .into_iter()
505 .map(|resp| resp.try_into().map_err(Self::Err::from))
506 .collect()
507 }
508
509 async fn check_outgoing_payment(
510 &self,
511 payment_identifier: &cdk_common::payment::PaymentIdentifier,
512 ) -> Result<CdkMakePaymentResponse, Self::Err> {
513 let mut inner = self.inner.clone();
514 let response = inner
515 .check_outgoing_payment(Request::new(CheckOutgoingPaymentRequest {
516 request_identifier: Some(payment_identifier.clone().into()),
517 }))
518 .await
519 .map_err(|err| {
520 tracing::error!("Could not check outgoing payment: {}", err);
521 cdk_common::payment::Error::Custom(err.to_string())
522 })?;
523
524 let check_outgoing = response.into_inner();
525
526 Ok(check_outgoing
527 .try_into()
528 .map_err(|_| cdk_common::payment::Error::UnknownPaymentState)?)
529 }
530}