1use std::fmt;
10use std::str::FromStr;
11
12use serde::{Deserialize, Serialize};
13use sha2::{Digest, Sha256};
14
15use crate::amount::Amount;
16use crate::error::{Error, Result};
17
18const WEBCASH_SYMBOL_BYTES: usize = 3;
20
21#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct SecureString(Vec<u8>);
24
25impl SecureString {
26 pub fn new<S: Into<String>>(s: S) -> Self {
28 let string = s.into();
29 let bytes = string.into_bytes();
30 SecureString(bytes)
31 }
32
33 pub fn from_bytes(bytes: Vec<u8>) -> Self {
35 SecureString(bytes)
36 }
37
38 pub fn as_str(&self) -> std::result::Result<&str, std::str::Utf8Error> {
40 std::str::from_utf8(&self.0)
41 }
42
43 pub fn as_bytes(&self) -> &[u8] {
45 &self.0
46 }
47
48 pub fn len(&self) -> usize {
50 self.0.len()
51 }
52
53 pub fn is_empty(&self) -> bool {
55 self.0.is_empty()
56 }
57
58 pub fn to_hex(&self) -> String {
60 hex::encode(&self.0)
61 }
62}
63
64impl fmt::Debug for SecureString {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 write!(f, "SecureString([redacted, {} bytes])", self.0.len())
67 }
68}
69
70impl fmt::Display for SecureString {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 write!(f, "[redacted]")
73 }
74}
75
76impl Drop for SecureString {
77 fn drop(&mut self) {
78 self.0.iter_mut().for_each(|byte| *byte = 0);
80 }
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85pub struct SecretWebcash {
86 pub secret: SecureString,
88 pub amount: Amount,
90}
91
92impl SecretWebcash {
93 pub fn new(secret: SecureString, amount: Amount) -> Self {
95 SecretWebcash { secret, amount }
96 }
97
98 pub fn parse(s: &str) -> Result<Self> {
100 let s = s.trim();
101 if !s.starts_with('e') {
102 return Err(Error::parse("SecretWebcash must start with 'e'"));
103 }
104
105 let parts: Vec<&str> = s[1..].split(':').collect();
106 if parts.len() < 3 {
107 return Err(Error::parse("Invalid SecretWebcash format"));
108 }
109
110 if parts[1] != "secret" {
111 return Err(Error::parse("Expected 'secret' type"));
112 }
113
114 let amount_str = parts[0];
115 let amount = Amount::from_str(amount_str)?;
116
117 let secret = parts[2..].join(":");
118
119 if secret.len() != 64 {
121 return Err(Error::parse("Secret must be 64 hex characters (32 bytes)"));
122 }
123
124 hex::decode(&secret).map_err(|_| Error::parse("Secret must be valid hex"))?;
125
126 Ok(SecretWebcash {
127 secret: SecureString::new(secret),
128 amount,
129 })
130 }
131
132 pub fn to_public(&self) -> PublicWebcash {
137 let secret_str = self.secret.as_str().unwrap_or("");
138 let hash = Sha256::digest(secret_str.as_bytes());
141 PublicWebcash {
142 hash: hash.into(),
143 amount: self.amount,
144 }
145 }
146
147 pub fn to_webcash_string(&self) -> String {
151 let secret_str = self.secret.as_str().unwrap_or("");
152 format!("e{}:secret:{}", self.amount, secret_str)
154 }
155}
156
157impl fmt::Display for SecretWebcash {
158 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159 write!(f, "{}", self.to_webcash_string())
160 }
161}
162
163impl FromStr for SecretWebcash {
164 type Err = Error;
165
166 fn from_str(s: &str) -> Result<Self> {
167 Self::parse(s)
168 }
169}
170
171#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
173pub struct PublicWebcash {
174 pub hash: [u8; 32],
176 pub amount: Amount,
178}
179
180impl PublicWebcash {
181 pub fn new(hash: [u8; 32], amount: Amount) -> Self {
183 PublicWebcash { hash, amount }
184 }
185
186 pub fn parse(s: &str) -> Result<Self> {
188 let s = s.trim();
189
190 let s = if let Some(stripped) = s.strip_prefix('e') {
192 stripped
193 } else if s.starts_with('₩') {
194 &s[WEBCASH_SYMBOL_BYTES..]
195 } else {
196 s
197 };
198
199 let parts: Vec<&str> = s.split(':').collect();
200 if parts.len() != 3 {
201 return Err(Error::parse("Invalid PublicWebcash format"));
202 }
203
204 if parts[1] != "public" {
205 return Err(Error::parse("Expected 'public' type"));
206 }
207
208 let amount_str = parts[0];
209 let amount = Amount::from_str(amount_str)?;
210
211 let hash_str = parts[2];
212 if hash_str.len() != 64 {
213 return Err(Error::parse("Hash must be 64 hex characters (32 bytes)"));
214 }
215
216 let hash_bytes =
217 hex::decode(hash_str).map_err(|_| Error::parse("Hash must be valid hex"))?;
218
219 let mut hash = [0u8; 32];
220 hash.copy_from_slice(&hash_bytes);
221
222 Ok(PublicWebcash { hash, amount })
223 }
224
225 pub fn hash_hex(&self) -> String {
227 hex::encode(self.hash)
228 }
229
230 pub fn to_webcash_string(&self) -> String {
234 format!("e{}:public:{}", self.amount, self.hash_hex())
236 }
237}
238
239impl fmt::Display for PublicWebcash {
240 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241 write!(f, "{}", self.to_webcash_string())
242 }
243}
244
245impl FromStr for PublicWebcash {
246 type Err = Error;
247
248 fn from_str(s: &str) -> Result<Self> {
249 Self::parse(s)
250 }
251}
252
253impl From<&SecretWebcash> for PublicWebcash {
254 fn from(secret: &SecretWebcash) -> Self {
255 secret.to_public()
256 }
257}