1use anyhow::{anyhow, bail, Context, Result};
7use serde::{Deserialize, Serialize};
8use std::path::{Path, PathBuf};
9use std::process::Command;
10use tracing::info;
11
12pub const MIN_SOLC_VERSION: &str = "0.6.0";
14
15#[derive(Debug, Clone)]
17pub struct SolcManager {
18 solc_path: PathBuf,
19 version: String,
20}
21
22#[derive(Debug, Clone, Deserialize, Serialize)]
24pub struct SolcOutput {
25 pub contracts: std::collections::HashMap<String, serde_json::Value>,
27 pub sources: std::collections::HashMap<String, SourceData>,
29}
30
31#[derive(Debug, Clone, Deserialize, Serialize)]
33pub struct SourceData {
34 #[serde(rename = "AST")]
35 pub ast: serde_json::Value,
37}
38
39impl SolcManager {
40 pub fn new() -> Result<Self> {
48 if let Ok(path) = std::env::var("SOLC_PATH") {
50 let path = PathBuf::from(path);
51 if path.exists() {
52 let version = Self::get_version(&path)?;
53 info!(
54 "Using solc from SOLC_PATH: {} (v{})",
55 path.display(),
56 version
57 );
58 return Ok(Self {
59 solc_path: path,
60 version,
61 });
62 }
63 }
64
65 if let Ok(output) = Command::new("solc").arg("--version").output() {
67 if output.status.success() {
68 let version_str = String::from_utf8_lossy(&output.stdout);
69 let version = Self::parse_version(&version_str)?;
70 info!("Using system solc (v{})", version);
71 return Ok(Self {
72 solc_path: PathBuf::from("solc"),
73 version,
74 });
75 }
76 }
77
78 let cache_path = Self::cache_path();
80 if cache_path.exists() {
81 let version = Self::get_version(&cache_path)?;
82 info!(
83 "Using cached solc from {} (v{})",
84 cache_path.display(),
85 version
86 );
87 return Ok(Self {
88 solc_path: cache_path,
89 version,
90 });
91 }
92
93 info!("solc not found locally, downloading...");
95 Self::download_solc()
96 }
97
98 pub fn get_ast_json(&self, source_path: &Path) -> Result<SolcOutput> {
100 let output = Command::new(&self.solc_path)
101 .args([
102 "--combined-json",
103 "ast,abi,bin,bin-runtime,srcmap,srcmap-runtime",
104 "--allow-paths",
105 ".",
106 ])
107 .arg(source_path)
108 .output()
109 .with_context(|| format!("Failed to run solc on: {}", source_path.display()))?;
110
111 if !output.status.success() {
112 let stderr = String::from_utf8_lossy(&output.stderr);
113 if Self::has_fatal_errors(&stderr) {
115 bail!("solc failed on {}: {}", source_path.display(), stderr);
116 }
117 }
118
119 let stdout = String::from_utf8_lossy(&output.stdout);
120 serde_json::from_str(&stdout).with_context(|| {
121 format!(
122 "Failed to parse solc JSON output for: {}",
123 source_path.display()
124 )
125 })
126 }
127
128 pub fn get_ast_for_source(&self, source: &str, _filename: &str) -> Result<SolcOutput> {
130 let tmp = tempfile::Builder::new().suffix(".sol").tempfile()?;
131 std::fs::write(tmp.path(), source)?;
132 self.get_ast_json(tmp.path())
133 }
134
135 fn cache_path() -> PathBuf {
136 let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
137 PathBuf::from(home)
138 .join(".sentri")
139 .join("solc")
140 .join("solc")
141 }
142
143 fn get_version(path: &Path) -> Result<String> {
144 let output = Command::new(path).arg("--version").output()?;
145 let stdout = String::from_utf8_lossy(&output.stdout);
146 Self::parse_version(&stdout)
147 }
148
149 fn parse_version(output: &str) -> Result<String> {
150 output
153 .lines()
154 .find(|l| l.contains("Version:"))
155 .and_then(|l| l.split_whitespace().nth(1))
156 .map(|v| v.split('+').next().unwrap_or(v).to_string())
157 .ok_or_else(|| anyhow!("Could not parse solc version"))
158 }
159
160 fn has_fatal_errors(stderr: &str) -> bool {
161 stderr
162 .lines()
163 .any(|l| l.to_lowercase().contains("error:") && !l.contains("Warning:"))
164 }
165
166 fn download_solc() -> Result<Self> {
167 let cache_path = Self::cache_path();
168 std::fs::create_dir_all(cache_path.parent().unwrap())?;
169
170 let platform = if cfg!(target_os = "macos") {
171 "macosx-amd64"
172 } else if cfg!(target_os = "windows") {
173 "windows-amd64"
174 } else {
175 "linux-amd64"
176 };
177
178 let version = "0.8.21";
179 let url =
180 format!("https://binaries.soliditylang.org/{platform}/solc-{platform}-v{version}");
181
182 info!("Downloading solc v{} for {}...", version, platform);
183
184 let response = ureq::get(&url)
185 .call()
186 .map_err(|e| anyhow!("Failed to download solc from {}: {}", url, e))?;
187
188 let mut file = std::fs::File::create(&cache_path)?;
189 std::io::copy(&mut response.into_reader(), &mut file)?;
190
191 #[cfg(unix)]
193 {
194 use std::os::unix::fs::PermissionsExt;
195 let mut perms = std::fs::metadata(&cache_path)?.permissions();
196 perms.set_mode(0o755);
197 std::fs::set_permissions(&cache_path, perms)?;
198 }
199
200 info!("Downloaded solc to {}", cache_path.display());
201
202 Ok(Self {
203 solc_path: cache_path,
204 version: version.to_string(),
205 })
206 }
207
208 pub fn version(&self) -> &str {
210 &self.version
211 }
212
213 pub fn path(&self) -> &Path {
215 &self.solc_path
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn test_parse_version() {
225 let output = "solc, the solidity compiler programm\nVersion: 0.8.21+commit.d9974bed";
226 let version = SolcManager::parse_version(output).unwrap();
227 assert_eq!(version, "0.8.21");
228 }
229
230 #[test]
231 fn test_has_fatal_errors() {
232 assert!(SolcManager::has_fatal_errors("Error: Something went wrong"));
233 assert!(!SolcManager::has_fatal_errors("Warning: Be careful"));
234 assert!(SolcManager::has_fatal_errors(
235 "Warning: Be careful\nError: Failed"
236 ));
237 }
238}