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, OUSD_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, OUSD: {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        "ousd" => Some(OUSD_ADDRESS),
71        _ => None,
72    }
73}
74
75fn token_id_to_address(token_id: u64) -> Address {
76    let mut address_bytes = [0u8; 20];
77    address_bytes[..12].copy_from_slice(&Address::TIP20_PREFIX);
78    address_bytes[12..20].copy_from_slice(&token_id.to_be_bytes());
79    Address::from(address_bytes)
80}
81
82/// Loads a TOML lanes file from `path`.
83///
84/// Each top-level key is a lane name, and the value is the `nonce_key` (an integer or a
85/// decimal/hex string parsed as [`U256`]).
86pub fn load_lanes(path: &Path) -> Result<BTreeMap<String, U256>> {
87    let contents = std::fs::read_to_string(path)
88        .map_err(|e| eyre!("failed to read tempo lanes file {}: {}", path.display(), e))?;
89    parse_lanes(&contents)
90        .map_err(|e| eyre!("failed to parse tempo lanes file {}: {}", path.display(), e))
91}
92
93fn parse_lanes(contents: &str) -> Result<BTreeMap<String, U256>> {
94    let raw: BTreeMap<String, toml::Value> = toml::from_str(contents)?;
95    let mut out = BTreeMap::new();
96    for (name, value) in raw {
97        let nonce_key = match value {
98            toml::Value::Integer(n) => {
99                if n < 0 {
100                    return Err(eyre!("invalid nonce_key for lane '{name}': must be non-negative"));
101                }
102                U256::from(n as u64)
103            }
104            toml::Value::String(s) => U256::from_str(s.trim())
105                .map_err(|e| eyre!("invalid nonce_key for lane '{name}': {e}"))?,
106            other => {
107                return Err(eyre!(
108                    "invalid nonce_key for lane '{name}': expected integer or string, got {}",
109                    other.type_str(),
110                ));
111            }
112        };
113        out.insert(name, nonce_key);
114    }
115    Ok(out)
116}
117
118/// Resolves `opts.lane` against a lanes file and writes the resulting `nonce_key` to
119/// `opts.nonce_key`. Returns the resolved lane (or `None` if no `--tempo.lane` was set).
120///
121/// `root` is the project root used to locate the default lanes file
122/// (`<root>/tempo.lanes.toml`) when `--tempo.lanes-file` was not provided.
123pub fn resolve_lane(opts: &mut TempoOpts, root: &Path) -> Result<Option<ResolvedLane>> {
124    let Some(lane_name) = opts.lane.clone() else { return Ok(None) };
125
126    let path: PathBuf = opts.lanes_file.clone().unwrap_or_else(|| root.join(DEFAULT_LANES_FILE));
127
128    if !path.exists() {
129        return Err(eyre!(
130            "tempo lanes file not found at {}\n\
131             create it with `name = <nonce_key>` entries, e.g.:\n  \
132             deploy   = 1\n  \
133             ops      = 2\n  \
134             payments = 3",
135            path.display(),
136        ));
137    }
138
139    let lanes = load_lanes(&path)?;
140
141    let nonce_key = lanes.get(&lane_name).copied().ok_or_else(|| {
142        let mut known: Vec<&str> = lanes.keys().map(String::as_str).collect();
143        known.sort_unstable();
144        eyre!(
145            "lane '{lane_name}' not found in {} (known lanes: {})",
146            path.display(),
147            if known.is_empty() { "<none>".to_string() } else { known.join(", ") },
148        )
149    })?;
150
151    opts.nonce_key = Some(nonce_key);
152    Ok(Some(ResolvedLane { name: lane_name, nonce_key }))
153}
154
155/// Prints `lane: <name> (nonce_key=<key>, nonce=<n>)` to stderr (so it doesn't pollute
156/// stdout for commands like `cast mktx` whose stdout is meant to be piped), giving
157/// visibility into which 2D nonce lane was used.
158pub fn maybe_print_resolved_lane(resolved: Option<&ResolvedLane>, nonce: u64) -> Result<()> {
159    if let Some(lane) = resolved {
160        sh_eprintln!("lane: {} (nonce_key={}, nonce={})", lane.name, lane.nonce_key, nonce)?;
161    }
162    Ok(())
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn parses_int_and_string_lane_values() {
171        let toml = r#"
172deploy   = 1
173ops      = 2
174payments = "3"
175big      = "115792089237316195423570985008687907853269984665640564039457584007913129639935"
176"#;
177        let lanes = parse_lanes(toml).unwrap();
178        assert_eq!(lanes.get("deploy"), Some(&U256::ONE));
179        assert_eq!(lanes.get("ops"), Some(&U256::from(2u64)));
180        assert_eq!(lanes.get("payments"), Some(&U256::from(3u64)));
181        assert_eq!(lanes.get("big"), Some(&U256::MAX));
182    }
183
184    #[test]
185    fn parse_lanes_rejects_invalid_string() {
186        let toml = "broken = \"not-a-number\"";
187        let err = parse_lanes(toml).unwrap_err();
188        assert!(err.to_string().contains("invalid nonce_key for lane 'broken'"));
189    }
190
191    #[test]
192    fn resolve_lane_sets_nonce_key_and_returns_resolved() {
193        let dir = tempfile::tempdir().unwrap();
194        let path = dir.path().join(DEFAULT_LANES_FILE);
195        std::fs::write(&path, "deploy = 7\npayments = 42\n").unwrap();
196
197        let mut opts = TempoOpts { lane: Some("payments".to_string()), ..Default::default() };
198        let resolved = resolve_lane(&mut opts, dir.path()).unwrap().unwrap();
199        assert_eq!(resolved.name, "payments");
200        assert_eq!(resolved.nonce_key, U256::from(42u64));
201        assert_eq!(opts.nonce_key, Some(U256::from(42u64)));
202    }
203
204    #[test]
205    fn resolve_lane_returns_none_when_no_lane() {
206        let dir = tempfile::tempdir().unwrap();
207        let mut opts = TempoOpts::default();
208        let resolved = resolve_lane(&mut opts, dir.path()).unwrap();
209        assert!(resolved.is_none());
210        assert!(opts.nonce_key.is_none());
211    }
212
213    #[test]
214    fn resolve_lane_errors_when_file_missing() {
215        let dir = tempfile::tempdir().unwrap();
216        let mut opts = TempoOpts { lane: Some("deploy".to_string()), ..Default::default() };
217        let err = resolve_lane(&mut opts, dir.path()).unwrap_err();
218        assert!(err.to_string().contains("tempo lanes file not found"));
219    }
220
221    #[test]
222    fn resolve_lane_errors_when_lane_unknown() {
223        let dir = tempfile::tempdir().unwrap();
224        let path = dir.path().join(DEFAULT_LANES_FILE);
225        std::fs::write(&path, "deploy = 1\nops = 2\n").unwrap();
226
227        let mut opts = TempoOpts { lane: Some("payments".to_string()), ..Default::default() };
228        let err = resolve_lane(&mut opts, dir.path()).unwrap_err();
229        let msg = err.to_string();
230        assert!(msg.contains("lane 'payments' not found"));
231        assert!(msg.contains("deploy, ops"));
232    }
233}