1use serde::{Deserialize as SerdeDeserialize, Deserializer, Serialize};
8use std::borrow::Cow;
9
10pub(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
38pub(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#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, SerdeDeserialize, Serialize)]
55#[serde(rename_all = "snake_case")]
56pub enum SnsProposalDecisionState {
57 Open,
59 Decided,
61 Executed,
63 Failed,
65}
66
67impl SnsProposalDecisionState {
68 #[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#[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#[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#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
145pub struct SnsProposalFailureReason {
146 pub error_type: i32,
147 pub error_message: String,
148}
149
150#[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}