Skip to main content

tronz_primitives/
address.rs

1//! TRON address type.
2//!
3//! A TRON address is 21 bytes: a single `0x41` prefix byte followed by the
4//! 20-byte EVM-style address (`keccak256(pubkey)[12..]`). It is most commonly
5//! displayed in base58check form (the familiar `T...` string).
6
7use core::fmt;
8use core::str::FromStr;
9
10use alloy_primitives::keccak256;
11use k256::ecdsa::VerifyingKey;
12use serde::{Deserialize, Deserializer, Serialize, Serializer};
13
14use crate::error::AddressError;
15
16/// The TRON mainnet address prefix byte.
17pub const ADDRESS_PREFIX: u8 = 0x41;
18
19/// Length of a raw TRON address in bytes (prefix + 20-byte body).
20pub const ADDRESS_LEN: usize = 21;
21
22/// Length of the EVM-style address body (without the `0x41` prefix).
23pub const EVM_ADDRESS_LEN: usize = 20;
24
25/// A TRON network address (`0x41` prefix + 20-byte body).
26#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
27pub struct Address([u8; ADDRESS_LEN]);
28
29impl Address {
30    /// Construct from the full 21-byte representation, validating the prefix.
31    pub fn from_bytes(bytes: [u8; ADDRESS_LEN]) -> Result<Self, AddressError> {
32        if bytes[0] != ADDRESS_PREFIX {
33            return Err(AddressError::BadPrefix(bytes[0]));
34        }
35        Ok(Self(bytes))
36    }
37
38    /// Construct from a 21-byte slice, validating length and prefix.
39    pub fn from_slice(slice: &[u8]) -> Result<Self, AddressError> {
40        let bytes: [u8; ADDRESS_LEN] = slice.try_into().map_err(|_| AddressError::BadLength {
41            expected: ADDRESS_LEN,
42            got: slice.len(),
43        })?;
44        Self::from_bytes(bytes)
45    }
46
47    /// Construct from the 20-byte EVM-style body, prepending the `0x41` prefix.
48    pub fn from_evm_bytes(evm: [u8; EVM_ADDRESS_LEN]) -> Self {
49        let mut bytes = [0u8; ADDRESS_LEN];
50        bytes[0] = ADDRESS_PREFIX;
51        bytes[1..].copy_from_slice(&evm);
52        Self(bytes)
53    }
54
55    /// Derive the address from a secp256k1 public key.
56    ///
57    /// `address = 0x41 || keccak256(uncompressed_pubkey[1..])[12..]`
58    pub fn from_public_key(key: &VerifyingKey) -> Self {
59        let point = key.to_encoded_point(false);
60        // Uncompressed SEC1 encoding is `0x04 || X(32) || Y(32)`; hash the 64
61        // coordinate bytes, skipping the `0x04` tag.
62        let hash = keccak256(&point.as_bytes()[1..]);
63        let mut evm = [0u8; EVM_ADDRESS_LEN];
64        evm.copy_from_slice(&hash[12..]);
65        Self::from_evm_bytes(evm)
66    }
67
68    /// Parse a base58check (`T...`) address string.
69    pub fn from_base58(s: &str) -> Result<Self, AddressError> {
70        let decoded = bs58::decode(s).with_check(None).into_vec()?;
71        Self::from_slice(&decoded)
72    }
73
74    /// Parse a hex address string (with or without `0x` / `41` semantics is
75    /// preserved: the bytes must already include the `0x41` prefix).
76    pub fn from_hex(s: &str) -> Result<Self, AddressError> {
77        let s = s.strip_prefix("0x").unwrap_or(s);
78        let bytes = hex::decode(s)?;
79        Self::from_slice(&bytes)
80    }
81
82    /// The full 21-byte representation, including the `0x41` prefix.
83    pub fn as_bytes(&self) -> &[u8; ADDRESS_LEN] {
84        &self.0
85    }
86
87    /// The 20-byte EVM-style body (prefix stripped). Use this when bridging to
88    /// `alloy` / ABI encoding.
89    pub fn as_evm_bytes(&self) -> &[u8; EVM_ADDRESS_LEN] {
90        self.0[1..]
91            .try_into()
92            .expect("address body is always 20 bytes")
93    }
94
95    /// Encode as a base58check (`T...`) string.
96    pub fn to_base58(&self) -> String {
97        bs58::encode(&self.0).with_check().into_string()
98    }
99
100    /// Encode as a lowercase hex string including the `0x41` prefix (no `0x`).
101    pub fn to_hex(&self) -> String {
102        hex::encode(self.0)
103    }
104}
105
106impl fmt::Display for Address {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        f.write_str(&self.to_base58())
109    }
110}
111
112impl fmt::Debug for Address {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        write!(f, "Address({})", self.to_base58())
115    }
116}
117
118impl FromStr for Address {
119    type Err = AddressError;
120
121    /// Accepts either a base58check (`T...`) or a hex (`41...` / `0x41...`)
122    /// address. Hex is detected when every character is a hex digit and the
123    /// string is the right length for a 21-byte address.
124    fn from_str(s: &str) -> Result<Self, Self::Err> {
125        let hexish = s.strip_prefix("0x").unwrap_or(s);
126        let looks_hex = hexish.len() == ADDRESS_LEN * 2
127            && hexish.bytes().all(|b| b.is_ascii_hexdigit());
128        if looks_hex {
129            Self::from_hex(s)
130        } else {
131            Self::from_base58(s)
132        }
133    }
134}
135
136// --- alloy bridging ---------------------------------------------------------
137
138impl From<Address> for alloy_primitives::Address {
139    fn from(a: Address) -> Self {
140        alloy_primitives::Address::from(*a.as_evm_bytes())
141    }
142}
143
144impl From<alloy_primitives::Address> for Address {
145    /// Re-attaches the TRON mainnet `0x41` prefix to a 20-byte EVM address.
146    fn from(a: alloy_primitives::Address) -> Self {
147        Address::from_evm_bytes(a.into_array())
148    }
149}
150
151// --- serde ------------------------------------------------------------------
152
153impl Serialize for Address {
154    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
155        serializer.serialize_str(&self.to_base58())
156    }
157}
158
159impl<'de> Deserialize<'de> for Address {
160    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
161        let s = String::deserialize(deserializer)?;
162        s.parse().map_err(serde::de::Error::custom)
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    // Well-known TRON address used widely in docs/tests.
171    const B58: &str = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t";
172    const HEX: &str = "41a614f803b6fd780986a42c78ec9c7f77e6ded13c";
173
174    #[test]
175    fn base58_roundtrip() {
176        let a = Address::from_base58(B58).unwrap();
177        assert_eq!(a.to_base58(), B58);
178        assert_eq!(a.to_hex(), HEX);
179    }
180
181    #[test]
182    fn hex_roundtrip() {
183        let a = Address::from_hex(HEX).unwrap();
184        assert_eq!(a.to_base58(), B58);
185    }
186
187    #[test]
188    fn fromstr_detects_format() {
189        assert_eq!(B58.parse::<Address>().unwrap().to_hex(), HEX);
190        assert_eq!(HEX.parse::<Address>().unwrap().to_base58(), B58);
191        let with_0x = format!("0x{HEX}");
192        assert_eq!(with_0x.parse::<Address>().unwrap().to_base58(), B58);
193    }
194
195    #[test]
196    fn bad_prefix_rejected() {
197        let mut bytes = [0u8; ADDRESS_LEN];
198        bytes[0] = 0x42;
199        assert!(matches!(
200            Address::from_bytes(bytes),
201            Err(AddressError::BadPrefix(0x42))
202        ));
203    }
204
205    #[test]
206    fn alloy_bridge_roundtrip() {
207        let a = Address::from_base58(B58).unwrap();
208        let evm: alloy_primitives::Address = a.into();
209        assert_eq!(evm.as_slice(), a.as_evm_bytes());
210        let back: Address = evm.into();
211        assert_eq!(back, a);
212    }
213
214    #[test]
215    fn evm_bytes_strip_prefix() {
216        let a = Address::from_hex(HEX).unwrap();
217        assert_eq!(a.as_evm_bytes().len(), 20);
218        assert_eq!(&a.as_bytes()[1..], a.as_evm_bytes());
219    }
220}