Skip to main content

cdk_mint_rpc/proto/
server.rs

1use std::net::SocketAddr;
2use std::path::PathBuf;
3use std::str::FromStr;
4use std::sync::Arc;
5
6use cdk::mint::{Mint, MintKeySetInfo, MintQuote};
7use cdk::nuts::nut04::MintMethodSettings;
8use cdk::nuts::nut05::MeltMethodSettings;
9use cdk::nuts::{CurrencyUnit, MintQuoteState, PaymentMethod};
10use cdk::types::QuoteTTL;
11use cdk::Amount;
12use cdk_common::grpc::create_version_check_interceptor;
13use cdk_common::nut00::KnownMethod;
14use cdk_common::payment::WaitPaymentResponse;
15use thiserror::Error;
16use tokio::sync::Notify;
17use tokio::task::JoinHandle;
18use tokio::time::Duration;
19use tonic::transport::{Certificate, Identity, Server, ServerTlsConfig};
20use tonic::{Request, Response, Status};
21
22use crate::cdk_mint_server::{CdkMint, CdkMintServer};
23use crate::keyset::keyset_service_server::{KeysetService, KeysetServiceServer};
24use crate::payment_method::payment_method_service_server::{
25    PaymentMethodService, PaymentMethodServiceServer,
26};
27use crate::quote::quote_service_server::{QuoteService, QuoteServiceServer};
28use crate::wallet::wallet_service_server::{WalletService, WalletServiceServer};
29use crate::{
30    ContactInfo, DynWalletInfoProvider, GetInfoRequest, GetInfoResponse, GetQuoteTtlRequest,
31    GetQuoteTtlResponse, RotateNextKeysetRequest, RotateNextKeysetResponse, UpdateContactRequest,
32    UpdateDescriptionRequest, UpdateIconUrlRequest, UpdateMotdRequest, UpdateNameRequest,
33    UpdateNut04QuoteRequest, UpdateNut04Request, UpdateNut05Request, UpdateQuoteTtlRequest,
34    UpdateResponse, UpdateTosUrlRequest, UpdateUrlRequest, WalletAddressPage,
35    WalletTransactionPage,
36};
37
38const DEFAULT_TRANSACTION_LIMIT: u32 = 20;
39const MAX_TRANSACTION_LIMIT: u32 = 100;
40const DEFAULT_ADDRESS_LIMIT: u32 = 100;
41const MAX_ADDRESS_LIMIT: u32 = 1_000;
42
43/// Error
44#[derive(Debug, Error)]
45pub enum Error {
46    /// Parse error
47    #[error(transparent)]
48    Parse(#[from] std::net::AddrParseError),
49    /// Transport error
50    #[error(transparent)]
51    Transport(#[from] tonic::transport::Error),
52    /// Io error
53    #[error(transparent)]
54    Io(#[from] std::io::Error),
55}
56
57/// Failure returned when a management mutation is not currently allowed.
58#[derive(Debug, Error)]
59pub enum MintMutationGuardError {
60    /// The mutation conflicts with the mint's current lifecycle state.
61    #[error("{0}")]
62    FailedPrecondition(String),
63    /// The lifecycle state could not be checked.
64    #[error("{0}")]
65    Internal(String),
66}
67
68/// Checks whether management RPC mutations are currently allowed.
69#[tonic::async_trait]
70pub trait MintMutationGuard: Send + Sync {
71    /// Returns successfully when a mutation may proceed.
72    async fn check(&self) -> Result<(), MintMutationGuardError>;
73}
74
75/// CDK Mint RPC Server
76#[derive(Clone)]
77#[allow(missing_debug_implementations)]
78pub struct MintRPCServer {
79    socket_addr: SocketAddr,
80    mint: Arc<Mint>,
81    mutation_guard: Option<Arc<dyn MintMutationGuard>>,
82    allow_mint_quote_payment_override: bool,
83    wallet_info_provider: Option<DynWalletInfoProvider>,
84    shutdown: Arc<Notify>,
85    handle: Option<Arc<JoinHandle<Result<(), Error>>>>,
86}
87
88impl MintRPCServer {
89    /// Creates a new MintRPCServer instance
90    ///
91    /// # Arguments
92    /// * `addr` - The address to bind to
93    /// * `port` - The port to listen on
94    /// * `mint` - The Mint instance to serve
95    pub fn new(addr: &str, port: u16, mint: Arc<Mint>) -> Result<Self, Error> {
96        Ok(Self {
97            socket_addr: format!("{addr}:{port}").parse()?,
98            mint,
99            mutation_guard: None,
100            allow_mint_quote_payment_override: false,
101            wallet_info_provider: None,
102            shutdown: Arc::new(Notify::new()),
103            handle: None,
104        })
105    }
106
107    /// Adds a guard that runs before every mutating management RPC.
108    pub fn with_mutation_guard(mut self, guard: Arc<dyn MintMutationGuard>) -> Self {
109        self.mutation_guard = Some(guard);
110        self
111    }
112
113    /// Enables or disables management RPC mint quote state overrides.
114    ///
115    /// This includes the legacy quote-state update endpoint and is disabled by
116    /// default because the paid state records a payment without confirmation
117    /// from the configured payment backend.
118    pub fn with_mint_quote_payment_override(mut self, enabled: bool) -> Self {
119        self.allow_mint_quote_payment_override = enabled;
120        self
121    }
122
123    async fn ensure_mutation_allowed(&self) -> Result<(), Status> {
124        let Some(guard) = &self.mutation_guard else {
125            return Ok(());
126        };
127
128        guard.check().await.map_err(|error| match error {
129            MintMutationGuardError::FailedPrecondition(message) => {
130                Status::failed_precondition(message)
131            }
132            MintMutationGuardError::Internal(message) => Status::internal(message),
133        })
134    }
135
136    fn ensure_mint_quote_state_override_allowed(&self) -> Result<(), Status> {
137        if !self.allow_mint_quote_payment_override {
138            return Err(Status::permission_denied(
139                "Mint quote state override is disabled",
140            ));
141        }
142
143        Ok(())
144    }
145
146    /// Configures the on-chain wallet management provider.
147    pub fn with_wallet_info_provider(mut self, provider: DynWalletInfoProvider) -> Self {
148        self.wallet_info_provider = Some(provider);
149        self
150    }
151
152    /// Starts the RPC server
153    ///
154    /// # Arguments
155    /// * `tls_dir` - Optional directory containing TLS certificates
156    ///
157    /// If TLS directory is provided, it must contain:
158    /// - server.pem: Server certificate
159    /// - server.key: Server private key
160    /// - ca.pem: CA certificate for client authentication
161    pub async fn start(&mut self, tls_dir: Option<PathBuf>) -> Result<(), Error> {
162        tracing::info!("Starting RPC server {}", self.socket_addr);
163
164        #[cfg(not(target_arch = "wasm32"))]
165        if rustls::crypto::CryptoProvider::get_default().is_none() {
166            let _ = rustls::crypto::ring::default_provider().install_default();
167        }
168
169        let server = match tls_dir {
170            Some(tls_dir) => {
171                tracing::info!("TLS configuration found, starting secure server");
172                let server_pem_path = tls_dir.join("server.pem");
173                let server_key_path = tls_dir.join("server.key");
174                let ca_pem_path = tls_dir.join("ca.pem");
175
176                if !server_pem_path.exists() {
177                    tracing::error!(
178                        "Server certificate file does not exist: {}",
179                        server_pem_path.display()
180                    );
181                    return Err(Error::Io(std::io::Error::new(
182                        std::io::ErrorKind::NotFound,
183                        format!(
184                            "Server certificate file not found: {}",
185                            server_pem_path.display()
186                        ),
187                    )));
188                }
189
190                if !server_key_path.exists() {
191                    tracing::error!(
192                        "Server key file does not exist: {}",
193                        server_key_path.display()
194                    );
195                    return Err(Error::Io(std::io::Error::new(
196                        std::io::ErrorKind::NotFound,
197                        format!("Server key file not found: {}", server_key_path.display()),
198                    )));
199                }
200
201                if !ca_pem_path.exists() {
202                    tracing::error!(
203                        "CA certificate file does not exist: {}",
204                        ca_pem_path.display()
205                    );
206                    return Err(Error::Io(std::io::Error::new(
207                        std::io::ErrorKind::NotFound,
208                        format!("CA certificate file not found: {}", ca_pem_path.display()),
209                    )));
210                }
211
212                let cert = std::fs::read_to_string(&server_pem_path)?;
213                let key = std::fs::read_to_string(&server_key_path)?;
214                let client_ca_cert = std::fs::read_to_string(&ca_pem_path)?;
215                let client_ca_cert = Certificate::from_pem(client_ca_cert);
216                let server_identity = Identity::from_pem(cert, key);
217                let tls_config = ServerTlsConfig::new()
218                    .identity(server_identity)
219                    .client_ca_root(client_ca_cert);
220
221                Server::builder()
222                    .tls_config(tls_config)?
223                    .add_service(CdkMintServer::with_interceptor(
224                        self.clone(),
225                        create_version_check_interceptor(
226                            cdk_common::grpc::VERSION_HEADER,
227                            cdk_common::MINT_RPC_PROTOCOL_VERSION,
228                        ),
229                    ))
230                    .add_service(KeysetServiceServer::with_interceptor(
231                        self.clone(),
232                        create_version_check_interceptor(
233                            cdk_common::grpc::VERSION_HEADER,
234                            cdk_common::MINT_RPC_PROTOCOL_VERSION,
235                        ),
236                    ))
237                    .add_service(PaymentMethodServiceServer::with_interceptor(
238                        self.clone(),
239                        create_version_check_interceptor(
240                            cdk_common::grpc::VERSION_HEADER,
241                            cdk_common::MINT_RPC_PROTOCOL_VERSION,
242                        ),
243                    ))
244                    .add_service(QuoteServiceServer::with_interceptor(
245                        self.clone(),
246                        create_version_check_interceptor(
247                            cdk_common::grpc::VERSION_HEADER,
248                            cdk_common::MINT_RPC_PROTOCOL_VERSION,
249                        ),
250                    ))
251                    .add_service(WalletServiceServer::with_interceptor(
252                        self.clone(),
253                        create_version_check_interceptor(
254                            cdk_common::grpc::VERSION_HEADER,
255                            cdk_common::MINT_RPC_PROTOCOL_VERSION,
256                        ),
257                    ))
258            }
259            None => {
260                tracing::warn!("No valid TLS configuration found, starting insecure server");
261                Server::builder()
262                    .add_service(CdkMintServer::with_interceptor(
263                        self.clone(),
264                        create_version_check_interceptor(
265                            cdk_common::grpc::VERSION_HEADER,
266                            cdk_common::MINT_RPC_PROTOCOL_VERSION,
267                        ),
268                    ))
269                    .add_service(KeysetServiceServer::with_interceptor(
270                        self.clone(),
271                        create_version_check_interceptor(
272                            cdk_common::grpc::VERSION_HEADER,
273                            cdk_common::MINT_RPC_PROTOCOL_VERSION,
274                        ),
275                    ))
276                    .add_service(PaymentMethodServiceServer::with_interceptor(
277                        self.clone(),
278                        create_version_check_interceptor(
279                            cdk_common::grpc::VERSION_HEADER,
280                            cdk_common::MINT_RPC_PROTOCOL_VERSION,
281                        ),
282                    ))
283                    .add_service(QuoteServiceServer::with_interceptor(
284                        self.clone(),
285                        create_version_check_interceptor(
286                            cdk_common::grpc::VERSION_HEADER,
287                            cdk_common::MINT_RPC_PROTOCOL_VERSION,
288                        ),
289                    ))
290                    .add_service(WalletServiceServer::with_interceptor(
291                        self.clone(),
292                        create_version_check_interceptor(
293                            cdk_common::grpc::VERSION_HEADER,
294                            cdk_common::MINT_RPC_PROTOCOL_VERSION,
295                        ),
296                    ))
297            }
298        };
299
300        let shutdown = self.shutdown.clone();
301        let addr = self.socket_addr;
302
303        self.handle = Some(Arc::new(tokio::spawn(async move {
304            let server = server.serve_with_shutdown(addr, async {
305                shutdown.notified().await;
306            });
307
308            server.await?;
309            Ok(())
310        })));
311
312        Ok(())
313    }
314
315    /// Stops the RPC server gracefully
316    pub async fn stop(&self) -> Result<(), Error> {
317        self.shutdown.notify_one();
318        if let Some(handle) = &self.handle {
319            while !handle.is_finished() {
320                tracing::info!("Waitning for mint rpc server to stop");
321                tokio::time::sleep(Duration::from_millis(100)).await;
322            }
323        }
324
325        tracing::info!("Mint rpc server stopped");
326        Ok(())
327    }
328
329    /// Rotates to the next keyset for the given unit
330    ///
331    /// Shared by the legacy [`CdkMint`] service and [`KeysetService`] while
332    /// both are served.
333    async fn rotate_keyset(
334        &self,
335        unit: CurrencyUnit,
336        amounts: Vec<u64>,
337        input_fee_ppk: Option<u64>,
338        use_keyset_v2: Option<bool>,
339        final_expiry: Option<u64>,
340    ) -> Result<MintKeySetInfo, Status> {
341        self.ensure_mutation_allowed().await?;
342        self.mint
343            .rotate_keyset(
344                unit,
345                amounts,
346                input_fee_ppk.unwrap_or(0),
347                use_keyset_v2.unwrap_or(true),
348                final_expiry,
349            )
350            .await
351            .map_err(|_| Status::invalid_argument("Could not rotate keyset".to_string()))
352    }
353
354    /// Returns the mint's quote time-to-live settings
355    ///
356    /// Shared by the legacy [`CdkMint`] service and [`QuoteService`] while both
357    /// are served.
358    async fn quote_ttl(&self) -> Result<QuoteTTL, Status> {
359        self.mint
360            .quote_ttl()
361            .await
362            .map_err(|err| Status::internal(err.to_string()))
363    }
364
365    /// Updates the mint's quote time-to-live settings, keeping the current
366    /// value of any setting that is not given
367    ///
368    /// Returns the settings in effect after the update. Shared by the legacy
369    /// [`CdkMint`] service and [`QuoteService`] while both are served.
370    async fn set_quote_ttl(
371        &self,
372        mint_ttl: Option<u64>,
373        melt_ttl: Option<u64>,
374    ) -> Result<QuoteTTL, Status> {
375        let current_ttl = self.quote_ttl().await?;
376
377        let quote_ttl = QuoteTTL {
378            mint_ttl: mint_ttl.unwrap_or(current_ttl.mint_ttl),
379            melt_ttl: melt_ttl.unwrap_or(current_ttl.melt_ttl),
380        };
381
382        self.mint
383            .set_quote_ttl(quote_ttl)
384            .await
385            .map_err(|err| Status::internal(err.to_string()))?;
386
387        Ok(quote_ttl)
388    }
389
390    /// Records a payment against a mint quote as though the payment backend
391    /// had reported it, marking the quote paid
392    ///
393    /// Returns the quote as it stands after the update. Shared by the legacy
394    /// [`CdkMint`] service and [`QuoteService`] while both are served.
395    async fn set_mint_quote_paid(&self, quote_id: &str) -> Result<MintQuote, Status> {
396        self.ensure_mint_quote_state_override_allowed()?;
397
398        let quote_id = quote_id
399            .parse()
400            .map_err(|_| Status::invalid_argument("Invalid quote id".to_string()))?;
401
402        let mint_quote = self
403            .mint
404            .localstore()
405            .get_mint_quote(&quote_id)
406            .await
407            .map_err(|_| Status::invalid_argument("Could not find quote".to_string()))?
408            .ok_or(Status::invalid_argument("Could not find quote".to_string()))?;
409
410        // Create a dummy payment response
411        let response = WaitPaymentResponse {
412            payment_id: mint_quote.request_lookup_id.to_string(),
413            payment_amount: mint_quote.clone().amount.unwrap_or(cdk::Amount::new(
414                mint_quote.amount_paid().value(),
415                mint_quote.unit.clone(),
416            )),
417            payment_identifier: mint_quote.request_lookup_id.clone(),
418        };
419
420        let localstore = self.mint.localstore();
421        let mut tx = localstore
422            .begin_transaction()
423            .await
424            .map_err(|_| Status::internal("Could not start db transaction".to_string()))?;
425
426        // Re-fetch the mint quote within the transaction to lock it
427        let mut mint_quote = tx
428            .get_mint_quote(&quote_id)
429            .await
430            .map_err(|_| Status::internal("Could not get quote in transaction".to_string()))?
431            .ok_or(Status::invalid_argument(
432                "Quote not found in transaction".to_string(),
433            ))?;
434
435        let should_notify = self
436            .mint
437            .pay_mint_quote(&mut tx, &mut mint_quote, response)
438            .await
439            .map_err(|_| Status::internal("Could not process payment".to_string()))?;
440
441        tx.commit()
442            .await
443            .map_err(|_| Status::internal("Could not commit db transaction".to_string()))?;
444
445        // Publish notification AFTER transaction commits
446        if should_notify {
447            self.mint
448                .pubsub_manager()
449                .mint_quote_payment(&mint_quote, mint_quote.amount_paid());
450        }
451
452        self.mint
453            .localstore()
454            .get_mint_quote(&quote_id)
455            .await
456            .map_err(|_| Status::invalid_argument("Could not find quote".to_string()))?
457            .ok_or(Status::invalid_argument("Could not find quote".to_string()))
458    }
459
460    fn wallet_info_provider(&self) -> Result<&DynWalletInfoProvider, Status> {
461        self.wallet_info_provider.as_ref().ok_or_else(|| {
462            Status::failed_precondition("No on-chain wallet information provider is configured")
463        })
464    }
465
466    /// Updates the settings of one mint (NUT-04) payment method, keeping the
467    /// current value of any setting that is not given
468    ///
469    /// Returns the method settings in effect after the update. Shared by the
470    /// legacy [`CdkMint`] service and [`PaymentMethodService`] while both are
471    /// served; `disabled` toggles minting for the whole mint and is only
472    /// passed by the legacy service.
473    #[allow(clippy::too_many_arguments)]
474    async fn set_mint_method(
475        &self,
476        unit: &str,
477        method: &str,
478        min_amount: Option<u64>,
479        max_amount: Option<u64>,
480        options: Option<cdk::nuts::nut04::MintMethodOptions>,
481        method_name: Option<String>,
482        disabled: Option<bool>,
483    ) -> Result<MintMethodSettings, Status> {
484        self.ensure_mutation_allowed().await?;
485        let mut info = self
486            .mint
487            .mint_info()
488            .await
489            .map_err(|err| Status::internal(err.to_string()))?;
490
491        let unit = CurrencyUnit::from_str(unit)
492            .map_err(|_| Status::invalid_argument("Invalid unit".to_string()))?;
493
494        let payment_method = PaymentMethod::from_str(method)
495            .map_err(|_| Status::invalid_argument("Invalid method".to_string()))?;
496
497        self.mint
498            .get_payment_processor(unit.clone(), payment_method.clone())
499            .map_err(|_| Status::invalid_argument("Unit payment method pair is not supported"))?;
500
501        let current_nut04_settings = info.nuts.nut04.remove_settings(&unit, &payment_method);
502
503        let updated_method_settings = MintMethodSettings {
504            method: payment_method,
505            unit,
506            method_name: method_name.or_else(|| {
507                current_nut04_settings
508                    .as_ref()
509                    .and_then(|s| s.method_name.clone())
510            }),
511            min_amount: min_amount
512                .map(Amount::from)
513                .or_else(|| current_nut04_settings.as_ref().and_then(|s| s.min_amount)),
514            max_amount: max_amount
515                .map(Amount::from)
516                .or_else(|| current_nut04_settings.as_ref().and_then(|s| s.max_amount)),
517            options: options.or_else(|| {
518                current_nut04_settings
519                    .as_ref()
520                    .and_then(|s| s.options.clone())
521            }),
522        };
523
524        info.nuts
525            .nut04
526            .methods
527            .push(updated_method_settings.clone());
528
529        if let Some(disabled) = disabled {
530            info.nuts.nut04.disabled = disabled;
531        }
532
533        self.mint
534            .set_mint_info(info)
535            .await
536            .map_err(|err| Status::internal(err.to_string()))?;
537
538        Ok(updated_method_settings)
539    }
540
541    /// Enables or disables minting and melting for the whole mint, keeping
542    /// the current value of any flag that is not given
543    ///
544    /// Returns the (minting disabled, melting disabled) flags in effect after
545    /// the update, applied in a single write.
546    async fn set_disabled(
547        &self,
548        mint_disabled: Option<bool>,
549        melt_disabled: Option<bool>,
550    ) -> Result<(bool, bool), Status> {
551        self.ensure_mutation_allowed().await?;
552        let mut info = self
553            .mint
554            .mint_info()
555            .await
556            .map_err(|err| Status::internal(err.to_string()))?;
557
558        if mint_disabled.is_none() && melt_disabled.is_none() {
559            return Ok((info.nuts.nut04.disabled, info.nuts.nut05.disabled));
560        }
561
562        if let Some(disabled) = mint_disabled {
563            info.nuts.nut04.disabled = disabled;
564        }
565
566        if let Some(disabled) = melt_disabled {
567            info.nuts.nut05.disabled = disabled;
568        }
569
570        let flags = (info.nuts.nut04.disabled, info.nuts.nut05.disabled);
571
572        self.mint
573            .set_mint_info(info)
574            .await
575            .map_err(|err| Status::internal(err.to_string()))?;
576
577        Ok(flags)
578    }
579
580    /// Updates the settings of one melt (NUT-05) payment method, keeping the
581    /// current value of any setting that is not given
582    ///
583    /// Returns the method settings in effect after the update. Shared by the
584    /// legacy [`CdkMint`] service and [`PaymentMethodService`] while both are
585    /// served; `disabled` toggles melting for the whole mint and is only
586    /// passed by the legacy service.
587    #[allow(clippy::too_many_arguments)]
588    async fn set_melt_method(
589        &self,
590        unit: &str,
591        method: &str,
592        min_amount: Option<u64>,
593        max_amount: Option<u64>,
594        options: Option<cdk::nuts::nut05::MeltMethodOptions>,
595        method_name: Option<String>,
596        disabled: Option<bool>,
597    ) -> Result<MeltMethodSettings, Status> {
598        self.ensure_mutation_allowed().await?;
599        let mut info = self
600            .mint
601            .mint_info()
602            .await
603            .map_err(|err| Status::internal(err.to_string()))?;
604
605        let unit = CurrencyUnit::from_str(unit)
606            .map_err(|_| Status::invalid_argument("Invalid unit".to_string()))?;
607
608        let payment_method = PaymentMethod::from_str(method)
609            .map_err(|_| Status::invalid_argument("Invalid method".to_string()))?;
610
611        self.mint
612            .get_payment_processor(unit.clone(), payment_method.clone())
613            .map_err(|_| Status::invalid_argument("Unit payment method pair is not supported"))?;
614
615        let current_nut05_settings = info.nuts.nut05.remove_settings(&unit, &payment_method);
616
617        let updated_method_settings = MeltMethodSettings {
618            method: payment_method,
619            unit,
620            method_name: method_name.or_else(|| {
621                current_nut05_settings
622                    .as_ref()
623                    .and_then(|s| s.method_name.clone())
624            }),
625            min_amount: min_amount
626                .map(Amount::from)
627                .or_else(|| current_nut05_settings.as_ref().and_then(|s| s.min_amount)),
628            max_amount: max_amount
629                .map(Amount::from)
630                .or_else(|| current_nut05_settings.as_ref().and_then(|s| s.max_amount)),
631            options: options.or_else(|| {
632                current_nut05_settings
633                    .as_ref()
634                    .and_then(|s| s.options.clone())
635            }),
636        };
637
638        info.nuts
639            .nut05
640            .methods
641            .push(updated_method_settings.clone());
642
643        if let Some(disabled) = disabled {
644            info.nuts.nut05.disabled = disabled;
645        }
646
647        self.mint
648            .set_mint_info(info)
649            .await
650            .map_err(|err| Status::internal(err.to_string()))?;
651
652        Ok(updated_method_settings)
653    }
654}
655
656impl Drop for MintRPCServer {
657    fn drop(&mut self) {
658        tracing::debug!("Dropping mint rpc server");
659        self.shutdown.notify_one();
660    }
661}
662
663#[tonic::async_trait]
664impl CdkMint for MintRPCServer {
665    /// Returns information about the mint
666    async fn get_info(
667        &self,
668        _request: Request<GetInfoRequest>,
669    ) -> Result<Response<GetInfoResponse>, Status> {
670        let info = self
671            .mint
672            .mint_info()
673            .await
674            .map_err(|err| Status::internal(err.to_string()))?;
675
676        let total_issued = self
677            .mint
678            .total_issued()
679            .await
680            .map_err(|err| Status::internal(err.to_string()))?;
681
682        let total_issued: Amount = Amount::try_sum(total_issued.values().cloned())
683            .map_err(|_| Status::internal("Overflow".to_string()))?;
684
685        let total_redeemed = self
686            .mint
687            .total_redeemed()
688            .await
689            .map_err(|err| Status::internal(err.to_string()))?;
690
691        let total_redeemed: Amount = Amount::try_sum(total_redeemed.values().cloned())
692            .map_err(|_| Status::internal("Overflow".to_string()))?;
693
694        let contact = info
695            .contact
696            .unwrap_or_default()
697            .into_iter()
698            .map(|c| ContactInfo {
699                method: c.method,
700                info: c.info,
701            })
702            .collect();
703
704        let response = Response::new(GetInfoResponse {
705            name: info.name,
706            description: info.description,
707            long_description: info.description_long,
708            version: info.version.map(|v| v.to_string()),
709            contact,
710            motd: info.motd,
711            icon_url: info.icon_url,
712            tos_url: info.tos_url,
713            urls: info.urls.unwrap_or_default(),
714            total_issued: total_issued.into(),
715            total_redeemed: total_redeemed.into(),
716        });
717
718        Ok(response)
719    }
720
721    /// Updates the mint's message of the day
722    async fn update_motd(
723        &self,
724        request: Request<UpdateMotdRequest>,
725    ) -> Result<Response<UpdateResponse>, Status> {
726        self.ensure_mutation_allowed().await?;
727        let motd = request.into_inner().motd;
728        let mut info = self
729            .mint
730            .mint_info()
731            .await
732            .map_err(|err| Status::internal(err.to_string()))?;
733        info.motd = Some(motd);
734
735        self.mint
736            .set_mint_info(info)
737            .await
738            .map_err(|err| Status::internal(err.to_string()))?;
739
740        Ok(Response::new(UpdateResponse {}))
741    }
742
743    /// Updates the mint's short description
744    async fn update_short_description(
745        &self,
746        request: Request<UpdateDescriptionRequest>,
747    ) -> Result<Response<UpdateResponse>, Status> {
748        self.ensure_mutation_allowed().await?;
749        let description = request.into_inner().description;
750        let mut info = self
751            .mint
752            .mint_info()
753            .await
754            .map_err(|err| Status::internal(err.to_string()))?;
755
756        info.description = Some(description);
757
758        self.mint
759            .set_mint_info(info)
760            .await
761            .map_err(|err| Status::internal(err.to_string()))?;
762        Ok(Response::new(UpdateResponse {}))
763    }
764
765    /// Updates the mint's long description
766    async fn update_long_description(
767        &self,
768        request: Request<UpdateDescriptionRequest>,
769    ) -> Result<Response<UpdateResponse>, Status> {
770        self.ensure_mutation_allowed().await?;
771        let description = request.into_inner().description;
772        let mut info = self
773            .mint
774            .mint_info()
775            .await
776            .map_err(|err| Status::internal(err.to_string()))?;
777
778        info.description_long = Some(description);
779
780        self.mint
781            .set_mint_info(info)
782            .await
783            .map_err(|err| Status::internal(err.to_string()))?;
784        Ok(Response::new(UpdateResponse {}))
785    }
786
787    /// Updates the mint's name
788    async fn update_name(
789        &self,
790        request: Request<UpdateNameRequest>,
791    ) -> Result<Response<UpdateResponse>, Status> {
792        self.ensure_mutation_allowed().await?;
793        let name = request.into_inner().name;
794        let mut info = self
795            .mint
796            .mint_info()
797            .await
798            .map_err(|err| Status::internal(err.to_string()))?;
799
800        info.name = Some(name);
801
802        self.mint
803            .set_mint_info(info)
804            .await
805            .map_err(|err| Status::internal(err.to_string()))?;
806        Ok(Response::new(UpdateResponse {}))
807    }
808
809    /// Updates the mint's icon URL
810    async fn update_icon_url(
811        &self,
812        request: Request<UpdateIconUrlRequest>,
813    ) -> Result<Response<UpdateResponse>, Status> {
814        self.ensure_mutation_allowed().await?;
815        let icon_url = request.into_inner().icon_url;
816
817        let mut info = self
818            .mint
819            .mint_info()
820            .await
821            .map_err(|err| Status::internal(err.to_string()))?;
822
823        info.icon_url = Some(icon_url);
824
825        self.mint
826            .set_mint_info(info)
827            .await
828            .map_err(|err| Status::internal(err.to_string()))?;
829        Ok(Response::new(UpdateResponse {}))
830    }
831
832    /// Updates the mint's terms of service URL
833    async fn update_tos_url(
834        &self,
835        request: Request<UpdateTosUrlRequest>,
836    ) -> Result<Response<UpdateResponse>, Status> {
837        self.ensure_mutation_allowed().await?;
838        let tos_url = request.into_inner().tos_url;
839
840        let mut info = self
841            .mint
842            .mint_info()
843            .await
844            .map_err(|err| Status::internal(err.to_string()))?;
845
846        info.tos_url = Some(tos_url);
847
848        self.mint
849            .set_mint_info(info)
850            .await
851            .map_err(|err| Status::internal(err.to_string()))?;
852        Ok(Response::new(UpdateResponse {}))
853    }
854
855    /// Adds a URL to the mint's list of URLs
856    async fn add_url(
857        &self,
858        request: Request<UpdateUrlRequest>,
859    ) -> Result<Response<UpdateResponse>, Status> {
860        self.ensure_mutation_allowed().await?;
861        let url = request.into_inner().url;
862        let mut info = self
863            .mint
864            .mint_info()
865            .await
866            .map_err(|err| Status::internal(err.to_string()))?;
867        let mut urls = info.urls.unwrap_or_default();
868        urls.push(url);
869
870        info.urls = Some(urls.clone());
871
872        self.mint
873            .set_mint_info(info)
874            .await
875            .map_err(|err| Status::internal(err.to_string()))?;
876        Ok(Response::new(UpdateResponse {}))
877    }
878
879    /// Removes a URL from the mint's list of URLs
880    async fn remove_url(
881        &self,
882        request: Request<UpdateUrlRequest>,
883    ) -> Result<Response<UpdateResponse>, Status> {
884        self.ensure_mutation_allowed().await?;
885        let url = request.into_inner().url;
886        let mut info = self
887            .mint
888            .mint_info()
889            .await
890            .map_err(|err| Status::internal(err.to_string()))?;
891        let urls = info.urls;
892        let mut urls = urls.clone().unwrap_or_default();
893
894        urls.retain(|u| u != &url);
895
896        let urls = if urls.is_empty() { None } else { Some(urls) };
897
898        info.urls = urls;
899
900        self.mint
901            .set_mint_info(info)
902            .await
903            .map_err(|err| Status::internal(err.to_string()))?;
904        Ok(Response::new(UpdateResponse {}))
905    }
906
907    /// Adds a contact method to the mint's contact information
908    async fn add_contact(
909        &self,
910        request: Request<UpdateContactRequest>,
911    ) -> Result<Response<UpdateResponse>, Status> {
912        self.ensure_mutation_allowed().await?;
913        let request_inner = request.into_inner();
914        let mut info = self
915            .mint
916            .mint_info()
917            .await
918            .map_err(|err| Status::internal(err.to_string()))?;
919
920        info.contact
921            .get_or_insert_with(Vec::new)
922            .push(cdk::nuts::ContactInfo::new(
923                request_inner.method,
924                request_inner.info,
925            ));
926
927        self.mint
928            .set_mint_info(info)
929            .await
930            .map_err(|err| Status::internal(err.to_string()))?;
931        Ok(Response::new(UpdateResponse {}))
932    }
933    /// Removes a contact method from the mint's contact information
934    async fn remove_contact(
935        &self,
936        request: Request<UpdateContactRequest>,
937    ) -> Result<Response<UpdateResponse>, Status> {
938        self.ensure_mutation_allowed().await?;
939        let request_inner = request.into_inner();
940        let mut info = self
941            .mint
942            .mint_info()
943            .await
944            .map_err(|err| Status::internal(err.to_string()))?;
945
946        if let Some(contact) = info.contact.as_mut() {
947            let contact_info =
948                cdk::nuts::ContactInfo::new(request_inner.method, request_inner.info);
949            contact.retain(|x| x != &contact_info);
950
951            self.mint
952                .set_mint_info(info)
953                .await
954                .map_err(|err| Status::internal(err.to_string()))?;
955        }
956        Ok(Response::new(UpdateResponse {}))
957    }
958
959    /// Updates the mint's NUT-04 (mint) settings
960    async fn update_nut04(
961        &self,
962        request: Request<UpdateNut04Request>,
963    ) -> Result<Response<UpdateResponse>, Status> {
964        let request = request.into_inner();
965
966        let options = request.options.map(|options| {
967            let description = options.description;
968            match PaymentMethod::from_str(&request.method) {
969                Ok(PaymentMethod::Known(KnownMethod::Bolt12)) => {
970                    cdk::nuts::nut04::MintMethodOptions::Bolt12 { description }
971                }
972                _ => cdk::nuts::nut04::MintMethodOptions::Bolt11 { description },
973            }
974        });
975
976        self.set_mint_method(
977            &request.unit,
978            &request.method,
979            request.min_amount,
980            request.max_amount,
981            options,
982            request.method_name,
983            request.disabled,
984        )
985        .await?;
986
987        Ok(Response::new(UpdateResponse {}))
988    }
989
990    /// Updates the mint's NUT-05 (melt) settings
991    async fn update_nut05(
992        &self,
993        request: Request<UpdateNut05Request>,
994    ) -> Result<Response<UpdateResponse>, Status> {
995        let request = request.into_inner();
996
997        let options = request
998            .options
999            .map(|options| cdk::nuts::nut05::MeltMethodOptions::Bolt11 {
1000                amountless: options.amountless,
1001            });
1002
1003        self.set_melt_method(
1004            &request.unit,
1005            &request.method,
1006            request.min_amount,
1007            request.max_amount,
1008            options,
1009            request.method_name,
1010            request.disabled,
1011        )
1012        .await?;
1013
1014        Ok(Response::new(UpdateResponse {}))
1015    }
1016
1017    /// Updates the mint's quote time-to-live settings
1018    async fn update_quote_ttl(
1019        &self,
1020        request: Request<UpdateQuoteTtlRequest>,
1021    ) -> Result<Response<UpdateResponse>, Status> {
1022        self.ensure_mutation_allowed().await?;
1023        let request = request.into_inner();
1024
1025        self.set_quote_ttl(request.mint_ttl, request.melt_ttl)
1026            .await?;
1027
1028        Ok(Response::new(UpdateResponse {}))
1029    }
1030
1031    /// Gets the mint's quote time-to-live settings
1032    async fn get_quote_ttl(
1033        &self,
1034        _request: Request<GetQuoteTtlRequest>,
1035    ) -> Result<Response<GetQuoteTtlResponse>, Status> {
1036        let ttl = self.quote_ttl().await?;
1037
1038        Ok(Response::new(GetQuoteTtlResponse {
1039            mint_ttl: ttl.mint_ttl,
1040            melt_ttl: ttl.melt_ttl,
1041        }))
1042    }
1043
1044    /// Updates a specific NUT-04 quote's state
1045    async fn update_nut04_quote(
1046        &self,
1047        request: Request<UpdateNut04QuoteRequest>,
1048    ) -> Result<Response<UpdateNut04QuoteRequest>, Status> {
1049        self.ensure_mutation_allowed().await?;
1050        self.ensure_mint_quote_state_override_allowed()?;
1051        let request = request.into_inner();
1052
1053        let state = MintQuoteState::from_str(&request.state)
1054            .map_err(|_| Status::invalid_argument("Invalid quote state".to_string()))?;
1055
1056        let mint_quote = match state {
1057            MintQuoteState::Paid => self.set_mint_quote_paid(&request.quote_id).await?,
1058            _ => {
1059                let quote_id = request
1060                    .quote_id
1061                    .parse()
1062                    .map_err(|_| Status::invalid_argument("Invalid quote id".to_string()))?;
1063
1064                let mint_quote = self
1065                    .mint
1066                    .localstore()
1067                    .get_mint_quote(&quote_id)
1068                    .await
1069                    .map_err(|_| Status::invalid_argument("Could not find quote".to_string()))?
1070                    .ok_or(Status::invalid_argument("Could not find quote".to_string()))?;
1071
1072                // Create a new quote with the same values
1073                let quote = MintQuote::new(
1074                    Some(mint_quote.id.clone()),          // id
1075                    mint_quote.request.clone(),           // request
1076                    mint_quote.unit.clone(),              // unit
1077                    mint_quote.amount.clone(),            // amount
1078                    mint_quote.expiry,                    // expiry
1079                    mint_quote.request_lookup_id.clone(), // request_lookup_id
1080                    mint_quote.pubkey,                    // pubkey
1081                    mint_quote.amount_issued(),           // amount_issued
1082                    mint_quote.amount_paid(),             // amount_paid
1083                    mint_quote.payment_method.clone(),    // method
1084                    0,                                    // created_at
1085                    0,                                    // updated_at
1086                    vec![],                               // blinded_messages
1087                    vec![],                               // payment_ids
1088                    None,                                 // extra_json
1089                );
1090
1091                let mint_store = self.mint.localstore();
1092                let mut tx = mint_store
1093                    .begin_transaction()
1094                    .await
1095                    .map_err(|_| Status::internal("Could not update quote".to_string()))?;
1096                tx.add_mint_quote(quote.clone())
1097                    .await
1098                    .map_err(|_| Status::internal("Could not update quote".to_string()))?;
1099                tx.commit()
1100                    .await
1101                    .map_err(|_| Status::internal("Could not update quote".to_string()))?;
1102
1103                self.mint
1104                    .localstore()
1105                    .get_mint_quote(&quote_id)
1106                    .await
1107                    .map_err(|_| Status::invalid_argument("Could not find quote".to_string()))?
1108                    .ok_or(Status::invalid_argument("Could not find quote".to_string()))?
1109            }
1110        };
1111
1112        Ok(Response::new(UpdateNut04QuoteRequest {
1113            state: mint_quote.state().to_string(),
1114            quote_id: mint_quote.id.to_string(),
1115        }))
1116    }
1117
1118    /// Rotates to the next keyset for the specified currency unit
1119    async fn rotate_next_keyset(
1120        &self,
1121        request: Request<RotateNextKeysetRequest>,
1122    ) -> Result<Response<RotateNextKeysetResponse>, Status> {
1123        let request = request.into_inner();
1124
1125        let unit = CurrencyUnit::from_str(&request.unit)
1126            .map_err(|_| Status::invalid_argument("Invalid unit".to_string()))?;
1127
1128        let keyset_info = self
1129            .rotate_keyset(
1130                unit,
1131                request.amounts,
1132                request.input_fee_ppk,
1133                request.use_keyset_v2,
1134                request.final_expiry,
1135            )
1136            .await?;
1137
1138        Ok(Response::new(RotateNextKeysetResponse {
1139            id: keyset_info.id.to_string(),
1140            unit: keyset_info.unit.to_string(),
1141            amounts: keyset_info.amounts,
1142            input_fee_ppk: keyset_info.input_fee_ppk,
1143        }))
1144    }
1145}
1146
1147#[tonic::async_trait]
1148impl KeysetService for MintRPCServer {
1149    /// Rotates to the next keyset for the specified currency unit
1150    async fn rotate_next_keyset(
1151        &self,
1152        request: Request<crate::keyset::RotateNextKeysetRequest>,
1153    ) -> Result<Response<crate::keyset::RotateNextKeysetResponse>, Status> {
1154        let request = request.into_inner();
1155
1156        let unit = CurrencyUnit::from_str(&request.unit)
1157            .map_err(|_| Status::invalid_argument("Invalid unit".to_string()))?;
1158
1159        let keyset_info = self
1160            .rotate_keyset(
1161                unit,
1162                request.amounts,
1163                request.input_fee_ppk,
1164                request.use_keyset_v2,
1165                request.final_expiry,
1166            )
1167            .await?;
1168
1169        Ok(Response::new(crate::keyset::RotateNextKeysetResponse {
1170            id: keyset_info.id.to_string(),
1171            unit: keyset_info.unit.to_string(),
1172            amounts: keyset_info.amounts,
1173            input_fee_ppk: keyset_info.input_fee_ppk,
1174        }))
1175    }
1176}
1177
1178#[tonic::async_trait]
1179impl QuoteService for MintRPCServer {
1180    /// Gets the mint's quote time-to-live settings
1181    async fn get_quote_ttl(
1182        &self,
1183        _request: Request<crate::quote::GetQuoteTtlRequest>,
1184    ) -> Result<Response<crate::quote::GetQuoteTtlResponse>, Status> {
1185        let ttl = self.quote_ttl().await?;
1186
1187        Ok(Response::new(crate::quote::GetQuoteTtlResponse {
1188            mint_ttl: ttl.mint_ttl,
1189            melt_ttl: ttl.melt_ttl,
1190        }))
1191    }
1192
1193    /// Updates the mint's quote time-to-live settings
1194    async fn update_quote_ttl(
1195        &self,
1196        request: Request<crate::quote::UpdateQuoteTtlRequest>,
1197    ) -> Result<Response<crate::quote::UpdateQuoteTtlResponse>, Status> {
1198        self.ensure_mutation_allowed().await?;
1199        let request = request.into_inner();
1200
1201        let ttl = self
1202            .set_quote_ttl(request.mint_ttl, request.melt_ttl)
1203            .await?;
1204
1205        Ok(Response::new(crate::quote::UpdateQuoteTtlResponse {
1206            mint_ttl: ttl.mint_ttl,
1207            melt_ttl: ttl.melt_ttl,
1208        }))
1209    }
1210
1211    /// Force-marks a mint quote as paid
1212    async fn update_mint_quote_state(
1213        &self,
1214        request: Request<crate::quote::UpdateMintQuoteStateRequest>,
1215    ) -> Result<Response<crate::quote::UpdateMintQuoteStateResponse>, Status> {
1216        self.ensure_mutation_allowed().await?;
1217        let request = request.into_inner();
1218
1219        match request.state() {
1220            crate::quote::MintQuoteState::Paid => (),
1221            crate::quote::MintQuoteState::Unpaid => {
1222                return Err(Status::invalid_argument(
1223                    "Cannot unpay a quote: payments cannot be retracted".to_string(),
1224                ));
1225            }
1226            crate::quote::MintQuoteState::Issued => {
1227                return Err(Status::invalid_argument(
1228                    "Cannot issue a quote: no signatures would back the issuance".to_string(),
1229                ));
1230            }
1231            crate::quote::MintQuoteState::Unspecified => {
1232                return Err(Status::invalid_argument(
1233                    "Quote state is required".to_string(),
1234                ));
1235            }
1236        }
1237
1238        let mint_quote = self.set_mint_quote_paid(&request.quote_id).await?;
1239
1240        Ok(Response::new(crate::quote::UpdateMintQuoteStateResponse {
1241            quote_id: mint_quote.id.to_string(),
1242            state: crate::quote::MintQuoteState::from(mint_quote.state()).into(),
1243        }))
1244    }
1245}
1246
1247#[tonic::async_trait]
1248impl WalletService for MintRPCServer {
1249    /// Creates an on-chain address for operator deposits.
1250    async fn create_deposit_address(
1251        &self,
1252        _request: Request<crate::wallet::CreateDepositAddressRequest>,
1253    ) -> Result<Response<crate::wallet::CreateDepositAddressResponse>, Status> {
1254        self.ensure_mutation_allowed().await?;
1255        let address = self
1256            .wallet_info_provider()?
1257            .create_deposit_address()
1258            .await
1259            .map_err(|err| Status::internal(err.to_string()))?;
1260
1261        Ok(Response::new(crate::wallet::CreateDepositAddressResponse {
1262            address,
1263        }))
1264    }
1265
1266    /// Gets the on-chain wallet balance.
1267    async fn get_balance(
1268        &self,
1269        _request: Request<crate::wallet::GetBalanceRequest>,
1270    ) -> Result<Response<crate::wallet::GetBalanceResponse>, Status> {
1271        let balance = self
1272            .wallet_info_provider()?
1273            .get_balance()
1274            .await
1275            .map_err(|err| Status::internal(err.to_string()))?;
1276
1277        Ok(Response::new(balance))
1278    }
1279
1280    /// Lists on-chain wallet transactions.
1281    async fn list_transactions(
1282        &self,
1283        request: Request<crate::wallet::ListTransactionsRequest>,
1284    ) -> Result<Response<crate::wallet::ListTransactionsResponse>, Status> {
1285        let request = request.into_inner();
1286        let limit = page_limit(
1287            request.limit,
1288            DEFAULT_TRANSACTION_LIMIT,
1289            MAX_TRANSACTION_LIMIT,
1290        )?;
1291
1292        let WalletTransactionPage {
1293            transactions,
1294            total,
1295        } = self
1296            .wallet_info_provider()?
1297            .list_transactions(request.offset as usize, limit)
1298            .await
1299            .map_err(|err| Status::internal(err.to_string()))?;
1300
1301        Ok(Response::new(crate::wallet::ListTransactionsResponse {
1302            transactions,
1303            total,
1304        }))
1305    }
1306
1307    /// Lists addresses revealed by the on-chain wallet.
1308    async fn list_addresses(
1309        &self,
1310        request: Request<crate::wallet::ListAddressesRequest>,
1311    ) -> Result<Response<crate::wallet::ListAddressesResponse>, Status> {
1312        let request = request.into_inner();
1313        let limit = page_limit(request.limit, DEFAULT_ADDRESS_LIMIT, MAX_ADDRESS_LIMIT)?;
1314
1315        let WalletAddressPage { addresses, total } = self
1316            .wallet_info_provider()?
1317            .list_addresses(request.offset as usize, limit)
1318            .await
1319            .map_err(|err| Status::internal(err.to_string()))?;
1320
1321        Ok(Response::new(crate::wallet::ListAddressesResponse {
1322            addresses,
1323            total,
1324        }))
1325    }
1326}
1327
1328fn page_limit(requested: u32, default: u32, maximum: u32) -> Result<usize, Status> {
1329    let limit = match requested {
1330        0 => default,
1331        requested if requested <= maximum => requested,
1332        requested => {
1333            return Err(Status::invalid_argument(format!(
1334                "Requested page size {requested} exceeds maximum {maximum}"
1335            )));
1336        }
1337    };
1338
1339    Ok(limit as usize)
1340}
1341
1342impl From<MintQuoteState> for crate::quote::MintQuoteState {
1343    fn from(state: MintQuoteState) -> Self {
1344        match state {
1345            MintQuoteState::Unpaid => Self::Unpaid,
1346            MintQuoteState::Paid => Self::Paid,
1347            MintQuoteState::Issued => Self::Issued,
1348        }
1349    }
1350}
1351
1352#[tonic::async_trait]
1353impl PaymentMethodService for MintRPCServer {
1354    /// Updates the settings of one mint (NUT-04) payment method
1355    async fn update_mint_method(
1356        &self,
1357        request: Request<crate::payment_method::UpdateMintMethodRequest>,
1358    ) -> Result<Response<crate::payment_method::UpdateMintMethodResponse>, Status> {
1359        let request = request.into_inner();
1360
1361        if request.options.is_some()
1362            && PaymentMethod::from_str(&request.method).is_ok_and(|method| !method.is_bolt11())
1363        {
1364            return Err(Status::invalid_argument(
1365                "Options can only be set on the bolt11 method".to_string(),
1366            ));
1367        }
1368
1369        let options = request
1370            .options
1371            .map(|options| cdk::nuts::nut04::MintMethodOptions::Bolt11 {
1372                description: options.description,
1373            });
1374
1375        let settings = self
1376            .set_mint_method(
1377                &request.unit,
1378                &request.method,
1379                request.min_amount,
1380                request.max_amount,
1381                options,
1382                request.method_name,
1383                None,
1384            )
1385            .await?;
1386
1387        Ok(Response::new(settings.into()))
1388    }
1389
1390    /// Updates the settings of one melt (NUT-05) payment method
1391    async fn update_melt_method(
1392        &self,
1393        request: Request<crate::payment_method::UpdateMeltMethodRequest>,
1394    ) -> Result<Response<crate::payment_method::UpdateMeltMethodResponse>, Status> {
1395        let request = request.into_inner();
1396
1397        if request.options.is_some()
1398            && PaymentMethod::from_str(&request.method).is_ok_and(|method| !method.is_bolt11())
1399        {
1400            return Err(Status::invalid_argument(
1401                "Options can only be set on the bolt11 method".to_string(),
1402            ));
1403        }
1404
1405        let options = request
1406            .options
1407            .map(|options| cdk::nuts::nut05::MeltMethodOptions::Bolt11 {
1408                amountless: options.amountless,
1409            });
1410
1411        let settings = self
1412            .set_melt_method(
1413                &request.unit,
1414                &request.method,
1415                request.min_amount,
1416                request.max_amount,
1417                options,
1418                request.method_name,
1419                None,
1420            )
1421            .await?;
1422
1423        Ok(Response::new(settings.into()))
1424    }
1425
1426    /// Enables or disables minting and melting for the whole mint
1427    async fn update_disabled(
1428        &self,
1429        request: Request<crate::payment_method::UpdateDisabledRequest>,
1430    ) -> Result<Response<crate::payment_method::UpdateDisabledResponse>, Status> {
1431        let request = request.into_inner();
1432
1433        let (mint_disabled, melt_disabled) = self
1434            .set_disabled(request.mint_disabled, request.melt_disabled)
1435            .await?;
1436
1437        Ok(Response::new(
1438            crate::payment_method::UpdateDisabledResponse {
1439                mint_disabled,
1440                melt_disabled,
1441            },
1442        ))
1443    }
1444}
1445
1446impl From<MintMethodSettings> for crate::payment_method::UpdateMintMethodResponse {
1447    fn from(settings: MintMethodSettings) -> Self {
1448        let options = settings.options.and_then(|options| match options {
1449            cdk::nuts::nut04::MintMethodOptions::Bolt11 { description }
1450            | cdk::nuts::nut04::MintMethodOptions::Bolt12 { description } => {
1451                Some(crate::payment_method::Bolt11MintMethodOptions { description })
1452            }
1453            _ => None,
1454        });
1455
1456        Self {
1457            unit: settings.unit.to_string(),
1458            method: settings.method.to_string(),
1459            min_amount: settings.min_amount.map(u64::from),
1460            max_amount: settings.max_amount.map(u64::from),
1461            options,
1462            method_name: settings.method_name,
1463        }
1464    }
1465}
1466
1467impl From<MeltMethodSettings> for crate::payment_method::UpdateMeltMethodResponse {
1468    fn from(settings: MeltMethodSettings) -> Self {
1469        let options = settings.options.map(|options| match options {
1470            cdk::nuts::nut05::MeltMethodOptions::Bolt11 { amountless } => {
1471                crate::payment_method::Bolt11MeltMethodOptions { amountless }
1472            }
1473        });
1474
1475        Self {
1476            unit: settings.unit.to_string(),
1477            method: settings.method.to_string(),
1478            min_amount: settings.min_amount.map(u64::from),
1479            max_amount: settings.max_amount.map(u64::from),
1480            options,
1481            method_name: settings.method_name,
1482        }
1483    }
1484}
1485
1486#[cfg(test)]
1487mod tests {
1488    use std::collections::{HashMap, HashSet};
1489    use std::sync::Arc;
1490
1491    use bip39::Mnemonic;
1492    use cdk::amount::SplitTarget;
1493    use cdk::mint::{MintBuilder, MintInput, MintMeltLimits};
1494    use cdk::nuts::{CurrencyUnit, MintRequest, PaymentMethod, PreMintSecrets};
1495    use cdk::types::QuoteTTL;
1496    use cdk_common::nut00::KnownMethod;
1497    use cdk_common::MintQuoteBolt11Request;
1498    use cdk_fake_wallet::FakeWallet;
1499    use tonic::{Code, Request};
1500
1501    use super::*;
1502    use crate::cdk_mint_server::CdkMint;
1503    use crate::{GetInfoRequest, UpdateTosUrlRequest};
1504
1505    struct TestWalletInfoProvider;
1506
1507    #[async_trait::async_trait]
1508    impl crate::WalletInfoProvider for TestWalletInfoProvider {
1509        async fn create_deposit_address(&self) -> Result<String, crate::WalletInfoError> {
1510            Ok("bcrt1qoperatordeposit".to_string())
1511        }
1512
1513        async fn get_balance(
1514            &self,
1515        ) -> Result<crate::wallet::GetBalanceResponse, crate::WalletInfoError> {
1516            Ok(crate::wallet::GetBalanceResponse {
1517                confirmed_sat: 21,
1518                trusted_pending_sat: 2,
1519                untrusted_pending_sat: 3,
1520                immature_sat: 4,
1521                trusted_spendable_sat: 23,
1522                total_sat: 30,
1523                network: "regtest".to_string(),
1524                synced_height: 123,
1525            })
1526        }
1527
1528        async fn list_transactions(
1529            &self,
1530            offset: usize,
1531            limit: usize,
1532        ) -> Result<crate::WalletTransactionPage, crate::WalletInfoError> {
1533            Ok(crate::WalletTransactionPage {
1534                transactions: vec![crate::wallet::WalletTransaction {
1535                    txid: format!("{offset}:{limit}"),
1536                    inputs: vec![crate::wallet::WalletTransactionInput {
1537                        txid: "previous-txid".to_string(),
1538                        vout: 1,
1539                        amount_sat: Some(42_000),
1540                        address: Some("bcrt1qinput".to_string()),
1541                    }],
1542                    outputs: vec![crate::wallet::WalletTransactionOutput {
1543                        vout: 2,
1544                        address: "bcrt1qoutput".to_string(),
1545                        amount_sat: 21_000,
1546                        quote_id: Some("quote-id".to_string()),
1547                    }],
1548                    ..Default::default()
1549                }],
1550                total: 7,
1551            })
1552        }
1553
1554        async fn list_addresses(
1555            &self,
1556            offset: usize,
1557            limit: usize,
1558        ) -> Result<crate::WalletAddressPage, crate::WalletInfoError> {
1559            Ok(crate::WalletAddressPage {
1560                addresses: vec![crate::wallet::WalletAddress {
1561                    address: format!("{offset}:{limit}"),
1562                    ..Default::default()
1563                }],
1564                total: 9,
1565            })
1566        }
1567    }
1568
1569    /// A well-formed quote id that no test mint has issued
1570    const UNKNOWN_QUOTE_ID: &str = "019820ab-cdef-7000-8000-000000000000";
1571
1572    async fn create_test_rpc_server() -> MintRPCServer {
1573        create_test_rpc_server_with_payment_delay(2).await
1574    }
1575
1576    /// Builds a test server whose fake payment backend waits `payment_delay`
1577    /// seconds before reporting a quote paid
1578    ///
1579    /// Tests that drive quote state themselves pass a delay long enough that
1580    /// the backend never reports a payment of its own.
1581    async fn create_test_rpc_server_with_payment_delay(payment_delay: u64) -> MintRPCServer {
1582        let db = Arc::new(cdk_sqlite::mint::memory::empty().await.unwrap());
1583
1584        let mut mint_builder = MintBuilder::new(db.clone());
1585
1586        let fee_reserve = cdk::types::FeeReserve {
1587            min_fee_reserve: 1.into(),
1588            percent_fee_reserve: 1.0,
1589        };
1590
1591        let fake_backend = FakeWallet::new(
1592            fee_reserve,
1593            HashMap::default(),
1594            HashSet::default(),
1595            payment_delay,
1596            CurrencyUnit::Sat,
1597        );
1598
1599        mint_builder
1600            .add_payment_processor(
1601                CurrencyUnit::Sat,
1602                PaymentMethod::Known(KnownMethod::Bolt11),
1603                MintMeltLimits::new(1, 10_000),
1604                Arc::new(fake_backend),
1605            )
1606            .await
1607            .unwrap();
1608
1609        let mnemonic = Mnemonic::generate(12).unwrap();
1610
1611        mint_builder = mint_builder
1612            .with_name("test mint".to_string())
1613            .with_description("test mint".to_string());
1614
1615        let mint = mint_builder
1616            .build_with_seed(db.clone(), &mnemonic.to_seed_normalized(""))
1617            .await
1618            .unwrap();
1619
1620        mint.set_quote_ttl(QuoteTTL::new(10000, 10000))
1621            .await
1622            .unwrap();
1623
1624        mint.start().await.unwrap();
1625
1626        MintRPCServer {
1627            socket_addr: "127.0.0.1:0".parse().unwrap(),
1628            mint: Arc::new(mint),
1629            mutation_guard: None,
1630            allow_mint_quote_payment_override: false,
1631            wallet_info_provider: None,
1632            shutdown: Arc::new(Notify::new()),
1633            handle: None,
1634        }
1635    }
1636
1637    #[derive(Debug)]
1638    struct RejectingMutationGuard;
1639
1640    #[tonic::async_trait]
1641    impl MintMutationGuard for RejectingMutationGuard {
1642        async fn check(&self) -> Result<(), MintMutationGuardError> {
1643            Err(MintMutationGuardError::FailedPrecondition(
1644                "configuration restart pending".to_owned(),
1645            ))
1646        }
1647    }
1648
1649    #[tokio::test]
1650    async fn wallet_service_requires_a_configured_provider() {
1651        let server = create_test_rpc_server().await;
1652
1653        let error = server
1654            .get_balance(Request::new(crate::wallet::GetBalanceRequest {}))
1655            .await
1656            .expect_err("wallet provider should be required");
1657
1658        assert_eq!(error.code(), tonic::Code::FailedPrecondition);
1659    }
1660
1661    #[tokio::test]
1662    async fn wallet_service_returns_provider_data_and_applies_page_defaults() {
1663        let server = create_test_rpc_server()
1664            .await
1665            .with_wallet_info_provider(Arc::new(TestWalletInfoProvider));
1666
1667        let deposit_address = server
1668            .create_deposit_address(Request::new(crate::wallet::CreateDepositAddressRequest {}))
1669            .await
1670            .expect("create deposit address")
1671            .into_inner();
1672        assert_eq!(deposit_address.address, "bcrt1qoperatordeposit");
1673
1674        let balance = server
1675            .get_balance(Request::new(crate::wallet::GetBalanceRequest {}))
1676            .await
1677            .expect("get balance")
1678            .into_inner();
1679        assert_eq!(balance.confirmed_sat, 21);
1680        assert_eq!(balance.total_sat, 30);
1681        assert_eq!(balance.network, "regtest");
1682        assert_eq!(balance.synced_height, 123);
1683
1684        let transactions = server
1685            .list_transactions(Request::new(crate::wallet::ListTransactionsRequest {
1686                limit: 0,
1687                offset: 2,
1688            }))
1689            .await
1690            .expect("list transactions")
1691            .into_inner();
1692        assert_eq!(transactions.total, 7);
1693        assert_eq!(transactions.transactions[0].txid, "2:20");
1694        let input = &transactions.transactions[0].inputs[0];
1695        assert_eq!(input.txid, "previous-txid");
1696        assert_eq!(input.vout, 1);
1697        assert_eq!(input.amount_sat, Some(42_000));
1698        assert_eq!(input.address.as_deref(), Some("bcrt1qinput"));
1699        let output = &transactions.transactions[0].outputs[0];
1700        assert_eq!(output.vout, 2);
1701        assert_eq!(output.address, "bcrt1qoutput");
1702        assert_eq!(output.amount_sat, 21_000);
1703        assert_eq!(output.quote_id.as_deref(), Some("quote-id"));
1704
1705        let addresses = server
1706            .list_addresses(Request::new(crate::wallet::ListAddressesRequest {
1707                limit: 3,
1708                offset: 4,
1709            }))
1710            .await
1711            .expect("list addresses")
1712            .into_inner();
1713        assert_eq!(addresses.total, 9);
1714        assert_eq!(addresses.addresses[0].address, "4:3");
1715    }
1716
1717    #[test]
1718    fn wallet_service_rejects_oversized_pages() {
1719        let error = page_limit(101, DEFAULT_TRANSACTION_LIMIT, MAX_TRANSACTION_LIMIT)
1720            .expect_err("oversized page should fail");
1721
1722        assert_eq!(error.code(), tonic::Code::InvalidArgument);
1723    }
1724
1725    #[tokio::test]
1726    async fn test_get_info_tos_url_none_when_not_set() {
1727        let server = create_test_rpc_server().await;
1728
1729        let response = server
1730            .get_info(Request::new(GetInfoRequest {}))
1731            .await
1732            .unwrap();
1733
1734        assert!(response.into_inner().tos_url.is_none());
1735    }
1736
1737    #[tokio::test]
1738    async fn test_get_info_includes_tos_url() {
1739        let server = create_test_rpc_server().await;
1740        let tos = "https://example.com/tos";
1741
1742        let mut info = server.mint.mint_info().await.unwrap();
1743        info.tos_url = Some(tos.to_string());
1744        server.mint.set_mint_info(info).await.unwrap();
1745
1746        let response = server
1747            .get_info(Request::new(GetInfoRequest {}))
1748            .await
1749            .unwrap();
1750
1751        assert_eq!(response.into_inner().tos_url.unwrap(), tos);
1752    }
1753
1754    #[tokio::test]
1755    async fn test_keyset_service_rotate_next_keyset() {
1756        let server = create_test_rpc_server().await;
1757
1758        let response = KeysetService::rotate_next_keyset(
1759            &server,
1760            Request::new(crate::keyset::RotateNextKeysetRequest {
1761                unit: "sat".to_string(),
1762                amounts: vec![1, 2, 4, 8],
1763                input_fee_ppk: Some(1),
1764                use_keyset_v2: Some(true),
1765                final_expiry: None,
1766            }),
1767        )
1768        .await
1769        .unwrap();
1770
1771        let response = response.into_inner();
1772        assert!(!response.id.is_empty());
1773        assert_eq!(response.unit, "sat");
1774        assert_eq!(response.amounts, vec![1, 2, 4, 8]);
1775        assert_eq!(response.input_fee_ppk, 1);
1776    }
1777
1778    #[tokio::test]
1779    async fn test_quote_service_get_quote_ttl() {
1780        let server = create_test_rpc_server().await;
1781
1782        let response =
1783            QuoteService::get_quote_ttl(&server, Request::new(crate::quote::GetQuoteTtlRequest {}))
1784                .await
1785                .unwrap();
1786
1787        let response = response.into_inner();
1788        assert_eq!(response.mint_ttl, 10000);
1789        assert_eq!(response.melt_ttl, 10000);
1790    }
1791
1792    #[tokio::test]
1793    async fn test_quote_service_update_quote_ttl_keeps_omitted_setting() {
1794        let server = create_test_rpc_server().await;
1795
1796        let response = QuoteService::update_quote_ttl(
1797            &server,
1798            Request::new(crate::quote::UpdateQuoteTtlRequest {
1799                mint_ttl: Some(60),
1800                melt_ttl: None,
1801            }),
1802        )
1803        .await
1804        .unwrap();
1805
1806        let response = response.into_inner();
1807        assert_eq!(response.mint_ttl, 60);
1808        assert_eq!(response.melt_ttl, 10000);
1809
1810        let persisted =
1811            QuoteService::get_quote_ttl(&server, Request::new(crate::quote::GetQuoteTtlRequest {}))
1812                .await
1813                .unwrap()
1814                .into_inner();
1815        assert_eq!(persisted.mint_ttl, 60);
1816        assert_eq!(persisted.melt_ttl, 10000);
1817    }
1818
1819    /// Creates a bolt11 mint quote for `amount` sats and returns its id
1820    async fn create_test_mint_quote(server: &MintRPCServer, amount: u64) -> String {
1821        let response = server
1822            .mint
1823            .get_mint_quote(
1824                MintQuoteBolt11Request {
1825                    amount: amount.into(),
1826                    unit: CurrencyUnit::Sat,
1827                    description: None,
1828                    pubkey: None,
1829                }
1830                .into(),
1831            )
1832            .await
1833            .unwrap();
1834
1835        response.quote().to_string()
1836    }
1837
1838    /// Issues the full paid amount of a quote, leaving it in the issued state
1839    async fn issue_test_mint_quote(server: &MintRPCServer, quote_id: &str, amount: u64) {
1840        let keyset_id = *server
1841            .mint
1842            .get_active_keysets()
1843            .get(&CurrencyUnit::Sat)
1844            .unwrap();
1845        let keys = server
1846            .mint
1847            .keyset_pubkeys(&keyset_id)
1848            .unwrap()
1849            .keysets
1850            .first()
1851            .unwrap()
1852            .keys
1853            .clone();
1854        let fees: (u64, Vec<u64>) = (0, keys.iter().map(|a| a.0.to_u64()).collect());
1855        let premint = PreMintSecrets::random(
1856            keyset_id,
1857            Amount::from(amount),
1858            &SplitTarget::None,
1859            &fees.into(),
1860        )
1861        .unwrap();
1862
1863        server
1864            .mint
1865            .process_mint_request(MintInput::Single(MintRequest {
1866                quote: quote_id.parse().unwrap(),
1867                outputs: premint.blinded_messages().to_vec(),
1868                signature: None,
1869            }))
1870            .await
1871            .unwrap();
1872    }
1873
1874    /// Returns the amount recorded as paid against a quote
1875    async fn amount_paid(server: &MintRPCServer, quote_id: &str) -> u64 {
1876        server
1877            .mint
1878            .localstore()
1879            .get_mint_quote(&quote_id.parse().unwrap())
1880            .await
1881            .unwrap()
1882            .unwrap()
1883            .amount_paid()
1884            .value()
1885    }
1886
1887    #[tokio::test]
1888    async fn test_quote_service_update_mint_quote_state_marks_quote_paid() {
1889        let server = create_test_rpc_server_with_payment_delay(3600)
1890            .await
1891            .with_mint_quote_payment_override(true);
1892        let quote_id = create_test_mint_quote(&server, 100).await;
1893
1894        assert_eq!(amount_paid(&server, &quote_id).await, 0);
1895
1896        let response = QuoteService::update_mint_quote_state(
1897            &server,
1898            Request::new(crate::quote::UpdateMintQuoteStateRequest {
1899                quote_id: quote_id.clone(),
1900                state: crate::quote::MintQuoteState::Paid.into(),
1901            }),
1902        )
1903        .await
1904        .unwrap()
1905        .into_inner();
1906
1907        assert_eq!(response.quote_id, quote_id);
1908        assert_eq!(response.state(), crate::quote::MintQuoteState::Paid);
1909        assert_eq!(amount_paid(&server, &quote_id).await, 100);
1910    }
1911
1912    #[tokio::test]
1913    async fn test_quote_service_update_mint_quote_state_paid_twice_pays_once() {
1914        let server = create_test_rpc_server_with_payment_delay(3600)
1915            .await
1916            .with_mint_quote_payment_override(true);
1917        let quote_id = create_test_mint_quote(&server, 100).await;
1918
1919        for _ in 0..2 {
1920            let response = QuoteService::update_mint_quote_state(
1921                &server,
1922                Request::new(crate::quote::UpdateMintQuoteStateRequest {
1923                    quote_id: quote_id.clone(),
1924                    state: crate::quote::MintQuoteState::Paid.into(),
1925                }),
1926            )
1927            .await
1928            .unwrap()
1929            .into_inner();
1930
1931            assert_eq!(response.state(), crate::quote::MintQuoteState::Paid);
1932        }
1933
1934        assert_eq!(amount_paid(&server, &quote_id).await, 100);
1935    }
1936
1937    #[tokio::test]
1938    async fn test_quote_service_update_mint_quote_state_reports_state_of_issued_quote() {
1939        let server = create_test_rpc_server_with_payment_delay(3600)
1940            .await
1941            .with_mint_quote_payment_override(true);
1942        let quote_id = create_test_mint_quote(&server, 32).await;
1943
1944        QuoteService::update_mint_quote_state(
1945            &server,
1946            Request::new(crate::quote::UpdateMintQuoteStateRequest {
1947                quote_id: quote_id.clone(),
1948                state: crate::quote::MintQuoteState::Paid.into(),
1949            }),
1950        )
1951        .await
1952        .unwrap();
1953        issue_test_mint_quote(&server, &quote_id, 32).await;
1954
1955        // The request asks for Paid; an already-issued quote stays Issued
1956        let response = QuoteService::update_mint_quote_state(
1957            &server,
1958            Request::new(crate::quote::UpdateMintQuoteStateRequest {
1959                quote_id: quote_id.clone(),
1960                state: crate::quote::MintQuoteState::Paid.into(),
1961            }),
1962        )
1963        .await
1964        .unwrap()
1965        .into_inner();
1966
1967        assert_eq!(response.state(), crate::quote::MintQuoteState::Issued);
1968        assert_eq!(amount_paid(&server, &quote_id).await, 32);
1969    }
1970
1971    #[tokio::test]
1972    async fn test_quote_service_update_mint_quote_state_rejects_unsupported_states() {
1973        let server = create_test_rpc_server().await;
1974
1975        // An unsupported state is rejected before the quote is looked up
1976        for (state, expected) in [
1977            (
1978                crate::quote::MintQuoteState::Unpaid,
1979                "Cannot unpay a quote: payments cannot be retracted",
1980            ),
1981            (
1982                crate::quote::MintQuoteState::Issued,
1983                "Cannot issue a quote: no signatures would back the issuance",
1984            ),
1985            (
1986                crate::quote::MintQuoteState::Unspecified,
1987                "Quote state is required",
1988            ),
1989        ] {
1990            let status = QuoteService::update_mint_quote_state(
1991                &server,
1992                Request::new(crate::quote::UpdateMintQuoteStateRequest {
1993                    quote_id: UNKNOWN_QUOTE_ID.to_string(),
1994                    state: state.into(),
1995                }),
1996            )
1997            .await
1998            .unwrap_err();
1999
2000            assert_eq!(status.code(), tonic::Code::InvalidArgument);
2001            assert_eq!(
2002                status.message(),
2003                expected,
2004                "unexpected rejection for {state:?}"
2005            );
2006        }
2007    }
2008
2009    #[tokio::test]
2010    async fn test_quote_service_update_mint_quote_state_unknown_quote() {
2011        let server = create_test_rpc_server()
2012            .await
2013            .with_mint_quote_payment_override(true);
2014
2015        let status = QuoteService::update_mint_quote_state(
2016            &server,
2017            Request::new(crate::quote::UpdateMintQuoteStateRequest {
2018                quote_id: UNKNOWN_QUOTE_ID.to_string(),
2019                state: crate::quote::MintQuoteState::Paid.into(),
2020            }),
2021        )
2022        .await
2023        .unwrap_err();
2024
2025        assert_eq!(status.code(), tonic::Code::InvalidArgument);
2026        assert_eq!(status.message(), "Could not find quote");
2027    }
2028
2029    #[tokio::test]
2030    async fn test_mint_quote_state_overrides_are_disabled_by_default() {
2031        let server = create_test_rpc_server_with_payment_delay(3600).await;
2032        let quote_id = create_test_mint_quote(&server, 100).await;
2033
2034        let status = QuoteService::update_mint_quote_state(
2035            &server,
2036            Request::new(crate::quote::UpdateMintQuoteStateRequest {
2037                quote_id: quote_id.clone(),
2038                state: crate::quote::MintQuoteState::Paid.into(),
2039            }),
2040        )
2041        .await
2042        .expect_err("payment override should be disabled");
2043
2044        assert_eq!(status.code(), tonic::Code::PermissionDenied);
2045        assert_eq!(status.message(), "Mint quote state override is disabled");
2046        assert_eq!(amount_paid(&server, &quote_id).await, 0);
2047
2048        for state in [
2049            MintQuoteState::Unpaid,
2050            MintQuoteState::Paid,
2051            MintQuoteState::Issued,
2052        ] {
2053            let legacy_status = CdkMint::update_nut04_quote(
2054                &server,
2055                Request::new(UpdateNut04QuoteRequest {
2056                    quote_id: quote_id.clone(),
2057                    state: state.to_string(),
2058                }),
2059            )
2060            .await
2061            .expect_err("legacy quote state override should be disabled");
2062
2063            assert_eq!(legacy_status.code(), tonic::Code::PermissionDenied);
2064            assert_eq!(
2065                legacy_status.message(),
2066                "Mint quote state override is disabled"
2067            );
2068        }
2069        assert_eq!(amount_paid(&server, &quote_id).await, 0);
2070    }
2071
2072    #[tokio::test]
2073    async fn test_update_tos_url() {
2074        let server = create_test_rpc_server().await;
2075        let tos = "https://example.com/terms";
2076
2077        server
2078            .update_tos_url(Request::new(UpdateTosUrlRequest {
2079                tos_url: tos.to_string(),
2080            }))
2081            .await
2082            .unwrap();
2083
2084        let response = server
2085            .get_info(Request::new(GetInfoRequest {}))
2086            .await
2087            .unwrap();
2088
2089        assert_eq!(response.into_inner().tos_url.unwrap(), tos);
2090    }
2091
2092    #[tokio::test]
2093    async fn test_mutation_guard_rejects_updates_without_blocking_reads() {
2094        let server = create_test_rpc_server()
2095            .await
2096            .with_mutation_guard(Arc::new(RejectingMutationGuard));
2097
2098        let error = server
2099            .update_tos_url(Request::new(UpdateTosUrlRequest {
2100                tos_url: "https://example.com/terms".to_owned(),
2101            }))
2102            .await
2103            .expect_err("mutation should be rejected");
2104
2105        assert_eq!(error.code(), Code::FailedPrecondition);
2106        assert_eq!(error.message(), "configuration restart pending");
2107
2108        let keyset_error = KeysetService::rotate_next_keyset(
2109            &server,
2110            Request::new(crate::keyset::RotateNextKeysetRequest {
2111                unit: "sat".to_owned(),
2112                amounts: vec![1, 2, 4, 8],
2113                input_fee_ppk: Some(1),
2114                use_keyset_v2: Some(true),
2115                final_expiry: None,
2116            }),
2117        )
2118        .await
2119        .expect_err("keyset mutation should be rejected");
2120
2121        assert_eq!(keyset_error.code(), Code::FailedPrecondition);
2122        assert_eq!(keyset_error.message(), "configuration restart pending");
2123
2124        let quote_ttl_error = QuoteService::update_quote_ttl(
2125            &server,
2126            Request::new(crate::quote::UpdateQuoteTtlRequest {
2127                mint_ttl: Some(60),
2128                melt_ttl: None,
2129            }),
2130        )
2131        .await
2132        .expect_err("quote TTL mutation should be rejected");
2133
2134        assert_eq!(quote_ttl_error.code(), Code::FailedPrecondition);
2135        assert_eq!(quote_ttl_error.message(), "configuration restart pending");
2136
2137        let quote_state_error = QuoteService::update_mint_quote_state(
2138            &server,
2139            Request::new(crate::quote::UpdateMintQuoteStateRequest {
2140                quote_id: UNKNOWN_QUOTE_ID.to_owned(),
2141                state: crate::quote::MintQuoteState::Paid.into(),
2142            }),
2143        )
2144        .await
2145        .expect_err("quote-state mutation should be rejected");
2146
2147        assert_eq!(quote_state_error.code(), Code::FailedPrecondition);
2148        assert_eq!(quote_state_error.message(), "configuration restart pending");
2149
2150        let mint_method_error = PaymentMethodService::update_mint_method(
2151            &server,
2152            Request::new(crate::payment_method::UpdateMintMethodRequest {
2153                unit: "sat".to_owned(),
2154                method: "bolt11".to_owned(),
2155                min_amount: Some(1),
2156                max_amount: None,
2157                options: None,
2158                method_name: None,
2159            }),
2160        )
2161        .await
2162        .expect_err("mint-method mutation should be rejected");
2163
2164        assert_eq!(mint_method_error.code(), Code::FailedPrecondition);
2165        assert_eq!(mint_method_error.message(), "configuration restart pending");
2166
2167        let melt_method_error = PaymentMethodService::update_melt_method(
2168            &server,
2169            Request::new(crate::payment_method::UpdateMeltMethodRequest {
2170                unit: "sat".to_owned(),
2171                method: "bolt11".to_owned(),
2172                min_amount: Some(1),
2173                max_amount: None,
2174                options: None,
2175                method_name: None,
2176            }),
2177        )
2178        .await
2179        .expect_err("melt-method mutation should be rejected");
2180
2181        assert_eq!(melt_method_error.code(), Code::FailedPrecondition);
2182        assert_eq!(melt_method_error.message(), "configuration restart pending");
2183
2184        let disabled_error = PaymentMethodService::update_disabled(
2185            &server,
2186            Request::new(crate::payment_method::UpdateDisabledRequest {
2187                mint_disabled: Some(true),
2188                melt_disabled: None,
2189            }),
2190        )
2191        .await
2192        .expect_err("disabled mutation should be rejected");
2193
2194        assert_eq!(disabled_error.code(), Code::FailedPrecondition);
2195        assert_eq!(disabled_error.message(), "configuration restart pending");
2196
2197        // A request that changes nothing is still a mutation RPC: the guard
2198        // runs before the both-flags-omitted early return
2199        let no_flags_error = PaymentMethodService::update_disabled(
2200            &server,
2201            Request::new(crate::payment_method::UpdateDisabledRequest {
2202                mint_disabled: None,
2203                melt_disabled: None,
2204            }),
2205        )
2206        .await
2207        .expect_err("no-flags disabled mutation should be rejected");
2208
2209        assert_eq!(no_flags_error.code(), Code::FailedPrecondition);
2210        assert_eq!(no_flags_error.message(), "configuration restart pending");
2211
2212        let deposit_address_error = WalletService::create_deposit_address(
2213            &server,
2214            Request::new(crate::wallet::CreateDepositAddressRequest {}),
2215        )
2216        .await
2217        .expect_err("deposit-address mutation should be rejected");
2218
2219        assert_eq!(deposit_address_error.code(), Code::FailedPrecondition);
2220        assert_eq!(
2221            deposit_address_error.message(),
2222            "configuration restart pending"
2223        );
2224        assert!(server
2225            .get_info(Request::new(GetInfoRequest {}))
2226            .await
2227            .expect("read should remain available")
2228            .into_inner()
2229            .tos_url
2230            .is_none());
2231    }
2232
2233    #[test]
2234    fn test_update_mint_method_response_maps_bolt12_description_options() {
2235        let settings = MintMethodSettings {
2236            method: PaymentMethod::Known(KnownMethod::Bolt12),
2237            unit: CurrencyUnit::Sat,
2238            method_name: Some("Bolt12".to_string()),
2239            min_amount: Some(Amount::from(1)),
2240            max_amount: Some(Amount::from(1_000)),
2241            options: Some(cdk::nuts::nut04::MintMethodOptions::Bolt12 { description: true }),
2242        };
2243
2244        let response: crate::payment_method::UpdateMintMethodResponse = settings.into();
2245
2246        assert_eq!(response.unit, "sat");
2247        assert_eq!(response.method, "bolt12");
2248        assert_eq!(response.min_amount, Some(1));
2249        assert_eq!(response.max_amount, Some(1_000));
2250        assert_eq!(
2251            response.options,
2252            Some(crate::payment_method::Bolt11MintMethodOptions { description: true })
2253        );
2254        assert_eq!(response.method_name.as_deref(), Some("Bolt12"));
2255    }
2256
2257    #[tokio::test]
2258    async fn test_payment_method_service_update_mint_method_keeps_omitted_settings() {
2259        let server = create_test_rpc_server().await;
2260
2261        let response = PaymentMethodService::update_mint_method(
2262            &server,
2263            Request::new(crate::payment_method::UpdateMintMethodRequest {
2264                unit: "sat".to_owned(),
2265                method: "bolt11".to_owned(),
2266                min_amount: Some(1),
2267                max_amount: Some(1_000),
2268                options: Some(crate::payment_method::Bolt11MintMethodOptions { description: true }),
2269                method_name: None,
2270            }),
2271        )
2272        .await
2273        .unwrap()
2274        .into_inner();
2275
2276        assert_eq!(response.unit, "sat");
2277        assert_eq!(response.method, "bolt11");
2278        assert_eq!(response.min_amount, Some(1));
2279        assert_eq!(response.max_amount, Some(1_000));
2280        assert_eq!(
2281            response.options,
2282            Some(crate::payment_method::Bolt11MintMethodOptions { description: true })
2283        );
2284
2285        let response = PaymentMethodService::update_mint_method(
2286            &server,
2287            Request::new(crate::payment_method::UpdateMintMethodRequest {
2288                unit: "sat".to_owned(),
2289                method: "bolt11".to_owned(),
2290                min_amount: None,
2291                max_amount: Some(5_000),
2292                options: None,
2293                method_name: None,
2294            }),
2295        )
2296        .await
2297        .unwrap()
2298        .into_inner();
2299
2300        assert_eq!(response.min_amount, Some(1));
2301        assert_eq!(response.max_amount, Some(5_000));
2302        assert_eq!(
2303            response.options,
2304            Some(crate::payment_method::Bolt11MintMethodOptions { description: true })
2305        );
2306
2307        let settings = server
2308            .mint
2309            .mint_info()
2310            .await
2311            .unwrap()
2312            .nuts
2313            .nut04
2314            .get_settings(
2315                &CurrencyUnit::Sat,
2316                &PaymentMethod::Known(KnownMethod::Bolt11),
2317            )
2318            .unwrap();
2319        assert_eq!(settings.min_amount, Some(Amount::from(1)));
2320        assert_eq!(settings.max_amount, Some(Amount::from(5_000)));
2321    }
2322
2323    #[tokio::test]
2324    async fn test_payment_method_service_update_mint_method_rejects_unknown_pair() {
2325        let server = create_test_rpc_server().await;
2326
2327        let error = PaymentMethodService::update_mint_method(
2328            &server,
2329            Request::new(crate::payment_method::UpdateMintMethodRequest {
2330                unit: "sat".to_owned(),
2331                method: "bolt12".to_owned(),
2332                min_amount: None,
2333                max_amount: None,
2334                options: None,
2335                method_name: None,
2336            }),
2337        )
2338        .await
2339        .expect_err("method without a payment processor should be rejected");
2340
2341        // The message separates this from the options guard, which must not
2342        // fire on a request that carries no options
2343        assert_eq!(error.code(), Code::InvalidArgument);
2344        assert_eq!(error.message(), "Unit payment method pair is not supported");
2345    }
2346
2347    #[tokio::test]
2348    async fn test_payment_method_service_rejects_options_on_non_bolt11_method() {
2349        let server = create_test_rpc_server().await;
2350
2351        let error = PaymentMethodService::update_mint_method(
2352            &server,
2353            Request::new(crate::payment_method::UpdateMintMethodRequest {
2354                unit: "sat".to_owned(),
2355                method: "onchain".to_owned(),
2356                min_amount: None,
2357                max_amount: None,
2358                options: Some(crate::payment_method::Bolt11MintMethodOptions { description: true }),
2359                method_name: None,
2360            }),
2361        )
2362        .await
2363        .expect_err("bolt11 options on an onchain method should be rejected");
2364
2365        // The processor check also rejects this pair; the message shows the guard fired
2366        assert_eq!(error.code(), Code::InvalidArgument);
2367        assert_eq!(
2368            error.message(),
2369            "Options can only be set on the bolt11 method"
2370        );
2371
2372        let error = PaymentMethodService::update_melt_method(
2373            &server,
2374            Request::new(crate::payment_method::UpdateMeltMethodRequest {
2375                unit: "sat".to_owned(),
2376                method: "onchain".to_owned(),
2377                min_amount: None,
2378                max_amount: None,
2379                options: Some(crate::payment_method::Bolt11MeltMethodOptions { amountless: true }),
2380                method_name: None,
2381            }),
2382        )
2383        .await
2384        .expect_err("bolt11 options on an onchain method should be rejected");
2385
2386        assert_eq!(error.code(), Code::InvalidArgument);
2387        assert_eq!(
2388            error.message(),
2389            "Options can only be set on the bolt11 method"
2390        );
2391    }
2392
2393    #[tokio::test]
2394    async fn test_payment_method_service_update_disabled_keeps_omitted_flag() {
2395        let server = create_test_rpc_server().await;
2396
2397        let response = PaymentMethodService::update_disabled(
2398            &server,
2399            Request::new(crate::payment_method::UpdateDisabledRequest {
2400                mint_disabled: Some(true),
2401                melt_disabled: None,
2402            }),
2403        )
2404        .await
2405        .unwrap()
2406        .into_inner();
2407
2408        assert!(response.mint_disabled);
2409        assert!(!response.melt_disabled);
2410        let info = server.mint.mint_info().await.unwrap();
2411        assert!(info.nuts.nut04.disabled);
2412        assert!(!info.nuts.nut05.disabled);
2413
2414        let response = PaymentMethodService::update_disabled(
2415            &server,
2416            Request::new(crate::payment_method::UpdateDisabledRequest {
2417                mint_disabled: Some(false),
2418                melt_disabled: Some(true),
2419            }),
2420        )
2421        .await
2422        .unwrap()
2423        .into_inner();
2424
2425        assert!(!response.mint_disabled);
2426        assert!(response.melt_disabled);
2427        let info = server.mint.mint_info().await.unwrap();
2428        assert!(!info.nuts.nut04.disabled);
2429        assert!(info.nuts.nut05.disabled);
2430    }
2431
2432    #[tokio::test]
2433    async fn test_payment_method_service_update_disabled_with_no_flags_changes_nothing() {
2434        let server = create_test_rpc_server().await;
2435
2436        // Set both flags first; on a fresh server every flag is already false
2437        PaymentMethodService::update_disabled(
2438            &server,
2439            Request::new(crate::payment_method::UpdateDisabledRequest {
2440                mint_disabled: Some(true),
2441                melt_disabled: Some(true),
2442            }),
2443        )
2444        .await
2445        .unwrap();
2446
2447        let response = PaymentMethodService::update_disabled(
2448            &server,
2449            Request::new(crate::payment_method::UpdateDisabledRequest {
2450                mint_disabled: None,
2451                melt_disabled: None,
2452            }),
2453        )
2454        .await
2455        .unwrap()
2456        .into_inner();
2457
2458        assert!(response.mint_disabled);
2459        assert!(response.melt_disabled);
2460        let info = server.mint.mint_info().await.unwrap();
2461        assert!(info.nuts.nut04.disabled);
2462        assert!(info.nuts.nut05.disabled);
2463    }
2464
2465    #[tokio::test]
2466    async fn test_payment_method_service_update_melt_method_keeps_omitted_settings() {
2467        let server = create_test_rpc_server().await;
2468
2469        PaymentMethodService::update_melt_method(
2470            &server,
2471            Request::new(crate::payment_method::UpdateMeltMethodRequest {
2472                unit: "sat".to_owned(),
2473                method: "bolt11".to_owned(),
2474                min_amount: Some(2),
2475                max_amount: Some(2_000),
2476                options: Some(crate::payment_method::Bolt11MeltMethodOptions { amountless: true }),
2477                method_name: None,
2478            }),
2479        )
2480        .await
2481        .unwrap();
2482
2483        let response = PaymentMethodService::update_melt_method(
2484            &server,
2485            Request::new(crate::payment_method::UpdateMeltMethodRequest {
2486                unit: "sat".to_owned(),
2487                method: "bolt11".to_owned(),
2488                min_amount: None,
2489                max_amount: None,
2490                options: None,
2491                method_name: Some("Lightning".to_owned()),
2492            }),
2493        )
2494        .await
2495        .unwrap()
2496        .into_inner();
2497
2498        assert_eq!(response.min_amount, Some(2));
2499        assert_eq!(response.max_amount, Some(2_000));
2500        assert_eq!(
2501            response.options,
2502            Some(crate::payment_method::Bolt11MeltMethodOptions { amountless: true })
2503        );
2504        assert_eq!(response.method_name, Some("Lightning".to_owned()));
2505    }
2506
2507    #[tokio::test]
2508    async fn test_legacy_update_nut04_disabled_flag_still_works() {
2509        let server = create_test_rpc_server().await;
2510
2511        server
2512            .update_nut04(Request::new(UpdateNut04Request {
2513                unit: "sat".to_owned(),
2514                method: "bolt11".to_owned(),
2515                disabled: Some(true),
2516                min_amount: Some(5),
2517                max_amount: None,
2518                options: None,
2519                method_name: None,
2520            }))
2521            .await
2522            .unwrap();
2523
2524        let info = server.mint.mint_info().await.unwrap();
2525        assert!(info.nuts.nut04.disabled);
2526        let settings = info
2527            .nuts
2528            .nut04
2529            .get_settings(
2530                &CurrencyUnit::Sat,
2531                &PaymentMethod::Known(KnownMethod::Bolt11),
2532            )
2533            .unwrap();
2534        assert_eq!(settings.min_amount, Some(Amount::from(5)));
2535    }
2536
2537    #[tokio::test]
2538    async fn test_legacy_update_nut05_disabled_flag_still_works() {
2539        let server = create_test_rpc_server().await;
2540
2541        server
2542            .update_nut05(Request::new(UpdateNut05Request {
2543                unit: "sat".to_owned(),
2544                method: "bolt11".to_owned(),
2545                disabled: Some(true),
2546                min_amount: Some(7),
2547                max_amount: Some(700),
2548                options: None,
2549                method_name: None,
2550            }))
2551            .await
2552            .unwrap();
2553
2554        let info = server.mint.mint_info().await.unwrap();
2555        assert!(info.nuts.nut05.disabled);
2556        assert!(!info.nuts.nut04.disabled);
2557        let settings = info
2558            .nuts
2559            .nut05
2560            .get_settings(
2561                &CurrencyUnit::Sat,
2562                &PaymentMethod::Known(KnownMethod::Bolt11),
2563            )
2564            .unwrap();
2565        assert_eq!(settings.min_amount, Some(Amount::from(7)));
2566        assert_eq!(settings.max_amount, Some(Amount::from(700)));
2567    }
2568}