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