Skip to main content

ic_query/sns/report/model/reports/neurons/
detail.rs

1//! Module: sns::report::model::reports::neurons::detail
2//!
3//! Responsibility: exact SNS neuron detail and permission-evidence DTOs.
4//! Does not own: live Governance calls, SNS discovery, or text rendering.
5//! Boundary: preserves variable-size native neuron evidence outside fixed-size list caches.
6
7use super::SnsNeuronRow;
8use serde::{Deserialize as SerdeDeserialize, Serialize};
9
10///
11/// SnsPolicyObservationStatus
12///
13/// Tri-state result for one observed maturity-conversion policy condition.
14///
15
16#[derive(Clone, Copy, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
17#[serde(rename_all = "snake_case")]
18pub enum SnsPolicyObservationStatus {
19    /// Every available value satisfies the observed condition.
20    ObservedSatisfied,
21    /// At least one available value violates the observed condition.
22    Violated,
23    /// Unknown or anomalous evidence prevents a closed-world assessment.
24    Unassessable,
25}
26
27impl SnsPolicyObservationStatus {
28    /// Return the stable report label for this status.
29    #[must_use]
30    pub const fn as_str(self) -> &'static str {
31        match self {
32            Self::ObservedSatisfied => "observed_satisfied",
33            Self::Violated => "violated",
34            Self::Unassessable => "unassessable",
35        }
36    }
37
38    /// Combine two observations, preserving a known violation over uncertainty.
39    #[must_use]
40    pub const fn combine(self, other: Self) -> Self {
41        match (self, other) {
42            (Self::Violated, _) | (_, Self::Violated) => Self::Violated,
43            (Self::Unassessable, _) | (_, Self::Unassessable) => Self::Unassessable,
44            (Self::ObservedSatisfied, Self::ObservedSatisfied) => Self::ObservedSatisfied,
45        }
46    }
47}
48
49///
50/// SnsNeuronPermissionValue
51///
52/// Raw SNS Governance permission code with its current native label.
53///
54
55#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
56#[serde(deny_unknown_fields)]
57pub struct SnsNeuronPermissionValue {
58    /// Raw integer permission code returned by Governance.
59    pub code: i32,
60    /// Current native permission label, or `unknown` for an unrecognized code.
61    pub name: String,
62}
63
64impl SnsNeuronPermissionValue {
65    /// Construct one permission value from its raw Governance code.
66    #[must_use]
67    pub fn from_code(code: i32) -> Self {
68        Self {
69            code,
70            name: sns_neuron_permission_name(code).to_string(),
71        }
72    }
73}
74
75///
76/// SnsNeuronPermissionRow
77///
78/// Permissions held by one principal on an SNS neuron.
79///
80
81#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
82#[serde(deny_unknown_fields)]
83pub struct SnsNeuronPermissionRow {
84    /// Canonical principal text when Governance supplied the permission holder.
85    pub principal: Option<String>,
86    /// Raw permission codes and current native labels.
87    pub permission_types: Vec<SnsNeuronPermissionValue>,
88}
89
90///
91/// SnsNeuronAccount
92///
93/// Native optional destination account retained for a pending maturity disbursement.
94///
95
96#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
97#[serde(deny_unknown_fields)]
98pub struct SnsNeuronAccount {
99    /// Canonical destination owner when Governance supplied one.
100    pub owner: Option<String>,
101    /// Native destination subaccount encoded as lowercase hexadecimal when supplied.
102    pub subaccount_hex: Option<String>,
103}
104
105///
106/// SnsMaturityDisbursementRow
107///
108/// Native maturity disbursement that Governance has not yet finalized.
109///
110
111#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
112#[serde(deny_unknown_fields)]
113pub struct SnsMaturityDisbursementRow {
114    /// Unix timestamp at which the maturity disbursement was scheduled.
115    pub timestamp_of_disbursement_seconds: u64,
116    /// Raw scheduled amount in e8s.
117    pub amount_e8s: u64,
118    /// Complete optional destination account returned by Governance.
119    pub account_to_disburse_to: Option<SnsNeuronAccount>,
120    /// Unix timestamp at which Governance expects to finalize the disbursement.
121    pub finalize_disbursement_timestamp_seconds: Option<u64>,
122}
123
124///
125/// SnsNeuronFolloweesRow
126///
127/// Legacy function-based followees retained from one SNS neuron.
128///
129
130#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
131#[serde(deny_unknown_fields)]
132pub struct SnsNeuronFolloweesRow {
133    /// Native nervous-system function identifier.
134    pub function_id: u64,
135    /// Full 32-byte followee neuron identifiers encoded as lowercase hexadecimal.
136    pub followee_neuron_ids: Vec<String>,
137}
138
139///
140/// SnsNeuronFolloweeRow
141///
142/// One native topic-following target and its optional alias.
143///
144
145#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
146#[serde(deny_unknown_fields)]
147pub struct SnsNeuronFolloweeRow {
148    /// Full followee neuron identifier when Governance supplied one.
149    pub neuron_id: Option<String>,
150    /// Native followee alias when Governance supplied one.
151    pub alias: Option<String>,
152}
153
154///
155/// SnsNeuronTopicFolloweesRow
156///
157/// Native topic-following entry retained from one SNS neuron.
158///
159
160#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
161#[serde(deny_unknown_fields)]
162pub struct SnsNeuronTopicFolloweesRow {
163    /// Raw topic code used as the native topic-following map key.
164    pub topic_code: i32,
165    /// Current native topic label when Governance supplied a known topic variant.
166    pub topic: Option<String>,
167    /// Native topic followees in response order.
168    pub followees: Vec<SnsNeuronFolloweeRow>,
169}
170
171///
172/// SnsNeuronDetail
173///
174/// Full native detail evidence for exactly one SNS neuron.
175///
176
177#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
178#[serde(deny_unknown_fields)]
179pub struct SnsNeuronDetail {
180    /// Fixed-size neuron state shared with bounded list and snapshot rows.
181    pub neuron: SnsNeuronRow,
182    /// Every current principal permission entry.
183    pub permissions: Vec<SnsNeuronPermissionRow>,
184    /// Every pending maturity disbursement.
185    pub disburse_maturity_in_progress: Vec<SnsMaturityDisbursementRow>,
186    /// Legacy function-based followees.
187    pub followees: Vec<SnsNeuronFolloweesRow>,
188    /// Topic-based followees when the native optional collection is present.
189    pub topic_followees: Option<Vec<SnsNeuronTopicFolloweesRow>>,
190    /// Observed status for disabling maturity mint conversions through permissions 7 and 8.
191    pub maturity_mint_conversion_observed_disabled: SnsPolicyObservationStatus,
192    /// Observed status for disabling manual maturity staking through permission 9.
193    pub manual_maturity_staking_observed_disabled: SnsPolicyObservationStatus,
194}
195
196impl SnsNeuronDetail {
197    /// Recompute both neuron-local maturity policy observations from raw evidence.
198    #[must_use]
199    pub fn derived_policy_observations(
200        &self,
201    ) -> (SnsPolicyObservationStatus, SnsPolicyObservationStatus) {
202        neuron_policy_observations(
203            &self.permissions,
204            !self.disburse_maturity_in_progress.is_empty(),
205        )
206    }
207}
208
209pub(in crate::sns::report::model) fn neuron_policy_observations(
210    permissions: &[SnsNeuronPermissionRow],
211    has_pending_maturity_disbursement: bool,
212) -> (SnsPolicyObservationStatus, SnsPolicyObservationStatus) {
213    let mut mint = if has_pending_maturity_disbursement {
214        SnsPolicyObservationStatus::Violated
215    } else {
216        SnsPolicyObservationStatus::ObservedSatisfied
217    };
218    let mut staking = if permissions.is_empty() {
219        mint = mint.combine(SnsPolicyObservationStatus::Unassessable);
220        SnsPolicyObservationStatus::Unassessable
221    } else {
222        SnsPolicyObservationStatus::ObservedSatisfied
223    };
224    for permission in permissions {
225        if permission.principal.is_none() || permission.permission_types.is_empty() {
226            mint = mint.combine(SnsPolicyObservationStatus::Unassessable);
227            staking = staking.combine(SnsPolicyObservationStatus::Unassessable);
228        }
229        for code in permission.permission_types.iter().map(|value| value.code) {
230            let (code_mint, code_staking) = permission_code_policy_observations(code);
231            mint = mint.combine(code_mint);
232            staking = staking.combine(code_staking);
233        }
234    }
235    (mint, staking)
236}
237
238pub(in crate::sns::report::model) const fn permission_code_policy_observations(
239    code: i32,
240) -> (SnsPolicyObservationStatus, SnsPolicyObservationStatus) {
241    match code {
242        7 | 8 => (
243            SnsPolicyObservationStatus::Violated,
244            SnsPolicyObservationStatus::ObservedSatisfied,
245        ),
246        9 => (
247            SnsPolicyObservationStatus::ObservedSatisfied,
248            SnsPolicyObservationStatus::Violated,
249        ),
250        0 | 11..=i32::MAX | i32::MIN..=-1 => (
251            SnsPolicyObservationStatus::Unassessable,
252            SnsPolicyObservationStatus::Unassessable,
253        ),
254        1..=6 | 10 => (
255            SnsPolicyObservationStatus::ObservedSatisfied,
256            SnsPolicyObservationStatus::ObservedSatisfied,
257        ),
258    }
259}
260
261///
262/// SnsNeuronDetailReport
263///
264/// Serializable live report for one exact SNS Governance neuron lookup.
265///
266
267#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
268#[serde(deny_unknown_fields)]
269pub struct SnsNeuronDetailReport {
270    /// Report schema version.
271    pub schema_version: u32,
272    /// Requested IC network identity.
273    pub network: String,
274    /// SNS-W canister used for targeted discovery.
275    pub sns_wasm_canister_id: String,
276    /// Collection timestamp in UTC.
277    pub fetched_at: String,
278    /// IC API endpoint used for discovery and Governance calls.
279    pub source_endpoint: String,
280    /// Collector identity recorded in report provenance.
281    pub fetched_by: String,
282    /// Current SNS-W list position retained as display metadata.
283    pub id: usize,
284    /// Current SNS name retained as display metadata.
285    pub name: String,
286    /// Stable SNS Root canister identity.
287    pub root_canister_id: String,
288    /// Stable SNS Governance canister identity.
289    pub governance_canister_id: String,
290    /// Exact requested neuron identifier.
291    pub neuron_id: String,
292    /// Explicit report data source; exact detail reports are live-only.
293    pub data_source: String,
294    /// Full native neuron detail and derived policy observations.
295    pub detail: SnsNeuronDetail,
296}
297
298/// Return the current native label for one raw SNS neuron permission code.
299#[must_use]
300pub const fn sns_neuron_permission_name(code: i32) -> &'static str {
301    match code {
302        0 => "unspecified",
303        1 => "configure_dissolve_state",
304        2 => "manage_principals",
305        3 => "submit_proposal",
306        4 => "vote",
307        5 => "disburse",
308        6 => "split",
309        7 => "merge_maturity",
310        8 => "disburse_maturity",
311        9 => "stake_maturity",
312        10 => "manage_voting_permission",
313        _ => "unknown",
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320
321    #[test]
322    fn permission_names_cover_native_and_unknown_codes() {
323        for (code, expected) in [
324            (0, "unspecified"),
325            (1, "configure_dissolve_state"),
326            (2, "manage_principals"),
327            (3, "submit_proposal"),
328            (4, "vote"),
329            (5, "disburse"),
330            (6, "split"),
331            (7, "merge_maturity"),
332            (8, "disburse_maturity"),
333            (9, "stake_maturity"),
334            (10, "manage_voting_permission"),
335            (11, "unknown"),
336            (-1, "unknown"),
337        ] {
338            assert_eq!(sns_neuron_permission_name(code), expected, "code {code}");
339        }
340    }
341
342    #[test]
343    fn policy_observations_fail_closed_and_prioritize_known_violations() {
344        let mut detail = SnsNeuronDetail {
345            neuron: SnsNeuronRow {
346                neuron_id: "00".repeat(32),
347                cached_neuron_stake_e8s: 0,
348                maturity_e8s_equivalent: 0,
349                staked_maturity_e8s_equivalent: None,
350                created_timestamp_seconds: 0,
351                created_at: "1970-01-01T00:00:00Z".to_string(),
352                source_nns_neuron_id: None,
353                auto_stake_maturity: None,
354                aging_since_timestamp_seconds: 0,
355                dissolve_state: None,
356                voting_power_percentage_multiplier: 100,
357                vesting_period_seconds: None,
358                neuron_fees_e8s: 0,
359            },
360            permissions: vec![SnsNeuronPermissionRow {
361                principal: Some("aaaaa-aa".to_string()),
362                permission_types: vec![SnsNeuronPermissionValue::from_code(11)],
363            }],
364            disburse_maturity_in_progress: Vec::new(),
365            followees: Vec::new(),
366            topic_followees: None,
367            maturity_mint_conversion_observed_disabled:
368                SnsPolicyObservationStatus::ObservedSatisfied,
369            manual_maturity_staking_observed_disabled:
370                SnsPolicyObservationStatus::ObservedSatisfied,
371        };
372
373        assert_eq!(
374            detail.derived_policy_observations(),
375            (
376                SnsPolicyObservationStatus::Unassessable,
377                SnsPolicyObservationStatus::Unassessable,
378            )
379        );
380
381        detail.permissions[0]
382            .permission_types
383            .push(SnsNeuronPermissionValue::from_code(7));
384        assert_eq!(
385            detail.derived_policy_observations(),
386            (
387                SnsPolicyObservationStatus::Violated,
388                SnsPolicyObservationStatus::Unassessable,
389            )
390        );
391    }
392}