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