1use super::SnsNeuronRow;
8use serde::{Deserialize as SerdeDeserialize, Serialize};
9
10#[derive(Clone, Copy, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
17#[serde(rename_all = "snake_case")]
18pub enum SnsPolicyObservationStatus {
19 ObservedSatisfied,
21 Violated,
23 Unassessable,
25}
26
27impl SnsPolicyObservationStatus {
28 #[must_use]
30 pub const fn as_str(self) -> &'static str {
31 match self {
32 Self::ObservedSatisfied => "observed_satisfied",
33 Self::Violated => "violated",
34 Self::Unassessable => "unassessable",
35 }
36 }
37
38 #[must_use]
40 pub const fn combine(self, other: Self) -> Self {
41 match (self, other) {
42 (Self::Violated, _) | (_, Self::Violated) => Self::Violated,
43 (Self::Unassessable, _) | (_, Self::Unassessable) => Self::Unassessable,
44 (Self::ObservedSatisfied, Self::ObservedSatisfied) => Self::ObservedSatisfied,
45 }
46 }
47}
48
49#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
56#[serde(deny_unknown_fields)]
57pub struct SnsNeuronPermissionValue {
58 pub code: i32,
60 pub name: String,
62}
63
64impl SnsNeuronPermissionValue {
65 #[must_use]
67 pub fn from_code(code: i32) -> Self {
68 Self {
69 code,
70 name: sns_neuron_permission_name(code).to_string(),
71 }
72 }
73}
74
75#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
82#[serde(deny_unknown_fields)]
83pub struct SnsNeuronPermissionRow {
84 pub principal: Option<String>,
86 pub permission_types: Vec<SnsNeuronPermissionValue>,
88}
89
90#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
97#[serde(deny_unknown_fields)]
98pub struct SnsNeuronAccount {
99 pub owner: Option<String>,
101 pub subaccount_hex: Option<String>,
103}
104
105#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
112#[serde(deny_unknown_fields)]
113pub struct SnsMaturityDisbursementRow {
114 pub timestamp_of_disbursement_seconds: u64,
116 pub amount_e8s: u64,
118 pub account_to_disburse_to: Option<SnsNeuronAccount>,
120 pub finalize_disbursement_timestamp_seconds: Option<u64>,
122}
123
124#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
131#[serde(deny_unknown_fields)]
132pub struct SnsNeuronFolloweesRow {
133 pub function_id: u64,
135 pub followee_neuron_ids: Vec<String>,
137}
138
139#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
146#[serde(deny_unknown_fields)]
147pub struct SnsNeuronFolloweeRow {
148 pub neuron_id: Option<String>,
150 pub alias: Option<String>,
152}
153
154#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
161#[serde(deny_unknown_fields)]
162pub struct SnsNeuronTopicFolloweesRow {
163 pub topic_code: i32,
165 pub topic: Option<String>,
167 pub followees: Vec<SnsNeuronFolloweeRow>,
169}
170
171#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
178#[serde(deny_unknown_fields)]
179pub struct SnsNeuronDetail {
180 pub neuron: SnsNeuronRow,
182 pub permissions: Vec<SnsNeuronPermissionRow>,
184 pub disburse_maturity_in_progress: Vec<SnsMaturityDisbursementRow>,
186 pub followees: Vec<SnsNeuronFolloweesRow>,
188 pub topic_followees: Option<Vec<SnsNeuronTopicFolloweesRow>>,
190 pub maturity_mint_conversion_observed_disabled: SnsPolicyObservationStatus,
192 pub manual_maturity_staking_observed_disabled: SnsPolicyObservationStatus,
194}
195
196impl SnsNeuronDetail {
197 #[must_use]
199 pub fn derived_policy_observations(
200 &self,
201 ) -> (SnsPolicyObservationStatus, SnsPolicyObservationStatus) {
202 neuron_policy_observations(
203 &self.permissions,
204 !self.disburse_maturity_in_progress.is_empty(),
205 )
206 }
207}
208
209pub(in crate::sns::report::model) fn neuron_policy_observations(
210 permissions: &[SnsNeuronPermissionRow],
211 has_pending_maturity_disbursement: bool,
212) -> (SnsPolicyObservationStatus, SnsPolicyObservationStatus) {
213 let mut mint = if has_pending_maturity_disbursement {
214 SnsPolicyObservationStatus::Violated
215 } else {
216 SnsPolicyObservationStatus::ObservedSatisfied
217 };
218 let mut staking = if permissions.is_empty() {
219 mint = mint.combine(SnsPolicyObservationStatus::Unassessable);
220 SnsPolicyObservationStatus::Unassessable
221 } else {
222 SnsPolicyObservationStatus::ObservedSatisfied
223 };
224 for permission in permissions {
225 if permission.principal.is_none() || permission.permission_types.is_empty() {
226 mint = mint.combine(SnsPolicyObservationStatus::Unassessable);
227 staking = staking.combine(SnsPolicyObservationStatus::Unassessable);
228 }
229 for code in permission.permission_types.iter().map(|value| value.code) {
230 let (code_mint, code_staking) = permission_code_policy_observations(code);
231 mint = mint.combine(code_mint);
232 staking = staking.combine(code_staking);
233 }
234 }
235 (mint, staking)
236}
237
238pub(in crate::sns::report::model) const fn permission_code_policy_observations(
239 code: i32,
240) -> (SnsPolicyObservationStatus, SnsPolicyObservationStatus) {
241 match code {
242 7 | 8 => (
243 SnsPolicyObservationStatus::Violated,
244 SnsPolicyObservationStatus::ObservedSatisfied,
245 ),
246 9 => (
247 SnsPolicyObservationStatus::ObservedSatisfied,
248 SnsPolicyObservationStatus::Violated,
249 ),
250 0 | 11..=i32::MAX | i32::MIN..=-1 => (
251 SnsPolicyObservationStatus::Unassessable,
252 SnsPolicyObservationStatus::Unassessable,
253 ),
254 1..=6 | 10 => (
255 SnsPolicyObservationStatus::ObservedSatisfied,
256 SnsPolicyObservationStatus::ObservedSatisfied,
257 ),
258 }
259}
260
261#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
268#[serde(deny_unknown_fields)]
269pub struct SnsNeuronDetailReport {
270 pub schema_version: u32,
272 pub network: String,
274 pub sns_wasm_canister_id: String,
276 pub fetched_at: String,
278 pub source_endpoint: String,
280 pub fetched_by: String,
282 pub id: usize,
284 pub name: String,
286 pub root_canister_id: String,
288 pub governance_canister_id: String,
290 pub neuron_id: String,
292 pub data_source: String,
294 pub detail: SnsNeuronDetail,
296}
297
298#[must_use]
300pub const fn sns_neuron_permission_name(code: i32) -> &'static str {
301 match code {
302 0 => "unspecified",
303 1 => "configure_dissolve_state",
304 2 => "manage_principals",
305 3 => "submit_proposal",
306 4 => "vote",
307 5 => "disburse",
308 6 => "split",
309 7 => "merge_maturity",
310 8 => "disburse_maturity",
311 9 => "stake_maturity",
312 10 => "manage_voting_permission",
313 _ => "unknown",
314 }
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 #[test]
322 fn permission_names_cover_native_and_unknown_codes() {
323 for (code, expected) in [
324 (0, "unspecified"),
325 (1, "configure_dissolve_state"),
326 (2, "manage_principals"),
327 (3, "submit_proposal"),
328 (4, "vote"),
329 (5, "disburse"),
330 (6, "split"),
331 (7, "merge_maturity"),
332 (8, "disburse_maturity"),
333 (9, "stake_maturity"),
334 (10, "manage_voting_permission"),
335 (11, "unknown"),
336 (-1, "unknown"),
337 ] {
338 assert_eq!(sns_neuron_permission_name(code), expected, "code {code}");
339 }
340 }
341
342 #[test]
343 fn policy_observations_fail_closed_and_prioritize_known_violations() {
344 let mut detail = SnsNeuronDetail {
345 neuron: SnsNeuronRow {
346 neuron_id: "00".repeat(32),
347 cached_neuron_stake_e8s: 0,
348 maturity_e8s_equivalent: 0,
349 staked_maturity_e8s_equivalent: None,
350 created_timestamp_seconds: 0,
351 created_at: "1970-01-01T00:00:00Z".to_string(),
352 source_nns_neuron_id: None,
353 auto_stake_maturity: None,
354 aging_since_timestamp_seconds: 0,
355 dissolve_state: None,
356 voting_power_percentage_multiplier: 100,
357 vesting_period_seconds: None,
358 neuron_fees_e8s: 0,
359 },
360 permissions: vec![SnsNeuronPermissionRow {
361 principal: Some("aaaaa-aa".to_string()),
362 permission_types: vec![SnsNeuronPermissionValue::from_code(11)],
363 }],
364 disburse_maturity_in_progress: Vec::new(),
365 followees: Vec::new(),
366 topic_followees: None,
367 maturity_mint_conversion_observed_disabled:
368 SnsPolicyObservationStatus::ObservedSatisfied,
369 manual_maturity_staking_observed_disabled:
370 SnsPolicyObservationStatus::ObservedSatisfied,
371 };
372
373 assert_eq!(
374 detail.derived_policy_observations(),
375 (
376 SnsPolicyObservationStatus::Unassessable,
377 SnsPolicyObservationStatus::Unassessable,
378 )
379 );
380
381 detail.permissions[0]
382 .permission_types
383 .push(SnsNeuronPermissionValue::from_code(7));
384 assert_eq!(
385 detail.derived_policy_observations(),
386 (
387 SnsPolicyObservationStatus::Violated,
388 SnsPolicyObservationStatus::Unassessable,
389 )
390 );
391 }
392}