Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions crates/cashu/src/nuts/nut04.rs
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,9 @@ pub struct MintQuoteCustomResponse<Q> {
pub amount_paid: Amount,
/// Amount that has been issued
pub amount_issued: Amount,
/// Unix timestamp indicating when the quote was last updated
#[serde(default)]
pub updated_at: u64,
/// Currency unit
pub unit: Option<CurrencyUnit>,
/// Unix timestamp until the quote is valid
Expand Down Expand Up @@ -433,6 +436,7 @@ impl<Q: ToString> MintQuoteCustomResponse<Q> {
amount: self.amount,
amount_paid: self.amount_paid,
amount_issued: self.amount_issued,
updated_at: self.updated_at,
unit: self.unit.clone(),
expiry: self.expiry,
pubkey: self.pubkey,
Expand All @@ -450,6 +454,7 @@ impl From<MintQuoteCustomResponse<QuoteId>> for MintQuoteCustomResponse<String>
amount: value.amount,
amount_paid: value.amount_paid,
amount_issued: value.amount_issued,
updated_at: value.updated_at,
unit: value.unit,
expiry: value.expiry,
pubkey: value.pubkey,
Expand Down Expand Up @@ -846,6 +851,7 @@ mod tests {
amount: Some(Amount::from(1000)),
amount_paid: Amount::ZERO,
amount_issued: Amount::ZERO,
updated_at: 0,
unit: Some(CurrencyUnit::Sat),
expiry: Some(9999999),
pubkey: None,
Expand All @@ -868,6 +874,7 @@ mod tests {
amount: Some(Amount::from(100)),
amount_paid: Amount::ZERO,
amount_issued: Amount::ZERO,
updated_at: 0,
unit: Some(CurrencyUnit::Sat),
expiry: Some(9999),
pubkey: None,
Expand Down
51 changes: 41 additions & 10 deletions crates/cashu/src/nuts/nut17/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,13 +190,9 @@ where
/// Subscription response
///
/// Note on variant ordering: serde `untagged` deserialization tries variants
/// in declaration order and selects the first that matches. The Onchain
/// variants are declared before the Bolt11/Bolt12 variants because the
/// Onchain response structs use `#[serde(deny_unknown_fields)]`, which makes
/// them reject Bolt11/Bolt12 payloads cleanly. Placing them first ensures
/// onchain payloads are classified correctly without being consumed by the
/// more permissive Bolt12 variant (which carries a superset of Onchain's
/// field names).
/// in declaration order and selects the first that matches. The stricter
/// variants are declared before the more permissive ones so payloads with
/// overlapping field names are classified correctly.
pub enum NotificationPayload<T>
where
T: Clone,
Expand All @@ -213,12 +209,12 @@ where
/// Declared before `MeltQuoteBolt11Response`/`MeltQuoteBolt12Response`
/// for the same reason.
MeltQuoteOnchainResponse(MeltQuoteOnchainResponse<T>),
/// Mint Quote Bolt12 Response
MintQuoteBolt12Response(MintQuoteBolt12Response<T>),
/// Melt Quote Bolt11 Response
MeltQuoteBolt11Response(MeltQuoteBolt11Response<T>),
/// Mint Quote Bolt11 Response
MintQuoteBolt11Response(MintQuoteBolt11Response<T>),
/// Mint Quote Bolt12 Response
MintQuoteBolt12Response(MintQuoteBolt12Response<T>),
/// Melt Quote Bolt12 Response
MeltQuoteBolt12Response(MeltQuoteBolt12Response<T>),
/// Custom Mint Quote Response (method, response)
Expand Down Expand Up @@ -336,7 +332,7 @@ mod tests {
use super::*;
use crate::nuts::nut00::CurrencyUnit;
use crate::nuts::nut01::PublicKey;
use crate::nuts::MeltQuoteState;
use crate::nuts::{MeltQuoteState, MintQuoteState};
use crate::Amount;

#[test]
Expand All @@ -352,6 +348,7 @@ mod tests {
.unwrap(),
amount_paid: Amount::from(100_000),
amount_issued: Amount::from(0),
updated_at: 0,
};
let payload: NotificationPayload<String> =
NotificationPayload::MintQuoteOnchainResponse(resp.clone());
Expand Down Expand Up @@ -384,6 +381,7 @@ mod tests {
.unwrap(),
amount_paid: Amount::from(0),
amount_issued: Amount::from(0),
updated_at: 0,
};
let payload: NotificationPayload<String> =
NotificationPayload::MintQuoteBolt12Response(resp.clone());
Expand All @@ -399,6 +397,39 @@ mod tests {
}
}

#[test]
fn notification_payload_bolt11_mint_with_pubkey_roundtrip() {
let resp: MintQuoteBolt11Response<String> = MintQuoteBolt11Response {
quote: "abc".to_string(),
request: "lnbc...".to_string(),
amount: Some(Amount::from(100_000)),
unit: Some(CurrencyUnit::Sat),
amount_paid: Amount::from(0),
amount_issued: Amount::from(0),
updated_at: 0,
state: MintQuoteState::Unpaid,
expiry: Some(1701704757),
pubkey: Some(
PublicKey::from_hex(
"03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac",
)
.unwrap(),
),
};
let payload: NotificationPayload<String> =
NotificationPayload::MintQuoteBolt11Response(resp.clone());

let encoded = serde_json::to_string(&payload).unwrap();
let decoded: NotificationPayload<String> = serde_json::from_str(&encoded).unwrap();

match decoded {
NotificationPayload::MintQuoteBolt11Response(r) => {
assert_eq!(r, resp);
}
other => panic!("expected MintQuoteBolt11Response, got {:?}", other),
}
}

#[test]
fn notification_payload_onchain_melt_roundtrip() {
let resp: MeltQuoteOnchainResponse<String> = MeltQuoteOnchainResponse {
Expand Down
16 changes: 16 additions & 0 deletions crates/cashu/src/nuts/nut23.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,17 @@ pub struct MintQuoteBolt11Response<Q> {
/// Unit
// REVIEW: This is now required in the spec, we should remove the option once all mints update
pub unit: Option<CurrencyUnit>,
/// Amount that has been paid
#[serde(default)]
pub amount_paid: Amount,
/// Amount that has been issued
#[serde(default)]
pub amount_issued: Amount,
/// Unix timestamp indicating when the quote was last updated
#[serde(default)]
pub updated_at: u64,
/// Quote State
#[serde(default)]
pub state: QuoteState,
/// Unix timestamp until the quote is valid
pub expiry: Option<u64>,
Expand All @@ -116,6 +126,9 @@ impl<Q: ToString> MintQuoteBolt11Response<Q> {
pubkey: self.pubkey,
amount: self.amount,
unit: self.unit.clone(),
amount_paid: self.amount_paid,
amount_issued: self.amount_issued,
updated_at: self.updated_at,
}
}
}
Expand All @@ -131,6 +144,9 @@ impl From<MintQuoteBolt11Response<QuoteId>> for MintQuoteBolt11Response<String>
pubkey: value.pubkey,
amount: value.amount,
unit: value.unit.clone(),
amount_paid: value.amount_paid,
amount_issued: value.amount_issued,
updated_at: value.updated_at,
}
}
}
Expand Down
12 changes: 12 additions & 0 deletions crates/cashu/src/nuts/nut25.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,15 @@ pub struct MintQuoteBolt12Request {
}

/// Mint quote response [NUT-24]
///
/// `deny_unknown_fields` is intentional: the `NotificationPayload` enum is
/// `#[serde(untagged)]` and Bolt11 mint quotes share the same core fields as
/// Bolt12 mint quotes. Rejecting unknown fields lets `NotificationPayload`
/// try the Bolt12 variant before Bolt11 without classifying Bolt11 payloads
/// that carry a `state` field as Bolt12.
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
#[serde(bound = "Q: Serialize + for<'a> Deserialize<'a>")]
#[serde(deny_unknown_fields)]
pub struct MintQuoteBolt12Response<Q> {
/// Quote Id
pub quote: Q,
Expand All @@ -54,6 +61,9 @@ pub struct MintQuoteBolt12Response<Q> {
pub amount_paid: Amount,
/// Amount that has been issued
pub amount_issued: Amount,
/// Unix timestamp indicating when the quote was last updated
#[serde(default)]
pub updated_at: u64,
}

#[cfg(feature = "mint")]
Expand All @@ -69,6 +79,7 @@ impl<Q: ToString> MintQuoteBolt12Response<Q> {
pubkey: self.pubkey,
amount_paid: self.amount_paid,
amount_issued: self.amount_issued,
updated_at: self.updated_at,
}
}
}
Expand All @@ -85,6 +96,7 @@ impl From<MintQuoteBolt12Response<QuoteId>> for MintQuoteBolt12Response<String>
pubkey: value.pubkey,
amount: value.amount,
unit: value.unit,
updated_at: value.updated_at,
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions crates/cashu/src/nuts/nut30.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ pub struct MintQuoteOnchainResponse<Q> {
/// Amount of ecash that has been issued for the given mint quote
#[serde(default)]
pub amount_issued: Amount,
/// Unix timestamp indicating when the quote was last updated
#[serde(default)]
pub updated_at: u64,
}

impl<Q: ToString> MintQuoteOnchainResponse<Q> {
Expand All @@ -65,6 +68,7 @@ impl<Q: ToString> MintQuoteOnchainResponse<Q> {
pubkey: self.pubkey,
amount_paid: self.amount_paid,
amount_issued: self.amount_issued,
updated_at: self.updated_at,
}
}
}
Expand All @@ -80,6 +84,7 @@ impl From<MintQuoteOnchainResponse<QuoteId>> for MintQuoteOnchainResponse<String
pubkey: value.pubkey,
amount_paid: value.amount_paid,
amount_issued: value.amount_issued,
updated_at: value.updated_at,
}
}
}
Expand Down Expand Up @@ -346,6 +351,7 @@ mod tests {
.unwrap(),
amount_paid: Amount::from(100000),
amount_issued: Amount::from(0),
updated_at: 0,
};

