neo_solidity/cli/cli_parts/cli_manifest/
build.rs

1fn parse_json_value(raw: &str, tag: &str) -> Option<Value> {
2    match serde_json::from_str::<Value>(raw) {
3        Ok(value) => Some(value),
4        Err(err) => {
5            eprintln!(
6                "warning: ignoring @custom:{tag} because its value is not valid JSON: {err}"
7            );
8            None
9        }
10    }
11}
12
13fn parse_json_or_string(raw: &str) -> Value {
14    serde_json::from_str::<Value>(raw).unwrap_or_else(|_| Value::String(raw.to_string()))
15}
16
17fn upsert_manifest_extra(manifest: &mut Value) -> Option<&mut serde_json::Map<String, Value>> {
18    let object = manifest.as_object_mut()?;
19    let entry = object
20        .entry("extra".to_string())
21        .or_insert_with(|| Value::Object(serde_json::Map::new()));
22
23    if !entry.is_object() {
24        *entry = Value::Object(serde_json::Map::new());
25    }
26
27    entry.as_object_mut()
28}
29
30fn apply_manifest_custom_overrides(manifest: &mut Value, metadata: &ContractMetadata) {
31    for (tag, raw_value) in &metadata.documentation.custom {
32        let raw = raw_value.trim();
33        if raw.is_empty() {
34            continue;
35        }
36
37        let Some(field) = tag
38            .strip_prefix("neo.manifest.")
39            .or_else(|| tag.strip_prefix("manifest."))
40        else {
41            continue;
42        };
43
44        match field {
45            "name" => {
46                let parsed = parse_json_or_string(raw);
47                let name = parsed
48                    .as_str()
49                    .map(|s| s.to_string())
50                    .unwrap_or_else(|| raw.to_string());
51                manifest["name"] = Value::String(name);
52            }
53            "groups" => {
54                let Some(value) = parse_json_value(raw, tag) else {
55                    continue;
56                };
57                if value.is_array() {
58                    manifest["groups"] = value;
59                } else {
60                    eprintln!(
61                        "warning: ignoring @custom:{tag}; expected a JSON array for manifest.groups"
62                    );
63                }
64            }
65            "features" => {
66                let Some(value) = parse_json_value(raw, tag) else {
67                    continue;
68                };
69                if !value.is_object() {
70                    eprintln!(
71                        "warning: ignoring @custom:{tag}; expected a JSON object for manifest.features"
72                    );
73                } else if value
74                    .as_object()
75                    .is_some_and(|features| features.is_empty())
76                {
77                    manifest["features"] = value;
78                } else {
79                    eprintln!(
80                        "warning: ignoring @custom:{tag}; Neo N3 requires manifest.features to be an empty object"
81                    );
82                }
83            }
84            "supportedstandards" => {
85                let Some(value) = parse_json_value(raw, tag) else {
86                    continue;
87                };
88                if value.is_array() {
89                    manifest["supportedstandards"] = value;
90                } else {
91                    eprintln!(
92                        "warning: ignoring @custom:{tag}; expected a JSON array for manifest.supportedstandards"
93                    );
94                }
95            }
96            "trusts" => {
97                let Some(value) = parse_json_value(raw, tag) else {
98                    continue;
99                };
100                if value.is_array() || value.is_string() {
101                    manifest["trusts"] = value;
102                } else {
103                    eprintln!(
104                        "warning: ignoring @custom:{tag}; expected a JSON array or string for manifest.trusts"
105                    );
106                }
107            }
108            _ => {
109                if let Some(extra_key) = field.strip_prefix("extra.") {
110                    if extra_key.is_empty() {
111                        continue;
112                    }
113                    if let Some(extra) = upsert_manifest_extra(manifest) {
114                        extra.insert(extra_key.to_string(), parse_json_or_string(raw));
115                    }
116                }
117            }
118        }
119    }
120}
121
122fn build_manifest(metadata: &ContractMetadata, ir_module: &ir::Module) -> serde_json::Value {
123    fn neotype_to_manifest_type(neotype: Option<&NeoType>, solidity_type: &str) -> &'static str {
124        match neotype {
125            Some(NeoType::Integer { .. }) => "Integer",
126            Some(NeoType::Boolean) => "Boolean",
127            Some(NeoType::String) => "String",
128            Some(NeoType::Address) => "Hash160",
129            Some(NeoType::ByteArray { fixed_len: Some(32) }) => "Hash256",
130            Some(NeoType::ByteArray { .. }) => "ByteArray",
131            Some(NeoType::Array(_)) => "Array",
132            Some(NeoType::Mapping { .. }) => "Map",
133            Some(NeoType::Struct { .. }) => "Array",
134            Some(NeoType::Any) | None => standard_json::solidity_to_manifest_type(solidity_type),
135        }
136    }
137
138    let abi_methods: Vec<&FunctionMetadata> = metadata
139        .methods
140        .iter()
141        .filter(|method| {
142            !matches!(method.kind, FunctionKind::Constructor)
143                && matches!(
144                    method.visibility,
145                    VisibilityKind::Public | VisibilityKind::External
146                )
147        })
148        .collect();
149
150    // Neo N3 dispatches methods by `name` and does not provide Solidity-style overload
151    // resolution. To keep manifests compatible with NEP tooling while still supporting
152    // overloaded functions, we emit:
153    // - the Solidity-visible `name` for a single "primary" overload (picked by max arity)
154    // - the mangled `neo_name` for all other overloads
155    //
156    // This guarantees unique manifest ABI names and preserves canonical names for common
157    // standards (e.g., NEP-17 `transfer(from,to,amount,data)` vs a convenience overload).
158    let mut overload_groups: HashMap<&str, Vec<&FunctionMetadata>> = HashMap::new();
159    for method in &abi_methods {
160        overload_groups
161            .entry(method.name.as_str())
162            .or_default()
163            .push(*method);
164    }
165
166    let mut primary_overload: HashMap<&str, &FunctionMetadata> = HashMap::new();
167    for (name, group) in &overload_groups {
168        if group.len() <= 1 {
169            continue;
170        }
171
172        let mut best = group[0];
173        for candidate in group.iter().skip(1) {
174            let best_params = best.parameters.len();
175            let cand_params = candidate.parameters.len();
176            if cand_params > best_params {
177                best = candidate;
178                continue;
179            }
180            if cand_params == best_params && candidate.neo_name < best.neo_name {
181                best = candidate;
182            }
183        }
184        primary_overload.insert(*name, best);
185    }
186
187    let methods_json: Vec<_> = abi_methods
188        .iter()
189        .map(|method| {
190            let is_overloaded = overload_groups
191                .get(method.name.as_str())
192                .is_some_and(|group| group.len() > 1);
193
194            let is_primary_overload = primary_overload
195                .get(method.name.as_str())
196                .is_some_and(|primary| primary.neo_name == method.neo_name)
197                && is_overloaded;
198
199            let abi_name = if !is_overloaded || is_primary_overload {
200                method.name.clone()
201            } else {
202                method.neo_name.clone()
203            };
204
205            let params_json: Vec<_> = method
206                .parameters
207                .iter()
208                .enumerate()
209                .map(|(param_index, param)| {
210                    json!({
211                        "name": param
212                            .name
213                            .clone()
214                            .unwrap_or_else(|| format!("arg{}", param_index)),
215                        "type": neotype_to_manifest_type(param.neo_type.as_ref(), &param.ty),
216                    })
217                })
218                .collect();
219
220            json!({
221                "name": abi_name,
222                "offset": method.offset,
223                "parameters": params_json,
224                "returntype": if method.return_parameters.len() > 1 {
225                    "Array"
226                } else {
227                    method
228                        .return_parameters
229                        .first()
230                        .map(|param| neotype_to_manifest_type(param.neo_type.as_ref(), &param.ty))
231                        .unwrap_or("Void")
232                },
233                "safe": method.state_mutability.is_safe(),
234            })
235        })
236        .collect();
237
238    let events_json: Vec<_> = metadata
239        .events
240        .iter()
241        .map(|event| {
242            let params: Vec<_> = event
243                .parameters
244                .iter()
245                .enumerate()
246                .map(|(idx, param)| {
247                    json!({
248                        "name": param
249                            .name
250                            .clone()
251                            .unwrap_or_else(|| format!("param{}", idx)),
252                        "type": standard_json::solidity_to_manifest_type(&param.ty),
253                    })
254                })
255                .collect();
256
257            json!({
258                "name": event.name,
259                "parameters": params,
260            })
261        })
262        .collect();
263
264    let detection = detect_supported_standards(&metadata.methods, &metadata.events);
265    let supported_standards = detection.standards;
266    for diag in &detection.diagnostics {
267        let prefix = match diag.level {
268            StandardsDiagnosticLevel::Warning => "warning",
269            StandardsDiagnosticLevel::Info => "info",
270        };
271        eprintln!(
272            "[{prefix}][{standard}] {msg}",
273            standard = diag.standard,
274            msg = diag.message
275        );
276    }
277    let permissions = infer_permissions(metadata, ir_module);
278    // Neo N3 keeps `features` reserved for future use; Neo's manifest parser will
279    // reject any populated keys. Keep the object empty for chain compatibility.
280    let features = serde_json::Map::new();
281
282    let mut manifest = json!({
283        "name": metadata.name,
284        "groups": [],
285        "features": features,
286        "supportedstandards": supported_standards,
287        "abi": {
288            "methods": methods_json,
289            "events": events_json,
290        },
291        "permissions": permissions,
292        "trusts": [],
293        "extra": {
294            "Author": COMPILER_EMAIL,
295            "Description": format!("Solidity contract '{}' compiled to NeoVM", metadata.name),
296            "Version": compiler_version_string_4(),
297            "Compiler": COMPILER_ID,
298        }
299    });
300
301    apply_manifest_custom_overrides(&mut manifest, metadata);
302    manifest
303}