refactor: nut04 and nut05 (#749)

This commit is contained in:
thesimplekid
2025-05-19 09:49:11 +01:00
committed by GitHub
parent fc2b0b3ea2
commit b63dc1045d
40 changed files with 1133 additions and 603 deletions

View File

@@ -23,6 +23,7 @@ pub mod nut17;
pub mod nut18;
pub mod nut19;
pub mod nut20;
pub mod nut23;
#[cfg(feature = "auth")]
mod auth;
@@ -45,13 +46,9 @@ pub use nut02::{Id, KeySet, KeySetInfo, KeysetResponse};
#[cfg(feature = "wallet")]
pub use nut03::PreSwap;
pub use nut03::{SwapRequest, SwapResponse};
pub use nut04::{
MintBolt11Request, MintBolt11Response, MintMethodSettings, MintQuoteBolt11Request,
MintQuoteBolt11Response, QuoteState as MintQuoteState, Settings as NUT04Settings,
};
pub use nut04::{MintMethodSettings, MintRequest, MintResponse, Settings as NUT04Settings};
pub use nut05::{
MeltBolt11Request, MeltMethodSettings, MeltOptions, MeltQuoteBolt11Request,
MeltQuoteBolt11Response, QuoteState as MeltQuoteState, Settings as NUT05Settings,
MeltMethodSettings, MeltRequest, QuoteState as MeltQuoteState, Settings as NUT05Settings,
};
pub use nut06::{ContactInfo, MintInfo, MintVersion, Nuts};
pub use nut07::{CheckStateRequest, CheckStateResponse, ProofState, State};
@@ -66,3 +63,7 @@ pub use nut18::{
PaymentRequest, PaymentRequestBuilder, PaymentRequestPayload, Transport, TransportBuilder,
TransportType,
};
pub use nut23::{
MeltOptions, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintQuoteBolt11Request,
MintQuoteBolt11Response, QuoteState as MintQuoteState,
};

View File

@@ -3,16 +3,17 @@
//! <https://github.com/cashubtc/nuts/blob/main/04.md>
use std::fmt;
#[cfg(feature = "mint")]
use std::str::FromStr;
use serde::de::DeserializeOwned;
use serde::de::{self, DeserializeOwned, Deserializer, MapAccess, Visitor};
use serde::ser::{SerializeStruct, Serializer};
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[cfg(feature = "mint")]
use uuid::Uuid;
use super::nut00::{BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod};
use super::{MintQuoteState, PublicKey};
use crate::Amount;
/// NUT04 Error
@@ -26,124 +27,11 @@ pub enum Error {
AmountOverflow,
}
/// Mint quote request [NUT-04]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
pub struct MintQuoteBolt11Request {
/// Amount
pub amount: Amount,
/// Unit wallet would like to pay with
pub unit: CurrencyUnit,
/// Memo to create the invoice with
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// NUT-19 Pubkey
#[serde(skip_serializing_if = "Option::is_none")]
pub pubkey: Option<PublicKey>,
}
/// Possible states of a quote
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = MintQuoteState))]
pub enum QuoteState {
/// Quote has not been paid
#[default]
Unpaid,
/// Quote has been paid and wallet can mint
Paid,
/// Minting is in progress
/// **Note:** This state is to be used internally but is not part of the
/// nut.
Pending,
/// ecash issued for quote
Issued,
}
impl fmt::Display for QuoteState {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Unpaid => write!(f, "UNPAID"),
Self::Paid => write!(f, "PAID"),
Self::Pending => write!(f, "PENDING"),
Self::Issued => write!(f, "ISSUED"),
}
}
}
impl FromStr for QuoteState {
type Err = Error;
fn from_str(state: &str) -> Result<Self, Self::Err> {
match state {
"PENDING" => Ok(Self::Pending),
"PAID" => Ok(Self::Paid),
"UNPAID" => Ok(Self::Unpaid),
"ISSUED" => Ok(Self::Issued),
_ => Err(Error::UnknownState),
}
}
}
/// Mint quote response [NUT-04]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
#[serde(bound = "Q: Serialize + DeserializeOwned")]
pub struct MintQuoteBolt11Response<Q> {
/// Quote Id
pub quote: Q,
/// Payment request to fulfil
pub request: String,
/// Amount
// REVIEW: This is now required in the spec, we should remove the option once all mints update
pub amount: Option<Amount>,
/// Unit
// REVIEW: This is now required in the spec, we should remove the option once all mints update
pub unit: Option<CurrencyUnit>,
/// Quote State
pub state: MintQuoteState,
/// Unix timestamp until the quote is valid
pub expiry: Option<u64>,
/// NUT-19 Pubkey
#[serde(skip_serializing_if = "Option::is_none")]
pub pubkey: Option<PublicKey>,
}
impl<Q: ToString> MintQuoteBolt11Response<Q> {
/// Convert the MintQuote with a quote type Q to a String
pub fn to_string_id(&self) -> MintQuoteBolt11Response<String> {
MintQuoteBolt11Response {
quote: self.quote.to_string(),
request: self.request.clone(),
state: self.state,
expiry: self.expiry,
pubkey: self.pubkey,
amount: self.amount,
unit: self.unit.clone(),
}
}
}
#[cfg(feature = "mint")]
impl From<MintQuoteBolt11Response<Uuid>> for MintQuoteBolt11Response<String> {
fn from(value: MintQuoteBolt11Response<Uuid>) -> Self {
Self {
quote: value.quote.to_string(),
request: value.request,
state: value.state,
expiry: value.expiry,
pubkey: value.pubkey,
amount: value.amount,
unit: value.unit.clone(),
}
}
}
/// Mint request [NUT-04]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
#[serde(bound = "Q: Serialize + DeserializeOwned")]
pub struct MintBolt11Request<Q> {
pub struct MintRequest<Q> {
/// Quote id
#[cfg_attr(feature = "swagger", schema(max_length = 1_000))]
pub quote: Q,
@@ -156,10 +44,10 @@ pub struct MintBolt11Request<Q> {
}
#[cfg(feature = "mint")]
impl TryFrom<MintBolt11Request<String>> for MintBolt11Request<Uuid> {
impl TryFrom<MintRequest<String>> for MintRequest<Uuid> {
type Error = uuid::Error;
fn try_from(value: MintBolt11Request<String>) -> Result<Self, Self::Error> {
fn try_from(value: MintRequest<String>) -> Result<Self, Self::Error> {
Ok(Self {
quote: Uuid::from_str(&value.quote)?,
outputs: value.outputs,
@@ -168,7 +56,7 @@ impl TryFrom<MintBolt11Request<String>> for MintBolt11Request<Uuid> {
}
}
impl<Q> MintBolt11Request<Q> {
impl<Q> MintRequest<Q> {
/// Total [`Amount`] of outputs
pub fn total_amount(&self) -> Result<Amount, Error> {
Amount::try_sum(
@@ -183,13 +71,13 @@ impl<Q> MintBolt11Request<Q> {
/// Mint response [NUT-04]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
pub struct MintBolt11Response {
pub struct MintResponse {
/// Blinded Signatures
pub signatures: Vec<BlindSignature>,
}
/// Mint Method Settings
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
pub struct MintMethodSettings {
/// Payment Method e.g. bolt11
@@ -197,14 +85,168 @@ pub struct MintMethodSettings {
/// Currency Unit e.g. sat
pub unit: CurrencyUnit,
/// Min Amount
#[serde(skip_serializing_if = "Option::is_none")]
pub min_amount: Option<Amount>,
/// Max Amount
#[serde(skip_serializing_if = "Option::is_none")]
pub max_amount: Option<Amount>,
/// Quote Description
#[serde(default)]
pub description: bool,
/// Options
pub options: Option<MintMethodOptions>,
}
impl Serialize for MintMethodSettings {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut num_fields = 3; // method and unit are always present
if self.min_amount.is_some() {
num_fields += 1;
}
if self.max_amount.is_some() {
num_fields += 1;
}
let mut description_in_top_level = false;
if let Some(MintMethodOptions::Bolt11 { description }) = &self.options {
if *description {
num_fields += 1;
description_in_top_level = true;
}
}
let mut state = serializer.serialize_struct("MintMethodSettings", num_fields)?;
state.serialize_field("method", &self.method)?;
state.serialize_field("unit", &self.unit)?;
if let Some(min_amount) = &self.min_amount {
state.serialize_field("min_amount", min_amount)?;
}
if let Some(max_amount) = &self.max_amount {
state.serialize_field("max_amount", max_amount)?;
}
// If there's a description flag in Bolt11 options, add it at the top level
if description_in_top_level {
state.serialize_field("description", &true)?;
}
state.end()
}
}
struct MintMethodSettingsVisitor;
impl<'de> Visitor<'de> for MintMethodSettingsVisitor {
type Value = MintMethodSettings;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a MintMethodSettings structure")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut method: Option<PaymentMethod> = None;
let mut unit: Option<CurrencyUnit> = None;
let mut min_amount: Option<Amount> = None;
let mut max_amount: Option<Amount> = None;
let mut description: Option<bool> = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"method" => {
if method.is_some() {
return Err(de::Error::duplicate_field("method"));
}
method = Some(map.next_value()?);
}
"unit" => {
if unit.is_some() {
return Err(de::Error::duplicate_field("unit"));
}
unit = Some(map.next_value()?);
}
"min_amount" => {
if min_amount.is_some() {
return Err(de::Error::duplicate_field("min_amount"));
}
min_amount = Some(map.next_value()?);
}
"max_amount" => {
if max_amount.is_some() {
return Err(de::Error::duplicate_field("max_amount"));
}
max_amount = Some(map.next_value()?);
}
"description" => {
if description.is_some() {
return Err(de::Error::duplicate_field("description"));
}
description = Some(map.next_value()?);
}
"options" => {
// If there are explicit options, they take precedence, except the description
// field which we will handle specially
let options: Option<MintMethodOptions> = map.next_value()?;
if let Some(MintMethodOptions::Bolt11 {
description: desc_from_options,
}) = options
{
// If we already found a top-level description, use that instead
if description.is_none() {
description = Some(desc_from_options);
}
}
}
_ => {
// Skip unknown fields
let _: serde::de::IgnoredAny = map.next_value()?;
}
}
}
let method = method.ok_or_else(|| de::Error::missing_field("method"))?;
let unit = unit.ok_or_else(|| de::Error::missing_field("unit"))?;
// Create options based on the method and the description flag
let options = if method == PaymentMethod::Bolt11 {
description.map(|description| MintMethodOptions::Bolt11 { description })
} else {
None
};
Ok(MintMethodSettings {
method,
unit,
min_amount,
max_amount,
options,
})
}
}
impl<'de> Deserialize<'de> for MintMethodSettings {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_map(MintMethodSettingsVisitor)
}
}
/// Mint Method settings options
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
#[serde(untagged)]
pub enum MintMethodOptions {
/// Bolt11 Options
Bolt11 {
/// Mint supports setting bolt11 description
description: bool,
},
}
/// Mint Settings
@@ -250,3 +292,73 @@ impl Settings {
.map(|index| self.methods.remove(index))
}
}
#[cfg(test)]
mod tests {
use serde_json::{from_str, json, to_string};
use super::*;
#[test]
fn test_mint_method_settings_top_level_description() {
// Create JSON with top-level description
let json_str = r#"{
"method": "bolt11",
"unit": "sat",
"min_amount": 0,
"max_amount": 10000,
"description": true
}"#;
// Deserialize it
let settings: MintMethodSettings = from_str(json_str).unwrap();
// Check that description was correctly moved to options
assert_eq!(settings.method, PaymentMethod::Bolt11);
assert_eq!(settings.unit, CurrencyUnit::Sat);
assert_eq!(settings.min_amount, Some(Amount::from(0)));
assert_eq!(settings.max_amount, Some(Amount::from(10000)));
match settings.options {
Some(MintMethodOptions::Bolt11 { description }) => {
assert_eq!(description, true);
}
_ => panic!("Expected Bolt11 options with description = true"),
}
// Serialize it back
let serialized = to_string(&settings).unwrap();
let parsed: serde_json::Value = from_str(&serialized).unwrap();
// Verify the description is at the top level
assert_eq!(parsed["description"], json!(true));
}
#[test]
fn test_both_description_locations() {
// Create JSON with description in both places (top level and in options)
let json_str = r#"{
"method": "bolt11",
"unit": "sat",
"min_amount": 0,
"max_amount": 10000,
"description": true,
"options": {
"description": false
}
}"#;
// Deserialize it - top level should take precedence
let settings: MintMethodSettings = from_str(json_str).unwrap();
match settings.options {
Some(MintMethodOptions::Bolt11 { description }) => {
assert_eq!(
description, true,
"Top-level description should take precedence"
);
}
_ => panic!("Expected Bolt11 options with description = true"),
}
}
}

View File

@@ -5,18 +5,16 @@
use std::fmt;
use std::str::FromStr;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use serde::de::{self, DeserializeOwned, Deserializer, MapAccess, Visitor};
use serde::ser::{SerializeStruct, Serializer};
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[cfg(feature = "mint")]
use uuid::Uuid;
use super::nut00::{BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, Proofs};
use super::nut15::Mpp;
use super::nut00::{BlindedMessage, CurrencyUnit, PaymentMethod, Proofs};
use super::ProofsMethods;
use crate::nuts::MeltQuoteState;
use crate::{Amount, Bolt11Invoice};
use crate::Amount;
/// NUT05 Error
#[derive(Debug, Error)]
@@ -27,118 +25,11 @@ pub enum Error {
/// Amount overflow
#[error("Amount Overflow")]
AmountOverflow,
/// Invalid Amount
#[error("Invalid Request")]
InvalidAmountRequest,
/// Unsupported unit
#[error("Unsupported unit")]
UnsupportedUnit,
}
/// Melt quote request [NUT-05]
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
pub struct MeltQuoteBolt11Request {
/// Bolt11 invoice to be paid
#[cfg_attr(feature = "swagger", schema(value_type = String))]
pub request: Bolt11Invoice,
/// Unit wallet would like to pay with
pub unit: CurrencyUnit,
/// Payment Options
pub options: Option<MeltOptions>,
}
/// Melt Options
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
pub enum MeltOptions {
/// Mpp Options
Mpp {
/// MPP
mpp: Mpp,
},
/// Amountless options
Amountless {
/// Amountless
amountless: Amountless,
},
}
impl MeltOptions {
/// Create new [`MeltOptions::Mpp`]
pub fn new_mpp<A>(amount: A) -> Self
where
A: Into<Amount>,
{
Self::Mpp {
mpp: Mpp {
amount: amount.into(),
},
}
}
/// Create new [`MeltOptions::Amountless`]
pub fn new_amountless<A>(amount_msat: A) -> Self
where
A: Into<Amount>,
{
Self::Amountless {
amountless: Amountless {
amount_msat: amount_msat.into(),
},
}
}
/// Payment amount
pub fn amount_msat(&self) -> Amount {
match self {
Self::Mpp { mpp } => mpp.amount,
Self::Amountless { amountless } => amountless.amount_msat,
}
}
}
/// Amountless payment
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
pub struct Amountless {
/// Amount to pay in msat
pub amount_msat: Amount,
}
impl MeltQuoteBolt11Request {
/// Amount from [`MeltQuoteBolt11Request`]
///
/// Amount can either be defined in the bolt11 invoice,
/// in the request for an amountless bolt11 or in MPP option.
pub fn amount_msat(&self) -> Result<Amount, Error> {
let MeltQuoteBolt11Request {
request,
unit: _,
options,
..
} = self;
match options {
None => Ok(request
.amount_milli_satoshis()
.ok_or(Error::InvalidAmountRequest)?
.into()),
Some(MeltOptions::Mpp { mpp }) => Ok(mpp.amount),
Some(MeltOptions::Amountless { amountless }) => {
let amount = amountless.amount_msat;
if let Some(amount_msat) = request.amount_milli_satoshis() {
if amount != amount_msat.into() {
return Err(Error::InvalidAmountRequest);
}
}
Ok(amount)
}
}
}
}
/// Possible states of a quote
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
@@ -184,177 +75,11 @@ impl FromStr for QuoteState {
}
}
/// Melt quote response [NUT-05]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
#[serde(bound = "Q: Serialize")]
pub struct MeltQuoteBolt11Response<Q> {
/// Quote Id
pub quote: Q,
/// The amount that needs to be provided
pub amount: Amount,
/// The fee reserve that is required
pub fee_reserve: Amount,
/// Whether the request haas be paid
// TODO: To be deprecated
/// Deprecated
pub paid: Option<bool>,
/// Quote State
pub state: MeltQuoteState,
/// Unix timestamp until the quote is valid
pub expiry: u64,
/// Payment preimage
#[serde(skip_serializing_if = "Option::is_none")]
pub payment_preimage: Option<String>,
/// Change
#[serde(skip_serializing_if = "Option::is_none")]
pub change: Option<Vec<BlindSignature>>,
/// Payment request to fulfill
// REVIEW: This is now required in the spec, we should remove the option once all mints update
#[serde(skip_serializing_if = "Option::is_none")]
pub request: Option<String>,
/// Unit
// REVIEW: This is now required in the spec, we should remove the option once all mints update
#[serde(skip_serializing_if = "Option::is_none")]
pub unit: Option<CurrencyUnit>,
}
impl<Q: ToString> MeltQuoteBolt11Response<Q> {
/// Convert a `MeltQuoteBolt11Response` with type Q (generic/unknown) to a
/// `MeltQuoteBolt11Response` with `String`
pub fn to_string_id(self) -> MeltQuoteBolt11Response<String> {
MeltQuoteBolt11Response {
quote: self.quote.to_string(),
amount: self.amount,
fee_reserve: self.fee_reserve,
paid: self.paid,
state: self.state,
expiry: self.expiry,
payment_preimage: self.payment_preimage,
change: self.change,
request: self.request,
unit: self.unit,
}
}
}
#[cfg(feature = "mint")]
impl From<MeltQuoteBolt11Response<Uuid>> for MeltQuoteBolt11Response<String> {
fn from(value: MeltQuoteBolt11Response<Uuid>) -> Self {
Self {
quote: value.quote.to_string(),
amount: value.amount,
fee_reserve: value.fee_reserve,
paid: value.paid,
state: value.state,
expiry: value.expiry,
payment_preimage: value.payment_preimage,
change: value.change,
request: value.request,
unit: value.unit,
}
}
}
// A custom deserializer is needed until all mints
// update some will return without the required state.
impl<'de, Q: DeserializeOwned> Deserialize<'de> for MeltQuoteBolt11Response<Q> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = Value::deserialize(deserializer)?;
let quote: Q = serde_json::from_value(
value
.get("quote")
.ok_or(serde::de::Error::missing_field("quote"))?
.clone(),
)
.map_err(|_| serde::de::Error::custom("Invalid quote if string"))?;
let amount = value
.get("amount")
.ok_or(serde::de::Error::missing_field("amount"))?
.as_u64()
.ok_or(serde::de::Error::missing_field("amount"))?;
let amount = Amount::from(amount);
let fee_reserve = value
.get("fee_reserve")
.ok_or(serde::de::Error::missing_field("fee_reserve"))?
.as_u64()
.ok_or(serde::de::Error::missing_field("fee_reserve"))?;
let fee_reserve = Amount::from(fee_reserve);
let paid: Option<bool> = value.get("paid").and_then(|p| p.as_bool());
let state: Option<String> = value
.get("state")
.and_then(|s| serde_json::from_value(s.clone()).ok());
let (state, paid) = match (state, paid) {
(None, None) => return Err(serde::de::Error::custom("State or paid must be defined")),
(Some(state), _) => {
let state: QuoteState = QuoteState::from_str(&state)
.map_err(|_| serde::de::Error::custom("Unknown state"))?;
let paid = state == QuoteState::Paid;
(state, paid)
}
(None, Some(paid)) => {
let state = if paid {
QuoteState::Paid
} else {
QuoteState::Unpaid
};
(state, paid)
}
};
let expiry = value
.get("expiry")
.ok_or(serde::de::Error::missing_field("expiry"))?
.as_u64()
.ok_or(serde::de::Error::missing_field("expiry"))?;
let payment_preimage: Option<String> = value
.get("payment_preimage")
.and_then(|p| serde_json::from_value(p.clone()).ok());
let change: Option<Vec<BlindSignature>> = value
.get("change")
.and_then(|b| serde_json::from_value(b.clone()).ok());
let request: Option<String> = value
.get("request")
.and_then(|r| serde_json::from_value(r.clone()).ok());
let unit: Option<CurrencyUnit> = value
.get("unit")
.and_then(|u| serde_json::from_value(u.clone()).ok());
Ok(Self {
quote,
amount,
fee_reserve,
paid: Some(paid),
state,
expiry,
payment_preimage,
change,
request,
unit,
})
}
}
/// Melt Bolt11 Request [NUT-05]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
#[serde(bound = "Q: Serialize + DeserializeOwned")]
pub struct MeltBolt11Request<Q> {
pub struct MeltRequest<Q> {
/// Quote ID
quote: Q,
/// Proofs
@@ -366,10 +91,10 @@ pub struct MeltBolt11Request<Q> {
}
#[cfg(feature = "mint")]
impl TryFrom<MeltBolt11Request<String>> for MeltBolt11Request<Uuid> {
impl TryFrom<MeltRequest<String>> for MeltRequest<Uuid> {
type Error = uuid::Error;
fn try_from(value: MeltBolt11Request<String>) -> Result<Self, Self::Error> {
fn try_from(value: MeltRequest<String>) -> Result<Self, Self::Error> {
Ok(Self {
quote: Uuid::from_str(&value.quote)?,
inputs: value.inputs,
@@ -379,7 +104,7 @@ impl TryFrom<MeltBolt11Request<String>> for MeltBolt11Request<Uuid> {
}
// Basic implementation without trait bounds
impl<Q> MeltBolt11Request<Q> {
impl<Q> MeltRequest<Q> {
/// Get inputs (proofs)
pub fn inputs(&self) -> &Proofs {
&self.inputs
@@ -391,8 +116,8 @@ impl<Q> MeltBolt11Request<Q> {
}
}
impl<Q: Serialize + DeserializeOwned> MeltBolt11Request<Q> {
/// Create new [`MeltBolt11Request`]
impl<Q: Serialize + DeserializeOwned> MeltRequest<Q> {
/// Create new [`MeltRequest`]
pub fn new(quote: Q, inputs: Proofs, outputs: Option<Vec<BlindedMessage>>) -> Self {
Self {
quote,
@@ -414,7 +139,7 @@ impl<Q: Serialize + DeserializeOwned> MeltBolt11Request<Q> {
}
/// Melt Method Settings
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
pub struct MeltMethodSettings {
/// Payment Method e.g. bolt11
@@ -422,14 +147,168 @@ pub struct MeltMethodSettings {
/// Currency Unit e.g. sat
pub unit: CurrencyUnit,
/// Min Amount
#[serde(skip_serializing_if = "Option::is_none")]
pub min_amount: Option<Amount>,
/// Max Amount
#[serde(skip_serializing_if = "Option::is_none")]
pub max_amount: Option<Amount>,
/// Amountless
#[serde(default)]
pub amountless: bool,
/// Options
pub options: Option<MeltMethodOptions>,
}
impl Serialize for MeltMethodSettings {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut num_fields = 3; // method and unit are always present
if self.min_amount.is_some() {
num_fields += 1;
}
if self.max_amount.is_some() {
num_fields += 1;
}
let mut amountless_in_top_level = false;
if let Some(MeltMethodOptions::Bolt11 { amountless }) = &self.options {
if *amountless {
num_fields += 1;
amountless_in_top_level = true;
}
}
let mut state = serializer.serialize_struct("MeltMethodSettings", num_fields)?;
state.serialize_field("method", &self.method)?;
state.serialize_field("unit", &self.unit)?;
if let Some(min_amount) = &self.min_amount {
state.serialize_field("min_amount", min_amount)?;
}
if let Some(max_amount) = &self.max_amount {
state.serialize_field("max_amount", max_amount)?;
}
// If there's an amountless flag in Bolt11 options, add it at the top level
if amountless_in_top_level {
state.serialize_field("amountless", &true)?;
}
state.end()
}
}
struct MeltMethodSettingsVisitor;
impl<'de> Visitor<'de> for MeltMethodSettingsVisitor {
type Value = MeltMethodSettings;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a MeltMethodSettings structure")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut method: Option<PaymentMethod> = None;
let mut unit: Option<CurrencyUnit> = None;
let mut min_amount: Option<Amount> = None;
let mut max_amount: Option<Amount> = None;
let mut amountless: Option<bool> = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"method" => {
if method.is_some() {
return Err(de::Error::duplicate_field("method"));
}
method = Some(map.next_value()?);
}
"unit" => {
if unit.is_some() {
return Err(de::Error::duplicate_field("unit"));
}
unit = Some(map.next_value()?);
}
"min_amount" => {
if min_amount.is_some() {
return Err(de::Error::duplicate_field("min_amount"));
}
min_amount = Some(map.next_value()?);
}
"max_amount" => {
if max_amount.is_some() {
return Err(de::Error::duplicate_field("max_amount"));
}
max_amount = Some(map.next_value()?);
}
"amountless" => {
if amountless.is_some() {
return Err(de::Error::duplicate_field("amountless"));
}
amountless = Some(map.next_value()?);
}
"options" => {
// If there are explicit options, they take precedence, except the amountless
// field which we will handle specially
let options: Option<MeltMethodOptions> = map.next_value()?;
if let Some(MeltMethodOptions::Bolt11 {
amountless: amountless_from_options,
}) = options
{
// If we already found a top-level amountless, use that instead
if amountless.is_none() {
amountless = Some(amountless_from_options);
}
}
}
_ => {
// Skip unknown fields
let _: serde::de::IgnoredAny = map.next_value()?;
}
}
}
let method = method.ok_or_else(|| de::Error::missing_field("method"))?;
let unit = unit.ok_or_else(|| de::Error::missing_field("unit"))?;
// Create options based on the method and the amountless flag
let options = if method == PaymentMethod::Bolt11 && amountless.is_some() {
amountless.map(|amountless| MeltMethodOptions::Bolt11 { amountless })
} else {
None
};
Ok(MeltMethodSettings {
method,
unit,
min_amount,
max_amount,
options,
})
}
}
impl<'de> Deserialize<'de> for MeltMethodSettings {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_map(MeltMethodSettingsVisitor)
}
}
/// Mint Method settings options
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
#[serde(untagged)]
pub enum MeltMethodOptions {
/// Bolt11 Options
Bolt11 {
/// Mint supports paying bolt11 amountless
amountless: bool,
},
}
impl Settings {
@@ -475,3 +354,73 @@ pub struct Settings {
/// Minting disabled
pub disabled: bool,
}
#[cfg(test)]
mod tests {
use serde_json::{from_str, json, to_string};
use super::*;
#[test]
fn test_melt_method_settings_top_level_amountless() {
// Create JSON with top-level amountless
let json_str = r#"{
"method": "bolt11",
"unit": "sat",
"min_amount": 0,
"max_amount": 10000,
"amountless": true
}"#;
// Deserialize it
let settings: MeltMethodSettings = from_str(json_str).unwrap();
// Check that amountless was correctly moved to options
assert_eq!(settings.method, PaymentMethod::Bolt11);
assert_eq!(settings.unit, CurrencyUnit::Sat);
assert_eq!(settings.min_amount, Some(Amount::from(0)));
assert_eq!(settings.max_amount, Some(Amount::from(10000)));
match settings.options {
Some(MeltMethodOptions::Bolt11 { amountless }) => {
assert_eq!(amountless, true);
}
_ => panic!("Expected Bolt11 options with amountless = true"),
}
// Serialize it back
let serialized = to_string(&settings).unwrap();
let parsed: serde_json::Value = from_str(&serialized).unwrap();
// Verify the amountless is at the top level
assert_eq!(parsed["amountless"], json!(true));
}
#[test]
fn test_both_amountless_locations() {
// Create JSON with amountless in both places (top level and in options)
let json_str = r#"{
"method": "bolt11",
"unit": "sat",
"min_amount": 0,
"max_amount": 10000,
"amountless": true,
"options": {
"amountless": false
}
}"#;
// Deserialize it - top level should take precedence
let settings: MeltMethodSettings = from_str(json_str).unwrap();
match settings.options {
Some(MeltMethodOptions::Bolt11 { amountless }) => {
assert_eq!(
amountless, true,
"Top-level amountless should take precedence"
);
}
_ => panic!("Expected Bolt11 options with amountless = true"),
}
}
}

View File

@@ -470,6 +470,7 @@ impl ContactInfo {
mod tests {
use super::*;
use crate::nut04::MintMethodOptions;
#[test]
fn test_des_mint_into() {
@@ -552,8 +553,10 @@ mod tests {
"unit": "sat",
"min_amount": 0,
"max_amount": 10000,
"options": {
"description": true
}
}
],
"disabled": false
},
@@ -598,8 +601,10 @@ mod tests {
"unit": "sat",
"min_amount": 0,
"max_amount": 10000,
"options": {
"description": true
}
}
],
"disabled": false
},
@@ -624,6 +629,16 @@ mod tests {
}"#;
let mint_info: MintInfo = serde_json::from_str(mint_info_str).unwrap();
let t = mint_info
.nuts
.nut04
.get_settings(&crate::CurrencyUnit::Sat, &crate::PaymentMethod::Bolt11)
.unwrap();
let t = t.options.unwrap();
matches!(t, MintMethodOptions::Bolt11 { description: true });
assert_eq!(info, mint_info);
}
}

View File

@@ -2,10 +2,11 @@
//!
//! <https://github.com/cashubtc/nuts/blob/main/08.md>
use super::nut05::{MeltBolt11Request, MeltQuoteBolt11Response};
use super::nut05::MeltRequest;
use super::nut23::MeltQuoteBolt11Response;
use crate::Amount;
impl<Q> MeltBolt11Request<Q> {
impl<Q> MeltRequest<Q> {
/// Total output [`Amount`]
pub fn output_amount(&self) -> Option<Amount> {
self.outputs()

View File

@@ -5,7 +5,7 @@ use std::str::FromStr;
use bitcoin::secp256k1::schnorr::Signature;
use thiserror::Error;
use super::{MintBolt11Request, PublicKey, SecretKey};
use super::{MintRequest, PublicKey, SecretKey};
/// Nut19 Error
#[derive(Debug, Error)]
@@ -21,7 +21,7 @@ pub enum Error {
NUT01(#[from] crate::nuts::nut01::Error),
}
impl<Q> MintBolt11Request<Q>
impl<Q> MintRequest<Q>
where
Q: ToString,
{
@@ -46,7 +46,7 @@ where
msg
}
/// Sign [`MintBolt11Request`]
/// Sign [`MintRequest`]
pub fn sign(&mut self, secret_key: SecretKey) -> Result<(), Error> {
let msg = self.msg_to_sign();
@@ -57,7 +57,7 @@ where
Ok(())
}
/// Verify signature on [`MintBolt11Request`]
/// Verify signature on [`MintRequest`]
pub fn verify_signature(&self, pubkey: PublicKey) -> Result<(), Error> {
let signature = self.signature.as_ref().ok_or(Error::SignatureMissing)?;
@@ -80,7 +80,7 @@ mod tests {
#[test]
fn test_msg_to_sign() {
let request: MintBolt11Request<String> = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}],"signature":"cb2b8e7ea69362dfe2a07093f2bbc319226db33db2ef686c940b5ec976bcbfc78df0cd35b3e998adf437b09ee2c950bd66dfe9eb64abd706e43ebc7c669c36c3"}"#).unwrap();
let request: MintRequest<String> = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}],"signature":"cb2b8e7ea69362dfe2a07093f2bbc319226db33db2ef686c940b5ec976bcbfc78df0cd35b3e998adf437b09ee2c950bd66dfe9eb64abd706e43ebc7c669c36c3"}"#).unwrap();
// let expected_msg_to_sign = "9d745270-1405-46de-b5c5-e2762b4f5e000342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c31102be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b5302209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79";
@@ -118,14 +118,14 @@ mod tests {
)
.unwrap();
let request: MintBolt11Request<Uuid> = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}], "signature": "d4b386f21f7aa7172f0994ee6e4dd966539484247ea71c99b81b8e09b1bb2acbc0026a43c221fd773471dc30d6a32b04692e6837ddaccf0830a63128308e4ee0"}"#).unwrap();
let request: MintRequest<Uuid> = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}], "signature": "d4b386f21f7aa7172f0994ee6e4dd966539484247ea71c99b81b8e09b1bb2acbc0026a43c221fd773471dc30d6a32b04692e6837ddaccf0830a63128308e4ee0"}"#).unwrap();
assert!(request.verify_signature(pubkey).is_ok());
}
#[test]
fn test_mint_request_signature() {
let mut request: MintBolt11Request<String> = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}]}"#).unwrap();
let mut request: MintRequest<String> = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}]}"#).unwrap();
let secret =
SecretKey::from_hex("50d7fd7aa2b2fe4607f41f4ce6f8794fc184dd47b8cdfbe4b3d1249aa02d35aa")
@@ -143,7 +143,7 @@ mod tests {
)
.unwrap();
let request: MintBolt11Request<String> = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}],"signature":"cb2b8e7ea69362dfe2a07093f2bbc319226db33db2ef686c940b5ec976bcbfc78df0cd35b3e998adf437b09ee2c950bd66dfe9eb64abd706e43ebc7c669c36c3"}"#).unwrap();
let request: MintRequest<String> = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}],"signature":"cb2b8e7ea69362dfe2a07093f2bbc319226db33db2ef686c940b5ec976bcbfc78df0cd35b3e998adf437b09ee2c950bd66dfe9eb64abd706e43ebc7c669c36c3"}"#).unwrap();
// Signature is on a different quote id verification should fail
assert!(request.verify_signature(pubkey).is_err());

