ic_platform/account/
mod.rs1use crate::{Principal, PrincipalError};
15use serde::{Deserialize, Serialize};
16use std::{fmt, hash::Hash, str::FromStr};
17
18pub type Subaccount = [u8; 32];
20
21pub const DEFAULT_SUBACCOUNT: &Subaccount = &[0; 32];
23
24#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
26pub struct Account {
27 pub owner: Principal,
29 pub subaccount: Option<Subaccount>,
31}
32
33impl Account {
34 pub const MAX_LENGTH_IN_TEXT: usize = Principal::MAX_LENGTH_IN_TEXT + 1 + 7 + 1 + 64;
36
37 pub fn effective_subaccount(&self) -> &Subaccount {
39 self.subaccount.as_ref().unwrap_or(DEFAULT_SUBACCOUNT)
40 }
41}
42
43impl PartialEq for Account {
44 fn eq(&self, other: &Self) -> bool {
45 self.owner == other.owner && self.effective_subaccount() == other.effective_subaccount()
46 }
47}
48impl Eq for Account {}
49impl PartialOrd for Account {
50 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
51 Some(self.cmp(other))
52 }
53}
54impl Ord for Account {
55 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
56 self.owner.cmp(&other.owner).then_with(|| {
57 self.effective_subaccount()
58 .cmp(other.effective_subaccount())
59 })
60 }
61}
62impl Hash for Account {
63 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
64 self.owner.hash(state);
65 self.effective_subaccount().hash(state);
66 }
67}
68impl From<Principal> for Account {
69 fn from(owner: Principal) -> Self {
70 Self {
71 owner,
72 subaccount: None,
73 }
74 }
75}
76
77fn checksum(owner: &[u8], subaccount: &Subaccount) -> [u8; 7] {
79 let mut hasher = crc32fast::Hasher::new();
80 hasher.update(owner);
81 hasher.update(subaccount);
82 let mut encoded = [0; 7];
83 data_encoding::BASE32_NOPAD.encode_mut(&hasher.finalize().to_be_bytes(), &mut encoded);
84 encoded.make_ascii_lowercase();
85 encoded
86}
87
88impl fmt::Display for Account {
89 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90 write!(f, "{}", self.owner)?;
91 let subaccount = self.effective_subaccount();
92 if subaccount == DEFAULT_SUBACCOUNT {
93 return Ok(());
94 }
95 let checksum = checksum(self.owner.as_slice(), subaccount);
96 let checksum = std::str::from_utf8(&checksum).map_err(|_| fmt::Error)?;
97 write!(f, "-{checksum}.")?;
98 let first = subaccount
99 .iter()
100 .position(|byte| *byte != 0)
101 .ok_or(fmt::Error)?;
102 write!(f, "{:x}", subaccount[first])?;
103 for byte in &subaccount[first + 1..] {
104 write!(f, "{byte:02x}")?;
105 }
106 Ok(())
107 }
108}
109
110#[derive(Clone, Debug, PartialEq, Eq)]
112pub enum AccountError {
113 TextTooLong,
115 InvalidPrincipal(PrincipalError),
117 MissingChecksum,
119 InvalidChecksum,
121 InvalidSubaccount,
123 LeadingZeroesInSubaccount,
125 DefaultSubaccountShouldBeOmitted,
127}
128impl fmt::Display for AccountError {
129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130 match self {
131 Self::TextTooLong => f.write_str("account text is too long"),
132 Self::InvalidPrincipal(error) => write!(f, "invalid principal: {error}"),
133 Self::MissingChecksum => f.write_str("missing account checksum"),
134 Self::InvalidChecksum => f.write_str("invalid account checksum"),
135 Self::InvalidSubaccount => f.write_str("subaccount must contain at most 64 hex digits"),
136 Self::LeadingZeroesInSubaccount => {
137 f.write_str("subaccount should not have leading zeroes")
138 }
139 Self::DefaultSubaccountShouldBeOmitted => {
140 f.write_str("default subaccount should be omitted")
141 }
142 }
143 }
144}
145impl std::error::Error for AccountError {
146 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
147 match self {
148 Self::InvalidPrincipal(error) => Some(error),
149 _ => None,
150 }
151 }
152}
153impl FromStr for Account {
154 type Err = AccountError;
155
156 fn from_str(text: &str) -> Result<Self, Self::Err> {
157 if text.len() > Self::MAX_LENGTH_IN_TEXT {
158 return Err(AccountError::TextTooLong);
159 }
160 let Some((prefix, hex)) = text.split_once('.') else {
161 return text
162 .parse::<Principal>()
163 .map(Self::from)
164 .map_err(AccountError::InvalidPrincipal);
165 };
166 let (owner, supplied_checksum) = prefix
167 .rsplit_once('-')
168 .filter(|(_, sum)| sum.len() == 7)
169 .ok_or(AccountError::MissingChecksum)?;
170 if hex.starts_with('0') {
171 return Err(AccountError::LeadingZeroesInSubaccount);
172 }
173 let owner: Principal = owner
174 .parse::<Principal>()
175 .map_err(AccountError::InvalidPrincipal)?;
176 if hex.len() > 64 {
177 return Err(AccountError::InvalidSubaccount);
178 }
179 if hex.is_empty() {
180 return Err(AccountError::DefaultSubaccountShouldBeOmitted);
181 }
182 let mut subaccount = [0; 32];
183 for (index, byte) in hex.bytes().enumerate() {
184 let nibble = match byte {
185 b'0'..=b'9' => byte - b'0',
186 b'a'..=b'f' => byte - b'a' + 10,
187 b'A'..=b'F' => byte - b'A' + 10,
188 _ => return Err(AccountError::InvalidSubaccount),
189 };
190 let offset = 64 - hex.len() + index;
191 subaccount[offset / 2] |= nibble << (if offset % 2 == 0 { 4 } else { 0 });
192 }
193 if supplied_checksum.as_bytes() != checksum(owner.as_slice(), &subaccount) {
194 return Err(AccountError::InvalidChecksum);
195 }
196 Ok(Self {
197 owner,
198 subaccount: Some(subaccount),
199 })
200 }
201}
202
203#[cfg(feature = "icrc-ledger-types")]
205pub use icrc_ledger_types::icrc1::account::Account as ExternalAccount;
206
207#[cfg(feature = "icrc-ledger-types")]
208impl From<ExternalAccount> for Account {
209 fn from(value: ExternalAccount) -> Self {
210 Self {
211 owner: value.owner.into(),
212 subaccount: value.subaccount,
213 }
214 }
215}
216#[cfg(feature = "icrc-ledger-types")]
217impl From<Account> for ExternalAccount {
218 fn from(value: Account) -> Self {
219 Self {
220 owner: value.owner.into(),
221 subaccount: value.subaccount,
222 }
223 }
224}
225
226#[cfg(test)]
227mod tests;