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