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 #[must_use]
43 pub fn effective_subaccount(&self) -> &Subaccount {
44 self.subaccount.as_ref().unwrap_or(DEFAULT_SUBACCOUNT)
45 }
46}
47
48impl Display for Account {
49 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 match &self.subaccount {
52 None => write!(f, "{}", self.owner),
53 Some(subaccount) if subaccount == &[0; 32] => write!(f, "{}", self.owner),
54 Some(subaccount) => {
55 let checksum = full_account_checksum(self.owner.as_slice(), subaccount.as_slice());
56 let hex_subaccount = hex::encode(subaccount.as_slice());
57 let hex_subaccount = hex_subaccount.trim_start_matches('0');
58 write!(f, "{}-{}.{}", self.owner, checksum, hex_subaccount)
59 }
60 }
61 }
62}
63
64impl Eq for Account {}
65
66impl FromStr for Account {
67 type Err = String;
68
69 fn from_str(s: &str) -> Result<Self, Self::Err> {
70 let acc = IcrcAccount::from_str(s).map_err(|e| e.to_string())?;
71
72 Ok(Self::new(acc.owner, acc.subaccount))
73 }
74}
75
76impl PartialEq for Account {
77 fn eq(&self, other: &Self) -> bool {
78 self.owner == other.owner && self.effective_subaccount() == other.effective_subaccount()
79 }
80}
81
82impl From<Principal> for Account {
83 fn from(owner: Principal) -> Self {
84 Self {
85 owner,
86 subaccount: None,
87 }
88 }
89}
90
91impl Hash for Account {
92 fn hash<H: Hasher>(&self, state: &mut H) {
93 self.owner.hash(state);
94 self.effective_subaccount().hash(state);
95 }
96}
97
98impl Ord for Account {
99 fn cmp(&self, other: &Self) -> Ordering {
100 self.owner.cmp(&other.owner).then_with(|| {
101 self.effective_subaccount()
102 .cmp(other.effective_subaccount())
103 })
104 }
105}
106
107impl PartialOrd for Account {
108 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
109 Some(self.cmp(other))
110 }
111}
112
113fn full_account_checksum(owner: &[u8], subaccount: &[u8]) -> String {
115 let mut crc32hasher = crc32fast::Hasher::new();
116 crc32hasher.update(owner);
117 crc32hasher.update(subaccount);
118 let checksum = crc32hasher.finalize().to_be_bytes();
119
120 base32::encode(base32::Alphabet::Rfc4648 { padding: false }, &checksum).to_lowercase()
121}
122
123#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn subaccount_checksum_matches_reference() {
133 let owner = [0x01; 29];
136 let subaccount = [0x02; 32];
137
138 let checksum = full_account_checksum(owner.as_slice(), subaccount.as_slice());
139 assert_eq!(checksum, "izgikni");
140 }
141}