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