layer_climb_address/address/
cosmos.rs

1use std::{borrow::Cow, str::FromStr};
2
3use anyhow::{anyhow, bail, Context, Result};
4use cosmwasm_schema::cw_schema;
5use subtle_encoding::bech32;
6
7/// Cosmos address
8// we implement our own Serialize/Deserialize to ensure it is serialized as a hex string
9// 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
10#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, bincode::Encode, bincode::Decode)]
11#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
12pub struct CosmosAddr {
13    bech32_addr: String,
14    // prefix is the first part of the bech32 address
15    prefix_len: usize,
16}
17
18// used internally for validation across both ways of creating addresses:
19// 1. parsing from a string
20// 2. creating from a public key
21impl CosmosAddr {
22    pub fn new_unchecked(value: impl ToString, prefix_len: usize) -> Self {
23        Self {
24            bech32_addr: value.to_string(),
25            prefix_len,
26        }
27    }
28
29    pub fn new_bytes(bytes: Vec<u8>, prefix: &str) -> Result<Self> {
30        if !prefix.chars().all(|c| matches!(c, 'a'..='z' | '0'..='9')) {
31            bail!("expected prefix to be lowercase alphanumeric characters only");
32        }
33
34        if bytes.len() > 255 {
35            bail!(
36                "account ID should be at most 255 bytes long, but was {} bytes long",
37                bytes.len()
38            );
39        }
40
41        let bech32_addr = bech32::encode(prefix, bytes);
42
43        Ok(Self {
44            bech32_addr,
45            prefix_len: prefix.len(),
46        })
47    }
48
49    /// if you just have a string address, use new_cosmos_string instead
50    pub fn new_pub_key(pub_key: &tendermint::PublicKey, prefix: &str) -> Result<Self> {
51        match pub_key {
52            tendermint::PublicKey::Secp256k1(encoded_point) => {
53                let id = tendermint::account::Id::from(*encoded_point);
54                Self::new_bytes(id.as_bytes().to_vec(), prefix)
55            }
56            _ => Err(anyhow!(
57                "Invalid public key type, currently only supports secp256k1"
58            )),
59        }
60    }
61
62    // if the prefix is supplied, this will attempt to validate the address against the prefix to ensure they match
63    // if you just have a public key, use new_cosmos_pub_key instead
64    pub fn new_str(value: &str, prefix: Option<&str>) -> Result<Self> {
65        let (decoded_prefix, decoded_bytes) = if value.starts_with(|c: char| c.is_uppercase()) {
66            bech32::decode_upper(value)
67        } else {
68            bech32::decode(value)
69        }
70        .context(format!("invalid bech32: '{value}'"))?;
71
72        if let Some(prefix) = prefix {
73            if decoded_prefix != prefix {
74                bail!(
75                    "Address prefix \"{}\" does not match expected prefix \"{}\"",
76                    decoded_prefix,
77                    prefix
78                );
79            }
80        }
81
82        Self::new_bytes(decoded_bytes, &decoded_prefix)
83    }
84
85    pub fn to_vec(&self) -> Vec<u8> {
86        let (_, bytes) = bech32::decode(&self.bech32_addr).unwrap();
87        bytes
88    }
89
90    pub fn prefix(&self) -> &str {
91        &self.bech32_addr[..self.prefix_len]
92    }
93
94    pub fn change_prefix(&self, new_prefix: &str) -> Result<Self> {
95        if self.prefix() == new_prefix {
96            Ok(self.clone())
97        } else {
98            Self::new_str(&self.bech32_addr, Some(new_prefix))
99        }
100    }
101}
102
103// the display impl ignores the prefix
104impl std::fmt::Display for CosmosAddr {
105    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106        write!(f, "{}", self.bech32_addr)
107    }
108}
109
110impl FromStr for CosmosAddr {
111    type Err = anyhow::Error;
112
113    fn from_str(s: &str) -> Result<Self> {
114        Self::new_str(s, None)
115    }
116}
117
118impl serde::Serialize for CosmosAddr {
119    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
120    where
121        S: serde::Serializer,
122    {
123        serializer.serialize_str(&self.to_string())
124    }
125}
126
127impl<'de> serde::Deserialize<'de> for CosmosAddr {
128    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
129    where
130        D: serde::Deserializer<'de>,
131    {
132        let s = String::deserialize(deserializer)?;
133        s.parse().map_err(serde::de::Error::custom)
134    }
135}
136
137impl cw_schema::Schemaifier for CosmosAddr {
138    #[inline]
139    fn visit_schema(visitor: &mut cw_schema::SchemaVisitor) -> cw_schema::DefinitionReference {
140        let node = cw_schema::Node {
141            name: Cow::Borrowed(std::any::type_name::<Self>()),
142            description: None,
143            value: cw_schema::NodeType::String,
144        };
145
146        visitor.insert(Self::id(), node)
147    }
148}
149
150impl cosmwasm_schema::schemars::JsonSchema for CosmosAddr {
151    fn schema_name() -> String {
152        "CosmosAddr".into()
153    }
154
155    fn json_schema(
156        _generator: &mut cosmwasm_schema::schemars::r#gen::SchemaGenerator,
157    ) -> cosmwasm_schema::schemars::schema::Schema {
158        cosmwasm_schema::schemars::schema::Schema::Object(
159            cosmwasm_schema::schemars::schema::SchemaObject {
160                instance_type: Some(cosmwasm_schema::schemars::schema::SingleOrVec::Single(
161                    Box::new(cosmwasm_schema::schemars::schema::InstanceType::String),
162                )),
163                format: Some("cosmos-address".into()),
164                ..Default::default()
165            },
166        )
167    }
168}
169
170// From/Into impls
171impl TryFrom<cosmwasm_std::Addr> for CosmosAddr {
172    type Error = anyhow::Error;
173
174    fn try_from(addr: cosmwasm_std::Addr) -> Result<Self> {
175        Self::new_str(addr.as_str(), None)
176    }
177}
178
179impl From<CosmosAddr> for cosmwasm_std::Addr {
180    fn from(addr: CosmosAddr) -> Self {
181        cosmwasm_std::Addr::unchecked(addr.to_string())
182    }
183}
184
185impl TryFrom<&cosmwasm_std::Addr> for CosmosAddr {
186    type Error = anyhow::Error;
187
188    fn try_from(addr: &cosmwasm_std::Addr) -> Result<Self> {
189        Self::new_str(addr.as_str(), None)
190    }
191}