neo_solidity/cli/cli_parts/cli_manifest/
standards.rs1#[derive(Debug, Clone, PartialEq)]
3enum StandardsDiagnosticLevel {
4 Warning,
6 Info,
8}
9
10#[derive(Debug, Clone)]
12struct StandardsDiagnostic {
13 level: StandardsDiagnosticLevel,
14 standard: &'static str,
15 message: String,
16}
17
18struct StandardsDetectionResult {
20 standards: Vec<String>,
21 diagnostics: Vec<StandardsDiagnostic>,
22}
23
24fn detect_supported_standards(
25 methods: &[FunctionMetadata],
26 events: &[EventMetadata],
27) -> StandardsDetectionResult {
28 let public_methods: Vec<&FunctionMetadata> = methods
29 .iter()
30 .filter(|m| {
31 !matches!(m.kind, FunctionKind::Constructor)
32 && matches!(m.visibility, VisibilityKind::Public | VisibilityKind::External)
33 })
34 .collect();
35 let names: HashSet<String> = public_methods
36 .iter()
37 .map(|m| m.name.to_ascii_lowercase())
38 .collect();
39 let mut standards = Vec::new();
40 let mut diagnostics: Vec<StandardsDiagnostic> = Vec::new();
41
42 let has_ownerof = names.contains("ownerof");
43
44 let nep17_required = ["symbol", "decimals", "totalsupply", "balanceof", "transfer"];
46 let nep17_present: Vec<&&str> = nep17_required.iter().filter(|m| names.contains(**m)).collect();
47 let nep17_match = nep17_present.len() == nep17_required.len() && !has_ownerof;
48
49 if nep17_match {
50 standards.push("NEP-17".to_string());
51 validate_transfer_event(events, "NEP-17", 3, &mut diagnostics);
53 check_transfer_params(&public_methods, "NEP-17", 4, &mut diagnostics);
55 } else if nep17_present.len() >= 3 && !has_ownerof {
56 let missing: Vec<&str> = nep17_required
58 .iter()
59 .filter(|m| !names.contains(**m))
60 .copied()
61 .collect();
62 diagnostics.push(StandardsDiagnostic {
63 level: StandardsDiagnosticLevel::Warning,
64 standard: "NEP-17",
65 message: format!(
66 "contract has {} of {} required NEP-17 methods (missing: {}). \
67 Add the missing method(s) to enable NEP-17 standard detection.",
68 nep17_present.len(),
69 nep17_required.len(),
70 missing.join(", "),
71 ),
72 });
73 }
74
75 let nep11_core = ["balanceof", "ownerof"];
77 let has_nep11_xfer = names.contains("transfer")
78 || names.contains("transferfrom")
79 || names.contains("tokensof");
80 let nep11_match =
81 nep11_core.iter().all(|m| names.contains(*m)) && has_nep11_xfer;
82
83 if nep11_match {
84 standards.push("NEP-11".to_string());
85 validate_transfer_event(events, "NEP-11", 4, &mut diagnostics);
87 check_transfer_params(&public_methods, "NEP-11", 3, &mut diagnostics);
89 } else if has_ownerof && !has_nep11_xfer {
90 diagnostics.push(StandardsDiagnostic {
92 level: StandardsDiagnosticLevel::Warning,
93 standard: "NEP-11",
94 message: "contract has `ownerOf` (NFT signal) but no transfer mechanism. \
95 Add `transfer`, `transferFrom`, or `tokensOf` to enable NEP-11."
96 .to_string(),
97 });
98 } else if has_ownerof && has_nep11_xfer && !names.contains("balanceof") {
99 diagnostics.push(StandardsDiagnostic {
100 level: StandardsDiagnosticLevel::Warning,
101 standard: "NEP-11",
102 message: "contract has `ownerOf` and a transfer mechanism but is missing \
103 `balanceOf`. Add it to enable NEP-11 standard detection."
104 .to_string(),
105 });
106 }
107
108 if names.contains("tokenuri") || names.contains("royaltyinfo") {
110 standards.push("NEP-24".to_string());
111 }
112
113 let has_update = names.contains("update");
115 let has_destroy = names.contains("destroy");
116 if has_update && has_destroy {
117 standards.push("NEP-26".to_string());
118 } else if has_update && !has_destroy {
119 diagnostics.push(StandardsDiagnostic {
120 level: StandardsDiagnosticLevel::Info,
121 standard: "NEP-26",
122 message: "contract has `update` but is missing `destroy`. \
123 Add both to enable NEP-26 standard detection."
124 .to_string(),
125 });
126 } else if !has_update && has_destroy {
127 diagnostics.push(StandardsDiagnostic {
128 level: StandardsDiagnosticLevel::Info,
129 standard: "NEP-26",
130 message: "contract has `destroy` but is missing `update`. \
131 Add both to enable NEP-26 standard detection."
132 .to_string(),
133 });
134 }
135
136 StandardsDetectionResult {
137 standards,
138 diagnostics,
139 }
140}
141
142fn validate_transfer_event(
144 events: &[EventMetadata],
145 standard: &'static str,
146 expected_params: usize,
147 diagnostics: &mut Vec<StandardsDiagnostic>,
148) {
149 let transfer_event = events.iter().find(|e| e.name == "Transfer");
150 match transfer_event {
151 None => {
152 diagnostics.push(StandardsDiagnostic {
153 level: StandardsDiagnosticLevel::Warning,
154 standard,
155 message: format!(
156 "{standard} detected but contract is missing the required `Transfer` event \
157 ({expected_params} parameters expected).",
158 ),
159 });
160 }
161 Some(evt) if evt.parameters.len() != expected_params => {
162 diagnostics.push(StandardsDiagnostic {
163 level: StandardsDiagnosticLevel::Info,
164 standard,
165 message: format!(
166 "{standard} `Transfer` event has {} parameter(s), expected {expected_params}.",
167 evt.parameters.len(),
168 ),
169 });
170 }
171 _ => {} }
173}
174
175fn check_transfer_params(
177 public_methods: &[&FunctionMetadata],
178 standard: &'static str,
179 expected_params: usize,
180 diagnostics: &mut Vec<StandardsDiagnostic>,
181) {
182 if let Some(transfer) = public_methods
183 .iter()
184 .find(|m| m.name.eq_ignore_ascii_case("transfer"))
185 {
186 let actual = transfer.parameters.len();
187 if actual != expected_params {
188 diagnostics.push(StandardsDiagnostic {
189 level: StandardsDiagnosticLevel::Info,
190 standard,
191 message: format!(
192 "{standard} `transfer` method has {actual} parameter(s), \
193 spec expects {expected_params}. See STANDARDS_MAPPING.md for details.",
194 ),
195 });
196 }
197 }
198}