foundry_cli/utils/
tempo.rs1use 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
33pub const DEFAULT_LANES_FILE: &str = "tempo.lanes.toml";
35
36#[derive(Clone, Debug, PartialEq, Eq)]
38pub struct ResolvedLane {
39 pub name: String,
41 pub nonce_key: U256,
43}
44
45pub 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
81pub 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
117pub 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
154pub 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}