foundry_cli/utils/
tempo.rs1use 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
30pub const DEFAULT_LANES_FILE: &str = "tempo.lanes.toml";
32
33#[derive(Clone, Debug, PartialEq, Eq)]
35pub struct ResolvedLane {
36 pub name: String,
38 pub nonce_key: U256,
40}
41
42pub 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
54pub 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
90pub 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
127pub 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}