reth_cli_util/
parsers.rs

1use alloy_eips::BlockHashOrNumber;
2use alloy_primitives::B256;
3use reth_fs_util::FsPathError;
4use std::{
5    net::{IpAddr, Ipv4Addr, SocketAddr, ToSocketAddrs},
6    path::Path,
7    str::FromStr,
8    time::Duration,
9};
10
11/// Helper to parse a [Duration] from seconds
12pub fn parse_duration_from_secs(arg: &str) -> eyre::Result<Duration, std::num::ParseIntError> {
13    let seconds = arg.parse()?;
14    Ok(Duration::from_secs(seconds))
15}
16
17/// Helper to parse a [Duration] from seconds if it's a number or milliseconds if the input contains
18/// a `ms` suffix:
19///  * `5ms` -> 5 milliseconds
20///  * `5` -> 5 seconds
21///  * `5s` -> 5 seconds
22pub fn parse_duration_from_secs_or_ms(
23    arg: &str,
24) -> eyre::Result<Duration, std::num::ParseIntError> {
25    if arg.ends_with("ms") {
26        arg.trim_end_matches("ms").parse().map(Duration::from_millis)
27    } else if arg.ends_with('s') {
28        arg.trim_end_matches('s').parse().map(Duration::from_secs)
29    } else {
30        arg.parse().map(Duration::from_secs)
31    }
32}
33
34/// Helper to format a [Duration] to the format that can be parsed by
35/// [`parse_duration_from_secs_or_ms`].
36pub fn format_duration_as_secs_or_ms(duration: Duration) -> String {
37    if duration.as_millis().is_multiple_of(1000) {
38        format!("{}", duration.as_secs())
39    } else {
40        format!("{}ms", duration.as_millis())
41    }
42}
43
44/// Parse [`BlockHashOrNumber`]
45pub fn hash_or_num_value_parser(value: &str) -> eyre::Result<BlockHashOrNumber, eyre::Error> {
46    match B256::from_str(value) {
47        Ok(hash) => Ok(BlockHashOrNumber::Hash(hash)),
48        Err(_) => Ok(BlockHashOrNumber::Number(value.parse()?)),
49    }
50}
51
52/// Error thrown while parsing a socket address.
53#[derive(thiserror::Error, Debug)]
54pub enum SocketAddressParsingError {
55    /// Failed to convert the string into a socket addr
56    #[error("could not parse socket address: {0}")]
57    Io(#[from] std::io::Error),
58    /// Input must not be empty
59    #[error("cannot parse socket address from empty string")]
60    Empty,
61    /// Failed to parse the address
62    #[error("could not parse socket address from {0}")]
63    Parse(String),
64    /// Failed to parse port
65    #[error("could not parse port: {0}")]
66    Port(#[from] std::num::ParseIntError),
67}
68
69/// Parse a [`SocketAddr`] from a `str`.
70///
71/// The following formats are checked:
72///
73/// - If the value can be parsed as a `u16` or starts with `:` it is considered a port, and the
74///   hostname is set to `localhost`.
75/// - If the value contains `:` it is assumed to be the format `<host>:<port>`
76/// - Otherwise it is assumed to be a hostname
77///
78/// An error is returned if the value is empty.
79pub fn parse_socket_address(value: &str) -> eyre::Result<SocketAddr, SocketAddressParsingError> {
80    if value.is_empty() {
81        return Err(SocketAddressParsingError::Empty)
82    }
83
84    if let Some(port) = value.strip_prefix(':').or_else(|| value.strip_prefix("localhost:")) {
85        let port: u16 = port.parse()?;
86        return Ok(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port))
87    }
88    if let Ok(port) = value.parse() {
89        return Ok(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port))
90    }
91    value
92        .to_socket_addrs()?
93        .next()
94        .ok_or_else(|| SocketAddressParsingError::Parse(value.to_string()))
95}
96
97/// Wrapper around [`reth_fs_util::read_json_file`] which can be used as a clap value parser.
98pub fn read_json_from_file<T: serde::de::DeserializeOwned>(path: &str) -> Result<T, FsPathError> {
99    reth_fs_util::read_json_file(Path::new(path))
100}
101
102/// Parses an ether value from a string.
103///
104/// The amount in eth like "1.05" will be interpreted in wei (1.05 * 1e18).
105/// Supports both decimal and integer inputs.
106///
107/// # Examples
108/// - "1.05" -> 1.05 ETH = 1.05 * 10^18 wei
109/// - "2" -> 2 ETH = 2 * 10^18 wei
110pub fn parse_ether_value(value: &str) -> eyre::Result<u128> {
111    let eth = value.parse::<f64>()?;
112    if eth.is_sign_negative() {
113        return Err(eyre::eyre!("Ether value cannot be negative"))
114    }
115    let wei = eth * 1e18;
116    Ok(wei as u128)
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use rand::Rng;
123
124    #[test]
125    fn parse_socket_addresses() {
126        for value in ["localhost:9000", ":9000", "9000"] {
127            let socket_addr = parse_socket_address(value)
128                .unwrap_or_else(|_| panic!("could not parse socket address: {value}"));
129
130            assert!(socket_addr.ip().is_loopback());
131            assert_eq!(socket_addr.port(), 9000);
132        }
133    }
134
135    #[test]
136    fn parse_socket_address_random() {
137        let port: u16 = rand::rng().random();
138
139        for value in [format!("localhost:{port}"), format!(":{port}"), port.to_string()] {
140            let socket_addr = parse_socket_address(&value)
141                .unwrap_or_else(|_| panic!("could not parse socket address: {value}"));
142
143            assert!(socket_addr.ip().is_loopback());
144            assert_eq!(socket_addr.port(), port);
145        }
146    }
147
148    #[test]
149    fn parse_ms_or_seconds() {
150        let ms = parse_duration_from_secs_or_ms("5ms").unwrap();
151        assert_eq!(ms, Duration::from_millis(5));
152
153        let seconds = parse_duration_from_secs_or_ms("5").unwrap();
154        assert_eq!(seconds, Duration::from_secs(5));
155
156        let seconds = parse_duration_from_secs_or_ms("5s").unwrap();
157        assert_eq!(seconds, Duration::from_secs(5));
158
159        assert!(parse_duration_from_secs_or_ms("5ns").is_err());
160    }
161
162    #[test]
163    fn parse_ether_values() {
164        // Test basic decimal value
165        let wei = parse_ether_value("1.05").unwrap();
166        assert_eq!(wei, 1_050_000_000_000_000_000u128);
167
168        // Test integer value
169        let wei = parse_ether_value("2").unwrap();
170        assert_eq!(wei, 2_000_000_000_000_000_000u128);
171
172        // Test zero
173        let wei = parse_ether_value("0").unwrap();
174        assert_eq!(wei, 0);
175
176        // Test negative value fails
177        assert!(parse_ether_value("-1").is_err());
178
179        // Test invalid input fails
180        assert!(parse_ether_value("abc").is_err());
181    }
182}