subxt_core/utils/
account_id20.rs

1// Copyright 2019-2024 Parity Technologies (UK) Ltd.
2// This file is dual-licensed as Apache-2.0 or GPL-3.0.
3// see LICENSE for license details.
4
5//! `AccountId20` is a representation of Ethereum address derived from hashing the public key.
6
7use alloc::format;
8use alloc::string::String;
9use codec::{Decode, Encode};
10use keccak_hash::keccak;
11use serde::{Deserialize, Serialize};
12use thiserror::Error as DeriveError;
13
14#[derive(
15    Copy,
16    Clone,
17    Eq,
18    PartialEq,
19    Ord,
20    PartialOrd,
21    Encode,
22    Decode,
23    Debug,
24    scale_encode::EncodeAsType,
25    scale_decode::DecodeAsType,
26    scale_info::TypeInfo,
27)]
28/// Ethereum-compatible `AccountId`.
29pub struct AccountId20(pub [u8; 20]);
30
31impl AsRef<[u8]> for AccountId20 {
32    fn as_ref(&self) -> &[u8] {
33        &self.0[..]
34    }
35}
36
37impl AsRef<[u8; 20]> for AccountId20 {
38    fn as_ref(&self) -> &[u8; 20] {
39        &self.0
40    }
41}
42
43impl From<[u8; 20]> for AccountId20 {
44    fn from(x: [u8; 20]) -> Self {
45        AccountId20(x)
46    }
47}
48
49impl AccountId20 {
50    /// Convert to a public key hash
51    pub fn checksum(&self) -> String {
52        let hex_address = hex::encode(self.0);
53        let hash = keccak(hex_address.as_bytes());
54
55        let mut checksum_address = String::with_capacity(42);
56        checksum_address.push_str("0x");
57
58        for (i, ch) in hex_address.chars().enumerate() {
59            // Get the corresponding nibble from the hash
60            let nibble = (hash[i / 2] >> (if i % 2 == 0 { 4 } else { 0 })) & 0xf;
61
62            if nibble >= 8 {
63                checksum_address.push(ch.to_ascii_uppercase());
64            } else {
65                checksum_address.push(ch);
66            }
67        }
68
69        checksum_address
70    }
71}
72
73/// An error obtained from trying to interpret a hex encoded string into an AccountId20
74#[derive(Clone, Copy, Eq, PartialEq, Debug, DeriveError)]
75#[allow(missing_docs)]
76pub enum FromChecksumError {
77    #[error("Length is bad")]
78    BadLength,
79    #[error("Invalid checksum")]
80    InvalidChecksum,
81    #[error("Invalid checksum prefix byte.")]
82    InvalidPrefix,
83}
84
85impl Serialize for AccountId20 {
86    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
87    where
88        S: serde::Serializer,
89    {
90        serializer.serialize_str(&self.checksum())
91    }
92}
93
94impl<'de> Deserialize<'de> for AccountId20 {
95    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
96    where
97        D: serde::Deserializer<'de>,
98    {
99        String::deserialize(deserializer)?
100            .parse::<AccountId20>()
101            .map_err(|e| serde::de::Error::custom(format!("{e:?}")))
102    }
103}
104
105impl core::fmt::Display for AccountId20 {
106    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
107        write!(f, "{}", self.checksum())
108    }
109}
110
111impl core::str::FromStr for AccountId20 {
112    type Err = FromChecksumError;
113    fn from_str(s: &str) -> Result<Self, Self::Err> {
114        if s.len() != 42 {
115            return Err(FromChecksumError::BadLength);
116        }
117        if !s.starts_with("0x") {
118            return Err(FromChecksumError::InvalidPrefix);
119        }
120        hex::decode(&s.as_bytes()[2..])
121            .map_err(|_| FromChecksumError::InvalidChecksum)?
122            .try_into()
123            .map(AccountId20)
124            .map_err(|_| FromChecksumError::BadLength)
125    }
126}
127
128#[cfg(test)]
129mod test {
130    use super::*;
131
132    #[test]
133    fn deserialisation() {
134        let key_hashes = vec![
135            "0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac",
136            "0x3Cd0A705a2DC65e5b1E1205896BaA2be8A07c6e0",
137            "0x798d4Ba9baf0064Ec19eB4F0a1a45785ae9D6DFc",
138            "0x773539d4Ac0e786233D90A233654ccEE26a613D9",
139            "0xFf64d3F6efE2317EE2807d223a0Bdc4c0c49dfDB",
140            "0xC0F0f4ab324C46e55D02D0033343B4Be8A55532d",
141        ];
142
143        for key_hash in key_hashes {
144            let parsed: AccountId20 = key_hash.parse().expect("Failed to parse");
145
146            let encoded = parsed.checksum();
147
148            // `encoded` should be equal to the initial key_hash
149            assert_eq!(encoded, key_hash);
150        }
151    }
152}