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    pub decision_state: SnsProposalDecisionState,
86    #[serde(deserialize_with = "deserialize_required_option")]
87    pub status: Option<i32>,
88    #[serde(deserialize_with = "deserialize_required_option")]
89    pub topic: Option<String>,
90    pub reject_cost_e8s: u64,
91    pub proposal_creation_timestamp_seconds: u64,
92    pub decided_timestamp_seconds: Option<u64>,
93    pub executed_timestamp_seconds: Option<u64>,
94    pub failed_timestamp_seconds: Option<u64>,
95    pub failure_reason: Option<SnsProposalFailureReason>,
96    pub reward_event_round: u64,
97    pub reward_event_end_timestamp_seconds: Option<u64>,
98    pub is_eligible_for_rewards: bool,
99    pub latest_tally: Option<SnsProposalTally>,
100    pub ballot_count: usize,
101    pub ballots: Vec<SnsProposalBallotRow>,
102    pub payload_text_rendering: Option<String>,
103    pub proposer_neuron_id: Option<String>,
104}
105
106fn deserialize_required_option<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
107where
108    D: Deserializer<'de>,
109    T: SerdeDeserialize<'de>,
110{
111    Option::<T>::deserialize(deserializer)
112}
113
114///
115/// SnsProposalBallotRow
116///
117/// Serializable row for one proposal ballot.
118///
119
120#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
121#[serde(deny_unknown_fields)]
122pub struct SnsProposalBallotRow {
123    pub neuron_id: String,
124    pub vote: i32,
125    pub cast_timestamp_seconds: u64,
126    pub voting_power: u64,
127}
128
129///
130/// SnsProposalFailureReason
131///
132/// Serializable SNS governance failure reason attached to a proposal.
133///
134
135#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
136pub struct SnsProposalFailureReason {
137    pub error_type: i32,
138    pub error_message: String,
139}
140
141///
142/// SnsProposalTally
143///
144/// Serializable SNS proposal vote tally.
145///
146
147#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
148pub struct SnsProposalTally {
149    pub timestamp_seconds: u64,
150    pub yes: u64,
151    pub no: u64,
152    pub total: u64,
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn proposal_decision_state_labels_round_trip() {
161        for (state, label) in [
162            (SnsProposalDecisionState::Open, "open"),
163            (SnsProposalDecisionState::Decided, "decided"),
164            (SnsProposalDecisionState::Executed, "executed"),
165            (SnsProposalDecisionState::Failed, "failed"),
166        ] {
167            assert_eq!(
168                serde_json::to_string(&state).unwrap(),
169                format!("\"{label}\"")
170            );
171            assert_eq!(
172                serde_json::from_str::<SnsProposalDecisionState>(&format!("\"{label}\"")).unwrap(),
173                state
174            );
175        }
176        assert!(serde_json::from_str::<SnsProposalDecisionState>("\"unknown\"").is_err());
177    }
178
179    #[test]
180    fn proposal_action_labels_cover_native_generic_and_unknown_ids() {
181        for (id, label) in [
182            (0, "unspecified"),
183            (1, "motion"),
184            (2, "manage_nervous_system_parameters"),
185            (3, "upgrade_sns_controlled_canister"),
186            (4, "add_generic_nervous_system_function"),
187            (5, "remove_generic_nervous_system_function"),
188            (6, "execute_generic_nervous_system_function"),
189            (7, "upgrade_sns_to_next_version"),
190            (8, "manage_sns_metadata"),
191            (9, "transfer_sns_treasury_funds"),
192            (10, "register_dapp_canisters"),
193            (11, "deregister_dapp_canisters"),
194            (12, "mint_sns_tokens"),
195            (13, "manage_ledger_parameters"),
196            (14, "manage_dapp_canister_settings"),
197            (15, "advance_sns_target_version"),
198            (16, "set_topics_for_custom_proposals"),
199            (17, "register_extension"),
200            (18, "execute_extension_operation"),
201            (19, "upgrade_extension"),
202            (20, "unknown:20"),
203            (1_000, "generic:1000"),
204            (u64::MAX, "generic:18446744073709551615"),
205        ] {
206            assert_eq!(sns_proposal_action_label(id), label);
207        }
208    }
209}