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};
8
9///
10/// SnsProposalDecisionState
11///
12/// Derived lifecycle state for one SNS Governance proposal.
13///
14
15#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, SerdeDeserialize, Serialize)]
16#[serde(rename_all = "snake_case")]
17pub enum SnsProposalDecisionState {
18    /// The proposal has not reached a decision.
19    Open,
20    /// The proposal was decided without an execution or failure timestamp.
21    Decided,
22    /// The proposal has an execution timestamp.
23    Executed,
24    /// The proposal has a failure timestamp.
25    Failed,
26}
27
28impl SnsProposalDecisionState {
29    /// Return the stable cache, JSON, and text label.
30    #[must_use]
31    pub const fn as_str(self) -> &'static str {
32        match self {
33            Self::Open => "open",
34            Self::Decided => "decided",
35            Self::Executed => "executed",
36            Self::Failed => "failed",
37        }
38    }
39}
40
41///
42/// SnsProposalRow
43///
44/// Serializable row for one SNS governance proposal.
45///
46
47#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
48pub struct SnsProposalRow {
49    pub proposal_id: u64,
50    pub action_id: u64,
51    pub action: String,
52    pub title: String,
53    pub summary: String,
54    pub url: Option<String>,
55    pub decision_state: SnsProposalDecisionState,
56    #[serde(deserialize_with = "deserialize_required_option")]
57    pub status: Option<i32>,
58    #[serde(deserialize_with = "deserialize_required_option")]
59    pub topic: Option<String>,
60    pub reject_cost_e8s: u64,
61    pub proposal_creation_timestamp_seconds: u64,
62    pub created_at: String,
63    pub decided_timestamp_seconds: Option<u64>,
64    pub decided_at: Option<String>,
65    pub executed_timestamp_seconds: Option<u64>,
66    pub executed_at: Option<String>,
67    pub failed_timestamp_seconds: Option<u64>,
68    pub failed_at: Option<String>,
69    pub failure_reason: Option<SnsProposalFailureReason>,
70    pub reward_event_round: u64,
71    pub reward_event_end_timestamp_seconds: Option<u64>,
72    pub is_eligible_for_rewards: bool,
73    pub latest_tally: Option<SnsProposalTally>,
74    pub ballot_count: usize,
75    pub ballots: Vec<SnsProposalBallotRow>,
76    pub payload_text_rendering: Option<String>,
77    pub proposer_neuron_id: Option<String>,
78}
79
80fn deserialize_required_option<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
81where
82    D: Deserializer<'de>,
83    T: SerdeDeserialize<'de>,
84{
85    Option::<T>::deserialize(deserializer)
86}
87
88///
89/// SnsProposalBallotRow
90///
91/// Serializable row for one proposal ballot.
92///
93
94#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
95pub struct SnsProposalBallotRow {
96    pub neuron_id: String,
97    pub vote: i32,
98    pub vote_text: String,
99    pub cast_timestamp_seconds: u64,
100    pub cast_at: Option<String>,
101    pub voting_power: u64,
102}
103
104///
105/// SnsProposalFailureReason
106///
107/// Serializable SNS governance failure reason attached to a proposal.
108///
109
110#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
111pub struct SnsProposalFailureReason {
112    pub error_type: i32,
113    pub error_message: String,
114}
115
116///
117/// SnsProposalTally
118///
119/// Serializable SNS proposal vote tally.
120///
121
122#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
123pub struct SnsProposalTally {
124    pub timestamp_seconds: u64,
125    pub yes: u64,
126    pub no: u64,
127    pub total: u64,
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn proposal_decision_state_labels_round_trip() {
136        for (state, label) in [
137            (SnsProposalDecisionState::Open, "open"),
138            (SnsProposalDecisionState::Decided, "decided"),
139            (SnsProposalDecisionState::Executed, "executed"),
140            (SnsProposalDecisionState::Failed, "failed"),
141        ] {
142            assert_eq!(
143                serde_json::to_string(&state).unwrap(),
144                format!("\"{label}\"")
145            );
146            assert_eq!(
147                serde_json::from_str::<SnsProposalDecisionState>(&format!("\"{label}\"")).unwrap(),
148                state
149            );
150        }
151        assert!(serde_json::from_str::<SnsProposalDecisionState>("\"unknown\"").is_err());
152    }
153}