mirror of
https://github.com/aljazceru/cdk.git
synced 2026-01-08 07:26:02 +01:00
refactor: nuts
This commit is contained in:
@@ -13,7 +13,6 @@ impl Amount {
|
||||
pub fn split(&self) -> Vec<Self> {
|
||||
let sats = self.0.to_sat();
|
||||
(0_u64..64)
|
||||
.into_iter()
|
||||
.rev()
|
||||
.filter_map(|bit| {
|
||||
let part = 1 << bit;
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
//! Cashu Wallet
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::nuts::nut00::{mint, BlindedMessages, Proofs, Token};
|
||||
use crate::nuts::nut01::Keys;
|
||||
use crate::nuts::nut03::RequestMintResponse;
|
||||
use crate::nuts::nut06::{SplitPayload, SplitRequest};
|
||||
use crate::nuts::nut08::MeltResponse;
|
||||
use crate::types::{ProofsStatus, SendProofs};
|
||||
pub use crate::Invoice;
|
||||
use crate::{
|
||||
client::Client,
|
||||
dhke::construct_proofs,
|
||||
error::Error,
|
||||
keyset::Keys,
|
||||
mint,
|
||||
types::{
|
||||
BlindedMessages, Melted, Proofs, ProofsStatus, RequestMintResponse, SendProofs,
|
||||
SplitPayload, SplitRequest, Token,
|
||||
},
|
||||
};
|
||||
use crate::{client::Client, dhke::construct_proofs, error::Error};
|
||||
|
||||
use crate::amount::Amount;
|
||||
|
||||
@@ -226,28 +222,14 @@ impl CashuWallet {
|
||||
invoice: Invoice,
|
||||
proofs: Proofs,
|
||||
fee_reserve: Amount,
|
||||
) -> Result<Melted, Error> {
|
||||
) -> Result<MeltResponse, Error> {
|
||||
let change = BlindedMessages::blank(fee_reserve)?;
|
||||
let melt_response = self
|
||||
.client
|
||||
.melt(proofs, invoice, Some(change.blinded_messages))
|
||||
.await?;
|
||||
|
||||
let change = match melt_response.change {
|
||||
Some(promises) => Some(construct_proofs(
|
||||
promises,
|
||||
change.rs,
|
||||
change.secrets,
|
||||
&self.mint_keys,
|
||||
)?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
Ok(Melted {
|
||||
paid: melt_response.paid,
|
||||
preimage: melt_response.preimage,
|
||||
change,
|
||||
})
|
||||
Ok(melt_response)
|
||||
}
|
||||
|
||||
pub fn proofs_to_token(&self, proofs: Proofs, memo: Option<String>) -> Result<String, Error> {
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
//! Client to connet to mint
|
||||
use std::fmt;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use url::Url;
|
||||
|
||||
use crate::amount::Amount;
|
||||
use crate::nuts::nut00::{BlindedMessage, BlindedMessages, Proof};
|
||||
use crate::nuts::nut01::Keys;
|
||||
use crate::nuts::nut03::RequestMintResponse;
|
||||
use crate::nuts::nut04::{MintRequest, PostMintResponse};
|
||||
use crate::nuts::nut05::{CheckFeesRequest, CheckFeesResponse};
|
||||
use crate::nuts::nut06::{SplitRequest, SplitResponse};
|
||||
use crate::nuts::nut07::{CheckSpendableRequest, CheckSpendableResponse};
|
||||
use crate::nuts::nut08::{MeltRequest, MeltResponse};
|
||||
use crate::nuts::nut09::MintInfo;
|
||||
use crate::nuts::*;
|
||||
use crate::utils;
|
||||
pub use crate::Invoice;
|
||||
use crate::{
|
||||
keyset::{self, Keys},
|
||||
mint,
|
||||
types::{
|
||||
BlindedMessage, BlindedMessages, CheckFeesRequest, CheckFeesResponse,
|
||||
CheckSpendableRequest, CheckSpendableResponse, MeltRequest, MeltResponse, MintInfo,
|
||||
MintRequest, PostMintResponse, Proof, RequestMintResponse, SplitRequest, SplitResponse,
|
||||
},
|
||||
utils,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
@@ -140,11 +141,11 @@ impl Client {
|
||||
}
|
||||
|
||||
/// Get Keysets [NUT-02]
|
||||
pub async fn get_keysets(&self) -> Result<keyset::Response, Error> {
|
||||
pub async fn get_keysets(&self) -> Result<nut02::Response, Error> {
|
||||
let url = self.mint_url.join("keysets")?;
|
||||
let res = minreq::get(url).send()?.json::<Value>()?;
|
||||
|
||||
let response: Result<keyset::Response, serde_json::Error> =
|
||||
let response: Result<nut02::Response, serde_json::Error> =
|
||||
serde_json::from_value(res.clone());
|
||||
|
||||
match response {
|
||||
@@ -268,7 +269,7 @@ impl Client {
|
||||
/// Spendable check [NUT-07]
|
||||
pub async fn check_spendable(
|
||||
&self,
|
||||
proofs: &Vec<mint::Proof>,
|
||||
proofs: &Vec<nut00::mint::Proof>,
|
||||
) -> Result<CheckSpendableResponse, Error> {
|
||||
let url = self.mint_url.join("check")?;
|
||||
let request = CheckSpendableRequest {
|
||||
|
||||
13
src/dhke.rs
13
src/dhke.rs
@@ -7,9 +7,12 @@ use bitcoin_hashes::Hash;
|
||||
use k256::{ProjectivePoint, Scalar, SecretKey};
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::keyset;
|
||||
use crate::keyset::{Keys, PublicKey};
|
||||
use crate::types::{Promise, Proof, Proofs};
|
||||
use crate::nuts::nut00::BlindedSignature;
|
||||
use crate::nuts::nut00::Proof;
|
||||
use crate::nuts::nut00::Proofs;
|
||||
use crate::nuts::nut01::Keys;
|
||||
use crate::nuts::nut01::PublicKey;
|
||||
use crate::nuts::*;
|
||||
|
||||
fn hash_to_curve(message: &[u8]) -> k256::PublicKey {
|
||||
let mut msg_to_hash = message.to_vec();
|
||||
@@ -66,8 +69,8 @@ pub fn unblind_message(
|
||||
|
||||
/// Construct Proof
|
||||
pub fn construct_proofs(
|
||||
promises: Vec<Promise>,
|
||||
rs: Vec<keyset::SecretKey>,
|
||||
promises: Vec<BlindedSignature>,
|
||||
rs: Vec<nut01::SecretKey>,
|
||||
secrets: Vec<String>,
|
||||
keys: &Keys,
|
||||
) -> Result<Proofs, Error> {
|
||||
|
||||
@@ -3,8 +3,8 @@ pub mod cashu_wallet;
|
||||
pub mod client;
|
||||
pub mod dhke;
|
||||
pub mod error;
|
||||
pub mod keyset;
|
||||
pub mod mint;
|
||||
pub mod nuts;
|
||||
pub mod serde_utils;
|
||||
pub mod types;
|
||||
pub mod utils;
|
||||
|
||||
89
src/mint.rs
89
src/mint.rs
@@ -1,27 +1,23 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::dhke::sign_message;
|
||||
use crate::dhke::verify_message;
|
||||
use crate::error::Error;
|
||||
use crate::types::{
|
||||
self, BlindedMessage, CheckSpendableRequest, CheckSpendableResponse, MeltRequest, MeltResponse,
|
||||
PostMintResponse, Promise, SplitRequest, SplitResponse,
|
||||
};
|
||||
use crate::nuts::nut00::BlindedMessage;
|
||||
use crate::nuts::nut00::BlindedSignature;
|
||||
use crate::nuts::nut00::Proof;
|
||||
use crate::nuts::nut06::SplitRequest;
|
||||
use crate::nuts::nut06::SplitResponse;
|
||||
use crate::nuts::nut07::CheckSpendableRequest;
|
||||
use crate::nuts::nut07::CheckSpendableResponse;
|
||||
use crate::nuts::nut08::MeltRequest;
|
||||
use crate::nuts::nut08::MeltResponse;
|
||||
use crate::nuts::*;
|
||||
use crate::Amount;
|
||||
use crate::{
|
||||
dhke::sign_message,
|
||||
keyset::{
|
||||
self,
|
||||
mint::{self, KeySet},
|
||||
PublicKey,
|
||||
},
|
||||
types::MintRequest,
|
||||
};
|
||||
|
||||
pub struct Mint {
|
||||
pub active_keyset: KeySet,
|
||||
pub inactive_keysets: HashMap<String, mint::KeySet>,
|
||||
pub active_keyset: nut02::mint::KeySet,
|
||||
pub inactive_keysets: HashMap<String, nut02::mint::KeySet>,
|
||||
pub spent_secrets: HashSet<String>,
|
||||
}
|
||||
|
||||
@@ -29,12 +25,12 @@ impl Mint {
|
||||
pub fn new(
|
||||
secret: &str,
|
||||
derivation_path: &str,
|
||||
inactive_keysets: HashMap<String, mint::KeySet>,
|
||||
inactive_keysets: HashMap<String, nut02::mint::KeySet>,
|
||||
spent_secrets: HashSet<String>,
|
||||
max_order: u8,
|
||||
) -> Self {
|
||||
Self {
|
||||
active_keyset: keyset::mint::KeySet::generate(secret, derivation_path, max_order),
|
||||
active_keyset: nut02::mint::KeySet::generate(secret, derivation_path, max_order),
|
||||
inactive_keysets,
|
||||
spent_secrets,
|
||||
}
|
||||
@@ -42,23 +38,23 @@ impl Mint {
|
||||
|
||||
/// Retrieve the public keys of the active keyset for distribution to
|
||||
/// wallet clients
|
||||
pub fn active_keyset_pubkeys(&self) -> keyset::KeySet {
|
||||
keyset::KeySet::from(self.active_keyset.clone())
|
||||
pub fn active_keyset_pubkeys(&self) -> nut02::KeySet {
|
||||
nut02::KeySet::from(self.active_keyset.clone())
|
||||
}
|
||||
|
||||
/// Return a list of all supported keysets
|
||||
pub fn keysets(&self) -> keyset::Response {
|
||||
pub fn keysets(&self) -> nut02::Response {
|
||||
let mut keysets: HashSet<_> = self.inactive_keysets.keys().cloned().collect();
|
||||
keysets.insert(self.active_keyset.id.clone());
|
||||
keyset::Response { keysets }
|
||||
nut02::Response { keysets }
|
||||
}
|
||||
|
||||
pub fn active_keyset(&self) -> keyset::mint::KeySet {
|
||||
pub fn active_keyset(&self) -> nut02::mint::KeySet {
|
||||
self.active_keyset.clone()
|
||||
}
|
||||
|
||||
pub fn keyset(&self, id: &str) -> Option<keyset::KeySet> {
|
||||
if &self.active_keyset.id == id {
|
||||
pub fn keyset(&self, id: &str) -> Option<nut02::KeySet> {
|
||||
if self.active_keyset.id == id {
|
||||
return Some(self.active_keyset.clone().into());
|
||||
}
|
||||
|
||||
@@ -67,20 +63,20 @@ impl Mint {
|
||||
|
||||
pub fn process_mint_request(
|
||||
&mut self,
|
||||
mint_request: MintRequest,
|
||||
) -> Result<PostMintResponse, Error> {
|
||||
mint_request: nut04::MintRequest,
|
||||
) -> Result<nut04::PostMintResponse, Error> {
|
||||
let mut blind_signatures = Vec::with_capacity(mint_request.outputs.len());
|
||||
|
||||
for blinded_message in mint_request.outputs {
|
||||
blind_signatures.push(self.blind_sign(&blinded_message)?);
|
||||
}
|
||||
|
||||
Ok(PostMintResponse {
|
||||
Ok(nut04::PostMintResponse {
|
||||
promises: blind_signatures,
|
||||
})
|
||||
}
|
||||
|
||||
fn blind_sign(&self, blinded_message: &BlindedMessage) -> Result<Promise, Error> {
|
||||
fn blind_sign(&self, blinded_message: &BlindedMessage) -> Result<BlindedSignature, Error> {
|
||||
let BlindedMessage { amount, b } = blinded_message;
|
||||
|
||||
let Some(key_pair) = self.active_keyset.keys.0.get(&amount.to_sat()) else {
|
||||
@@ -90,8 +86,8 @@ impl Mint {
|
||||
|
||||
let c = sign_message(key_pair.secret_key.clone(), b.clone().into())?;
|
||||
|
||||
Ok(Promise {
|
||||
amount: amount.clone(),
|
||||
Ok(BlindedSignature {
|
||||
amount: *amount,
|
||||
c: c.into(),
|
||||
id: self.active_keyset.id.clone(),
|
||||
})
|
||||
@@ -111,7 +107,7 @@ impl Mint {
|
||||
// in the outputs (blind messages). As we loop, take from those sets,
|
||||
// target amount first.
|
||||
for output in outputs {
|
||||
let signed = self.blind_sign(&output)?;
|
||||
let signed = self.blind_sign(output)?;
|
||||
|
||||
// Accumulate outputs into the target (send) list
|
||||
if target_total + signed.amount <= amount {
|
||||
@@ -172,7 +168,7 @@ impl Mint {
|
||||
Ok(split_response)
|
||||
}
|
||||
|
||||
fn verify_proof(&self, proof: &types::Proof) -> Result<String, Error> {
|
||||
fn verify_proof(&self, proof: &Proof) -> Result<String, Error> {
|
||||
if self.spent_secrets.contains(&proof.secret) {
|
||||
return Err(Error::TokenSpent);
|
||||
}
|
||||
@@ -223,7 +219,7 @@ impl Mint {
|
||||
|
||||
let mut secrets = Vec::with_capacity(melt_request.proofs.len());
|
||||
for proof in &melt_request.proofs {
|
||||
secrets.push(self.verify_proof(&proof)?);
|
||||
secrets.push(self.verify_proof(proof)?);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -248,7 +244,7 @@ impl Mint {
|
||||
for (i, amount) in amounts.iter().enumerate() {
|
||||
let mut message = outputs[i].clone();
|
||||
|
||||
message.amount = amount.clone();
|
||||
message.amount = *amount;
|
||||
|
||||
let signature = self.blind_sign(&message)?;
|
||||
change.push(signature)
|
||||
@@ -262,24 +258,3 @@ impl Mint {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Proofs [NUT-00]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Proof {
|
||||
/// Amount in satoshi
|
||||
pub amount: Option<Amount>,
|
||||
/// Secret message
|
||||
// #[serde(with = "crate::serde_utils::bytes_base64")]
|
||||
pub secret: String,
|
||||
/// Unblinded signature
|
||||
#[serde(rename = "C")]
|
||||
pub c: Option<PublicKey>,
|
||||
/// `Keyset id`
|
||||
pub id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
/// P2SHScript that specifies the spending condition for this Proof
|
||||
pub script: Option<String>,
|
||||
}
|
||||
|
||||
/// List of proofs
|
||||
pub type Proofs = Vec<Proof>;
|
||||
|
||||
10
src/nuts/mod.rs
Normal file
10
src/nuts/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
pub mod nut00;
|
||||
pub mod nut01;
|
||||
pub mod nut02;
|
||||
pub mod nut03;
|
||||
pub mod nut04;
|
||||
pub mod nut05;
|
||||
pub mod nut06;
|
||||
pub mod nut07;
|
||||
pub mod nut08;
|
||||
pub mod nut09;
|
||||
256
src/nuts/nut00.rs
Normal file
256
src/nuts/nut00.rs
Normal file
@@ -0,0 +1,256 @@
|
||||
//! Notation and Models
|
||||
// https://github.com/cashubtc/nuts/blob/main/00.md
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
use crate::utils::generate_secret;
|
||||
use crate::Amount;
|
||||
use crate::{dhke::blind_message, error::Error, serde_utils::serde_url, utils::split_amount};
|
||||
|
||||
use super::nut01::{self, PublicKey};
|
||||
|
||||
/// Blinded Message [NUT-00]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BlindedMessage {
|
||||
/// Amount in satoshi
|
||||
pub amount: Amount,
|
||||
/// encrypted secret message (B_)
|
||||
#[serde(rename = "B_")]
|
||||
pub b: PublicKey,
|
||||
}
|
||||
|
||||
/// Blinded Messages [NUT-00]
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BlindedMessages {
|
||||
/// Blinded messages
|
||||
pub blinded_messages: Vec<BlindedMessage>,
|
||||
/// Secrets
|
||||
pub secrets: Vec<String>,
|
||||
/// Rs
|
||||
pub rs: Vec<nut01::SecretKey>,
|
||||
/// Amounts
|
||||
pub amounts: Vec<Amount>,
|
||||
}
|
||||
|
||||
impl BlindedMessages {
|
||||
/// Outputs for speceifed amount with random secret
|
||||
pub fn random(amount: Amount) -> Result<Self, Error> {
|
||||
let mut blinded_messages = BlindedMessages::default();
|
||||
|
||||
for amount in split_amount(amount) {
|
||||
let secret = generate_secret();
|
||||
let (blinded, r) = blind_message(secret.as_bytes(), None)?;
|
||||
|
||||
let blinded_message = BlindedMessage { amount, b: blinded };
|
||||
|
||||
blinded_messages.secrets.push(secret);
|
||||
blinded_messages.blinded_messages.push(blinded_message);
|
||||
blinded_messages.rs.push(r.into());
|
||||
blinded_messages.amounts.push(amount);
|
||||
}
|
||||
|
||||
Ok(blinded_messages)
|
||||
}
|
||||
|
||||
/// Blank Outputs used for NUT-08 change
|
||||
pub fn blank(fee_reserve: Amount) -> Result<Self, Error> {
|
||||
let mut blinded_messages = BlindedMessages::default();
|
||||
|
||||
let fee_reserve = bitcoin::Amount::from_sat(fee_reserve.to_sat());
|
||||
|
||||
let count = (fee_reserve
|
||||
.to_float_in(bitcoin::Denomination::Satoshi)
|
||||
.log2()
|
||||
.ceil() as u64)
|
||||
.max(1);
|
||||
|
||||
for _i in 0..count {
|
||||
let secret = generate_secret();
|
||||
let (blinded, r) = blind_message(secret.as_bytes(), None)?;
|
||||
|
||||
let blinded_message = BlindedMessage {
|
||||
amount: Amount::ZERO,
|
||||
b: blinded,
|
||||
};
|
||||
|
||||
blinded_messages.secrets.push(secret);
|
||||
blinded_messages.blinded_messages.push(blinded_message);
|
||||
blinded_messages.rs.push(r.into());
|
||||
blinded_messages.amounts.push(Amount::ZERO);
|
||||
}
|
||||
|
||||
Ok(blinded_messages)
|
||||
}
|
||||
}
|
||||
|
||||
/// Promise (BlindedSignature) [NUT-00]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BlindedSignature {
|
||||
pub id: String,
|
||||
pub amount: Amount,
|
||||
/// blinded signature (C_) on the secret message `B_` of [BlindedMessage]
|
||||
#[serde(rename = "C_")]
|
||||
pub c: PublicKey,
|
||||
}
|
||||
|
||||
/// Proofs [NUT-00]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Proof {
|
||||
/// Amount in satoshi
|
||||
pub amount: Amount,
|
||||
/// Secret message
|
||||
// #[serde(with = "crate::serde_utils::bytes_base64")]
|
||||
pub secret: String,
|
||||
/// Unblinded signature
|
||||
#[serde(rename = "C")]
|
||||
pub c: PublicKey,
|
||||
/// `Keyset id`
|
||||
pub id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
/// P2SHScript that specifies the spending condition for this Proof
|
||||
pub script: Option<String>,
|
||||
}
|
||||
|
||||
/// List of proofs
|
||||
pub type Proofs = Vec<Proof>;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct MintProofs {
|
||||
#[serde(with = "serde_url")]
|
||||
pub mint: Url,
|
||||
pub proofs: Proofs,
|
||||
}
|
||||
|
||||
impl MintProofs {
|
||||
fn new(mint_url: Url, proofs: Proofs) -> Self {
|
||||
Self {
|
||||
mint: mint_url,
|
||||
proofs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Token {
|
||||
pub token: Vec<MintProofs>,
|
||||
pub memo: Option<String>,
|
||||
}
|
||||
|
||||
impl Token {
|
||||
pub fn new(mint_url: Url, proofs: Proofs, memo: Option<String>) -> Self {
|
||||
Self {
|
||||
token: vec![MintProofs::new(mint_url, proofs)],
|
||||
memo,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn token_info(&self) -> (u64, String) {
|
||||
let mut amount = Amount::ZERO;
|
||||
|
||||
for proofs in &self.token {
|
||||
for proof in &proofs.proofs {
|
||||
amount += proof.amount;
|
||||
}
|
||||
}
|
||||
|
||||
(amount.to_sat(), self.token[0].mint.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Token {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if !s.starts_with("cashuA") {
|
||||
return Err(Error::UnsupportedToken);
|
||||
}
|
||||
|
||||
let s = s.replace("cashuA", "");
|
||||
let decoded = general_purpose::STANDARD.decode(s)?;
|
||||
let decoded_str = String::from_utf8(decoded)?;
|
||||
println!("decode: {:?}", decoded_str);
|
||||
let token: Token = serde_json::from_str(&decoded_str)?;
|
||||
Ok(token)
|
||||
}
|
||||
}
|
||||
|
||||
impl Token {
|
||||
pub fn convert_to_string(&self) -> Result<String, Error> {
|
||||
let json_string = serde_json::to_string(self)?;
|
||||
let encoded = general_purpose::STANDARD.encode(json_string);
|
||||
Ok(format!("cashuA{}", encoded))
|
||||
}
|
||||
}
|
||||
|
||||
pub mod mint {
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::amount::Amount;
|
||||
|
||||
use super::PublicKey;
|
||||
|
||||
/// Proofs [NUT-00]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Proof {
|
||||
/// Amount in satoshi
|
||||
pub amount: Option<Amount>,
|
||||
/// Secret message
|
||||
// #[serde(with = "crate::serde_utils::bytes_base64")]
|
||||
pub secret: String,
|
||||
/// Unblinded signature
|
||||
#[serde(rename = "C")]
|
||||
pub c: Option<PublicKey>,
|
||||
/// `Keyset id`
|
||||
pub id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
/// P2SHScript that specifies the spending condition for this Proof
|
||||
pub script: Option<String>,
|
||||
}
|
||||
|
||||
/// List of proofs
|
||||
pub type Proofs = Vec<Proof>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_proof_seralize() {
|
||||
let proof = "[{\"id\":\"DSAl9nvvyfva\",\"amount\":2,\"secret\":\"EhpennC9qB3iFlW8FZ_pZw\",\"C\":\"02c020067db727d586bc3183aecf97fcb800c3f4cc4759f69c626c9db5d8f5b5d4\"},{\"id\":\"DSAl9nvvyfva\",\"amount\":8,\"secret\":\"TmS6Cv0YT5PU_5ATVKnukw\",\"C\":\"02ac910bef28cbe5d7325415d5c263026f15f9b967a079ca9779ab6e5c2db133a7\"}]";
|
||||
let proof: Proofs = serde_json::from_str(proof).unwrap();
|
||||
|
||||
assert_eq!(proof[0].clone().id.unwrap(), "DSAl9nvvyfva");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_token_str_round_trip() {
|
||||
let token_str = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJpZCI6IkRTQWw5bnZ2eWZ2YSIsImFtb3VudCI6Miwic2VjcmV0IjoiRWhwZW5uQzlxQjNpRmxXOEZaX3BadyIsIkMiOiIwMmMwMjAwNjdkYjcyN2Q1ODZiYzMxODNhZWNmOTdmY2I4MDBjM2Y0Y2M0NzU5ZjY5YzYyNmM5ZGI1ZDhmNWI1ZDQifSx7ImlkIjoiRFNBbDludnZ5ZnZhIiwiYW1vdW50Ijo4LCJzZWNyZXQiOiJUbVM2Q3YwWVQ1UFVfNUFUVktudWt3IiwiQyI6IjAyYWM5MTBiZWYyOGNiZTVkNzMyNTQxNWQ1YzI2MzAyNmYxNWY5Yjk2N2EwNzljYTk3NzlhYjZlNWMyZGIxMzNhNyJ9XX1dLCJtZW1vIjoiVGhhbmt5b3UuIn0=";
|
||||
let token = Token::from_str(token_str).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
token.token[0].mint,
|
||||
Url::from_str("https://8333.space:3338").unwrap()
|
||||
);
|
||||
assert_eq!(token.token[0].proofs[0].clone().id.unwrap(), "DSAl9nvvyfva");
|
||||
|
||||
let encoded = &token.convert_to_string().unwrap();
|
||||
|
||||
let token_data = Token::from_str(encoded).unwrap();
|
||||
|
||||
assert_eq!(token_data, token);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blank_blinded_messages() {
|
||||
let b = BlindedMessages::blank(Amount::from_sat(1000)).unwrap();
|
||||
assert_eq!(b.blinded_messages.len(), 10);
|
||||
|
||||
let b = BlindedMessages::blank(Amount::from_sat(1)).unwrap();
|
||||
assert_eq!(b.blinded_messages.len(), 1);
|
||||
}
|
||||
}
|
||||
111
src/nuts/nut01.rs
Normal file
111
src/nuts/nut01.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
//! Mint public key exchange
|
||||
// https://github.com/cashubtc/nuts/blob/main/01.md
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct PublicKey(#[serde(with = "crate::serde_utils::serde_public_key")] k256::PublicKey);
|
||||
|
||||
impl From<PublicKey> for k256::PublicKey {
|
||||
fn from(value: PublicKey) -> k256::PublicKey {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&PublicKey> for k256::PublicKey {
|
||||
fn from(value: &PublicKey) -> k256::PublicKey {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<k256::PublicKey> for PublicKey {
|
||||
fn from(value: k256::PublicKey) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct SecretKey(#[serde(with = "crate::serde_utils::serde_secret_key")] k256::SecretKey);
|
||||
|
||||
impl From<SecretKey> for k256::SecretKey {
|
||||
fn from(value: SecretKey) -> k256::SecretKey {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<k256::SecretKey> for SecretKey {
|
||||
fn from(value: k256::SecretKey) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Mint Keys [NUT-01]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct Keys(BTreeMap<u64, PublicKey>);
|
||||
|
||||
impl Keys {
|
||||
pub fn new(keys: BTreeMap<u64, PublicKey>) -> Self {
|
||||
Self(keys)
|
||||
}
|
||||
|
||||
pub fn keys(&self) -> BTreeMap<u64, PublicKey> {
|
||||
self.0.clone()
|
||||
}
|
||||
|
||||
pub fn amount_key(&self, amount: &u64) -> Option<PublicKey> {
|
||||
self.0.get(amount).cloned()
|
||||
}
|
||||
|
||||
pub fn as_hashmap(&self) -> HashMap<u64, String> {
|
||||
self.0
|
||||
.iter()
|
||||
.map(|(k, v)| (k.to_owned(), hex::encode(v.0.to_sec1_bytes())))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<mint::Keys> for Keys {
|
||||
fn from(keys: mint::Keys) -> Self {
|
||||
Self(
|
||||
keys.0
|
||||
.iter()
|
||||
.map(|(amount, keypair)| (*amount, keypair.public_key.clone()))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub mod mint {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use k256::SecretKey;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use super::PublicKey;
|
||||
use crate::serde_utils;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Keys(pub BTreeMap<u64, KeyPair>);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct KeyPair {
|
||||
pub public_key: PublicKey,
|
||||
#[serde(with = "serde_utils::serde_secret_key")]
|
||||
pub secret_key: SecretKey,
|
||||
}
|
||||
|
||||
impl KeyPair {
|
||||
pub fn from_secret_key(secret_key: SecretKey) -> Self {
|
||||
Self {
|
||||
public_key: secret_key.public_key().into(),
|
||||
secret_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
//! Keysets
|
||||
//! Keysets and keyset ID
|
||||
// https://github.com/cashubtc/nuts/blob/main/02.md
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
@@ -9,77 +8,7 @@ use bitcoin::hashes::sha256::Hash as Sha256;
|
||||
use bitcoin::hashes::Hash;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct PublicKey(#[serde(with = "crate::serde_utils::serde_public_key")] k256::PublicKey);
|
||||
|
||||
impl From<PublicKey> for k256::PublicKey {
|
||||
fn from(value: PublicKey) -> k256::PublicKey {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<k256::PublicKey> for PublicKey {
|
||||
fn from(value: k256::PublicKey) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct SecretKey(#[serde(with = "crate::serde_utils::serde_secret_key")] k256::SecretKey);
|
||||
|
||||
impl From<SecretKey> for k256::SecretKey {
|
||||
fn from(value: SecretKey) -> k256::SecretKey {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<k256::SecretKey> for SecretKey {
|
||||
fn from(value: k256::SecretKey) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Mint Keys [NUT-01]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct Keys(BTreeMap<u64, PublicKey>);
|
||||
|
||||
impl Keys {
|
||||
pub fn new(keys: BTreeMap<u64, PublicKey>) -> Self {
|
||||
Self(keys)
|
||||
}
|
||||
|
||||
pub fn amount_key(&self, amount: &u64) -> Option<PublicKey> {
|
||||
self.0.get(amount).cloned()
|
||||
}
|
||||
|
||||
pub fn as_hashmap(&self) -> HashMap<u64, String> {
|
||||
self.0
|
||||
.iter()
|
||||
.map(|(k, v)| (k.to_owned(), hex::encode(v.0.to_sec1_bytes())))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn id(&self) -> String {
|
||||
/* 1 - sort keyset by amount
|
||||
* 2 - concatenate all (sorted) public keys to one string
|
||||
* 3 - HASH_SHA256 the concatenated public keys
|
||||
* 4 - take the first 12 characters of the hash
|
||||
*/
|
||||
|
||||
let pubkeys_concat = self
|
||||
.0
|
||||
.values()
|
||||
.map(|pubkey| hex::encode(&pubkey.0.to_sec1_bytes()))
|
||||
.collect::<Vec<String>>()
|
||||
.join("");
|
||||
|
||||
let hash = general_purpose::STANDARD.encode(Sha256::hash(pubkeys_concat.as_bytes()));
|
||||
|
||||
hash[0..12].to_string()
|
||||
}
|
||||
}
|
||||
use super::nut01::Keys;
|
||||
|
||||
/// Mint Keysets [NUT-02]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -94,17 +23,6 @@ pub struct KeySet {
|
||||
pub keys: Keys,
|
||||
}
|
||||
|
||||
impl From<mint::Keys> for Keys {
|
||||
fn from(keys: mint::Keys) -> Self {
|
||||
Self(
|
||||
keys.0
|
||||
.iter()
|
||||
.map(|(amount, keypair)| (*amount, keypair.public_key.clone()))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<mint::KeySet> for KeySet {
|
||||
fn from(keyset: mint::KeySet) -> Self {
|
||||
Self {
|
||||
@@ -114,6 +32,27 @@ impl From<mint::KeySet> for KeySet {
|
||||
}
|
||||
}
|
||||
|
||||
impl Keys {
|
||||
pub fn id(&self) -> String {
|
||||
/* 1 - sort keyset by amount
|
||||
* 2 - concatenate all (sorted) public keys to one string
|
||||
* 3 - HASH_SHA256 the concatenated public keys
|
||||
* 4 - take the first 12 characters of the hash
|
||||
*/
|
||||
|
||||
let pubkeys_concat = self
|
||||
.keys()
|
||||
.values()
|
||||
.map(|pubkey| hex::encode(k256::PublicKey::from(pubkey).to_sec1_bytes()))
|
||||
.collect::<Vec<String>>()
|
||||
.join("");
|
||||
|
||||
let hash = general_purpose::STANDARD.encode(Sha256::hash(pubkeys_concat.as_bytes()));
|
||||
|
||||
hash[0..12].to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub mod mint {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@@ -125,11 +64,7 @@ pub mod mint {
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use super::PublicKey;
|
||||
use crate::serde_utils;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Keys(pub BTreeMap<u64, KeyPair>);
|
||||
use crate::nuts::nut01::mint::{KeyPair, Keys};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct KeySet {
|
||||
@@ -184,7 +119,9 @@ pub mod mint {
|
||||
|
||||
let pubkeys_concat = map
|
||||
.values()
|
||||
.map(|keypair| hex::encode(&keypair.public_key.0.to_sec1_bytes()))
|
||||
.map(|keypair| {
|
||||
hex::encode(k256::PublicKey::from(&keypair.public_key).to_sec1_bytes())
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("");
|
||||
|
||||
@@ -193,22 +130,6 @@ pub mod mint {
|
||||
hash[0..12].to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct KeyPair {
|
||||
pub public_key: PublicKey,
|
||||
#[serde(with = "serde_utils::serde_secret_key")]
|
||||
pub secret_key: SecretKey,
|
||||
}
|
||||
|
||||
impl KeyPair {
|
||||
fn from_secret_key(secret_key: SecretKey) -> Self {
|
||||
Self {
|
||||
public_key: secret_key.public_key().into(),
|
||||
secret_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
15
src/nuts/nut03.rs
Normal file
15
src/nuts/nut03.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
//! Request mint
|
||||
// https://github.com/cashubtc/nuts/blob/main/03.md
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub use crate::Invoice;
|
||||
|
||||
/// Mint request response [NUT-03]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RequestMintResponse {
|
||||
/// Bolt11 payment request
|
||||
pub pr: Invoice,
|
||||
/// Random hash MUST not be the hash of invoice
|
||||
pub hash: String,
|
||||
}
|
||||
27
src/nuts/nut04.rs
Normal file
27
src/nuts/nut04.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
//! Mint Tokens
|
||||
// https://github.com/cashubtc/nuts/blob/main/04.md
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::nut00::{BlindedMessage, BlindedSignature};
|
||||
use crate::Amount;
|
||||
|
||||
/// Post Mint Request [NUT-04]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct MintRequest {
|
||||
pub outputs: Vec<BlindedMessage>,
|
||||
}
|
||||
|
||||
impl MintRequest {
|
||||
pub fn total_amount(&self) -> Amount {
|
||||
self.outputs
|
||||
.iter()
|
||||
.map(|BlindedMessage { amount, .. }| *amount)
|
||||
.sum()
|
||||
}
|
||||
}
|
||||
|
||||
/// Post Mint Response [NUT-04]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PostMintResponse {
|
||||
pub promises: Vec<BlindedSignature>,
|
||||
}
|
||||
51
src/nuts/nut05.rs
Normal file
51
src/nuts/nut05.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
//! Melting Tokens
|
||||
// https://github.com/cashubtc/nuts/blob/main/05.md
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::nut00::Proofs;
|
||||
use crate::amount::Amount;
|
||||
use crate::error::Error;
|
||||
use crate::Invoice;
|
||||
|
||||
/// Check Fees Response [NUT-05]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct CheckFeesResponse {
|
||||
/// Expected Mac Fee in satoshis
|
||||
pub fee: Amount,
|
||||
}
|
||||
|
||||
/// Check Fees request [NUT-05]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct CheckFeesRequest {
|
||||
/// Lighting Invoice
|
||||
pub pr: Invoice,
|
||||
}
|
||||
|
||||
/// Melt Request [NUT-05]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct MeltRequest {
|
||||
pub proofs: Proofs,
|
||||
/// bollt11
|
||||
pub pr: Invoice,
|
||||
}
|
||||
|
||||
impl MeltRequest {
|
||||
pub fn proofs_amount(&self) -> Amount {
|
||||
self.proofs.iter().map(|proof| proof.amount).sum()
|
||||
}
|
||||
|
||||
pub fn invoice_amount(&self) -> Result<Amount, Error> {
|
||||
match self.pr.amount_milli_satoshis() {
|
||||
Some(value) => Ok(Amount::from_sat(value)),
|
||||
None => Err(Error::InvoiceAmountUndefined),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Melt Response [NUT-05]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct MeltResponse {
|
||||
pub paid: bool,
|
||||
pub preimage: Option<String>,
|
||||
}
|
||||
57
src/nuts/nut06.rs
Normal file
57
src/nuts/nut06.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
//! Split
|
||||
// https://github.com/cashubtc/nuts/blob/main/06.md
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::amount::Amount;
|
||||
use crate::nuts::nut00::{BlindedMessage, BlindedMessages, Proofs};
|
||||
|
||||
use super::nut00::BlindedSignature;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SplitPayload {
|
||||
pub keep_blinded_messages: BlindedMessages,
|
||||
pub send_blinded_messages: BlindedMessages,
|
||||
pub split_payload: SplitRequest,
|
||||
}
|
||||
|
||||
/// Split Request [NUT-06]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SplitRequest {
|
||||
pub amount: Amount,
|
||||
pub proofs: Proofs,
|
||||
pub outputs: Vec<BlindedMessage>,
|
||||
}
|
||||
|
||||
impl SplitRequest {
|
||||
pub fn proofs_amount(&self) -> Amount {
|
||||
self.proofs.iter().map(|proof| proof.amount).sum()
|
||||
}
|
||||
pub fn output_amount(&self) -> Amount {
|
||||
self.outputs.iter().map(|proof| proof.amount).sum()
|
||||
}
|
||||
}
|
||||
|
||||
/// Split Response [NUT-06]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SplitResponse {
|
||||
/// Promises to keep
|
||||
pub fst: Vec<BlindedSignature>,
|
||||
/// Promises to send
|
||||
pub snd: Vec<BlindedSignature>,
|
||||
}
|
||||
|
||||
impl SplitResponse {
|
||||
pub fn change_amount(&self) -> Amount {
|
||||
self.fst
|
||||
.iter()
|
||||
.map(|BlindedSignature { amount, .. }| *amount)
|
||||
.sum()
|
||||
}
|
||||
|
||||
pub fn target_amount(&self) -> Amount {
|
||||
self.snd
|
||||
.iter()
|
||||
.map(|BlindedSignature { amount, .. }| *amount)
|
||||
.sum()
|
||||
}
|
||||
}
|
||||
20
src/nuts/nut07.rs
Normal file
20
src/nuts/nut07.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
//! Spendable Check
|
||||
// https://github.com/cashubtc/nuts/blob/main/07.md
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::nut00::mint;
|
||||
|
||||
/// Check spendabale request [NUT-07]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct CheckSpendableRequest {
|
||||
pub proofs: mint::Proofs,
|
||||
}
|
||||
|
||||
/// Check Spendable Response [NUT-07]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct CheckSpendableResponse {
|
||||
/// booleans indicating whether the provided Proof is still spendable.
|
||||
/// In same order as provided proofs
|
||||
pub spendable: Vec<bool>,
|
||||
}
|
||||
42
src/nuts/nut08.rs
Normal file
42
src/nuts/nut08.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
//! Lightning fee return
|
||||
// https://github.com/cashubtc/nuts/blob/main/08.md
|
||||
|
||||
use lightning_invoice::Invoice;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{error::Error, Amount};
|
||||
|
||||
use super::nut00::{BlindedMessage, BlindedSignature, Proofs};
|
||||
|
||||
/// Melt Request [NUT-08]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct MeltRequest {
|
||||
pub proofs: Proofs,
|
||||
/// bollt11
|
||||
pub pr: Invoice,
|
||||
/// Blinded Message that can be used to return change [NUT-08]
|
||||
/// Amount field of blindedMessages `SHOULD` be set to zero
|
||||
pub outputs: Option<Vec<BlindedMessage>>,
|
||||
}
|
||||
|
||||
impl MeltRequest {
|
||||
pub fn proofs_amount(&self) -> Amount {
|
||||
self.proofs.iter().map(|proof| proof.amount).sum()
|
||||
}
|
||||
|
||||
pub fn invoice_amount(&self) -> Result<Amount, Error> {
|
||||
match self.pr.amount_milli_satoshis() {
|
||||
Some(value) => Ok(Amount::from_sat(value)),
|
||||
None => Err(Error::InvoiceAmountUndefined),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Melt Response [NUT-08]
|
||||
/// Lightning fee return [NUT-08] if change is defined
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct MeltResponse {
|
||||
pub paid: bool,
|
||||
pub preimage: Option<String>,
|
||||
pub change: Option<Vec<BlindedSignature>>,
|
||||
}
|
||||
61
src/nuts/nut09.rs
Normal file
61
src/nuts/nut09.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
//! Mint Information
|
||||
// https://github.com/cashubtc/nuts/blob/main/09.md
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
use super::nut01::PublicKey;
|
||||
|
||||
/// Mint Version
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct MintVersion {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
impl Serialize for MintVersion {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let combined = format!("{}/{}", self.name, self.version);
|
||||
serializer.serialize_str(&combined)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for MintVersion {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let combined = String::deserialize(deserializer)?;
|
||||
let parts: Vec<&str> = combined.split('/').collect();
|
||||
if parts.len() != 2 {
|
||||
return Err(serde::de::Error::custom("Invalid input string"));
|
||||
}
|
||||
Ok(MintVersion {
|
||||
name: parts[0].to_string(),
|
||||
version: parts[1].to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Mint Info [NIP-09]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct MintInfo {
|
||||
/// name of the mint and should be recognizable
|
||||
pub name: Option<String>,
|
||||
/// hex pubkey of the mint
|
||||
pub pubkey: Option<PublicKey>,
|
||||
/// implementation name and the version running
|
||||
pub version: Option<MintVersion>,
|
||||
/// short description of the mint
|
||||
pub description: Option<String>,
|
||||
/// long description
|
||||
pub description_long: Option<String>,
|
||||
/// contact methods to reach the mint operator
|
||||
pub contact: Vec<Vec<String>>,
|
||||
/// shows which NUTs the mint supports
|
||||
pub nuts: Vec<String>,
|
||||
/// message of the day that the wallet must display to the user
|
||||
pub motd: Option<String>,
|
||||
}
|
||||
421
src/types.rs
421
src/types.rs
@@ -1,262 +1,8 @@
|
||||
//! Types for `cashu-crab`
|
||||
|
||||
use std::str::FromStr;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use url::Url;
|
||||
|
||||
use crate::keyset::{self, PublicKey};
|
||||
use crate::utils::generate_secret;
|
||||
use crate::Amount;
|
||||
pub use crate::Invoice;
|
||||
use crate::{dhke::blind_message, error::Error, mint, serde_utils::serde_url, utils::split_amount};
|
||||
|
||||
/// Blinded Message [NUT-00]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BlindedMessage {
|
||||
/// Amount in satoshi
|
||||
pub amount: Amount,
|
||||
/// encrypted secret message (B_)
|
||||
#[serde(rename = "B_")]
|
||||
pub b: PublicKey,
|
||||
}
|
||||
|
||||
/// Blinded Messages [NUT-00]
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BlindedMessages {
|
||||
/// Blinded messages
|
||||
pub blinded_messages: Vec<BlindedMessage>,
|
||||
/// Secrets
|
||||
pub secrets: Vec<String>,
|
||||
/// Rs
|
||||
pub rs: Vec<keyset::SecretKey>,
|
||||
/// Amounts
|
||||
pub amounts: Vec<Amount>,
|
||||
}
|
||||
|
||||
impl BlindedMessages {
|
||||
/// Outputs for speceifed amount with random secret
|
||||
pub fn random(amount: Amount) -> Result<Self, Error> {
|
||||
let mut blinded_messages = BlindedMessages::default();
|
||||
|
||||
for amount in split_amount(amount) {
|
||||
let secret = generate_secret();
|
||||
let (blinded, r) = blind_message(secret.as_bytes(), None)?;
|
||||
|
||||
let blinded_message = BlindedMessage { amount, b: blinded };
|
||||
|
||||
blinded_messages.secrets.push(secret);
|
||||
blinded_messages.blinded_messages.push(blinded_message);
|
||||
blinded_messages.rs.push(r.into());
|
||||
blinded_messages.amounts.push(amount);
|
||||
}
|
||||
|
||||
Ok(blinded_messages)
|
||||
}
|
||||
|
||||
/// Blank Outputs used for NUT-08 change
|
||||
pub fn blank(fee_reserve: Amount) -> Result<Self, Error> {
|
||||
let mut blinded_messages = BlindedMessages::default();
|
||||
|
||||
let fee_reserve = bitcoin::Amount::from_sat(fee_reserve.to_sat());
|
||||
|
||||
let count = (fee_reserve
|
||||
.to_float_in(bitcoin::Denomination::Satoshi)
|
||||
.log2()
|
||||
.ceil() as u64)
|
||||
.max(1);
|
||||
|
||||
for _i in 0..count {
|
||||
let secret = generate_secret();
|
||||
let (blinded, r) = blind_message(secret.as_bytes(), None)?;
|
||||
|
||||
let blinded_message = BlindedMessage {
|
||||
amount: Amount::ZERO,
|
||||
b: blinded,
|
||||
};
|
||||
|
||||
blinded_messages.secrets.push(secret);
|
||||
blinded_messages.blinded_messages.push(blinded_message);
|
||||
blinded_messages.rs.push(r.into());
|
||||
blinded_messages.amounts.push(Amount::ZERO);
|
||||
}
|
||||
|
||||
Ok(blinded_messages)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SplitPayload {
|
||||
pub keep_blinded_messages: BlindedMessages,
|
||||
pub send_blinded_messages: BlindedMessages,
|
||||
pub split_payload: SplitRequest,
|
||||
}
|
||||
|
||||
/// Promise (BlindedSignature) [NUT-00]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Promise {
|
||||
pub id: String,
|
||||
pub amount: Amount,
|
||||
/// blinded signature (C_) on the secret message `B_` of [BlindedMessage]
|
||||
#[serde(rename = "C_")]
|
||||
pub c: PublicKey,
|
||||
}
|
||||
|
||||
/// Proofs [NUT-00]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Proof {
|
||||
/// Amount in satoshi
|
||||
pub amount: Amount,
|
||||
/// Secret message
|
||||
// #[serde(with = "crate::serde_utils::bytes_base64")]
|
||||
pub secret: String,
|
||||
/// Unblinded signature
|
||||
#[serde(rename = "C")]
|
||||
pub c: PublicKey,
|
||||
/// `Keyset id`
|
||||
pub id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
/// P2SHScript that specifies the spending condition for this Proof
|
||||
pub script: Option<String>,
|
||||
}
|
||||
|
||||
/// List of proofs
|
||||
pub type Proofs = Vec<Proof>;
|
||||
|
||||
/// Mint request response [NUT-03]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RequestMintResponse {
|
||||
/// Bolt11 payment request
|
||||
pub pr: Invoice,
|
||||
/// Random hash MUST not be the hash of invoice
|
||||
pub hash: String,
|
||||
}
|
||||
|
||||
/// Post Mint Request [NUT-04]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct MintRequest {
|
||||
pub outputs: Vec<BlindedMessage>,
|
||||
}
|
||||
|
||||
impl MintRequest {
|
||||
pub fn total_amount(&self) -> Amount {
|
||||
self.outputs
|
||||
.iter()
|
||||
.map(|BlindedMessage { amount, .. }| *amount)
|
||||
.sum()
|
||||
}
|
||||
}
|
||||
|
||||
/// Post Mint Response [NUT-05]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PostMintResponse {
|
||||
pub promises: Vec<Promise>,
|
||||
}
|
||||
|
||||
/// Check Fees Response [NUT-05]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct CheckFeesResponse {
|
||||
/// Expected Mac Fee in satoshis
|
||||
pub fee: Amount,
|
||||
}
|
||||
|
||||
/// Check Fees request [NUT-05]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct CheckFeesRequest {
|
||||
/// Lighting Invoice
|
||||
pub pr: Invoice,
|
||||
}
|
||||
|
||||
/// Melt Request [NUT-05]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct MeltRequest {
|
||||
pub proofs: Proofs,
|
||||
/// bollt11
|
||||
pub pr: Invoice,
|
||||
/// Blinded Message that can be used to return change [NUT-08]
|
||||
/// Amount field of blindedMessages `SHOULD` be set to zero
|
||||
pub outputs: Option<Vec<BlindedMessage>>,
|
||||
}
|
||||
|
||||
impl MeltRequest {
|
||||
pub fn proofs_amount(&self) -> Amount {
|
||||
self.proofs.iter().map(|proof| proof.amount).sum()
|
||||
}
|
||||
|
||||
pub fn invoice_amount(&self) -> Result<Amount, Error> {
|
||||
match self.pr.amount_milli_satoshis() {
|
||||
Some(value) => Ok(Amount::from_sat(value)),
|
||||
None => Err(Error::InvoiceAmountUndefined),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Melt Response [NUT-05]
|
||||
/// Lightning fee return [NUT-08] if change is defined
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct MeltResponse {
|
||||
pub paid: bool,
|
||||
pub preimage: Option<String>,
|
||||
pub change: Option<Vec<Promise>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Melted {
|
||||
pub paid: bool,
|
||||
pub preimage: Option<String>,
|
||||
pub change: Option<Proofs>,
|
||||
}
|
||||
|
||||
/// Split Request [NUT-06]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SplitRequest {
|
||||
pub amount: Amount,
|
||||
pub proofs: Proofs,
|
||||
pub outputs: Vec<BlindedMessage>,
|
||||
}
|
||||
|
||||
impl SplitRequest {
|
||||
pub fn proofs_amount(&self) -> Amount {
|
||||
self.proofs.iter().map(|proof| proof.amount).sum()
|
||||
}
|
||||
pub fn output_amount(&self) -> Amount {
|
||||
self.outputs.iter().map(|proof| proof.amount).sum()
|
||||
}
|
||||
}
|
||||
|
||||
/// Split Response [NUT-06]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SplitResponse {
|
||||
/// Promises to keep
|
||||
pub fst: Vec<Promise>,
|
||||
/// Promises to send
|
||||
pub snd: Vec<Promise>,
|
||||
}
|
||||
|
||||
impl SplitResponse {
|
||||
pub fn change_amount(&self) -> Amount {
|
||||
self.fst.iter().map(|Promise { amount, .. }| *amount).sum()
|
||||
}
|
||||
|
||||
pub fn target_amount(&self) -> Amount {
|
||||
self.snd.iter().map(|Promise { amount, .. }| *amount).sum()
|
||||
}
|
||||
}
|
||||
|
||||
/// Check spendabale request [NUT-07]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct CheckSpendableRequest {
|
||||
pub proofs: mint::Proofs,
|
||||
}
|
||||
|
||||
/// Check Spendable Response [NUT-07]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct CheckSpendableResponse {
|
||||
/// booleans indicating whether the provided Proof is still spendable.
|
||||
/// In same order as provided proofs
|
||||
pub spendable: Vec<bool>,
|
||||
}
|
||||
use crate::nuts::nut00::{mint, Proofs};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ProofsStatus {
|
||||
@@ -269,166 +15,3 @@ pub struct SendProofs {
|
||||
pub change_proofs: Proofs,
|
||||
pub send_proofs: Proofs,
|
||||
}
|
||||
|
||||
/// Mint Version
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct MintVersion {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
impl Serialize for MintVersion {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let combined = format!("{}/{}", self.name, self.version);
|
||||
serializer.serialize_str(&combined)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for MintVersion {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let combined = String::deserialize(deserializer)?;
|
||||
let parts: Vec<&str> = combined.split('/').collect();
|
||||
if parts.len() != 2 {
|
||||
return Err(serde::de::Error::custom("Invalid input string"));
|
||||
}
|
||||
Ok(MintVersion {
|
||||
name: parts[0].to_string(),
|
||||
version: parts[1].to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Mint Info [NIP-09]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct MintInfo {
|
||||
/// name of the mint and should be recognizable
|
||||
pub name: Option<String>,
|
||||
/// hex pubkey of the mint
|
||||
pub pubkey: Option<PublicKey>,
|
||||
/// implementation name and the version running
|
||||
pub version: Option<MintVersion>,
|
||||
/// short description of the mint
|
||||
pub description: Option<String>,
|
||||
/// long description
|
||||
pub description_long: Option<String>,
|
||||
/// contact methods to reach the mint operator
|
||||
pub contact: Vec<Vec<String>>,
|
||||
/// shows which NUTs the mint supports
|
||||
pub nuts: Vec<String>,
|
||||
/// message of the day that the wallet must display to the user
|
||||
pub motd: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct MintProofs {
|
||||
#[serde(with = "serde_url")]
|
||||
pub mint: Url,
|
||||
pub proofs: Proofs,
|
||||
}
|
||||
|
||||
impl MintProofs {
|
||||
fn new(mint_url: Url, proofs: Proofs) -> Self {
|
||||
Self {
|
||||
mint: mint_url,
|
||||
proofs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Token {
|
||||
pub token: Vec<MintProofs>,
|
||||
pub memo: Option<String>,
|
||||
}
|
||||
|
||||
impl Token {
|
||||
pub fn new(mint_url: Url, proofs: Proofs, memo: Option<String>) -> Self {
|
||||
Self {
|
||||
token: vec![MintProofs::new(mint_url, proofs)],
|
||||
memo,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn token_info(&self) -> (u64, String) {
|
||||
let mut amount = Amount::ZERO;
|
||||
|
||||
for proofs in &self.token {
|
||||
for proof in &proofs.proofs {
|
||||
amount += proof.amount;
|
||||
}
|
||||
}
|
||||
|
||||
(amount.to_sat(), self.token[0].mint.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Token {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if !s.starts_with("cashuA") {
|
||||
return Err(Error::UnsupportedToken);
|
||||
}
|
||||
|
||||
let s = s.replace("cashuA", "");
|
||||
let decoded = general_purpose::STANDARD.decode(s)?;
|
||||
let decoded_str = String::from_utf8(decoded)?;
|
||||
println!("decode: {:?}", decoded_str);
|
||||
let token: Token = serde_json::from_str(&decoded_str)?;
|
||||
Ok(token)
|
||||
}
|
||||
}
|
||||
|
||||
impl Token {
|
||||
pub fn convert_to_string(&self) -> Result<String, Error> {
|
||||
let json_string = serde_json::to_string(self)?;
|
||||
let encoded = general_purpose::STANDARD.encode(json_string);
|
||||
Ok(format!("cashuA{}", encoded))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_proof_seralize() {
|
||||
let proof = "[{\"id\":\"DSAl9nvvyfva\",\"amount\":2,\"secret\":\"EhpennC9qB3iFlW8FZ_pZw\",\"C\":\"02c020067db727d586bc3183aecf97fcb800c3f4cc4759f69c626c9db5d8f5b5d4\"},{\"id\":\"DSAl9nvvyfva\",\"amount\":8,\"secret\":\"TmS6Cv0YT5PU_5ATVKnukw\",\"C\":\"02ac910bef28cbe5d7325415d5c263026f15f9b967a079ca9779ab6e5c2db133a7\"}]";
|
||||
let proof: Proofs = serde_json::from_str(proof).unwrap();
|
||||
|
||||
assert_eq!(proof[0].clone().id.unwrap(), "DSAl9nvvyfva");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_token_str_round_trip() {
|
||||
let token_str = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJpZCI6IkRTQWw5bnZ2eWZ2YSIsImFtb3VudCI6Miwic2VjcmV0IjoiRWhwZW5uQzlxQjNpRmxXOEZaX3BadyIsIkMiOiIwMmMwMjAwNjdkYjcyN2Q1ODZiYzMxODNhZWNmOTdmY2I4MDBjM2Y0Y2M0NzU5ZjY5YzYyNmM5ZGI1ZDhmNWI1ZDQifSx7ImlkIjoiRFNBbDludnZ5ZnZhIiwiYW1vdW50Ijo4LCJzZWNyZXQiOiJUbVM2Q3YwWVQ1UFVfNUFUVktudWt3IiwiQyI6IjAyYWM5MTBiZWYyOGNiZTVkNzMyNTQxNWQ1YzI2MzAyNmYxNWY5Yjk2N2EwNzljYTk3NzlhYjZlNWMyZGIxMzNhNyJ9XX1dLCJtZW1vIjoiVGhhbmt5b3UuIn0=";
|
||||
let token = Token::from_str(token_str).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
token.token[0].mint,
|
||||
Url::from_str("https://8333.space:3338").unwrap()
|
||||
);
|
||||
assert_eq!(token.token[0].proofs[0].clone().id.unwrap(), "DSAl9nvvyfva");
|
||||
|
||||
let encoded = &token.convert_to_string().unwrap();
|
||||
|
||||
let token_data = Token::from_str(encoded).unwrap();
|
||||
|
||||
assert_eq!(token_data, token);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blank_blinded_messages() {
|
||||
let b = BlindedMessages::blank(Amount::from_sat(1000)).unwrap();
|
||||
assert_eq!(b.blinded_messages.len(), 10);
|
||||
|
||||
let b = BlindedMessages::blank(Amount::from_sat(1)).unwrap();
|
||||
assert_eq!(b.blinded_messages.len(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user