1use super::SnsNeuronRow;
8use crate::report::ReportDataSource;
9use serde::{Deserialize as SerdeDeserialize, Serialize};
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
18#[serde(rename_all = "snake_case")]
19pub enum SnsPolicyObservationStatus {
20 ObservedSatisfied,
22 Violated,
24 Unassessable,
26}
27
28impl SnsPolicyObservationStatus {
29 #[must_use]
31 pub const fn as_str(self) -> &'static str {
32 match self {
33 Self::ObservedSatisfied => "observed_satisfied",
34 Self::Violated => "violated",
35 Self::Unassessable => "unassessable",
36 }
37 }
38
39 #[must_use]
41 pub const fn combine(self, other: Self) -> Self {
42 match (self, other) {
43 (Self::Violated, _) | (_, Self::Violated) => Self::Violated,
44 (Self::Unassessable, _) | (_, Self::Unassessable) => Self::Unassessable,
45 (Self::ObservedSatisfied, Self::ObservedSatisfied) => Self::ObservedSatisfied,
46 }
47 }
48}
49
50#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
57#[serde(deny_unknown_fields)]
58pub struct SnsNeuronPermissionValue {
59 pub code: i32,
61 pub name: String,
63}
64
65impl SnsNeuronPermissionValue {
66 #[must_use]
68 pub fn from_code(code: i32) -> Self {
69 Self {
70 code,
71 name: sns_neuron_permission_name(code).to_string(),
72 }
73 }
74}
75
76#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
83#[serde(deny_unknown_fields)]
84pub struct SnsNeuronPermissionRow {
85 pub principal: Option<String>,
87 pub permission_types: Vec<SnsNeuronPermissionValue>,
89}
90
91#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
98#[serde(deny_unknown_fields)]
99pub struct SnsNeuronAccount {
100 pub owner: Option<String>,
102 pub subaccount_hex: Option<String>,
104}
105
106#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
113#[serde(deny_unknown_fields)]
114pub struct SnsMaturityDisbursementRow {
115 pub timestamp_of_disbursement_seconds: u64,
117 pub amount_e8s: u64,
119 pub account_to_disburse_to: Option<SnsNeuronAccount>,
121 pub finalize_disbursement_timestamp_seconds: Option<u64>,
123}
124
125#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
132#[serde(deny_unknown_fields)]
133pub struct SnsNeuronFolloweesRow {
134 pub function_id: u64,
136 pub followee_neuron_ids: Vec<String>,
138}
139
140#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
147#[serde(deny_unknown_fields)]
148pub struct SnsNeuronFolloweeRow {
149 pub neuron_id: Option<String>,
151 pub alias: Option<String>,
153}
154
155#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
162#[serde(deny_unknown_fields)]
163pub struct SnsNeuronTopicFolloweesRow {
164 pub topic_code: i32,
166 pub topic: Option<String>,
168 pub followees: Vec<SnsNeuronFolloweeRow>,
170}
171
172#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
179#[serde(deny_unknown_fields)]
180pub struct SnsNeuronDetail {
181 pub neuron: SnsNeuronRow,
183 pub permissions: Vec<SnsNeuronPermissionRow>,
185 pub disburse_maturity_in_progress: Vec<SnsMaturityDisbursementRow>,
187 pub followees: Vec<SnsNeuronFolloweesRow>,
189 pub topic_followees: Option<Vec<SnsNeuronTopicFolloweesRow>>,
191 pub maturity_mint_conversion_observed_disabled: SnsPolicyObservationStatus,
193 pub manual_maturity_staking_observed_disabled: SnsPolicyObservationStatus,
195}
196
197impl SnsNeuronDetail {
198 #[must_use]
200 pub fn derived_policy_observations(
201 &self,
202 ) -> (SnsPolicyObservationStatus, SnsPolicyObservationStatus) {
203 neuron_policy_observations(
204 &self.permissions,
205 !self.disburse_maturity_in_progress.is_empty(),
206 )
207 }
208}
209
210pub(in crate::sns::report::model) fn neuron_policy_observations(
211 permissions: &[SnsNeuronPermissionRow],
212 has_pending_maturity_disbursement: bool,
213) -> (SnsPolicyObservationStatus, SnsPolicyObservationStatus) {
214 let mut mint = if has_pending_maturity_disbursement {
215 SnsPolicyObservationStatus::Violated
216 } else {
217 SnsPolicyObservationStatus::ObservedSatisfied
218 };
219 let mut staking = if permissions.is_empty() {
220 mint = mint.combine(SnsPolicyObservationStatus::Unassessable);
221 SnsPolicyObservationStatus::Unassessable
222 } else {
223 SnsPolicyObservationStatus::ObservedSatisfied
224 };
225 for permission in permissions {
226 if permission.principal.is_none() || permission.permission_types.is_empty() {
227 mint = mint.combine(SnsPolicyObservationStatus::Unassessable);
228 staking = staking.combine(SnsPolicyObservationStatus::Unassessable);
229 }
230 for code in permission.permission_types.iter().map(|value| value.code) {
231 let (code_mint, code_staking) = permission_code_policy_observations(code);
232 mint = mint.combine(code_mint);
233 staking = staking.combine(code_staking);
234 }
235 }
236 (mint, staking)
237}
238
239pub(in crate::sns::report::model) const fn permission_code_policy_observations(
240 code: i32,
241) -> (SnsPolicyObservationStatus, SnsPolicyObservationStatus) {
242 match code {
243 7 | 8 => (
244 SnsPolicyObservationStatus::Violated,
245 SnsPolicyObservationStatus::ObservedSatisfied,
246 ),
247 9 => (
248 SnsPolicyObservationStatus::ObservedSatisfied,
249 SnsPolicyObservationStatus::Violated,
250 ),
251 0 | 11..=i32::MAX | i32::MIN..=-1 => (
252 SnsPolicyObservationStatus::Unassessable,
253 SnsPolicyObservationStatus::Unassessable,
254 ),
255 1..=6 | 10 => (
256 SnsPolicyObservationStatus::ObservedSatisfied,
257 SnsPolicyObservationStatus::ObservedSatisfied,
258 ),
259 }
260}
261
262#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
269#[serde(deny_unknown_fields)]
270pub struct SnsNeuronDetailReport {
271 pub schema_version: u32,
273 pub network: String,
275 pub sns_wasm_canister_id: String,
277 pub fetched_at: String,
279 pub source_endpoint: String,
281 pub fetched_by: String,
283 pub id: usize,
285 pub name: String,
287 pub root_canister_id: String,
289 pub governance_canister_id: String,
291 pub neuron_id: String,
293 pub data_source: ReportDataSource,
295 pub detail: SnsNeuronDetail,
297}
298
299#[must_use]
301pub const fn sns_neuron_permission_name(code: i32) -> &'static str {
302 match code {
303 0 => "unspecified",
304 1 => "configure_dissolve_state",
305 2 => "manage_principals",
306 3 => "submit_proposal",
307 4 => "vote",
308 5 => "disburse",
309 6 => "split",
310 7 => "merge_maturity",
311 8 => "disburse_maturity",
312 9 => "stake_maturity",
313 10 => "manage_voting_permission",
314 _ => "unknown",
315 }
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 #[test]
323 fn permission_names_cover_native_and_unknown_codes() {
324 for (code, expected) in [
325 (0, "unspecified"),
326 (1, "configure_dissolve_state"),
327 (2, "manage_principals"),
328 (3, "submit_proposal"),
329 (4, "vote"),
330 (5, "disburse"),
331 (6, "split"),
332 (7, "merge_maturity"),
333 (8, "disburse_maturity"),
334 (9, "stake_maturity"),
335 (10, "manage_voting_permission"),
336 (11, "unknown"),
337 (-1, "unknown"),
338 ] {
339 assert_eq!(sns_neuron_permission_name(code), expected, "code {code}");
340 }
341 }
342
343 #[test]
344 fn policy_observations_fail_closed_and_prioritize_known_violations() {
345 let mut detail = SnsNeuronDetail {
346 neuron: SnsNeuronRow {
347 neuron_id: "00".repeat(32),
348 cached_neuron_stake_e8s: 0,
349 maturity_e8s_equivalent: 0,
350 staked_maturity_e8s_equivalent: None,
351 created_timestamp_seconds: 0,
352 created_at: "1970-01-01T00:00:00Z".to_string(),
353 source_nns_neuron_id: None,
354 auto_stake_maturity: None,
355 aging_since_timestamp_seconds: 0,
356 dissolve_state: None,
357 voting_power_percentage_multiplier: 100,
358 vesting_period_seconds: None,
359 neuron_fees_e8s: 0,
360 },
361 permissions: vec![SnsNeuronPermissionRow {
362 principal: Some("aaaaa-aa".to_string()),
363 permission_types: vec![SnsNeuronPermissionValue::from_code(11)],
364 }],
365 disburse_maturity_in_progress: Vec::new(),
366 followees: Vec::new(),
367 topic_followees: None,
368 maturity_mint_conversion_observed_disabled:
369 SnsPolicyObservationStatus::ObservedSatisfied,
370 manual_maturity_staking_observed_disabled:
371 SnsPolicyObservationStatus::ObservedSatisfied,
372 };
373
374 assert_eq!(
375 detail.derived_policy_observations(),
376 (
377 SnsPolicyObservationStatus::Unassessable,
378 SnsPolicyObservationStatus::Unassessable,
379 )
380 );
381
382 detail.permissions[0]
383 .permission_types
384 .push(SnsNeuronPermissionValue::from_code(7));
385 assert_eq!(
386 detail.derived_policy_observations(),
387 (
388 SnsPolicyObservationStatus::Violated,
389 SnsPolicyObservationStatus::Unassessable,
390 )
391 );
392 }
393}