1use serde::{Deserialize as SerdeDeserialize, Deserializer, Serialize, Serializer, de::Error as _};
8use std::{borrow::Cow, fmt};
9
10#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
17pub enum SnsProposalAction {
18 Unspecified,
20 Motion,
22 ManageNervousSystemParameters,
24 UpgradeSnsControlledCanister,
26 AddGenericNervousSystemFunction,
28 RemoveGenericNervousSystemFunction,
30 ExecuteGenericNervousSystemFunction,
32 UpgradeSnsToNextVersion,
34 ManageSnsMetadata,
36 TransferSnsTreasuryFunds,
38 RegisterDappCanisters,
40 DeregisterDappCanisters,
42 MintSnsTokens,
44 ManageLedgerParameters,
46 ManageDappCanisterSettings,
48 AdvanceSnsTargetVersion,
50 SetTopicsForCustomProposals,
52 RegisterExtension,
54 ExecuteExtensionOperation,
56 UpgradeExtension,
58 Generic(u64),
60 Unknown(u64),
62}
63
64impl SnsProposalAction {
65 #[must_use]
67 pub const fn from_id(id: u64) -> Self {
68 match id {
69 0 => Self::Unspecified,
70 1 => Self::Motion,
71 2 => Self::ManageNervousSystemParameters,
72 3 => Self::UpgradeSnsControlledCanister,
73 4 => Self::AddGenericNervousSystemFunction,
74 5 => Self::RemoveGenericNervousSystemFunction,
75 6 => Self::ExecuteGenericNervousSystemFunction,
76 7 => Self::UpgradeSnsToNextVersion,
77 8 => Self::ManageSnsMetadata,
78 9 => Self::TransferSnsTreasuryFunds,
79 10 => Self::RegisterDappCanisters,
80 11 => Self::DeregisterDappCanisters,
81 12 => Self::MintSnsTokens,
82 13 => Self::ManageLedgerParameters,
83 14 => Self::ManageDappCanisterSettings,
84 15 => Self::AdvanceSnsTargetVersion,
85 16 => Self::SetTopicsForCustomProposals,
86 17 => Self::RegisterExtension,
87 18 => Self::ExecuteExtensionOperation,
88 19 => Self::UpgradeExtension,
89 id if id >= 1_000 => Self::Generic(id),
90 id => Self::Unknown(id),
91 }
92 }
93
94 #[must_use]
96 pub const fn id(self) -> u64 {
97 match self {
98 Self::Unspecified => 0,
99 Self::Motion => 1,
100 Self::ManageNervousSystemParameters => 2,
101 Self::UpgradeSnsControlledCanister => 3,
102 Self::AddGenericNervousSystemFunction => 4,
103 Self::RemoveGenericNervousSystemFunction => 5,
104 Self::ExecuteGenericNervousSystemFunction => 6,
105 Self::UpgradeSnsToNextVersion => 7,
106 Self::ManageSnsMetadata => 8,
107 Self::TransferSnsTreasuryFunds => 9,
108 Self::RegisterDappCanisters => 10,
109 Self::DeregisterDappCanisters => 11,
110 Self::MintSnsTokens => 12,
111 Self::ManageLedgerParameters => 13,
112 Self::ManageDappCanisterSettings => 14,
113 Self::AdvanceSnsTargetVersion => 15,
114 Self::SetTopicsForCustomProposals => 16,
115 Self::RegisterExtension => 17,
116 Self::ExecuteExtensionOperation => 18,
117 Self::UpgradeExtension => 19,
118 Self::Generic(id) | Self::Unknown(id) => id,
119 }
120 }
121
122 #[must_use]
124 pub fn label(self) -> Cow<'static, str> {
125 match self {
126 Self::Unspecified => Cow::Borrowed("unspecified"),
127 Self::Motion => Cow::Borrowed("motion"),
128 Self::ManageNervousSystemParameters => {
129 Cow::Borrowed("manage_nervous_system_parameters")
130 }
131 Self::UpgradeSnsControlledCanister => Cow::Borrowed("upgrade_sns_controlled_canister"),
132 Self::AddGenericNervousSystemFunction => {
133 Cow::Borrowed("add_generic_nervous_system_function")
134 }
135 Self::RemoveGenericNervousSystemFunction => {
136 Cow::Borrowed("remove_generic_nervous_system_function")
137 }
138 Self::ExecuteGenericNervousSystemFunction => {
139 Cow::Borrowed("execute_generic_nervous_system_function")
140 }
141 Self::UpgradeSnsToNextVersion => Cow::Borrowed("upgrade_sns_to_next_version"),
142 Self::ManageSnsMetadata => Cow::Borrowed("manage_sns_metadata"),
143 Self::TransferSnsTreasuryFunds => Cow::Borrowed("transfer_sns_treasury_funds"),
144 Self::RegisterDappCanisters => Cow::Borrowed("register_dapp_canisters"),
145 Self::DeregisterDappCanisters => Cow::Borrowed("deregister_dapp_canisters"),
146 Self::MintSnsTokens => Cow::Borrowed("mint_sns_tokens"),
147 Self::ManageLedgerParameters => Cow::Borrowed("manage_ledger_parameters"),
148 Self::ManageDappCanisterSettings => Cow::Borrowed("manage_dapp_canister_settings"),
149 Self::AdvanceSnsTargetVersion => Cow::Borrowed("advance_sns_target_version"),
150 Self::SetTopicsForCustomProposals => Cow::Borrowed("set_topics_for_custom_proposals"),
151 Self::RegisterExtension => Cow::Borrowed("register_extension"),
152 Self::ExecuteExtensionOperation => Cow::Borrowed("execute_extension_operation"),
153 Self::UpgradeExtension => Cow::Borrowed("upgrade_extension"),
154 Self::Generic(id) => Cow::Owned(format!("generic:{id}")),
155 Self::Unknown(id) => Cow::Owned(format!("unknown:{id}")),
156 }
157 }
158
159 fn from_label(label: &str) -> Option<Self> {
160 let action = match label {
161 "unspecified" => Self::Unspecified,
162 "motion" => Self::Motion,
163 "manage_nervous_system_parameters" => Self::ManageNervousSystemParameters,
164 "upgrade_sns_controlled_canister" => Self::UpgradeSnsControlledCanister,
165 "add_generic_nervous_system_function" => Self::AddGenericNervousSystemFunction,
166 "remove_generic_nervous_system_function" => Self::RemoveGenericNervousSystemFunction,
167 "execute_generic_nervous_system_function" => Self::ExecuteGenericNervousSystemFunction,
168 "upgrade_sns_to_next_version" => Self::UpgradeSnsToNextVersion,
169 "manage_sns_metadata" => Self::ManageSnsMetadata,
170 "transfer_sns_treasury_funds" => Self::TransferSnsTreasuryFunds,
171 "register_dapp_canisters" => Self::RegisterDappCanisters,
172 "deregister_dapp_canisters" => Self::DeregisterDappCanisters,
173 "mint_sns_tokens" => Self::MintSnsTokens,
174 "manage_ledger_parameters" => Self::ManageLedgerParameters,
175 "manage_dapp_canister_settings" => Self::ManageDappCanisterSettings,
176 "advance_sns_target_version" => Self::AdvanceSnsTargetVersion,
177 "set_topics_for_custom_proposals" => Self::SetTopicsForCustomProposals,
178 "register_extension" => Self::RegisterExtension,
179 "execute_extension_operation" => Self::ExecuteExtensionOperation,
180 "upgrade_extension" => Self::UpgradeExtension,
181 _ => {
182 let id = label
183 .strip_prefix("generic:")
184 .or_else(|| label.strip_prefix("unknown:"))?
185 .parse::<u64>()
186 .ok()?;
187 Self::from_id(id)
188 }
189 };
190 (action.label().as_ref() == label).then_some(action)
191 }
192}
193
194impl fmt::Display for SnsProposalAction {
195 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
196 formatter.write_str(self.label().as_ref())
197 }
198}
199
200impl Serialize for SnsProposalAction {
201 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
202 where
203 S: Serializer,
204 {
205 serializer.serialize_str(self.label().as_ref())
206 }
207}
208
209impl<'de> SerdeDeserialize<'de> for SnsProposalAction {
210 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
211 where
212 D: Deserializer<'de>,
213 {
214 let label = String::deserialize(deserializer)?;
215 Self::from_label(&label)
216 .ok_or_else(|| D::Error::custom(format!("invalid SNS proposal action {label:?}")))
217 }
218}
219
220#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
227pub enum SnsProposalVote {
228 Unspecified,
230 Yes,
232 No,
234 Unknown(i32),
236}
237
238impl SnsProposalVote {
239 #[must_use]
241 pub const fn from_code(code: i32) -> Self {
242 match code {
243 0 => Self::Unspecified,
244 1 => Self::Yes,
245 2 => Self::No,
246 code => Self::Unknown(code),
247 }
248 }
249
250 #[must_use]
252 pub const fn code(self) -> i32 {
253 match self {
254 Self::Unspecified => 0,
255 Self::Yes => 1,
256 Self::No => 2,
257 Self::Unknown(code) => code,
258 }
259 }
260
261 #[must_use]
263 pub fn label(self) -> Cow<'static, str> {
264 match self {
265 Self::Unspecified => Cow::Borrowed("unspecified"),
266 Self::Yes => Cow::Borrowed("yes"),
267 Self::No => Cow::Borrowed("no"),
268 Self::Unknown(code) => Cow::Owned(format!("unknown:{code}")),
269 }
270 }
271
272 fn from_label(label: &str) -> Option<Self> {
273 let vote = match label {
274 "unspecified" => Self::Unspecified,
275 "yes" => Self::Yes,
276 "no" => Self::No,
277 _ => Self::from_code(label.strip_prefix("unknown:")?.parse::<i32>().ok()?),
278 };
279 (vote.label().as_ref() == label).then_some(vote)
280 }
281}
282
283impl fmt::Display for SnsProposalVote {
284 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
285 formatter.write_str(self.label().as_ref())
286 }
287}
288
289impl Serialize for SnsProposalVote {
290 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
291 where
292 S: Serializer,
293 {
294 serializer.serialize_str(self.label().as_ref())
295 }
296}
297
298impl<'de> SerdeDeserialize<'de> for SnsProposalVote {
299 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
300 where
301 D: Deserializer<'de>,
302 {
303 let label = String::deserialize(deserializer)?;
304 Self::from_label(&label)
305 .ok_or_else(|| D::Error::custom(format!("invalid SNS proposal vote {label:?}")))
306 }
307}
308
309#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, SerdeDeserialize, Serialize)]
316#[serde(rename_all = "snake_case")]
317pub enum SnsProposalDecisionState {
318 Open,
320 Decided,
322 Executed,
324 Failed,
326}
327
328impl SnsProposalDecisionState {
329 #[must_use]
331 pub const fn as_str(self) -> &'static str {
332 match self {
333 Self::Open => "open",
334 Self::Decided => "decided",
335 Self::Executed => "executed",
336 Self::Failed => "failed",
337 }
338 }
339}
340
341#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
348pub struct SnsProposalRow {
349 pub proposal_id: u64,
350 pub action_id: u64,
351 pub action: SnsProposalAction,
352 pub title: String,
353 pub summary: String,
354 pub url: Option<String>,
355 pub decision_state: SnsProposalDecisionState,
356 #[serde(deserialize_with = "deserialize_required_option")]
357 pub status: Option<i32>,
358 #[serde(deserialize_with = "deserialize_required_option")]
359 pub topic: Option<String>,
360 pub reject_cost_e8s: u64,
361 pub proposal_creation_timestamp_seconds: u64,
362 pub created_at: String,
363 pub decided_timestamp_seconds: Option<u64>,
364 pub decided_at: Option<String>,
365 pub executed_timestamp_seconds: Option<u64>,
366 pub executed_at: Option<String>,
367 pub failed_timestamp_seconds: Option<u64>,
368 pub failed_at: Option<String>,
369 pub failure_reason: Option<SnsProposalFailureReason>,
370 pub reward_event_round: u64,
371 pub reward_event_end_timestamp_seconds: Option<u64>,
372 pub is_eligible_for_rewards: bool,
373 pub latest_tally: Option<SnsProposalTally>,
374 pub ballot_count: usize,
375 pub ballots: Vec<SnsProposalBallotRow>,
376 pub payload_text_rendering: Option<String>,
377 pub proposer_neuron_id: Option<String>,
378}
379
380fn deserialize_required_option<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
381where
382 D: Deserializer<'de>,
383 T: SerdeDeserialize<'de>,
384{
385 Option::<T>::deserialize(deserializer)
386}
387
388#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
395pub struct SnsProposalBallotRow {
396 pub neuron_id: String,
397 pub vote: i32,
398 pub vote_text: SnsProposalVote,
399 pub cast_timestamp_seconds: u64,
400 pub cast_at: Option<String>,
401 pub voting_power: u64,
402}
403
404#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
411pub struct SnsProposalFailureReason {
412 pub error_type: i32,
413 pub error_message: String,
414}
415
416#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
423pub struct SnsProposalTally {
424 pub timestamp_seconds: u64,
425 pub yes: u64,
426 pub no: u64,
427 pub total: u64,
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433
434 const NATIVE_ACTION_CASES: [(u64, SnsProposalAction, &str); 20] = [
435 (0, SnsProposalAction::Unspecified, "unspecified"),
436 (1, SnsProposalAction::Motion, "motion"),
437 (
438 2,
439 SnsProposalAction::ManageNervousSystemParameters,
440 "manage_nervous_system_parameters",
441 ),
442 (
443 3,
444 SnsProposalAction::UpgradeSnsControlledCanister,
445 "upgrade_sns_controlled_canister",
446 ),
447 (
448 4,
449 SnsProposalAction::AddGenericNervousSystemFunction,
450 "add_generic_nervous_system_function",
451 ),
452 (
453 5,
454 SnsProposalAction::RemoveGenericNervousSystemFunction,
455 "remove_generic_nervous_system_function",
456 ),
457 (
458 6,
459 SnsProposalAction::ExecuteGenericNervousSystemFunction,
460 "execute_generic_nervous_system_function",
461 ),
462 (
463 7,
464 SnsProposalAction::UpgradeSnsToNextVersion,
465 "upgrade_sns_to_next_version",
466 ),
467 (
468 8,
469 SnsProposalAction::ManageSnsMetadata,
470 "manage_sns_metadata",
471 ),
472 (
473 9,
474 SnsProposalAction::TransferSnsTreasuryFunds,
475 "transfer_sns_treasury_funds",
476 ),
477 (
478 10,
479 SnsProposalAction::RegisterDappCanisters,
480 "register_dapp_canisters",
481 ),
482 (
483 11,
484 SnsProposalAction::DeregisterDappCanisters,
485 "deregister_dapp_canisters",
486 ),
487 (12, SnsProposalAction::MintSnsTokens, "mint_sns_tokens"),
488 (
489 13,
490 SnsProposalAction::ManageLedgerParameters,
491 "manage_ledger_parameters",
492 ),
493 (
494 14,
495 SnsProposalAction::ManageDappCanisterSettings,
496 "manage_dapp_canister_settings",
497 ),
498 (
499 15,
500 SnsProposalAction::AdvanceSnsTargetVersion,
501 "advance_sns_target_version",
502 ),
503 (
504 16,
505 SnsProposalAction::SetTopicsForCustomProposals,
506 "set_topics_for_custom_proposals",
507 ),
508 (
509 17,
510 SnsProposalAction::RegisterExtension,
511 "register_extension",
512 ),
513 (
514 18,
515 SnsProposalAction::ExecuteExtensionOperation,
516 "execute_extension_operation",
517 ),
518 (19, SnsProposalAction::UpgradeExtension, "upgrade_extension"),
519 ];
520
521 #[test]
522 fn proposal_decision_state_labels_round_trip() {
523 for (state, label) in [
524 (SnsProposalDecisionState::Open, "open"),
525 (SnsProposalDecisionState::Decided, "decided"),
526 (SnsProposalDecisionState::Executed, "executed"),
527 (SnsProposalDecisionState::Failed, "failed"),
528 ] {
529 assert_eq!(
530 serde_json::to_string(&state).unwrap(),
531 format!("\"{label}\"")
532 );
533 assert_eq!(
534 serde_json::from_str::<SnsProposalDecisionState>(&format!("\"{label}\"")).unwrap(),
535 state
536 );
537 }
538 assert!(serde_json::from_str::<SnsProposalDecisionState>("\"unknown\"").is_err());
539 }
540
541 #[test]
542 fn proposal_action_labels_round_trip_native_generic_and_unknown_ids() {
543 for (id, action, label) in NATIVE_ACTION_CASES.into_iter().chain([
544 (20, SnsProposalAction::Unknown(20), "unknown:20"),
545 (1_000, SnsProposalAction::Generic(1_000), "generic:1000"),
546 ]) {
547 assert_eq!(SnsProposalAction::from_id(id), action);
548 assert_eq!(action.id(), id);
549 assert_eq!(action.label(), label);
550 assert_eq!(
551 serde_json::to_string(&action).unwrap(),
552 format!("\"{label}\"")
553 );
554 assert_eq!(
555 serde_json::from_str::<SnsProposalAction>(&format!("\"{label}\"")).unwrap(),
556 action
557 );
558 }
559 for invalid in ["unknown:1", "generic:20", "unknown:020", "future"] {
560 assert!(serde_json::from_str::<SnsProposalAction>(&format!("\"{invalid}\"")).is_err());
561 }
562 }
563
564 #[test]
565 fn proposal_vote_labels_round_trip_known_and_unknown_codes() {
566 for (code, vote, label) in [
567 (0, SnsProposalVote::Unspecified, "unspecified"),
568 (1, SnsProposalVote::Yes, "yes"),
569 (2, SnsProposalVote::No, "no"),
570 (99, SnsProposalVote::Unknown(99), "unknown:99"),
571 (-1, SnsProposalVote::Unknown(-1), "unknown:-1"),
572 ] {
573 assert_eq!(SnsProposalVote::from_code(code), vote);
574 assert_eq!(vote.code(), code);
575 assert_eq!(vote.label(), label);
576 assert_eq!(
577 serde_json::to_string(&vote).unwrap(),
578 format!("\"{label}\"")
579 );
580 assert_eq!(
581 serde_json::from_str::<SnsProposalVote>(&format!("\"{label}\"")).unwrap(),
582 vote
583 );
584 }
585 for invalid in ["unknown:1", "unknown:+3", "unknown:03", "maybe"] {
586 assert!(serde_json::from_str::<SnsProposalVote>(&format!("\"{invalid}\"")).is_err());
587 }
588 }
589}