1#[cfg(test)]
4mod tests;
5
6use std::{
7 fmt::{self, Display, Formatter},
8 str::FromStr,
9};
10
11use serde::{Deserialize, Serialize};
12
13use crate::{Principal, Subaccount};
14
15#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
17pub struct Account {
18 owner: Principal,
19 subaccount: Option<Subaccount>,
20}
21
22impl Account {
23 const PRINCIPAL_MAX_LEN: usize = Principal::MAX_LENGTH_IN_BYTES as usize;
24 const MAX_TEXT_LEN: usize = 136;
27 const TAG_SUBACCOUNT: u8 = 0x80;
28
29 pub fn new<P: Into<Principal>, S: Into<Subaccount>>(owner: P, subaccount: Option<S>) -> Self {
31 Self {
32 owner: owner.into(),
33 subaccount: subaccount.map(Into::into),
34 }
35 }
36
37 #[must_use]
39 pub const fn from_owner_and_subaccount(
40 owner: Principal,
41 subaccount: Option<Subaccount>,
42 ) -> Self {
43 Self { owner, subaccount }
44 }
45
46 #[must_use]
48 pub const fn owner(&self) -> Principal {
49 self.owner
50 }
51
52 #[must_use]
54 pub const fn subaccount(&self) -> Option<Subaccount> {
55 self.subaccount
56 }
57
58 fn checksum(owner: Principal, subaccount: Subaccount) -> String {
60 let mut hasher = crc32fast::Hasher::new();
61 hasher.update(owner.as_slice());
62 hasher.update(subaccount.as_slice());
63 data_encoding::BASE32_NOPAD
64 .encode(&hasher.finalize().to_be_bytes())
65 .to_ascii_lowercase()
66 }
67
68 #[expect(clippy::cast_possible_truncation)]
69 fn ordering_tag(&self) -> u8 {
70 let mut tag = self.owner.as_slice().len().min(u8::MAX as usize) as u8;
71 if self.subaccount.is_some() {
72 tag |= Self::TAG_SUBACCOUNT;
73 }
74 tag
75 }
76}
77
78impl Display for Account {
79 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
80 let Some(subaccount) = self.subaccount.filter(|value| *value != Subaccount::MIN) else {
81 return Display::fmt(&self.owner, formatter);
82 };
83 let hex = subaccount.to_string();
84 write!(
85 formatter,
86 "{}-{}.{}",
87 self.owner,
88 Self::checksum(self.owner, subaccount),
89 hex.trim_start_matches('0')
90 )
91 }
92}
93
94impl From<Principal> for Account {
95 fn from(owner: Principal) -> Self {
96 Self {
97 owner,
98 subaccount: None,
99 }
100 }
101}
102
103impl FromStr for Account {
104 type Err = String;
105
106 fn from_str(input: &str) -> Result<Self, Self::Err> {
107 if input.len() > Self::MAX_TEXT_LEN || !input.is_ascii() {
108 return Err("invalid account text width or alphabet".into());
109 }
110 let Some((prefix, hex)) = input.split_once('.') else {
111 return input
112 .parse::<Principal>()
113 .map(Self::from)
114 .map_err(|error| error.to_string());
115 };
116 let (owner, checksum) = prefix
117 .rsplit_once('-')
118 .filter(|(_, checksum)| checksum.len() == 7)
119 .ok_or("missing account checksum")?;
120 if hex.is_empty() || hex.len() > 64 || hex.starts_with('0') {
121 return Err("invalid account subaccount width or leading zero".into());
122 }
123 let owner = owner
124 .parse::<Principal>()
125 .map_err(|error| error.to_string())?;
126 let mut padded = [b'0'; 64];
128 padded[64 - hex.len()..].copy_from_slice(hex.as_bytes());
129 let hex = std::str::from_utf8(&padded).map_err(|error| error.to_string())?;
130 let subaccount = Subaccount::from_hex(hex).map_err(|error| error.to_string())?;
131 if checksum != Self::checksum(owner, subaccount) {
132 return Err("invalid account checksum".into());
133 }
134 Ok(Self::from_owner_and_subaccount(owner, Some(subaccount)))
135 }
136}
137
138impl Ord for Account {
139 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
140 self.ordering_tag()
141 .cmp(&other.ordering_tag())
142 .then_with(|| {
143 let mut left = [0; Self::PRINCIPAL_MAX_LEN];
144 let left_bytes = self.owner.as_slice();
145 left[..left_bytes.len()].copy_from_slice(left_bytes);
146 let mut right = [0; Self::PRINCIPAL_MAX_LEN];
147 let right_bytes = other.owner.as_slice();
148 right[..right_bytes.len()].copy_from_slice(right_bytes);
149 left.cmp(&right)
150 })
151 .then_with(|| {
152 self.subaccount
153 .unwrap_or(Subaccount::MIN)
154 .cmp(&other.subaccount.unwrap_or(Subaccount::MIN))
155 })
156 }
157}
158
159impl PartialOrd for Account {
160 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
161 Some(self.cmp(other))
162 }
163}