Skip to main content

ic_query/sns/report/model/reports/proposals/
row.rs

1//! Module: sns::report::model::reports::proposals::row
2//!
3//! Responsibility: define SNS proposal row and nested value DTOs.
4//! Does not own: source conversion, report-level metadata, or rendering.
5//! Boundary: preserves proposal detail fields for cache snapshots and JSON output.
6
7use serde::{Deserialize as SerdeDeserialize, Deserializer, Serialize};
8use std::borrow::Cow;
9
10/// Label a native SNS action id without replacing its original numeric evidence.
11#[must_use]
12pub fn sns_proposal_action_label(id: u64) -> Cow<'static, str> {
13    match id {
14        0 => Cow::Borrowed("unspecified"),
15        1 => Cow::Borrowed("motion"),
16        2 => Cow::Borrowed("manage_nervous_system_parameters"),
17        3 => Cow::Borrowed("upgrade_sns_controlled_canister"),
18        4 => Cow::Borrowed("add_generic_nervous_system_function"),
19        5 => Cow::Borrowed("remove_generic_nervous_system_function"),
20        6 => Cow::Borrowed("execute_generic_nervous_system_function"),
21        7 => Cow::Borrowed("upgrade_sns_to_next_version"),
22        8 => Cow::Borrowed("manage_sns_metadata"),
23        9 => Cow::Borrowed("transfer_sns_treasury_funds"),
24        10 => Cow::Borrowed("register_dapp_canisters"),
25        11 => Cow::Borrowed("deregister_dapp_canisters"),
26        12 => Cow::Borrowed("mint_sns_tokens"),
27        13 => Cow::Borrowed("manage_ledger_parameters"),
28        14 => Cow::Borrowed("manage_dapp_canister_settings"),
29        15 => Cow::Borrowed("advance_sns_target_version"),
30        16 => Cow::Borrowed("set_topics_for_custom_proposals"),
31        17 => Cow::Borrowed("register_extension"),
32        18 => Cow::Borrowed("execute_extension_operation"),
33        19 => Cow::Borrowed("upgrade_extension"),
34        id if id >= 1_000 => Cow::Owned(format!("generic:{id}")),
35        id => Cow::Owned(format!("unknown:{id}")),
36    }
37}
38
39///
40/// SnsProposalDecisionState
41///
42/// Derived lifecycle state for one SNS Governance proposal.
43///
44
45#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, SerdeDeserialize, Serialize)]
46#[serde(rename_all = "snake_case")]
47pub enum SnsProposalDecisionState {
48    /// The proposal has not reached a decision.
49    Open,
50    /// The proposal was decided without an execution or failure timestamp.
51    Decided,
52    /// The proposal has an execution timestamp.
53    Executed,
54    /// The proposal has a failure timestamp.
55    Failed,
56}
57
58impl SnsProposalDecisionState {
59    /// Return the stable cache, JSON, and text label.
60    #[must_use]
61    pub const fn as_str(self) -> &'static str {
62        match self {
63            Self::Open => "open",
64            Self::Decided => "decided",
65            Self::Executed => "executed",
66            Self::Failed => "failed",
67        }
68    }
69}
70
71///
72/// SnsProposalRow
73///
74/// Serializable row for one SNS governance proposal.
75///
76
77#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
78#[serde(deny_unknown_fields)]
79pub struct SnsProposalRow {
80    pub proposal_id: u64,
81    pub action_id: u64,
82    pub title: String,
83    pub summary: String,
84    pub url: Option<String>,
85    #[serde(deserialize_with = "deserialize_required_option")]
86    pub status: Option<i32>,
87    #[serde(deserialize_with = "deserialize_required_option")]
88    pub topic: Option<String>,
89    pub reject_cost_e8s: u64,
90    pub proposal_creation_timestamp_seconds: u64,
91    pub decided_timestamp_seconds: Option<u64>,
92    pub executed_timestamp_seconds: Option<u64>,
93    pub failed_timestamp_seconds: Option<u64>,
94    pub failure_reason: Option<SnsProposalFailureReason>,
95    pub reward_event_round: u64,
96    pub reward_event_end_timestamp_seconds: Option<u64>,
97    pub is_eligible_for_rewards: bool,
98    pub latest_tally: Option<SnsProposalTally>,
99    pub ballots: Vec<SnsProposalBallotRow>,
100    pub payload_text_rendering: Option<String>,
101    pub proposer_neuron_id: Option<String>,
102}
103
104impl SnsProposalRow {
105    /// Derive the lifecycle from raw Governance timestamps, with failure taking precedence.
106    #[must_use]
107    pub const fn decision_state(&self) -> SnsProposalDecisionState {
108        if matches!(self.failed_timestamp_seconds, Some(1..)) {
109            SnsProposalDecisionState::Failed
110        } else if matches!(self.executed_timestamp_seconds, Some(1..)) {
111            SnsProposalDecisionState::Executed
112        } else if matches!(self.decided_timestamp_seconds, Some(1..)) {
113            SnsProposalDecisionState::Decided
114        } else {
115            SnsProposalDecisionState::Open
116        }
117    }
118
119    /// Number of retained ballot rows.
120    #[must_use]
121    pub const fn ballot_count(&self) -> usize {
122        self.ballots.len()
123    }
124}
125
126fn deserialize_required_option<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
127where
128    D: Deserializer<'de>,
129    T: SerdeDeserialize<'de>,
130{
131    Option::<T>::deserialize(deserializer)
132}
133
134///
135/// SnsProposalBallotRow
136///
137/// Serializable row for one proposal ballot.
138///
139
140#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
141#[serde(deny_unknown_fields)]
142pub struct SnsProposalBallotRow {
143    pub neuron_id: String,
144    pub vote: i32,
145    pub cast_timestamp_seconds: u64,
146    pub voting_power: u64,
147}
148
149///
150/// SnsProposalFailureReason
151///
152/// Serializable SNS governance failure reason attached to a proposal.
153///
154
155#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
156pub struct SnsProposalFailureReason {
157    pub error_type: i32,
158    pub error_message: String,
159}
160
161///
162/// SnsProposalTally
163///
164/// Serializable SNS proposal vote tally.
165///
166
167#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
168pub struct SnsProposalTally {
169    pub timestamp_seconds: u64,
170    pub yes: u64,
171    pub no: u64,
172    pub total: u64,
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn proposal_decision_state_labels_round_trip() {
181        for (state, label) in [
182            (SnsProposalDecisionState::Open, "open"),
183            (SnsProposalDecisionState::Decided, "decided"),
184            (SnsProposalDecisionState::Executed, "executed"),
185            (SnsProposalDecisionState::Failed, "failed"),
186        ] {
187            assert_eq!(
188                serde_json::to_string(&state).unwrap(),
189                format!("\"{label}\"")
190            );
191            assert_eq!(
192                serde_json::from_str::<SnsProposalDecisionState>(&format!("\"{label}\"")).unwrap(),
193                state
194            );
195        }
196        assert!(serde_json::from_str::<SnsProposalDecisionState>("\"unknown\"").is_err());
197    }
198
199    #[test]
200    fn proposal_action_labels_cover_native_generic_and_unknown_ids() {
201        for (id, label) in [
202            (0, "unspecified"),
203            (1, "motion"),
204            (2, "manage_nervous_system_parameters"),
205            (3, "upgrade_sns_controlled_canister"),
206            (4, "add_generic_nervous_system_function"),
207            (5, "remove_generic_nervous_system_function"),
208            (6, "execute_generic_nervous_system_function"),
209            (7, "upgrade_sns_to_next_version"),
210            (8, "manage_sns_metadata"),
211            (9, "transfer_sns_treasury_funds"),
212            (10, "register_dapp_canisters"),
213            (11, "deregister_dapp_canisters"),
214            (12, "mint_sns_tokens"),
215            (13, "manage_ledger_parameters"),
216            (14, "manage_dapp_canister_settings"),
217            (15, "advance_sns_target_version"),
218            (16, "set_topics_for_custom_proposals"),
219            (17, "register_extension"),
220            (18, "execute_extension_operation"),
221            (19, "upgrade_extension"),
222            (20, "unknown:20"),
223            (1_000, "generic:1000"),
224            (u64::MAX, "generic:18446744073709551615"),
225        ] {
226            assert_eq!(sns_proposal_action_label(id), label);
227        }
228    }
229}