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;
7use utoipa::ToSchema;
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
12pub enum Address {
13 Cosmos {
14 bech32_addr: String,
15 prefix_len: usize,
17 },
18 Evm(AddrEvm),
19}
20
21impl Hash for Address {
22 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
23 match self {
24 Address::Cosmos { .. } => {
25 1u32.hash(state);
26 }
27 Address::Evm(_) => {
28 2u32.hash(state);
29 }
30 }
31 self.to_string().hash(state);
32 }
33}
34
35impl Address {
36 fn new_cosmos(bytes: Vec<u8>, prefix: &str) -> Result<Self> {
40 if !prefix.chars().all(|c| matches!(c, 'a'..='z' | '0'..='9')) {
41 bail!("expected prefix to be lowercase alphanumeric characters only");
42 }
43
44 if bytes.len() > 255 {
45 bail!(
46 "account ID should be at most 255 bytes long, but was {} bytes long",
47 bytes.len()
48 );
49 }
50
51 let bech32_addr = bech32::encode(prefix, bytes);
52
53 Ok(Self::Cosmos {
54 bech32_addr,
55 prefix_len: prefix.len(),
56 })
57 }
58 pub fn new_cosmos_string(value: &str, prefix: Option<&str>) -> Result<Self> {
61 let (decoded_prefix, decoded_bytes) = if value.starts_with(|c: char| c.is_uppercase()) {
62 bech32::decode_upper(value)
63 } else {
64 bech32::decode(value)
65 }
66 .context(format!("invalid bech32: '{}'", value))?;
67
68 if matches!(prefix, Some(prefix) if prefix != decoded_prefix) {
69 bail!(
70 "Address prefix \"{}\" does not match expected prefix \"{}\"",
71 decoded_prefix,
72 prefix.unwrap()
73 );
74 }
75
76 Self::new_cosmos(decoded_bytes, &decoded_prefix)
77 }
78
79 pub fn as_bytes(&self) -> Vec<u8> {
80 match self {
81 Address::Cosmos { bech32_addr, .. } => {
82 let (_, bytes) = bech32::decode(bech32_addr).unwrap();
83 bytes
84 }
85 Address::Evm(addr_evm) => addr_evm.as_bytes().to_vec(),
86 }
87 }
88
89 pub fn new_cosmos_pub_key(pub_key: &PublicKey, prefix: &str) -> Result<Self> {
91 match pub_key {
92 PublicKey::Secp256k1(encoded_point) => {
93 let id = tendermint::account::Id::from(*encoded_point);
94 Self::new_cosmos(id.as_bytes().to_vec(), prefix)
95 }
96 _ => Err(anyhow!(
97 "Invalid public key type, currently only supports secp256k1"
98 )),
99 }
100 }
101
102 pub fn cosmos_prefix(&self) -> Result<&str> {
103 match self {
104 Address::Cosmos {
105 prefix_len,
106 bech32_addr,
107 } => Ok(&bech32_addr[..*prefix_len]),
108 Address::Evm(_) => Err(anyhow!("Address is not cosmos")),
109 }
110 }
111
112 pub fn new_evm_string(value: &str) -> Result<Self> {
113 let addr_evm: AddrEvm = value.parse()?;
114 Ok(Self::Evm(addr_evm))
115 }
116
117 pub fn new_evm_pub_key(_pub_key: &PublicKey) -> Result<Self> {
119 bail!("TODO - implement evm address from public key");
120 }
121
122 pub fn into_cosmos(&self, new_prefix: &str) -> Result<Self> {
123 match self {
124 Address::Cosmos { bech32_addr, .. } => {
125 if self.cosmos_prefix()? == new_prefix {
126 Ok(self.clone())
127 } else {
128 Self::new_cosmos_string(bech32_addr, Some(new_prefix))
129 }
130 }
131 Address::Evm(_) => {
132 bail!("TODO - implement evm to cosmos addr");
133 }
134 }
135 }
136
137 pub fn into_evm(&self) -> Result<Self> {
138 match self {
139 Address::Evm(_) => Ok(self.clone()),
140 Address::Cosmos { .. } => {
141 bail!("TODO - implement cosmos to evm addr");
142 }
143 }
144 }
145
146 pub fn try_from_str(value: &str, addr_kind: &AddrKind) -> Result<Self> {
147 match addr_kind {
148 AddrKind::Cosmos { prefix } => Self::new_cosmos_string(value, Some(prefix)),
149 AddrKind::Evm => Self::new_evm_string(value),
150 }
151 }
152
153 pub fn try_from_pub_key(pub_key: &PublicKey, addr_kind: &AddrKind) -> Result<Address> {
154 match addr_kind {
155 AddrKind::Cosmos { prefix } => Address::new_cosmos_pub_key(pub_key, prefix),
156 AddrKind::Evm => Address::new_evm_pub_key(pub_key),
157 }
158 }
159}
160
161impl std::fmt::Display for Address {
163 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164 match self {
165 Self::Cosmos { bech32_addr, .. } => {
166 write!(f, "{}", bech32_addr)
167 }
168 Self::Evm(addr_evm) => {
169 write!(f, "{}", addr_evm)
170 }
171 }
172 }
173}
174
175impl From<alloy_primitives::Address> for Address {
176 fn from(addr: alloy_primitives::Address) -> Self {
177 Self::Evm(addr.into())
178 }
179}
180
181impl TryFrom<Address> for alloy_primitives::Address {
182 type Error = anyhow::Error;
183
184 fn try_from(addr: Address) -> Result<Self> {
185 match addr {
186 Address::Evm(addr_evm) => Ok(addr_evm.into()),
187 Address::Cosmos { .. } => Err(anyhow!("Expected EVM address, got Cosmos")),
188 }
189 }
190}
191
192#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
194#[serde(transparent)]
195pub struct AddrEvm([u8; 20]);
196
197impl AddrEvm {
198 pub fn new(bytes: [u8; 20]) -> Self {
199 Self(bytes)
200 }
201
202 pub fn new_vec(bytes: Vec<u8>) -> Result<Self> {
203 if bytes.len() != 20 {
204 bail!("Invalid length for EVM address");
205 }
206 let mut arr = [0u8; 20];
207 arr.copy_from_slice(&bytes);
208 Ok(Self(arr))
209 }
210
211 pub fn as_bytes(&self) -> [u8; 20] {
212 self.0
213 }
214}
215
216impl std::fmt::Display for AddrEvm {
217 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218 write!(f, "0x{}", hex::encode(self.0))
219 }
220}
221
222impl FromStr for AddrEvm {
223 type Err = anyhow::Error;
224
225 fn from_str(s: &str) -> Result<Self> {
226 let s = s.trim();
227 if s.len() != 42 {
228 bail!("Invalid length for EVM address");
229 }
230 if !s.starts_with("0x") {
231 bail!("Invalid prefix for EVM address");
232 }
233 let bytes = hex::decode(&s[2..])?;
234 Self::new_vec(bytes)
235 }
236}
237
238impl TryFrom<Address> for AddrEvm {
239 type Error = anyhow::Error;
240
241 fn try_from(addr: Address) -> Result<Self> {
242 match addr {
243 Address::Evm(addr_evm) => Ok(addr_evm),
244 Address::Cosmos { .. } => bail!("Address must be EVM - use into_evm() instead"),
245 }
246 }
247}
248
249impl From<AddrEvm> for Address {
250 fn from(addr: AddrEvm) -> Self {
251 Self::Evm(addr)
252 }
253}
254
255impl From<alloy_primitives::Address> for AddrEvm {
256 fn from(addr: alloy_primitives::Address) -> Self {
257 Self(**addr)
258 }
259}
260
261impl From<AddrEvm> for alloy_primitives::Address {
262 fn from(addr: AddrEvm) -> Self {
263 alloy_primitives::Address::new(addr.0)
264 }
265}
266
267#[cfg(test)]
268mod test {
269 use super::{AddrEvm, Address};
270
271 const TEST_COSMOS_STR: &str = "osmo1h5qke5tzc0fgz93wcxg8da2en3advfect0gh4a";
274 const TEST_COSMOS_PREFIX: &str = "osmo";
275 const TEST_EVM_STR: &str = "0xb794f5ea0ba39494ce839613fffba74279579268";
276
277 #[test]
278 fn test_basic_roundtrip_evm() {
279 let test_string = TEST_EVM_STR;
280 let addr_evm: AddrEvm = test_string.parse().unwrap();
281 let addr: Address = addr_evm.into();
282
283 assert_eq!(addr.to_string(), test_string);
284
285 let addr_evm_2: AddrEvm = addr.try_into().unwrap();
286 assert_eq!(addr_evm_2, addr_evm);
287 }
288
289 #[test]
290 fn test_basic_roundtrip_cosmos() {
291 let test_string = TEST_COSMOS_STR;
292 let test_prefix = TEST_COSMOS_PREFIX;
293 let addr = Address::new_cosmos_string(test_string, None).unwrap();
294
295 assert_eq!(addr.to_string(), test_string);
296 assert_eq!(addr.cosmos_prefix().unwrap(), test_prefix);
297 }
298
299 #[test]
300 fn test_convert_evm_to_cosmos() {
301 }
307
308 #[test]
309 fn test_convert_cosmos_to_evm() {
310 }
316}