|
@@ -1,20 +1,31 @@
|
|
|
-//! Notation and Models
|
|
|
-// https://github.com/cashubtc/nuts/blob/main/00.md
|
|
|
+//! NUT-00: Notation and Models
|
|
|
+//!
|
|
|
+//! <https://github.com/cashubtc/nuts/blob/main/00.md>
|
|
|
|
|
|
+use std::cmp::Ordering;
|
|
|
use std::fmt;
|
|
|
-use std::hash::{self, Hasher};
|
|
|
+use std::hash::{Hash, Hasher};
|
|
|
use std::str::FromStr;
|
|
|
use std::string::FromUtf8Error;
|
|
|
|
|
|
-use serde::{Deserialize, Serialize};
|
|
|
+use base64::engine::{general_purpose, GeneralPurpose};
|
|
|
+use base64::{alphabet, Engine as _};
|
|
|
+use serde::{Deserialize, Deserializer, Serialize};
|
|
|
use thiserror::Error;
|
|
|
+use url::Url;
|
|
|
|
|
|
-use super::{BlindSignatureDleq, Id, ProofDleq, Proofs, PublicKey, Signatures};
|
|
|
-use crate::nuts::nut11::{witness_deserialize, witness_serialize};
|
|
|
+use crate::dhke::blind_message;
|
|
|
+use crate::nuts::nut01::{PublicKey, SecretKey};
|
|
|
+use crate::nuts::nut11::{witness_deserialize, witness_serialize, Signatures};
|
|
|
+use crate::nuts::nut12::BlindSignatureDleq;
|
|
|
+use crate::nuts::{Id, P2PKConditions, ProofDleq};
|
|
|
use crate::secret::Secret;
|
|
|
use crate::url::UncheckedUrl;
|
|
|
use crate::Amount;
|
|
|
|
|
|
+/// List of [Proof]
|
|
|
+pub type Proofs = Vec<Proof>;
|
|
|
+
|
|
|
#[derive(Debug, Error)]
|
|
|
pub enum Error {
|
|
|
/// Proofs required
|
|
@@ -46,18 +57,26 @@ pub enum Error {
|
|
|
NUT11(#[from] crate::nuts::nut11::Error),
|
|
|
}
|
|
|
|
|
|
-/// Blinded Message [NUT-00]
|
|
|
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
+/// Blinded Message (also called `output`)
|
|
|
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
|
pub struct BlindedMessage {
|
|
|
/// Amount
|
|
|
+ ///
|
|
|
+ /// The value for the requested [BlindSignature]
|
|
|
pub amount: Amount,
|
|
|
- /// Keyset Id
|
|
|
+ /// Keyset ID
|
|
|
+ ///
|
|
|
+ /// ID from which we expect a signature.
|
|
|
#[serde(rename = "id")]
|
|
|
pub keyset_id: Id,
|
|
|
/// Blinded secret message (B_)
|
|
|
+ ///
|
|
|
+ /// The blinded secret message generated by the sender.
|
|
|
#[serde(rename = "B_")]
|
|
|
- pub b: PublicKey,
|
|
|
+ pub blinded_secret: PublicKey,
|
|
|
/// Witness
|
|
|
+ ///
|
|
|
+ /// <https://github.com/cashubtc/nuts/blob/main/11.md>
|
|
|
#[serde(default)]
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
//#[serde(serialize_with = "witness_serialize")]
|
|
@@ -66,18 +85,104 @@ pub struct BlindedMessage {
|
|
|
}
|
|
|
|
|
|
impl BlindedMessage {
|
|
|
- pub fn new(amount: Amount, keyset_id: Id, b: PublicKey) -> Self {
|
|
|
+ /// Compose new blinded message
|
|
|
+ #[inline]
|
|
|
+ pub fn new(amount: Amount, keyset_id: Id, blinded_secret: PublicKey) -> Self {
|
|
|
Self {
|
|
|
amount,
|
|
|
keyset_id,
|
|
|
- b,
|
|
|
+ blinded_secret,
|
|
|
witness: None,
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ /// Add witness
|
|
|
+ pub fn witness(mut self, witness: Signatures) -> Self {
|
|
|
+ self.witness = Some(witness);
|
|
|
+ self
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/// Blind Signature (also called `promise`)
|
|
|
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
+pub struct BlindSignature {
|
|
|
+ /// Amount
|
|
|
+ ///
|
|
|
+ /// The value of the blinded token.
|
|
|
+ pub amount: Amount,
|
|
|
+ /// Keyset ID
|
|
|
+ ///
|
|
|
+ /// ID of the mint keys that signed the token.
|
|
|
+ #[serde(rename = "id")]
|
|
|
+ pub keyset_id: Id,
|
|
|
+ /// Blinded signature (C_)
|
|
|
+ ///
|
|
|
+ /// The blinded signature on the secret message `B_` of [BlindedMessage].
|
|
|
+ #[serde(rename = "C_")]
|
|
|
+ pub c: PublicKey,
|
|
|
+ /// DLEQ Proof
|
|
|
+ ///
|
|
|
+ /// <https://github.com/cashubtc/nuts/blob/main/12.md>
|
|
|
+ #[serde(default)]
|
|
|
+ #[serde(skip_serializing_if = "Option::is_none")]
|
|
|
+ pub dleq: Option<BlindSignatureDleq>,
|
|
|
+}
|
|
|
+
|
|
|
+/// Proofs
|
|
|
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
+pub struct Proof {
|
|
|
+ /// Amount
|
|
|
+ pub amount: Amount,
|
|
|
+ /// `Keyset id`
|
|
|
+ #[serde(rename = "id")]
|
|
|
+ pub keyset_id: Id,
|
|
|
+ /// Secret message
|
|
|
+ pub secret: Secret,
|
|
|
+ /// Unblinded signature
|
|
|
+ #[serde(rename = "C")]
|
|
|
+ pub c: PublicKey,
|
|
|
+ /// Witness
|
|
|
+ #[serde(default)]
|
|
|
+ #[serde(skip_serializing_if = "Option::is_none")]
|
|
|
+ #[serde(serialize_with = "witness_serialize")]
|
|
|
+ #[serde(deserialize_with = "witness_deserialize")]
|
|
|
+ pub witness: Option<Signatures>,
|
|
|
+ /// DLEQ Proof
|
|
|
+ pub dleq: Option<ProofDleq>,
|
|
|
+}
|
|
|
+
|
|
|
+impl Proof {
|
|
|
+ pub fn new(amount: Amount, keyset_id: Id, secret: Secret, c: PublicKey) -> Self {
|
|
|
+ Proof {
|
|
|
+ amount,
|
|
|
+ keyset_id,
|
|
|
+ secret,
|
|
|
+ c,
|
|
|
+ witness: None,
|
|
|
+ dleq: None,
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl Hash for Proof {
|
|
|
+ fn hash<H: Hasher>(&self, state: &mut H) {
|
|
|
+ self.secret.hash(state);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl Ord for Proof {
|
|
|
+ fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
|
|
+ self.amount.cmp(&other.amount)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl PartialOrd for Proof {
|
|
|
+ fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
|
|
+ Some(self.cmp(other))
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, hash::Hash)]
|
|
|
-#[serde(rename_all = "lowercase")]
|
|
|
+#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
|
|
|
pub enum CurrencyUnit {
|
|
|
#[default]
|
|
|
Sat,
|
|
@@ -85,14 +190,15 @@ pub enum CurrencyUnit {
|
|
|
Custom(String),
|
|
|
}
|
|
|
|
|
|
-impl FromStr for CurrencyUnit {
|
|
|
- type Err = Error;
|
|
|
-
|
|
|
- fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
|
- match s {
|
|
|
- "sat" => Ok(Self::Sat),
|
|
|
- "usd" => Ok(Self::Usd),
|
|
|
- _ => Ok(Self::Custom(s.to_string())),
|
|
|
+impl<S> From<S> for CurrencyUnit
|
|
|
+where
|
|
|
+ S: AsRef<str>,
|
|
|
+{
|
|
|
+ fn from(currency: S) -> Self {
|
|
|
+ match currency.as_ref() {
|
|
|
+ "sat" => Self::Sat,
|
|
|
+ "usd" => Self::Usd,
|
|
|
+ o => Self::Custom(o.to_string()),
|
|
|
}
|
|
|
}
|
|
|
}
|
|
@@ -102,26 +208,45 @@ impl fmt::Display for CurrencyUnit {
|
|
|
match self {
|
|
|
CurrencyUnit::Sat => write!(f, "sat"),
|
|
|
CurrencyUnit::Usd => write!(f, "usd"),
|
|
|
- CurrencyUnit::Custom(unit) => write!(f, "{}", unit),
|
|
|
+ CurrencyUnit::Custom(unit) => write!(f, "{unit}"),
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-#[derive(Default, Deserialize, Serialize, Debug, PartialEq, Eq, Clone, Hash)]
|
|
|
-#[serde(rename_all = "lowercase")]
|
|
|
+impl Serialize for CurrencyUnit {
|
|
|
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
|
+ where
|
|
|
+ S: serde::Serializer,
|
|
|
+ {
|
|
|
+ serializer.serialize_str(&self.to_string())
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl<'de> Deserialize<'de> for CurrencyUnit {
|
|
|
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
|
+ where
|
|
|
+ D: Deserializer<'de>,
|
|
|
+ {
|
|
|
+ let currency: String = String::deserialize(deserializer)?;
|
|
|
+ Ok(Self::from(currency))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
|
|
|
pub enum PaymentMethod {
|
|
|
#[default]
|
|
|
Bolt11,
|
|
|
Custom(String),
|
|
|
}
|
|
|
|
|
|
-impl FromStr for PaymentMethod {
|
|
|
- type Err = Error;
|
|
|
-
|
|
|
- fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
|
- match s {
|
|
|
- "bolt11" => Ok(Self::Bolt11),
|
|
|
- _ => Ok(Self::Custom(s.to_string())),
|
|
|
+impl<S> From<S> for PaymentMethod
|
|
|
+where
|
|
|
+ S: AsRef<str>,
|
|
|
+{
|
|
|
+ fn from(method: S) -> Self {
|
|
|
+ match method.as_ref() {
|
|
|
+ "bolt11" => Self::Bolt11,
|
|
|
+ o => Self::Custom(o.to_string()),
|
|
|
}
|
|
|
}
|
|
|
}
|
|
@@ -135,285 +260,285 @@ impl fmt::Display for PaymentMethod {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-#[cfg(feature = "wallet")]
|
|
|
-pub mod wallet {
|
|
|
- use std::cmp::Ordering;
|
|
|
- use std::fmt;
|
|
|
- use std::str::FromStr;
|
|
|
+impl Serialize for PaymentMethod {
|
|
|
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
|
+ where
|
|
|
+ S: serde::Serializer,
|
|
|
+ {
|
|
|
+ serializer.serialize_str(&self.to_string())
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
- use base64::engine::{general_purpose, GeneralPurpose};
|
|
|
- use base64::{alphabet, Engine as _};
|
|
|
- use serde::{Deserialize, Serialize};
|
|
|
- use url::Url;
|
|
|
-
|
|
|
- use super::{CurrencyUnit, MintProofs, *};
|
|
|
- use crate::dhke::blind_message;
|
|
|
- use crate::nuts::{BlindedMessage, Id, P2PKConditions, Proofs, SecretKey};
|
|
|
- use crate::secret::Secret;
|
|
|
- use crate::url::UncheckedUrl;
|
|
|
- use crate::Amount;
|
|
|
-
|
|
|
- #[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
|
|
- pub struct PreMint {
|
|
|
- /// Blinded message
|
|
|
- pub blinded_message: BlindedMessage,
|
|
|
- /// Secret
|
|
|
- pub secret: Secret,
|
|
|
- /// R
|
|
|
- pub r: SecretKey,
|
|
|
- /// Amount
|
|
|
- pub amount: Amount,
|
|
|
- }
|
|
|
-
|
|
|
- impl Ord for PreMint {
|
|
|
- fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
|
|
- self.amount.cmp(&other.amount)
|
|
|
- }
|
|
|
+impl<'de> Deserialize<'de> for PaymentMethod {
|
|
|
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
|
+ where
|
|
|
+ D: Deserializer<'de>,
|
|
|
+ {
|
|
|
+ let payment_method: String = String::deserialize(deserializer)?;
|
|
|
+ Ok(Self::from(payment_method))
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- impl PartialOrd for PreMint {
|
|
|
- fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
|
|
- Some(self.cmp(other))
|
|
|
- }
|
|
|
+#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
|
|
+pub struct PreMint {
|
|
|
+ /// Blinded message
|
|
|
+ pub blinded_message: BlindedMessage,
|
|
|
+ /// Secret
|
|
|
+ pub secret: Secret,
|
|
|
+ /// R
|
|
|
+ pub r: SecretKey,
|
|
|
+ /// Amount
|
|
|
+ pub amount: Amount,
|
|
|
+}
|
|
|
+
|
|
|
+impl Ord for PreMint {
|
|
|
+ fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
|
|
+ self.amount.cmp(&other.amount)
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)]
|
|
|
- pub struct PreMintSecrets {
|
|
|
- pub secrets: Vec<PreMint>,
|
|
|
+impl PartialOrd for PreMint {
|
|
|
+ fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
|
|
+ Some(self.cmp(other))
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- impl PreMintSecrets {
|
|
|
- /// Outputs for speceifed amount with random secret
|
|
|
- pub fn random(keyset_id: Id, amount: Amount) -> Result<Self, Error> {
|
|
|
- let amount_split = amount.split();
|
|
|
+#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)]
|
|
|
+pub struct PreMintSecrets {
|
|
|
+ pub secrets: Vec<PreMint>,
|
|
|
+}
|
|
|
|
|
|
- let mut output = Vec::with_capacity(amount_split.len());
|
|
|
+impl PreMintSecrets {
|
|
|
+ /// Outputs for speceifed amount with random secret
|
|
|
+ pub fn random(keyset_id: Id, amount: Amount) -> Result<Self, Error> {
|
|
|
+ let amount_split = amount.split();
|
|
|
|
|
|
- for amount in amount_split {
|
|
|
- let secret = Secret::generate();
|
|
|
- let (blinded, r) = blind_message(&secret.to_bytes(), None)?;
|
|
|
+ let mut output = Vec::with_capacity(amount_split.len());
|
|
|
|
|
|
- let blinded_message = BlindedMessage::new(amount, keyset_id, blinded);
|
|
|
+ for amount in amount_split {
|
|
|
+ let secret = Secret::generate();
|
|
|
+ let (blinded, r) = blind_message(&secret.to_bytes(), None)?;
|
|
|
|
|
|
- output.push(PreMint {
|
|
|
- secret,
|
|
|
- blinded_message,
|
|
|
- r,
|
|
|
- amount,
|
|
|
- });
|
|
|
- }
|
|
|
+ let blinded_message = BlindedMessage::new(amount, keyset_id, blinded);
|
|
|
|
|
|
- Ok(PreMintSecrets { secrets: output })
|
|
|
+ output.push(PreMint {
|
|
|
+ secret,
|
|
|
+ blinded_message,
|
|
|
+ r,
|
|
|
+ amount,
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
- pub fn from_secrets(
|
|
|
- keyset_id: Id,
|
|
|
- amounts: Vec<Amount>,
|
|
|
- secrets: Vec<Secret>,
|
|
|
- ) -> Result<Self, wallet::Error> {
|
|
|
- let mut output = Vec::with_capacity(secrets.len());
|
|
|
+ Ok(PreMintSecrets { secrets: output })
|
|
|
+ }
|
|
|
|
|
|
- for (secret, amount) in secrets.into_iter().zip(amounts) {
|
|
|
- let (blinded, r) = blind_message(&secret.to_bytes(), None)?;
|
|
|
+ pub fn from_secrets(
|
|
|
+ keyset_id: Id,
|
|
|
+ amounts: Vec<Amount>,
|
|
|
+ secrets: Vec<Secret>,
|
|
|
+ ) -> Result<Self, Error> {
|
|
|
+ let mut output = Vec::with_capacity(secrets.len());
|
|
|
|
|
|
- let blinded_message = BlindedMessage::new(amount, keyset_id, blinded);
|
|
|
+ for (secret, amount) in secrets.into_iter().zip(amounts) {
|
|
|
+ let (blinded, r) = blind_message(&secret.to_bytes(), None)?;
|
|
|
|
|
|
- output.push(PreMint {
|
|
|
- secret,
|
|
|
- blinded_message,
|
|
|
- r,
|
|
|
- amount,
|
|
|
- });
|
|
|
- }
|
|
|
+ let blinded_message = BlindedMessage::new(amount, keyset_id, blinded);
|
|
|
|
|
|
- Ok(PreMintSecrets { secrets: output })
|
|
|
+ output.push(PreMint {
|
|
|
+ secret,
|
|
|
+ blinded_message,
|
|
|
+ r,
|
|
|
+ amount,
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
- /// Blank Outputs used for NUT-08 change
|
|
|
- pub fn blank(keyset_id: Id, fee_reserve: Amount) -> Result<Self, wallet::Error> {
|
|
|
- let count = ((u64::from(fee_reserve) as f64).log2().ceil() as u64).max(1);
|
|
|
+ Ok(PreMintSecrets { secrets: output })
|
|
|
+ }
|
|
|
|
|
|
- let mut output = Vec::with_capacity(count as usize);
|
|
|
+ /// Blank Outputs used for NUT-08 change
|
|
|
+ pub fn blank(keyset_id: Id, fee_reserve: Amount) -> Result<Self, Error> {
|
|
|
+ let count = ((u64::from(fee_reserve) as f64).log2().ceil() as u64).max(1);
|
|
|
|
|
|
- for _i in 0..count {
|
|
|
- let secret = Secret::generate();
|
|
|
- let (blinded, r) = blind_message(&secret.to_bytes(), None)?;
|
|
|
+ let mut output = Vec::with_capacity(count as usize);
|
|
|
|
|
|
- let blinded_message = BlindedMessage::new(Amount::ZERO, keyset_id, blinded);
|
|
|
+ for _i in 0..count {
|
|
|
+ let secret = Secret::generate();
|
|
|
+ let (blinded, r) = blind_message(&secret.to_bytes(), None)?;
|
|
|
|
|
|
- output.push(PreMint {
|
|
|
- secret,
|
|
|
- blinded_message,
|
|
|
- r,
|
|
|
- amount: Amount::ZERO,
|
|
|
- })
|
|
|
- }
|
|
|
+ let blinded_message = BlindedMessage::new(Amount::ZERO, keyset_id, blinded);
|
|
|
|
|
|
- Ok(PreMintSecrets { secrets: output })
|
|
|
+ output.push(PreMint {
|
|
|
+ secret,
|
|
|
+ blinded_message,
|
|
|
+ r,
|
|
|
+ amount: Amount::ZERO,
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
- pub fn with_p2pk_conditions(
|
|
|
- keyset_id: Id,
|
|
|
- amount: Amount,
|
|
|
- conditions: P2PKConditions,
|
|
|
- ) -> Result<Self, wallet::Error> {
|
|
|
- let amount_split = amount.split();
|
|
|
-
|
|
|
- let mut output = Vec::with_capacity(amount_split.len());
|
|
|
+ Ok(PreMintSecrets { secrets: output })
|
|
|
+ }
|
|
|
|
|
|
- for amount in amount_split {
|
|
|
- let secret: Secret = conditions.clone().try_into()?;
|
|
|
- let (blinded, r) = blind_message(&secret.to_bytes(), None)?;
|
|
|
+ pub fn with_p2pk_conditions(
|
|
|
+ keyset_id: Id,
|
|
|
+ amount: Amount,
|
|
|
+ conditions: P2PKConditions,
|
|
|
+ ) -> Result<Self, Error> {
|
|
|
+ let amount_split = amount.split();
|
|
|
|
|
|
- let blinded_message = BlindedMessage::new(amount, keyset_id, blinded);
|
|
|
+ let mut output = Vec::with_capacity(amount_split.len());
|
|
|
|
|
|
- output.push(PreMint {
|
|
|
- secret,
|
|
|
- blinded_message,
|
|
|
- r,
|
|
|
- amount,
|
|
|
- });
|
|
|
- }
|
|
|
+ for amount in amount_split {
|
|
|
+ let secret: Secret = conditions.clone().try_into()?;
|
|
|
+ let (blinded, r) = blind_message(&secret.to_bytes(), None)?;
|
|
|
|
|
|
- Ok(PreMintSecrets { secrets: output })
|
|
|
- }
|
|
|
+ let blinded_message = BlindedMessage::new(amount, keyset_id, blinded);
|
|
|
|
|
|
- pub fn iter(&self) -> impl Iterator<Item = &PreMint> {
|
|
|
- self.secrets.iter()
|
|
|
+ output.push(PreMint {
|
|
|
+ secret,
|
|
|
+ blinded_message,
|
|
|
+ r,
|
|
|
+ amount,
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
- pub fn len(&self) -> usize {
|
|
|
- self.secrets.len()
|
|
|
- }
|
|
|
+ Ok(PreMintSecrets { secrets: output })
|
|
|
+ }
|
|
|
|
|
|
- pub fn is_empty(&self) -> bool {
|
|
|
- self.secrets.is_empty()
|
|
|
- }
|
|
|
+ pub fn iter(&self) -> impl Iterator<Item = &PreMint> {
|
|
|
+ self.secrets.iter()
|
|
|
+ }
|
|
|
|
|
|
- pub fn total_amount(&self) -> Amount {
|
|
|
- self.secrets
|
|
|
- .iter()
|
|
|
- .map(|PreMint { amount, .. }| *amount)
|
|
|
- .sum()
|
|
|
- }
|
|
|
+ pub fn len(&self) -> usize {
|
|
|
+ self.secrets.len()
|
|
|
+ }
|
|
|
|
|
|
- pub fn blinded_messages(&self) -> Vec<BlindedMessage> {
|
|
|
- self.iter().map(|pm| pm.blinded_message.clone()).collect()
|
|
|
- }
|
|
|
+ pub fn is_empty(&self) -> bool {
|
|
|
+ self.secrets.is_empty()
|
|
|
+ }
|
|
|
|
|
|
- pub fn secrets(&self) -> Vec<Secret> {
|
|
|
- self.iter().map(|pm| pm.secret.clone()).collect()
|
|
|
- }
|
|
|
+ pub fn total_amount(&self) -> Amount {
|
|
|
+ self.secrets
|
|
|
+ .iter()
|
|
|
+ .map(|PreMint { amount, .. }| *amount)
|
|
|
+ .sum()
|
|
|
+ }
|
|
|
|
|
|
- pub fn rs(&self) -> Vec<SecretKey> {
|
|
|
- self.iter().map(|pm| pm.r.clone()).collect()
|
|
|
- }
|
|
|
+ pub fn blinded_messages(&self) -> Vec<BlindedMessage> {
|
|
|
+ self.iter().map(|pm| pm.blinded_message.clone()).collect()
|
|
|
+ }
|
|
|
|
|
|
- pub fn amounts(&self) -> Vec<Amount> {
|
|
|
- self.iter().map(|pm| pm.amount).collect()
|
|
|
- }
|
|
|
+ pub fn secrets(&self) -> Vec<Secret> {
|
|
|
+ self.iter().map(|pm| pm.secret.clone()).collect()
|
|
|
+ }
|
|
|
|
|
|
- pub fn combine(&mut self, mut other: Self) {
|
|
|
- self.secrets.append(&mut other.secrets)
|
|
|
- }
|
|
|
+ pub fn rs(&self) -> Vec<SecretKey> {
|
|
|
+ self.iter().map(|pm| pm.r.clone()).collect()
|
|
|
+ }
|
|
|
|
|
|
- pub fn sort_secrets(&mut self) {
|
|
|
- self.secrets.sort();
|
|
|
- }
|
|
|
+ pub fn amounts(&self) -> Vec<Amount> {
|
|
|
+ self.iter().map(|pm| pm.amount).collect()
|
|
|
}
|
|
|
|
|
|
- // Implement Iterator for PreMintSecrets
|
|
|
- impl Iterator for PreMintSecrets {
|
|
|
- type Item = PreMint;
|
|
|
+ pub fn combine(&mut self, mut other: Self) {
|
|
|
+ self.secrets.append(&mut other.secrets)
|
|
|
+ }
|
|
|
|
|
|
- fn next(&mut self) -> Option<Self::Item> {
|
|
|
- // Use the iterator of the vector
|
|
|
- self.secrets.pop()
|
|
|
- }
|
|
|
+ pub fn sort_secrets(&mut self) {
|
|
|
+ self.secrets.sort();
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- impl Ord for PreMintSecrets {
|
|
|
- fn cmp(&self, other: &Self) -> Ordering {
|
|
|
- self.secrets.cmp(&other.secrets)
|
|
|
- }
|
|
|
+// Implement Iterator for PreMintSecrets
|
|
|
+impl Iterator for PreMintSecrets {
|
|
|
+ type Item = PreMint;
|
|
|
+
|
|
|
+ fn next(&mut self) -> Option<Self::Item> {
|
|
|
+ // Use the iterator of the vector
|
|
|
+ self.secrets.pop()
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- impl PartialOrd for PreMintSecrets {
|
|
|
- fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
|
- Some(self.cmp(other))
|
|
|
- }
|
|
|
+impl Ord for PreMintSecrets {
|
|
|
+ fn cmp(&self, other: &Self) -> Ordering {
|
|
|
+ self.secrets.cmp(&other.secrets)
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
- pub struct Token {
|
|
|
- pub token: Vec<MintProofs>,
|
|
|
- /// Memo for token
|
|
|
- #[serde(skip_serializing_if = "Option::is_none")]
|
|
|
- pub memo: Option<String>,
|
|
|
- /// Token Unit
|
|
|
- #[serde(skip_serializing_if = "Option::is_none")]
|
|
|
- pub unit: Option<CurrencyUnit>,
|
|
|
- }
|
|
|
-
|
|
|
- impl Token {
|
|
|
- pub fn new(
|
|
|
- mint_url: UncheckedUrl,
|
|
|
- proofs: Proofs,
|
|
|
- memo: Option<String>,
|
|
|
- unit: Option<CurrencyUnit>,
|
|
|
- ) -> Result<Self, Error> {
|
|
|
- if proofs.is_empty() {
|
|
|
- return Err(Error::ProofsRequired);
|
|
|
- }
|
|
|
+impl PartialOrd for PreMintSecrets {
|
|
|
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
|
+ Some(self.cmp(other))
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
- // Check Url is valid
|
|
|
- let _: Url = (&mint_url).try_into().map_err(|_| Error::InvalidUrl)?;
|
|
|
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
+pub struct Token {
|
|
|
+ pub token: Vec<MintProofs>,
|
|
|
+ /// Memo for token
|
|
|
+ #[serde(skip_serializing_if = "Option::is_none")]
|
|
|
+ pub memo: Option<String>,
|
|
|
+ /// Token Unit
|
|
|
+ #[serde(skip_serializing_if = "Option::is_none")]
|
|
|
+ pub unit: Option<CurrencyUnit>,
|
|
|
+}
|
|
|
|
|
|
- Ok(Self {
|
|
|
- token: vec![MintProofs::new(mint_url, proofs)],
|
|
|
- memo,
|
|
|
- unit,
|
|
|
- })
|
|
|
+impl Token {
|
|
|
+ pub fn new(
|
|
|
+ mint_url: UncheckedUrl,
|
|
|
+ proofs: Proofs,
|
|
|
+ memo: Option<String>,
|
|
|
+ unit: Option<CurrencyUnit>,
|
|
|
+ ) -> Result<Self, Error> {
|
|
|
+ if proofs.is_empty() {
|
|
|
+ return Err(Error::ProofsRequired);
|
|
|
}
|
|
|
|
|
|
- pub fn token_info(&self) -> (u64, String) {
|
|
|
- let mut amount = Amount::ZERO;
|
|
|
+ // Check Url is valid
|
|
|
+ let _: Url = (&mint_url).try_into().map_err(|_| Error::InvalidUrl)?;
|
|
|
|
|
|
- for proofs in &self.token {
|
|
|
- for proof in &proofs.proofs {
|
|
|
- amount += proof.amount;
|
|
|
- }
|
|
|
- }
|
|
|
+ Ok(Self {
|
|
|
+ token: vec![MintProofs::new(mint_url, proofs)],
|
|
|
+ memo,
|
|
|
+ unit,
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ pub fn token_info(&self) -> (u64, String) {
|
|
|
+ let mut amount = Amount::ZERO;
|
|
|
|
|
|
- (amount.into(), self.token[0].mint.to_string())
|
|
|
+ for proofs in &self.token {
|
|
|
+ for proof in &proofs.proofs {
|
|
|
+ amount += proof.amount;
|
|
|
+ }
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- impl FromStr for Token {
|
|
|
- type Err = Error;
|
|
|
+ (amount.into(), self.token[0].mint.to_string())
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
- fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
|
- let s = if s.starts_with("cashuA") {
|
|
|
- s.replace("cashuA", "")
|
|
|
- } else {
|
|
|
- return Err(Error::UnsupportedToken);
|
|
|
- };
|
|
|
+impl FromStr for Token {
|
|
|
+ type Err = Error;
|
|
|
|
|
|
- let decode_config = general_purpose::GeneralPurposeConfig::new()
|
|
|
- .with_decode_padding_mode(base64::engine::DecodePaddingMode::Indifferent);
|
|
|
- let decoded = GeneralPurpose::new(&alphabet::STANDARD, decode_config).decode(s)?;
|
|
|
- let decoded_str = String::from_utf8(decoded)?;
|
|
|
- let token: Token = serde_json::from_str(&decoded_str)?;
|
|
|
- Ok(token)
|
|
|
- }
|
|
|
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
|
+ let s = if s.starts_with("cashuA") {
|
|
|
+ s.replace("cashuA", "")
|
|
|
+ } else {
|
|
|
+ return Err(Error::UnsupportedToken);
|
|
|
+ };
|
|
|
+
|
|
|
+ let decode_config = general_purpose::GeneralPurposeConfig::new()
|
|
|
+ .with_decode_padding_mode(base64::engine::DecodePaddingMode::Indifferent);
|
|
|
+ let decoded = GeneralPurpose::new(&alphabet::STANDARD, decode_config).decode(s)?;
|
|
|
+ let decoded_str = String::from_utf8(decoded)?;
|
|
|
+ let token: Token = serde_json::from_str(&decoded_str)?;
|
|
|
+ Ok(token)
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- impl fmt::Display for Token {
|
|
|
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
- let json_string = serde_json::to_string(self).map_err(|_| fmt::Error)?;
|
|
|
- let encoded = general_purpose::STANDARD.encode(json_string);
|
|
|
- write!(f, "cashuA{}", encoded)
|
|
|
- }
|
|
|
+impl fmt::Display for Token {
|
|
|
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
+ let json_string = serde_json::to_string(self).map_err(|_| fmt::Error)?;
|
|
|
+ let encoded = general_purpose::STANDARD.encode(json_string);
|
|
|
+ write!(f, "cashuA{}", encoded)
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -423,9 +548,8 @@ pub struct MintProofs {
|
|
|
pub proofs: Proofs,
|
|
|
}
|
|
|
|
|
|
-#[cfg(feature = "wallet")]
|
|
|
impl MintProofs {
|
|
|
- fn new(mint_url: UncheckedUrl, proofs: Proofs) -> Self {
|
|
|
+ pub fn new(mint_url: UncheckedUrl, proofs: Proofs) -> Self {
|
|
|
Self {
|
|
|
mint: mint_url,
|
|
|
proofs,
|
|
@@ -433,80 +557,10 @@ impl MintProofs {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-/// Promise (BlindSignature) [NUT-00]
|
|
|
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
-pub struct BlindSignature {
|
|
|
- pub amount: Amount,
|
|
|
- /// Keyset Id
|
|
|
- #[serde(rename = "id")]
|
|
|
- pub keyset_id: Id,
|
|
|
- /// blinded signature (C_) on the secret message `B_` of [BlindedMessage]
|
|
|
- #[serde(rename = "C_")]
|
|
|
- pub c: PublicKey,
|
|
|
- /// DLEQ Proof
|
|
|
- pub dleq: Option<BlindSignatureDleq>,
|
|
|
-}
|
|
|
-
|
|
|
-/// Proofs [NUT-00]
|
|
|
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
-pub struct Proof {
|
|
|
- /// Amount in satoshi
|
|
|
- pub amount: Amount,
|
|
|
- /// `Keyset id`
|
|
|
- #[serde(rename = "id")]
|
|
|
- pub keyset_id: Id,
|
|
|
- /// Secret message
|
|
|
- pub secret: Secret,
|
|
|
- /// Unblinded signature
|
|
|
- #[serde(rename = "C")]
|
|
|
- pub c: PublicKey,
|
|
|
- /// Witness
|
|
|
- #[serde(default)]
|
|
|
- #[serde(skip_serializing_if = "Option::is_none")]
|
|
|
- #[serde(serialize_with = "witness_serialize")]
|
|
|
- #[serde(deserialize_with = "witness_deserialize")]
|
|
|
- pub witness: Option<Signatures>,
|
|
|
- /// DLEQ Proof
|
|
|
- pub dleq: Option<ProofDleq>,
|
|
|
-}
|
|
|
-
|
|
|
-impl Proof {
|
|
|
- pub fn new(amount: Amount, keyset_id: Id, secret: Secret, c: PublicKey) -> Self {
|
|
|
- Proof {
|
|
|
- amount,
|
|
|
- keyset_id,
|
|
|
- secret,
|
|
|
- c,
|
|
|
- witness: None,
|
|
|
- dleq: None,
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-impl hash::Hash for Proof {
|
|
|
- fn hash<H: Hasher>(&self, state: &mut H) {
|
|
|
- self.secret.hash(state);
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-impl Ord for Proof {
|
|
|
- fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
|
|
- self.amount.cmp(&other.amount)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-impl PartialOrd for Proof {
|
|
|
- fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
|
|
- Some(self.cmp(other))
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
#[cfg(test)]
|
|
|
mod tests {
|
|
|
use std::str::FromStr;
|
|
|
|
|
|
- #[cfg(feature = "wallet")]
|
|
|
- use super::wallet::*;
|
|
|
use super::*;
|
|
|
|
|
|
#[test]
|
|
@@ -523,7 +577,6 @@ mod tests {
|
|
|
}
|
|
|
|
|
|
#[test]
|
|
|
- #[cfg(feature = "wallet")]
|
|
|
fn test_token_str_round_trip() {
|
|
|
let token_str = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
|
|
|
|
|
@@ -547,7 +600,6 @@ mod tests {
|
|
|
}
|
|
|
|
|
|
#[test]
|
|
|
- #[cfg(feature = "wallet")]
|
|
|
fn test_blank_blinded_messages() {
|
|
|
// TODO: Need to update id to new type in proof
|
|
|
let b = PreMintSecrets::blank(
|
|
@@ -564,7 +616,6 @@ mod tests {
|
|
|
}
|
|
|
|
|
|
#[test]
|
|
|
- #[cfg(feature = "wallet")]
|
|
|
fn incorrect_tokens() {
|
|
|
let incorrect_prefix = "casshuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
|
|
|
|