cdk_common/wallet/saga/
send.rs1use core::fmt;
4
5use serde::{Deserialize, Serialize};
6
7use crate::nuts::Proofs;
8use crate::{Amount, Error};
9
10#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub enum SendSagaState {
14 ProofsReserved,
16 TokenCreated,
18 RollingBack,
20}
21
22impl std::fmt::Display for SendSagaState {
23 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24 match self {
25 SendSagaState::ProofsReserved => write!(f, "proofs_reserved"),
26 SendSagaState::TokenCreated => write!(f, "token_created"),
27 SendSagaState::RollingBack => write!(f, "rolling_back"),
28 }
29 }
30}
31
32impl std::str::FromStr for SendSagaState {
33 type Err = Error;
34 fn from_str(s: &str) -> Result<Self, Self::Err> {
35 match s {
36 "proofs_reserved" => Ok(SendSagaState::ProofsReserved),
37 "token_created" => Ok(SendSagaState::TokenCreated),
38 "rolling_back" => Ok(SendSagaState::RollingBack),
39 _ => Err(Error::InvalidOperationState),
40 }
41 }
42}
43
44#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
46pub struct SendOperationData {
47 pub amount: Amount,
49 pub memo: Option<String>,
51 pub counter_start: Option<u32>,
53 pub counter_end: Option<u32>,
55 pub token: Option<String>,
57 pub proofs: Option<Proofs>,
59}
60
61impl fmt::Debug for SendOperationData {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 f.debug_struct("SendOperationData")
64 .field("amount", &self.amount)
65 .field("memo", &self.memo)
66 .field("counter_start", &self.counter_start)
67 .field("counter_end", &self.counter_end)
68 .field("token", &self.token.as_ref().map(|_| "[redacted]"))
69 .field("proofs", &self.proofs.as_ref().map(|_| "[redacted]"))
70 .finish()
71 }
72}
73
74#[cfg(test)]
75mod tests {
76 use std::str::FromStr;
77
78 use super::SendOperationData;
79 use crate::nuts::{Id, Proof, SecretKey};
80 use crate::secret::Secret;
81 use crate::Amount;
82
83 const SECRET_MARKER: &str = "super_secret_spending_material_xyz";
84 const TOKEN_MARKER: &str = "cashuB_super_secret_bearer_token_marker";
85
86 #[allow(clippy::use_debug)]
87 #[test]
88 fn send_operation_data_debug_does_not_leak_spending_secrets() {
89 let keyset_id = Id::from_str("00deadbeef123456").expect("valid keyset id");
90 let proof = Proof::new(
91 Amount::from(1),
92 keyset_id,
93 Secret::new(SECRET_MARKER),
94 SecretKey::generate().public_key(),
95 );
96
97 let data = SendOperationData {
98 amount: Amount::from(1),
99 memo: None,
100 counter_start: None,
101 counter_end: None,
102 token: Some(TOKEN_MARKER.to_string()),
103 proofs: Some(vec![proof]),
104 };
105
106 let debug_output = format!("{data:?}");
107 assert!(
108 !debug_output.contains(SECRET_MARKER),
109 "SendOperationData Debug leaked a proof spending secret: {debug_output}"
110 );
111 assert!(
112 !debug_output.contains(TOKEN_MARKER),
113 "SendOperationData Debug leaked the bearer token: {debug_output}"
114 );
115 }
116}