canic_cdk/types/
account.rs1use crate::icrc_ledger_types::icrc1::account::Account as IcrcAccount;
2use candid::{CandidType, Principal};
3use serde::{Deserialize, Serialize};
4use std::{
5 cmp::Ordering,
6 fmt::{self, Display},
7 hash::{Hash, Hasher},
8 str::FromStr,
9};
10
11pub type Subaccount = [u8; 32];
16
17pub const DEFAULT_SUBACCOUNT: &Subaccount = &[0; 32];
18
19#[derive(CandidType, Clone, Copy, Debug, Deserialize, Serialize)]
27pub struct Account {
28 pub owner: Principal,
29 pub subaccount: Option<Subaccount>,
30}
31
32impl Account {
33 pub fn new<P: Into<Principal>, S: Into<Subaccount>>(owner: P, subaccount: Option<S>) -> Self {
34 Self {
35 owner: owner.into(),
36 subaccount: subaccount.map(Into::into),
37 }
38 }
39
40 #[inline]
43 #[must_use]
44 pub fn effective_subaccount(&self) -> &Subaccount {
45 self.subaccount.as_ref().unwrap_or(DEFAULT_SUBACCOUNT)
46 }
47}
48
49impl Display for Account {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 match &self.subaccount {
53 None => write!(f, "{}", self.owner),
54 Some(subaccount) if subaccount == &[0; 32] => write!(f, "{}", self.owner),
55 Some(subaccount) => {
56 let checksum = full_account_checksum(self.owner.as_slice(), subaccount.as_slice());
57 let hex_subaccount = hex::encode(subaccount.as_slice());
58 let hex_subaccount = hex_subaccount.trim_start_matches('0');
59 write!(f, "{}-{}.{}", self.owner, checksum, hex_subaccount)
60 }
61 }
62 }
63}
64
65impl Eq for Account {}
66
67impl FromStr for Account {
68 type Err = String;
69
70 fn from_str(s: &str) -> Result<Self, Self::Err> {
71 let acc = IcrcAccount::from_str(s).map_err(|e| e.to_string())?;
72
73 Ok(Self::new(acc.owner, acc.subaccount))
74 }
75}
76
77impl PartialEq for Account {
78 fn eq(&self, other: &Self) -> bool {
79 self.owner == other.owner && self.effective_subaccount() == other.effective_subaccount()
80 }
81}
82
83impl From<Principal> for Account {
84 fn from(owner: Principal) -> Self {
85 Self {
86 owner,
87 subaccount: None,
88 }
89 }
90}
91
92impl Hash for Account {
93 fn hash<H: Hasher>(&self, state: &mut H) {
94 self.owner.hash(state);
95 self.effective_subaccount().hash(state);
96 }
97}
98
99impl Ord for Account {
100 fn cmp(&self, other: &Self) -> Ordering {
101 self.owner.cmp(&other.owner).then_with(|| {
102 self.effective_subaccount()
103 .cmp(other.effective_subaccount())
104 })
105 }
106}
107
108impl PartialOrd for Account {
109 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
110 Some(self.cmp(other))
111 }
112}
113
114fn full_account_checksum(owner: &[u8], subaccount: &[u8]) -> String {
116 let mut crc32hasher = crc32fast::Hasher::new();
117 crc32hasher.update(owner);
118 crc32hasher.update(subaccount);
119 let checksum = crc32hasher.finalize().to_be_bytes();
120
121 base32::encode(base32::Alphabet::Rfc4648 { padding: false }, &checksum).to_lowercase()
122}
123
124#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn subaccount_checksum_matches_reference() {
134 let owner = [0x01; 29];
137 let subaccount = [0x02; 32];
138
139 let checksum = full_account_checksum(owner.as_slice(), subaccount.as_slice());
140 assert_eq!(checksum, "izgikni");
141 }
142}