nibiru_std/
address.rs

1//! Address conversion utilities for Nibiru
2
3use bech32::{self, FromBase32, ToBase32};
4
5use crate::errors::{NibiruError, NibiruResult};
6
7/// Converts a Nibiru bech32 address to an Ethereum hex address.
8///
9/// This function decodes a bech32-encoded Nibiru address (with "nibi" prefix)
10/// and converts it to an Ethereum-compatible hex address by taking the first
11/// 20 bytes of the decoded data.
12///
13/// # Arguments
14///
15/// * `bech32_addr` - A bech32-encoded Nibiru address string (e.g., "nibi1...")
16///
17/// # Returns
18///
19/// * `Ok(String)` - The Ethereum hex address prefixed with "0x"
20/// * `Err(NibiruError)` - If the address is invalid, has wrong prefix, or is too short
21///
22/// # Example
23///
24/// ```
25/// use nibiru_std::address::nibiru_bech32_to_eth_address;
26///
27/// let bech32_addr = "nibi1gc24lt74ses9swkq6g7cug4e5y72p7e34jqgul";
28/// let eth_addr = nibiru_bech32_to_eth_address(bech32_addr).unwrap();
29/// assert_eq!(eth_addr, "0x46155fafd58660583ac0d23d8e22b9a13ca0fb31");
30/// ```
31pub fn nibiru_bech32_to_eth_address(bech32_addr: &str) -> NibiruResult<String> {
32    // Decode the bech32 address
33    let (hrp, data, _variant) = bech32::decode(bech32_addr)?;
34
35    // Verify the human-readable part is "nibi"
36    if hrp != "nibi" {
37        return Err(NibiruError::InvalidBech32Prefix {
38            expected: "nibi".to_string(),
39            actual: hrp,
40        });
41    }
42
43    // Convert from base32 to bytes
44    let bytes = Vec::<u8>::from_base32(&data)?;
45
46    // Ethereum addresses are 20 bytes
47    if bytes.len() < 20 {
48        return Err(NibiruError::InvalidAddressLength);
49    }
50
51    // Take the first 20 bytes and format as hex with 0x prefix
52    let eth_addr = format!("0x{}", hex::encode(&bytes[..20]));
53    Ok(eth_addr)
54}
55
56/// Converts an Ethereum hex address to a Nibiru bech32 address.
57///
58/// This function takes an Ethereum address in hex format (with or without "0x" prefix)
59/// and converts it to a bech32-encoded Nibiru address with "nibi" prefix.
60///
61/// # Arguments
62///
63/// * `eth_addr` - An Ethereum address as a hex string (e.g., "0x..." or just the hex)
64///
65/// # Returns
66///
67/// * `Ok(String)` - The Nibiru bech32 address
68/// * `Err(NibiruError)` - If the address is invalid or not exactly 20 bytes
69///
70/// # Example
71///
72/// ```
73/// use nibiru_std::address::eth_address_to_nibiru_bech32;
74///
75/// let eth_addr = "0x46155fAfd58660583ac0d23d8E22B9A13Ca0fb31";
76/// let bech32_addr = eth_address_to_nibiru_bech32(eth_addr).unwrap();
77/// assert_eq!(bech32_addr, "nibi1gc24lt74ses9swkq6g7cug4e5y72p7e34jqgul");
78/// ```
79pub fn eth_address_to_nibiru_bech32(eth_addr: &str) -> NibiruResult<String> {
80    // Remove "0x" prefix if present
81    let hex_str = eth_addr.strip_prefix("0x").unwrap_or(eth_addr);
82
83    // Validate hex string length (20 bytes = 40 hex chars)
84    if hex_str.len() != 40 {
85        return Err(NibiruError::InvalidEthAddress(format!(
86            "Ethereum address must be 20 bytes (40 hex chars), got {} chars",
87            hex_str.len()
88        )));
89    }
90
91    // Decode hex to bytes
92    let bytes = hex::decode(hex_str)?;
93
94    // Sanity check: should be exactly 20 bytes
95    if bytes.len() != 20 {
96        return Err(NibiruError::InvalidEthAddress(format!(
97            "Invalid Ethereum address length: expected 20 bytes, got {}",
98            bytes.len()
99        )));
100    }
101
102    // Encode as bech32 with "nibi" prefix
103    let bech32_addr =
104        bech32::encode("nibi", bytes.to_base32(), bech32::Variant::Bech32)?;
105    Ok(bech32_addr)
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn test_nibiru_bech32_to_eth_address_valid() {
114        // Test case from the Go implementation
115        let bech32_addr = "nibi1gc24lt74ses9swkq6g7cug4e5y72p7e34jqgul";
116        let expected_eth = "0x46155fafd58660583ac0d23d8e22b9a13ca0fb31";
117
118        let result = nibiru_bech32_to_eth_address(bech32_addr).unwrap();
119        assert_eq!(result.to_lowercase(), expected_eth);
120    }
121
122    #[test]
123    fn test_nibiru_bech32_to_eth_address_invalid_prefix() {
124        // Valid bech32 address but with cosmos prefix instead of nibi
125        let bech32_addr = "cosmos1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnrql8a";
126
127        let result = nibiru_bech32_to_eth_address(bech32_addr);
128        match result {
129            Err(NibiruError::InvalidBech32Prefix { expected, actual }) => {
130                assert_eq!(expected, "nibi");
131                assert_eq!(actual, "cosmos");
132            }
133            _ => panic!("Expected InvalidBech32Prefix error, got: {:?}", result),
134        }
135    }
136
137    #[test]
138    fn test_nibiru_bech32_to_eth_address_invalid_bech32() {
139        let invalid_addr = "nibi1invalid!@#$";
140
141        let result = nibiru_bech32_to_eth_address(invalid_addr);
142        assert!(matches!(result, Err(NibiruError::Bech32Error(_))));
143    }
144
145    #[test]
146    fn test_nibiru_bech32_to_eth_address_length_validation() {
147        // Test that we properly validate address length
148        // We'll use a test helper to create a short address
149        use bech32::ToBase32;
150
151        // Create a short address with only 10 bytes (need 20 for Ethereum)
152        let short_data = vec![0u8; 10];
153        let short_addr = bech32::encode(
154            "nibi",
155            short_data.to_base32(),
156            bech32::Variant::Bech32,
157        )
158        .unwrap();
159
160        let result = nibiru_bech32_to_eth_address(&short_addr);
161        match result {
162            Err(NibiruError::InvalidAddressLength) => {}
163            _ => {
164                panic!("Expected InvalidAddressLength error, got: {:?}", result)
165            }
166        }
167    }
168
169    #[test]
170    fn test_nibiru_bech32_to_eth_address_case_sensitivity() {
171        // Test that the output maintains proper case
172        let bech32_addr = "nibi1gc24lt74ses9swkq6g7cug4e5y72p7e34jqgul";
173        let result = nibiru_bech32_to_eth_address(bech32_addr).unwrap();
174
175        // The hex should have lowercase letters after 0x
176        assert!(result.starts_with("0x"));
177        // But we'll compare case-insensitively for the actual value
178        assert_eq!(
179            result.to_lowercase(),
180            "0x46155fafd58660583ac0d23d8e22b9a13ca0fb31"
181        );
182    }
183
184    #[test]
185    fn test_eth_address_to_nibiru_bech32_valid() {
186        // Test case matching the Go implementation
187        let eth_addr = "0x46155fAfd58660583ac0d23d8E22B9A13Ca0fb31";
188        let expected_bech32 = "nibi1gc24lt74ses9swkq6g7cug4e5y72p7e34jqgul";
189
190        let result = eth_address_to_nibiru_bech32(eth_addr).unwrap();
191        assert_eq!(result, expected_bech32);
192    }
193
194    #[test]
195    fn test_eth_address_to_nibiru_bech32_without_prefix() {
196        // Test without 0x prefix
197        let eth_addr = "46155fAfd58660583ac0d23d8E22B9A13Ca0fb31";
198        let expected_bech32 = "nibi1gc24lt74ses9swkq6g7cug4e5y72p7e34jqgul";
199
200        let result = eth_address_to_nibiru_bech32(eth_addr).unwrap();
201        assert_eq!(result, expected_bech32);
202    }
203
204    #[test]
205    fn test_eth_address_to_nibiru_bech32_lowercase() {
206        // Test with lowercase hex
207        let eth_addr = "0x46155fafd58660583ac0d23d8e22b9a13ca0fb31";
208        let expected_bech32 = "nibi1gc24lt74ses9swkq6g7cug4e5y72p7e34jqgul";
209
210        let result = eth_address_to_nibiru_bech32(eth_addr).unwrap();
211        assert_eq!(result, expected_bech32);
212    }
213
214    #[test]
215    fn test_eth_address_to_nibiru_bech32_invalid_length() {
216        // Too short
217        let short_addr = "0x46155fafd58660583ac0d23d8e22b9a13ca0fb";
218        let result = eth_address_to_nibiru_bech32(short_addr);
219        match result {
220            Err(NibiruError::InvalidEthAddress(msg)) => {
221                assert!(msg.contains("40 hex chars"));
222            }
223            _ => panic!("Expected InvalidEthAddress error"),
224        }
225
226        // Too long
227        let long_addr = "0x46155fafd58660583ac0d23d8e22b9a13ca0fb3100";
228        let result = eth_address_to_nibiru_bech32(long_addr);
229        match result {
230            Err(NibiruError::InvalidEthAddress(msg)) => {
231                assert!(msg.contains("40 hex chars"));
232            }
233            _ => panic!("Expected InvalidEthAddress error"),
234        }
235    }
236
237    #[test]
238    fn test_eth_address_to_nibiru_bech32_invalid_hex() {
239        // Invalid hex characters
240        let invalid_addr = "0x46155fXXd58660583ac0d23d8e22b9a13ca0fb31";
241        let result = eth_address_to_nibiru_bech32(invalid_addr);
242        assert!(matches!(result, Err(NibiruError::HexError(_))));
243    }
244
245    #[test]
246    fn test_round_trip_conversion() {
247        // Test that converting back and forth gives the same result
248        let original_bech32 = "nibi1gc24lt74ses9swkq6g7cug4e5y72p7e34jqgul";
249
250        // Convert to Ethereum
251        let eth_addr = nibiru_bech32_to_eth_address(original_bech32).unwrap();
252
253        // Convert back to bech32
254        let result_bech32 = eth_address_to_nibiru_bech32(&eth_addr).unwrap();
255
256        assert_eq!(original_bech32, result_bech32);
257    }
258
259    #[test]
260    fn test_multiple_round_trips() {
261        // Test multiple addresses round-trip correctly
262        // Generate some valid test addresses
263        use bech32::ToBase32;
264
265        let test_bytes = vec![
266            vec![0u8; 20],   // All zeros
267            vec![255u8; 20], // All ones
268            vec![
269                1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18,
270                19, 20,
271            ], // Sequential
272        ];
273
274        for bytes in test_bytes {
275            // Create a valid bech32 address
276            let original_bech32 = bech32::encode(
277                "nibi",
278                bytes.to_base32(),
279                bech32::Variant::Bech32,
280            )
281            .unwrap();
282
283            // Convert to Ethereum
284            let eth_addr =
285                nibiru_bech32_to_eth_address(&original_bech32).unwrap();
286
287            // Convert back to bech32
288            let result_bech32 = eth_address_to_nibiru_bech32(&eth_addr).unwrap();
289
290            assert_eq!(
291                original_bech32, result_bech32,
292                "Round trip failed for address"
293            );
294        }
295    }
296}