View File

@@ -0,0 +1,411 @@
//! Bolt11
use std::fmt;
use std::str::FromStr;
use lightning_invoice::Bolt11Invoice;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use thiserror::Error;
#[cfg(feature = "mint")]
use uuid::Uuid;
use super::{BlindSignature, CurrencyUnit, MeltQuoteState, Mpp, PublicKey};
use crate::Amount;
/// NUT023 Error
#[derive(Debug, Error)]
pub enum Error {
/// Unknown Quote State
#[error("Unknown Quote State")]
UnknownState,
/// Amount overflow
#[error("Amount overflow")]
AmountOverflow,
/// Invalid Amount
#[error("Invalid Request")]
InvalidAmountRequest,
}
/// Mint quote request [NUT-04]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
pub struct MintQuoteBolt11Request {
/// Amount
pub amount: Amount,
/// Unit wallet would like to pay with
pub unit: CurrencyUnit,
/// Memo to create the invoice with
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// NUT-19 Pubkey
#[serde(skip_serializing_if = "Option::is_none")]
pub pubkey: Option<PublicKey>,
}
/// Possible states of a quote
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = MintQuoteState))]
pub enum QuoteState {
/// Quote has not been paid
#[default]
Unpaid,
/// Quote has been paid and wallet can mint
Paid,
/// Minting is in progress
/// **Note:** This state is to be used internally but is not part of the
/// nut.
Pending,
/// ecash issued for quote
Issued,
}
impl fmt::Display for QuoteState {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Unpaid => write!(f, "UNPAID"),
Self::Paid => write!(f, "PAID"),
Self::Pending => write!(f, "PENDING"),
Self::Issued => write!(f, "ISSUED"),
}
}
}
impl FromStr for QuoteState {
type Err = Error;
fn from_str(state: &str) -> Result<Self, Self::Err> {
match state {
"PENDING" => Ok(Self::Pending),
"PAID" => Ok(Self::Paid),
"UNPAID" => Ok(Self::Unpaid),
"ISSUED" => Ok(Self::Issued),
_ => Err(Error::UnknownState),
}
}
}
/// Mint quote response [NUT-04]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
#[serde(bound = "Q: Serialize + DeserializeOwned")]
pub struct MintQuoteBolt11Response<Q> {
/// Quote Id
pub quote: Q,
/// Payment request to fulfil
pub request: String,
/// Amount
// REVIEW: This is now required in the spec, we should remove the option once all mints update
pub amount: Option<Amount>,
/// Unit
// REVIEW: This is now required in the spec, we should remove the option once all mints update
pub unit: Option<CurrencyUnit>,
/// Quote State
pub state: QuoteState,
/// Unix timestamp until the quote is valid
pub expiry: Option<u64>,
/// NUT-19 Pubkey
#[serde(skip_serializing_if = "Option::is_none")]
pub pubkey: Option<PublicKey>,
}
impl<Q: ToString> MintQuoteBolt11Response<Q> {
/// Convert the MintQuote with a quote type Q to a String
pub fn to_string_id(&self) -> MintQuoteBolt11Response<String> {
MintQuoteBolt11Response {
quote: self.quote.to_string(),
request: self.request.clone(),
state: self.state,
expiry: self.expiry,
pubkey: self.pubkey,
amount: self.amount,
unit: self.unit.clone(),
}
}
}
#[cfg(feature = "mint")]
impl From<MintQuoteBolt11Response<Uuid>> for MintQuoteBolt11Response<String> {
fn from(value: MintQuoteBolt11Response<Uuid>) -> Self {
Self {
quote: value.quote.to_string(),
request: value.request,
state: value.state,
expiry: value.expiry,
pubkey: value.pubkey,
amount: value.amount,
unit: value.unit.clone(),
}
}
}
/// BOLT11 melt quote request [NUT-23]
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
pub struct MeltQuoteBolt11Request {
/// Bolt11 invoice to be paid
#[cfg_attr(feature = "swagger", schema(value_type = String))]
pub request: Bolt11Invoice,
/// Unit wallet would like to pay with
pub unit: CurrencyUnit,
/// Payment Options
pub options: Option<MeltOptions>,
}
/// Melt Options
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
pub enum MeltOptions {
/// Mpp Options
Mpp {
/// MPP
mpp: Mpp,
},
/// Amountless options
Amountless {
/// Amountless
amountless: Amountless,
},
}
impl MeltOptions {
/// Create new [`MeltOptions::Mpp`]
pub fn new_mpp<A>(amount: A) -> Self
where
A: Into<Amount>,
{
Self::Mpp {
mpp: Mpp {
amount: amount.into(),
},
}
}
/// Create new [`MeltOptions::Amountless`]
pub fn new_amountless<A>(amount_msat: A) -> Self
where
A: Into<Amount>,
{
Self::Amountless {
amountless: Amountless {
amount_msat: amount_msat.into(),
},
}
}
/// Payment amount
pub fn amount_msat(&self) -> Amount {
match self {
Self::Mpp { mpp } => mpp.amount,
Self::Amountless { amountless } => amountless.amount_msat,
}
}
}
/// Amountless payment
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
pub struct Amountless {
/// Amount to pay in msat
pub amount_msat: Amount,
}
impl MeltQuoteBolt11Request {
/// Amount from [`MeltQuoteBolt11Request`]
///
/// Amount can either be defined in the bolt11 invoice,
/// in the request for an amountless bolt11 or in MPP option.
pub fn amount_msat(&self) -> Result<Amount, Error> {
let MeltQuoteBolt11Request {
request,
unit: _,
options,
..
} = self;
match options {
None => Ok(request
.amount_milli_satoshis()
.ok_or(Error::InvalidAmountRequest)?
.into()),
Some(MeltOptions::Mpp { mpp }) => Ok(mpp.amount),
Some(MeltOptions::Amountless { amountless }) => {
let amount = amountless.amount_msat;
if let Some(amount_msat) = request.amount_milli_satoshis() {
if amount != amount_msat.into() {
return Err(Error::InvalidAmountRequest);
}
}
Ok(amount)
}
}
}
}
/// Melt quote response [NUT-05]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
#[serde(bound = "Q: Serialize")]
pub struct MeltQuoteBolt11Response<Q> {
/// Quote Id
pub quote: Q,
/// The amount that needs to be provided
pub amount: Amount,
/// The fee reserve that is required
pub fee_reserve: Amount,
/// Whether the request haas be paid
// TODO: To be deprecated
/// Deprecated
pub paid: Option<bool>,
/// Quote State
pub state: MeltQuoteState,
/// Unix timestamp until the quote is valid
pub expiry: u64,
/// Payment preimage
#[serde(skip_serializing_if = "Option::is_none")]
pub payment_preimage: Option<String>,
/// Change
#[serde(skip_serializing_if = "Option::is_none")]
pub change: Option<Vec<BlindSignature>>,
/// Payment request to fulfill
// REVIEW: This is now required in the spec, we should remove the option once all mints update
#[serde(skip_serializing_if = "Option::is_none")]
pub request: Option<String>,
/// Unit
// REVIEW: This is now required in the spec, we should remove the option once all mints update
#[serde(skip_serializing_if = "Option::is_none")]
pub unit: Option<CurrencyUnit>,
}
impl<Q: ToString> MeltQuoteBolt11Response<Q> {
/// Convert a `MeltQuoteBolt11Response` with type Q (generic/unknown) to a
/// `MeltQuoteBolt11Response` with `String`
pub fn to_string_id(self) -> MeltQuoteBolt11Response<String> {
MeltQuoteBolt11Response {
quote: self.quote.to_string(),
amount: self.amount,
fee_reserve: self.fee_reserve,
paid: self.paid,
state: self.state,
expiry: self.expiry,
payment_preimage: self.payment_preimage,
change: self.change,
request: self.request,
unit: self.unit,
}
}
}
#[cfg(feature = "mint")]
impl From<MeltQuoteBolt11Response<Uuid>> for MeltQuoteBolt11Response<String> {
fn from(value: MeltQuoteBolt11Response<Uuid>) -> Self {
Self {
quote: value.quote.to_string(),
amount: value.amount,
fee_reserve: value.fee_reserve,
paid: value.paid,
state: value.state,
expiry: value.expiry,
payment_preimage: value.payment_preimage,
change: value.change,
request: value.request,
unit: value.unit,
}
}
}
// A custom deserializer is needed until all mints
// update some will return without the required state.
impl<'de, Q: DeserializeOwned> Deserialize<'de> for MeltQuoteBolt11Response<Q> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = Value::deserialize(deserializer)?;
let quote: Q = serde_json::from_value(
value
.get("quote")
.ok_or(serde::de::Error::missing_field("quote"))?
.clone(),
)
.map_err(|_| serde::de::Error::custom("Invalid quote if string"))?;
let amount = value
.get("amount")
.ok_or(serde::de::Error::missing_field("amount"))?
.as_u64()
.ok_or(serde::de::Error::missing_field("amount"))?;
let amount = Amount::from(amount);
let fee_reserve = value
.get("fee_reserve")
.ok_or(serde::de::Error::missing_field("fee_reserve"))?
.as_u64()
.ok_or(serde::de::Error::missing_field("fee_reserve"))?;
let fee_reserve = Amount::from(fee_reserve);
let paid: Option<bool> = value.get("paid").and_then(|p| p.as_bool());
let state: Option<String> = value
.get("state")
.and_then(|s| serde_json::from_value(s.clone()).ok());
let (state, paid) = match (state, paid) {
(None, None) => return Err(serde::de::Error::custom("State or paid must be defined")),
(Some(state), _) => {
let state: MeltQuoteState = MeltQuoteState::from_str(&state)
.map_err(|_| serde::de::Error::custom("Unknown state"))?;
let paid = state == MeltQuoteState::Paid;
(state, paid)
}
(None, Some(paid)) => {
let state = if paid {
MeltQuoteState::Paid
} else {
MeltQuoteState::Unpaid
};
(state, paid)
}
};
let expiry = value
.get("expiry")
.ok_or(serde::de::Error::missing_field("expiry"))?
.as_u64()
.ok_or(serde::de::Error::missing_field("expiry"))?;
let payment_preimage: Option<String> = value
.get("payment_preimage")
.and_then(|p| serde_json::from_value(p.clone()).ok());
let change: Option<Vec<BlindSignature>> = value
.get("change")
.and_then(|b| serde_json::from_value(b.clone()).ok());
let request: Option<String> = value
.get("request")
.and_then(|r| serde_json::from_value(r.clone()).ok());
let unit: Option<CurrencyUnit> = value
.get("unit")
.and_then(|u| serde_json::from_value(u.clone()).ok());
Ok(Self {
quote,
amount,
fee_reserve,
paid: Some(paid),
state,
expiry,
payment_preimage,
change,
request,
unit,
})
}
}

