mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-19 05:35:18 +01:00
refactor: nut04 and nut05 (#749)
This commit is contained in:
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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());
|
||||
|
||||
411
crates/cashu/src/nuts/nut23.rs
Normal file
411
crates/cashu/src/nuts/nut23.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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")]
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user