canic_cdk/types/
account.rs

1use 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
11///
12/// Subaccount
13///
14
15pub type Subaccount = [u8; 32];
16
17pub const DEFAULT_SUBACCOUNT: &Subaccount = &[0; 32];
18
19///
20/// Account
21///
22/// Code ported from icrc-ledger-types as we don't want to include that one, it's out of
23/// date and has a lot of extra dependencies
24///
25
26#[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    /// The effective subaccount of an account - the subaccount if it is set, otherwise the default
41    /// subaccount of all zeroes.
42    #[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        // https://github.com/dfinity/ICRC-1/blob/main/standards/ICRC-1/TextualEncoding.md#textual-encoding-of-icrc-1-accounts
52        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
114// make your internal code public, dfinity!
115fn 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///
125/// TESTS
126///
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn subaccount_checksum_matches_reference() {
134        // Values lifted from icrc-ledger-types, base32(crc32(owner + subaccount)) of
135        // 0x01 bytes (owner) plus 0x02 bytes (subaccount).
136        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}