subxt_core/utils/
account_id.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//! The "default" Substrate/Polkadot AccountId. This is used in codegen, as well as signing related bits.
6//! This doesn't contain much functionality itself, but is easy to convert to/from an `sp_core::AccountId32`
7//! for instance, to gain functionality without forcing a dependency on Substrate crates here.
8
9use alloc::format;
10use alloc::string::String;
11use alloc::vec;
12use alloc::vec::Vec;
13use codec::{Decode, Encode};
14use serde::{Deserialize, Serialize};
15use thiserror::Error as DeriveError;
16
17/// A 32-byte cryptographic identifier. This is a simplified version of Substrate's
18/// `sp_core::crypto::AccountId32`. To obtain more functionality, convert this into
19/// that type.
20#[derive(
21    Clone,
22    Eq,
23    PartialEq,
24    Ord,
25    PartialOrd,
26    Encode,
27    Decode,
28    Debug,
29    scale_encode::EncodeAsType,
30    scale_decode::DecodeAsType,
31    scale_info::TypeInfo,
32)]
33pub struct AccountId32(pub [u8; 32]);
34
35impl AsRef<[u8]> for AccountId32 {
36    fn as_ref(&self) -> &[u8] {
37        &self.0[..]
38    }
39}
40
41impl AsRef<[u8; 32]> for AccountId32 {
42    fn as_ref(&self) -> &[u8; 32] {
43        &self.0
44    }
45}
46
47impl From<[u8; 32]> for AccountId32 {
48    fn from(x: [u8; 32]) -> Self {
49        AccountId32(x)
50    }
51}
52
53impl AccountId32 {
54    // Return the ss58-check string for this key. Adapted from `sp_core::crypto`. We need this to
55    // serialize our account appropriately but otherwise don't care.
56    fn to_ss58check(&self) -> String {
57        // For serializing to a string to obtain the account nonce, we use the default substrate
58        // prefix (since we have no way to otherwise pick one). It doesn't really matter, since when
59        // it's deserialized back in system_accountNextIndex, we ignore this (so long as it's valid).
60        const SUBSTRATE_SS58_PREFIX: u8 = 42;
61        // prefix <= 63 just take up one byte at the start:
62        let mut v = vec![SUBSTRATE_SS58_PREFIX];
63        // then push the account ID bytes.
64        v.extend(self.0);
65        // then push a 2 byte checksum of what we have so far.
66        let r = ss58hash(&v);
67        v.extend(&r[0..2]);
68        // then encode to base58.
69        use base58::ToBase58;
70        v.to_base58()
71    }
72
73    // This isn't strictly needed, but to give our AccountId32 a little more usefulness, we also
74    // implement the logic needed to decode an AccountId32 from an SS58 encoded string. This is exposed
75    // via a `FromStr` impl.
76    fn from_ss58check(s: &str) -> Result<Self, FromSs58Error> {
77        const CHECKSUM_LEN: usize = 2;
78        let body_len = 32;
79
80        use base58::FromBase58;
81        let data = s.from_base58().map_err(|_| FromSs58Error::BadBase58)?;
82        if data.len() < 2 {
83            return Err(FromSs58Error::BadLength);
84        }
85        let prefix_len = match data[0] {
86            0..=63 => 1,
87            64..=127 => 2,
88            _ => return Err(FromSs58Error::InvalidPrefix),
89        };
90        if data.len() != prefix_len + body_len + CHECKSUM_LEN {
91            return Err(FromSs58Error::BadLength);
92        }
93        let hash = ss58hash(&data[0..body_len + prefix_len]);
94        let checksum = &hash[0..CHECKSUM_LEN];
95        if data[body_len + prefix_len..body_len + prefix_len + CHECKSUM_LEN] != *checksum {
96            // Invalid checksum.
97            return Err(FromSs58Error::InvalidChecksum);
98        }
99
100        let result = data[prefix_len..body_len + prefix_len]
101            .try_into()
102            .map_err(|_| FromSs58Error::BadLength)?;
103        Ok(AccountId32(result))
104    }
105}
106
107/// An error obtained from trying to interpret an SS58 encoded string into an AccountId32
108#[derive(Clone, Copy, Eq, PartialEq, Debug, DeriveError)]
109#[allow(missing_docs)]
110pub enum FromSs58Error {
111    #[error("Base 58 requirement is violated")]
112    BadBase58,
113    #[error("Length is bad")]
114    BadLength,
115    #[error("Invalid checksum")]
116    InvalidChecksum,
117    #[error("Invalid SS58 prefix byte.")]
118    InvalidPrefix,
119}
120
121// We do this just to get a checksum to help verify the validity of the address in to_ss58check
122fn ss58hash(data: &[u8]) -> Vec<u8> {
123    use blake2::{Blake2b512, Digest};
124    const PREFIX: &[u8] = b"SS58PRE";
125    let mut ctx = Blake2b512::new();
126    ctx.update(PREFIX);
127    ctx.update(data);
128    ctx.finalize().to_vec()
129}
130
131impl Serialize for AccountId32 {
132    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
133    where
134        S: serde::Serializer,
135    {
136        serializer.serialize_str(&self.to_ss58check())
137    }
138}
139
140impl<'de> Deserialize<'de> for AccountId32 {
141    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
142    where
143        D: serde::Deserializer<'de>,
144    {
145        AccountId32::from_ss58check(&String::deserialize(deserializer)?)
146            .map_err(|e| serde::de::Error::custom(format!("{e:?}")))
147    }
148}
149
150impl core::fmt::Display for AccountId32 {
151    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
152        write!(f, "{}", self.to_ss58check())
153    }
154}
155
156impl core::str::FromStr for AccountId32 {
157    type Err = FromSs58Error;
158    fn from_str(s: &str) -> Result<Self, Self::Err> {
159        AccountId32::from_ss58check(s)
160    }
161}
162
163#[cfg(test)]
164mod test {
165    use super::*;
166    use sp_core::{self, crypto::Ss58Codec};
167    use sp_keyring::AccountKeyring;
168
169    #[test]
170    fn ss58_is_compatible_with_substrate_impl() {
171        let keyrings = vec![
172            AccountKeyring::Alice,
173            AccountKeyring::Bob,
174            AccountKeyring::Charlie,
175        ];
176
177        for keyring in keyrings {
178            let substrate_account = keyring.to_account_id();
179            let local_account = AccountId32(substrate_account.clone().into());
180
181            // Both should encode to ss58 the same way:
182            let substrate_ss58 = substrate_account.to_ss58check();
183            assert_eq!(substrate_ss58, local_account.to_ss58check());
184
185            // Both should decode from ss58 back to the same:
186            assert_eq!(
187                sp_core::crypto::AccountId32::from_ss58check(&substrate_ss58).unwrap(),
188                substrate_account
189            );
190            assert_eq!(
191                AccountId32::from_ss58check(&substrate_ss58).unwrap(),
192                local_account
193            );
194        }
195    }
196}