icydb_schema/
subaccount.rs1use crate::{Principal, SchemaContractError, atom::deserialize_fixed_bytes};
4
5use serde::{Deserialize, Deserializer, Serialize};
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.bytes().all(|byte| byte.is_ascii_hexdigit()) {
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 if deserializer.is_human_readable() {
75 return SubaccountBytes::deserialize(deserializer).map(Self);
76 }
77
78 deserialize_fixed_bytes(deserializer).map(Self::from_array)
79 }
80}
81
82impl Display for Subaccount {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 for byte in &self.0 {
85 write!(f, "{byte:02x}")?;
86 }
87
88 Ok(())
89 }
90}
91
92#[expect(clippy::cast_possible_truncation)]
95impl From<Principal> for Subaccount {
96 fn from(principal: Principal) -> Self {
97 let mut bytes = [0u8; 32];
98 let p = principal.as_slice();
99
100 let len = p.len().min(31); bytes[0] = len as u8;
103
104 bytes[1..=len].copy_from_slice(&p[..len]);
106
107 Self(bytes)
108 }
109}
110
111impl Serialize for Subaccount {
112 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
113 where
114 S: serde::Serializer,
115 {
116 if serializer.is_human_readable() {
117 self.0.serialize(serializer)
118 } else {
119 serializer.serialize_bytes(&self.0)
120 }
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use crate::Account;
128
129 #[test]
130 fn hex_text_preserves_every_byte_with_optional_prefix() {
131 for byte in 0..=u8::MAX {
132 let expected = Subaccount::from_array([byte; 32]);
133 for text in [
134 format!("{byte:02x}").repeat(32),
135 format!("{byte:02X}").repeat(32),
136 ] {
137 assert_eq!(Subaccount::from_hex(&text), Ok(expected));
138 assert_eq!(Subaccount::from_hex(&format!("0x{text}")), Ok(expected));
139 }
140 }
141 }
142
143 #[test]
144 fn hex_text_rejects_width_unicode_and_nonhex_boundaries() {
145 for text in [
146 String::new(),
147 "0x".into(),
148 "0".repeat(63),
149 "0".repeat(65),
150 "g".repeat(64),
151 "+1".repeat(32),
152 "-1".repeat(32),
153 format!("aƩ{}", "0".repeat(61)),
154 format!(" {}", "0".repeat(64)),
155 format!("0X{}", "0".repeat(64)),
156 ] {
157 assert_eq!(
158 Subaccount::from_hex(&text),
159 Err(SchemaContractError::InvalidLiteral)
160 );
161 }
162 }
163
164 #[test]
165 fn binary_serde_and_wire_preserve_exact_subaccount_bytes() {
166 for byte in [0, 23, 24, 128, 255] {
167 let value = Subaccount::from_array([byte; 32]);
168 let mut encoded = Vec::new();
169 serde_cbor::to_writer(&mut encoded, &value).unwrap();
170 assert_eq!(encoded.len(), 34);
171 let wire: serde_cbor::Value = serde_cbor::from_slice(encoded.as_slice()).unwrap();
172 assert_eq!(wire, serde_cbor::Value::Bytes(value.to_bytes().to_vec()));
173 assert_eq!(
174 serde_cbor::from_slice::<Subaccount>(encoded.as_slice()).unwrap(),
175 value
176 );
177
178 let json = serde_json::to_string(&value).unwrap();
179 assert_eq!(json, serde_json::to_string(&value.to_array()).unwrap());
180 assert_eq!(serde_json::from_str::<Subaccount>(&json).unwrap(), value);
181
182 let wire = crate::wire::encode_one(value).unwrap();
183 assert_eq!(
184 wire,
185 crate::wire::encode_one(serde_bytes::Bytes::new(&value.to_array())).unwrap()
186 );
187 assert_eq!(wire.len(), 33);
188 assert_eq!(crate::wire::decode_one::<Subaccount>(&wire).unwrap(), value);
189
190 let batch = vec![value; 1_000];
191 encoded.clear();
192 serde_cbor::to_writer(&mut encoded, &batch).unwrap();
193 assert_eq!(encoded.len(), 34_003);
194 assert_eq!(
195 serde_cbor::from_slice::<Vec<Subaccount>>(encoded.as_slice()).unwrap(),
196 batch
197 );
198 }
199 }
200
201 #[test]
202 fn binary_subaccount_ingress_enforces_width_and_byte_elements() {
203 for len in [0, 31, 33, 1_024] {
204 let wire = crate::wire::encode_one(vec![0_u8; len]).unwrap();
205 assert!(crate::wire::decode_one::<Subaccount>(&wire).is_err());
206 let mut encoded = Vec::new();
207 serde_cbor::to_writer(&mut encoded, &serde_bytes::Bytes::new(&vec![0; len])).unwrap();
208 assert!(serde_cbor::from_slice::<Subaccount>(encoded.as_slice()).is_err());
209 }
210 let wire = crate::wire::encode_one(vec![256_u16; 32]).unwrap();
213 assert!(crate::wire::decode_one::<Subaccount>(&wire).is_err());
214
215 let array = serde_cbor::to_vec(&vec![255_u8; 32]).unwrap();
218 assert_eq!(
219 serde_cbor::from_slice::<Subaccount>(&array).unwrap(),
220 Subaccount::MAX
221 );
222 for array in [vec![0_u16; 31], vec![0; 33], vec![256; 32]] {
223 let encoded = serde_cbor::to_vec(&array).unwrap();
224 assert!(serde_cbor::from_slice::<Subaccount>(&encoded).is_err());
225 }
226
227 let mut encoded = Vec::new();
228 serde_cbor::to_writer(&mut encoded, &Subaccount::MAX).unwrap();
229 for end in 0..encoded.len() {
230 assert!(serde_cbor::from_slice::<Subaccount>(&encoded[..end]).is_err());
231 }
232 }
233
234 #[test]
235 fn accounts_roundtrip_optional_subaccounts_in_all_transports() {
236 for subaccount in [None, Some(Subaccount::MIN), Some(Subaccount::MAX)] {
237 let account = Account::from_owner_and_subaccount(Principal::anonymous(), subaccount);
238 let mut encoded = Vec::new();
239 serde_cbor::to_writer(&mut encoded, &account).unwrap();
240 assert_eq!(encoded.len(), if subaccount.is_some() { 54 } else { 21 });
241 assert_eq!(
242 serde_cbor::from_slice::<Account>(encoded.as_slice()).unwrap(),
243 account
244 );
245 let json = serde_json::to_string(&account).unwrap();
246 assert_eq!(serde_json::from_str::<Account>(&json).unwrap(), account);
247 let wire = crate::wire::encode_one(account).unwrap();
248 assert_eq!(
250 wire,
251 crate::wire::encode_one((account.owner(), subaccount)).unwrap()
252 );
253 assert_eq!(crate::wire::decode_one::<Account>(&wire).unwrap(), account);
254 }
255 }
256}