Skip to main content

cdk_payment_processor/proto/
client.rs

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/// Payment Processor
30#[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    /// Payment Processor
76    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            // TLS directory exists, configure TLS
82
83            // Check for ca.pem
84            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            // Check for client.pem
92            let client_pem_path = tls_dir.join("client.pem");
93
94            // Check for client.key
95            let client_key_path = tls_dir.join("client.key");
96            // check for ca cert
97            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            // No TLS directory, skip TLS configuration
116            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    /// Create a new invoice
175    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    /// Is payment event stream active
471    fn is_payment_event_stream_active(&self) -> bool {
472        self.payment_event_stream_is_active.load(Ordering::SeqCst)
473    }
474
475    /// Cancel payment event stream
476    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}