use axum::extract::{FromRequestParts, Json, Path, State};
use axum::http::request::Parts;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use cdk::mint::QuoteId;
use cdk::nuts::nut21::{Method, ProtectedEndpoint, RoutePath};
use cdk::nuts::{
BatchCheckMintQuoteRequest, BatchMintRequest, MeltQuoteBolt11Request, MeltQuoteBolt11Response,
MeltQuoteBolt12Request, MeltQuoteCustomRequest, MintQuoteBolt11Request,
MintQuoteBolt11Response, MintQuoteBolt12Request, MintQuoteBolt12Response,
MintQuoteCustomRequest, MintRequest, MintResponse,
};
use serde_json::Value;
use tracing::instrument;
use crate::auth::AuthHeader;
use crate::router_handlers::into_response;
use crate::MintState;
const PREFER_HEADER_KEY: &str = "Prefer";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PreferHeader {
pub respond_async: bool,
}
impl<S> FromRequestParts<S> for PreferHeader
where
S: Send + Sync,
{
type Rejection = (StatusCode, String);
async fn from_request_parts(
parts: &mut Parts,
_state: &S,
) -> anyhow::Result<Self, Self::Rejection> {
if let Some(prefer_value) = parts.headers.get(PREFER_HEADER_KEY) {
let value = prefer_value.to_str().map_err(|_| {
(
StatusCode::BAD_REQUEST,
"Invalid Prefer header value".to_string(),
)
})?;
let respond_async = value.to_lowercase().contains("respond-async");
return Ok(PreferHeader { respond_async });
}
Ok(PreferHeader {
respond_async: false,
})
}
}
#[instrument(skip_all, fields(method = ?method))]
pub async fn post_mint_custom_quote(
auth: AuthHeader,
State(state): State<MintState>,
Path(method): Path<String>,
Json(payload): Json<Value>,
) -> Result<Response, Response> {
state
.mint
.verify_auth(
auth.into(),
&ProtectedEndpoint::new(Method::Post, RoutePath::MintQuote(method.clone())),
)
.await
.map_err(into_response)?;
match method.as_str() {
"bolt11" => {
let bolt11_request: MintQuoteBolt11Request =
serde_json::from_value(payload).map_err(|e| {
tracing::error!("Failed to parse bolt11 request: {}", e);
into_response(cdk::Error::InvalidPaymentMethod)
})?;
let quote = state
.mint
.get_mint_quote(bolt11_request.into())
.await
.map_err(into_response)?;
let response: MintQuoteBolt11Response<QuoteId> =
quote.try_into().map_err(into_response)?;
Ok(Json(response).into_response())
}
"bolt12" => {
let bolt12_request: MintQuoteBolt12Request =
serde_json::from_value(payload).map_err(|e| {
tracing::error!("Failed to parse bolt12 request: {}", e);
into_response(cdk::Error::InvalidPaymentMethod)
})?;
let quote = state
.mint
.get_mint_quote(bolt12_request.into())
.await
.map_err(into_response)?;
let response: MintQuoteBolt12Response<QuoteId> =
quote.try_into().map_err(into_response)?;
Ok(Json(response).into_response())
}
_ => {
let custom_request: MintQuoteCustomRequest =
serde_json::from_value(payload).map_err(|e| {
tracing::error!("Failed to parse custom request: {}", e);
into_response(cdk::Error::InvalidPaymentMethod)
})?;
let quote_request = cdk::mint::MintQuoteRequest::Custom {
method: method.clone(),
request: custom_request,
};
let response = state
.mint
.get_mint_quote(quote_request)
.await
.map_err(into_response)?;
match response {
cdk::mint::MintQuoteResponse::Custom { response, .. } => {
Ok(Json(response).into_response())
}
_ => Err(into_response(cdk::Error::InvalidPaymentMethod)),
}
}
}
}
#[instrument(skip_all, fields(method = ?method, quote_id = ?quote_id))]
pub async fn get_check_mint_custom_quote(
auth: AuthHeader,
State(state): State<MintState>,
Path((method, quote_id)): Path<(String, QuoteId)>,
) -> Result<Response, Response> {
state
.mint
.verify_auth(
auth.into(),
&ProtectedEndpoint::new(Method::Get, RoutePath::MintQuote(method.clone())),
)
.await
.map_err(into_response)?;
let quote_response = state
.mint
.check_mint_quotes(&[quote_id])
.await
.map_err(into_response)?
.first()
.cloned()
.ok_or(cdk::Error::UnknownQuote)
.map_err(into_response)?;
match method.as_str() {
"bolt11" => {
let response: MintQuoteBolt11Response<QuoteId> =
quote_response.try_into().map_err(into_response)?;
Ok(Json(response).into_response())
}
"bolt12" => {
let response: MintQuoteBolt12Response<QuoteId> =
quote_response.try_into().map_err(into_response)?;
Ok(Json(response).into_response())
}
_ => {
match quote_response {
cdk::mint::MintQuoteResponse::Custom {
method: quote_method,
response,
} => {
if quote_method != method.clone() {
return Err(into_response(cdk::Error::InvalidPaymentMethod));
}
Ok(Json(response).into_response())
}
_ => Err(into_response(cdk::Error::InvalidPaymentMethod)),
}
}
}
}
#[instrument(skip_all, fields(method = ?method))]
pub async fn post_batch_check_mint_quote(
auth: AuthHeader,
State(state): State<MintState>,
Path(method): Path<String>,
Json(payload): Json<BatchCheckMintQuoteRequest<QuoteId>>,
) -> Result<Response, Response> {
state
.mint
.verify_auth(
auth.into(),
&ProtectedEndpoint::new(Method::Post, RoutePath::MintQuote(method.clone())),
)
.await
.map_err(into_response)?;
let responses = state
.mint
.check_mint_quotes(&payload.quotes)
.await
.map_err(into_response)?;
match method.as_str() {
"bolt11" => {
let responses: Vec<MintQuoteBolt11Response<QuoteId>> = responses
.into_iter()
.map(|r| r.try_into())
.collect::<Result<Vec<_>, _>>()
.map_err(into_response)?;
Ok(Json(responses).into_response())
}
"bolt12" => {
let responses: Vec<MintQuoteBolt12Response<QuoteId>> = responses
.into_iter()
.map(|r| r.try_into())
.collect::<Result<Vec<_>, _>>()
.map_err(into_response)?;
Ok(Json(responses).into_response())
}
_ => {
let responses: Vec<cdk::nuts::MintQuoteCustomResponse<QuoteId>> = responses
.into_iter()
.map(|r| match r {
cdk::mint::MintQuoteResponse::Custom {
method: _,
response,
} => Ok(response),
_ => Err(cdk::Error::InvalidPaymentMethod),
})
.collect::<Result<Vec<_>, _>>()
.map_err(into_response)?;
Ok(Json(responses).into_response())
}
}
}
#[instrument(skip_all, fields(method = ?method, quote_id = ?payload.quote))]
pub async fn post_mint_custom(
auth: AuthHeader,
State(state): State<MintState>,
Path(method): Path<String>,
Json(payload): Json<MintRequest<QuoteId>>,
) -> Result<Json<MintResponse>, Response> {
state
.mint
.verify_auth(
auth.into(),
&ProtectedEndpoint::new(Method::Post, RoutePath::Mint(method.clone())),
)
.await
.map_err(into_response)?;
let res = state
.mint
.process_mint_request(cdk::mint::MintInput::Single(payload))
.await
.map_err(into_response)?;
Ok(Json(res))
}
#[instrument(skip_all, fields(method = ?method))]
pub async fn post_batch_mint(
auth: AuthHeader,
State(state): State<MintState>,
Path(method): Path<String>,
Json(payload): Json<BatchMintRequest<QuoteId>>,
) -> Result<Json<MintResponse>, Response> {
state
.mint
.verify_auth(
auth.into(),
&ProtectedEndpoint::new(Method::Post, RoutePath::Mint(method.clone())),
)
.await
.map_err(into_response)?;
let res = state
.mint
.process_mint_request(cdk::mint::MintInput::Batch(payload))
.await
.map_err(into_response)?;
Ok(Json(res))
}
#[instrument(skip_all, fields(method = ?method))]
pub async fn post_melt_custom_quote(
auth: AuthHeader,
State(state): State<MintState>,
Path(method): Path<String>,
Json(payload): Json<Value>,
) -> Result<Json<MeltQuoteBolt11Response<QuoteId>>, Response> {
state
.mint
.verify_auth(
auth.into(),
&ProtectedEndpoint::new(Method::Post, RoutePath::MeltQuote(method.clone())),
)
.await
.map_err(into_response)?;
let response = match method.as_str() {
"bolt11" => {
let bolt11_request: MeltQuoteBolt11Request =
serde_json::from_value(payload).map_err(|e| {
tracing::error!("Failed to parse bolt11 melt request: {}", e);
into_response(cdk::Error::InvalidPaymentMethod)
})?;
state
.mint
.get_melt_quote(bolt11_request.into())
.await
.map_err(into_response)?
}
"bolt12" => {
let bolt12_request: MeltQuoteBolt12Request =
serde_json::from_value(payload).map_err(|e| {
tracing::error!("Failed to parse bolt12 melt request: {}", e);
into_response(cdk::Error::InvalidPaymentMethod)
})?;
state
.mint
.get_melt_quote(bolt12_request.into())
.await
.map_err(into_response)?
}
_ => {
let custom_request: MeltQuoteCustomRequest =
serde_json::from_value(payload).map_err(|e| {
tracing::error!("Failed to parse custom melt request: {}", e);
into_response(cdk::Error::InvalidPaymentMethod)
})?;
state
.mint
.get_melt_quote(custom_request.into())
.await
.map_err(into_response)?
}
};
Ok(Json(response))
}
#[instrument(skip_all, fields(method = ?method, quote_id = ?quote_id))]
pub async fn get_check_melt_custom_quote(
auth: AuthHeader,
State(state): State<MintState>,
Path((method, quote_id)): Path<(String, QuoteId)>,
) -> Result<Json<MeltQuoteBolt11Response<QuoteId>>, Response> {
state
.mint
.verify_auth(
auth.into(),
&ProtectedEndpoint::new(Method::Get, RoutePath::MeltQuote(method.clone())),
)
.await
.map_err(into_response)?;
let quote = state
.mint
.check_melt_quote("e_id)
.await
.map_err(into_response)?;
Ok(Json(quote))
}
#[instrument(skip_all, fields(method = ?method))]
pub async fn post_melt_custom(
auth: AuthHeader,
prefer: PreferHeader,
State(state): State<MintState>,
Path(method): Path<String>,
Json(payload): Json<cdk::nuts::MeltRequest<QuoteId>>,
) -> Result<Json<MeltQuoteBolt11Response<QuoteId>>, Response> {
state
.mint
.verify_auth(
auth.into(),
&ProtectedEndpoint::new(Method::Post, RoutePath::Melt(method.clone())),
)
.await
.map_err(into_response)?;
let respond_async = prefer.respond_async || payload.is_prefer_async();
let pending = state.mint.melt(&payload).await.map_err(into_response)?;
let res = if respond_async {
pending.into_pending_response()
} else {
pending.await.map_err(into_response)?
};
Ok(Json(res))
}
#[instrument(skip_all, fields(method = ?method, quote_id = ?payload.quote))]
pub async fn cache_post_mint_custom(
auth: AuthHeader,
state: State<MintState>,
method: Path<String>,
payload: Json<MintRequest<QuoteId>>,
) -> Result<Json<MintResponse>, Response> {
use std::ops::Deref;
let State(mint_state) = state.clone();
let json_extracted_payload = payload.deref();
let cache_key = match mint_state.cache.calculate_key(json_extracted_payload) {
Some(key) => key,
None => {
return post_mint_custom(auth, state, method, payload).await;
}
};
if let Some(cached_response) = mint_state.cache.get::<MintResponse>(&cache_key).await {
return Ok(Json(cached_response));
}
let result = post_mint_custom(auth, state, method, payload).await?;
mint_state.cache.set(cache_key, result.deref()).await;
Ok(result)
}
#[instrument(skip_all, fields(method = ?method))]
pub async fn cache_post_melt_custom(
auth: AuthHeader,
prefer: PreferHeader,
state: State<MintState>,
method: Path<String>,
payload: Json<cdk::nuts::MeltRequest<QuoteId>>,
) -> Result<Json<MeltQuoteBolt11Response<QuoteId>>, Response> {
use std::ops::Deref;
let State(mint_state) = state.clone();
let json_extracted_payload = payload.deref();
let cache_key = match mint_state.cache.calculate_key(json_extracted_payload) {
Some(key) => key,
None => {
return post_melt_custom(auth, prefer, state, method, payload).await;
}
};
if let Some(cached_response) = mint_state
.cache
.get::<MeltQuoteBolt11Response<QuoteId>>(&cache_key)
.await
{
return Ok(Json(cached_response));
}
let result = post_melt_custom(auth, prefer, state, method, payload).await?;
mint_state.cache.set(cache_key, result.deref()).await;
Ok(result)
}
#[instrument(skip_all, fields(method = ?method))]
pub async fn cache_post_batch_mint(
auth: AuthHeader,
state: State<MintState>,
method: Path<String>,
payload: Json<BatchMintRequest<QuoteId>>,
) -> Result<Json<MintResponse>, Response> {
use std::ops::Deref;
let State(mint_state) = state.clone();
let json_extracted_payload = payload.deref();
let cache_key = match mint_state.cache.calculate_key(json_extracted_payload) {
Some(key) => key,
None => {
return post_batch_mint(auth, state, method, payload).await;
}
};
if let Some(cached_response) = mint_state.cache.get::<MintResponse>(&cache_key).await {
return Ok(Json(cached_response));
}
let result = post_batch_mint(auth, state, method, payload).await?;
mint_state.cache.set(cache_key, result.deref()).await;
Ok(result)
}
#[cfg(test)]
mod tests {
use axum::http::{HeaderValue, Request, StatusCode};
use super::*;
fn create_test_request(prefer_header: Option<&str>) -> Request<()> {
let mut req = Request::builder()
.method("POST")
.uri("/test")
.body(())
.unwrap();
if let Some(header_value) = prefer_header {
req.headers_mut().insert(
PREFER_HEADER_KEY,
HeaderValue::from_str(header_value).unwrap(),
);
}
req
}
fn create_test_request_with_bytes(bytes: &[u8]) -> Request<()> {
let mut req = Request::builder()
.method("POST")
.uri("/test")
.body(())
.unwrap();
req.headers_mut()
.insert(PREFER_HEADER_KEY, HeaderValue::from_bytes(bytes).unwrap());
req
}
#[tokio::test]
async fn test_prefer_header_respond_async() {
let req = create_test_request(Some("respond-async"));
let (mut parts, _) = req.into_parts();
let result = PreferHeader::from_request_parts(&mut parts, &()).await;
assert!(result.is_ok());
assert!(result.unwrap().respond_async);
}
#[tokio::test]
async fn test_prefer_header_respond_async_with_other_values() {
let req = create_test_request(Some("respond-async; wait=10"));
let (mut parts, _) = req.into_parts();
let result = PreferHeader::from_request_parts(&mut parts, &()).await;
assert!(result.is_ok());
assert!(result.unwrap().respond_async);
}
#[tokio::test]
async fn test_prefer_header_case_insensitive() {
let req = create_test_request(Some("RESPOND-ASYNC"));
let (mut parts, _) = req.into_parts();
let result = PreferHeader::from_request_parts(&mut parts, &()).await;
assert!(result.is_ok());
assert!(result.unwrap().respond_async);
}
#[tokio::test]
async fn test_prefer_header_no_respond_async() {
let req = create_test_request(Some("wait=10"));
let (mut parts, _) = req.into_parts();
let result = PreferHeader::from_request_parts(&mut parts, &()).await;
assert!(result.is_ok());
assert!(!result.unwrap().respond_async);
}
#[tokio::test]
async fn test_prefer_header_missing() {
let req = create_test_request(None);
let (mut parts, _) = req.into_parts();
let result = PreferHeader::from_request_parts(&mut parts, &()).await;
assert!(result.is_ok());
assert!(!result.unwrap().respond_async);
}
#[tokio::test]
async fn test_prefer_header_invalid_value() {
let req = create_test_request_with_bytes(&[0xFF, 0xFE]);
let (mut parts, _) = req.into_parts();
let result = PreferHeader::from_request_parts(&mut parts, &()).await;
assert!(result.is_err());
let (status, message) = result.unwrap_err();
assert_eq!(status, StatusCode::BAD_REQUEST);
assert_eq!(message, "Invalid Prefer header value");
}
#[tokio::test]
async fn test_prefer_header_empty_value() {
let req = create_test_request(Some(""));
let (mut parts, _) = req.into_parts();
let result = PreferHeader::from_request_parts(&mut parts, &()).await;
assert!(result.is_ok());
assert!(!result.unwrap().respond_async);
}
}