icydb_schema/
subaccount.rs1use crate::Principal;
4use candid::CandidType;
5use serde::{Deserialize, Deserializer, Serialize, de::Error as DeError};
6use std::fmt::{self, Display};
7
8type SubaccountBytes = [u8; 32];
13
14#[derive(CandidType, 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 #[must_use]
38 pub const fn as_slice(&self) -> &[u8] {
39 &self.0
40 }
41
42 #[must_use]
44 pub const fn to_bytes(self) -> [u8; 32] {
45 self.0
46 }
47}
48
49impl<'de> Deserialize<'de> for Subaccount {
50 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
51 where
52 D: Deserializer<'de>,
53 {
54 struct SubaccountBytesVisitor;
57
58 impl<'de> serde::de::Visitor<'de> for SubaccountBytesVisitor {
59 type Value = Subaccount;
60
61 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
62 formatter.write_str("exactly 32 subaccount bytes")
63 }
64
65 fn visit_seq<A: serde::de::SeqAccess<'de>>(
66 self,
67 mut sequence: A,
68 ) -> Result<Subaccount, A::Error> {
69 let mut bytes = [0; 32];
70 for (index, byte) in bytes.iter_mut().enumerate() {
71 *byte = sequence
72 .next_element()?
73 .ok_or_else(|| A::Error::invalid_length(index, &self))?;
74 }
75 if sequence.next_element::<u8>()?.is_some() {
76 return Err(A::Error::invalid_length(33, &self));
77 }
78 Ok(Subaccount::from_array(bytes))
79 }
80 }
81
82 if deserializer.is_human_readable() {
83 return SubaccountBytes::deserialize(deserializer).map(Self);
84 }
85
86 deserializer.deserialize_seq(SubaccountBytesVisitor)
87 }
88}
89
90impl Display for Subaccount {
91 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92 for byte in &self.0 {
93 write!(f, "{byte:02x}")?;
94 }
95
96 Ok(())
97 }
98}
99
100#[expect(clippy::cast_possible_truncation)]
103impl From<Principal> for Subaccount {
104 fn from(principal: Principal) -> Self {
105 let mut bytes = [0u8; 32];
106 let p = principal.as_slice();
107
108 let len = p.len().min(31); bytes[0] = len as u8;
111
112 bytes[1..=len].copy_from_slice(&p[..len]);
114
115 Self(bytes)
116 }
117}
118
119impl Serialize for Subaccount {
120 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
121 where
122 S: serde::Serializer,
123 {
124 if serializer.is_human_readable() {
125 self.0.serialize(serializer)
126 } else {
127 serializer.serialize_bytes(&self.0)
128 }
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use crate::Account;
136
137 #[test]
138 fn binary_serde_and_candid_preserve_exact_subaccount_bytes() {
139 for byte in [0, 23, 24, 128, 255] {
140 let value = Subaccount::from_array([byte; 32]);
141 let mut encoded = Vec::new();
142 ciborium::into_writer(&value, &mut encoded).unwrap();
143 assert_eq!(encoded.len(), 34);
144 let wire: ciborium::Value = ciborium::from_reader(encoded.as_slice()).unwrap();
145 assert_eq!(wire, ciborium::Value::Bytes(value.to_bytes().to_vec()));
146 assert_eq!(
147 ciborium::from_reader::<Subaccount, _>(encoded.as_slice()).unwrap(),
148 value
149 );
150
151 let json = serde_json::to_string(&value).unwrap();
152 assert_eq!(json, serde_json::to_string(&value.to_array()).unwrap());
153 assert_eq!(serde_json::from_str::<Subaccount>(&json).unwrap(), value);
154
155 let candid = candid::encode_one(value).unwrap();
156 assert_eq!(candid, candid::encode_one(value.to_array()).unwrap());
157 assert_eq!(candid.len(), 42);
158 assert_eq!(candid::decode_one::<Subaccount>(&candid).unwrap(), value);
159
160 let batch = vec![value; 1_000];
161 encoded.clear();
162 ciborium::into_writer(&batch, &mut encoded).unwrap();
163 assert_eq!(encoded.len(), 34_003);
164 assert_eq!(
165 ciborium::from_reader::<Vec<Subaccount>, _>(encoded.as_slice()).unwrap(),
166 batch
167 );
168 }
169 }
170
171 #[test]
172 fn binary_subaccount_ingress_enforces_width_and_byte_elements() {
173 for len in [0, 31, 33, 1_024] {
174 let wire = candid::encode_one(vec![0_u8; len]).unwrap();
175 assert!(candid::decode_one::<Subaccount>(&wire).is_err());
176 let mut encoded = Vec::new();
177 ciborium::into_writer(&serde_bytes::Bytes::new(&vec![0; len]), &mut encoded).unwrap();
178 assert!(ciborium::from_reader::<Subaccount, _>(encoded.as_slice()).is_err());
179 }
180 let wire = candid::encode_one(vec![0_u16; 32]).unwrap();
181 assert!(candid::decode_one::<Subaccount>(&wire).is_err());
182
183 let mut encoded = Vec::new();
184 ciborium::into_writer(&Subaccount::MAX, &mut encoded).unwrap();
185 for end in 0..encoded.len() {
186 assert!(ciborium::from_reader::<Subaccount, _>(&encoded[..end]).is_err());
187 }
188 }
189
190 #[test]
191 fn accounts_roundtrip_optional_subaccounts_in_all_transports() {
192 for subaccount in [None, Some(Subaccount::MIN), Some(Subaccount::MAX)] {
193 let account = Account::from_owner_and_subaccount(Principal::anonymous(), subaccount);
194 let mut encoded = Vec::new();
195 ciborium::into_writer(&account, &mut encoded).unwrap();
196 assert_eq!(encoded.len(), if subaccount.is_some() { 54 } else { 21 });
197 assert_eq!(
198 ciborium::from_reader::<Account, _>(encoded.as_slice()).unwrap(),
199 account
200 );
201 let json = serde_json::to_string(&account).unwrap();
202 assert_eq!(serde_json::from_str::<Account>(&json).unwrap(), account);
203 let candid = candid::encode_one(account).unwrap();
204 assert_eq!(candid, candid::encode_one(account.to_icrc_type()).unwrap());
205 assert_eq!(candid::decode_one::<Account>(&candid).unwrap(), account);
206 }
207 }
208}