token.rs 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829
  1. //! Cashu Token
  2. //!
  3. //! <https://github.com/cashubtc/nuts/blob/main/00.md>
  4. use std::collections::HashMap;
  5. use std::fmt;
  6. use std::str::FromStr;
  7. use bitcoin::base64::engine::{general_purpose, GeneralPurpose};
  8. use bitcoin::base64::{alphabet, Engine as _};
  9. use serde::{Deserialize, Serialize};
  10. use super::{Error, Proof, ProofV3, ProofV4, Proofs};
  11. use crate::mint_url::MintUrl;
  12. use crate::nut02::ShortKeysetId;
  13. use crate::nuts::{CurrencyUnit, Id};
  14. use crate::{ensure_cdk, Amount, KeySetInfo};
  15. /// Token Enum
  16. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  17. #[serde(untagged)]
  18. pub enum Token {
  19. /// Token V3
  20. TokenV3(TokenV3),
  21. /// Token V4
  22. TokenV4(TokenV4),
  23. }
  24. impl fmt::Display for Token {
  25. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  26. let token = match self {
  27. Self::TokenV3(token) => token.to_string(),
  28. Self::TokenV4(token) => token.to_string(),
  29. };
  30. write!(f, "{token}")
  31. }
  32. }
  33. impl Token {
  34. /// Create new [`Token`]
  35. pub fn new(
  36. mint_url: MintUrl,
  37. proofs: Proofs,
  38. memo: Option<String>,
  39. unit: CurrencyUnit,
  40. ) -> Self {
  41. let proofs = proofs
  42. .into_iter()
  43. .fold(HashMap::new(), |mut acc, val| {
  44. acc.entry(val.keyset_id)
  45. .and_modify(|p: &mut Vec<Proof>| p.push(val.clone()))
  46. .or_insert(vec![val.clone()]);
  47. acc
  48. })
  49. .into_iter()
  50. .map(|(id, proofs)| TokenV4Token::new(id, proofs))
  51. .collect();
  52. Token::TokenV4(TokenV4 {
  53. mint_url,
  54. unit,
  55. memo,
  56. token: proofs,
  57. })
  58. }
  59. /// Proofs in [`Token`]
  60. pub fn proofs(&self, mint_keysets: &[KeySetInfo]) -> Result<Proofs, Error> {
  61. match self {
  62. Self::TokenV3(token) => token.proofs(mint_keysets),
  63. Self::TokenV4(token) => token.proofs(mint_keysets),
  64. }
  65. }
  66. /// Total value of [`Token`]
  67. pub fn value(&self) -> Result<Amount, Error> {
  68. match self {
  69. Self::TokenV3(token) => token.value(),
  70. Self::TokenV4(token) => token.value(),
  71. }
  72. }
  73. /// [`Token`] memo
  74. pub fn memo(&self) -> &Option<String> {
  75. match self {
  76. Self::TokenV3(token) => token.memo(),
  77. Self::TokenV4(token) => token.memo(),
  78. }
  79. }
  80. /// Unit
  81. pub fn unit(&self) -> Option<CurrencyUnit> {
  82. match self {
  83. Self::TokenV3(token) => token.unit().clone(),
  84. Self::TokenV4(token) => Some(token.unit().clone()),
  85. }
  86. }
  87. /// Mint url
  88. pub fn mint_url(&self) -> Result<MintUrl, Error> {
  89. match self {
  90. Self::TokenV3(token) => {
  91. let mint_urls = token.mint_urls();
  92. ensure_cdk!(mint_urls.len() == 1, Error::UnsupportedToken);
  93. mint_urls.first().ok_or(Error::UnsupportedToken).cloned()
  94. }
  95. Self::TokenV4(token) => Ok(token.mint_url.clone()),
  96. }
  97. }
  98. /// To v3 string
  99. pub fn to_v3_string(&self) -> String {
  100. let v3_token = match self {
  101. Self::TokenV3(token) => token.clone(),
  102. Self::TokenV4(token) => token.clone().into(),
  103. };
  104. v3_token.to_string()
  105. }
  106. /// Serialize the token to raw binary
  107. pub fn to_raw_bytes(&self) -> Result<Vec<u8>, Error> {
  108. match self {
  109. Self::TokenV3(_) => Err(Error::UnsupportedToken),
  110. Self::TokenV4(token) => token.to_raw_bytes(),
  111. }
  112. }
  113. }
  114. impl FromStr for Token {
  115. type Err = Error;
  116. fn from_str(s: &str) -> Result<Self, Self::Err> {
  117. let (is_v3, s) = match (s.strip_prefix("cashuA"), s.strip_prefix("cashuB")) {
  118. (Some(s), None) => (true, s),
  119. (None, Some(s)) => (false, s),
  120. _ => return Err(Error::UnsupportedToken),
  121. };
  122. let decode_config = general_purpose::GeneralPurposeConfig::new()
  123. .with_decode_padding_mode(bitcoin::base64::engine::DecodePaddingMode::Indifferent);
  124. let decoded = GeneralPurpose::new(&alphabet::URL_SAFE, decode_config).decode(s)?;
  125. match is_v3 {
  126. true => {
  127. let decoded_str = String::from_utf8(decoded)?;
  128. let token: TokenV3 = serde_json::from_str(&decoded_str)?;
  129. Ok(Token::TokenV3(token))
  130. }
  131. false => {
  132. let token: TokenV4 = ciborium::from_reader(&decoded[..])?;
  133. Ok(Token::TokenV4(token))
  134. }
  135. }
  136. }
  137. }
  138. impl TryFrom<&Vec<u8>> for Token {
  139. type Error = Error;
  140. fn try_from(bytes: &Vec<u8>) -> Result<Self, Self::Error> {
  141. ensure_cdk!(bytes.len() >= 5, Error::UnsupportedToken);
  142. let prefix = String::from_utf8(bytes[..5].to_vec())?;
  143. match prefix.as_str() {
  144. "crawB" => {
  145. let token: TokenV4 = ciborium::from_reader(&bytes[5..])?;
  146. Ok(Token::TokenV4(token))
  147. }
  148. _ => Err(Error::UnsupportedToken),
  149. }
  150. }
  151. }
  152. /// Token V3 Token
  153. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  154. pub struct TokenV3Token {
  155. /// Url of mint
  156. pub mint: MintUrl,
  157. /// [`Vec<ProofV3>`]
  158. pub proofs: Vec<ProofV3>,
  159. }
  160. impl TokenV3Token {
  161. /// Create new [`TokenV3Token`]
  162. pub fn new(mint_url: MintUrl, proofs: Proofs) -> Self {
  163. Self {
  164. mint: mint_url,
  165. proofs: proofs.into_iter().map(ProofV3::from).collect(),
  166. }
  167. }
  168. }
  169. /// Token
  170. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  171. pub struct TokenV3 {
  172. /// Proofs in [`Token`] by mint
  173. pub token: Vec<TokenV3Token>,
  174. /// Memo for token
  175. #[serde(skip_serializing_if = "Option::is_none")]
  176. pub memo: Option<String>,
  177. /// Token Unit
  178. #[serde(skip_serializing_if = "Option::is_none")]
  179. pub unit: Option<CurrencyUnit>,
  180. }
  181. impl TokenV3 {
  182. /// Create new [`Token`]
  183. pub fn new(
  184. mint_url: MintUrl,
  185. proofs: Proofs,
  186. memo: Option<String>,
  187. unit: Option<CurrencyUnit>,
  188. ) -> Result<Self, Error> {
  189. ensure_cdk!(!proofs.is_empty(), Error::ProofsRequired);
  190. Ok(Self {
  191. token: vec![TokenV3Token::new(mint_url, proofs)],
  192. memo,
  193. unit,
  194. })
  195. }
  196. /// Proofs
  197. pub fn proofs(&self, mint_keysets: &[KeySetInfo]) -> Result<Proofs, Error> {
  198. let mut proofs: Proofs = vec![];
  199. for t in self.token.iter() {
  200. for p in t.proofs.iter() {
  201. let long_id = Id::from_short_keyset_id(&p.keyset_id, mint_keysets)?;
  202. proofs.push(p.into_proof(&long_id));
  203. }
  204. }
  205. Ok(proofs)
  206. }
  207. /// Value - errors if duplicate proofs are found
  208. #[inline]
  209. pub fn value(&self) -> Result<Amount, Error> {
  210. let proofs: Vec<ProofV3> = self.token.iter().flat_map(|t| t.proofs.clone()).collect();
  211. let unique_count = proofs
  212. .iter()
  213. .collect::<std::collections::HashSet<_>>()
  214. .len();
  215. // Check if there are any duplicate proofs
  216. if unique_count != proofs.len() {
  217. return Err(Error::DuplicateProofs);
  218. }
  219. Ok(Amount::try_sum(
  220. self.token
  221. .iter()
  222. .map(|t| Amount::try_sum(t.proofs.iter().map(|p| p.amount)))
  223. .collect::<Result<Vec<Amount>, _>>()?,
  224. )?)
  225. }
  226. /// Memo
  227. #[inline]
  228. pub fn memo(&self) -> &Option<String> {
  229. &self.memo
  230. }
  231. /// Unit
  232. #[inline]
  233. pub fn unit(&self) -> &Option<CurrencyUnit> {
  234. &self.unit
  235. }
  236. /// Mint Url
  237. pub fn mint_urls(&self) -> Vec<MintUrl> {
  238. let mut mint_urls = Vec::new();
  239. for token in self.token.iter() {
  240. mint_urls.push(token.mint.clone());
  241. }
  242. mint_urls
  243. }
  244. /// Checks if a token has multiple mints
  245. ///
  246. /// These tokens are not supported by this crate
  247. pub fn is_multi_mint(&self) -> bool {
  248. self.token.len() > 1
  249. }
  250. }
  251. impl FromStr for TokenV3 {
  252. type Err = Error;
  253. fn from_str(s: &str) -> Result<Self, Self::Err> {
  254. let s = s.strip_prefix("cashuA").ok_or(Error::UnsupportedToken)?;
  255. let decode_config = general_purpose::GeneralPurposeConfig::new()
  256. .with_decode_padding_mode(bitcoin::base64::engine::DecodePaddingMode::Indifferent);
  257. let decoded = GeneralPurpose::new(&alphabet::URL_SAFE, decode_config).decode(s)?;
  258. let decoded_str = String::from_utf8(decoded)?;
  259. let token: TokenV3 = serde_json::from_str(&decoded_str)?;
  260. Ok(token)
  261. }
  262. }
  263. impl fmt::Display for TokenV3 {
  264. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  265. let json_string = serde_json::to_string(self).map_err(|_| fmt::Error)?;
  266. let encoded = general_purpose::URL_SAFE.encode(json_string);
  267. write!(f, "cashuA{encoded}")
  268. }
  269. }
  270. impl From<TokenV4> for TokenV3 {
  271. fn from(token: TokenV4) -> Self {
  272. let proofs: Vec<ProofV3> = token
  273. .token
  274. .into_iter()
  275. .flat_map(|token| {
  276. token.proofs.into_iter().map(move |p| ProofV3 {
  277. amount: p.amount,
  278. keyset_id: token.keyset_id.clone(),
  279. secret: p.secret,
  280. c: p.c,
  281. witness: p.witness,
  282. dleq: p.dleq,
  283. })
  284. })
  285. .collect();
  286. let token_v3_token = TokenV3Token {
  287. mint: token.mint_url,
  288. proofs,
  289. };
  290. TokenV3 {
  291. token: vec![token_v3_token],
  292. memo: token.memo,
  293. unit: Some(token.unit),
  294. }
  295. }
  296. }
  297. /// Token V4
  298. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  299. pub struct TokenV4 {
  300. /// Mint Url
  301. #[serde(rename = "m")]
  302. pub mint_url: MintUrl,
  303. /// Token Unit
  304. #[serde(rename = "u")]
  305. pub unit: CurrencyUnit,
  306. /// Memo for token
  307. #[serde(rename = "d", skip_serializing_if = "Option::is_none")]
  308. pub memo: Option<String>,
  309. /// Proofs grouped by keyset_id
  310. #[serde(rename = "t")]
  311. pub token: Vec<TokenV4Token>,
  312. }
  313. impl TokenV4 {
  314. /// Proofs from token
  315. pub fn proofs(&self, mint_keysets: &[KeySetInfo]) -> Result<Proofs, Error> {
  316. let mut proofs: Proofs = vec![];
  317. for t in self.token.iter() {
  318. let long_id = Id::from_short_keyset_id(&t.keyset_id, mint_keysets)?;
  319. proofs.extend(t.proofs.iter().map(|p| p.into_proof(&long_id)));
  320. }
  321. Ok(proofs)
  322. }
  323. /// Value - errors if duplicate proofs are found
  324. #[inline]
  325. pub fn value(&self) -> Result<Amount, Error> {
  326. let proofs: Vec<ProofV4> = self.token.iter().flat_map(|t| t.proofs.clone()).collect();
  327. let unique_count = proofs
  328. .iter()
  329. .collect::<std::collections::HashSet<_>>()
  330. .len();
  331. // Check if there are any duplicate proofs
  332. if unique_count != proofs.len() {
  333. return Err(Error::DuplicateProofs);
  334. }
  335. Ok(Amount::try_sum(
  336. self.token
  337. .iter()
  338. .map(|t| Amount::try_sum(t.proofs.iter().map(|p| p.amount)))
  339. .collect::<Result<Vec<Amount>, _>>()?,
  340. )?)
  341. }
  342. /// Memo
  343. #[inline]
  344. pub fn memo(&self) -> &Option<String> {
  345. &self.memo
  346. }
  347. /// Unit
  348. #[inline]
  349. pub fn unit(&self) -> &CurrencyUnit {
  350. &self.unit
  351. }
  352. /// Serialize the token to raw binary
  353. pub fn to_raw_bytes(&self) -> Result<Vec<u8>, Error> {
  354. let mut prefix = b"crawB".to_vec();
  355. let mut data = Vec::new();
  356. ciborium::into_writer(self, &mut data).map_err(Error::CiboriumSerError)?;
  357. prefix.extend(data);
  358. Ok(prefix)
  359. }
  360. }
  361. impl fmt::Display for TokenV4 {
  362. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  363. use serde::ser::Error;
  364. let mut data = Vec::new();
  365. ciborium::into_writer(self, &mut data).map_err(|e| fmt::Error::custom(e.to_string()))?;
  366. let encoded = general_purpose::URL_SAFE.encode(data);
  367. write!(f, "cashuB{encoded}")
  368. }
  369. }
  370. impl FromStr for TokenV4 {
  371. type Err = Error;
  372. fn from_str(s: &str) -> Result<Self, Self::Err> {
  373. let s = s.strip_prefix("cashuB").ok_or(Error::UnsupportedToken)?;
  374. let decode_config = general_purpose::GeneralPurposeConfig::new()
  375. .with_decode_padding_mode(bitcoin::base64::engine::DecodePaddingMode::Indifferent);
  376. let decoded = GeneralPurpose::new(&alphabet::URL_SAFE, decode_config).decode(s)?;
  377. let token: TokenV4 = ciborium::from_reader(&decoded[..])?;
  378. Ok(token)
  379. }
  380. }
  381. impl TryFrom<&Vec<u8>> for TokenV4 {
  382. type Error = Error;
  383. fn try_from(bytes: &Vec<u8>) -> Result<Self, Self::Error> {
  384. ensure_cdk!(bytes.len() >= 5, Error::UnsupportedToken);
  385. let prefix = String::from_utf8(bytes[..5].to_vec())?;
  386. ensure_cdk!(prefix.as_str() == "crawB", Error::UnsupportedToken);
  387. Ok(ciborium::from_reader(&bytes[5..])?)
  388. }
  389. }
  390. impl TryFrom<TokenV3> for TokenV4 {
  391. type Error = Error;
  392. fn try_from(token: TokenV3) -> Result<Self, Self::Error> {
  393. let mint_urls = token.mint_urls();
  394. let proofs: Vec<ProofV3> = token.token.into_iter().flat_map(|t| t.proofs).collect();
  395. ensure_cdk!(mint_urls.len() == 1, Error::UnsupportedToken);
  396. let mint_url = mint_urls.first().ok_or(Error::UnsupportedToken)?;
  397. let proofs = proofs
  398. .into_iter()
  399. .fold(
  400. HashMap::<ShortKeysetId, Vec<ProofV4>>::new(),
  401. |mut acc, val| {
  402. acc.entry(val.keyset_id.clone())
  403. .and_modify(|p: &mut Vec<ProofV4>| p.push(val.clone().into()))
  404. .or_insert(vec![val.clone().into()]);
  405. acc
  406. },
  407. )
  408. .into_iter()
  409. .map(|(id, proofs)| TokenV4Token {
  410. keyset_id: id,
  411. proofs,
  412. })
  413. .collect();
  414. Ok(TokenV4 {
  415. mint_url: mint_url.clone(),
  416. token: proofs,
  417. memo: token.memo,
  418. unit: token.unit.ok_or(Error::UnsupportedUnit)?,
  419. })
  420. }
  421. }
  422. /// Token V4 Token
  423. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  424. pub struct TokenV4Token {
  425. /// `Keyset id`
  426. #[serde(
  427. rename = "i",
  428. serialize_with = "serialize_v4_keyset_id",
  429. deserialize_with = "deserialize_v4_keyset_id"
  430. )]
  431. pub keyset_id: ShortKeysetId,
  432. /// Proofs
  433. #[serde(rename = "p")]
  434. pub proofs: Vec<ProofV4>,
  435. }
  436. fn serialize_v4_keyset_id<S>(keyset_id: &ShortKeysetId, serializer: S) -> Result<S::Ok, S::Error>
  437. where
  438. S: serde::Serializer,
  439. {
  440. serializer.serialize_bytes(&keyset_id.to_bytes())
  441. }
  442. fn deserialize_v4_keyset_id<'de, D>(deserializer: D) -> Result<ShortKeysetId, D::Error>
  443. where
  444. D: serde::Deserializer<'de>,
  445. {
  446. let bytes = Vec::<u8>::deserialize(deserializer)?;
  447. ShortKeysetId::from_bytes(&bytes).map_err(serde::de::Error::custom)
  448. }
  449. impl TokenV4Token {
  450. /// Create new [`TokenV4Token`]
  451. pub fn new(keyset_id: Id, proofs: Proofs) -> Self {
  452. // Create a short keyset id from id
  453. let short_id = ShortKeysetId::from(keyset_id);
  454. Self {
  455. keyset_id: short_id,
  456. proofs: proofs.into_iter().map(|p| p.into()).collect(),
  457. }
  458. }
  459. }
  460. #[cfg(test)]
  461. mod tests {
  462. use std::str::FromStr;
  463. use bip39::rand::{self, RngCore};
  464. use super::*;
  465. use crate::dhke::hash_to_curve;
  466. use crate::mint_url::MintUrl;
  467. use crate::secret::Secret;
  468. use crate::util::hex;
  469. #[test]
  470. fn test_token_padding() {
  471. let token_str_with_padding = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91IHZlcnkgbXVjaC4ifQ==";
  472. let token = TokenV3::from_str(token_str_with_padding).unwrap();
  473. let token_str_without_padding = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91IHZlcnkgbXVjaC4ifQ";
  474. let token_without = TokenV3::from_str(token_str_without_padding).unwrap();
  475. assert_eq!(token, token_without);
  476. }
  477. #[test]
  478. fn test_token_v4_str_round_trip() {
  479. let token_str = "cashuBpGF0gaJhaUgArSaMTR9YJmFwgaNhYQFhc3hAOWE2ZGJiODQ3YmQyMzJiYTc2ZGIwZGYxOTcyMTZiMjlkM2I4Y2MxNDU1M2NkMjc4MjdmYzFjYzk0MmZlZGI0ZWFjWCEDhhhUP_trhpXfStS6vN6So0qWvc2X3O4NfM-Y1HISZ5JhZGlUaGFuayB5b3VhbXVodHRwOi8vbG9jYWxob3N0OjMzMzhhdWNzYXQ=";
  480. let token = TokenV4::from_str(token_str).unwrap();
  481. assert_eq!(
  482. token.mint_url,
  483. MintUrl::from_str("http://localhost:3338").unwrap()
  484. );
  485. assert_eq!(
  486. token.token[0].keyset_id,
  487. ShortKeysetId::from_str("00ad268c4d1f5826").unwrap()
  488. );
  489. let encoded = &token.to_string();
  490. let token_data = TokenV4::from_str(encoded).unwrap();
  491. assert_eq!(token_data, token);
  492. }
  493. #[test]
  494. fn test_token_v4_multi_keyset() {
  495. let token_str_multi_keysets = "cashuBo2F0gqJhaUgA_9SLj17PgGFwgaNhYQFhc3hAYWNjMTI0MzVlN2I4NDg0YzNjZjE4NTAxNDkyMThhZjkwZjcxNmE1MmJmNGE1ZWQzNDdlNDhlY2MxM2Y3NzM4OGFjWCECRFODGd5IXVW-07KaZCvuWHk3WrnnpiDhHki6SCQh88-iYWlIAK0mjE0fWCZhcIKjYWECYXN4QDEzMjNkM2Q0NzA3YTU4YWQyZTIzYWRhNGU5ZjFmNDlmNWE1YjRhYzdiNzA4ZWIwZDYxZjczOGY0ODMwN2U4ZWVhY1ghAjRWqhENhLSsdHrr2Cw7AFrKUL9Ffr1XN6RBT6w659lNo2FhAWFzeEA1NmJjYmNiYjdjYzY0MDZiM2ZhNWQ1N2QyMTc0ZjRlZmY4YjQ0MDJiMTc2OTI2ZDNhNTdkM2MzZGNiYjU5ZDU3YWNYIQJzEpxXGeWZN5qXSmJjY8MzxWyvwObQGr5G1YCCgHicY2FtdWh0dHA6Ly9sb2NhbGhvc3Q6MzMzOGF1Y3NhdA==";
  496. let token = Token::from_str(token_str_multi_keysets).unwrap();
  497. let amount = token.value().expect("valid amount");
  498. assert_eq!(amount, Amount::from(4));
  499. let unit = token.unit().unwrap();
  500. assert_eq!(CurrencyUnit::Sat, unit);
  501. match token {
  502. Token::TokenV4(token) => {
  503. let tokens: Vec<ShortKeysetId> =
  504. token.token.iter().map(|t| t.keyset_id.clone()).collect();
  505. assert_eq!(tokens.len(), 2);
  506. assert!(tokens.contains(&ShortKeysetId::from_str("00ffd48b8f5ecf80").unwrap()));
  507. assert!(tokens.contains(&ShortKeysetId::from_str("00ad268c4d1f5826").unwrap()));
  508. let mint_url = token.mint_url;
  509. assert_eq!("http://localhost:3338", &mint_url.to_string());
  510. }
  511. _ => {
  512. panic!("Token should be a v4 token")
  513. }
  514. }
  515. }
  516. #[test]
  517. fn test_tokenv4_from_tokenv3() {
  518. let token_v3_str = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
  519. let token_v3 =
  520. TokenV3::from_str(token_v3_str).expect("TokenV3 should be created from string");
  521. let token_v4 = TokenV4::try_from(token_v3).expect("TokenV3 should be converted to TokenV4");
  522. let token_v4_expected = "cashuBpGFtd2h0dHBzOi8vODMzMy5zcGFjZTozMzM4YXVjc2F0YWRqVGhhbmsgeW91LmF0gaJhaUgAmh8pMlPkHmFwgqRhYQJhc3hANDA3OTE1YmMyMTJiZTYxYTc3ZTNlNmQyYWViNGM3Mjc5ODBiZGE1MWNkMDZhNmFmYzI5ZTI4NjE3NjhhNzgzN2FjWCECvJCXmX2Br7LMc0a15DRak0a9KlBut5WFmKcvDPhRY-phZPakYWEIYXN4QGZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmVhY1ghAp6OUFC4kKfWwJaNsWvB1dX6BA6h3ihPbsadYSmfZxBZYWT2";
  523. assert_eq!(token_v4.to_string(), token_v4_expected);
  524. }
  525. #[test]
  526. fn test_token_str_round_trip() {
  527. let token_str = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
  528. let token = TokenV3::from_str(token_str).unwrap();
  529. assert_eq!(
  530. token.token[0].mint,
  531. MintUrl::from_str("https://8333.space:3338").unwrap()
  532. );
  533. assert_eq!(
  534. token.token[0].proofs[0].clone().keyset_id,
  535. ShortKeysetId::from_str("009a1f293253e41e").unwrap()
  536. );
  537. assert_eq!(token.unit.clone().unwrap(), CurrencyUnit::Sat);
  538. let encoded = &token.to_string();
  539. let token_data = TokenV3::from_str(encoded).unwrap();
  540. assert_eq!(token_data, token);
  541. }
  542. #[test]
  543. fn incorrect_tokens() {
  544. let incorrect_prefix = "casshuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
  545. let incorrect_prefix_token = TokenV3::from_str(incorrect_prefix);
  546. assert!(incorrect_prefix_token.is_err());
  547. let no_prefix = "eyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
  548. let no_prefix_token = TokenV3::from_str(no_prefix);
  549. assert!(no_prefix_token.is_err());
  550. let correct_token = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
  551. let correct_token = TokenV3::from_str(correct_token);
  552. assert!(correct_token.is_ok());
  553. }
  554. #[test]
  555. fn test_token_v4_raw_roundtrip() {
  556. let token_raw = hex::decode("6372617742a4617481a261694800ad268c4d1f5826617081a3616101617378403961366462623834376264323332626137366462306466313937323136623239643362386363313435353363643237383237666331636339343266656462346561635821038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d4721267926164695468616e6b20796f75616d75687474703a2f2f6c6f63616c686f73743a33333338617563736174").unwrap();
  557. let token = TokenV4::try_from(&token_raw).expect("Token deserialization error");
  558. let token_raw_ = token.to_raw_bytes().expect("Token serialization error");
  559. let token_ = TokenV4::try_from(&token_raw_).expect("Token deserialization error");
  560. assert!(token_ == token)
  561. }
  562. #[test]
  563. fn test_token_generic_raw_roundtrip() {
  564. let tokenv4_raw = hex::decode("6372617742a4617481a261694800ad268c4d1f5826617081a3616101617378403961366462623834376264323332626137366462306466313937323136623239643362386363313435353363643237383237666331636339343266656462346561635821038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d4721267926164695468616e6b20796f75616d75687474703a2f2f6c6f63616c686f73743a33333338617563736174").unwrap();
  565. let tokenv4 = Token::try_from(&tokenv4_raw).expect("Token deserialization error");
  566. let tokenv4_ = TokenV4::try_from(&tokenv4_raw).expect("Token deserialization error");
  567. let tokenv4_bytes = tokenv4.to_raw_bytes().expect("Serialization error");
  568. let tokenv4_bytes_ = tokenv4_.to_raw_bytes().expect("Serialization error");
  569. assert!(tokenv4_bytes_ == tokenv4_bytes);
  570. }
  571. #[test]
  572. fn test_token_with_duplicate_proofs() {
  573. // Create a token with duplicate proofs
  574. let mint_url = MintUrl::from_str("https://example.com").unwrap();
  575. let keyset_id = Id::from_str("009a1f293253e41e").unwrap();
  576. let secret = Secret::generate();
  577. // Create two identical proofs
  578. let proof1 = Proof {
  579. amount: Amount::from(10),
  580. keyset_id,
  581. secret: secret.clone(),
  582. c: "02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea"
  583. .parse()
  584. .unwrap(),
  585. witness: None,
  586. dleq: None,
  587. };
  588. let proof2 = proof1.clone(); // Duplicate proof
  589. // Create a token with the duplicate proofs
  590. let proofs = vec![proof1.clone(), proof2].into_iter().collect();
  591. let token = Token::new(mint_url.clone(), proofs, None, CurrencyUnit::Sat);
  592. // Verify that value() returns an error
  593. let result = token.value();
  594. assert!(result.is_err());
  595. // Create a token with unique proofs
  596. let proof3 = Proof {
  597. amount: Amount::from(10),
  598. keyset_id,
  599. secret: Secret::generate(),
  600. c: "03bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea"
  601. .parse()
  602. .unwrap(), // Different C value
  603. witness: None,
  604. dleq: None,
  605. };
  606. let proofs = vec![proof1, proof3].into_iter().collect();
  607. let token = Token::new(mint_url, proofs, None, CurrencyUnit::Sat);
  608. // Verify that value() succeeds with unique proofs
  609. let result = token.value();
  610. assert!(result.is_ok());
  611. assert_eq!(result.unwrap(), Amount::from(20));
  612. }
  613. #[test]
  614. fn test_token_from_proofs_with_idv2_round_trip() {
  615. let mint_url = MintUrl::from_str("https://example.com").unwrap();
  616. let keysets_info: Vec<KeySetInfo> = (0..10)
  617. .map(|_| {
  618. let mut bytes: [u8; 33] = [0u8; 33];
  619. bytes[0] = 1u8;
  620. rand::thread_rng().fill_bytes(&mut bytes[1..]);
  621. let id = Id::from_bytes(&bytes).unwrap();
  622. KeySetInfo {
  623. id,
  624. unit: CurrencyUnit::Sat,
  625. active: true,
  626. input_fee_ppk: 0,
  627. final_expiry: None,
  628. }
  629. })
  630. .collect();
  631. let chosen_keyset_id = keysets_info[0].id;
  632. // Make up a bunch of fake proofs
  633. let proofs = (0..5)
  634. .map(|_| {
  635. let mut c_preimage: [u8; 33] = [0u8; 33];
  636. c_preimage[0] = 1u8;
  637. rand::thread_rng().fill_bytes(&mut c_preimage[1..]);
  638. Proof::new(
  639. Amount::from(1),
  640. chosen_keyset_id,
  641. Secret::generate(),
  642. hash_to_curve(&c_preimage).unwrap(),
  643. )
  644. })
  645. .collect();
  646. let token = Token::new(mint_url.clone(), proofs, None, CurrencyUnit::Sat);
  647. let token_str = token.to_string();
  648. let token1 = Token::from_str(&token_str);
  649. assert!(token1.is_ok());
  650. let proofs1 = token1.unwrap().proofs(&keysets_info);
  651. assert!(proofs1.is_ok());
  652. //println!("{:?}", proofs1);
  653. }
  654. #[test]
  655. fn test_token_proofs_with_unknown_short_keyset_id() {
  656. let mint_url = MintUrl::from_str("https://example.com").unwrap();
  657. let keysets_info: Vec<KeySetInfo> = (0..10)
  658. .map(|_| {
  659. let mut bytes: [u8; 33] = [0u8; 33];
  660. bytes[0] = 1u8;
  661. rand::thread_rng().fill_bytes(&mut bytes[1..]);
  662. let id = Id::from_bytes(&bytes).unwrap();
  663. KeySetInfo {
  664. id,
  665. unit: CurrencyUnit::Sat,
  666. active: true,
  667. input_fee_ppk: 0,
  668. final_expiry: None,
  669. }
  670. })
  671. .collect();
  672. let chosen_keyset_id =
  673. Id::from_str("01c352c0b47d42edb764bddf8c53d77b85f057157d92084d9d05e876251ecd8422")
  674. .unwrap();
  675. // Make up a bunch of fake proofs
  676. let proofs = (0..5)
  677. .map(|_| {
  678. let mut c_preimage: [u8; 33] = [0u8; 33];
  679. c_preimage[0] = 1u8;
  680. rand::thread_rng().fill_bytes(&mut c_preimage[1..]);
  681. Proof::new(
  682. Amount::from(1),
  683. chosen_keyset_id,
  684. Secret::generate(),
  685. hash_to_curve(&c_preimage).unwrap(),
  686. )
  687. })
  688. .collect();
  689. let token = Token::new(mint_url.clone(), proofs, None, CurrencyUnit::Sat);
  690. let token_str = token.to_string();
  691. let token1 = Token::from_str(&token_str);
  692. assert!(token1.is_ok());
  693. let proofs1 = token1.unwrap().proofs(&keysets_info);
  694. assert!(proofs1.is_err());
  695. }
  696. }