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 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 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 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
165impl 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 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#[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 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 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 }
431
432 #[test]
433 fn test_convert_cosmos_to_evm() {
434 }
440}