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, 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
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, 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
82pub 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
118pub 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
155pub 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}