Skip to content

Commit 05fc5fe

Browse files
committed
refactor(cdk): use polymorphic enums for mint and melt quotes
1 parent 5b333b0 commit 05fc5fe

31 files changed

Lines changed: 782 additions & 268 deletions

crates/cashu/examples/payment_request_encoding_benchmark.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,7 @@ mod tests {
502502
amount: Some(Amount::from(1000)),
503503
unit: Some(CurrencyUnit::Sat),
504504
single_use: None,
505-
mints: Some(vec![MintUrl::from_str("https://mint.example.com").unwrap()]),
505+
mints: vec![MintUrl::from_str("https://mint.example.com").unwrap()],
506506
description: Some("Test".to_string()),
507507
transports: vec![],
508508
nut10: None,

crates/cdk-axum/src/custom_handlers.rs

Lines changed: 99 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ use axum::response::{IntoResponse, Response};
1414
use cdk::mint::QuoteId;
1515
use cdk::nuts::nut21::{Method, ProtectedEndpoint, RoutePath};
1616
use cdk::nuts::{
17-
BatchCheckMintQuoteRequest, BatchMintRequest, MeltQuoteBolt11Request, MeltQuoteBolt11Response,
18-
MeltQuoteBolt12Request, MeltQuoteCustomRequest, MintQuoteBolt11Request,
19-
MintQuoteBolt11Response, MintQuoteBolt12Request, MintQuoteBolt12Response,
20-
MintQuoteCustomRequest, MintRequest, MintResponse,
17+
BatchCheckMintQuoteRequest, BatchMintRequest, MeltQuoteBolt11Request, MeltQuoteBolt12Request,
18+
MeltQuoteCustomRequest, MintQuoteBolt11Request, MintQuoteBolt11Response,
19+
MintQuoteBolt12Request, MintQuoteBolt12Response, MintQuoteCustomRequest, MintRequest,
20+
MintResponse,
2121
};
22+
use cdk::{MeltQuoteCreateResponse, MeltQuoteResponse};
2223
use serde_json::Value;
2324
use tracing::instrument;
2425

@@ -68,6 +69,46 @@ where
6869
})
6970
}
7071
}
72+
73+
/// Serialize a `MeltQuoteResponse` into an HTTP JSON response that carries the
74+
/// per-variant payload shape (bolt11, bolt12, or custom) rather than the tag.
75+
fn melt_quote_response_to_json(response: MeltQuoteResponse<QuoteId>) -> Response {
76+
match response {
77+
MeltQuoteResponse::Bolt11(r) => Json(r).into_response(),
78+
MeltQuoteResponse::Bolt12(r) => Json(r).into_response(),
79+
MeltQuoteResponse::Custom((_, r)) => Json(r).into_response(),
80+
}
81+
}
82+
83+
/// Serialize a `MeltQuoteCreateResponse` into an HTTP JSON response that
84+
/// carries the per-variant payload shape (bolt11, bolt12, or custom) rather
85+
/// than the tag.
86+
fn melt_quote_create_response_to_json(response: MeltQuoteCreateResponse<QuoteId>) -> Response {
87+
match response {
88+
MeltQuoteCreateResponse::Bolt11(r) => Json(r).into_response(),
89+
MeltQuoteCreateResponse::Bolt12(r) => Json(r).into_response(),
90+
MeltQuoteCreateResponse::Custom((_, r)) => Json(r).into_response(),
91+
}
92+
}
93+
94+
async fn validate_melt_quote_method(
95+
state: &MintState,
96+
method: &str,
97+
quote_id: &QuoteId,
98+
) -> Result<(), cdk::Error> {
99+
let quote_method = state
100+
.mint
101+
.get_melt_quote_method(quote_id)
102+
.await?
103+
.to_string();
104+
105+
if quote_method != method {
106+
return Err(cdk::Error::InvalidPaymentMethod);
107+
}
108+
109+
Ok(())
110+
}
111+
71112
/// Generic handler for custom payment method mint quotes
72113
///
73114
/// This handler works for ANY custom payment method (e.g., paypal, venmo, cashapp, bolt11, bolt12).
@@ -108,6 +149,9 @@ pub async fn post_mint_custom_quote(
108149
Ok(Json(response).into_response())
109150
}
110151
"bolt12" => {
152+
if payload.get("pubkey").is_none_or(|v| v.is_null()) {
153+
return Err(into_response(cdk::Error::PubkeyRequired));
154+
}
111155
let bolt12_request: MintQuoteBolt12Request =
112156
serde_json::from_value(payload).map_err(|e| {
113157
tracing::error!("Failed to parse bolt12 request: {}", e);
@@ -132,7 +176,7 @@ pub async fn post_mint_custom_quote(
132176
})?;
133177

134178
let quote_request = cdk::mint::MintQuoteRequest::Custom {
135-
method: cdk::nuts::PaymentMethod::Custom(method.clone()),
179+
method: cdk::nuts::PaymentMethod::from(method.clone()),
136180
request: custom_request,
137181
};
138182

@@ -251,10 +295,7 @@ pub async fn post_batch_check_mint_quote(
251295
let responses: Vec<cdk::nuts::MintQuoteCustomResponse<QuoteId>> = responses
252296
.into_iter()
253297
.map(|r| match r {
254-
cdk::mint::MintQuoteResponse::Custom {
255-
method: _,
256-
response,
257-
} => Ok(response),
298+
cdk::mint::MintQuoteResponse::Custom { response, .. } => Ok(response),
258299
_ => Err(cdk::Error::InvalidPaymentMethod),
259300
})
260301
.collect::<Result<Vec<_>, _>>()
@@ -323,7 +364,7 @@ pub async fn post_melt_custom_quote(
323364
State(state): State<MintState>,
324365
Path(method): Path<String>,
325366
Json(payload): Json<Value>,
326-
) -> Result<Json<MeltQuoteBolt11Response<QuoteId>>, Response> {
367+
) -> Result<Response, Response> {
327368
state
328369
.mint
329370
.verify_auth(
@@ -375,7 +416,7 @@ pub async fn post_melt_custom_quote(
375416
}
376417
};
377418

378-
Ok(Json(response))
419+
Ok(melt_quote_create_response_to_json(response))
379420
}
380421

381422
/// Get custom payment method melt quote status
@@ -384,7 +425,7 @@ pub async fn get_check_melt_custom_quote(
384425
auth: AuthHeader,
385426
State(state): State<MintState>,
386427
Path((method, quote_id)): Path<(String, QuoteId)>,
387-
) -> Result<Json<MeltQuoteBolt11Response<QuoteId>>, Response> {
428+
) -> Result<Response, Response> {
388429
state
389430
.mint
390431
.verify_auth(
@@ -394,49 +435,66 @@ pub async fn get_check_melt_custom_quote(
394435
.await
395436
.map_err(into_response)?;
396437

397-
// Note: check_melt_quote returns the response directly
398-
// The payment method validation is done when the quote was created
438+
validate_melt_quote_method(&state, &method, &quote_id)
439+
.await
440+
.map_err(into_response)?;
441+
399442
let quote = state
400443
.mint
401444
.check_melt_quote(&quote_id)
402445
.await
403446
.map_err(into_response)?;
404447

405-
Ok(Json(quote))
448+
Ok(melt_quote_response_to_json(quote))
406449
}
407450

408-
/// Melt tokens with custom payment method
409-
#[instrument(skip_all, fields(method = ?method))]
410-
pub async fn post_melt_custom(
451+
async fn post_melt_custom_internal(
411452
auth: AuthHeader,
412453
prefer: PreferHeader,
413-
State(state): State<MintState>,
414-
Path(method): Path<String>,
415-
Json(payload): Json<cdk::nuts::MeltRequest<QuoteId>>,
416-
) -> Result<Json<MeltQuoteBolt11Response<QuoteId>>, Response> {
454+
state: MintState,
455+
method: String,
456+
payload: cdk::nuts::MeltRequest<QuoteId>,
457+
) -> Result<MeltQuoteResponse<QuoteId>, cdk::Error> {
417458
state
418459
.mint
419460
.verify_auth(
420461
auth.into(),
421462
&ProtectedEndpoint::new(Method::Post, RoutePath::Melt(method.clone())),
422463
)
423-
.await
424-
.map_err(into_response)?;
464+
.await?;
465+
466+
validate_melt_quote_method(&state, &method, payload.quote()).await?;
425467

426468
// Check for async preference in either the Prefer header or the request body
427469
let respond_async = prefer.respond_async || payload.is_prefer_async();
428470

429-
let pending = state.mint.melt(&payload).await.map_err(into_response)?;
471+
let pending = state.mint.melt(&payload).await?;
430472

431473
let res = if respond_async {
432474
// Asynchronous processing - return immediately after setup
433475
pending.into_pending_response()
434476
} else {
435477
// Synchronous processing - wait for completion
436-
pending.await.map_err(into_response)?
478+
pending.await?
437479
};
438480

439-
Ok(Json(res))
481+
Ok(res)
482+
}
483+
484+
/// Melt tokens with custom payment method
485+
#[instrument(skip_all, fields(method = ?method))]
486+
pub async fn post_melt_custom(
487+
auth: AuthHeader,
488+
prefer: PreferHeader,
489+
State(state): State<MintState>,
490+
Path(method): Path<String>,
491+
Json(payload): Json<cdk::nuts::MeltRequest<QuoteId>>,
492+
) -> Result<Response, Response> {
493+
let res = post_melt_custom_internal(auth, prefer, state, method, payload)
494+
.await
495+
.map_err(into_response)?;
496+
497+
Ok(melt_quote_response_to_json(res))
440498
}
441499

442500
// ============================================================================
@@ -484,7 +542,7 @@ pub async fn cache_post_melt_custom(
484542
state: State<MintState>,
485543
method: Path<String>,
486544
payload: Json<cdk::nuts::MeltRequest<QuoteId>>,
487-
) -> Result<Json<MeltQuoteBolt11Response<QuoteId>>, Response> {
545+
) -> Result<Response, Response> {
488546
use std::ops::Deref;
489547

490548
let State(mint_state) = state.clone();
@@ -500,18 +558,25 @@ pub async fn cache_post_melt_custom(
500558

501559
if let Some(cached_response) = mint_state
502560
.cache
503-
.get::<MeltQuoteBolt11Response<QuoteId>>(&cache_key)
561+
.get::<MeltQuoteResponse<QuoteId>>(&cache_key)
504562
.await
505563
{
506-
return Ok(Json(cached_response));
564+
return Ok(melt_quote_response_to_json(cached_response));
507565
}
508566

509-
let result = post_melt_custom(auth, prefer, state, method, payload).await?;
567+
let result = post_melt_custom_internal(
568+
auth,
569+
prefer,
570+
mint_state.clone(),
571+
method.clone(),
572+
json_extracted_payload.clone(),
573+
)
574+
.await
575+
.map_err(into_response)?;
510576

511-
// Cache the response
512-
mint_state.cache.set(cache_key, result.deref()).await;
577+
mint_state.cache.set(cache_key, &result).await;
513578

514-
Ok(result)
579+
Ok(melt_quote_response_to_json(result))
515580
}
516581

517582
/// Cached version of post_batch_mint for NUT-19 caching support

crates/cdk-common/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ pub use cdk_http_client::{
5757
// Re-export common types
5858
pub use common::FinalizedMelt;
5959
pub use error::Error;
60-
pub use melt::{MeltQuoteRequest, MeltQuoteResponse};
60+
pub use melt::{MeltQuoteCreateResponse, MeltQuoteRequest, MeltQuoteResponse};
6161
pub use mint_quote::{MintQuoteRequest, MintQuoteResponse};
6262
/// Re-export parking_lot for reuse
6363
pub use parking_lot;

0 commit comments

Comments
 (0)