token.rs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687
  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, ProofV4, Proofs};
  11. use crate::mint_url::MintUrl;
  12. use crate::nuts::nut00::ProofsMethods;
  13. use crate::nuts::{CurrencyUnit, Id};
  14. use crate::{ensure_cdk, Amount};
  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) -> Proofs {
  61. match self {
  62. Self::TokenV3(token) => token.proofs(),
  63. Self::TokenV4(token) => token.proofs(),
  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. /// [`Proofs`]
  158. pub proofs: Proofs,
  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,
  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) -> Proofs {
  198. self.token
  199. .iter()
  200. .flat_map(|token| token.proofs.clone())
  201. .collect()
  202. }
  203. /// Value - errors if duplicate proofs are found
  204. #[inline]
  205. pub fn value(&self) -> Result<Amount, Error> {
  206. let proofs = self.proofs();
  207. let unique_count = proofs
  208. .iter()
  209. .collect::<std::collections::HashSet<_>>()
  210. .len();
  211. // Check if there are any duplicate proofs
  212. if unique_count != proofs.len() {
  213. return Err(Error::DuplicateProofs);
  214. }
  215. proofs.total_amount()
  216. }
  217. /// Memo
  218. #[inline]
  219. pub fn memo(&self) -> &Option<String> {
  220. &self.memo
  221. }
  222. /// Unit
  223. #[inline]
  224. pub fn unit(&self) -> &Option<CurrencyUnit> {
  225. &self.unit
  226. }
  227. /// Mint Url
  228. pub fn mint_urls(&self) -> Vec<MintUrl> {
  229. let mut mint_urls = Vec::new();
  230. for token in self.token.iter() {
  231. mint_urls.push(token.mint.clone());
  232. }
  233. mint_urls
  234. }
  235. /// Checks if a token has multiple mints
  236. ///
  237. /// These tokens are not supported by this crate
  238. pub fn is_multi_mint(&self) -> bool {
  239. self.token.len() > 1
  240. }
  241. }
  242. impl FromStr for TokenV3 {
  243. type Err = Error;
  244. fn from_str(s: &str) -> Result<Self, Self::Err> {
  245. let s = s.strip_prefix("cashuA").ok_or(Error::UnsupportedToken)?;
  246. let decode_config = general_purpose::GeneralPurposeConfig::new()
  247. .with_decode_padding_mode(bitcoin::base64::engine::DecodePaddingMode::Indifferent);
  248. let decoded = GeneralPurpose::new(&alphabet::URL_SAFE, decode_config).decode(s)?;
  249. let decoded_str = String::from_utf8(decoded)?;
  250. let token: TokenV3 = serde_json::from_str(&decoded_str)?;
  251. Ok(token)
  252. }
  253. }
  254. impl fmt::Display for TokenV3 {
  255. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  256. let json_string = serde_json::to_string(self).map_err(|_| fmt::Error)?;
  257. let encoded = general_purpose::URL_SAFE.encode(json_string);
  258. write!(f, "cashuA{}", encoded)
  259. }
  260. }
  261. impl From<TokenV4> for TokenV3 {
  262. fn from(token: TokenV4) -> Self {
  263. let proofs = token.proofs();
  264. TokenV3 {
  265. token: vec![TokenV3Token::new(token.mint_url, proofs)],
  266. memo: token.memo,
  267. unit: Some(token.unit),
  268. }
  269. }
  270. }
  271. /// Token V4
  272. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  273. pub struct TokenV4 {
  274. /// Mint Url
  275. #[serde(rename = "m")]
  276. pub mint_url: MintUrl,
  277. /// Token Unit
  278. #[serde(rename = "u")]
  279. pub unit: CurrencyUnit,
  280. /// Memo for token
  281. #[serde(rename = "d", skip_serializing_if = "Option::is_none")]
  282. pub memo: Option<String>,
  283. /// Proofs grouped by keyset_id
  284. #[serde(rename = "t")]
  285. pub token: Vec<TokenV4Token>,
  286. }
  287. impl TokenV4 {
  288. /// Proofs from token
  289. pub fn proofs(&self) -> Proofs {
  290. self.token
  291. .iter()
  292. .flat_map(|token| token.proofs.iter().map(|p| p.into_proof(&token.keyset_id)))
  293. .collect()
  294. }
  295. /// Value - errors if duplicate proofs are found
  296. #[inline]
  297. pub fn value(&self) -> Result<Amount, Error> {
  298. let proofs = self.proofs();
  299. let unique_count = proofs
  300. .iter()
  301. .collect::<std::collections::HashSet<_>>()
  302. .len();
  303. // Check if there are any duplicate proofs
  304. if unique_count != proofs.len() {
  305. return Err(Error::DuplicateProofs);
  306. }
  307. proofs.total_amount()
  308. }
  309. /// Memo
  310. #[inline]
  311. pub fn memo(&self) -> &Option<String> {
  312. &self.memo
  313. }
  314. /// Unit
  315. #[inline]
  316. pub fn unit(&self) -> &CurrencyUnit {
  317. &self.unit
  318. }
  319. /// Serialize the token to raw binary
  320. pub fn to_raw_bytes(&self) -> Result<Vec<u8>, Error> {
  321. let mut prefix = b"crawB".to_vec();
  322. let mut data = Vec::new();
  323. ciborium::into_writer(self, &mut data).map_err(Error::CiboriumSerError)?;
  324. prefix.extend(data);
  325. Ok(prefix)
  326. }
  327. }
  328. impl fmt::Display for TokenV4 {
  329. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  330. use serde::ser::Error;
  331. let mut data = Vec::new();
  332. ciborium::into_writer(self, &mut data).map_err(|e| fmt::Error::custom(e.to_string()))?;
  333. let encoded = general_purpose::URL_SAFE.encode(data);
  334. write!(f, "cashuB{}", encoded)
  335. }
  336. }
  337. impl FromStr for TokenV4 {
  338. type Err = Error;
  339. fn from_str(s: &str) -> Result<Self, Self::Err> {
  340. let s = s.strip_prefix("cashuB").ok_or(Error::UnsupportedToken)?;
  341. let decode_config = general_purpose::GeneralPurposeConfig::new()
  342. .with_decode_padding_mode(bitcoin::base64::engine::DecodePaddingMode::Indifferent);
  343. let decoded = GeneralPurpose::new(&alphabet::URL_SAFE, decode_config).decode(s)?;
  344. let token: TokenV4 = ciborium::from_reader(&decoded[..])?;
  345. Ok(token)
  346. }
  347. }
  348. impl TryFrom<&Vec<u8>> for TokenV4 {
  349. type Error = Error;
  350. fn try_from(bytes: &Vec<u8>) -> Result<Self, Self::Error> {
  351. ensure_cdk!(bytes.len() >= 5, Error::UnsupportedToken);
  352. let prefix = String::from_utf8(bytes[..5].to_vec())?;
  353. ensure_cdk!(prefix.as_str() == "crawB", Error::UnsupportedToken);
  354. Ok(ciborium::from_reader(&bytes[5..])?)
  355. }
  356. }
  357. impl TryFrom<TokenV3> for TokenV4 {
  358. type Error = Error;
  359. fn try_from(token: TokenV3) -> Result<Self, Self::Error> {
  360. let proofs = token.proofs();
  361. let mint_urls = token.mint_urls();
  362. ensure_cdk!(mint_urls.len() == 1, Error::UnsupportedToken);
  363. let mint_url = mint_urls.first().ok_or(Error::UnsupportedToken)?;
  364. let proofs = proofs
  365. .iter()
  366. .fold(HashMap::new(), |mut acc, val| {
  367. acc.entry(val.keyset_id)
  368. .and_modify(|p: &mut Vec<Proof>| p.push(val.clone()))
  369. .or_insert(vec![val.clone()]);
  370. acc
  371. })
  372. .into_iter()
  373. .map(|(id, proofs)| TokenV4Token::new(id, proofs))
  374. .collect();
  375. Ok(TokenV4 {
  376. mint_url: mint_url.clone(),
  377. token: proofs,
  378. memo: token.memo,
  379. unit: token.unit.ok_or(Error::UnsupportedUnit)?,
  380. })
  381. }
  382. }
  383. /// Token V4 Token
  384. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  385. pub struct TokenV4Token {
  386. /// `Keyset id`
  387. #[serde(
  388. rename = "i",
  389. serialize_with = "serialize_v4_keyset_id",
  390. deserialize_with = "deserialize_v4_keyset_id"
  391. )]
  392. pub keyset_id: Id,
  393. /// Proofs
  394. #[serde(rename = "p")]
  395. pub proofs: Vec<ProofV4>,
  396. }
  397. fn serialize_v4_keyset_id<S>(keyset_id: &Id, serializer: S) -> Result<S::Ok, S::Error>
  398. where
  399. S: serde::Serializer,
  400. {
  401. serializer.serialize_bytes(&keyset_id.to_bytes())
  402. }
  403. fn deserialize_v4_keyset_id<'de, D>(deserializer: D) -> Result<Id, D::Error>
  404. where
  405. D: serde::Deserializer<'de>,
  406. {
  407. let bytes = Vec::<u8>::deserialize(deserializer)?;
  408. Id::from_bytes(&bytes).map_err(serde::de::Error::custom)
  409. }
  410. impl TokenV4Token {
  411. /// Create new [`TokenV4Token`]
  412. pub fn new(keyset_id: Id, proofs: Proofs) -> Self {
  413. Self {
  414. keyset_id,
  415. proofs: proofs.into_iter().map(|p| p.into()).collect(),
  416. }
  417. }
  418. }
  419. #[cfg(test)]
  420. mod tests {
  421. use std::str::FromStr;
  422. use super::*;
  423. use crate::mint_url::MintUrl;
  424. use crate::secret::Secret;
  425. use crate::util::hex;
  426. #[test]
  427. fn test_token_padding() {
  428. let token_str_with_padding = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91IHZlcnkgbXVjaC4ifQ==";
  429. let token = TokenV3::from_str(token_str_with_padding).unwrap();
  430. let token_str_without_padding = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91IHZlcnkgbXVjaC4ifQ";
  431. let token_without = TokenV3::from_str(token_str_without_padding).unwrap();
  432. assert_eq!(token, token_without);
  433. }
  434. #[test]
  435. fn test_token_v4_str_round_trip() {
  436. let token_str = "cashuBpGF0gaJhaUgArSaMTR9YJmFwgaNhYQFhc3hAOWE2ZGJiODQ3YmQyMzJiYTc2ZGIwZGYxOTcyMTZiMjlkM2I4Y2MxNDU1M2NkMjc4MjdmYzFjYzk0MmZlZGI0ZWFjWCEDhhhUP_trhpXfStS6vN6So0qWvc2X3O4NfM-Y1HISZ5JhZGlUaGFuayB5b3VhbXVodHRwOi8vbG9jYWxob3N0OjMzMzhhdWNzYXQ=";
  437. let token = TokenV4::from_str(token_str).unwrap();
  438. assert_eq!(
  439. token.mint_url,
  440. MintUrl::from_str("http://localhost:3338").unwrap()
  441. );
  442. assert_eq!(
  443. token.token[0].keyset_id,
  444. Id::from_str("00ad268c4d1f5826").unwrap()
  445. );
  446. let encoded = &token.to_string();
  447. let token_data = TokenV4::from_str(encoded).unwrap();
  448. assert_eq!(token_data, token);
  449. }
  450. #[test]
  451. fn test_token_v4_multi_keyset() {
  452. let token_str_multi_keysets = "cashuBo2F0gqJhaUgA_9SLj17PgGFwgaNhYQFhc3hAYWNjMTI0MzVlN2I4NDg0YzNjZjE4NTAxNDkyMThhZjkwZjcxNmE1MmJmNGE1ZWQzNDdlNDhlY2MxM2Y3NzM4OGFjWCECRFODGd5IXVW-07KaZCvuWHk3WrnnpiDhHki6SCQh88-iYWlIAK0mjE0fWCZhcIKjYWECYXN4QDEzMjNkM2Q0NzA3YTU4YWQyZTIzYWRhNGU5ZjFmNDlmNWE1YjRhYzdiNzA4ZWIwZDYxZjczOGY0ODMwN2U4ZWVhY1ghAjRWqhENhLSsdHrr2Cw7AFrKUL9Ffr1XN6RBT6w659lNo2FhAWFzeEA1NmJjYmNiYjdjYzY0MDZiM2ZhNWQ1N2QyMTc0ZjRlZmY4YjQ0MDJiMTc2OTI2ZDNhNTdkM2MzZGNiYjU5ZDU3YWNYIQJzEpxXGeWZN5qXSmJjY8MzxWyvwObQGr5G1YCCgHicY2FtdWh0dHA6Ly9sb2NhbGhvc3Q6MzMzOGF1Y3NhdA==";
  453. let token = Token::from_str(token_str_multi_keysets).unwrap();
  454. let amount = token.value().expect("valid amount");
  455. assert_eq!(amount, Amount::from(4));
  456. let unit = token.unit().unwrap();
  457. assert_eq!(CurrencyUnit::Sat, unit);
  458. match token {
  459. Token::TokenV4(token) => {
  460. let tokens: Vec<Id> = token.token.iter().map(|t| t.keyset_id).collect();
  461. assert_eq!(tokens.len(), 2);
  462. assert!(tokens.contains(&Id::from_str("00ffd48b8f5ecf80").unwrap()));
  463. assert!(tokens.contains(&Id::from_str("00ad268c4d1f5826").unwrap()));
  464. let mint_url = token.mint_url;
  465. assert_eq!("http://localhost:3338", &mint_url.to_string());
  466. }
  467. _ => {
  468. panic!("Token should be a v4 token")
  469. }
  470. }
  471. }
  472. #[test]
  473. fn test_tokenv4_from_tokenv3() {
  474. let token_v3_str = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
  475. let token_v3 =
  476. TokenV3::from_str(token_v3_str).expect("TokenV3 should be created from string");
  477. let token_v4 = TokenV4::try_from(token_v3).expect("TokenV3 should be converted to TokenV4");
  478. let token_v4_expected = "cashuBpGFtd2h0dHBzOi8vODMzMy5zcGFjZTozMzM4YXVjc2F0YWRqVGhhbmsgeW91LmF0gaJhaUgAmh8pMlPkHmFwgqRhYQJhc3hANDA3OTE1YmMyMTJiZTYxYTc3ZTNlNmQyYWViNGM3Mjc5ODBiZGE1MWNkMDZhNmFmYzI5ZTI4NjE3NjhhNzgzN2FjWCECvJCXmX2Br7LMc0a15DRak0a9KlBut5WFmKcvDPhRY-phZPakYWEIYXN4QGZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmVhY1ghAp6OUFC4kKfWwJaNsWvB1dX6BA6h3ihPbsadYSmfZxBZYWT2";
  479. assert_eq!(token_v4.to_string(), token_v4_expected);
  480. }
  481. #[test]
  482. fn test_token_str_round_trip() {
  483. let token_str = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
  484. let token = TokenV3::from_str(token_str).unwrap();
  485. assert_eq!(
  486. token.token[0].mint,
  487. MintUrl::from_str("https://8333.space:3338").unwrap()
  488. );
  489. assert_eq!(
  490. token.token[0].proofs[0].clone().keyset_id,
  491. Id::from_str("009a1f293253e41e").unwrap()
  492. );
  493. assert_eq!(token.unit.clone().unwrap(), CurrencyUnit::Sat);
  494. let encoded = &token.to_string();
  495. let token_data = TokenV3::from_str(encoded).unwrap();
  496. assert_eq!(token_data, token);
  497. }
  498. #[test]
  499. fn incorrect_tokens() {
  500. let incorrect_prefix = "casshuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
  501. let incorrect_prefix_token = TokenV3::from_str(incorrect_prefix);
  502. assert!(incorrect_prefix_token.is_err());
  503. let no_prefix = "eyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
  504. let no_prefix_token = TokenV3::from_str(no_prefix);
  505. assert!(no_prefix_token.is_err());
  506. let correct_token = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
  507. let correct_token = TokenV3::from_str(correct_token);
  508. assert!(correct_token.is_ok());
  509. }
  510. #[test]
  511. fn test_token_v4_raw_roundtrip() {
  512. let token_raw = hex::decode("6372617742a4617481a261694800ad268c4d1f5826617081a3616101617378403961366462623834376264323332626137366462306466313937323136623239643362386363313435353363643237383237666331636339343266656462346561635821038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d4721267926164695468616e6b20796f75616d75687474703a2f2f6c6f63616c686f73743a33333338617563736174").unwrap();
  513. let token = TokenV4::try_from(&token_raw).expect("Token deserialization error");
  514. let token_raw_ = token.to_raw_bytes().expect("Token serialization error");
  515. let token_ = TokenV4::try_from(&token_raw_).expect("Token deserialization error");
  516. assert!(token_ == token)
  517. }
  518. #[test]
  519. fn test_token_generic_raw_roundtrip() {
  520. let tokenv4_raw = hex::decode("6372617742a4617481a261694800ad268c4d1f5826617081a3616101617378403961366462623834376264323332626137366462306466313937323136623239643362386363313435353363643237383237666331636339343266656462346561635821038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d4721267926164695468616e6b20796f75616d75687474703a2f2f6c6f63616c686f73743a33333338617563736174").unwrap();
  521. let tokenv4 = Token::try_from(&tokenv4_raw).expect("Token deserialization error");
  522. let tokenv4_ = TokenV4::try_from(&tokenv4_raw).expect("Token deserialization error");
  523. let tokenv4_bytes = tokenv4.to_raw_bytes().expect("Serialization error");
  524. let tokenv4_bytes_ = tokenv4_.to_raw_bytes().expect("Serialization error");
  525. assert!(tokenv4_bytes_ == tokenv4_bytes);
  526. }
  527. #[test]
  528. fn test_token_with_duplicate_proofs() {
  529. // Create a token with duplicate proofs
  530. let mint_url = MintUrl::from_str("https://example.com").unwrap();
  531. let keyset_id = Id::from_str("009a1f293253e41e").unwrap();
  532. let secret = Secret::generate();
  533. // Create two identical proofs
  534. let proof1 = Proof {
  535. amount: Amount::from(10),
  536. keyset_id,
  537. secret: secret.clone(),
  538. c: "02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea"
  539. .parse()
  540. .unwrap(),
  541. witness: None,
  542. dleq: None,
  543. };
  544. let proof2 = proof1.clone(); // Duplicate proof
  545. // Create a token with the duplicate proofs
  546. let proofs = vec![proof1.clone(), proof2].into_iter().collect();
  547. let token = Token::new(mint_url.clone(), proofs, None, CurrencyUnit::Sat);
  548. // Verify that value() returns an error
  549. let result = token.value();
  550. assert!(result.is_err());
  551. // Create a token with unique proofs
  552. let proof3 = Proof {
  553. amount: Amount::from(10),
  554. keyset_id,
  555. secret: Secret::generate(),
  556. c: "03bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea"
  557. .parse()
  558. .unwrap(), // Different C value
  559. witness: None,
  560. dleq: None,
  561. };
  562. let proofs = vec![proof1, proof3].into_iter().collect();
  563. let token = Token::new(mint_url, proofs, None, CurrencyUnit::Sat);
  564. // Verify that value() succeeds with unique proofs
  565. let result = token.value();
  566. assert!(result.is_ok());
  567. assert_eq!(result.unwrap(), Amount::from(20));
  568. }
  569. }