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#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
9#[derive(Eq)]
10#[cw_serde]
11pub enum Address {
12 Cosmos {
13 bech32_addr: String,
14 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 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 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 pub fn new_cosmos_pub_key(pub_key: &tendermint::PublicKey, prefix: &str) -> Result<Self> {
90 match pub_key {
91 tendermint::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 pub fn new_evm_pub_key(_pub_key: &tendermint::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(
153 pub_key: &tendermint::PublicKey,
154 addr_kind: &AddrKind,
155 ) -> Result<Address> {
156 match addr_kind {
157 AddrKind::Cosmos { prefix } => Address::new_cosmos_pub_key(pub_key, prefix),
158 AddrKind::Evm => Address::new_evm_pub_key(pub_key),
159 }
160 }
161}
162
163impl std::fmt::Display for Address {
165 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166 match self {
167 Self::Cosmos { bech32_addr, .. } => {
168 write!(f, "{bech32_addr}")
169 }
170 Self::Evm(addr_evm) => {
171 write!(f, "{addr_evm}")
172 }
173 }
174 }
175}
176
177impl From<alloy_primitives::Address> for Address {
178 fn from(addr: alloy_primitives::Address) -> Self {
179 Self::Evm(addr.into())
180 }
181}
182
183impl TryFrom<Address> for alloy_primitives::Address {
184 type Error = anyhow::Error;
185
186 fn try_from(addr: Address) -> Result<Self> {
187 match addr {
188 Address::Evm(addr_evm) => Ok(addr_evm.into()),
189 Address::Cosmos { .. } => Err(anyhow!("Expected EVM address, got Cosmos")),
190 }
191 }
192}
193
194#[derive(Clone, Debug, PartialEq, Eq)]
198#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
199#[cfg_attr(feature = "cw-storage", derive(cw_storage_plus::NewTypeKey))]
200pub struct AddrEvm([u8; 20]);
201
202impl cw_schema::Schemaifier for AddrEvm {
203 #[inline]
204 fn visit_schema(visitor: &mut cw_schema::SchemaVisitor) -> cw_schema::DefinitionReference {
205 let node = cw_schema::Node {
206 name: Cow::Borrowed(std::any::type_name::<Self>()),
207 description: None,
208 value: cw_schema::NodeType::String,
209 };
210
211 visitor.insert(Self::id(), node)
212 }
213}
214
215impl cosmwasm_schema::schemars::JsonSchema for AddrEvm {
216 fn schema_name() -> String {
217 "AddrEvm".into()
218 }
219
220 fn json_schema(
221 _generator: &mut cosmwasm_schema::schemars::r#gen::SchemaGenerator,
222 ) -> cosmwasm_schema::schemars::schema::Schema {
223 cosmwasm_schema::schemars::schema::Schema::Object(
224 cosmwasm_schema::schemars::schema::SchemaObject {
225 instance_type: Some(cosmwasm_schema::schemars::schema::SingleOrVec::Single(
226 Box::new(cosmwasm_schema::schemars::schema::InstanceType::String),
227 )),
228 format: Some("hex".into()),
229 ..Default::default()
230 },
231 )
232 }
233}
234
235impl AddrEvm {
236 pub fn new(bytes: [u8; 20]) -> Self {
237 Self(bytes)
238 }
239
240 pub fn new_vec(bytes: Vec<u8>) -> Result<Self> {
241 if bytes.len() != 20 {
242 bail!("Invalid length for EVM address");
243 }
244 let mut arr = [0u8; 20];
245 arr.copy_from_slice(&bytes);
246 Ok(Self(arr))
247 }
248
249 pub fn as_bytes(&self) -> [u8; 20] {
250 self.0
251 }
252}
253
254impl std::fmt::Display for AddrEvm {
255 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256 write!(f, "0x{}", const_hex::encode(self.0))
257 }
258}
259
260impl FromStr for AddrEvm {
261 type Err = anyhow::Error;
262
263 fn from_str(s: &str) -> Result<Self> {
264 let s = s.trim();
265 if s.len() != 42 {
266 bail!("Invalid length for EVM address");
267 }
268 if !s.starts_with("0x") {
269 bail!("Invalid prefix for EVM address");
270 }
271 let bytes = const_hex::decode(&s[2..])?;
272 Self::new_vec(bytes)
273 }
274}
275
276impl TryFrom<Address> for AddrEvm {
277 type Error = anyhow::Error;
278
279 fn try_from(addr: Address) -> Result<Self> {
280 match addr {
281 Address::Evm(addr_evm) => Ok(addr_evm),
282 Address::Cosmos { .. } => bail!("Address must be EVM - use into_evm() instead"),
283 }
284 }
285}
286
287impl From<AddrEvm> for Address {
288 fn from(addr: AddrEvm) -> Self {
289 Self::Evm(addr)
290 }
291}
292
293impl From<alloy_primitives::Address> for AddrEvm {
294 fn from(addr: alloy_primitives::Address) -> Self {
295 Self(**addr)
296 }
297}
298
299impl From<AddrEvm> for alloy_primitives::Address {
300 fn from(addr: AddrEvm) -> Self {
301 alloy_primitives::Address::new(addr.0)
302 }
303}
304
305impl serde::Serialize for AddrEvm {
306 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
307 where
308 S: serde::Serializer,
309 {
310 serializer.serialize_str(&self.to_string())
311 }
312}
313
314impl<'de> serde::Deserialize<'de> for AddrEvm {
315 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
316 where
317 D: serde::Deserializer<'de>,
318 {
319 let s = String::deserialize(deserializer)?;
320 s.parse().map_err(serde::de::Error::custom)
321 }
322}
323
324#[cw_serde]
325#[derive(Eq, Hash)]
326pub enum AddrKind {
327 Cosmos { prefix: String },
328 Evm,
329}
330
331impl AddrKind {
332 pub fn parse_address(&self, value: &str) -> Result<Address> {
333 Address::try_from_str(value, self)
334 }
335
336 pub fn address_from_pub_key(&self, pub_key: &tendermint::PublicKey) -> Result<Address> {
337 Address::try_from_pub_key(pub_key, self)
338 }
339}
340
341#[cfg(test)]
342mod test {
343 use super::{AddrEvm, Address};
344
345 const TEST_COSMOS_STR: &str = "osmo1h5qke5tzc0fgz93wcxg8da2en3advfect0gh4a";
348 const TEST_COSMOS_PREFIX: &str = "osmo";
349 const TEST_EVM_STR: &str = "0xb794f5ea0ba39494ce839613fffba74279579268";
350
351 #[test]
352 fn test_basic_roundtrip_evm() {
353 let test_string = TEST_EVM_STR;
354 let addr_evm: AddrEvm = test_string.parse().unwrap();
355 let addr: Address = addr_evm.clone().into();
356
357 assert_eq!(addr.to_string(), test_string);
358
359 let addr_evm_2: AddrEvm = addr.clone().try_into().unwrap();
360 assert_eq!(addr_evm_2, addr_evm);
361
362 assert_eq!(
364 serde_json::to_string(&addr_evm).unwrap(),
365 format!("\"{test_string}\"")
366 );
367 assert_eq!(
368 serde_json::from_str::<AddrEvm>(&format!("\"{test_string}\"")).unwrap(),
369 addr_evm
370 );
371 }
372
373 #[test]
374 fn test_basic_roundtrip_cosmos() {
375 let test_string = TEST_COSMOS_STR;
376 let test_prefix = TEST_COSMOS_PREFIX;
377 let addr = Address::new_cosmos_string(test_string, None).unwrap();
378
379 assert_eq!(addr.to_string(), test_string);
380 assert_eq!(addr.cosmos_prefix().unwrap(), test_prefix);
381 }
382
383 #[test]
384 fn test_convert_evm_to_cosmos() {
385 }
391
392 #[test]
393 fn test_convert_cosmos_to_evm() {
394 }
400}