layer_climb_address/
address.rs

1use anyhow::{anyhow, bail, Context, Result};
2use cosmwasm_schema::{cw_schema, cw_serde};
3use std::{borrow::Cow, hash::Hash, str::FromStr};
4use subtle_encoding::bech32;
5
6/// The canonical type used everywhere for addresses
7/// Display is implemented as plain string
8#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
9#[derive(Eq)]
10#[cw_serde]
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 let Some(prefix) = prefix {
68            if decoded_prefix != prefix {
69                bail!(
70                    "Address prefix \"{}\" does not match expected prefix \"{}\"",
71                    decoded_prefix,
72                    prefix
73                );
74            }
75        }
76
77        Self::new_cosmos(decoded_bytes, &decoded_prefix)
78    }
79
80    pub fn as_bytes(&self) -> Vec<u8> {
81        match self {
82            Address::Cosmos { bech32_addr, .. } => {
83                let (_, bytes) = bech32::decode(bech32_addr).unwrap();
84                bytes
85            }
86            Address::Evm(addr_evm) => addr_evm.as_bytes().to_vec(),
87        }
88    }
89
90    /// if you just have a string address, use new_cosmos_string instead
91    pub fn new_cosmos_pub_key(pub_key: &tendermint::PublicKey, prefix: &str) -> Result<Self> {
92        match pub_key {
93            tendermint::PublicKey::Secp256k1(encoded_point) => {
94                let id = tendermint::account::Id::from(*encoded_point);
95                Self::new_cosmos(id.as_bytes().to_vec(), prefix)
96            }
97            _ => Err(anyhow!(
98                "Invalid public key type, currently only supports secp256k1"
99            )),
100        }
101    }
102
103    pub fn cosmos_prefix(&self) -> Result<&str> {
104        match self {
105            Address::Cosmos {
106                prefix_len,
107                bech32_addr,
108            } => Ok(&bech32_addr[..*prefix_len]),
109            Address::Evm(_) => Err(anyhow!("Address is not cosmos")),
110        }
111    }
112
113    pub fn new_evm_string(value: &str) -> Result<Self> {
114        let addr_evm: AddrEvm = value.parse()?;
115        Ok(Self::Evm(addr_evm))
116    }
117
118    /// if you just have a string address, use parse_evm instead
119    pub fn new_evm_pub_key(_pub_key: &tendermint::PublicKey) -> Result<Self> {
120        bail!("TODO - implement evm address from public key");
121    }
122
123    pub fn into_cosmos(&self, new_prefix: &str) -> Result<Self> {
124        match self {
125            Address::Cosmos { bech32_addr, .. } => {
126                if self.cosmos_prefix()? == new_prefix {
127                    Ok(self.clone())
128                } else {
129                    Self::new_cosmos_string(bech32_addr, Some(new_prefix))
130                }
131            }
132            Address::Evm(_) => {
133                bail!("TODO - implement evm to cosmos addr");
134            }
135        }
136    }
137
138    pub fn into_evm(&self) -> Result<Self> {
139        match self {
140            Address::Evm(_) => Ok(self.clone()),
141            Address::Cosmos { .. } => {
142                bail!("TODO - implement cosmos to evm addr");
143            }
144        }
145    }
146
147    pub fn try_from_str(value: &str, addr_kind: &AddrKind) -> Result<Self> {
148        match addr_kind {
149            AddrKind::Cosmos { prefix } => Self::new_cosmos_string(value, Some(prefix)),
150            AddrKind::Evm => Self::new_evm_string(value),
151        }
152    }
153
154    pub fn try_from_pub_key(
155        pub_key: &tendermint::PublicKey,
156        addr_kind: &AddrKind,
157    ) -> Result<Address> {
158        match addr_kind {
159            AddrKind::Cosmos { prefix } => Address::new_cosmos_pub_key(pub_key, prefix),
160            AddrKind::Evm => Address::new_evm_pub_key(pub_key),
161        }
162    }
163}
164
165// the display impl ignores the kind
166impl std::fmt::Display for Address {
167    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        match self {
169            Self::Cosmos { bech32_addr, .. } => {
170                write!(f, "{bech32_addr}")
171            }
172            Self::Evm(addr_evm) => {
173                write!(f, "{addr_evm}")
174            }
175        }
176    }
177}
178
179impl From<alloy_primitives::Address> for Address {
180    fn from(addr: alloy_primitives::Address) -> Self {
181        Self::Evm(addr.into())
182    }
183}
184
185impl TryFrom<Address> for alloy_primitives::Address {
186    type Error = anyhow::Error;
187
188    fn try_from(addr: Address) -> Result<Self> {
189        match addr {
190            Address::Evm(addr_evm) => Ok(addr_evm.into()),
191            Address::Cosmos { .. } => Err(anyhow!("Expected EVM address, got Cosmos")),
192        }
193    }
194}
195
196impl TryFrom<&Address> for cosmwasm_std::Addr {
197    type Error = anyhow::Error;
198
199    fn try_from(addr: &Address) -> Result<Self> {
200        match addr {
201            Address::Cosmos { bech32_addr, .. } => {
202                // We can safely ignore the prefix_len here since cosmwasm_std::Addr does not use it
203                Ok(cosmwasm_std::Addr::unchecked(bech32_addr))
204            }
205            Address::Evm(_) => Err(anyhow!("Expected Cosmos address, got EVM")),
206        }
207    }
208}
209
210impl TryFrom<Address> for cosmwasm_std::Addr {
211    type Error = anyhow::Error;
212
213    fn try_from(addr: Address) -> Result<Self> {
214        cosmwasm_std::Addr::try_from(&addr)
215    }
216}
217
218impl TryFrom<&cosmwasm_std::Addr> for Address {
219    type Error = anyhow::Error;
220
221    fn try_from(addr: &cosmwasm_std::Addr) -> Result<Self> {
222        Self::new_cosmos_string(addr.as_str(), None)
223    }
224}
225
226impl TryFrom<cosmwasm_std::Addr> for Address {
227    type Error = anyhow::Error;
228
229    fn try_from(addr: cosmwasm_std::Addr) -> Result<Self> {
230        Address::try_from(&addr)
231    }
232}
233
234/// EVM address
235// we implement our own Serialize/Deserialize to ensure it is serialized as a hex string
236// so we need to manually implement the cw_serde derives from https://github.com/CosmWasm/cosmwasm/blob/fa5439a9e4e6884abe1e76f04443a95961eaa73f/packages/schema-derive/src/cw_serde.rs#L47C5-L61C7
237#[derive(Clone, Debug, PartialEq, Eq)]
238#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
239#[cfg_attr(feature = "cw-storage", derive(cw_storage_plus::NewTypeKey))]
240pub struct AddrEvm([u8; 20]);
241
242impl cw_schema::Schemaifier for AddrEvm {
243    #[inline]
244    fn visit_schema(visitor: &mut cw_schema::SchemaVisitor) -> cw_schema::DefinitionReference {
245        let node = cw_schema::Node {
246            name: Cow::Borrowed(std::any::type_name::<Self>()),
247            description: None,
248            value: cw_schema::NodeType::String,
249        };
250
251        visitor.insert(Self::id(), node)
252    }
253}
254
255impl cosmwasm_schema::schemars::JsonSchema for AddrEvm {
256    fn schema_name() -> String {
257        "AddrEvm".into()
258    }
259
260    fn json_schema(
261        _generator: &mut cosmwasm_schema::schemars::r#gen::SchemaGenerator,
262    ) -> cosmwasm_schema::schemars::schema::Schema {
263        cosmwasm_schema::schemars::schema::Schema::Object(
264            cosmwasm_schema::schemars::schema::SchemaObject {
265                instance_type: Some(cosmwasm_schema::schemars::schema::SingleOrVec::Single(
266                    Box::new(cosmwasm_schema::schemars::schema::InstanceType::String),
267                )),
268                format: Some("hex".into()),
269                ..Default::default()
270            },
271        )
272    }
273}
274
275impl AddrEvm {
276    pub fn new(bytes: [u8; 20]) -> Self {
277        Self(bytes)
278    }
279
280    pub fn new_vec(bytes: Vec<u8>) -> Result<Self> {
281        if bytes.len() != 20 {
282            bail!("Invalid length for EVM address");
283        }
284        let mut arr = [0u8; 20];
285        arr.copy_from_slice(&bytes);
286        Ok(Self(arr))
287    }
288
289    pub fn as_bytes(&self) -> [u8; 20] {
290        self.0
291    }
292}
293
294impl std::fmt::Display for AddrEvm {
295    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
296        write!(f, "0x{}", const_hex::encode(self.0))
297    }
298}
299
300impl FromStr for AddrEvm {
301    type Err = anyhow::Error;
302
303    fn from_str(s: &str) -> Result<Self> {
304        let s = s.trim();
305        if s.len() != 42 {
306            bail!("Invalid length for EVM address");
307        }
308        if !s.starts_with("0x") {
309            bail!("Invalid prefix for EVM address");
310        }
311        let bytes = const_hex::decode(&s[2..])?;
312        Self::new_vec(bytes)
313    }
314}
315
316impl TryFrom<Address> for AddrEvm {
317    type Error = anyhow::Error;
318
319    fn try_from(addr: Address) -> Result<Self> {
320        match addr {
321            Address::Evm(addr_evm) => Ok(addr_evm),
322            Address::Cosmos { .. } => bail!("Address must be EVM - use into_evm() instead"),
323        }
324    }
325}
326
327impl From<AddrEvm> for Address {
328    fn from(addr: AddrEvm) -> Self {
329        Self::Evm(addr)
330    }
331}
332
333impl From<alloy_primitives::Address> for AddrEvm {
334    fn from(addr: alloy_primitives::Address) -> Self {
335        Self(**addr)
336    }
337}
338
339impl From<AddrEvm> for alloy_primitives::Address {
340    fn from(addr: AddrEvm) -> Self {
341        alloy_primitives::Address::new(addr.0)
342    }
343}
344
345impl serde::Serialize for AddrEvm {
346    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
347    where
348        S: serde::Serializer,
349    {
350        serializer.serialize_str(&self.to_string())
351    }
352}
353
354impl<'de> serde::Deserialize<'de> for AddrEvm {
355    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
356    where
357        D: serde::Deserializer<'de>,
358    {
359        let s = String::deserialize(deserializer)?;
360        s.parse().map_err(serde::de::Error::custom)
361    }
362}
363
364#[cw_serde]
365#[derive(Eq, Hash)]
366pub enum AddrKind {
367    Cosmos { prefix: String },
368    Evm,
369}
370
371impl AddrKind {
372    pub fn parse_address(&self, value: &str) -> Result<Address> {
373        Address::try_from_str(value, self)
374    }
375
376    pub fn address_from_pub_key(&self, pub_key: &tendermint::PublicKey) -> Result<Address> {
377        Address::try_from_pub_key(pub_key, self)
378    }
379}
380
381#[cfg(test)]
382mod test {
383    use super::{AddrEvm, Address};
384
385    // TODO get addresses that are actually the same underlying public key
386
387    const TEST_COSMOS_STR: &str = "osmo1h5qke5tzc0fgz93wcxg8da2en3advfect0gh4a";
388    const TEST_COSMOS_PREFIX: &str = "osmo";
389    const TEST_EVM_STR: &str = "0xb794f5ea0ba39494ce839613fffba74279579268";
390
391    #[test]
392    fn test_basic_roundtrip_evm() {
393        let test_string = TEST_EVM_STR;
394        let addr_evm: AddrEvm = test_string.parse().unwrap();
395        let addr: Address = addr_evm.clone().into();
396
397        assert_eq!(addr.to_string(), test_string);
398
399        let addr_evm_2: AddrEvm = addr.clone().try_into().unwrap();
400        assert_eq!(addr_evm_2, addr_evm);
401
402        // serde should be as hex string
403        assert_eq!(
404            serde_json::to_string(&addr_evm).unwrap(),
405            format!("\"{test_string}\"")
406        );
407        assert_eq!(
408            serde_json::from_str::<AddrEvm>(&format!("\"{test_string}\"")).unwrap(),
409            addr_evm
410        );
411    }
412
413    #[test]
414    fn test_basic_roundtrip_cosmos() {
415        let test_string = TEST_COSMOS_STR;
416        let test_prefix = TEST_COSMOS_PREFIX;
417        let addr = Address::new_cosmos_string(test_string, None).unwrap();
418
419        assert_eq!(addr.to_string(), test_string);
420        assert_eq!(addr.cosmos_prefix().unwrap(), test_prefix);
421    }
422
423    #[test]
424    fn test_convert_evm_to_cosmos() {
425        // let test_string = "0xb794f5ea0ba39494ce839613fffba74279579268";
426        // let addr_bytes:AddrEvm = test_string.try_into().unwrap();
427        // let addr_string:AddrString = (&addr_bytes).into();
428        // let addr_string_cosmos = addr_string.convert_into_cosmos("osmo".to_string()).unwrap();
429        // assert_eq!(addr_string_cosmos.to_string(), "osmo1suhgf5svhu4usrurvxzlgn54ksxmn8gljarjtxqnapv8kjnp4nrsll0sqv");
430    }
431
432    #[test]
433    fn test_convert_cosmos_to_evm() {
434        // let test_string = "osmo1suhgf5svhu4usrurvxzlgn54ksxmn8gljarjtxqnapv8kjnp4nrsll0sqv";
435        // let account_id:AccountId = test_string.parse().unwrap();
436        // let addr_string:AddrString = (&account_id).try_into().unwrap();
437        // let addr_string_evm = addr_string.convert_into_evm().unwrap();
438        // assert_eq!(addr_string_evm.to_string(), "0xb794f5ea0ba39494ce839613fffba74279579268");
439    }
440}