Skip to main content

foundry_cli/utils/
tempo.rs

1//! Tempo utilities: fee token parsing and named nonce lanes (2D nonces).
2//!
3//! A "lane" is a friendly alias for a Tempo `nonce_key` (a [`U256`]). Lanes are defined in a
4//! shared TOML file (default `tempo.lanes.toml` at the project root) so a team can reserve
5//! independent sequential nonce streams for parallel scripts without coordinating on raw
6//! `U256` selectors.
7//!
8//! Example `tempo.lanes.toml`:
9//!
10//! ```toml
11//! deploy   = 1
12//! ops      = 2
13//! payments = 3
14//! ```
15//!
16//! ```bash
17//! cast erc20 transfer ... --tempo.lane payments
18//! ```
19
20use crate::opts::TempoOpts;
21use alloy_primitives::{Address, U256};
22use eyre::{Result, eyre};
23use foundry_common::tempo::{
24    ALPHA_USD_ADDRESS, BETA_USD_ADDRESS, PATH_USD_ADDRESS, THETA_USD_ADDRESS,
25};
26use std::{
27    collections::BTreeMap,
28    path::{Path, PathBuf},
29    str::FromStr,
30};
31use tempo_primitives::TempoAddressExt;
32
33/// Default name of the lanes file at the project root.
34pub const DEFAULT_LANES_FILE: &str = "tempo.lanes.toml";
35
36/// Result of resolving a `--tempo.lane <name>` argument against a lanes file.
37#[derive(Clone, Debug, PartialEq, Eq)]
38pub struct ResolvedLane {
39    /// The lane name as provided on the CLI.
40    pub name: String,
41    /// The `nonce_key` the lane resolved to.
42    pub nonce_key: U256,
43}
44
45/// Parses a fee token address, numeric TIP-20 token id, or known Tempo fee-token symbol.
46pub fn parse_fee_token_address(symbol_or_address: &str) -> eyre::Result<Address> {
47    parse_fee_token_symbol(symbol_or_address).map_or_else(
48        || {
49            if let Ok(address) = Address::from_str(symbol_or_address) {
50                return Ok(address);
51            }
52
53            symbol_or_address.parse::<u64>().map(token_id_to_address).map_err(|e| {
54                eyre!(
55                    "invalid fee token '{symbol_or_address}': expected address, numeric TIP-20 token \
56                     id, or one of PathUSD, AlphaUSD, BetaUSD, ThetaUSD: {e}"
57                )
58            })
59        },
60        Ok,
61    )
62}
63
64fn parse_fee_token_symbol(symbol: &str) -> Option<Address> {
65    match symbol.trim().to_ascii_lowercase().as_str() {
66        "pathusd" | "path_usd" | "path-usd" | "usd" | "default" => Some(PATH_USD_ADDRESS),
67        "alphausd" | "alpha_usd" | "alpha-usd" => Some(ALPHA_USD_ADDRESS),
68        "betausd" | "beta_usd" | "beta-usd" => Some(BETA_USD_ADDRESS),
69        "thetausd" | "theta_usd" | "theta-usd" => Some(THETA_USD_ADDRESS),
70        _ => None,
71    }
72}
73
74fn token_id_to_address(token_id: u64) -> Address {
75    let mut address_bytes = [0u8; 20];
76    address_bytes[..12].copy_from_slice(&Address::TIP20_PREFIX);
77    address_bytes[12..20].copy_from_slice(&token_id.to_be_bytes());
78    Address::from(address_bytes)
79}
80
81/// Loads a TOML lanes file from `path`.
82///
83/// Each top-level key is a lane name, and the value is the `nonce_key` (an integer or a
84/// decimal/hex string parsed as [`U256`]).
85pub fn load_lanes(path: &Path) -> Result<BTreeMap<String, U256>> {
86    let contents = std::fs::read_to_string(path)
87        .map_err(|e| eyre!("failed to read tempo lanes file {}: {}", path.display(), e))?;
88    parse_lanes(&contents)
89        .map_err(|e| eyre!("failed to parse tempo lanes file {}: {}", path.display(), e))
90}
91
92fn parse_lanes(contents: &str) -> Result<BTreeMap<String, U256>> {
93    let raw: BTreeMap<String, toml::Value> = toml::from_str(contents)?;
94    let mut out = BTreeMap::new();
95    for (name, value) in raw {
96        let nonce_key = match value {
97            toml::Value::Integer(n) => {
98                if n < 0 {
99                    return Err(eyre!("invalid nonce_key for lane '{name}': must be non-negative"));
100                }
101                U256::from(n as u64)
102            }
103            toml::Value::String(s) => U256::from_str(s.trim())
104                .map_err(|e| eyre!("invalid nonce_key for lane '{name}': {e}"))?,
105            other => {
106                return Err(eyre!(
107                    "invalid nonce_key for lane '{name}': expected integer or string, got {}",
108                    other.type_str(),
109                ));
110            }
111        };
112        out.insert(name, nonce_key);
113    }
114    Ok(out)
115}
116
117/// Resolves `opts.lane` against a lanes file and writes the resulting `nonce_key` to
118/// `opts.nonce_key`. Returns the resolved lane (or `None` if no `--tempo.lane` was set).
119///
120/// `root` is the project root used to locate the default lanes file
121/// (`<root>/tempo.lanes.toml`) when `--tempo.lanes-file` was not provided.
122pub fn resolve_lane(opts: &mut TempoOpts, root: &Path) -> Result<Option<ResolvedLane>> {
123    let Some(lane_name) = opts.lane.clone() else { return Ok(None) };
124
125    let path: PathBuf = opts.lanes_file.clone().unwrap_or_else(|| root.join(DEFAULT_LANES_FILE));
126
127    if !path.exists() {
128        return Err(eyre!(
129            "tempo lanes file not found at {}\n\
130             create it with `name = <nonce_key>` entries, e.g.:\n  \
131             deploy   = 1\n  \
132             ops      = 2\n  \
133             payments = 3",
134            path.display(),
135        ));
136    }
137
138    let lanes = load_lanes(&path)?;
139
140    let nonce_key = lanes.get(&lane_name).copied().ok_or_else(|| {
141        let mut known: Vec<&str> = lanes.keys().map(String::as_str).collect();
142        known.sort_unstable();
143        eyre!(
144            "lane '{lane_name}' not found in {} (known lanes: {})",
145            path.display(),
146            if known.is_empty() { "<none>".to_string() } else { known.join(", ") },
147        )
148    })?;
149
150    opts.nonce_key = Some(nonce_key);
151    Ok(Some(ResolvedLane { name: lane_name, nonce_key }))
152}
153
154/// Prints `lane: <name> (nonce_key=<key>, nonce=<n>)` to stderr (so it doesn't pollute
155/// stdout for commands like `cast mktx` whose stdout is meant to be piped), giving
156/// visibility into which 2D nonce lane was used.
157pub fn maybe_print_resolved_lane(resolved: Option<&ResolvedLane>, nonce: u64) -> Result<()> {
158    if let Some(lane) = resolved {
159        sh_eprintln!("lane: {} (nonce_key={}, nonce={})", lane.name, lane.nonce_key, nonce)?;
160    }
161    Ok(())
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn parses_int_and_string_lane_values() {
170        let toml = r#"
171deploy   = 1
172ops      = 2
173payments = "3"
174big      = "115792089237316195423570985008687907853269984665640564039457584007913129639935"
175"#;
176        let lanes = parse_lanes(toml).unwrap();
177        assert_eq!(lanes.get("deploy"), Some(&U256::from(1u64)));
178        assert_eq!(lanes.get("ops"), Some(&U256::from(2u64)));
179        assert_eq!(lanes.get("payments"), Some(&U256::from(3u64)));
180        assert_eq!(lanes.get("big"), Some(&U256::MAX));
181    }
182
183    #[test]
184    fn parse_lanes_rejects_invalid_string() {
185        let toml = "broken = \"not-a-number\"";
186        let err = parse_lanes(toml).unwrap_err();
187        assert!(err.to_string().contains("invalid nonce_key for lane 'broken'"));
188    }
189
190    #[test]
191    fn resolve_lane_sets_nonce_key_and_returns_resolved() {
192        let dir = tempfile::tempdir().unwrap();
193        let path = dir.path().join(DEFAULT_LANES_FILE);
194        std::fs::write(&path, "deploy = 7\npayments = 42\n").unwrap();
195
196        let mut opts = TempoOpts { lane: Some("payments".to_string()), ..Default::default() };
197        let resolved = resolve_lane(&mut opts, dir.path()).unwrap().unwrap();
198        assert_eq!(resolved.name, "payments");
199        assert_eq!(resolved.nonce_key, U256::from(42u64));
200        assert_eq!(opts.nonce_key, Some(U256::from(42u64)));
201    }
202
203    #[test]
204    fn resolve_lane_returns_none_when_no_lane() {
205        let dir = tempfile::tempdir().unwrap();
206        let mut opts = TempoOpts::default();
207        let resolved = resolve_lane(&mut opts, dir.path()).unwrap();
208        assert!(resolved.is_none());
209        assert!(opts.nonce_key.is_none());
210    }
211
212    #[test]
213    fn resolve_lane_errors_when_file_missing() {
214        let dir = tempfile::tempdir().unwrap();
215        let mut opts = TempoOpts { lane: Some("deploy".to_string()), ..Default::default() };
216        let err = resolve_lane(&mut opts, dir.path()).unwrap_err();
217        assert!(err.to_string().contains("tempo lanes file not found"));
218    }
219
220    #[test]
221    fn resolve_lane_errors_when_lane_unknown() {
222        let dir = tempfile::tempdir().unwrap();
223        let path = dir.path().join(DEFAULT_LANES_FILE);
224        std::fs::write(&path, "deploy = 1\nops = 2\n").unwrap();
225
226        let mut opts = TempoOpts { lane: Some("payments".to_string()), ..Default::default() };
227        let err = resolve_lane(&mut opts, dir.path()).unwrap_err();
228        let msg = err.to_string();
229        assert!(msg.contains("lane 'payments' not found"));
230        assert!(msg.contains("deploy, ops"));
231    }
232}