let string_id_response = response.to_string_id();
Expand Down
37 changes: 35 additions & 2 deletions crates/cdk-common/src/mint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,16 @@ impl MintQuote {
self.amount_issued.clone()
}

/// Unix timestamp indicating when this quote was last updated.
pub fn updated_at(&self) -> u64 {
self.payments
.iter()
.map(|payment| payment.time)
.chain(self.issuance.iter().map(|issuance| issuance.time))
.max()
.unwrap_or(self.created_time)
}

/// Get state of mint quote
#[instrument(skip(self))]
pub fn state(&self) -> MintQuoteState {
Expand Down Expand Up @@ -1182,6 +1192,7 @@ impl TryFrom<MintQuote> for MintQuoteOnchainResponse<QuoteId> {
pubkey: quote.pubkey.ok_or(crate::error::Error::MissingPubkey)?,
amount_paid: quote.amount_paid().into(),
amount_issued: quote.amount_issued().into(),
updated_at: quote.updated_at(),
})
}
}
Expand Down Expand Up @@ -1239,6 +1250,10 @@ impl From<MintKeySetInfo> for KeySetInfo {

impl From<MintQuote> for MintQuoteBolt11Response<QuoteId> {
fn from(mint_quote: MintQuote) -> MintQuoteBolt11Response<QuoteId> {
let amount_paid = mint_quote.amount_paid().into();
let amount_issued = mint_quote.amount_issued().into();
let updated_at = mint_quote.updated_at();

MintQuoteBolt11Response {
quote: mint_quote.id.clone(),
state: mint_quote.state(),
Expand All @@ -1247,6 +1262,9 @@ impl From<MintQuote> for MintQuoteBolt11Response<QuoteId> {
pubkey: mint_quote.pubkey,
amount: mint_quote.amount.map(Into::into),
unit: Some(mint_quote.unit),
amount_paid,
amount_issued,
updated_at,
}
}
}
Expand All @@ -1262,15 +1280,20 @@ impl TryFrom<MintQuote> for MintQuoteBolt12Response<QuoteId> {
type Error = Error;

fn try_from(mint_quote: MintQuote) -> Result<Self, Self::Error> {
let amount_paid = mint_quote.amount_paid().into();
let amount_issued = mint_quote.amount_issued().into();
let updated_at = mint_quote.updated_at();

Ok(MintQuoteBolt12Response {
quote: mint_quote.id.clone(),
request: mint_quote.request,
expiry: Some(mint_quote.expiry),
amount_paid: mint_quote.amount_paid.into(),
amount_issued: mint_quote.amount_issued.into(),
amount_paid,
amount_issued,
pubkey: mint_quote.pubkey.ok_or(Error::PubkeyRequired)?,
amount: mint_quote.amount.map(Into::into),
unit: mint_quote.unit,
updated_at,
})
}
}
Expand All @@ -1290,6 +1313,7 @@ impl TryFrom<MintQuote> for MintQuoteCustomResponse<QuoteId> {
fn try_from(quote: MintQuote) -> Result<Self, Self::Error> {
let amount_paid = quote.amount_paid().into();
let amount_issued = quote.amount_issued().into();
let updated_at = quote.updated_at();

Ok(MintQuoteCustomResponse {
quote: quote.id,
Expand All @@ -1300,6 +1324,7 @@ impl TryFrom<MintQuote> for MintQuoteCustomResponse<QuoteId> {
amount: quote.amount.map(Into::into),
amount_paid,
amount_issued,
updated_at,
extra: quote.extra_json.unwrap_or_default(),
})
}
Expand Down Expand Up @@ -1348,6 +1373,9 @@ impl TryFrom<MintQuote> for MintQuoteResponse<QuoteId> {
amount: quote.amount.as_ref().map(|a| a.clone().into()),
unit: Some(quote.unit.clone()),
pubkey: quote.pubkey,
amount_paid: quote.amount_paid().into(),
amount_issued: quote.amount_issued().into(),
updated_at: quote.updated_at(),
}))
} else if quote.payment_method.is_bolt12() {
Ok(Self::Bolt12(crate::nuts::nut25::MintQuoteBolt12Response {
Expand All @@ -1359,6 +1387,7 @@ impl TryFrom<MintQuote> for MintQuoteResponse<QuoteId> {
pubkey: quote.pubkey.ok_or(Error::PubkeyRequired)?,
amount_paid: quote.amount_paid().into(),
amount_issued: quote.amount_issued().into(),
updated_at: quote.updated_at(),
}))
} else if quote.payment_method.is_onchain() {
let onchain_response = MintQuoteOnchainResponse::try_from(quote)?;
Expand All @@ -1374,6 +1403,7 @@ impl TryFrom<MintQuote> for MintQuoteResponse<QuoteId> {
amount: quote.amount.as_ref().map(|a| a.clone().into()),
amount_paid: quote.amount_paid().into(),
amount_issued: quote.amount_issued().into(),
updated_at: quote.updated_at(),
unit: Some(quote.unit.clone()),
pubkey: quote.pubkey,
extra: quote.extra_json.clone().unwrap_or_default(),
Expand Down Expand Up @@ -1408,6 +1438,9 @@ impl From<MintQuoteResponse<QuoteId>> for MintQuoteBolt11Response<String> {
pubkey: bolt11_response.pubkey,
amount: bolt11_response.amount,
unit: bolt11_response.unit,
amount_paid: bolt11_response.amount_paid,
amount_issued: bolt11_response.amount_issued,
updated_at: bolt11_response.updated_at,
},
_ => panic!("Expected Bolt11 response"),
}
Expand Down
Loading
Loading