mirror of
https://github.com/aljazceru/cdk.git
synced 2026-02-23 14:06:56 +01:00
refactor: nuts
This commit is contained in:
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
218
src/nuts/nut02.rs
Normal file
218
src/nuts/nut02.rs
Normal file
@@ -0,0 +1,218 @@
|
||||
//! Keysets and keyset ID
|
||||
// https://github.com/cashubtc/nuts/blob/main/02.md
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use bitcoin::hashes::sha256::Hash as Sha256;
|
||||
use bitcoin::hashes::Hash;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::nut01::Keys;
|
||||
|
||||
/// Mint Keysets [NUT-02]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Response {
|
||||
/// set of public keys that the mint generates
|
||||
pub keysets: HashSet<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct KeySet {
|
||||
pub id: String,
|
||||
pub keys: Keys,
|
||||
}
|
||||
|
||||
impl From<mint::KeySet> for KeySet {
|
||||
fn from(keyset: mint::KeySet) -> Self {
|
||||
Self {
|
||||
id: keyset.id,
|
||||
keys: Keys::from(keyset.keys),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use bitcoin::hashes::sha256::Hash as Sha256;
|
||||
use bitcoin_hashes::Hash;
|
||||
use bitcoin_hashes::HashEngine;
|
||||
use k256::SecretKey;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::nuts::nut01::mint::{KeyPair, Keys};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct KeySet {
|
||||
pub id: String,
|
||||
pub keys: Keys,
|
||||
}
|
||||
|
||||
impl KeySet {
|
||||
pub fn generate(
|
||||
secret: impl Into<String>,
|
||||
derivation_path: impl Into<String>,
|
||||
max_order: u8,
|
||||
) -> Self {
|
||||
// Elliptic curve math context
|
||||
|
||||
/* NUT-02 § 2.1
|
||||
for i in range(MAX_ORDER):
|
||||
k_i = HASH_SHA256(s + D + i)[:32]
|
||||
*/
|
||||
|
||||
let mut map = BTreeMap::new();
|
||||
|
||||
// SHA-256 midstate, for quicker hashing
|
||||
let mut engine = Sha256::engine();
|
||||
engine.input(secret.into().as_bytes());
|
||||
engine.input(derivation_path.into().as_bytes());
|
||||
|
||||
for i in 0..max_order {
|
||||
let amount = 2_u64.pow(i as u32);
|
||||
|
||||
// Reuse midstate
|
||||
let mut e = engine.clone();
|
||||
e.input(i.to_string().as_bytes());
|
||||
let hash = Sha256::from_engine(e);
|
||||
let secret_key = SecretKey::from_slice(&hash.to_byte_array()).unwrap();
|
||||
let keypair = KeyPair::from_secret_key(secret_key);
|
||||
map.insert(amount, keypair);
|
||||
}
|
||||
|
||||
Self {
|
||||
id: Self::id(&map),
|
||||
keys: Keys(map),
|
||||
}
|
||||
}
|
||||
|
||||
fn id(map: &BTreeMap<u64, KeyPair>) -> 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 = map
|
||||
.values()
|
||||
.map(|keypair| {
|
||||
hex::encode(k256::PublicKey::from(&keypair.public_key).to_sec1_bytes())
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("");
|
||||
|
||||
let hash = general_purpose::STANDARD.encode(Sha256::hash(pubkeys_concat.as_bytes()));
|
||||
|
||||
hash[0..12].to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use super::Keys;
|
||||
|
||||
const KEYSET_ID: &str = "I2yN+iRYfkzT";
|
||||
const KEYSET: &str = r#"
|
||||
{
|
||||
"1":"03ba786a2c0745f8c30e490288acd7a72dd53d65afd292ddefa326a4a3fa14c566",
|
||||
"2":"03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5",
|
||||
"4":"036e378bcf78738ddf68859293c69778035740e41138ab183c94f8fee7572214c7",
|
||||
"8":"03909d73beaf28edfb283dbeb8da321afd40651e8902fcf5454ecc7d69788626c0",
|
||||
"16":"028a36f0e6638ea7466665fe174d958212723019ec08f9ce6898d897f88e68aa5d",
|
||||
"32":"03a97a40e146adee2687ac60c2ba2586a90f970de92a9d0e6cae5a4b9965f54612",
|
||||
"64":"03ce86f0c197aab181ddba0cfc5c5576e11dfd5164d9f3d4a3fc3ffbbf2e069664",
|
||||
"128":"0284f2c06d938a6f78794814c687560a0aabab19fe5e6f30ede38e113b132a3cb9",
|
||||
"256":"03b99f475b68e5b4c0ba809cdecaae64eade2d9787aa123206f91cd61f76c01459",
|
||||
"512":"03d4db82ea19a44d35274de51f78af0a710925fe7d9e03620b84e3e9976e3ac2eb",
|
||||
"1024":"031fbd4ba801870871d46cf62228a1b748905ebc07d3b210daf48de229e683f2dc",
|
||||
"2048":"0276cedb9a3b160db6a158ad4e468d2437f021293204b3cd4bf6247970d8aff54b",
|
||||
"4096":"02fc6b89b403ee9eb8a7ed457cd3973638080d6e04ca8af7307c965c166b555ea2",
|
||||
"8192":"0320265583e916d3a305f0d2687fcf2cd4e3cd03a16ea8261fda309c3ec5721e21",
|
||||
"16384":"036e41de58fdff3cb1d8d713f48c63bc61fa3b3e1631495a444d178363c0d2ed50",
|
||||
"32768":"0365438f613f19696264300b069d1dad93f0c60a37536b72a8ab7c7366a5ee6c04",
|
||||
"65536":"02408426cfb6fc86341bac79624ba8708a4376b2d92debdf4134813f866eb57a8d",
|
||||
"131072":"031063e9f11c94dc778c473e968966eac0e70b7145213fbaff5f7a007e71c65f41",
|
||||
"262144":"02f2a3e808f9cd168ec71b7f328258d0c1dda250659c1aced14c7f5cf05aab4328",
|
||||
"524288":"038ac10de9f1ff9395903bb73077e94dbf91e9ef98fd77d9a2debc5f74c575bc86",
|
||||
"1048576":"0203eaee4db749b0fc7c49870d082024b2c31d889f9bc3b32473d4f1dfa3625788",
|
||||
"2097152":"033cdb9d36e1e82ae652b7b6a08e0204569ec7ff9ebf85d80a02786dc7fe00b04c",
|
||||
"4194304":"02c8b73f4e3a470ae05e5f2fe39984d41e9f6ae7be9f3b09c9ac31292e403ac512",
|
||||
"8388608":"025bbe0cfce8a1f4fbd7f3a0d4a09cb6badd73ef61829dc827aa8a98c270bc25b0",
|
||||
"16777216":"037eec3d1651a30a90182d9287a5c51386fe35d4a96839cf7969c6e2a03db1fc21",
|
||||
"33554432":"03280576b81a04e6abd7197f305506476f5751356b7643988495ca5c3e14e5c262",
|
||||
"67108864":"03268bfb05be1dbb33ab6e7e00e438373ca2c9b9abc018fdb452d0e1a0935e10d3",
|
||||
"134217728":"02573b68784ceba9617bbcc7c9487836d296aa7c628c3199173a841e7a19798020",
|
||||
"268435456":"0234076b6e70f7fbf755d2227ecc8d8169d662518ee3a1401f729e2a12ccb2b276",
|
||||
"536870912":"03015bd88961e2a466a2163bd4248d1d2b42c7c58a157e594785e7eb34d880efc9",
|
||||
"1073741824":"02c9b076d08f9020ebee49ac8ba2610b404d4e553a4f800150ceb539e9421aaeee",
|
||||
"2147483648":"034d592f4c366afddc919a509600af81b489a03caf4f7517c2b3f4f2b558f9a41a",
|
||||
"4294967296":"037c09ecb66da082981e4cbdb1ac65c0eb631fc75d85bed13efb2c6364148879b5",
|
||||
"8589934592":"02b4ebb0dda3b9ad83b39e2e31024b777cc0ac205a96b9a6cfab3edea2912ed1b3",
|
||||
"17179869184":"026cc4dacdced45e63f6e4f62edbc5779ccd802e7fabb82d5123db879b636176e9",
|
||||
"34359738368":"02b2cee01b7d8e90180254459b8f09bbea9aad34c3a2fd98c85517ecfc9805af75",
|
||||
"68719476736":"037a0c0d564540fc574b8bfa0253cca987b75466e44b295ed59f6f8bd41aace754",
|
||||
"137438953472":"021df6585cae9b9ca431318a713fd73dbb76b3ef5667957e8633bca8aaa7214fb6",
|
||||
"274877906944":"02b8f53dde126f8c85fa5bb6061c0be5aca90984ce9b902966941caf963648d53a",
|
||||
"549755813888":"029cc8af2840d59f1d8761779b2496623c82c64be8e15f9ab577c657c6dd453785",
|
||||
"1099511627776":"03e446fdb84fad492ff3a25fc1046fb9a93a5b262ebcd0151caa442ea28959a38a",
|
||||
"2199023255552":"02d6b25bd4ab599dd0818c55f75702fde603c93f259222001246569018842d3258",
|
||||
"4398046511104":"03397b522bb4e156ec3952d3f048e5a986c20a00718e5e52cd5718466bf494156a",
|
||||
"8796093022208":"02d1fb9e78262b5d7d74028073075b80bb5ab281edcfc3191061962c1346340f1e",
|
||||
"17592186044416":"030d3f2ad7a4ca115712ff7f140434f802b19a4c9b2dd1c76f3e8e80c05c6a9310",
|
||||
"35184372088832":"03e325b691f292e1dfb151c3fb7cad440b225795583c32e24e10635a80e4221c06",
|
||||
"70368744177664":"03bee8f64d88de3dee21d61f89efa32933da51152ddbd67466bef815e9f93f8fd1",
|
||||
"140737488355328":"0327244c9019a4892e1f04ba3bf95fe43b327479e2d57c25979446cc508cd379ed",
|
||||
"281474976710656":"02fb58522cd662f2f8b042f8161caae6e45de98283f74d4e99f19b0ea85e08a56d",
|
||||
"562949953421312":"02adde4b466a9d7e59386b6a701a39717c53f30c4810613c1b55e6b6da43b7bc9a",
|
||||
"1125899906842624":"038eeda11f78ce05c774f30e393cda075192b890d68590813ff46362548528dca9",
|
||||
"2251799813685248":"02ec13e0058b196db80f7079d329333b330dc30c000dbdd7397cbbc5a37a664c4f",
|
||||
"4503599627370496":"02d2d162db63675bd04f7d56df04508840f41e2ad87312a3c93041b494efe80a73",
|
||||
"9007199254740992":"0356969d6aef2bb40121dbd07c68b6102339f4ea8e674a9008bb69506795998f49",
|
||||
"18014398509481984":"02f4e667567ebb9f4e6e180a4113bb071c48855f657766bb5e9c776a880335d1d6",
|
||||
"36028797018963968":"0385b4fe35e41703d7a657d957c67bb536629de57b7e6ee6fe2130728ef0fc90b0",
|
||||
"72057594037927936":"02b2bc1968a6fddbcc78fb9903940524824b5f5bed329c6ad48a19b56068c144fd",
|
||||
"144115188075855872":"02e0dbb24f1d288a693e8a49bc14264d1276be16972131520cf9e055ae92fba19a",
|
||||
"288230376151711744":"03efe75c106f931a525dc2d653ebedddc413a2c7d8cb9da410893ae7d2fa7d19cc",
|
||||
"576460752303423488":"02c7ec2bd9508a7fc03f73c7565dc600b30fd86f3d305f8f139c45c404a52d958a",
|
||||
"1152921504606846976":"035a6679c6b25e68ff4e29d1c7ef87f21e0a8fc574f6a08c1aa45ff352c1d59f06",
|
||||
"2305843009213693952":"033cdc225962c052d485f7cfbf55a5b2367d200fe1fe4373a347deb4cc99e9a099",
|
||||
"4611686018427387904":"024a4b806cf413d14b294719090a9da36ba75209c7657135ad09bc65328fba9e6f",
|
||||
"9223372036854775808":"0377a6fe114e291a8d8e991627c38001c8305b23b9e98b1c7b1893f5cd0dda6cad"
|
||||
}
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn deserialization_and_id_generation() {
|
||||
let keys: Keys = serde_json::from_str(KEYSET).unwrap();
|
||||
|
||||
let id = keys.id();
|
||||
|
||||
assert_eq!(id, KEYSET_ID);
|
||||
}
|
||||
}
|
||||
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>,
|
||||
}
|
||||
Reference in New Issue
Block a user