layer_climb_address/
address.rs

1use crate::PublicKey;
2use anyhow::{anyhow, bail, Context, Result};
3use layer_climb_config::AddrKind;
4use serde::{Deserialize, Serialize};
5use std::{hash::Hash, str::FromStr};
6use subtle_encoding::bech32;
7
8/// The canonical type used everywhere for addresses
9/// Display is implemented as plain string
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11pub enum Address {
12    Cosmos {
13        bech32_addr: String,
14        // prefix is the first part of the bech32 address
15        prefix_len: usize,
16    },
17    Evm(AddrEvm),
18}
19
20impl Hash for Address {
21    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
22        match self {
23            Address::Cosmos { .. } => {
24                1u32.hash(state);
25            }
26            Address::Evm(_) => {
27                2u32.hash(state);
28            }
29        }
30        self.to_string().hash(state);
31    }
32}
33
34impl Address {
35    // used internally for validation across both ways of creating addresses:
36    // 1. parsing from a string
37    // 2. creating from a public key
38    fn new_cosmos(bytes: Vec<u8>, prefix: &str) -> Result<Self> {
39        if !prefix.chars().all(|c| matches!(c, 'a'..='z' | '0'..='9')) {
40            bail!("expected prefix to be lowercase alphanumeric characters only");
41        }
42
43        if bytes.len() > 255 {
44            bail!(
45                "account ID should be at most 255 bytes long, but was {} bytes long",
46                bytes.len()
47            );
48        }
49
50        let bech32_addr = bech32::encode(prefix, bytes);
51
52        Ok(Self::Cosmos {
53            bech32_addr,
54            prefix_len: prefix.len(),
55        })
56    }
57    // if the prefix is supplied, this will attempt to validate the address against the prefix to ensure they match
58    // if you just have a public key, use new_cosmos_pub_key instead
59    pub fn new_cosmos_string(value: &str, prefix: Option<&str>) -> Result<Self> {
60        let (decoded_prefix, decoded_bytes) = if value.starts_with(|c: char| c.is_uppercase()) {
61            bech32::decode_upper(value)
62        } else {
63            bech32::decode(value)
64        }
65        .context(format!("invalid bech32: '{}'", value))?;
66
67        if matches!(prefix, Some(prefix) if prefix != decoded_prefix) {
68            bail!(
69                "Address prefix \"{}\" does not match expected prefix \"{}\"",
70                decoded_prefix,
71                prefix.unwrap()
72            );
73        }
74
75        Self::new_cosmos(decoded_bytes, &decoded_prefix)
76    }
77
78    pub fn as_bytes(&self) -> Vec<u8> {
79        match self {
80            Address::Cosmos { bech32_addr, .. } => {
81                let (_, bytes) = bech32::decode(bech32_addr).unwrap();
82                bytes
83            }
84            Address::Evm(addr_evm) => addr_evm.as_bytes().to_vec(),
85        }
86    }
87
88    /// if you just have a string address, use new_cosmos_string instead
89    pub fn new_cosmos_pub_key(pub_key: &PublicKey, prefix: &str) -> Result<Self> {
90        match pub_key {
91            PublicKey::Secp256k1(encoded_point) => {
92                let id = tendermint::account::Id::from(*encoded_point);
93                Self::new_cosmos(id.as_bytes().to_vec(), prefix)
94            }
95            _ => Err(anyhow!(
96                "Invalid public key type, currently only supports secp256k1"
97            )),
98        }
99    }
100
101    pub fn cosmos_prefix(&self) -> Result<&str> {
102        match self {
103            Address::Cosmos {
104                prefix_len,
105                bech32_addr,
106            } => Ok(&bech32_addr[..*prefix_len]),
107            Address::Evm(_) => Err(anyhow!("Address is not cosmos")),
108        }
109    }
110
111    pub fn new_evm_string(value: &str) -> Result<Self> {
112        let addr_evm: AddrEvm = value.parse()?;
113        Ok(Self::Evm(addr_evm))
114    }
115
116    /// if you just have a string address, use parse_evm instead
117    pub fn new_evm_pub_key(_pub_key: &PublicKey) -> Result<Self> {
118        bail!("TODO - implement evm address from public key");
119    }
120
121    pub fn into_cosmos(&self, new_prefix: &str) -> Result<Self> {
122        match self {
123            Address::Cosmos { bech32_addr, .. } => {
124                if self.cosmos_prefix()? == new_prefix {
125                    Ok(self.clone())
126                } else {
127                    Self::new_cosmos_string(bech32_addr, Some(new_prefix))
128                }
129            }
130            Address::Evm(_) => {
131                bail!("TODO - implement evm to cosmos addr");
132            }
133        }
134    }
135
136    pub fn into_evm(&self) -> Result<Self> {
137        match self {
138            Address::Evm(_) => Ok(self.clone()),
139            Address::Cosmos { .. } => {
140                bail!("TODO - implement cosmos to evm addr");
141            }
142        }
143    }
144
145    pub fn try_from_str(value: &str, addr_kind: &AddrKind) -> Result<Self> {
146        match addr_kind {
147            AddrKind::Cosmos { prefix } => Self::new_cosmos_string(value, Some(prefix)),
148            AddrKind::Evm => Self::new_evm_string(value),
149        }
150    }
151
152    pub fn try_from_pub_key(pub_key: &PublicKey, addr_kind: &AddrKind) -> Result<Address> {
153        match addr_kind {
154            AddrKind::Cosmos { prefix } => Address::new_cosmos_pub_key(pub_key, prefix),
155            AddrKind::Evm => Address::new_evm_pub_key(pub_key),
156        }
157    }
158}
159
160// the display impl ignores the kind
161impl std::fmt::Display for Address {
162    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163        match self {
164            Self::Cosmos { bech32_addr, .. } => {
165                write!(f, "{}", bech32_addr)
166            }
167            Self::Evm(addr_evm) => {
168                write!(f, "{}", addr_evm)
169            }
170        }
171    }
172}
173
174impl From<alloy_primitives::Address> for Address {
175    fn from(addr: alloy_primitives::Address) -> Self {
176        Self::Evm(addr.into())
177    }
178}
179
180impl TryFrom<Address> for alloy_primitives::Address {
181    type Error = anyhow::Error;
182
183    fn try_from(addr: Address) -> Result<Self> {
184        match addr {
185            Address::Evm(addr_evm) => Ok(addr_evm.into()),
186            Address::Cosmos { .. } => Err(anyhow!("Expected EVM address, got Cosmos")),
187        }
188    }
189}
190
191///// EVM address
192#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
193#[serde(transparent)]
194pub struct AddrEvm([u8; 20]);
195
196impl AddrEvm {
197    pub fn new(bytes: [u8; 20]) -> Self {
198        Self(bytes)
199    }
200
201    pub fn new_vec(bytes: Vec<u8>) -> Result<Self> {
202        if bytes.len() != 20 {
203            bail!("Invalid length for EVM address");
204        }
205        let mut arr = [0u8; 20];
206        arr.copy_from_slice(&bytes);
207        Ok(Self(arr))
208    }
209
210    pub fn as_bytes(&self) -> [u8; 20] {
211        self.0
212    }
213}
214
215impl std::fmt::Display for AddrEvm {
216    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217        write!(f, "0x{}", hex::encode(self.0))
218    }
219}
220
221impl FromStr for AddrEvm {
222    type Err = anyhow::Error;
223
224    fn from_str(s: &str) -> Result<Self> {
225        let s = s.trim();
226        if s.len() != 42 {
227            bail!("Invalid length for EVM address");
228        }
229        if !s.starts_with("0x") {
230            bail!("Invalid prefix for EVM address");
231        }
232        let bytes = hex::decode(&s[2..])?;
233        Self::new_vec(bytes)
234    }
235}
236
237impl TryFrom<Address> for AddrEvm {
238    type Error = anyhow::Error;
239
240    fn try_from(addr: Address) -> Result<Self> {
241        match addr {
242            Address::Evm(addr_evm) => Ok(addr_evm),
243            Address::Cosmos { .. } => bail!("Address must be EVM - use into_evm() instead"),
244        }
245    }
246}
247
248impl From<AddrEvm> for Address {
249    fn from(addr: AddrEvm) -> Self {
250        Self::Evm(addr)
251    }
252}
253
254impl From<alloy_primitives::Address> for AddrEvm {
255    fn from(addr: alloy_primitives::Address) -> Self {
256        Self(**addr)
257    }
258}
259
260impl From<AddrEvm> for alloy_primitives::Address {
261    fn from(addr: AddrEvm) -> Self {
262        alloy_primitives::Address::new(addr.0)
263    }
264}
265
266#[cfg(test)]
267mod test {
268    use super::{AddrEvm, Address};
269
270    // TODO get addresses that are actually the same underlying public key
271
272    const TEST_COSMOS_STR: &str = "osmo1h5qke5tzc0fgz93wcxg8da2en3advfect0gh4a";
273    const TEST_COSMOS_PREFIX: &str = "osmo";
274    const TEST_EVM_STR: &str = "0xb794f5ea0ba39494ce839613fffba74279579268";
275
276    #[test]
277    fn test_basic_roundtrip_evm() {
278        let test_string = TEST_EVM_STR;
279        let addr_evm: AddrEvm = test_string.parse().unwrap();
280        let addr: Address = addr_evm.into();
281
282        assert_eq!(addr.to_string(), test_string);
283
284        let addr_evm_2: AddrEvm = addr.try_into().unwrap();
285        assert_eq!(addr_evm_2, addr_evm);
286    }
287
288    #[test]
289    fn test_basic_roundtrip_cosmos() {
290        let test_string = TEST_COSMOS_STR;
291        let test_prefix = TEST_COSMOS_PREFIX;
292        let addr = Address::new_cosmos_string(test_string, None).unwrap();
293
294        assert_eq!(addr.to_string(), test_string);
295        assert_eq!(addr.cosmos_prefix().unwrap(), test_prefix);
296    }
297
298    #[test]
299    fn test_convert_evm_to_cosmos() {
300        // let test_string = "0xb794f5ea0ba39494ce839613fffba74279579268";
301        // let addr_bytes:AddrEvm = test_string.try_into().unwrap();
302        // let addr_string:AddrString = (&addr_bytes).into();
303        // let addr_string_cosmos = addr_string.convert_into_cosmos("osmo".to_string()).unwrap();
304        // assert_eq!(addr_string_cosmos.to_string(), "osmo1suhgf5svhu4usrurvxzlgn54ksxmn8gljarjtxqnapv8kjnp4nrsll0sqv");
305    }
306
307    #[test]
308    fn test_convert_cosmos_to_evm() {
309        // let test_string = "osmo1suhgf5svhu4usrurvxzlgn54ksxmn8gljarjtxqnapv8kjnp4nrsll0sqv";
310        // let account_id:AccountId = test_string.parse().unwrap();
311        // let addr_string:AddrString = (&account_id).try_into().unwrap();
312        // let addr_string_evm = addr_string.convert_into_evm().unwrap();
313        // assert_eq!(addr_string_evm.to_string(), "0xb794f5ea0ba39494ce839613fffba74279579268");
314    }
315}