nut00.rs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. //! Notation and Models
  2. // https://github.com/cashubtc/nuts/blob/main/00.md
  3. use serde::{Deserialize, Serialize};
  4. use super::nut01::PublicKey;
  5. use super::nut02::Id;
  6. use crate::secret::Secret;
  7. use crate::url::UncheckedUrl;
  8. use crate::Amount;
  9. /// Blinded Message [NUT-00]
  10. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  11. pub struct BlindedMessage {
  12. /// Amount in satoshi
  13. pub amount: Amount,
  14. /// encrypted secret message (B_)
  15. #[serde(rename = "B_")]
  16. pub b: PublicKey,
  17. }
  18. #[cfg(feature = "wallet")]
  19. pub mod wallet {
  20. use std::str::FromStr;
  21. use base64::engine::{general_purpose, GeneralPurpose};
  22. use base64::{alphabet, Engine as _};
  23. use serde::{Deserialize, Serialize};
  24. use url::Url;
  25. use super::MintProofs;
  26. use crate::dhke::blind_message;
  27. use crate::error::wallet;
  28. use crate::nuts::nut00::{BlindedMessage, Proofs};
  29. use crate::nuts::nut01;
  30. use crate::secret::Secret;
  31. use crate::url::UncheckedUrl;
  32. use crate::{error, Amount};
  33. /// Blinded Messages [NUT-00]
  34. #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)]
  35. pub struct BlindedMessages {
  36. /// Blinded messages
  37. pub blinded_messages: Vec<BlindedMessage>,
  38. /// Secrets
  39. pub secrets: Vec<Secret>,
  40. /// Rs
  41. pub rs: Vec<nut01::SecretKey>,
  42. /// Amounts
  43. pub amounts: Vec<Amount>,
  44. }
  45. impl BlindedMessages {
  46. /// Outputs for speceifed amount with random secret
  47. pub fn random(amount: Amount) -> Result<Self, wallet::Error> {
  48. let mut blinded_messages = BlindedMessages::default();
  49. for amount in amount.split() {
  50. let secret = Secret::new();
  51. let (blinded, r) = blind_message(secret.as_bytes(), None)?;
  52. let blinded_message = BlindedMessage { amount, b: blinded };
  53. blinded_messages.secrets.push(secret);
  54. blinded_messages.blinded_messages.push(blinded_message);
  55. blinded_messages.rs.push(r.into());
  56. blinded_messages.amounts.push(amount);
  57. }
  58. Ok(blinded_messages)
  59. }
  60. /// Blank Outputs used for NUT-08 change
  61. pub fn blank(fee_reserve: Amount) -> Result<Self, wallet::Error> {
  62. let mut blinded_messages = BlindedMessages::default();
  63. let fee_reserve = bitcoin::Amount::from_sat(fee_reserve.to_sat());
  64. let count = (fee_reserve
  65. .to_float_in(bitcoin::Denomination::Satoshi)
  66. .log2()
  67. .ceil() as u64)
  68. .max(1);
  69. for _i in 0..count {
  70. let secret = Secret::new();
  71. let (blinded, r) = blind_message(secret.as_bytes(), None)?;
  72. let blinded_message = BlindedMessage {
  73. amount: Amount::ZERO,
  74. b: blinded,
  75. };
  76. blinded_messages.secrets.push(secret);
  77. blinded_messages.blinded_messages.push(blinded_message);
  78. blinded_messages.rs.push(r.into());
  79. blinded_messages.amounts.push(Amount::ZERO);
  80. }
  81. Ok(blinded_messages)
  82. }
  83. }
  84. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  85. pub struct Token {
  86. pub token: Vec<MintProofs>,
  87. pub memo: Option<String>,
  88. }
  89. impl Token {
  90. pub fn new(
  91. mint_url: UncheckedUrl,
  92. proofs: Proofs,
  93. memo: Option<String>,
  94. ) -> Result<Self, wallet::Error> {
  95. if proofs.is_empty() {
  96. return Err(wallet::Error::ProofsRequired);
  97. }
  98. // Check Url is valid
  99. let _: Url = (&mint_url).try_into()?;
  100. Ok(Self {
  101. token: vec![MintProofs::new(mint_url, proofs)],
  102. memo,
  103. })
  104. }
  105. pub fn token_info(&self) -> (u64, String) {
  106. let mut amount = Amount::ZERO;
  107. for proofs in &self.token {
  108. for proof in &proofs.proofs {
  109. amount += proof.amount;
  110. }
  111. }
  112. (amount.to_sat(), self.token[0].mint.to_string())
  113. }
  114. }
  115. impl FromStr for Token {
  116. type Err = error::wallet::Error;
  117. fn from_str(s: &str) -> Result<Self, Self::Err> {
  118. if !s.starts_with("cashuA") {
  119. return Err(wallet::Error::UnsupportedToken);
  120. }
  121. let s = s.replace("cashuA", "");
  122. let decode_config = general_purpose::GeneralPurposeConfig::new()
  123. .with_decode_padding_mode(base64::engine::DecodePaddingMode::Indifferent);
  124. let decoded = GeneralPurpose::new(&alphabet::STANDARD, decode_config).decode(s)?;
  125. let decoded_str = String::from_utf8(decoded)?;
  126. let token: Token = serde_json::from_str(&decoded_str)?;
  127. Ok(token)
  128. }
  129. }
  130. impl Token {
  131. pub fn convert_to_string(&self) -> Result<String, wallet::Error> {
  132. let json_string = serde_json::to_string(self)?;
  133. let encoded = general_purpose::STANDARD.encode(json_string);
  134. Ok(format!("cashuA{}", encoded))
  135. }
  136. }
  137. }
  138. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  139. pub struct MintProofs {
  140. pub mint: UncheckedUrl,
  141. pub proofs: Proofs,
  142. }
  143. #[cfg(feature = "wallet")]
  144. impl MintProofs {
  145. fn new(mint_url: UncheckedUrl, proofs: Proofs) -> Self {
  146. Self {
  147. mint: mint_url,
  148. proofs,
  149. }
  150. }
  151. }
  152. /// Promise (BlindedSignature) [NUT-00]
  153. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  154. pub struct BlindedSignature {
  155. pub id: Id,
  156. pub amount: Amount,
  157. /// blinded signature (C_) on the secret message `B_` of [BlindedMessage]
  158. #[serde(rename = "C_")]
  159. pub c: PublicKey,
  160. }
  161. /// Proofs [NUT-00]
  162. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  163. pub struct Proof {
  164. /// Amount in satoshi
  165. pub amount: Amount,
  166. /// Secret message
  167. pub secret: Secret,
  168. /// Unblinded signature
  169. #[serde(rename = "C")]
  170. pub c: PublicKey,
  171. /// `Keyset id`
  172. pub id: Option<Id>,
  173. }
  174. /// List of proofs
  175. pub type Proofs = Vec<Proof>;
  176. impl From<Proof> for mint::Proof {
  177. fn from(proof: Proof) -> Self {
  178. Self {
  179. amount: Some(proof.amount),
  180. secret: proof.secret,
  181. c: Some(proof.c),
  182. id: proof.id,
  183. }
  184. }
  185. }
  186. pub mod mint {
  187. use serde::{Deserialize, Serialize};
  188. use super::PublicKey;
  189. use crate::nuts::nut02::Id;
  190. use crate::secret::Secret;
  191. use crate::Amount;
  192. /// Proofs [NUT-00]
  193. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  194. pub struct Proof {
  195. /// Amount in satoshi
  196. pub amount: Option<Amount>,
  197. /// Secret message
  198. pub secret: Secret,
  199. /// Unblinded signature
  200. #[serde(rename = "C")]
  201. pub c: Option<PublicKey>,
  202. /// `Keyset id`
  203. pub id: Option<Id>,
  204. }
  205. /// List of proofs
  206. pub type Proofs = Vec<Proof>;
  207. pub fn mint_proofs_from_proofs(proofs: super::Proofs) -> Proofs {
  208. proofs.iter().map(|p| p.to_owned().into()).collect()
  209. }
  210. }
  211. #[cfg(test)]
  212. mod tests {
  213. use std::str::FromStr;
  214. use super::wallet::*;
  215. use super::*;
  216. #[test]
  217. fn test_proof_serialize() {
  218. let proof = "[{\"id\":\"DSAl9nvvyfva\",\"amount\":2,\"secret\":\"EhpennC9qB3iFlW8FZ_pZw\",\"C\":\"02c020067db727d586bc3183aecf97fcb800c3f4cc4759f69c626c9db5d8f5b5d4\"},{\"id\":\"DSAl9nvvyfva\",\"amount\":8,\"secret\":\"TmS6Cv0YT5PU_5ATVKnukw\",\"C\":\"02ac910bef28cbe5d7325415d5c263026f15f9b967a079ca9779ab6e5c2db133a7\"}]";
  219. let proof: Proofs = serde_json::from_str(proof).unwrap();
  220. assert_eq!(
  221. proof[0].clone().id.unwrap(),
  222. Id::try_from_base64("DSAl9nvvyfva").unwrap()
  223. );
  224. }
  225. #[test]
  226. fn test_token_str_round_trip() {
  227. let token_str = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJpZCI6IkRTQWw5bnZ2eWZ2YSIsImFtb3VudCI6Miwic2VjcmV0IjoiRWhwZW5uQzlxQjNpRmxXOEZaX3BadyIsIkMiOiIwMmMwMjAwNjdkYjcyN2Q1ODZiYzMxODNhZWNmOTdmY2I4MDBjM2Y0Y2M0NzU5ZjY5YzYyNmM5ZGI1ZDhmNWI1ZDQifSx7ImlkIjoiRFNBbDludnZ5ZnZhIiwiYW1vdW50Ijo4LCJzZWNyZXQiOiJUbVM2Q3YwWVQ1UFVfNUFUVktudWt3IiwiQyI6IjAyYWM5MTBiZWYyOGNiZTVkNzMyNTQxNWQ1YzI2MzAyNmYxNWY5Yjk2N2EwNzljYTk3NzlhYjZlNWMyZGIxMzNhNyJ9XX1dLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
  228. let token = Token::from_str(token_str).unwrap();
  229. assert_eq!(
  230. token.token[0].mint,
  231. UncheckedUrl::from_str("https://8333.space:3338").unwrap()
  232. );
  233. assert_eq!(
  234. token.token[0].proofs[0].clone().id.unwrap(),
  235. Id::try_from_base64("DSAl9nvvyfva").unwrap()
  236. );
  237. let encoded = &token.convert_to_string().unwrap();
  238. let token_data = Token::from_str(encoded).unwrap();
  239. assert_eq!(token_data, token);
  240. }
  241. #[test]
  242. fn test_token_with_and_without_padding() {
  243. let proof = "[{\"id\":\"DSAl9nvvyfva\",\"amount\":2,\"secret\":\"EhpennC9qB3iFlW8FZ_pZw\",\"C\":\"02c020067db727d586bc3183aecf97fcb800c3f4cc4759f69c626c9db5d8f5b5d4\"},{\"id\":\"DSAl9nvvyfva\",\"amount\":8,\"secret\":\"TmS6Cv0YT5PU_5ATVKnukw\",\"C\":\"02ac910bef28cbe5d7325415d5c263026f15f9b967a079ca9779ab6e5c2db133a7\"}]";
  244. let proof: Proofs = serde_json::from_str(proof).unwrap();
  245. let token = Token::new(
  246. UncheckedUrl::from_str("https://localhost:5000/cashu").unwrap(),
  247. proof,
  248. None,
  249. )
  250. .unwrap();
  251. let _token = Token::from_str(&token.convert_to_string().unwrap()).unwrap();
  252. let _token = Token::from_str("cashuAeyJ0b2tlbiI6W3sicHJvb2ZzIjpbeyJpZCI6IjBOSTNUVUFzMVNmeSIsImFtb3VudCI6MSwic2VjcmV0IjoiVE92cGVmZGxSZ0EzdlhMN05pM2MvRE1oY29URXNQdnV4eFc0Rys2dXVycz0iLCJDIjoiMDNiZThmMzQwOTMxYTI4ZTlkMGRmNGFmMWQwMWY1ZTcxNTFkMmQ1M2RiN2Y0ZDAyMWQzZGUwZmRiMDNjZGY4ZTlkIn1dLCJtaW50IjoiaHR0cHM6Ly9sZWdlbmQubG5iaXRzLmNvbS9jYXNodS9hcGkvdjEvNGdyOVhjbXozWEVrVU53aUJpUUdvQyJ9XX0").unwrap();
  253. }
  254. #[test]
  255. fn test_blank_blinded_messages() {
  256. let b = BlindedMessages::blank(Amount::from_sat(1000)).unwrap();
  257. assert_eq!(b.blinded_messages.len(), 10);
  258. let b = BlindedMessages::blank(Amount::from_sat(1)).unwrap();
  259. assert_eq!(b.blinded_messages.len(), 1);
  260. }
  261. }