layer_climb_address/
address.rs1use 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;
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
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: &PublicKey, prefix: &str) -> Result<Self> {
90 match pub_key {
91 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: &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(pub_key: &PublicKey, addr_kind: &AddrKind) -> Result<Address> {
153 match addr_kind {
154 AddrKind::Cosmos { prefix } => Address::new_cosmos_pub_key(pub_key, prefix),
155 AddrKind::Evm => Address::new_evm_pub_key(pub_key),
156 }
157 }
158}
159
160impl std::fmt::Display for Address {
162 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163 match self {
164 Self::Cosmos { bech32_addr, .. } => {
165 write!(f, "{}", bech32_addr)
166 }
167 Self::Evm(addr_evm) => {
168 write!(f, "{}", addr_evm)
169 }
170 }
171 }
172}
173
174impl From<alloy_primitives::Address> for Address {
175 fn from(addr: alloy_primitives::Address) -> Self {
176 Self::Evm(addr.into())
177 }
178}
179
180impl TryFrom<Address> for alloy_primitives::Address {
181 type Error = anyhow::Error;
182
183 fn try_from(addr: Address) -> Result<Self> {
184 match addr {
185 Address::Evm(addr_evm) => Ok(addr_evm.into()),
186 Address::Cosmos { .. } => Err(anyhow!("Expected EVM address, got Cosmos")),
187 }
188 }
189}
190
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
193#[serde(transparent)]
194pub struct AddrEvm([u8; 20]);
195
196impl AddrEvm {
197 pub fn new(bytes: [u8; 20]) -> Self {
198 Self(bytes)
199 }
200
201 pub fn new_vec(bytes: Vec<u8>) -> Result<Self> {
202 if bytes.len() != 20 {
203 bail!("Invalid length for EVM address");
204 }
205 let mut arr = [0u8; 20];
206 arr.copy_from_slice(&bytes);
207 Ok(Self(arr))
208 }
209
210 pub fn as_bytes(&self) -> [u8; 20] {
211 self.0
212 }
213}
214
215impl std::fmt::Display for AddrEvm {
216 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217 write!(f, "0x{}", hex::encode(self.0))
218 }
219}
220
221impl FromStr for AddrEvm {
222 type Err = anyhow::Error;
223
224 fn from_str(s: &str) -> Result<Self> {
225 let s = s.trim();
226 if s.len() != 42 {
227 bail!("Invalid length for EVM address");
228 }
229 if !s.starts_with("0x") {
230 bail!("Invalid prefix for EVM address");
231 }
232 let bytes = hex::decode(&s[2..])?;
233 Self::new_vec(bytes)
234 }
235}
236
237impl TryFrom<Address> for AddrEvm {
238 type Error = anyhow::Error;
239
240 fn try_from(addr: Address) -> Result<Self> {
241 match addr {
242 Address::Evm(addr_evm) => Ok(addr_evm),
243 Address::Cosmos { .. } => bail!("Address must be EVM - use into_evm() instead"),
244 }
245 }
246}
247
248impl From<AddrEvm> for Address {
249 fn from(addr: AddrEvm) -> Self {
250 Self::Evm(addr)
251 }
252}
253
254impl From<alloy_primitives::Address> for AddrEvm {
255 fn from(addr: alloy_primitives::Address) -> Self {
256 Self(**addr)
257 }
258}
259
260impl From<AddrEvm> for alloy_primitives::Address {
261 fn from(addr: AddrEvm) -> Self {
262 alloy_primitives::Address::new(addr.0)
263 }
264}
265
266#[cfg(test)]
267mod test {
268 use super::{AddrEvm, Address};
269
270 const TEST_COSMOS_STR: &str = "osmo1h5qke5tzc0fgz93wcxg8da2en3advfect0gh4a";
273 const TEST_COSMOS_PREFIX: &str = "osmo";
274 const TEST_EVM_STR: &str = "0xb794f5ea0ba39494ce839613fffba74279579268";
275
276 #[test]
277 fn test_basic_roundtrip_evm() {
278 let test_string = TEST_EVM_STR;
279 let addr_evm: AddrEvm = test_string.parse().unwrap();
280 let addr: Address = addr_evm.into();
281
282 assert_eq!(addr.to_string(), test_string);
283
284 let addr_evm_2: AddrEvm = addr.try_into().unwrap();
285 assert_eq!(addr_evm_2, addr_evm);
286 }
287
288 #[test]
289 fn test_basic_roundtrip_cosmos() {
290 let test_string = TEST_COSMOS_STR;
291 let test_prefix = TEST_COSMOS_PREFIX;
292 let addr = Address::new_cosmos_string(test_string, None).unwrap();
293
294 assert_eq!(addr.to_string(), test_string);
295 assert_eq!(addr.cosmos_prefix().unwrap(), test_prefix);
296 }
297
298 #[test]
299 fn test_convert_evm_to_cosmos() {
300 }
306
307 #[test]
308 fn test_convert_cosmos_to_evm() {
309 }
315}