1use std::{fmt, str::FromStr};
4
5use bech32::{self, FromBase32, ToBase32, Variant};
6use cosmwasm_schema::schemars::{
7 gen::SchemaGenerator, schema::Schema, JsonSchema,
8};
9use cosmwasm_std::Addr;
10use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer};
11use tiny_keccak::{Hasher, Keccak};
12
13use crate::errors::{NibiruError, NibiruResult};
14
15pub const USER_ADDR_LEN: usize = 20;
17
18#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
24pub struct UserAddr([u8; USER_ADDR_LEN]);
25
26impl UserAddr {
27 pub fn to_hex(self) -> String {
29 eip55_checksum_hex(&self.0)
30 }
31
32 pub fn to_bech32_addr(self) -> Addr {
34 let encoded =
35 bech32::encode("nibi", self.0.to_base32(), Variant::Bech32)
36 .expect("fixed Nibiru HRP and 20-byte payload are valid");
37 Addr::unchecked(encoded)
38 }
39
40 pub fn as_bytes(&self) -> &[u8; USER_ADDR_LEN] {
42 &self.0
43 }
44
45 fn from_bech32(input: &str) -> NibiruResult<Self> {
46 let (hrp, data, variant) = bech32::decode(input)?;
47 if hrp != "nibi" {
48 return Err(NibiruError::InvalidBech32Prefix {
49 expected: "nibi".to_string(),
50 actual: hrp,
51 });
52 }
53 if variant != Variant::Bech32 {
54 return Err(NibiruError::InvalidEthAddress(
55 "Nibiru user address must use the Bech32 checksum variant"
56 .to_string(),
57 ));
58 }
59
60 let bytes = Vec::<u8>::from_base32(&data)?;
61 let bytes: [u8; USER_ADDR_LEN] = bytes.try_into().map_err(
62 |bytes: Vec<u8>| {
63 NibiruError::InvalidEthAddress(format!(
64 "Nibiru user address must decode to {USER_ADDR_LEN} bytes, got {}",
65 bytes.len()
66 ))
67 },
68 )?;
69 Ok(Self(bytes))
70 }
71
72 fn from_hex(input: &str) -> NibiruResult<Self> {
73 let hex = input
74 .strip_prefix("0x")
75 .or_else(|| input.strip_prefix("0X"))
76 .ok_or_else(|| {
77 NibiruError::InvalidEthAddress(
78 "EVM user address must start with 0x".to_string(),
79 )
80 })?;
81 if hex.len() != USER_ADDR_LEN * 2 {
82 return Err(NibiruError::InvalidEthAddress(format!(
83 "EVM user address must contain 40 hex characters, got {}",
84 hex.len()
85 )));
86 }
87 let bytes = hex::decode(hex)?;
88 let bytes: [u8; USER_ADDR_LEN] = bytes.try_into().map_err(
89 |bytes: Vec<u8>| {
90 NibiruError::InvalidEthAddress(format!(
91 "EVM user address must decode to {USER_ADDR_LEN} bytes, got {}",
92 bytes.len()
93 ))
94 },
95 )?;
96 Ok(Self(bytes))
97 }
98}
99
100impl FromStr for UserAddr {
101 type Err = NibiruError;
102
103 fn from_str(input: &str) -> Result<Self, Self::Err> {
104 let input = input.trim();
105 if input.is_empty() {
106 return Err(NibiruError::InvalidEthAddress(
107 "user address is empty".to_string(),
108 ));
109 }
110
111 if input.to_ascii_lowercase().starts_with("nibi1") {
112 Self::from_bech32(input)
113 } else {
114 Self::from_hex(input)
115 }
116 }
117}
118
119impl fmt::Display for UserAddr {
120 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121 f.write_str(&self.to_hex())
122 }
123}
124
125impl Serialize for UserAddr {
126 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
127 where
128 S: Serializer,
129 {
130 serializer.serialize_str(&self.to_hex())
131 }
132}
133
134impl<'de> Deserialize<'de> for UserAddr {
135 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
136 where
137 D: Deserializer<'de>,
138 {
139 String::deserialize(deserializer)?
140 .parse()
141 .map_err(D::Error::custom)
142 }
143}
144
145impl JsonSchema for UserAddr {
146 fn schema_name() -> String {
147 "UserAddr".to_string()
148 }
149
150 fn json_schema(generator: &mut SchemaGenerator) -> Schema {
151 let mut schema = String::json_schema(generator);
152 if let Schema::Object(object) = &mut schema {
153 object.metadata().description = Some(
154 "A Nibiru externally owned account. Input accepts a Nibiru bech32 address or a 0x-prefixed 20-byte EVM address; output uses EIP-55 hex."
155 .to_string(),
156 );
157 }
158 schema
159 }
160}
161
162fn eip55_checksum_hex(bytes: &[u8; USER_ADDR_LEN]) -> String {
163 let lowercase = hex::encode(bytes);
164 let mut hash = [0u8; 32];
165 let mut hasher = Keccak::v256();
166 hasher.update(lowercase.as_bytes());
167 hasher.finalize(&mut hash);
168
169 let mut output = String::with_capacity(42);
170 output.push_str("0x");
171 for (index, ch) in lowercase.chars().enumerate() {
172 let nibble = if index % 2 == 0 {
173 hash[index / 2] >> 4
174 } else {
175 hash[index / 2] & 0x0f
176 };
177 if ch.is_ascii_alphabetic() && nibble >= 8 {
178 output.push(ch.to_ascii_uppercase());
179 } else {
180 output.push(ch);
181 }
182 }
183 output
184}
185
186pub fn nibiru_bech32_to_eth_address(bech32_addr: &str) -> NibiruResult<String> {
211 let (hrp, data, _variant) = bech32::decode(bech32_addr)?;
213
214 if hrp != "nibi" {
216 return Err(NibiruError::InvalidBech32Prefix {
217 expected: "nibi".to_string(),
218 actual: hrp,
219 });
220 }
221
222 let bytes = Vec::<u8>::from_base32(&data)?;
224
225 if bytes.len() < 20 {
227 return Err(NibiruError::InvalidAddressLength);
228 }
229
230 let eth_addr = format!("0x{}", hex::encode(&bytes[..20]));
232 Ok(eth_addr)
233}
234
235pub fn eth_address_to_nibiru_bech32(eth_addr: &str) -> NibiruResult<String> {
259 let hex_str = eth_addr.strip_prefix("0x").unwrap_or(eth_addr);
261
262 if hex_str.len() != 40 {
264 return Err(NibiruError::InvalidEthAddress(format!(
265 "Ethereum address must be 20 bytes (40 hex chars), got {} chars",
266 hex_str.len()
267 )));
268 }
269
270 let bytes = hex::decode(hex_str)?;
272
273 if bytes.len() != 20 {
275 return Err(NibiruError::InvalidEthAddress(format!(
276 "Invalid Ethereum address length: expected 20 bytes, got {}",
277 bytes.len()
278 )));
279 }
280
281 let bech32_addr =
283 bech32::encode("nibi", bytes.to_base32(), bech32::Variant::Bech32)?;
284 Ok(bech32_addr)
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 #[test]
292 fn test_nibiru_bech32_to_eth_address_valid() {
293 let bech32_addr = "nibi1gc24lt74ses9swkq6g7cug4e5y72p7e34jqgul";
295 let expected_eth = "0x46155fafd58660583ac0d23d8e22b9a13ca0fb31";
296
297 let result = nibiru_bech32_to_eth_address(bech32_addr).unwrap();
298 assert_eq!(result.to_lowercase(), expected_eth);
299 }
300
301 #[test]
302 fn test_nibiru_bech32_to_eth_address_invalid_prefix() {
303 let bech32_addr = "cosmos1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnrql8a";
305
306 let result = nibiru_bech32_to_eth_address(bech32_addr);
307 match result {
308 Err(NibiruError::InvalidBech32Prefix { expected, actual }) => {
309 assert_eq!(expected, "nibi");
310 assert_eq!(actual, "cosmos");
311 }
312 _ => panic!("Expected InvalidBech32Prefix error, got: {:?}", result),
313 }
314 }
315
316 #[test]
317 fn test_nibiru_bech32_to_eth_address_invalid_bech32() {
318 let invalid_addr = "nibi1invalid!@#$";
319
320 let result = nibiru_bech32_to_eth_address(invalid_addr);
321 assert!(matches!(result, Err(NibiruError::Bech32Error(_))));
322 }
323
324 #[test]
325 fn test_nibiru_bech32_to_eth_address_length_validation() {
326 use bech32::ToBase32;
329
330 let short_data = vec![0u8; 10];
332 let short_addr = bech32::encode(
333 "nibi",
334 short_data.to_base32(),
335 bech32::Variant::Bech32,
336 )
337 .unwrap();
338
339 let result = nibiru_bech32_to_eth_address(&short_addr);
340 match result {
341 Err(NibiruError::InvalidAddressLength) => {}
342 _ => {
343 panic!("Expected InvalidAddressLength error, got: {:?}", result)
344 }
345 }
346 }
347
348 #[test]
349 fn test_nibiru_bech32_to_eth_address_case_sensitivity() {
350 let bech32_addr = "nibi1gc24lt74ses9swkq6g7cug4e5y72p7e34jqgul";
352 let result = nibiru_bech32_to_eth_address(bech32_addr).unwrap();
353
354 assert!(result.starts_with("0x"));
356 assert_eq!(
358 result.to_lowercase(),
359 "0x46155fafd58660583ac0d23d8e22b9a13ca0fb31"
360 );
361 }
362
363 #[test]
364 fn test_eth_address_to_nibiru_bech32_valid() {
365 let eth_addr = "0x46155fAfd58660583ac0d23d8E22B9A13Ca0fb31";
367 let expected_bech32 = "nibi1gc24lt74ses9swkq6g7cug4e5y72p7e34jqgul";
368
369 let result = eth_address_to_nibiru_bech32(eth_addr).unwrap();
370 assert_eq!(result, expected_bech32);
371 }
372
373 #[test]
374 fn test_eth_address_to_nibiru_bech32_without_prefix() {
375 let eth_addr = "46155fAfd58660583ac0d23d8E22B9A13Ca0fb31";
377 let expected_bech32 = "nibi1gc24lt74ses9swkq6g7cug4e5y72p7e34jqgul";
378
379 let result = eth_address_to_nibiru_bech32(eth_addr).unwrap();
380 assert_eq!(result, expected_bech32);
381 }
382
383 #[test]
384 fn test_eth_address_to_nibiru_bech32_lowercase() {
385 let eth_addr = "0x46155fafd58660583ac0d23d8e22b9a13ca0fb31";
387 let expected_bech32 = "nibi1gc24lt74ses9swkq6g7cug4e5y72p7e34jqgul";
388
389 let result = eth_address_to_nibiru_bech32(eth_addr).unwrap();
390 assert_eq!(result, expected_bech32);
391 }
392
393 #[test]
394 fn test_eth_address_to_nibiru_bech32_invalid_length() {
395 let short_addr = "0x46155fafd58660583ac0d23d8e22b9a13ca0fb";
397 let result = eth_address_to_nibiru_bech32(short_addr);
398 match result {
399 Err(NibiruError::InvalidEthAddress(msg)) => {
400 assert!(msg.contains("40 hex chars"));
401 }
402 _ => panic!("Expected InvalidEthAddress error"),
403 }
404
405 let long_addr = "0x46155fafd58660583ac0d23d8e22b9a13ca0fb3100";
407 let result = eth_address_to_nibiru_bech32(long_addr);
408 match result {
409 Err(NibiruError::InvalidEthAddress(msg)) => {
410 assert!(msg.contains("40 hex chars"));
411 }
412 _ => panic!("Expected InvalidEthAddress error"),
413 }
414 }
415
416 #[test]
417 fn test_eth_address_to_nibiru_bech32_invalid_hex() {
418 let invalid_addr = "0x46155fXXd58660583ac0d23d8e22b9a13ca0fb31";
420 let result = eth_address_to_nibiru_bech32(invalid_addr);
421 assert!(matches!(result, Err(NibiruError::HexError(_))));
422 }
423
424 #[test]
425 fn test_round_trip_conversion() {
426 let original_bech32 = "nibi1gc24lt74ses9swkq6g7cug4e5y72p7e34jqgul";
428
429 let eth_addr = nibiru_bech32_to_eth_address(original_bech32).unwrap();
431
432 let result_bech32 = eth_address_to_nibiru_bech32(ð_addr).unwrap();
434
435 assert_eq!(original_bech32, result_bech32);
436 }
437
438 #[test]
439 fn test_multiple_round_trips() {
440 use bech32::ToBase32;
443
444 let test_bytes = vec![
445 vec![0u8; 20], vec![255u8; 20], vec![
448 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18,
449 19, 20,
450 ], ];
452
453 for bytes in test_bytes {
454 let original_bech32 = bech32::encode(
456 "nibi",
457 bytes.to_base32(),
458 bech32::Variant::Bech32,
459 )
460 .unwrap();
461
462 let eth_addr =
464 nibiru_bech32_to_eth_address(&original_bech32).unwrap();
465
466 let result_bech32 = eth_address_to_nibiru_bech32(ð_addr).unwrap();
468
469 assert_eq!(
470 original_bech32, result_bech32,
471 "Round trip failed for address"
472 );
473 }
474 }
475
476 #[test]
477 fn user_addr_accepts_equivalent_forms_and_serializes_eip55() {
478 let bech32 = "nibi1gc24lt74ses9swkq6g7cug4e5y72p7e34jqgul";
479 let hex = "0x46155fafd58660583ac0d23d8e22b9a13ca0fb31";
480 let from_bech32: UserAddr = bech32.parse().unwrap();
481 let from_hex: UserAddr = format!(" {hex} ").parse().unwrap();
482
483 assert_eq!(from_bech32, from_hex);
484 assert_eq!(from_hex.to_bech32_addr(), Addr::unchecked(bech32));
485 assert_eq!(
486 from_hex.to_hex(),
487 "0x46155fAfd58660583ac0d23d8E22B9A13Ca0fb31"
488 );
489 assert_eq!(
490 serde_json::to_string(&from_hex).unwrap(),
491 "\"0x46155fAfd58660583ac0d23d8E22B9A13Ca0fb31\""
492 );
493 assert_eq!(
494 "0X46155FAFD58660583AC0D23D8E22B9A13CA0FB31"
495 .parse::<UserAddr>()
496 .unwrap(),
497 from_hex
498 );
499 let zero = "0x0000000000000000000000000000000000000000"
500 .parse::<UserAddr>()
501 .unwrap();
502 assert_eq!(zero.as_bytes(), &[0; USER_ADDR_LEN]);
503 }
504
505 #[test]
506 fn user_addr_serde_accepts_bech32_and_rejects_non_string_json() {
507 let encoded = "\"nibi1gc24lt74ses9swkq6g7cug4e5y72p7e34jqgul\"";
508 let parsed: UserAddr = serde_json::from_str(encoded).unwrap();
509 assert_eq!(
510 parsed.to_hex(),
511 "0x46155fAfd58660583ac0d23d8E22B9A13Ca0fb31"
512 );
513 assert!(serde_json::from_str::<UserAddr>("[0, 1]").is_err());
514 }
515
516 #[test]
517 fn user_addr_rejects_invalid_encodings_and_contract_addresses() {
518 assert!("46155fafd58660583ac0d23d8e22b9a13ca0fb31"
519 .parse::<UserAddr>()
520 .is_err());
521 assert!("0x1234".parse::<UserAddr>().is_err());
522 assert!("0xzz155fafd58660583ac0d23d8e22b9a13ca0fb31"
523 .parse::<UserAddr>()
524 .is_err());
525 assert!("cosmos1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnrql8a"
526 .parse::<UserAddr>()
527 .is_err());
528 assert!("nibi1gc24lt74ses9swkq6g7cug4e5y72p7e34jqgum"
529 .parse::<UserAddr>()
530 .is_err());
531
532 let contract =
533 bech32::encode("nibi", [7u8; 32].to_base32(), Variant::Bech32)
534 .unwrap();
535 assert!(contract.parse::<UserAddr>().is_err());
536 }
537
538 #[test]
539 fn user_addr_schema_is_a_string() {
540 let schema = cosmwasm_schema::schema_for!(UserAddr);
541 let json = serde_json::to_value(schema).unwrap();
542 assert_eq!(json["type"], "string");
543 assert!(json["description"].as_str().unwrap().contains("EIP-55"));
544 }
545}