xand_address/
lib.rs

1//! An address is derived from the public portion of a network entity's keypair.
2//! They are commonly used on a blockchain to convey ownership of assets or
3//! the issuer of a transaction.
4//!
5//! Currently, this means an SS58-check address as specified by substrate here:
6//! https://github.com/paritytech/substrate/wiki/External-Address-Format-(SS58)
7
8use blake2::{Blake2b, Digest};
9use serde::{Deserialize, Serialize};
10use std::convert::{TryFrom, TryInto};
11use std::fmt::{Display, Error, Formatter};
12use std::str::FromStr;
13use thiserror::Error;
14
15#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize)]
16pub struct Address(String);
17
18impl Display for Address {
19    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
20        Display::fmt(&self.0, f)
21    }
22}
23
24impl TryFrom<String> for Address {
25    type Error = AddressError;
26
27    fn try_from(value: String) -> Result<Self, Self::Error> {
28        let bytes = bs58::decode(&value).into_vec()?;
29        if bytes.len() != 35 {
30            return Err(AddressError::InvalidSs58Length { len: bytes.len() });
31        }
32        let hash = ss58hash(&bytes[..33]);
33        if bytes[33..] != hash.as_ref()[0..2] {
34            return Err(AddressError::InvalidSs58Checksum);
35        }
36        Ok(Address(value))
37    }
38}
39
40impl FromStr for Address {
41    type Err = AddressError;
42
43    fn from_str(s: &str) -> Result<Self, Self::Err> {
44        s.to_owned().try_into()
45    }
46}
47
48impl<'de> Deserialize<'de> for Address {
49    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
50    where
51        D: serde::Deserializer<'de>,
52    {
53        Address::try_from(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
54    }
55}
56
57const PREFIX: &[u8] = b"SS58PRE";
58/// Hashes a byte slice using blake2 in accordance with the ss58 spec
59/// https://github.com/paritytech/substrate/wiki/External-Address-Format-(SS58)
60///
61/// This is re-implemented to enable usage without depending on substrate
62fn ss58hash(data: &[u8]) -> impl AsRef<[u8]> {
63    let mut context = Blake2b::new();
64    context.input(PREFIX);
65    context.input(data);
66    context.result()
67}
68
69#[derive(Clone, Debug, Eq, Error, PartialEq, Serialize)]
70pub enum AddressError {
71    #[error("Address did not have a valid checksum")]
72    InvalidSs58Checksum,
73    #[error("Expected 35 bytes in the address, but found {len}")]
74    InvalidSs58Length { len: usize },
75    #[error("Address wasn't proper b58: {source:?}")]
76    NotBase58 {
77        #[serde(serialize_with = "xand_utils::snafu_extensions::debug_serialize")]
78        #[from]
79        source: bs58::decode::Error,
80    },
81}
82
83#[cfg(test)]
84mod test {
85    use super::*;
86    use proptest::proptest;
87    use sp_core::{crypto::Ss58Codec, ed25519, sr25519, Pair};
88
89    proptest! {
90        #[test]
91        fn test_address_validation(pubkey_bytes: [u8; 32]) {
92            let sr_key = sr25519::Public::from_raw(pubkey_bytes);
93            let ed_key = ed25519::Public::from_raw(pubkey_bytes);
94            let sr_str = sr_key.to_ss58check();
95            let ed_str = ed_key.to_ss58check();
96            // Both of these should never fail since they are produced *by* the substrate lib.
97            Address::try_from(sr_str).unwrap();
98            Address::try_from(ed_str).unwrap();
99        }
100    }
101
102    #[test]
103    fn test_bad_address_checksum_fails_validation() {
104        let sr_key = sr25519::Pair::from_seed(&[42; 32]);
105        let mut as_ss58 = sr_key.public().to_ss58check();
106        // Mess with checksum bytes
107        as_ss58.pop();
108        as_ss58.push('s');
109        assert_eq!(
110            as_ss58.parse::<Address>().unwrap_err(),
111            AddressError::InvalidSs58Checksum
112        );
113    }
114
115    #[test]
116    fn test_bad_address_length_fails_validation() {
117        let sr_key = sr25519::Pair::from_seed(&[42; 32]);
118        let mut as_ss58 = sr_key.public().to_ss58check();
119        // Drop a char
120        as_ss58.pop();
121        assert_eq!(
122            Address::try_from(as_ss58.clone()).unwrap_err(),
123            AddressError::InvalidSs58Length { len: 34 }
124        );
125        // Make it too long
126        as_ss58.push('s');
127        as_ss58.push('s');
128        assert_eq!(
129            Address::try_from(as_ss58).unwrap_err(),
130            AddressError::InvalidSs58Length { len: 36 }
131        );
132    }
133
134    #[test]
135    fn deserializing_invalid_address_fails() {
136        let address = serde_json::from_str::<Address>(r#""""#);
137        assert!(address.is_err());
138    }
139}