token.rs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631
  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::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. if mint_urls.len() != 1 {
  93. return Err(Error::UnsupportedToken);
  94. }
  95. Ok(mint_urls.first().expect("Length is checked above").clone())
  96. }
  97. Self::TokenV4(token) => Ok(token.mint_url.clone()),
  98. }
  99. }
  100. /// To v3 string
  101. pub fn to_v3_string(&self) -> String {
  102. let v3_token = match self {
  103. Self::TokenV3(token) => token.clone(),
  104. Self::TokenV4(token) => token.clone().into(),
  105. };
  106. v3_token.to_string()
  107. }
  108. /// Serialize the token to raw binary
  109. pub fn to_raw_bytes(&self) -> Result<Vec<u8>, Error> {
  110. match self {
  111. Self::TokenV3(_) => Err(Error::UnsupportedToken),
  112. Self::TokenV4(token) => token.to_raw_bytes(),
  113. }
  114. }
  115. }
  116. impl FromStr for Token {
  117. type Err = Error;
  118. fn from_str(s: &str) -> Result<Self, Self::Err> {
  119. let (is_v3, s) = match (s.strip_prefix("cashuA"), s.strip_prefix("cashuB")) {
  120. (Some(s), None) => (true, s),
  121. (None, Some(s)) => (false, s),
  122. _ => return Err(Error::UnsupportedToken),
  123. };
  124. let decode_config = general_purpose::GeneralPurposeConfig::new()
  125. .with_decode_padding_mode(bitcoin::base64::engine::DecodePaddingMode::Indifferent);
  126. let decoded = GeneralPurpose::new(&alphabet::URL_SAFE, decode_config).decode(s)?;
  127. match is_v3 {
  128. true => {
  129. let decoded_str = String::from_utf8(decoded)?;
  130. let token: TokenV3 = serde_json::from_str(&decoded_str)?;
  131. Ok(Token::TokenV3(token))
  132. }
  133. false => {
  134. let token: TokenV4 = ciborium::from_reader(&decoded[..])?;
  135. Ok(Token::TokenV4(token))
  136. }
  137. }
  138. }
  139. }
  140. impl TryFrom<&Vec<u8>> for Token {
  141. type Error = Error;
  142. fn try_from(bytes: &Vec<u8>) -> Result<Self, Self::Error> {
  143. if bytes.len() < 5 {
  144. return Err(Error::UnsupportedToken);
  145. }
  146. let prefix = String::from_utf8(bytes[..5].to_vec())?;
  147. match prefix.as_str() {
  148. "crawB" => {
  149. let token: TokenV4 = ciborium::from_reader(&bytes[5..])?;
  150. Ok(Token::TokenV4(token))
  151. }
  152. _ => Err(Error::UnsupportedToken),
  153. }
  154. }
  155. }
  156. /// Token V3 Token
  157. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  158. pub struct TokenV3Token {
  159. /// Url of mint
  160. pub mint: MintUrl,
  161. /// [`Proofs`]
  162. pub proofs: Proofs,
  163. }
  164. impl TokenV3Token {
  165. /// Create new [`TokenV3Token`]
  166. pub fn new(mint_url: MintUrl, proofs: Proofs) -> Self {
  167. Self {
  168. mint: mint_url,
  169. proofs,
  170. }
  171. }
  172. }
  173. /// Token
  174. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  175. pub struct TokenV3 {
  176. /// Proofs in [`Token`] by mint
  177. pub token: Vec<TokenV3Token>,
  178. /// Memo for token
  179. #[serde(skip_serializing_if = "Option::is_none")]
  180. pub memo: Option<String>,
  181. /// Token Unit
  182. #[serde(skip_serializing_if = "Option::is_none")]
  183. pub unit: Option<CurrencyUnit>,
  184. }
  185. impl TokenV3 {
  186. /// Create new [`Token`]
  187. pub fn new(
  188. mint_url: MintUrl,
  189. proofs: Proofs,
  190. memo: Option<String>,
  191. unit: Option<CurrencyUnit>,
  192. ) -> Result<Self, Error> {
  193. if proofs.is_empty() {
  194. return Err(Error::ProofsRequired);
  195. }
  196. Ok(Self {
  197. token: vec![TokenV3Token::new(mint_url, proofs)],
  198. memo,
  199. unit,
  200. })
  201. }
  202. /// Proofs
  203. pub fn proofs(&self) -> Proofs {
  204. self.token
  205. .iter()
  206. .flat_map(|token| token.proofs.clone())
  207. .collect()
  208. }
  209. /// Value
  210. #[inline]
  211. pub fn value(&self) -> Result<Amount, Error> {
  212. Ok(Amount::try_sum(
  213. self.token
  214. .iter()
  215. .map(|t| t.proofs.total_amount())
  216. .collect::<Result<Vec<Amount>, _>>()?,
  217. )?)
  218. }
  219. /// Memo
  220. #[inline]
  221. pub fn memo(&self) -> &Option<String> {
  222. &self.memo
  223. }
  224. /// Unit
  225. #[inline]
  226. pub fn unit(&self) -> &Option<CurrencyUnit> {
  227. &self.unit
  228. }
  229. /// Mint Url
  230. pub fn mint_urls(&self) -> Vec<MintUrl> {
  231. let mut mint_urls = Vec::new();
  232. for token in self.token.iter() {
  233. mint_urls.push(token.mint.clone());
  234. }
  235. mint_urls
  236. }
  237. }
  238. impl FromStr for TokenV3 {
  239. type Err = Error;
  240. fn from_str(s: &str) -> Result<Self, Self::Err> {
  241. let s = s.strip_prefix("cashuA").ok_or(Error::UnsupportedToken)?;
  242. let decode_config = general_purpose::GeneralPurposeConfig::new()
  243. .with_decode_padding_mode(bitcoin::base64::engine::DecodePaddingMode::Indifferent);
  244. let decoded = GeneralPurpose::new(&alphabet::URL_SAFE, decode_config).decode(s)?;
  245. let decoded_str = String::from_utf8(decoded)?;
  246. let token: TokenV3 = serde_json::from_str(&decoded_str)?;
  247. Ok(token)
  248. }
  249. }
  250. impl fmt::Display for TokenV3 {
  251. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  252. let json_string = serde_json::to_string(self).map_err(|_| fmt::Error)?;
  253. let encoded = general_purpose::URL_SAFE.encode(json_string);
  254. write!(f, "cashuA{}", encoded)
  255. }
  256. }
  257. impl From<TokenV4> for TokenV3 {
  258. fn from(token: TokenV4) -> Self {
  259. let proofs = token.proofs();
  260. TokenV3 {
  261. token: vec![TokenV3Token::new(token.mint_url, proofs)],
  262. memo: token.memo,
  263. unit: Some(token.unit),
  264. }
  265. }
  266. }
  267. /// Token V4
  268. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  269. pub struct TokenV4 {
  270. /// Mint Url
  271. #[serde(rename = "m")]
  272. pub mint_url: MintUrl,
  273. /// Token Unit
  274. #[serde(rename = "u")]
  275. pub unit: CurrencyUnit,
  276. /// Memo for token
  277. #[serde(rename = "d", skip_serializing_if = "Option::is_none")]
  278. pub memo: Option<String>,
  279. /// Proofs grouped by keyset_id
  280. #[serde(rename = "t")]
  281. pub token: Vec<TokenV4Token>,
  282. }
  283. impl TokenV4 {
  284. /// Proofs from token
  285. pub fn proofs(&self) -> Proofs {
  286. self.token
  287. .iter()
  288. .flat_map(|token| token.proofs.iter().map(|p| p.into_proof(&token.keyset_id)))
  289. .collect()
  290. }
  291. /// Value
  292. #[inline]
  293. pub fn value(&self) -> Result<Amount, Error> {
  294. Ok(Amount::try_sum(
  295. self.token
  296. .iter()
  297. .map(|t| Amount::try_sum(t.proofs.iter().map(|p| p.amount)))
  298. .collect::<Result<Vec<Amount>, _>>()?,
  299. )?)
  300. }
  301. /// Memo
  302. #[inline]
  303. pub fn memo(&self) -> &Option<String> {
  304. &self.memo
  305. }
  306. /// Unit
  307. #[inline]
  308. pub fn unit(&self) -> &CurrencyUnit {
  309. &self.unit
  310. }
  311. /// Serialize the token to raw binary
  312. pub fn to_raw_bytes(&self) -> Result<Vec<u8>, Error> {
  313. let mut prefix = b"crawB".to_vec();
  314. let mut data = Vec::new();
  315. ciborium::into_writer(self, &mut data).map_err(Error::CiboriumSerError)?;
  316. prefix.extend(data);
  317. Ok(prefix)
  318. }
  319. }
  320. impl fmt::Display for TokenV4 {
  321. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  322. use serde::ser::Error;
  323. let mut data = Vec::new();
  324. ciborium::into_writer(self, &mut data).map_err(|e| fmt::Error::custom(e.to_string()))?;
  325. let encoded = general_purpose::URL_SAFE.encode(data);
  326. write!(f, "cashuB{}", encoded)
  327. }
  328. }
  329. impl FromStr for TokenV4 {
  330. type Err = Error;
  331. fn from_str(s: &str) -> Result<Self, Self::Err> {
  332. let s = s.strip_prefix("cashuB").ok_or(Error::UnsupportedToken)?;
  333. let decode_config = general_purpose::GeneralPurposeConfig::new()
  334. .with_decode_padding_mode(bitcoin::base64::engine::DecodePaddingMode::Indifferent);
  335. let decoded = GeneralPurpose::new(&alphabet::URL_SAFE, decode_config).decode(s)?;
  336. let token: TokenV4 = ciborium::from_reader(&decoded[..])?;
  337. Ok(token)
  338. }
  339. }
  340. impl TryFrom<&Vec<u8>> for TokenV4 {
  341. type Error = Error;
  342. fn try_from(bytes: &Vec<u8>) -> Result<Self, Self::Error> {
  343. if bytes.len() < 5 {
  344. return Err(Error::UnsupportedToken);
  345. }
  346. let prefix = String::from_utf8(bytes[..5].to_vec())?;
  347. if prefix.as_str() == "crawB" {
  348. let token: TokenV4 = ciborium::from_reader(&bytes[5..])?;
  349. Ok(token)
  350. } else {
  351. Err(Error::UnsupportedToken)
  352. }
  353. }
  354. }
  355. impl TryFrom<TokenV3> for TokenV4 {
  356. type Error = Error;
  357. fn try_from(token: TokenV3) -> Result<Self, Self::Error> {
  358. let proofs = token.proofs();
  359. let mint_urls = token.mint_urls();
  360. if mint_urls.len() != 1 {
  361. return Err(Error::UnsupportedToken);
  362. }
  363. let mint_url = mint_urls.first().expect("Len is checked");
  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::util::hex;
  425. #[test]
  426. fn test_token_padding() {
  427. let token_str_with_padding = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91IHZlcnkgbXVjaC4ifQ==";
  428. let token = TokenV3::from_str(token_str_with_padding).unwrap();
  429. let token_str_without_padding = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91IHZlcnkgbXVjaC4ifQ";
  430. let token_without = TokenV3::from_str(token_str_without_padding).unwrap();
  431. assert_eq!(token, token_without);
  432. }
  433. #[test]
  434. fn test_token_v4_str_round_trip() {
  435. let token_str = "cashuBpGF0gaJhaUgArSaMTR9YJmFwgaNhYQFhc3hAOWE2ZGJiODQ3YmQyMzJiYTc2ZGIwZGYxOTcyMTZiMjlkM2I4Y2MxNDU1M2NkMjc4MjdmYzFjYzk0MmZlZGI0ZWFjWCEDhhhUP_trhpXfStS6vN6So0qWvc2X3O4NfM-Y1HISZ5JhZGlUaGFuayB5b3VhbXVodHRwOi8vbG9jYWxob3N0OjMzMzhhdWNzYXQ=";
  436. let token = TokenV4::from_str(token_str).unwrap();
  437. assert_eq!(
  438. token.mint_url,
  439. MintUrl::from_str("http://localhost:3338").unwrap()
  440. );
  441. assert_eq!(
  442. token.token[0].keyset_id,
  443. Id::from_str("00ad268c4d1f5826").unwrap()
  444. );
  445. let encoded = &token.to_string();
  446. let token_data = TokenV4::from_str(encoded).unwrap();
  447. assert_eq!(token_data, token);
  448. }
  449. #[test]
  450. fn test_token_v4_multi_keyset() {
  451. let token_str_multi_keysets = "cashuBo2F0gqJhaUgA_9SLj17PgGFwgaNhYQFhc3hAYWNjMTI0MzVlN2I4NDg0YzNjZjE4NTAxNDkyMThhZjkwZjcxNmE1MmJmNGE1ZWQzNDdlNDhlY2MxM2Y3NzM4OGFjWCECRFODGd5IXVW-07KaZCvuWHk3WrnnpiDhHki6SCQh88-iYWlIAK0mjE0fWCZhcIKjYWECYXN4QDEzMjNkM2Q0NzA3YTU4YWQyZTIzYWRhNGU5ZjFmNDlmNWE1YjRhYzdiNzA4ZWIwZDYxZjczOGY0ODMwN2U4ZWVhY1ghAjRWqhENhLSsdHrr2Cw7AFrKUL9Ffr1XN6RBT6w659lNo2FhAWFzeEA1NmJjYmNiYjdjYzY0MDZiM2ZhNWQ1N2QyMTc0ZjRlZmY4YjQ0MDJiMTc2OTI2ZDNhNTdkM2MzZGNiYjU5ZDU3YWNYIQJzEpxXGeWZN5qXSmJjY8MzxWyvwObQGr5G1YCCgHicY2FtdWh0dHA6Ly9sb2NhbGhvc3Q6MzMzOGF1Y3NhdA==";
  452. let token = Token::from_str(token_str_multi_keysets).unwrap();
  453. let amount = token.value().expect("valid amount");
  454. assert_eq!(amount, Amount::from(4));
  455. let unit = token.unit().unwrap();
  456. assert_eq!(CurrencyUnit::Sat, unit);
  457. match token {
  458. Token::TokenV4(token) => {
  459. let tokens: Vec<Id> = token.token.iter().map(|t| t.keyset_id).collect();
  460. assert_eq!(tokens.len(), 2);
  461. assert!(tokens.contains(&Id::from_str("00ffd48b8f5ecf80").unwrap()));
  462. assert!(tokens.contains(&Id::from_str("00ad268c4d1f5826").unwrap()));
  463. let mint_url = token.mint_url;
  464. assert_eq!("http://localhost:3338", &mint_url.to_string());
  465. }
  466. _ => {
  467. panic!("Token should be a v4 token")
  468. }
  469. }
  470. }
  471. #[test]
  472. fn test_tokenv4_from_tokenv3() {
  473. let token_v3_str = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
  474. let token_v3 =
  475. TokenV3::from_str(token_v3_str).expect("TokenV3 should be created from string");
  476. let token_v4 = TokenV4::try_from(token_v3).expect("TokenV3 should be converted to TokenV4");
  477. let token_v4_expected = "cashuBpGFtd2h0dHBzOi8vODMzMy5zcGFjZTozMzM4YXVjc2F0YWRqVGhhbmsgeW91LmF0gaJhaUgAmh8pMlPkHmFwgqRhYQJhc3hANDA3OTE1YmMyMTJiZTYxYTc3ZTNlNmQyYWViNGM3Mjc5ODBiZGE1MWNkMDZhNmFmYzI5ZTI4NjE3NjhhNzgzN2FjWCECvJCXmX2Br7LMc0a15DRak0a9KlBut5WFmKcvDPhRY-phZPakYWEIYXN4QGZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmVhY1ghAp6OUFC4kKfWwJaNsWvB1dX6BA6h3ihPbsadYSmfZxBZYWT2";
  478. assert_eq!(token_v4.to_string(), token_v4_expected);
  479. }
  480. #[test]
  481. fn test_token_str_round_trip() {
  482. let token_str = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
  483. let token = TokenV3::from_str(token_str).unwrap();
  484. assert_eq!(
  485. token.token[0].mint,
  486. MintUrl::from_str("https://8333.space:3338").unwrap()
  487. );
  488. assert_eq!(
  489. token.token[0].proofs[0].clone().keyset_id,
  490. Id::from_str("009a1f293253e41e").unwrap()
  491. );
  492. assert_eq!(token.unit.clone().unwrap(), CurrencyUnit::Sat);
  493. let encoded = &token.to_string();
  494. let token_data = TokenV3::from_str(encoded).unwrap();
  495. assert_eq!(token_data, token);
  496. }
  497. #[test]
  498. fn incorrect_tokens() {
  499. let incorrect_prefix = "casshuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
  500. let incorrect_prefix_token = TokenV3::from_str(incorrect_prefix);
  501. assert!(incorrect_prefix_token.is_err());
  502. let no_prefix = "eyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
  503. let no_prefix_token = TokenV3::from_str(no_prefix);
  504. assert!(no_prefix_token.is_err());
  505. let correct_token = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
  506. let correct_token = TokenV3::from_str(correct_token);
  507. assert!(correct_token.is_ok());
  508. }
  509. #[test]
  510. fn test_token_v4_raw_roundtrip() {
  511. let token_raw = hex::decode("6372617742a4617481a261694800ad268c4d1f5826617081a3616101617378403961366462623834376264323332626137366462306466313937323136623239643362386363313435353363643237383237666331636339343266656462346561635821038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d4721267926164695468616e6b20796f75616d75687474703a2f2f6c6f63616c686f73743a33333338617563736174").unwrap();
  512. let token = TokenV4::try_from(&token_raw).expect("Token deserialization error");
  513. let token_raw_ = token.to_raw_bytes().expect("Token serialization error");
  514. let token_ = TokenV4::try_from(&token_raw_).expect("Token deserialization error");
  515. assert!(token_ == token)
  516. }
  517. #[test]
  518. fn test_token_generic_raw_roundtrip() {
  519. let tokenv4_raw = hex::decode("6372617742a4617481a261694800ad268c4d1f5826617081a3616101617378403961366462623834376264323332626137366462306466313937323136623239643362386363313435353363643237383237666331636339343266656462346561635821038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d4721267926164695468616e6b20796f75616d75687474703a2f2f6c6f63616c686f73743a33333338617563736174").unwrap();
  520. let tokenv4 = Token::try_from(&tokenv4_raw).expect("Token deserialization error");
  521. let tokenv4_ = TokenV4::try_from(&tokenv4_raw).expect("Token deserialization error");
  522. let tokenv4_bytes = tokenv4.to_raw_bytes().expect("Serialization error");
  523. let tokenv4_bytes_ = tokenv4_.to_raw_bytes().expect("Serialization error");
  524. assert!(tokenv4_bytes_ == tokenv4_bytes);
  525. }
  526. }