Files
cdk/crates/cdk-common/src/mint.rs
thesimplekid ae6c107809 feat: bolt12
2025-07-13 18:48:35 +01:00

475 lines
13 KiB
Rust

//! Mint types
use bitcoin::bip32::DerivationPath;
use cashu::util::unix_time;
use cashu::{
Bolt11Invoice, MeltOptions, MeltQuoteBolt11Response, MintQuoteBolt11Response,
MintQuoteBolt12Response, PaymentMethod,
};
use lightning::offers::offer::Offer;
use serde::{Deserialize, Serialize};
use tracing::instrument;
use uuid::Uuid;
use crate::nuts::{MeltQuoteState, MintQuoteState};
use crate::payment::PaymentIdentifier;
use crate::{Amount, CurrencyUnit, Id, KeySetInfo, PublicKey};
/// Mint Quote Info
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct MintQuote {
/// Quote id
pub id: Uuid,
/// Amount of quote
pub amount: Option<Amount>,
/// Unit of quote
pub unit: CurrencyUnit,
/// Quote payment request e.g. bolt11
pub request: String,
/// Expiration time of quote
pub expiry: u64,
/// Value used by ln backend to look up state of request
pub request_lookup_id: PaymentIdentifier,
/// Pubkey
pub pubkey: Option<PublicKey>,
/// Unix time quote was created
#[serde(default)]
pub created_time: u64,
/// Amount paid
#[serde(default)]
amount_paid: Amount,
/// Amount issued
#[serde(default)]
amount_issued: Amount,
/// Payment of payment(s) that filled quote
#[serde(default)]
pub payments: Vec<IncomingPayment>,
/// Payment Method
#[serde(default)]
pub payment_method: PaymentMethod,
/// Payment of payment(s) that filled quote
#[serde(default)]
pub issuance: Vec<Issuance>,
}
impl MintQuote {
/// Create new [`MintQuote`]
#[allow(clippy::too_many_arguments)]
pub fn new(
id: Option<Uuid>,
request: String,
unit: CurrencyUnit,
amount: Option<Amount>,
expiry: u64,
request_lookup_id: PaymentIdentifier,
pubkey: Option<PublicKey>,
amount_paid: Amount,
amount_issued: Amount,
payment_method: PaymentMethod,
created_time: u64,
payments: Vec<IncomingPayment>,
issuance: Vec<Issuance>,
) -> Self {
let id = id.unwrap_or(Uuid::new_v4());
Self {
id,
amount,
unit,
request,
expiry,
request_lookup_id,
pubkey,
created_time,
amount_paid,
amount_issued,
payment_method,
payments,
issuance,
}
}
/// Increment the amount paid on the mint quote by a given amount
#[instrument(skip(self))]
pub fn increment_amount_paid(
&mut self,
additional_amount: Amount,
) -> Result<Amount, crate::Error> {
self.amount_paid = self
.amount_paid
.checked_add(additional_amount)
.ok_or(crate::Error::AmountOverflow)?;
Ok(self.amount_paid)
}
/// Amount paid
#[instrument(skip(self))]
pub fn amount_paid(&self) -> Amount {
self.amount_paid
}
/// Increment the amount issued on the mint quote by a given amount
#[instrument(skip(self))]
pub fn increment_amount_issued(
&mut self,
additional_amount: Amount,
) -> Result<Amount, crate::Error> {
self.amount_issued = self
.amount_issued
.checked_add(additional_amount)
.ok_or(crate::Error::AmountOverflow)?;
Ok(self.amount_issued)
}
/// Amount issued
#[instrument(skip(self))]
pub fn amount_issued(&self) -> Amount {
self.amount_issued
}
/// Get state of mint quote
#[instrument(skip(self))]
pub fn state(&self) -> MintQuoteState {
self.compute_quote_state()
}
/// Existing payment ids of a mint quote
pub fn payment_ids(&self) -> Vec<&String> {
self.payments.iter().map(|a| &a.payment_id).collect()
}
/// Add a payment ID to the list of payment IDs
///
/// Returns an error if the payment ID is already in the list
#[instrument(skip(self))]
pub fn add_payment(
&mut self,
amount: Amount,
payment_id: String,
time: u64,
) -> Result<(), crate::Error> {
let payment_ids = self.payment_ids();
if payment_ids.contains(&&payment_id) {
return Err(crate::Error::DuplicatePaymentId);
}
let payment = IncomingPayment::new(amount, payment_id, time);
self.payments.push(payment);
Ok(())
}
/// Compute quote state
#[instrument(skip(self))]
fn compute_quote_state(&self) -> MintQuoteState {
if self.amount_paid == Amount::ZERO && self.amount_issued == Amount::ZERO {
return MintQuoteState::Unpaid;
}
match self.amount_paid.cmp(&self.amount_issued) {
std::cmp::Ordering::Less => {
// self.amount_paid is less than other (amount issued)
// Handle case where paid amount is insufficient
tracing::error!("We should not have issued more then has been paid");
MintQuoteState::Issued
}
std::cmp::Ordering::Equal => {
// We do this extra check for backwards compatibility for quotes where amount paid/issed was not tracked
// self.amount_paid equals other (amount issued)
// Handle case where paid amount exactly matches
MintQuoteState::Issued
}
std::cmp::Ordering::Greater => {
// self.amount_paid is greater than other (amount issued)
// Handle case where paid amount exceeds required amount
MintQuoteState::Paid
}
}
}
}
/// Mint Payments
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct IncomingPayment {
/// Amount
pub amount: Amount,
/// Pyament unix time
pub time: u64,
/// Payment id
pub payment_id: String,
}
impl IncomingPayment {
/// New [`IncomingPayment`]
pub fn new(amount: Amount, payment_id: String, time: u64) -> Self {
Self {
payment_id,
time,
amount,
}
}
}
/// Informattion about issued quote
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct Issuance {
/// Amount
pub amount: Amount,
/// Time
pub time: u64,
}
impl Issuance {
/// Create new [`Issuance`]
pub fn new(amount: Amount, time: u64) -> Self {
Self { amount, time }
}
}
/// Melt Quote Info
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct MeltQuote {
/// Quote id
pub id: Uuid,
/// Quote unit
pub unit: CurrencyUnit,
/// Quote amount
pub amount: Amount,
/// Quote Payment request e.g. bolt11
pub request: MeltPaymentRequest,
/// Quote fee reserve
pub fee_reserve: Amount,
/// Quote state
pub state: MeltQuoteState,
/// Expiration time of quote
pub expiry: u64,
/// Payment preimage
pub payment_preimage: Option<String>,
/// Value used by ln backend to look up state of request
pub request_lookup_id: PaymentIdentifier,
/// Payment options
///
/// Used for amountless invoices and MPP payments
pub options: Option<MeltOptions>,
/// Unix time quote was created
#[serde(default)]
pub created_time: u64,
/// Unix time quote was paid
pub paid_time: Option<u64>,
/// Payment method
#[serde(default)]
pub payment_method: PaymentMethod,
}
impl MeltQuote {
/// Create new [`MeltQuote`]
#[allow(clippy::too_many_arguments)]
pub fn new(
request: MeltPaymentRequest,
unit: CurrencyUnit,
amount: Amount,
fee_reserve: Amount,
expiry: u64,
request_lookup_id: PaymentIdentifier,
options: Option<MeltOptions>,
payment_method: PaymentMethod,
) -> Self {
let id = Uuid::new_v4();
Self {
id,
amount,
unit,
request,
fee_reserve,
state: MeltQuoteState::Unpaid,
expiry,
payment_preimage: None,
request_lookup_id,
options,
created_time: unix_time(),
paid_time: None,
payment_method,
}
}
}
/// Mint Keyset Info
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct MintKeySetInfo {
/// Keyset [`Id`]
pub id: Id,
/// Keyset [`CurrencyUnit`]
pub unit: CurrencyUnit,
/// Keyset active or inactive
/// Mint will only issue new signatures on active keysets
pub active: bool,
/// Starting unix time Keyset is valid from
pub valid_from: u64,
/// [`DerivationPath`] keyset
pub derivation_path: DerivationPath,
/// DerivationPath index of Keyset
pub derivation_path_index: Option<u32>,
/// Max order of keyset
pub max_order: u8,
/// Input Fee ppk
#[serde(default = "default_fee")]
pub input_fee_ppk: u64,
/// Final expiry
pub final_expiry: Option<u64>,
}
/// Default fee
pub fn default_fee() -> u64 {
0
}
impl From<MintKeySetInfo> for KeySetInfo {
fn from(keyset_info: MintKeySetInfo) -> Self {
Self {
id: keyset_info.id,
unit: keyset_info.unit,
active: keyset_info.active,
input_fee_ppk: keyset_info.input_fee_ppk,
final_expiry: keyset_info.final_expiry,
}
}
}
impl From<MintQuote> for MintQuoteBolt11Response<Uuid> {
fn from(mint_quote: crate::mint::MintQuote) -> MintQuoteBolt11Response<Uuid> {
MintQuoteBolt11Response {
quote: mint_quote.id,
state: mint_quote.state(),
request: mint_quote.request,
expiry: Some(mint_quote.expiry),
pubkey: mint_quote.pubkey,
amount: mint_quote.amount,
unit: Some(mint_quote.unit.clone()),
}
}
}
impl From<MintQuote> for MintQuoteBolt11Response<String> {
fn from(quote: MintQuote) -> Self {
let quote: MintQuoteBolt11Response<Uuid> = quote.into();
quote.into()
}
}
impl TryFrom<crate::mint::MintQuote> for MintQuoteBolt12Response<Uuid> {
type Error = crate::Error;
fn try_from(mint_quote: crate::mint::MintQuote) -> Result<Self, Self::Error> {
Ok(MintQuoteBolt12Response {
quote: mint_quote.id,
request: mint_quote.request,
expiry: Some(mint_quote.expiry),
amount_paid: mint_quote.amount_paid,
amount_issued: mint_quote.amount_issued,
pubkey: mint_quote.pubkey.ok_or(crate::Error::PubkeyRequired)?,
amount: mint_quote.amount,
unit: mint_quote.unit,
})
}
}
impl TryFrom<MintQuote> for MintQuoteBolt12Response<String> {
type Error = crate::Error;
fn try_from(quote: MintQuote) -> Result<Self, Self::Error> {
let quote: MintQuoteBolt12Response<Uuid> = quote.try_into()?;
Ok(quote.into())
}
}
impl From<&MeltQuote> for MeltQuoteBolt11Response<Uuid> {
fn from(melt_quote: &MeltQuote) -> MeltQuoteBolt11Response<Uuid> {
MeltQuoteBolt11Response {
quote: melt_quote.id,
payment_preimage: None,
change: None,
state: melt_quote.state,
paid: Some(melt_quote.state == MeltQuoteState::Paid),
expiry: melt_quote.expiry,
amount: melt_quote.amount,
fee_reserve: melt_quote.fee_reserve,
request: None,
unit: Some(melt_quote.unit.clone()),
}
}
}
impl From<MeltQuote> for MeltQuoteBolt11Response<Uuid> {
fn from(melt_quote: MeltQuote) -> MeltQuoteBolt11Response<Uuid> {
let paid = melt_quote.state == MeltQuoteState::Paid;
MeltQuoteBolt11Response {
quote: melt_quote.id,
amount: melt_quote.amount,
fee_reserve: melt_quote.fee_reserve,
paid: Some(paid),
state: melt_quote.state,
expiry: melt_quote.expiry,
payment_preimage: melt_quote.payment_preimage,
change: None,
request: Some(melt_quote.request.to_string()),
unit: Some(melt_quote.unit.clone()),
}
}
}
/// Payment request
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub enum MeltPaymentRequest {
/// Bolt11 Payment
Bolt11 {
/// Bolt11 invoice
bolt11: Bolt11Invoice,
},
/// Bolt12 Payment
Bolt12 {
/// Offer
#[serde(with = "offer_serde")]
offer: Box<Offer>,
/// Invoice
invoice: Option<Vec<u8>>,
},
}
impl std::fmt::Display for MeltPaymentRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MeltPaymentRequest::Bolt11 { bolt11 } => write!(f, "{bolt11}"),
MeltPaymentRequest::Bolt12 { offer, invoice: _ } => write!(f, "{offer}"),
}
}
}
mod offer_serde {
use std::str::FromStr;
use serde::{self, Deserialize, Deserializer, Serializer};
use super::Offer;
pub fn serialize<S>(offer: &Offer, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let s = offer.to_string();
serializer.serialize_str(&s)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Box<Offer>, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(Box::new(Offer::from_str(&s).map_err(|_| {
serde::de::Error::custom("Invalid Bolt12 Offer")
})?))
}
}