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, PrincipalError, SchemaContractError, Subaccount};
14
15#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
23#[cfg_attr(not(target_family = "wasm"), derive(ic_wire::Wire))]
24pub struct Account {
25 owner: Principal,
26 subaccount: Option<Subaccount>,
27}
28
29impl Account {
30 const PRINCIPAL_MAX_LEN: usize = Principal::MAX_LENGTH_IN_BYTES as usize;
31 const TAG_SUBACCOUNT: u8 = 0x80;
32
33 pub fn new<P: Into<Principal>, S: Into<Subaccount>>(owner: P, subaccount: Option<S>) -> Self {
35 Self {
36 owner: owner.into(),
37 subaccount: subaccount.map(Into::into),
38 }
39 }
40
41 #[must_use]
43 pub const fn from_owner_and_subaccount(
44 owner: Principal,
45 subaccount: Option<Subaccount>,
46 ) -> Self {
47 Self { owner, subaccount }
48 }
49
50 #[must_use]
52 pub const fn owner(&self) -> Principal {
53 self.owner
54 }
55
56 #[must_use]
58 pub const fn subaccount(&self) -> Option<Subaccount> {
59 self.subaccount
60 }
61
62 #[expect(clippy::cast_possible_truncation)]
63 fn ordering_tag(&self) -> u8 {
64 let mut tag = self.owner.as_slice().len().min(u8::MAX as usize) as u8;
65 if self.subaccount.is_some() {
66 tag |= Self::TAG_SUBACCOUNT;
67 }
68 tag
69 }
70}
71
72impl Display for Account {
73 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
74 Display::fmt(&ic_platform::Account::from(*self), formatter)
75 }
76}
77
78impl From<ic_platform::Account> for Account {
80 fn from(value: ic_platform::Account) -> Self {
81 Self::from_owner_and_subaccount(
82 value.owner.into(),
83 value.subaccount.map(Subaccount::from_array),
84 )
85 }
86}
87
88impl From<Account> for ic_platform::Account {
89 fn from(value: Account) -> Self {
90 Self {
91 owner: value.owner.into(),
92 subaccount: value.subaccount.map(Subaccount::to_bytes),
93 }
94 }
95}
96
97impl From<Principal> for Account {
98 fn from(owner: Principal) -> Self {
99 Self {
100 owner,
101 subaccount: None,
102 }
103 }
104}
105
106impl FromStr for Account {
107 type Err = String;
108
109 fn from_str(input: &str) -> Result<Self, Self::Err> {
110 if input.len() > ic_platform::Account::MAX_LENGTH_IN_TEXT || !input.is_ascii() {
111 return Err("invalid account text width or alphabet".into());
112 }
113 if let Some((prefix, hex)) = input.split_once('.')
117 && prefix
118 .rsplit_once('-')
119 .is_some_and(|(_, checksum)| checksum.len() == 7)
120 && (hex.is_empty() || hex.len() > 64)
121 {
122 return Err("invalid account subaccount width or leading zero".into());
123 }
124 input
125 .parse::<ic_platform::Account>()
126 .map(Self::from)
127 .map_err(|error| match error {
128 ic_platform::AccountError::TextTooLong => {
129 "invalid account text width or alphabet".into()
130 }
131 ic_platform::AccountError::InvalidPrincipal(_) => {
132 PrincipalError::InvalidText.to_string()
133 }
134 ic_platform::AccountError::MissingChecksum => "missing account checksum".into(),
135 ic_platform::AccountError::InvalidChecksum => "invalid account checksum".into(),
136 ic_platform::AccountError::InvalidSubaccount => {
137 SchemaContractError::InvalidLiteral.to_string()
138 }
139 ic_platform::AccountError::LeadingZeroesInSubaccount
140 | ic_platform::AccountError::DefaultSubaccountShouldBeOmitted => {
141 "invalid account subaccount width or leading zero".into()
142 }
143 })
144 }
145}
146
147impl Ord for Account {
148 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
149 self.ordering_tag()
150 .cmp(&other.ordering_tag())
151 .then_with(|| {
152 let mut left = [0; Self::PRINCIPAL_MAX_LEN];
153 let left_bytes = self.owner.as_slice();
154 left[..left_bytes.len()].copy_from_slice(left_bytes);
155 let mut right = [0; Self::PRINCIPAL_MAX_LEN];
156 let right_bytes = other.owner.as_slice();
157 right[..right_bytes.len()].copy_from_slice(right_bytes);
158 left.cmp(&right)
159 })
160 .then_with(|| {
161 self.subaccount
162 .unwrap_or(Subaccount::MIN)
163 .cmp(&other.subaccount.unwrap_or(Subaccount::MIN))
164 })
165 }
166}
167
168impl PartialOrd for Account {
169 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
170 Some(self.cmp(other))
171 }
172}