neo_solidity/cli/standard_json/standard_json_process/
process.rs

1pub(crate) fn process_standard_json(
2    input_path: &str,
3    output_path: Option<&str>,
4    options: StandardJsonOptions<'_>,
5) -> Result<(), String> {
6    let input_content = fs::read_to_string(input_path)
7        .map_err(|err| format!("Failed to read input file: {err}"))?;
8    process_standard_json_content(&input_content, output_path, options)
9}
10
11pub(crate) fn process_standard_json_content(
12    input_content: &str,
13    output_path: Option<&str>,
14    options: StandardJsonOptions<'_>,
15) -> Result<(), String> {
16    let request: StandardJsonInput = serde_json::from_str(input_content)
17        .map_err(|err| format!("Failed to parse standard JSON input: {err}"))?;
18
19    if request.sources.is_empty() {
20        return Err("Standard JSON input must contain at least one source".to_string());
21    }
22
23    if !request.language.trim().is_empty() && !request.language.eq_ignore_ascii_case("solidity") {
24        return Err(format!(
25            "Unsupported language '{}' in standard JSON input",
26            request.language
27        ));
28    }
29
30    let mut optimizer_level = options.optimizer_level.min(3);
31    optimizer_level = read_optimizer_level(&request.settings).unwrap_or(optimizer_level);
32    let mut contracts_output = Map::new();
33    let mut sources_output = Map::new();
34    let mut errors: Vec<Value> = Vec::new();
35    if let Some(warning) = unsupported_settings_warning(&request.settings) {
36        errors.push(warning);
37    }
38    let mut ordered_sources: Vec<(String, String, String)> = Vec::new();
39
40    for (index, (file_name, source)) in request.sources.iter().enumerate() {
41        sources_output.insert(file_name.clone(), json!({ "id": index as u32 }));
42        let Some(content) = source.content.as_ref() else {
43            let mut message = format!(
44                "Source '{file_name}' is missing inline content; URL imports are not supported."
45            );
46            if let Some(urls) = &source.urls {
47                if let Some(first_url) = urls.first() {
48                    message.push_str(&format!(" First URL provided: '{first_url}'."));
49                }
50            }
51            errors.push(json!({
52                "component": "neo-solidity",
53                "severity": "error",
54                "type": "MissingSourceContent",
55                "sourceLocation": { "file": file_name },
56                "formattedMessage": message,
57                "message": message,
58            }));
59            continue;
60        };
61
62        let keccak_hex = keccak256_hex(content);
63        let keccak_prefixed = hex_prefixed(&keccak_hex);
64        sources_output.insert(
65            file_name.clone(),
66            json!({
67                "id": index as u32,
68                "keccak256": keccak_prefixed,
69            }),
70        );
71
72        ordered_sources.push((file_name.clone(), content.clone(), keccak_prefixed));
73    }
74
75    if !ordered_sources.is_empty() {
76        let combined_source =
77            build_combined_source_with_import_validation(&ordered_sources, &mut errors);
78
79        let contract_file_map = build_contract_file_map(&ordered_sources);
80
81        match compile_contracts_with_options(
82            &combined_source,
83            false,
84            CompileOptions {
85                optimizer_level,
86                use_callt: options.use_callt,
87                deny_wildcard_permissions: options.deny_wildcard_permissions,
88                deny_wildcard_contracts: options.deny_wildcard_contracts,
89                deny_wildcard_methods: options.deny_wildcard_methods,
90                manifest_permissions: options.manifest_permissions.clone(),
91            },
92        ) {
93            Ok(artifacts) => {
94                let artifacts: Vec<CompilationArtifacts> = if options.contract_names.is_empty() {
95                    artifacts
96                } else {
97                    artifacts
98                        .into_iter()
99                        .filter(|artifact| {
100                            options
101                                .contract_names
102                                .iter()
103                                .any(|name| name == &artifact.metadata.name)
104                        })
105                        .collect()
106                };
107
108                if artifacts.is_empty() {
109                    let file_name = &ordered_sources[0].0;
110                    if options.contract_names.is_empty() {
111                        errors.push(json!({
112                            "component": "neo-solidity",
113                            "severity": "error",
114                            "type": "NoContracts",
115                            "sourceLocation": { "file": file_name },
116                            "formattedMessage": format!("No contracts found in {file_name}"),
117                            "message": format!("No contracts found in {file_name}"),
118                        }));
119                    } else {
120                        let names = options.contract_names.join(", ");
121                        errors.push(json!({
122                            "component": "neo-solidity",
123                            "severity": "error",
124                            "type": "ContractNotFound",
125                            "sourceLocation": { "file": file_name },
126                            "formattedMessage": format!("No matching contract(s) found for --contract {names}"),
127                            "message": format!("No matching contract(s) found for --contract {names}"),
128                        }));
129                    }
130                } else {
131                    for artifact in artifacts {
132                        let target_file = contract_file_map
133                            .get(&artifact.metadata.name)
134                            .cloned()
135                            .unwrap_or_else(|| ordered_sources[0].0.clone());
136
137                        let source_keccak = ordered_sources
138                            .iter()
139                            .find(|(name, _, _)| *name == target_file)
140                            .map(|(_, _, keccak)| keccak.as_str());
141
142                        let abi_entries = build_standard_abi(&artifact.metadata);
143                        let raw_source = options.nef_source.unwrap_or(target_file.as_str());
144                        let (source_field, truncated) = clamp_nef_source_with_flag(raw_source);
145                        if truncated {
146                            errors.push(json!({
147                                "component": "neo-solidity",
148                                "severity": "warning",
149                                "type": "NefSourceTruncated",
150                                "code": "NEF_SOURCE_TRUNCATED",
151                                "sourceLocation": { "file": target_file },
152                                "formattedMessage": format!(
153                                    "NEF source exceeds {} bytes and will be truncated",
154                                    NEF_SOURCE_MAX_BYTES
155                                ),
156                                "message": format!(
157                                    "NEF source exceeds {} bytes and will be truncated",
158                                    NEF_SOURCE_MAX_BYTES
159                                ),
160                            }));
161                        }
162
163                        let compiled_contract = match build_compiled_contract_value(
164                            &target_file,
165                            &artifact,
166                            &abi_entries,
167                            &request.settings,
168                            source_keccak,
169                            Some(source_field.as_ref()),
170                        ) {
171                            Ok(value) => value,
172                            Err(message) => {
173                                errors.push(json!({
174                                    "component": "neo-solidity",
175                                    "severity": "error",
176                                    "type": "StandardJsonOutput",
177                                    "sourceLocation": { "file": target_file },
178                                    "formattedMessage": message,
179                                    "message": message,
180                                }));
181                                continue;
182                            }
183                        };
184
185                        let per_file_value = contracts_output
186                            .entry(target_file.clone())
187                            .or_insert_with(|| Value::Object(Map::new()));
188                        let per_file = match per_file_value {
189                            Value::Object(map) => map,
190                            other => {
191                                *other = Value::Object(Map::new());
192                                match other {
193                                    Value::Object(map) => map,
194                                    _ => {
195                                        errors.push(json!({
196                                            "component": "neo-solidity",
197                                            "severity": "error",
198                                            "type": "StandardJsonOutput",
199                                            "sourceLocation": { "file": target_file },
200                                            "formattedMessage": "Failed to build standard JSON output: contracts entry is not an object",
201                                            "message": "Failed to build standard JSON output: contracts entry is not an object",
202                                        }));
203                                        continue;
204                                    }
205                                }
206                            }
207                        };
208
209                        per_file.insert(artifact.metadata.name.clone(), compiled_contract);
210
211                        for warning in &artifact.warnings {
212                            errors.push(diagnostic_to_standard_error(warning, &target_file));
213                        }
214                    }
215                }
216            }
217            Err(err) => {
218                let file_hint = ordered_sources
219                    .first()
220                    .map(|(name, _, _)| name.as_str())
221                    .unwrap_or("unknown");
222                errors.extend(err.into_errors(file_hint));
223            }
224        }
225    }
226
227    let mut output = Map::new();
228    output.insert("contracts".into(), Value::Object(contracts_output));
229    output.insert("sources".into(), Value::Object(sources_output));
230    if !errors.is_empty() {
231        output.insert("errors".into(), Value::Array(errors));
232    }
233
234    let serialized = serde_json::to_string_pretty(&Value::Object(output))
235        .map_err(|err| format!("Failed to serialise standard JSON output: {err}"))?;
236    if let Some(path) = output_path {
237        fs::write(path, serialized)
238            .map_err(|err| format!("Failed to write standard JSON output: {err}"))?;
239    } else {
240        println!("{serialized}");
241    }
242
243    Ok(())
244}