View File

@@ -9,7 +9,7 @@ use axum::{Json, Router};
#[cfg(feature = "swagger")]
use cdk::error::ErrorResponse;
use cdk::nuts::{
AuthToken, BlindAuthToken, KeysResponse, KeysetResponse, MintAuthRequest, MintBolt11Response,
AuthToken, BlindAuthToken, KeysResponse, KeysetResponse, MintAuthRequest, MintResponse,
};
use serde::{Deserialize, Serialize};
@@ -144,7 +144,7 @@ pub async fn get_blind_auth_keys(
path = "/blind/mint",
request_body(content = MintAuthRequest, description = "Request params", content_type = "application/json"),
responses(
(status = 200, description = "Successful response", body = MintBolt11Response, content_type = "application/json"),
(status = 200, description = "Successful response", body = MintResponse, content_type = "application/json"),
(status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
)
))]
@@ -152,7 +152,7 @@ pub async fn post_mint_auth(
auth: AuthHeader,
State(state): State<MintState>,
Json(payload): Json<MintAuthRequest>,
) -> Result<Json<MintBolt11Response>, Response> {
) -> Result<Json<MintResponse>, Response> {
let auth_token = match auth {
AuthHeader::Clear(cat) => {
if cat.is_empty() {

View File

@@ -33,13 +33,8 @@ mod swagger_imports {
pub use cdk::nuts::nut01::{Keys, KeysResponse, PublicKey, SecretKey};
pub use cdk::nuts::nut02::{KeySet, KeySetInfo, KeysetResponse};
pub use cdk::nuts::nut03::{SwapRequest, SwapResponse};
pub use cdk::nuts::nut04::{
MintBolt11Request, MintBolt11Response, MintMethodSettings, MintQuoteBolt11Request,
MintQuoteBolt11Response,
};
pub use cdk::nuts::nut05::{
MeltBolt11Request, MeltMethodSettings, MeltQuoteBolt11Request, MeltQuoteBolt11Response,
};
pub use cdk::nuts::nut04::{MintMethodSettings, MintRequest, MintResponse};
pub use cdk::nuts::nut05::{MeltMethodSettings, MeltRequest};
pub use cdk::nuts::nut06::{ContactInfo, MintInfo, MintVersion, Nuts, SupportedSettings};
pub use cdk::nuts::nut07::{CheckStateRequest, CheckStateResponse, ProofState, State};
pub use cdk::nuts::nut09::{RestoreRequest, RestoreResponse};
@@ -47,6 +42,10 @@ mod swagger_imports {
pub use cdk::nuts::nut12::{BlindSignatureDleq, ProofDleq};
pub use cdk::nuts::nut14::HTLCWitness;
pub use cdk::nuts::nut15::{Mpp, MppMethodSettings};
pub use cdk::nuts::nut23::{
MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintQuoteBolt11Request,
MintQuoteBolt11Response,
};
pub use cdk::nuts::{nut04, nut05, nut15, MeltQuoteState, MintQuoteState};
}
@@ -80,13 +79,13 @@ pub struct MintState {
KeysetResponse,
KeySet,
KeySetInfo,
MeltBolt11Request<String>,
MeltRequest<String>,
MeltQuoteBolt11Request,
MeltQuoteBolt11Response<String>,
MeltQuoteState,
MeltMethodSettings,
MintBolt11Request<String>,
MintBolt11Response,
MintRequest<String>,
MintResponse,
MintInfo,
MintQuoteBolt11Request,
MintQuoteBolt11Response<String>,

View File

@@ -7,9 +7,9 @@ use cdk::error::ErrorResponse;
#[cfg(feature = "auth")]
use cdk::nuts::nut21::{Method, ProtectedEndpoint, RoutePath};
use cdk::nuts::{
CheckStateRequest, CheckStateResponse, Id, KeysResponse, KeysetResponse, MeltBolt11Request,
MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintBolt11Request, MintBolt11Response,
MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response, RestoreRequest, RestoreResponse,
CheckStateRequest, CheckStateResponse, Id, KeysResponse, KeysetResponse,
MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltRequest, MintInfo, MintQuoteBolt11Request,
MintQuoteBolt11Response, MintRequest, MintResponse, RestoreRequest, RestoreResponse,
SwapRequest, SwapResponse,
};
use cdk::util::unix_time;
@@ -60,14 +60,10 @@ macro_rules! post_cache_wrapper {
}
post_cache_wrapper!(post_swap, SwapRequest, SwapResponse);
post_cache_wrapper!(
post_mint_bolt11,
MintBolt11Request<Uuid>,
MintBolt11Response
);
post_cache_wrapper!(post_mint_bolt11, MintRequest<Uuid>, MintResponse);
post_cache_wrapper!(
post_melt_bolt11,
MeltBolt11Request<Uuid>,
MeltRequest<Uuid>,
MeltQuoteBolt11Response<Uuid>
);
@@ -246,9 +242,9 @@ pub(crate) async fn ws_handler(
post,
context_path = "/v1",
path = "/mint/bolt11",
request_body(content = MintBolt11Request<String>, description = "Request params", content_type = "application/json"),
request_body(content = MintRequest<String>, description = "Request params", content_type = "application/json"),
responses(
(status = 200, description = "Successful response", body = MintBolt11Response, content_type = "application/json"),
(status = 200, description = "Successful response", body = MintResponse, content_type = "application/json"),
(status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
)
))]
@@ -256,8 +252,8 @@ pub(crate) async fn ws_handler(
pub(crate) async fn post_mint_bolt11(
#[cfg(feature = "auth")] auth: AuthHeader,
State(state): State<MintState>,
Json(payload): Json<MintBolt11Request<Uuid>>,
) -> Result<Json<MintBolt11Response>, Response> {
Json(payload): Json<MintRequest<Uuid>>,
) -> Result<Json<MintResponse>, Response> {
#[cfg(feature = "auth")]
{
state
@@ -369,7 +365,7 @@ pub(crate) async fn get_check_melt_bolt11_quote(
post,
context_path = "/v1",
path = "/melt/bolt11",
request_body(content = MeltBolt11Request<String>, description = "Melt params", content_type = "application/json"),
request_body(content = MeltRequest<String>, description = "Melt params", content_type = "application/json"),
responses(
(status = 200, description = "Successful response", body = MeltQuoteBolt11Response<String>, content_type = "application/json"),
(status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
@@ -382,7 +378,7 @@ pub(crate) async fn get_check_melt_bolt11_quote(
pub(crate) async fn post_melt_bolt11(
#[cfg(feature = "auth")] auth: AuthHeader,
State(state): State<MintState>,
Json(payload): Json<MeltBolt11Request<Uuid>>,
Json(payload): Json<MeltRequest<Uuid>>,
) -> Result<Json<MeltQuoteBolt11Response<Uuid>>, Response> {
#[cfg(feature = "auth")]
{

View File

@@ -10,8 +10,8 @@ use super::Error;
use crate::common::{PaymentProcessorKey, QuoteTTL};
use crate::mint::{self, MintKeySetInfo, MintQuote as MintMintQuote};
use crate::nuts::{
BlindSignature, CurrencyUnit, Id, MeltBolt11Request, MeltQuoteState, MintQuoteState, Proof,
Proofs, PublicKey, State,
BlindSignature, CurrencyUnit, Id, MeltQuoteState, MeltRequest, MintQuoteState, Proof, Proofs,
PublicKey, State,
};
#[cfg(feature = "auth")]
@@ -96,14 +96,14 @@ pub trait QuotesDatabase {
/// Add melt request
async fn add_melt_request(
&self,
melt_request: MeltBolt11Request<Uuid>,
melt_request: MeltRequest<Uuid>,
ln_key: PaymentProcessorKey,
) -> Result<(), Self::Err>;
/// Get melt request
async fn get_melt_request(
&self,
quote_id: &Uuid,
) -> Result<Option<(MeltBolt11Request<Uuid>, PaymentProcessorKey)>, Self::Err>;
) -> Result<Option<(MeltRequest<Uuid>, PaymentProcessorKey)>, Self::Err>;
}
/// Mint Proof Database trait

View File

@@ -309,12 +309,15 @@ pub enum Error {
/// NUT20 Error
#[error(transparent)]
NUT20(#[from] crate::nuts::nut20::Error),
/// NUTXX Error
/// NUT21 Error
#[error(transparent)]
NUT21(#[from] crate::nuts::nut21::Error),
/// NUTXX1 Error
/// NUT22 Error
#[error(transparent)]
NUT22(#[from] crate::nuts::nut22::Error),
/// NUT23 Error
#[error(transparent)]
NUT23(#[from] crate::nuts::nut23::Error),
/// Database Error
#[error(transparent)]
Database(crate::database::Error),

View File

@@ -52,6 +52,9 @@ pub enum Error {
/// NUT05 Error
#[error(transparent)]
NUT05(#[from] crate::nuts::nut05::Error),
/// NUT23 Error
#[error(transparent)]
NUT23(#[from] crate::nuts::nut23::Error),
/// Custom
#[error("`{0}`")]
Custom(String),

View File

@@ -14,9 +14,9 @@ use cdk::mint::{MintBuilder, MintMeltLimits};
use cdk::nuts::nut00::ProofsMethods;
use cdk::nuts::{
CheckStateRequest, CheckStateResponse, CurrencyUnit, Id, KeySet, KeysetResponse,
MeltBolt11Request, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintBolt11Request,
MintBolt11Response, MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response, PaymentMethod,
RestoreRequest, RestoreResponse, SwapRequest, SwapResponse,
MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltRequest, MintInfo, MintQuoteBolt11Request,
MintQuoteBolt11Response, MintRequest, MintResponse, PaymentMethod, RestoreRequest,
RestoreResponse, SwapRequest, SwapResponse,
};
use cdk::types::{FeeReserve, QuoteTTL};
use cdk::util::unix_time;
@@ -91,10 +91,7 @@ impl MintConnector for DirectMintConnection {
.map(Into::into)
}
async fn post_mint(
&self,
request: MintBolt11Request<String>,
) -> Result<MintBolt11Response, Error> {
async fn post_mint(&self, request: MintRequest<String>) -> Result<MintResponse, Error> {
let request_uuid = request.try_into().unwrap();
self.mint.process_mint_request(request_uuid).await
}
@@ -122,7 +119,7 @@ impl MintConnector for DirectMintConnection {
async fn post_melt(
&self,
request: MeltBolt11Request<String>,
request: MeltRequest<String>,
) -> Result<MeltQuoteBolt11Response<String>, Error> {
let request_uuid = request.try_into().unwrap();
self.mint.melt_bolt11(&request_uuid).await.map(Into::into)

View File

@@ -8,9 +8,9 @@ use cdk::amount::{Amount, SplitTarget};
use cdk::mint_url::MintUrl;
use cdk::nuts::nut00::ProofsMethods;
use cdk::nuts::{
AuthProof, AuthToken, BlindAuthToken, CheckStateRequest, CurrencyUnit, MeltBolt11Request,
MeltQuoteBolt11Request, MeltQuoteState, MintBolt11Request, MintQuoteBolt11Request,
RestoreRequest, State, SwapRequest,
AuthProof, AuthToken, BlindAuthToken, CheckStateRequest, CurrencyUnit, MeltQuoteBolt11Request,
MeltQuoteState, MeltRequest, MintQuoteBolt11Request, MintRequest, RestoreRequest, State,
SwapRequest,
};
use cdk::wallet::{AuthHttpClient, AuthMintConnector, HttpClient, MintConnector, WalletBuilder};
use cdk::{Error, OidcClient};
@@ -109,7 +109,7 @@ async fn test_mint_without_auth() {
}
{
let request = MintBolt11Request {
let request = MintRequest {
quote: "123e4567-e89b-12d3-a456-426614174000".to_string(),
outputs: vec![],
signature: None,
@@ -207,7 +207,7 @@ async fn test_melt_without_auth() {
// Test melt
{
let request = MeltBolt11Request::new(
let request = MeltRequest::new(
"123e4567-e89b-12d3-a456-426614174000".to_string(),
vec![],
None,

View File

@@ -5,8 +5,8 @@ use cashu::Amount;
use cdk::amount::SplitTarget;
use cdk::nuts::nut00::ProofsMethods;
use cdk::nuts::{
CurrencyUnit, MeltBolt11Request, MeltQuoteState, MintBolt11Request, PreMintSecrets, Proofs,
SecretKey, State, SwapRequest,
CurrencyUnit, MeltQuoteState, MeltRequest, MintRequest, PreMintSecrets, Proofs, SecretKey,
State, SwapRequest,
};
use cdk::wallet::types::TransactionDirection;
use cdk::wallet::{HttpClient, MintConnector, Wallet};
@@ -388,7 +388,7 @@ async fn test_fake_melt_change_in_quote() {
let client = HttpClient::new(MINT_URL.parse().unwrap(), None);
let melt_request = MeltBolt11Request::new(
let melt_request = MeltRequest::new(
melt_quote.id.clone(),
proofs.clone(),
Some(premint_secrets.blinded_messages()),
@@ -494,7 +494,7 @@ async fn test_fake_mint_without_witness() {
let premint_secrets =
PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::default()).unwrap();
let request = MintBolt11Request {
let request = MintRequest {
quote: mint_quote.id,
outputs: premint_secrets.blinded_messages(),
signature: None,
@@ -534,7 +534,7 @@ async fn test_fake_mint_with_wrong_witness() {
let premint_secrets =
PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::default()).unwrap();
let mut request = MintBolt11Request {
let mut request = MintRequest {
quote: mint_quote.id,
outputs: premint_secrets.blinded_messages(),
signature: None,
@@ -585,7 +585,7 @@ async fn test_fake_mint_inflated() {
.unwrap()
.expect("there is a quote");
let mut mint_request = MintBolt11Request {
let mut mint_request = MintRequest {
quote: mint_quote.id,
outputs: pre_mint.blinded_messages(),
signature: None,
@@ -662,7 +662,7 @@ async fn test_fake_mint_multiple_units() {
sat_outputs.append(&mut usd_outputs);
let mut mint_request = MintBolt11Request {
let mut mint_request = MintRequest {
quote: mint_quote.id,
outputs: sat_outputs,
signature: None,
@@ -859,7 +859,7 @@ async fn test_fake_mint_multiple_unit_melt() {
let invoice = create_fake_invoice((input_amount - 1) * 1000, "".to_string());
let melt_quote = wallet.melt_quote(invoice.to_string(), None).await.unwrap();
let melt_request = MeltBolt11Request::new(melt_quote.id, inputs, None);
let melt_request = MeltRequest::new(melt_quote.id, inputs, None);
let http_client = HttpClient::new(MINT_URL.parse().unwrap(), None);
let response = http_client.post_melt(melt_request.clone()).await;
@@ -901,7 +901,7 @@ async fn test_fake_mint_multiple_unit_melt() {
usd_outputs.append(&mut sat_outputs);
let quote = wallet.melt_quote(invoice.to_string(), None).await.unwrap();
let melt_request = MeltBolt11Request::new(quote.id, inputs, Some(usd_outputs));
let melt_request = MeltRequest::new(quote.id, inputs, Some(usd_outputs));
let http_client = HttpClient::new(MINT_URL.parse().unwrap(), None);
@@ -1148,7 +1148,7 @@ async fn test_fake_mint_melt_spend_after_fail() {
let invoice = create_fake_invoice((input_amount - 1) * 1000, "".to_string());
let melt_quote = wallet.melt_quote(invoice.to_string(), None).await.unwrap();
let melt_request = MeltBolt11Request::new(melt_quote.id, proofs, None);
let melt_request = MeltRequest::new(melt_quote.id, proofs, None);
let http_client = HttpClient::new(MINT_URL.parse().unwrap(), None);
let response = http_client.post_melt(melt_request.clone()).await;
@@ -1274,7 +1274,7 @@ async fn test_fake_mint_duplicate_proofs_melt() {
let melt_quote = wallet.melt_quote(invoice.to_string(), None).await.unwrap();
let melt_request = MeltBolt11Request::new(melt_quote.id, inputs, None);
let melt_request = MeltRequest::new(melt_quote.id, inputs, None);
let http_client = HttpClient::new(MINT_URL.parse().unwrap(), None);
let response = http_client.post_melt(melt_request.clone()).await;

View File

@@ -16,7 +16,7 @@ use std::time::Duration;
use std::{char, env};
use bip39::Mnemonic;
use cashu::{MeltBolt11Request, PreMintSecrets};
use cashu::{MeltRequest, PreMintSecrets};
use cdk::amount::{Amount, SplitTarget};
use cdk::nuts::nut00::ProofsMethods;
use cdk::nuts::{CurrencyUnit, MeltQuoteState, NotificationPayload, State};
@@ -358,7 +358,7 @@ async fn test_fake_melt_change_in_quote() {
let client = HttpClient::new(get_mint_url_from_env().parse().unwrap(), None);
let melt_request = MeltBolt11Request::new(
let melt_request = MeltRequest::new(
melt_quote.id.clone(),
proofs.clone(),
Some(premint_secrets.blinded_messages()),

View File

@@ -13,8 +13,8 @@ use cashu::amount::SplitTarget;
use cashu::dhke::construct_proofs;
use cashu::mint_url::MintUrl;
use cashu::{
CurrencyUnit, Id, MeltBolt11Request, NotificationPayload, PreMintSecrets, ProofState,
SecretKey, SpendingConditions, State, SwapRequest,
CurrencyUnit, Id, MeltRequest, NotificationPayload, PreMintSecrets, ProofState, SecretKey,
SpendingConditions, State, SwapRequest,
};
use cdk::mint::Mint;
use cdk::nuts::nut00::ProofsMethods;
@@ -855,7 +855,7 @@ async fn test_concurrent_double_spend_melt() {
let mint_clone2 = mint_bob.clone();
let mint_clone3 = mint_bob.clone();
let melt_request = MeltBolt11Request::new(quote_id.parse().unwrap(), proofs.clone(), None);
let melt_request = MeltRequest::new(quote_id.parse().unwrap(), proofs.clone(), None);
let melt_request2 = melt_request.clone();
let melt_request3 = melt_request.clone();

View File

@@ -6,7 +6,7 @@ use bip39::Mnemonic;
use cashu::ProofsMethods;
use cdk::amount::{Amount, SplitTarget};
use cdk::nuts::{
CurrencyUnit, MeltOptions, MeltQuoteState, MintBolt11Request, MintQuoteState, Mpp,
CurrencyUnit, MeltOptions, MeltQuoteState, MintQuoteState, MintRequest, Mpp,
NotificationPayload, PreMintSecrets,
};
use cdk::wallet::{HttpClient, MintConnector, Wallet, WalletSubscription};
@@ -313,7 +313,7 @@ async fn test_cached_mint() {
let premint_secrets =
PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::default()).unwrap();
let mut request = MintBolt11Request {
let mut request = MintRequest {
quote: quote.id,
outputs: premint_secrets.blinded_messages(),
signature: None,

View File

@@ -4,7 +4,7 @@ use tonic::transport::Channel;
use tonic::Request;
use crate::cdk_mint_client::CdkMintClient;
use crate::UpdateNut04Request;
use crate::{MintMethodOptions, UpdateNut04Request};
/// Command to update NUT-04 (mint process) settings for the mint
///
@@ -46,14 +46,19 @@ pub async fn update_nut04(
client: &mut CdkMintClient<Channel>,
sub_command_args: &UpdateNut04Command,
) -> Result<()> {
// Create options if description is set
let options = sub_command_args
.description
.map(|description| MintMethodOptions { description });
let _response = client
.update_nut04(Request::new(UpdateNut04Request {
method: sub_command_args.method.clone(),
unit: sub_command_args.unit.clone(),
disabled: sub_command_args.disabled,
min: sub_command_args.min_amount,
max: sub_command_args.max_amount,
description: sub_command_args.description,
min_amount: sub_command_args.min_amount,
max_amount: sub_command_args.max_amount,
options,
}))
.await?;

View File

@@ -4,7 +4,7 @@ use tonic::transport::Channel;
use tonic::Request;
use crate::cdk_mint_client::CdkMintClient;
use crate::UpdateNut05Request;
use crate::{MeltMethodOptions, UpdateNut05Request};
/// Command to update NUT-05 (melt process) settings for the mint
///
@@ -30,6 +30,9 @@ pub struct UpdateNut05Command {
/// Whether this melt method is disabled (true) or enabled (false)
#[arg(long)]
disabled: Option<bool>,
/// Whether amountless bolt11 invoices are allowed
#[arg(long)]
amountless: Option<bool>,
}
/// Executes the update_nut05 command against the mint server
@@ -43,13 +46,19 @@ pub async fn update_nut05(
client: &mut CdkMintClient<Channel>,
sub_command_args: &UpdateNut05Command,
) -> Result<()> {
// Create options if amountless is set
let options = sub_command_args
.amountless
.map(|amountless| MeltMethodOptions { amountless });
let _response = client
.update_nut05(Request::new(UpdateNut05Request {
method: sub_command_args.method.clone(),
unit: sub_command_args.unit.clone(),
disabled: sub_command_args.disabled,
min: sub_command_args.min_amount,
max: sub_command_args.max_amount,
min_amount: sub_command_args.min_amount,
max_amount: sub_command_args.max_amount,
options,
}))
.await?;

View File

@@ -72,22 +72,33 @@ message UpdateContactRequest {
string info = 2;
}
message MintMethodOptions {
// Bolt11 options
bool description = 1;
}
message UpdateNut04Request {
string unit = 1;
string method = 2;
optional bool disabled = 3;
optional uint64 min = 4;
optional uint64 max = 5;
optional bool description = 6;
optional uint64 min_amount = 4;
optional uint64 max_amount = 5;
optional MintMethodOptions options = 6;
}
message MeltMethodOptions {
// Bolt11 options
bool amountless = 1;
}
message UpdateNut05Request {
string unit = 1;
string method = 2;
optional bool disabled = 3;
optional uint64 min = 4;
optional uint64 max = 5;
optional uint64 min_amount = 4;
optional uint64 max_amount = 5;
optional MeltMethodOptions options = 6;
}
message UpdateQuoteTtlRequest {

View File

@@ -465,22 +465,29 @@ impl CdkMint for MintRPCServer {
let mut methods = nut04_settings.methods.clone();
// Create options from the request
let options = if let Some(options) = request_inner.options {
Some(cdk::nuts::nut04::MintMethodOptions::Bolt11 {
description: options.description,
})
} else if let Some(current_settings) = current_nut04_settings.as_ref() {
current_settings.options.clone()
} else {
None
};
let updated_method_settings = MintMethodSettings {
method: payment_method,
unit,
min_amount: request_inner
.min
.min_amount
.map(Amount::from)
.or_else(|| current_nut04_settings.as_ref().and_then(|s| s.min_amount)),
max_amount: request_inner
.max
.max_amount
.map(Amount::from)
.or_else(|| current_nut04_settings.as_ref().and_then(|s| s.max_amount)),
description: request_inner.description.unwrap_or(
current_nut04_settings
.map(|c| c.description)
.unwrap_or_default(),
),
options,
};
methods.push(updated_method_settings);
@@ -529,21 +536,29 @@ impl CdkMint for MintRPCServer {
let mut methods = nut05_settings.methods;
// Create options from the request
let options = if let Some(options) = request_inner.options {
Some(cdk::nuts::nut05::MeltMethodOptions::Bolt11 {
amountless: options.amountless,
})
} else if let Some(current_settings) = current_nut05_settings.as_ref() {
current_settings.options.clone()
} else {
None
};
let updated_method_settings = MeltMethodSettings {
method: payment_method,
unit,
min_amount: request_inner
.min
.min_amount
.map(Amount::from)
.or_else(|| current_nut05_settings.as_ref().and_then(|s| s.min_amount)),
max_amount: request_inner
.max
.max_amount
.map(Amount::from)
.or_else(|| current_nut05_settings.as_ref().and_then(|s| s.max_amount)),
amountless: current_nut05_settings
.as_ref()
.map(|s| s.amountless)
.unwrap_or_default(),
options,
};
methods.push(updated_method_settings);

View File

@@ -83,16 +83,16 @@ impl From<cdk_common::payment::PaymentQuoteResponse> for PaymentQuoteResponse {
}
}
impl From<cdk_common::nut05::MeltOptions> for MeltOptions {
fn from(value: cdk_common::nut05::MeltOptions) -> Self {
impl From<cdk_common::nut23::MeltOptions> for MeltOptions {
fn from(value: cdk_common::nut23::MeltOptions) -> Self {
Self {
options: Some(value.into()),
}
}
}
impl From<cdk_common::nut05::MeltOptions> for Options {
fn from(value: cdk_common::nut05::MeltOptions) -> Self {
impl From<cdk_common::nut23::MeltOptions> for Options {
fn from(value: cdk_common::nut23::MeltOptions) -> Self {
match value {
cdk_common::MeltOptions::Mpp { mpp } => Self::Mpp(Mpp {
amount: mpp.amount.into(),
@@ -104,7 +104,7 @@ impl From<cdk_common::nut05::MeltOptions> for Options {
}
}
impl From<MeltOptions> for cdk_common::nut05::MeltOptions {
impl From<MeltOptions> for cdk_common::nut23::MeltOptions {
fn from(value: MeltOptions) -> Self {
let options = value.options.expect("option defined");
match options {
@@ -152,8 +152,8 @@ impl From<cdk_common::nut05::QuoteState> for QuoteState {
}
}
impl From<cdk_common::nut04::QuoteState> for QuoteState {
fn from(value: cdk_common::nut04::QuoteState) -> Self {
impl From<cdk_common::nut23::QuoteState> for QuoteState {
fn from(value: cdk_common::nut23::QuoteState) -> Self {
match value {
cdk_common::MintQuoteState::Unpaid => Self::Unpaid,
cdk_common::MintQuoteState::Paid => Self::Paid,

View File

@@ -18,8 +18,8 @@ use cdk_common::nut00::ProofsMethods;
use cdk_common::state::check_state_transition;
use cdk_common::util::unix_time;
use cdk_common::{
BlindSignature, CurrencyUnit, Id, MeltBolt11Request, MeltQuoteState, MintInfo, MintQuoteState,
Proof, Proofs, PublicKey, State,
BlindSignature, CurrencyUnit, Id, MeltQuoteState, MeltRequest, MintInfo, MintQuoteState, Proof,
Proofs, PublicKey, State,
};
use migrations::{migrate_01_to_02, migrate_04_to_05};
use redb::{Database, MultimapTableDefinition, ReadableTable, TableDefinition};
@@ -543,7 +543,7 @@ impl MintQuotesDatabase for MintRedbDatabase {
/// Add melt request
async fn add_melt_request(
&self,
melt_request: MeltBolt11Request<Uuid>,
melt_request: MeltRequest<Uuid>,
ln_key: PaymentProcessorKey,
) -> Result<(), Self::Err> {
let write_txn = self.db.begin_write().map_err(Error::from)?;
@@ -565,7 +565,7 @@ impl MintQuotesDatabase for MintRedbDatabase {
async fn get_melt_request(
&self,
quote_id: &Uuid,
) -> Result<Option<(MeltBolt11Request<Uuid>, PaymentProcessorKey)>, Self::Err> {
) -> Result<Option<(MeltRequest<Uuid>, PaymentProcessorKey)>, Self::Err> {
let read_txn = self.db.begin_read().map_err(Error::from)?;
let table = read_txn.open_table(MELT_REQUESTS).map_err(Error::from)?;

View File

@@ -26,6 +26,9 @@ pub enum Error {
/// NUT07 Error
#[error(transparent)]
CDKNUT07(#[from] cdk_common::nuts::nut07::Error),
/// NUT23 Error
#[error(transparent)]
CDKNUT23(#[from] cdk_common::nuts::nut23::Error),
/// Secret Error
#[error(transparent)]
CDKSECRET(#[from] cdk_common::secret::Error),

View File

@@ -6,7 +6,7 @@ use cdk_common::database::{
self, MintDatabase, MintKeysDatabase, MintProofsDatabase, MintQuotesDatabase,
};
use cdk_common::mint::{self, MintKeySetInfo, MintQuote};
use cdk_common::nuts::{CurrencyUnit, Id, MeltBolt11Request, Proofs};
use cdk_common::nuts::{CurrencyUnit, Id, MeltRequest, Proofs};
use cdk_common::MintInfo;
use uuid::Uuid;
@@ -30,7 +30,7 @@ pub async fn new_with_state(
melt_quotes: Vec<mint::MeltQuote>,
pending_proofs: Proofs,
spent_proofs: Proofs,
melt_request: Vec<(MeltBolt11Request<Uuid>, PaymentProcessorKey)>,
melt_request: Vec<(MeltRequest<Uuid>, PaymentProcessorKey)>,
mint_info: MintInfo,
) -> Result<MintSqliteDatabase, database::Error> {
let db = empty().await?;

View File

@@ -18,9 +18,8 @@ use cdk_common::secret::Secret;
use cdk_common::state::check_state_transition;
use cdk_common::util::unix_time;
use cdk_common::{
Amount, BlindSignature, BlindSignatureDleq, CurrencyUnit, Id, MeltBolt11Request,
MeltQuoteState, MintInfo, MintQuoteState, PaymentMethod, Proof, Proofs, PublicKey, SecretKey,
State,
Amount, BlindSignature, BlindSignatureDleq, CurrencyUnit, Id, MeltQuoteState, MeltRequest,
MintInfo, MintQuoteState, PaymentMethod, Proof, Proofs, PublicKey, SecretKey, State,
};
use error::Error;
use lightning_invoice::Bolt11Invoice;
@@ -946,7 +945,7 @@ WHERE id=?
async fn add_melt_request(
&self,
melt_request: MeltBolt11Request<Uuid>,
melt_request: MeltRequest<Uuid>,
ln_key: PaymentProcessorKey,
) -> Result<(), Self::Err> {
let mut transaction = self.pool.begin().await.map_err(Error::from)?;
@@ -990,7 +989,7 @@ ON CONFLICT(id) DO UPDATE SET
async fn get_melt_request(
&self,
quote_id: &Uuid,
) -> Result<Option<(MeltBolt11Request<Uuid>, PaymentProcessorKey)>, Self::Err> {
) -> Result<Option<(MeltRequest<Uuid>, PaymentProcessorKey)>, Self::Err> {
let mut transaction = self.pool.begin().await.map_err(Error::from)?;
let rec = sqlx::query(
@@ -1818,14 +1817,14 @@ fn sqlite_row_to_blind_signature(row: SqliteRow) -> Result<BlindSignature, Error
fn sqlite_row_to_melt_request(
row: SqliteRow,
) -> Result<(MeltBolt11Request<Uuid>, PaymentProcessorKey), Error> {
) -> Result<(MeltRequest<Uuid>, PaymentProcessorKey), Error> {
let quote_id: Hyphenated = row.try_get("id").map_err(Error::from)?;
let row_inputs: String = row.try_get("inputs").map_err(Error::from)?;
let row_outputs: Option<String> = row.try_get("outputs").map_err(Error::from)?;
let row_method: String = row.try_get("method").map_err(Error::from)?;
let row_unit: String = row.try_get("unit").map_err(Error::from)?;
let melt_request = MeltBolt11Request::new(
let melt_request = MeltRequest::new(
quote_id.into_uuid(),
serde_json::from_str(&row_inputs)?,
row_outputs.and_then(|o| serde_json::from_str(&o).ok()),

View File

@@ -32,6 +32,9 @@ pub enum Error {
/// NUT07 Error
#[error(transparent)]
CDKNUT07(#[from] cdk_common::nuts::nut07::Error),
/// NUT23 Error
#[error(transparent)]
CDKNUT23(#[from] cdk_common::nuts::nut23::Error),
/// Secret Error
#[error(transparent)]
CDKSECRET(#[from] cdk_common::secret::Error),

View File

@@ -7,6 +7,8 @@ use anyhow::anyhow;
use bitcoin::bip32::DerivationPath;
use cdk_common::database::{self, MintDatabase};
use cdk_common::error::Error;
use cdk_common::nut04::MintMethodOptions;
use cdk_common::nut05::MeltMethodOptions;
use cdk_common::payment::Bolt11Settings;
use cdk_common::{nut21, nut22};
@@ -195,7 +197,9 @@ impl MintBuilder {
unit: unit.clone(),
min_amount: Some(limits.mint_min),
max_amount: Some(limits.mint_max),
options: Some(MintMethodOptions::Bolt11 {
description: settings.invoice_description,
}),
};
self.mint_info.nuts.nut04.methods.push(mint_method_settings);
@@ -206,7 +210,9 @@ impl MintBuilder {
unit,
min_amount: Some(limits.melt_min),
max_amount: Some(limits.melt_max),
options: Some(MeltMethodOptions::Bolt11 {
amountless: settings.amountless,
}),
};
self.mint_info.nuts.nut05.methods.push(melt_method_settings);
self.mint_info.nuts.nut05.disabled = false;

View File

@@ -1,7 +1,7 @@
use tracing::instrument;
use crate::mint::nut22::MintAuthRequest;
use crate::mint::{AuthToken, MintBolt11Response};
use crate::mint::{AuthToken, MintResponse};
use crate::{Amount, Error, Mint};
impl Mint {
@@ -11,7 +11,7 @@ impl Mint {
&self,
auth_token: AuthToken,
mint_auth_request: MintAuthRequest,
) -> Result<MintBolt11Response, Error> {
) -> Result<MintResponse, Error> {
let cat = if let AuthToken::ClearAuth(cat) = auth_token {
cat
} else {
@@ -47,7 +47,7 @@ impl Mint {
blind_signatures.push(blind_signature);
}
Ok(MintBolt11Response {
Ok(MintResponse {
signatures: blind_signatures,
})
}

View File

@@ -3,8 +3,8 @@ use tracing::instrument;
use uuid::Uuid;
use crate::mint::{
CurrencyUnit, MintBolt11Request, MintBolt11Response, MintQuote, MintQuoteBolt11Request,
MintQuoteBolt11Response, MintQuoteState, NotificationPayload, PublicKey, Verification,
CurrencyUnit, MintQuote, MintQuoteBolt11Request, MintQuoteBolt11Response, MintQuoteState,
MintRequest, MintResponse, NotificationPayload, PublicKey, Verification,
};
use crate::nuts::PaymentMethod;
use crate::util::unix_time;
@@ -236,8 +236,8 @@ impl Mint {
#[instrument(skip_all)]
pub async fn process_mint_request(
&self,
mint_request: MintBolt11Request<Uuid>,
) -> Result<MintBolt11Response, Error> {
mint_request: MintRequest<Uuid>,
) -> Result<MintResponse, Error> {
let mint_quote = self
.localstore
.get_mint_quote(&mint_request.quote)
@@ -332,7 +332,7 @@ impl Mint {
self.pubsub_manager
.mint_quote_bolt11_status(mint_quote, MintQuoteState::Issued);
Ok(MintBolt11Response {
Ok(MintResponse {
signatures: blind_signatures,
})
}

View File

@@ -2,14 +2,15 @@ use std::str::FromStr;
use anyhow::bail;
use cdk_common::nut00::ProofsMethods;
use cdk_common::nut05::MeltMethodOptions;
use cdk_common::MeltOptions;
use lightning_invoice::Bolt11Invoice;
use tracing::instrument;
use uuid::Uuid;
use super::{
CurrencyUnit, MeltBolt11Request, MeltQuote, MeltQuoteBolt11Request, MeltQuoteBolt11Response,
Mint, PaymentMethod, PublicKey, State,
CurrencyUnit, MeltQuote, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltRequest, Mint,
PaymentMethod, PublicKey, State,
};
use crate::amount::to_unit;
use crate::cdk_payment::{MakePaymentResponse, MintPayment};
@@ -62,7 +63,10 @@ impl Mint {
amount
}
Some(MeltOptions::Amountless { amountless: _ }) => {
if !settings.amountless {
if !matches!(
settings.options,
Some(MeltMethodOptions::Bolt11 { amountless: true })
) {
return Err(Error::AmountlessInvoiceNotSupported(unit, method));
}
@@ -235,7 +239,7 @@ impl Mint {
pub async fn check_melt_expected_ln_fees(
&self,
melt_quote: &MeltQuote,
melt_request: &MeltBolt11Request<Uuid>,
melt_request: &MeltRequest<Uuid>,
) -> Result<Option<Amount>, Error> {
let invoice = Bolt11Invoice::from_str(&melt_quote.request)?;
@@ -291,7 +295,7 @@ impl Mint {
#[instrument(skip_all)]
pub async fn verify_melt_request(
&self,
melt_request: &MeltBolt11Request<Uuid>,
melt_request: &MeltRequest<Uuid>,
) -> Result<MeltQuote, Error> {
let state = self
.localstore
@@ -377,10 +381,7 @@ impl Mint {
/// made The proofs should be returned to an unspent state and the
/// quote should be unpaid
#[instrument(skip_all)]
pub async fn process_unpaid_melt(
&self,
melt_request: &MeltBolt11Request<Uuid>,
) -> Result<(), Error> {
pub async fn process_unpaid_melt(&self, melt_request: &MeltRequest<Uuid>) -> Result<(), Error> {
let input_ys = melt_request.inputs().ys()?;
self.localstore
@@ -408,7 +409,7 @@ impl Mint {
#[instrument(skip_all)]
pub async fn melt_bolt11(
&self,
melt_request: &MeltBolt11Request<Uuid>,
melt_request: &MeltRequest<Uuid>,
) -> Result<MeltQuoteBolt11Response<Uuid>, Error> {
use std::sync::Arc;
async fn check_payment_state(
@@ -619,7 +620,7 @@ impl Mint {
#[instrument(skip_all)]
pub async fn process_melt_request(
&self,
melt_request: &MeltBolt11Request<Uuid>,
melt_request: &MeltRequest<Uuid>,
payment_preimage: Option<String>,
total_spent: Amount,
) -> Result<MeltQuoteBolt11Response<Uuid>, Error> {

View File

@@ -482,7 +482,7 @@ impl Mint {
pub async fn handle_internal_melt_mint(
&self,
melt_quote: &MeltQuote,
melt_request: &MeltBolt11Request<Uuid>,
melt_request: &MeltRequest<Uuid>,
) -> Result<Option<Amount>, Error> {
let mint_quote = match self
.localstore
@@ -761,7 +761,7 @@ mod tests {
seed: &'a [u8],
mint_info: MintInfo,
supported_units: HashMap<CurrencyUnit, (u64, u8)>,
melt_requests: Vec<(MeltBolt11Request<Uuid>, PaymentProcessorKey)>,
melt_requests: Vec<(MeltRequest<Uuid>, PaymentProcessorKey)>,
}
async fn create_mint(config: MintConfig<'_>) -> Mint {

View File

@@ -4,7 +4,7 @@ use async_trait::async_trait;
use cdk_common::{AuthToken, MintInfo};
use super::Error;
use crate::nuts::{Id, KeySet, KeysetResponse, MintAuthRequest, MintBolt11Response};
use crate::nuts::{Id, KeySet, KeysetResponse, MintAuthRequest, MintResponse};
/// Interface that connects a wallet to a mint. Typically represents an HttpClient.
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
@@ -23,8 +23,5 @@ pub trait AuthMintConnector: Debug {
/// Get Blind Auth keysets
async fn get_mint_blind_auth_keysets(&self) -> Result<KeysetResponse, Error>;
/// Post mint blind auth
async fn post_mint_blind_auth(
&self,
request: MintAuthRequest,
) -> Result<MintBolt11Response, Error>;
async fn post_mint_blind_auth(&self, request: MintAuthRequest) -> Result<MintResponse, Error>;
}

View File

@@ -9,7 +9,7 @@ use super::MeltQuote;
use crate::amount::to_unit;
use crate::dhke::construct_proofs;
use crate::nuts::{
CurrencyUnit, MeltBolt11Request, MeltOptions, MeltQuoteBolt11Request, MeltQuoteBolt11Response,
CurrencyUnit, MeltOptions, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltRequest,
PreMintSecrets, Proofs, ProofsMethods, State,
};
use crate::types::{Melted, ProofInfo};
@@ -152,7 +152,7 @@ impl Wallet {
proofs_total - quote_info.amount,
)?;
let request = MeltBolt11Request::new(
let request = MeltRequest::new(
quote_id.to_string(),
proofs.clone(),
Some(premint_secrets.blinded_messages()),

View File

@@ -1,6 +1,6 @@
use std::collections::HashMap;
use cdk_common::ensure_cdk;
use cdk_common::nut04::MintMethodOptions;
use cdk_common::wallet::{Transaction, TransactionDirection};
use tracing::instrument;
@@ -9,8 +9,8 @@ use crate::amount::SplitTarget;
use crate::dhke::construct_proofs;
use crate::nuts::nut00::ProofsMethods;
use crate::nuts::{
nut12, MintBolt11Request, MintQuoteBolt11Request, MintQuoteBolt11Response, PreMintSecrets,
Proofs, SecretKey, SpendingConditions, State,
nut12, MintQuoteBolt11Request, MintQuoteBolt11Response, MintRequest, PreMintSecrets, Proofs,
SecretKey, SpendingConditions, State,
};
use crate::types::ProofInfo;
use crate::util::unix_time;
@@ -64,7 +64,10 @@ impl Wallet {
.get_settings(&unit, &crate::nuts::PaymentMethod::Bolt11)
.ok_or(Error::UnsupportedUnit)?;
ensure_cdk!(settings.description, Error::InvoiceDescriptionUnsupported);
match settings.options {
Some(MintMethodOptions::Bolt11 { description }) if description => (),
_ => return Err(Error::InvoiceDescriptionUnsupported),
}
}
let secret_key = SecretKey::generate();
@@ -224,7 +227,7 @@ impl Wallet {
)?,
};
let mut request = MintBolt11Request {
let mut request = MintRequest {
quote: quote_id.to_string(),
outputs: premint_secrets.blinded_messages(),
signature: None,

View File

@@ -20,9 +20,9 @@ use crate::mint_url::MintUrl;
use crate::nuts::nut22::MintAuthRequest;
use crate::nuts::{
AuthToken, CheckStateRequest, CheckStateResponse, Id, KeySet, KeysResponse, KeysetResponse,
MeltBolt11Request, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintBolt11Request,
MintBolt11Response, MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response, RestoreRequest,
RestoreResponse, SwapRequest, SwapResponse,
MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltRequest, MintInfo, MintQuoteBolt11Request,
MintQuoteBolt11Response, MintRequest, MintResponse, RestoreRequest, RestoreResponse,
SwapRequest, SwapResponse,
};
#[cfg(feature = "auth")]
use crate::wallet::auth::{AuthMintConnector, AuthWallet};
@@ -263,10 +263,7 @@ impl MintConnector for HttpClient {
/// Mint Tokens [NUT-04]
#[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
async fn post_mint(
&self,
request: MintBolt11Request<String>,
) -> Result<MintBolt11Response, Error> {
async fn post_mint(&self, request: MintRequest<String>) -> Result<MintResponse, Error> {
let url = self.mint_url.join_paths(&["v1", "mint", "bolt11"])?;
#[cfg(feature = "auth")]
let auth_token = self
@@ -322,7 +319,7 @@ impl MintConnector for HttpClient {
#[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
async fn post_melt(
&self,
request: MeltBolt11Request<String>,
request: MeltRequest<String>,
) -> Result<MeltQuoteBolt11Response<String>, Error> {
let url = self.mint_url.join_paths(&["v1", "melt", "bolt11"])?;
#[cfg(feature = "auth")]
@@ -469,10 +466,7 @@ impl AuthMintConnector for AuthHttpClient {
/// Mint Tokens [NUT-22]
#[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
async fn post_mint_blind_auth(
&self,
request: MintAuthRequest,
) -> Result<MintBolt11Response, Error> {
async fn post_mint_blind_auth(&self, request: MintAuthRequest) -> Result<MintResponse, Error> {
let url = self.mint_url.join_paths(&["v1", "auth", "blind", "mint"])?;
self.core
.http_post(url, Some(self.cat.read().await.clone()), &request)

View File

@@ -6,9 +6,9 @@ use async_trait::async_trait;
use super::Error;
use crate::nuts::{
CheckStateRequest, CheckStateResponse, Id, KeySet, KeysetResponse, MeltBolt11Request,
MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintBolt11Request, MintBolt11Response,
MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response, RestoreRequest, RestoreResponse,
CheckStateRequest, CheckStateResponse, Id, KeySet, KeysetResponse, MeltQuoteBolt11Request,
MeltQuoteBolt11Response, MeltRequest, MintInfo, MintQuoteBolt11Request,
MintQuoteBolt11Response, MintRequest, MintResponse, RestoreRequest, RestoreResponse,
SwapRequest, SwapResponse,
};
#[cfg(feature = "auth")]
@@ -41,10 +41,7 @@ pub trait MintConnector: Debug {
quote_id: &str,
) -> Result<MintQuoteBolt11Response<String>, Error>;
/// Mint Tokens [NUT-04]
async fn post_mint(
&self,
request: MintBolt11Request<String>,
) -> Result<MintBolt11Response, Error>;
async fn post_mint(&self, request: MintRequest<String>) -> Result<MintResponse, Error>;
/// Melt Quote [NUT-05]
async fn post_melt_quote(
&self,
@@ -59,7 +56,7 @@ pub trait MintConnector: Debug {
/// [Nut-08] Lightning fee return if outputs defined
async fn post_melt(
&self,
request: MeltBolt11Request<String>,
request: MeltRequest<String>,
) -> Result<MeltQuoteBolt11Response<String>, Error>;
/// Split Token [NUT-06]
async fn post_swap(&self, request: SwapRequest) -> Result<SwapResponse, Error>;

View File

@@ -7,7 +7,7 @@ use tokio::time;
use super::WsSubscriptionBody;
use crate::nuts::nut17::Kind;
use crate::nuts::{nut01, nut04, nut05, nut07, CheckStateRequest, NotificationPayload};
use crate::nuts::{nut01, nut05, nut07, nut23, CheckStateRequest, NotificationPayload};
use crate::pub_sub::SubId;
use crate::wallet::MintConnector;
use crate::Wallet;
@@ -21,7 +21,7 @@ enum UrlType {
#[derive(Debug, Eq, PartialEq)]
enum AnyState {
MintQuoteState(nut04::QuoteState),
MintQuoteState(nut23::QuoteState),
MeltQuoteState(nut05::QuoteState),
PublicKey(nut07::State),
Empty,