diff --git a/crates/cdk/src/nuts/nut00/mod.rs b/crates/cdk/src/nuts/nut00/mod.rs index 4aae0fab..9c7fd095 100644 --- a/crates/cdk/src/nuts/nut00/mod.rs +++ b/crates/cdk/src/nuts/nut00/mod.rs @@ -8,6 +8,7 @@ use std::hash::{Hash, Hasher}; use std::str::FromStr; use std::string::FromUtf8Error; +use bitcoin::hashes::{sha256, Hash as _, HashEngine as _}; use serde::{Deserialize, Deserializer, Serialize}; use thiserror::Error; @@ -21,6 +22,7 @@ use crate::nuts::nut12::BlindSignatureDleq; use crate::nuts::nut14::{serde_htlc_witness, HTLCWitness}; use crate::nuts::{Id, ProofDleq}; use crate::secret::Secret; +use crate::util::hex; use crate::Amount; pub mod token; @@ -65,6 +67,9 @@ pub enum Error { /// NUT11 error #[error(transparent)] NUT11(#[from] crate::nuts::nut11::Error), + /// Hex error + #[error(transparent)] + HexError(#[from] hex::Error), } /// Blinded Message (also called `output`) @@ -675,10 +680,51 @@ impl PartialOrd for PreMintSecrets { } } +/// Transaction ID +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct TransactionId([u8; 32]); + +impl TransactionId { + /// Create new [`TransactionId`] + pub fn new(proofs: Proofs) -> Result { + let mut ys = proofs + .iter() + .map(|proof| proof.y()) + .collect::, Error>>()?; + ys.sort(); + let mut hasher = sha256::Hash::engine(); + for y in ys { + hasher.input(&y.to_bytes()); + } + let hash = sha256::Hash::from_engine(hasher); + Ok(Self(hash.to_byte_array())) + } + + /// Get inner value + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } +} + +impl fmt::Display for TransactionId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", hex::encode(&self.0)) + } +} + +impl FromStr for TransactionId { + type Err = Error; + fn from_str(value: &str) -> Result { + let bytes = hex::decode(value)?; + let mut array = [0u8; 32]; + array.copy_from_slice(&bytes); + Ok(Self(array)) + } +} + #[cfg(test)] mod tests { - use std::str::FromStr; - use super::*; #[test] @@ -694,6 +740,22 @@ mod tests { assert_eq!(proof.len(), 2); } + #[test] + fn test_transaction_id() { + let proof = "[{\"id\":\"009a1f293253e41e\",\"amount\":2,\"secret\":\"407915bc212be61a77e3e6d2aeb4c727980bda51cd06a6afc29e2861768a7837\",\"C\":\"02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea\"},{\"id\":\"009a1f293253e41e\",\"amount\":8,\"secret\":\"fe15109314e61d7756b0f8ee0f23a624acaa3f4e042f61433c728c7057b931be\",\"C\":\"029e8e5050b890a7d6c0968db16bc1d5d5fa040ea1de284f6ec69d61299f671059\"}]"; + let mut proofs: Proofs = serde_json::from_str(proof).unwrap(); + + let transaction_id = TransactionId::new(proofs.clone()).unwrap(); + assert_eq!( + transaction_id.to_string(), + "dac0748828d855ac4bc0e0a008cbc4b02e7d4238af06d730461cc559a5ae24b1" + ); + + proofs.reverse(); + let rev_transaction_id = TransactionId::new(proofs).unwrap(); + assert_eq!(transaction_id, rev_transaction_id); + } + #[test] fn test_blank_blinded_messages() { let b = PreMintSecrets::blank(