use std::str::FromStr;
use cashu::nut00::KnownMethod;
use cashu::PaymentMethod;
use cdk_common::mint_url::MintUrl;
use cdk_common::nuts::{CurrencyUnit, MintQuoteState};
use cdk_common::wallet::MintQuote;
use cdk_common::Amount;
use serde::{Deserialize, Serialize};
const DEFAULT_MINT_URL: &str = "http://localhost:3338";
const DEFAULT_QUOTE_EXPIRY_SECS: u64 = 86_400;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Quote {
#[serde(rename = "quoteId")]
pub id: String,
pub amount: u64,
#[serde(default = "default_unit")]
pub unit: String,
#[serde(default)]
pub created_at: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub paid_at: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_at: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mint_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub request: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub locked: Option<bool>,
}
fn default_unit() -> String {
"sat".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QuotesResponse {
pub data: QuotesData,
pub metadata: Metadata,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QuotesData {
pub quotes: Vec<Quote>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Metadata {
pub total: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<usize>,
pub limit: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub since: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UserResponse {
#[serde(default)]
pub error: bool,
pub data: UserDataContainer,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserDataContainer {
pub user: UserData,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UserData {
pub pubkey: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub mint_url: Option<String>,
#[serde(default)]
pub lock_quote: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Nip98Response {
pub data: Nip98Data,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Nip98Data {
pub token: String,
}
impl From<Quote> for MintQuote {
fn from(quote: Quote) -> Self {
let mint_url = quote
.mint_url
.and_then(|url| MintUrl::from_str(&url).ok())
.unwrap_or_else(|| {
MintUrl::from_str(DEFAULT_MINT_URL).expect("default mint URL should be valid")
});
let unit = CurrencyUnit::from_str("e.unit).unwrap_or(CurrencyUnit::Sat);
let state = match quote.state.as_deref() {
Some("PAID") => MintQuoteState::Paid,
Some("ISSUED") => MintQuoteState::Issued,
_ => MintQuoteState::Unpaid,
};
let expiry = quote
.expires_at
.unwrap_or_else(|| quote.created_at.saturating_add(DEFAULT_QUOTE_EXPIRY_SECS));
Self {
id: quote.id,
mint_url,
payment_method: PaymentMethod::Known(KnownMethod::Bolt11),
amount: Some(Amount::from(quote.amount)),
unit,
request: quote.request.unwrap_or_default(),
state,
expiry,
secret_key: None,
amount_issued: Amount::ZERO,
amount_paid: if quote.paid_at.is_some() {
Amount::from(quote.amount)
} else {
Amount::ZERO
},
estimated_blocks: None,
used_by_operation: None,
version: 0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_quote_saturates_default_expiry() {
let quote = Quote {
id: "poison-overflow".to_string(),
amount: 1_000,
unit: "sat".to_string(),
created_at: u64::MAX - 100,
paid_at: None,
expires_at: None,
mint_url: None,
request: None,
state: None,
locked: None,
};
let mint_quote = MintQuote::from(quote);
assert_eq!(mint_quote.expiry, u64::MAX);
}
}