neo_solidity/cli/cli_parts/cli_manifest/
standards.rs

1/// Severity level for standards-detection diagnostics.
2#[derive(Debug, Clone, PartialEq)]
3enum StandardsDiagnosticLevel {
4    /// Contract almost matches a standard, or a detected standard has issues.
5    Warning,
6    /// Informational hint about parameter signatures or events.
7    Info,
8}
9
10/// A diagnostic emitted during standards detection.
11#[derive(Debug, Clone)]
12struct StandardsDiagnostic {
13    level: StandardsDiagnosticLevel,
14    standard: &'static str,
15    message: String,
16}
17
18/// Result of standards detection: detected standards + any diagnostics.
19struct 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    // ── NEP-17: Fungible Token Standard (ERC-20 equivalent) ──────────
45    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
52        validate_transfer_event(events, "NEP-17", 3, &mut diagnostics);
53        // Hint: NEP-17 transfer should have 4 params (from, to, amount, data)
54        check_transfer_params(&public_methods, "NEP-17", 4, &mut diagnostics);
55    } else if nep17_present.len() >= 3 && !has_ownerof {
56        // Near-miss: contract has most NEP-17 methods but not all
57        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    // ── NEP-11: Non-Fungible Token Standard (ERC-721 equivalent) ─────
76    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 (NEP-11 requires 4-param Transfer)
86        validate_transfer_event(events, "NEP-11", 4, &mut diagnostics);
87        // Hint: NEP-11 transfer should have 3 params (to, tokenId, data)
88        check_transfer_params(&public_methods, "NEP-11", 3, &mut diagnostics);
89    } else if has_ownerof && !has_nep11_xfer {
90        // Near-miss: has ownerOf (NFT signal) but no transfer mechanism
91        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    // ── NEP-24: Token Discovery / Royalty Standard ───────────────────
109    if names.contains("tokenuri") || names.contains("royaltyinfo") {
110        standards.push("NEP-24".to_string());
111    }
112
113    // ── NEP-26: Contract Upgrade Standard ────────────────────────────
114    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
142/// Check that a `Transfer` event exists with the expected parameter count.
143fn 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        _ => {} // Event present with correct param count — all good.
172    }
173}
174
175/// Hint when the `transfer` method parameter count doesn't match the standard.
176fn 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}