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#[derive(Debug, Error)]
45pub enum Error {
46 #[error(transparent)]
48 Parse(#[from] std::net::AddrParseError),
49 #[error(transparent)]
51 Transport(#[from] tonic::transport::Error),
52 #[error(transparent)]
54 Io(#[from] std::io::Error),
55}
56
57#[derive(Debug, Error)]
59pub enum MintMutationGuardError {
60 #[error("{0}")]
62 FailedPrecondition(String),
63 #[error("{0}")]
65 Internal(String),
66}
67
68#[tonic::async_trait]
70pub trait MintMutationGuard: Send + Sync {
71 async fn check(&self) -> Result<(), MintMutationGuardError>;
73}
74
75#[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 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 pub fn with_mutation_guard(mut self, guard: Arc<dyn MintMutationGuard>) -> Self {
109 self.mutation_guard = Some(guard);
110 self
111 }
112
113 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 pub fn with_wallet_info_provider(mut self, provider: DynWalletInfoProvider) -> Self {
148 self.wallet_info_provider = Some(provider);
149 self
150 }
151
152 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 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 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 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 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 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("e_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 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 let mut mint_quote = tx
428 .get_mint_quote("e_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 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("e_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 #[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 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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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("e_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 let quote = MintQuote::new(
1074 Some(mint_quote.id.clone()), mint_quote.request.clone(), mint_quote.unit.clone(), mint_quote.amount.clone(), mint_quote.expiry, mint_quote.request_lookup_id.clone(), mint_quote.pubkey, mint_quote.amount_issued(), mint_quote.amount_paid(), mint_quote.payment_method.clone(), 0, 0, vec![], vec![], None, );
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("e_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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 async fn amount_paid(server: &MintRPCServer, quote_id: &str) -> u64 {
1876 server
1877 .mint
1878 .localstore()
1879 .get_mint_quote("e_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, "e_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, "e_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, "e_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, "e_id, 32).await;
1954
1955 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, "e_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 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, "e_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, "e_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 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 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 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 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}