icydb_schema/
subaccount.rs1use crate::{Principal, SchemaContractError};
4
5use serde::{Deserialize, Deserializer, Serialize, de::Error as DeError};
6use std::fmt::{self, Display};
7
8type SubaccountBytes = [u8; 32];
13
14#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
15pub struct Subaccount(SubaccountBytes);
17
18impl Subaccount {
19 pub const MIN: Self = Self::from_array([0x00; 32]);
21 pub const MAX: Self = Self::from_array([0xFF; 32]);
23
24 #[must_use]
26 pub const fn to_array(&self) -> [u8; 32] {
27 self.0
28 }
29
30 #[must_use]
32 pub const fn from_array(array: [u8; 32]) -> Self {
33 Self(array)
34 }
35
36 pub fn from_hex(text: &str) -> Result<Self, SchemaContractError> {
42 let text = text.strip_prefix("0x").unwrap_or(text);
43 if text.len() != 64 || !text.is_ascii() {
44 return Err(SchemaContractError::InvalidLiteral);
45 }
46 let mut bytes = [0; 32];
47 for (index, pair) in text.as_bytes().as_chunks::<2>().0.iter().enumerate() {
48 let pair =
49 std::str::from_utf8(pair).map_err(|_| SchemaContractError::InvalidLiteral)?;
50 bytes[index] =
51 u8::from_str_radix(pair, 16).map_err(|_| SchemaContractError::InvalidLiteral)?;
52 }
53 Ok(Self::from_array(bytes))
54 }
55
56 #[must_use]
58 pub const fn as_slice(&self) -> &[u8] {
59 &self.0
60 }
61
62 #[must_use]
64 pub const fn to_bytes(self) -> [u8; 32] {
65 self.0
66 }
67}
68
69impl<'de> Deserialize<'de> for Subaccount {
70 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
71 where
72 D: Deserializer<'de>,
73 {
74 struct SubaccountBytesVisitor;
77
78 impl<'de> serde::de::Visitor<'de> for SubaccountBytesVisitor {
79 type Value = Subaccount;
80
81 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
82 formatter.write_str("exactly 32 subaccount bytes")
83 }
84
85 fn visit_seq<A: serde::de::SeqAccess<'de>>(
86 self,
87 mut sequence: A,
88 ) -> Result<Subaccount, A::Error> {
89 let mut bytes = [0; 32];
90 for (index, byte) in bytes.iter_mut().enumerate() {
91 *byte = sequence
92 .next_element()?
93 .ok_or_else(|| A::Error::invalid_length(index, &self))?;
94 }
95 if sequence.next_element::<u8>()?.is_some() {
96 return Err(A::Error::invalid_length(33, &self));
97 }
98 Ok(Subaccount::from_array(bytes))
99 }
100 }
101
102 if deserializer.is_human_readable() {
103 return SubaccountBytes::deserialize(deserializer).map(Self);
104 }
105
106 deserializer.deserialize_seq(SubaccountBytesVisitor)
107 }
108}
109
110impl Display for Subaccount {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 for byte in &self.0 {
113 write!(f, "{byte:02x}")?;
114 }
115
116 Ok(())
117 }
118}
119
120#[expect(clippy::cast_possible_truncation)]
123impl From<Principal> for Subaccount {
124 fn from(principal: Principal) -> Self {
125 let mut bytes = [0u8; 32];
126 let p = principal.as_slice();
127
128 let len = p.len().min(31); bytes[0] = len as u8;
131
132 bytes[1..=len].copy_from_slice(&p[..len]);
134
135 Self(bytes)
136 }
137}
138
139impl Serialize for Subaccount {
140 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
141 where
142 S: serde::Serializer,
143 {
144 if serializer.is_human_readable() {
145 self.0.serialize(serializer)
146 } else {
147 serializer.serialize_bytes(&self.0)
148 }
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use crate::Account;
156
157 #[test]
158 fn hex_text_preserves_every_byte_with_optional_prefix() {
159 for byte in 0..=u8::MAX {
160 let expected = Subaccount::from_array([byte; 32]);
161 for text in [
162 format!("{byte:02x}").repeat(32),
163 format!("{byte:02X}").repeat(32),
164 ] {
165 assert_eq!(Subaccount::from_hex(&text), Ok(expected));
166 assert_eq!(Subaccount::from_hex(&format!("0x{text}")), Ok(expected));
167 }
168 }
169 }
170
171 #[test]
172 fn hex_text_rejects_width_unicode_and_nonhex_boundaries() {
173 for text in [
174 String::new(),
175 "0x".into(),
176 "0".repeat(63),
177 "0".repeat(65),
178 "g".repeat(64),
179 format!("aƩ{}", "0".repeat(61)),
180 format!(" {}", "0".repeat(64)),
181 format!("0X{}", "0".repeat(64)),
182 ] {
183 assert_eq!(
184 Subaccount::from_hex(&text),
185 Err(SchemaContractError::InvalidLiteral)
186 );
187 }
188 }
189
190 #[test]
191 fn binary_serde_and_wire_preserve_exact_subaccount_bytes() {
192 for byte in [0, 23, 24, 128, 255] {
193 let value = Subaccount::from_array([byte; 32]);
194 let mut encoded = Vec::new();
195 ciborium::into_writer(&value, &mut encoded).unwrap();
196 assert_eq!(encoded.len(), 34);
197 let wire: ciborium::Value = ciborium::from_reader(encoded.as_slice()).unwrap();
198 assert_eq!(wire, ciborium::Value::Bytes(value.to_bytes().to_vec()));
199 assert_eq!(
200 ciborium::from_reader::<Subaccount, _>(encoded.as_slice()).unwrap(),
201 value
202 );
203
204 let json = serde_json::to_string(&value).unwrap();
205 assert_eq!(json, serde_json::to_string(&value.to_array()).unwrap());
206 assert_eq!(serde_json::from_str::<Subaccount>(&json).unwrap(), value);
207
208 let wire = crate::wire::encode_one(value).unwrap();
209 assert_eq!(
210 wire,
211 crate::wire::encode_one(serde_bytes::Bytes::new(&value.to_array())).unwrap()
212 );
213 assert_eq!(wire.len(), 33);
214 assert_eq!(crate::wire::decode_one::<Subaccount>(&wire).unwrap(), value);
215
216 let batch = vec![value; 1_000];
217 encoded.clear();
218 ciborium::into_writer(&batch, &mut encoded).unwrap();
219 assert_eq!(encoded.len(), 34_003);
220 assert_eq!(
221 ciborium::from_reader::<Vec<Subaccount>, _>(encoded.as_slice()).unwrap(),
222 batch
223 );
224 }
225 }
226
227 #[test]
228 fn binary_subaccount_ingress_enforces_width_and_byte_elements() {
229 for len in [0, 31, 33, 1_024] {
230 let wire = crate::wire::encode_one(vec![0_u8; len]).unwrap();
231 assert!(crate::wire::decode_one::<Subaccount>(&wire).is_err());
232 let mut encoded = Vec::new();
233 ciborium::into_writer(&serde_bytes::Bytes::new(&vec![0; len]), &mut encoded).unwrap();
234 assert!(ciborium::from_reader::<Subaccount, _>(encoded.as_slice()).is_err());
235 }
236 let wire = crate::wire::encode_one(vec![256_u16; 32]).unwrap();
239 assert!(crate::wire::decode_one::<Subaccount>(&wire).is_err());
240
241 let mut encoded = Vec::new();
242 ciborium::into_writer(&Subaccount::MAX, &mut encoded).unwrap();
243 for end in 0..encoded.len() {
244 assert!(ciborium::from_reader::<Subaccount, _>(&encoded[..end]).is_err());
245 }
246 }
247
248 #[test]
249 fn accounts_roundtrip_optional_subaccounts_in_all_transports() {
250 for subaccount in [None, Some(Subaccount::MIN), Some(Subaccount::MAX)] {
251 let account = Account::from_owner_and_subaccount(Principal::anonymous(), subaccount);
252 let mut encoded = Vec::new();
253 ciborium::into_writer(&account, &mut encoded).unwrap();
254 assert_eq!(encoded.len(), if subaccount.is_some() { 54 } else { 21 });
255 assert_eq!(
256 ciborium::from_reader::<Account, _>(encoded.as_slice()).unwrap(),
257 account
258 );
259 let json = serde_json::to_string(&account).unwrap();
260 assert_eq!(serde_json::from_str::<Account>(&json).unwrap(), account);
261 let wire = crate::wire::encode_one(account).unwrap();
262 assert_eq!(
264 wire,
265 crate::wire::encode_one((account.owner(), subaccount)).unwrap()
266 );
267 assert_eq!(crate::wire::decode_one::<Account>(&wire).unwrap(), account);
268 }
269 }
270}