1use std::collections::{BTreeSet, HashMap, HashSet};
6use std::fmt;
7use std::str::FromStr;
8
9use bitcoin::base64::engine::{general_purpose, GeneralPurpose};
10use bitcoin::base64::{alphabet, Engine as _};
11use bitcoin::hashes::sha256;
12use serde::{Deserialize, Serialize};
13
14use super::{Error, Proof, ProofV3, ProofV4, Proofs};
15use crate::mint_url::MintUrl;
16use crate::nut02::ShortKeysetId;
17use crate::nuts::nut10::SpendingConditions;
18use crate::nuts::{CurrencyUnit, Id, Kind, PublicKey};
19use crate::{ensure_cdk, Amount, KeySetInfo};
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(untagged)]
24pub enum Token {
25 TokenV3(TokenV3),
27 TokenV4(TokenV4),
29}
30
31impl fmt::Display for Token {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 let token = match self {
34 Self::TokenV3(token) => token.to_string(),
35 Self::TokenV4(token) => token.to_string(),
36 };
37
38 write!(f, "{token}")
39 }
40}
41
42impl Token {
43 pub fn new(
45 mint_url: MintUrl,
46 proofs: Proofs,
47 memo: Option<String>,
48 unit: CurrencyUnit,
49 ) -> Self {
50 let proofs = proofs
51 .into_iter()
52 .fold(HashMap::new(), |mut acc, val| {
53 acc.entry(val.keyset_id)
54 .and_modify(|p: &mut Vec<Proof>| p.push(val.clone()))
55 .or_insert(vec![val.clone()]);
56 acc
57 })
58 .into_iter()
59 .map(|(id, proofs)| TokenV4Token::new(id, proofs))
60 .collect();
61
62 Token::TokenV4(TokenV4 {
63 mint_url,
64 unit,
65 memo,
66 token: proofs,
67 })
68 }
69
70 pub fn proofs(&self, mint_keysets: &[KeySetInfo]) -> Result<Proofs, Error> {
72 match self {
73 Self::TokenV3(token) => token.proofs(mint_keysets),
74 Self::TokenV4(token) => token.proofs(mint_keysets),
75 }
76 }
77
78 pub fn value(&self) -> Result<Amount, Error> {
80 match self {
81 Self::TokenV3(token) => token.value(),
82 Self::TokenV4(token) => token.value(),
83 }
84 }
85
86 pub fn memo(&self) -> &Option<String> {
88 match self {
89 Self::TokenV3(token) => token.memo(),
90 Self::TokenV4(token) => token.memo(),
91 }
92 }
93
94 pub fn unit(&self) -> Option<CurrencyUnit> {
96 match self {
97 Self::TokenV3(token) => token.unit().clone(),
98 Self::TokenV4(token) => Some(token.unit().clone()),
99 }
100 }
101
102 pub fn mint_url(&self) -> Result<MintUrl, Error> {
104 match self {
105 Self::TokenV3(token) => {
106 let mint_urls = token.mint_urls();
107
108 ensure_cdk!(mint_urls.len() == 1, Error::UnsupportedToken);
109
110 mint_urls.first().ok_or(Error::UnsupportedToken).cloned()
111 }
112 Self::TokenV4(token) => Ok(token.mint_url.clone()),
113 }
114 }
115
116 pub fn to_v3_string(&self) -> String {
118 let v3_token = match self {
119 Self::TokenV3(token) => token.clone(),
120 Self::TokenV4(token) => token.clone().into(),
121 };
122
123 v3_token.to_string()
124 }
125
126 pub fn to_raw_bytes(&self) -> Result<Vec<u8>, Error> {
128 match self {
129 Self::TokenV3(_) => Err(Error::UnsupportedToken),
130 Self::TokenV4(token) => token.to_raw_bytes(),
131 }
132 }
133
134 pub fn token_secrets(&self) -> Vec<&crate::secret::Secret> {
137 match self {
138 Token::TokenV3(t) => t
139 .token
140 .iter()
141 .flat_map(|kt| kt.proofs.iter().map(|p| &p.secret))
142 .collect(),
143 Token::TokenV4(t) => t
144 .token
145 .iter()
146 .flat_map(|kt| kt.proofs.iter().map(|p| &p.secret))
147 .collect(),
148 }
149 }
150
151 pub fn spending_conditions(&self) -> Result<HashSet<SpendingConditions>, Error> {
153 let mut set = HashSet::new();
154 for secret in self.token_secrets().into_iter() {
155 if let Ok(cond) = SpendingConditions::try_from(secret) {
156 set.insert(cond);
157 }
158 }
159 Ok(set)
160 }
161
162 pub fn p2pk_pubkeys(&self) -> Result<HashSet<PublicKey>, Error> {
164 let mut keys: HashSet<PublicKey> = HashSet::new();
165 for secret in self.token_secrets().into_iter() {
166 if let Ok(cond) = SpendingConditions::try_from(secret) {
167 if cond.kind() == Kind::P2PK {
168 if let Some(ps) = cond.pubkeys() {
169 keys.extend(ps);
170 }
171 }
172 }
173 }
174 Ok(keys)
175 }
176
177 pub fn p2pk_refund_pubkeys(&self) -> Result<HashSet<PublicKey>, Error> {
179 let mut keys: HashSet<PublicKey> = HashSet::new();
180 for secret in self.token_secrets().into_iter() {
181 if let Ok(cond) = SpendingConditions::try_from(secret) {
182 if cond.kind() == Kind::P2PK {
183 if let Some(ps) = cond.refund_keys() {
184 keys.extend(ps);
185 }
186 }
187 }
188 }
189 Ok(keys)
190 }
191
192 pub fn htlc_hashes(&self) -> Result<HashSet<sha256::Hash>, Error> {
194 let mut hashes: HashSet<sha256::Hash> = HashSet::new();
195 for secret in self.token_secrets().into_iter() {
196 if let Ok(SpendingConditions::HTLCConditions { data, .. }) =
197 SpendingConditions::try_from(secret)
198 {
199 hashes.insert(data);
200 }
201 }
202 Ok(hashes)
203 }
204
205 pub fn locktimes(&self) -> Result<BTreeSet<u64>, Error> {
207 let mut set: BTreeSet<u64> = BTreeSet::new();
208 for secret in self.token_secrets().into_iter() {
209 if let Ok(cond) = SpendingConditions::try_from(secret) {
210 if let Some(lt) = cond.locktime() {
211 set.insert(lt);
212 }
213 }
214 }
215 Ok(set)
216 }
217}
218
219impl FromStr for Token {
220 type Err = Error;
221
222 fn from_str(s: &str) -> Result<Self, Self::Err> {
223 let (is_v3, s) = match (s.strip_prefix("cashuA"), s.strip_prefix("cashuB")) {
224 (Some(s), None) => (true, s),
225 (None, Some(s)) => (false, s),
226 _ => return Err(Error::UnsupportedToken),
227 };
228
229 let decode_config = general_purpose::GeneralPurposeConfig::new()
230 .with_decode_padding_mode(bitcoin::base64::engine::DecodePaddingMode::Indifferent);
231 let decoded = GeneralPurpose::new(&alphabet::URL_SAFE, decode_config).decode(s)?;
232
233 match is_v3 {
234 true => {
235 let decoded_str = String::from_utf8(decoded)?;
236 let token: TokenV3 = serde_json::from_str(&decoded_str)?;
237 Ok(Token::TokenV3(token))
238 }
239 false => {
240 let token: TokenV4 = ciborium::from_reader(&decoded[..])?;
241 Ok(Token::TokenV4(token))
242 }
243 }
244 }
245}
246
247impl TryFrom<&Vec<u8>> for Token {
248 type Error = Error;
249
250 fn try_from(bytes: &Vec<u8>) -> Result<Self, Self::Error> {
251 ensure_cdk!(bytes.len() >= 5, Error::UnsupportedToken);
252
253 let prefix = String::from_utf8(bytes[..5].to_vec())?;
254
255 match prefix.as_str() {
256 "crawB" => {
257 let token: TokenV4 = ciborium::from_reader(&bytes[5..])?;
258 Ok(Token::TokenV4(token))
259 }
260 _ => Err(Error::UnsupportedToken),
261 }
262 }
263}
264
265#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
267pub struct TokenV3Token {
268 pub mint: MintUrl,
270 pub proofs: Vec<ProofV3>,
272}
273
274impl TokenV3Token {
275 pub fn new(mint_url: MintUrl, proofs: Proofs) -> Self {
277 Self {
278 mint: mint_url,
279 proofs: proofs.into_iter().map(ProofV3::from).collect(),
280 }
281 }
282}
283
284#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
286pub struct TokenV3 {
287 pub token: Vec<TokenV3Token>,
289 #[serde(skip_serializing_if = "Option::is_none")]
291 pub memo: Option<String>,
292 #[serde(skip_serializing_if = "Option::is_none")]
294 pub unit: Option<CurrencyUnit>,
295}
296
297impl TokenV3 {
298 pub fn new(
300 mint_url: MintUrl,
301 proofs: Proofs,
302 memo: Option<String>,
303 unit: Option<CurrencyUnit>,
304 ) -> Result<Self, Error> {
305 ensure_cdk!(!proofs.is_empty(), Error::ProofsRequired);
306
307 Ok(Self {
308 token: vec![TokenV3Token::new(mint_url, proofs)],
309 memo,
310 unit,
311 })
312 }
313
314 pub fn proofs(&self, mint_keysets: &[KeySetInfo]) -> Result<Proofs, Error> {
316 let mut proofs: Proofs = vec![];
317 for t in self.token.iter() {
318 for p in t.proofs.iter() {
319 let long_id = Id::from_short_keyset_id(&p.keyset_id, mint_keysets)?;
320 proofs.push(p.into_proof(&long_id));
321 }
322 }
323 Ok(proofs)
324 }
325
326 #[inline]
328 pub fn value(&self) -> Result<Amount, Error> {
329 let proof_count = self.token.iter().map(|t| t.proofs.len()).sum::<usize>();
330 let unique_count = self
331 .token
332 .iter()
333 .flat_map(|t| t.proofs.iter().map(|p| &p.secret))
334 .collect::<std::collections::HashSet<_>>()
335 .len();
336
337 if unique_count != proof_count {
339 return Err(Error::DuplicateProofs);
340 }
341
342 Ok(Amount::try_sum(
343 self.token
344 .iter()
345 .map(|t| Amount::try_sum(t.proofs.iter().map(|p| p.amount)))
346 .collect::<Result<Vec<Amount>, _>>()?,
347 )?)
348 }
349
350 #[inline]
352 pub fn memo(&self) -> &Option<String> {
353 &self.memo
354 }
355
356 #[inline]
358 pub fn unit(&self) -> &Option<CurrencyUnit> {
359 &self.unit
360 }
361
362 pub fn mint_urls(&self) -> Vec<MintUrl> {
364 let mut mint_urls = Vec::new();
365
366 for token in self.token.iter() {
367 mint_urls.push(token.mint.clone());
368 }
369
370 mint_urls
371 }
372
373 pub fn is_multi_mint(&self) -> bool {
377 self.token.len() > 1
378 }
379}
380
381impl FromStr for TokenV3 {
382 type Err = Error;
383
384 fn from_str(s: &str) -> Result<Self, Self::Err> {
385 let s = s.strip_prefix("cashuA").ok_or(Error::UnsupportedToken)?;
386
387 let decode_config = general_purpose::GeneralPurposeConfig::new()
388 .with_decode_padding_mode(bitcoin::base64::engine::DecodePaddingMode::Indifferent);
389 let decoded = GeneralPurpose::new(&alphabet::URL_SAFE, decode_config).decode(s)?;
390 let decoded_str = String::from_utf8(decoded)?;
391 let token: TokenV3 = serde_json::from_str(&decoded_str)?;
392 Ok(token)
393 }
394}
395
396impl fmt::Display for TokenV3 {
397 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
398 let json_string = serde_json::to_string(self).map_err(|_| fmt::Error)?;
399 let encoded = general_purpose::URL_SAFE.encode(json_string);
400 write!(f, "cashuA{encoded}")
401 }
402}
403
404impl From<TokenV4> for TokenV3 {
405 fn from(token: TokenV4) -> Self {
406 let proofs: Vec<ProofV3> = token
407 .token
408 .into_iter()
409 .flat_map(|token| {
410 token.proofs.into_iter().map(move |p| ProofV3 {
411 amount: p.amount,
412 keyset_id: token.keyset_id.clone(),
413 secret: p.secret,
414 c: p.c,
415 witness: p.witness,
416 dleq: p.dleq,
417 })
418 })
419 .collect();
420
421 let token_v3_token = TokenV3Token {
422 mint: token.mint_url,
423 proofs,
424 };
425 TokenV3 {
426 token: vec![token_v3_token],
427 memo: token.memo,
428 unit: Some(token.unit),
429 }
430 }
431}
432
433#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
435pub struct TokenV4 {
436 #[serde(rename = "m")]
438 pub mint_url: MintUrl,
439 #[serde(rename = "u")]
441 pub unit: CurrencyUnit,
442 #[serde(rename = "d", skip_serializing_if = "Option::is_none")]
444 pub memo: Option<String>,
445 #[serde(rename = "t")]
447 pub token: Vec<TokenV4Token>,
448}
449
450impl TokenV4 {
451 pub fn proofs(&self, mint_keysets: &[KeySetInfo]) -> Result<Proofs, Error> {
453 let mut proofs: Proofs = vec![];
454 for t in self.token.iter() {
455 let long_id = Id::from_short_keyset_id(&t.keyset_id, mint_keysets)?;
456 proofs.extend(t.proofs.iter().map(|p| p.into_proof(&long_id)));
457 }
458 Ok(proofs)
459 }
460
461 #[inline]
463 pub fn value(&self) -> Result<Amount, Error> {
464 let proof_count = self.token.iter().map(|t| t.proofs.len()).sum::<usize>();
465 let unique_count = self
466 .token
467 .iter()
468 .flat_map(|t| t.proofs.iter().map(|p| &p.secret))
469 .collect::<std::collections::HashSet<_>>()
470 .len();
471
472 if unique_count != proof_count {
474 return Err(Error::DuplicateProofs);
475 }
476
477 Ok(Amount::try_sum(
478 self.token
479 .iter()
480 .map(|t| Amount::try_sum(t.proofs.iter().map(|p| p.amount)))
481 .collect::<Result<Vec<Amount>, _>>()?,
482 )?)
483 }
484
485 #[inline]
487 pub fn memo(&self) -> &Option<String> {
488 &self.memo
489 }
490
491 #[inline]
493 pub fn unit(&self) -> &CurrencyUnit {
494 &self.unit
495 }
496
497 pub fn to_raw_bytes(&self) -> Result<Vec<u8>, Error> {
499 let mut prefix = b"crawB".to_vec();
500 let mut data = Vec::new();
501 ciborium::into_writer(self, &mut data).map_err(Error::CiboriumSerError)?;
502 prefix.extend(data);
503 Ok(prefix)
504 }
505}
506
507impl fmt::Display for TokenV4 {
508 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
509 use serde::ser::Error;
510 let mut data = Vec::new();
511 ciborium::into_writer(self, &mut data).map_err(|e| fmt::Error::custom(e.to_string()))?;
512 let encoded = general_purpose::URL_SAFE.encode(data);
513 write!(f, "cashuB{encoded}")
514 }
515}
516
517impl FromStr for TokenV4 {
518 type Err = Error;
519
520 fn from_str(s: &str) -> Result<Self, Self::Err> {
521 let s = s.strip_prefix("cashuB").ok_or(Error::UnsupportedToken)?;
522
523 let decode_config = general_purpose::GeneralPurposeConfig::new()
524 .with_decode_padding_mode(bitcoin::base64::engine::DecodePaddingMode::Indifferent);
525 let decoded = GeneralPurpose::new(&alphabet::URL_SAFE, decode_config).decode(s)?;
526 let token: TokenV4 = ciborium::from_reader(&decoded[..])?;
527 Ok(token)
528 }
529}
530
531impl TryFrom<&Vec<u8>> for TokenV4 {
532 type Error = Error;
533
534 fn try_from(bytes: &Vec<u8>) -> Result<Self, Self::Error> {
535 ensure_cdk!(bytes.len() >= 5, Error::UnsupportedToken);
536
537 let prefix = String::from_utf8(bytes[..5].to_vec())?;
538 ensure_cdk!(prefix.as_str() == "crawB", Error::UnsupportedToken);
539
540 Ok(ciborium::from_reader(&bytes[5..])?)
541 }
542}
543
544impl TryFrom<TokenV3> for TokenV4 {
545 type Error = Error;
546 fn try_from(token: TokenV3) -> Result<Self, Self::Error> {
547 let mint_urls = token.mint_urls();
548 let proofs: Vec<ProofV3> = token.token.into_iter().flat_map(|t| t.proofs).collect();
549
550 ensure_cdk!(mint_urls.len() == 1, Error::UnsupportedToken);
551
552 let mint_url = mint_urls.first().ok_or(Error::UnsupportedToken)?;
553
554 let proofs = proofs
555 .into_iter()
556 .fold(
557 HashMap::<ShortKeysetId, Vec<ProofV4>>::new(),
558 |mut acc, val| {
559 acc.entry(val.keyset_id.clone())
560 .and_modify(|p: &mut Vec<ProofV4>| p.push(val.clone().into()))
561 .or_insert(vec![val.clone().into()]);
562 acc
563 },
564 )
565 .into_iter()
566 .map(|(id, proofs)| TokenV4Token {
567 keyset_id: id,
568 proofs,
569 })
570 .collect();
571
572 Ok(TokenV4 {
573 mint_url: mint_url.clone(),
574 token: proofs,
575 memo: token.memo,
576 unit: token.unit.ok_or(Error::UnsupportedUnit)?,
577 })
578 }
579}
580
581#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
583pub struct TokenV4Token {
584 #[serde(
586 rename = "i",
587 serialize_with = "serialize_v4_keyset_id",
588 deserialize_with = "deserialize_v4_keyset_id"
589 )]
590 pub keyset_id: ShortKeysetId,
591 #[serde(rename = "p")]
593 pub proofs: Vec<ProofV4>,
594}
595
596fn serialize_v4_keyset_id<S>(keyset_id: &ShortKeysetId, serializer: S) -> Result<S::Ok, S::Error>
597where
598 S: serde::Serializer,
599{
600 serializer.serialize_bytes(&keyset_id.to_bytes())
601}
602
603fn deserialize_v4_keyset_id<'de, D>(deserializer: D) -> Result<ShortKeysetId, D::Error>
604where
605 D: serde::Deserializer<'de>,
606{
607 let bytes = Vec::<u8>::deserialize(deserializer)?;
608 ShortKeysetId::from_bytes(&bytes).map_err(serde::de::Error::custom)
609}
610
611impl TokenV4Token {
612 pub fn new(keyset_id: Id, proofs: Proofs) -> Self {
614 let short_id = ShortKeysetId::from(keyset_id);
616 Self {
617 keyset_id: short_id,
618 proofs: proofs.into_iter().map(Into::into).collect(),
619 }
620 }
621}
622
623#[cfg(test)]
624mod tests {
625 use std::str::FromStr;
626
627 use bip39::rand::{self, RngCore};
628 use bitcoin::hashes::sha256::Hash as Sha256Hash;
629 use bitcoin::hashes::Hash;
630
631 use super::*;
632 use crate::dhke::hash_to_curve;
633 use crate::mint_url::MintUrl;
634 use crate::nuts::nut10::{Conditions, SpendingConditions};
635 use crate::nuts::nut11::SigFlag;
636 use crate::secret::Secret;
637 use crate::util::hex;
638
639 #[test]
640 fn test_token_padding() {
641 let token_str_with_padding = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91IHZlcnkgbXVjaC4ifQ==";
642
643 let token = TokenV3::from_str(token_str_with_padding).unwrap();
644
645 let token_str_without_padding = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91IHZlcnkgbXVjaC4ifQ";
646
647 let token_without = TokenV3::from_str(token_str_without_padding).unwrap();
648
649 assert_eq!(token, token_without);
650 }
651
652 #[test]
653 fn test_token_v4_str_round_trip() {
654 let token_str = "cashuBpGF0gaJhaUgArSaMTR9YJmFwgaNhYQFhc3hAOWE2ZGJiODQ3YmQyMzJiYTc2ZGIwZGYxOTcyMTZiMjlkM2I4Y2MxNDU1M2NkMjc4MjdmYzFjYzk0MmZlZGI0ZWFjWCEDhhhUP_trhpXfStS6vN6So0qWvc2X3O4NfM-Y1HISZ5JhZGlUaGFuayB5b3VhbXVodHRwOi8vbG9jYWxob3N0OjMzMzhhdWNzYXQ=";
655 let token = TokenV4::from_str(token_str).unwrap();
656
657 assert_eq!(
658 token.mint_url,
659 MintUrl::from_str("http://localhost:3338").unwrap()
660 );
661 assert_eq!(
662 token.token[0].keyset_id,
663 ShortKeysetId::from_str("00ad268c4d1f5826").unwrap()
664 );
665
666 let encoded = &token.to_string();
667
668 let token_data = TokenV4::from_str(encoded).unwrap();
669
670 assert_eq!(token_data, token);
671 }
672
673 #[test]
674 fn test_token_v4_multi_keyset() {
675 let token_str_multi_keysets = "cashuBo2F0gqJhaUgA_9SLj17PgGFwgaNhYQFhc3hAYWNjMTI0MzVlN2I4NDg0YzNjZjE4NTAxNDkyMThhZjkwZjcxNmE1MmJmNGE1ZWQzNDdlNDhlY2MxM2Y3NzM4OGFjWCECRFODGd5IXVW-07KaZCvuWHk3WrnnpiDhHki6SCQh88-iYWlIAK0mjE0fWCZhcIKjYWECYXN4QDEzMjNkM2Q0NzA3YTU4YWQyZTIzYWRhNGU5ZjFmNDlmNWE1YjRhYzdiNzA4ZWIwZDYxZjczOGY0ODMwN2U4ZWVhY1ghAjRWqhENhLSsdHrr2Cw7AFrKUL9Ffr1XN6RBT6w659lNo2FhAWFzeEA1NmJjYmNiYjdjYzY0MDZiM2ZhNWQ1N2QyMTc0ZjRlZmY4YjQ0MDJiMTc2OTI2ZDNhNTdkM2MzZGNiYjU5ZDU3YWNYIQJzEpxXGeWZN5qXSmJjY8MzxWyvwObQGr5G1YCCgHicY2FtdWh0dHA6Ly9sb2NhbGhvc3Q6MzMzOGF1Y3NhdA==";
676
677 let token = Token::from_str(token_str_multi_keysets).unwrap();
678 let amount = token.value().expect("valid amount");
679
680 assert_eq!(amount, Amount::from(4));
681
682 let unit = token.unit().unwrap();
683 assert_eq!(CurrencyUnit::Sat, unit);
684
685 match token {
686 Token::TokenV4(token) => {
687 let tokens: Vec<ShortKeysetId> =
688 token.token.iter().map(|t| t.keyset_id.clone()).collect();
689
690 assert_eq!(tokens.len(), 2);
691
692 assert!(tokens.contains(&ShortKeysetId::from_str("00ffd48b8f5ecf80").unwrap()));
693 assert!(tokens.contains(&ShortKeysetId::from_str("00ad268c4d1f5826").unwrap()));
694
695 let mint_url = token.mint_url;
696
697 assert_eq!("http://localhost:3338", &mint_url.to_string());
698 }
699 _ => {
700 panic!("Token should be a v4 token")
701 }
702 }
703 }
704
705 #[test]
706 fn test_tokenv4_from_tokenv3() {
707 let token_v3_str = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
708 let token_v3 =
709 TokenV3::from_str(token_v3_str).expect("TokenV3 should be created from string");
710 let token_v4 = TokenV4::try_from(token_v3).expect("TokenV3 should be converted to TokenV4");
711 let token_v4_expected = "cashuBpGFtd2h0dHBzOi8vODMzMy5zcGFjZTozMzM4YXVjc2F0YWRqVGhhbmsgeW91LmF0gaJhaUgAmh8pMlPkHmFwgqRhYQJhc3hANDA3OTE1YmMyMTJiZTYxYTc3ZTNlNmQyYWViNGM3Mjc5ODBiZGE1MWNkMDZhNmFmYzI5ZTI4NjE3NjhhNzgzN2FjWCECvJCXmX2Br7LMc0a15DRak0a9KlBut5WFmKcvDPhRY-phZPakYWEIYXN4QGZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmVhY1ghAp6OUFC4kKfWwJaNsWvB1dX6BA6h3ihPbsadYSmfZxBZYWT2";
712 assert_eq!(token_v4.to_string(), token_v4_expected);
713 }
714
715 #[test]
716 fn test_token_str_round_trip() {
717 let token_str = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
718
719 let token = TokenV3::from_str(token_str).unwrap();
720 assert_eq!(
721 token.token[0].mint,
722 MintUrl::from_str("https://8333.space:3338").unwrap()
723 );
724 assert_eq!(
725 token.token[0].proofs[0].clone().keyset_id,
726 ShortKeysetId::from_str("009a1f293253e41e").unwrap()
727 );
728 assert_eq!(token.unit.clone().unwrap(), CurrencyUnit::Sat);
729
730 let encoded = &token.to_string();
731
732 let token_data = TokenV3::from_str(encoded).unwrap();
733
734 assert_eq!(token_data, token);
735 }
736
737 #[test]
738 fn incorrect_tokens() {
739 let incorrect_prefix = "casshuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
740
741 let incorrect_prefix_token = TokenV3::from_str(incorrect_prefix);
742
743 assert!(incorrect_prefix_token.is_err());
744
745 let no_prefix = "eyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
746
747 let no_prefix_token = TokenV3::from_str(no_prefix);
748
749 assert!(no_prefix_token.is_err());
750
751 let correct_token = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
752
753 let correct_token = TokenV3::from_str(correct_token);
754
755 assert!(correct_token.is_ok());
756 }
757
758 #[test]
759 fn test_token_v4_raw_roundtrip() {
760 let token_raw = hex::decode("6372617742a4617481a261694800ad268c4d1f5826617081a3616101617378403961366462623834376264323332626137366462306466313937323136623239643362386363313435353363643237383237666331636339343266656462346561635821038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d4721267926164695468616e6b20796f75616d75687474703a2f2f6c6f63616c686f73743a33333338617563736174").unwrap();
761 let token = TokenV4::try_from(&token_raw).expect("Token deserialization error");
762 let token_raw_ = token.to_raw_bytes().expect("Token serialization error");
763 let token_ = TokenV4::try_from(&token_raw_).expect("Token deserialization error");
764 assert!(token_ == token)
765 }
766
767 #[test]
768 fn test_token_generic_raw_roundtrip() {
769 let tokenv4_raw = hex::decode("6372617742a4617481a261694800ad268c4d1f5826617081a3616101617378403961366462623834376264323332626137366462306466313937323136623239643362386363313435353363643237383237666331636339343266656462346561635821038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d4721267926164695468616e6b20796f75616d75687474703a2f2f6c6f63616c686f73743a33333338617563736174").unwrap();
770 let tokenv4 = Token::try_from(&tokenv4_raw).expect("Token deserialization error");
771 let tokenv4_ = TokenV4::try_from(&tokenv4_raw).expect("Token deserialization error");
772 let tokenv4_bytes = tokenv4.to_raw_bytes().expect("Serialization error");
773 let tokenv4_bytes_ = tokenv4_.to_raw_bytes().expect("Serialization error");
774 assert!(tokenv4_bytes_ == tokenv4_bytes);
775 }
776
777 #[test]
778 fn test_token_with_duplicate_proofs() {
779 let mint_url = MintUrl::from_str("https://example.com").unwrap();
781 let keyset_id = Id::from_str("009a1f293253e41e").unwrap();
782
783 let secret = Secret::generate();
784 let proof1 = Proof {
786 amount: Amount::from(10),
787 keyset_id,
788 secret: secret.clone(),
789 c: "02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea"
790 .parse()
791 .unwrap(),
792 witness: None,
793 dleq: None,
794 p2pk_e: None,
795 };
796
797 let proof2 = proof1.clone(); let proofs = vec![proof1.clone(), proof2].into_iter().collect();
801 let token = Token::new(mint_url.clone(), proofs, None, CurrencyUnit::Sat);
802
803 let result = token.value();
805 assert!(result.is_err());
806
807 let proof2 = Proof {
809 amount: Amount::from(10_000),
810 ..proof1.clone()
811 };
812
813 let proofs = vec![proof1.clone(), proof2.clone()].into_iter().collect();
814 let token = Token::new(mint_url.clone(), proofs, None, CurrencyUnit::Sat);
815
816 let result = token.value();
817 assert!(result.is_err());
818
819 let proofs = vec![proof1.clone(), proof2].into_iter().collect();
820 let token = TokenV3::new(mint_url.clone(), proofs, None, Some(CurrencyUnit::Sat))
821 .expect("token should be created");
822
823 let result = token.value();
824 assert!(result.is_err());
825
826 let proof3 = Proof {
828 amount: Amount::from(10),
829 keyset_id,
830 secret: Secret::generate(),
831 c: "03bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea"
832 .parse()
833 .unwrap(), witness: None,
835 dleq: None,
836 p2pk_e: None,
837 };
838
839 let proofs = vec![proof1, proof3].into_iter().collect();
840 let token = Token::new(mint_url, proofs, None, CurrencyUnit::Sat);
841
842 let result = token.value();
844 assert!(result.is_ok());
845 assert_eq!(result.unwrap(), Amount::from(20));
846 }
847
848 #[test]
849 fn test_token_from_proofs_with_idv2_round_trip() {
850 let mint_url = MintUrl::from_str("https://example.com").unwrap();
851
852 let keysets_info: Vec<KeySetInfo> = (0..10)
853 .map(|_| {
854 let mut bytes: [u8; 33] = [0u8; 33];
855 bytes[0] = 1u8;
856 rand::thread_rng().fill_bytes(&mut bytes[1..]);
857 let id = Id::from_bytes(&bytes).unwrap();
858 KeySetInfo {
859 id,
860 unit: CurrencyUnit::Sat,
861 active: true,
862 input_fee_ppk: 0,
863 final_expiry: None,
864 }
865 })
866 .collect();
867
868 let chosen_keyset_id = keysets_info[0].id;
869 let proofs = (0..5)
871 .map(|_| {
872 let mut c_preimage: [u8; 33] = [0u8; 33];
873 c_preimage[0] = 1u8;
874 rand::thread_rng().fill_bytes(&mut c_preimage[1..]);
875 Proof::new(
876 Amount::from(1),
877 chosen_keyset_id,
878 Secret::generate(),
879 hash_to_curve(&c_preimage).unwrap(),
880 )
881 })
882 .collect();
883
884 let token = Token::new(mint_url.clone(), proofs, None, CurrencyUnit::Sat);
885 let token_str = token.to_string();
886
887 let token1 = Token::from_str(&token_str);
888 assert!(token1.is_ok());
889
890 let proofs1 = token1.unwrap().proofs(&keysets_info);
891 assert!(proofs1.is_ok());
892
893 }
895
896 #[test]
897 fn test_token_proofs_with_unknown_short_keyset_id() {
898 let mint_url = MintUrl::from_str("https://example.com").unwrap();
899
900 let keysets_info: Vec<KeySetInfo> = (0..10)
901 .map(|_| {
902 let mut bytes: [u8; 33] = [0u8; 33];
903 bytes[0] = 1u8;
904 rand::thread_rng().fill_bytes(&mut bytes[1..]);
905 let id = Id::from_bytes(&bytes).unwrap();
906 KeySetInfo {
907 id,
908 unit: CurrencyUnit::Sat,
909 active: true,
910 input_fee_ppk: 0,
911 final_expiry: None,
912 }
913 })
914 .collect();
915
916 let chosen_keyset_id =
917 Id::from_str("01c352c0b47d42edb764bddf8c53d77b85f057157d92084d9d05e876251ecd8422")
918 .unwrap();
919
920 let proofs = (0..5)
922 .map(|_| {
923 let mut c_preimage: [u8; 33] = [0u8; 33];
924 c_preimage[0] = 1u8;
925 rand::thread_rng().fill_bytes(&mut c_preimage[1..]);
926 Proof::new(
927 Amount::from(1),
928 chosen_keyset_id,
929 Secret::generate(),
930 hash_to_curve(&c_preimage).unwrap(),
931 )
932 })
933 .collect();
934
935 let token = Token::new(mint_url.clone(), proofs, None, CurrencyUnit::Sat);
936 let token_str = token.to_string();
937
938 let token1 = Token::from_str(&token_str);
939 assert!(token1.is_ok());
940
941 let proofs1 = token1.unwrap().proofs(&keysets_info);
942 assert!(proofs1.is_err());
943 }
944 #[test]
945 fn test_token_spending_condition_helpers_p2pk_htlc_v4() {
946 let mint_url = MintUrl::from_str("https://example.com").unwrap();
947 let keyset_id = Id::from_str("009a1f293253e41e").unwrap();
948
949 let sk1 = crate::nuts::SecretKey::generate();
951 let pk1 = sk1.public_key();
952 let sk2 = crate::nuts::SecretKey::generate();
953 let pk2 = sk2.public_key();
954 let refund_sk = crate::nuts::SecretKey::generate();
955 let refund_pk = refund_sk.public_key();
956
957 let cond_p2pk = Conditions {
958 locktime: Some(1_700_000_000),
959 pubkeys: Some(vec![pk2]),
960 refund_keys: Some(vec![refund_pk]),
961 num_sigs: Some(1),
962 sig_flag: SigFlag::SigInputs,
963 num_sigs_refund: None,
964 };
965
966 let nut10_p2pk = crate::nuts::Nut10Secret::new(
967 crate::nuts::Kind::P2PK,
968 crate::nuts::SecretData::new(pk1.to_string(), Some(cond_p2pk.clone())),
969 );
970 let secret_p2pk: Secret = nut10_p2pk.try_into().unwrap();
971
972 let preimage = b"cdk-test-preimage";
974 let htlc_hash = Sha256Hash::hash(preimage);
975 let cond_htlc = Conditions {
976 locktime: Some(1_800_000_000),
977 ..Default::default()
978 };
979 let nut10_htlc = crate::nuts::Nut10Secret::new(
980 crate::nuts::Kind::HTLC,
981 crate::nuts::SecretData::new(htlc_hash.to_string(), Some(cond_htlc.clone())),
982 );
983 let secret_htlc: Secret = nut10_htlc.try_into().unwrap();
984
985 let proof_p2pk = Proof::new(Amount::from(1), keyset_id, secret_p2pk.clone(), pk1);
987 let proof_htlc = Proof::new(Amount::from(2), keyset_id, secret_htlc.clone(), pk2);
988 let token = Token::new(
989 mint_url,
990 vec![proof_p2pk, proof_htlc].into_iter().collect(),
991 None,
992 CurrencyUnit::Sat,
993 );
994
995 assert_eq!(token.token_secrets().len(), 2);
997
998 let sc = token.spending_conditions().unwrap();
1000 assert!(sc.contains(&SpendingConditions::P2PKConditions {
1001 data: pk1,
1002 conditions: Some(cond_p2pk.clone())
1003 }));
1004 assert!(sc.contains(&SpendingConditions::HTLCConditions {
1005 data: htlc_hash,
1006 conditions: Some(cond_htlc.clone())
1007 }));
1008
1009 let pks = token.p2pk_pubkeys().unwrap();
1011 assert!(pks.contains(&pk1));
1012 assert!(pks.contains(&pk2));
1013 assert_eq!(pks.len(), 2);
1014
1015 let refund = token.p2pk_refund_pubkeys().unwrap();
1017 assert!(refund.contains(&refund_pk));
1018 assert_eq!(refund.len(), 1);
1019
1020 let hashes = token.htlc_hashes().unwrap();
1022 assert!(hashes.contains(&htlc_hash));
1023 assert_eq!(hashes.len(), 1);
1024
1025 let lts = token.locktimes().unwrap();
1027 assert!(lts.contains(&1_700_000_000));
1028 assert!(lts.contains(&1_800_000_000));
1029 assert_eq!(lts.len(), 2);
1030 }
1031
1032 #[test]
1033 fn test_token_spending_condition_helpers_dedup_and_v3() {
1034 let mint_url = MintUrl::from_str("https://example.org").unwrap();
1035 let id = Id::from_str("00ad268c4d1f5826").unwrap();
1036
1037 let sk = crate::nuts::SecretKey::generate();
1039 let pk = sk.public_key();
1040
1041 let cond = Conditions {
1042 locktime: Some(1_650_000_000),
1043 pubkeys: Some(vec![pk]), refund_keys: Some(vec![pk]), num_sigs: Some(1),
1046 sig_flag: SigFlag::SigInputs,
1047 num_sigs_refund: None,
1048 };
1049
1050 let nut10 = crate::nuts::Nut10Secret::new(
1051 crate::nuts::Kind::P2PK,
1052 crate::nuts::SecretData::new(pk.to_string(), Some(cond.clone())),
1053 );
1054 let secret: Secret = nut10.try_into().unwrap();
1055
1056 let p1 = Proof::new(Amount::from(1), id, secret.clone(), pk);
1057 let p2 = Proof::new(Amount::from(2), id, secret.clone(), pk);
1058
1059 let token_v3 = TokenV3::new(
1061 mint_url,
1062 vec![p1, p2].into_iter().collect(),
1063 None,
1064 Some(CurrencyUnit::Sat),
1065 )
1066 .unwrap();
1067 let token = Token::TokenV3(token_v3);
1068
1069 let sc = token.spending_conditions().unwrap();
1071 assert_eq!(sc.len(), 1); let pks = token.p2pk_pubkeys().unwrap();
1074 assert!(pks.contains(&pk));
1075 assert_eq!(pks.len(), 1); let refunds = token.p2pk_refund_pubkeys().unwrap();
1078 assert!(refunds.contains(&pk));
1079 assert_eq!(refunds.len(), 1);
1080
1081 let lts = token.locktimes().unwrap();
1082 assert!(lts.contains(&1_650_000_000));
1083 assert_eq!(lts.len(), 1);
1084
1085 let hashes = token.htlc_hashes().unwrap();
1087 assert!(hashes.is_empty());
1088
1089 assert_eq!(token.token_secrets().len(), 2);
1091 }
1092}