use super::SnsNeuronRow;
use serde::{Deserialize as SerdeDeserialize, Serialize};
#[derive(Clone, Copy, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SnsPolicyObservationStatus {
ObservedSatisfied,
Violated,
Unassessable,
}
impl SnsPolicyObservationStatus {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::ObservedSatisfied => "observed_satisfied",
Self::Violated => "violated",
Self::Unassessable => "unassessable",
}
}
#[must_use]
pub const fn combine(self, other: Self) -> Self {
match (self, other) {
(Self::Violated, _) | (_, Self::Violated) => Self::Violated,
(Self::Unassessable, _) | (_, Self::Unassessable) => Self::Unassessable,
(Self::ObservedSatisfied, Self::ObservedSatisfied) => Self::ObservedSatisfied,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct SnsNeuronPermissionValue {
pub code: i32,
pub name: String,
}
impl SnsNeuronPermissionValue {
#[must_use]
pub fn from_code(code: i32) -> Self {
Self {
code,
name: sns_neuron_permission_name(code).to_string(),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct SnsNeuronPermissionRow {
pub principal: Option<String>,
pub permission_types: Vec<SnsNeuronPermissionValue>,
}
#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct SnsNeuronAccount {
pub owner: Option<String>,
pub subaccount_hex: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct SnsMaturityDisbursementRow {
pub timestamp_of_disbursement_seconds: u64,
pub amount_e8s: u64,
pub account_to_disburse_to: Option<SnsNeuronAccount>,
pub finalize_disbursement_timestamp_seconds: Option<u64>,
}
#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct SnsNeuronFolloweesRow {
pub function_id: u64,
pub followee_neuron_ids: Vec<String>,
}
#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct SnsNeuronFolloweeRow {
pub neuron_id: Option<String>,
pub alias: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct SnsNeuronTopicFolloweesRow {
pub topic_code: i32,
pub topic: Option<String>,
pub followees: Vec<SnsNeuronFolloweeRow>,
}
#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct SnsNeuronDetail {
pub neuron: SnsNeuronRow,
pub permissions: Vec<SnsNeuronPermissionRow>,
pub disburse_maturity_in_progress: Vec<SnsMaturityDisbursementRow>,
pub followees: Vec<SnsNeuronFolloweesRow>,
pub topic_followees: Option<Vec<SnsNeuronTopicFolloweesRow>>,
pub maturity_mint_conversion_observed_disabled: SnsPolicyObservationStatus,
pub manual_maturity_staking_observed_disabled: SnsPolicyObservationStatus,
}
impl SnsNeuronDetail {
#[must_use]
pub fn derived_policy_observations(
&self,
) -> (SnsPolicyObservationStatus, SnsPolicyObservationStatus) {
neuron_policy_observations(
&self.permissions,
!self.disburse_maturity_in_progress.is_empty(),
)
}
}
pub(in crate::sns::report::model) fn neuron_policy_observations(
permissions: &[SnsNeuronPermissionRow],
has_pending_maturity_disbursement: bool,
) -> (SnsPolicyObservationStatus, SnsPolicyObservationStatus) {
let mut mint = if has_pending_maturity_disbursement {
SnsPolicyObservationStatus::Violated
} else {
SnsPolicyObservationStatus::ObservedSatisfied
};
let mut staking = if permissions.is_empty() {
mint = mint.combine(SnsPolicyObservationStatus::Unassessable);
SnsPolicyObservationStatus::Unassessable
} else {
SnsPolicyObservationStatus::ObservedSatisfied
};
for permission in permissions {
if permission.principal.is_none() || permission.permission_types.is_empty() {
mint = mint.combine(SnsPolicyObservationStatus::Unassessable);
staking = staking.combine(SnsPolicyObservationStatus::Unassessable);
}
for code in permission.permission_types.iter().map(|value| value.code) {
let (code_mint, code_staking) = permission_code_policy_observations(code);
mint = mint.combine(code_mint);
staking = staking.combine(code_staking);
}
}
(mint, staking)
}
pub(in crate::sns::report::model) const fn permission_code_policy_observations(
code: i32,
) -> (SnsPolicyObservationStatus, SnsPolicyObservationStatus) {
match code {
7 | 8 => (
SnsPolicyObservationStatus::Violated,
SnsPolicyObservationStatus::ObservedSatisfied,
),
9 => (
SnsPolicyObservationStatus::ObservedSatisfied,
SnsPolicyObservationStatus::Violated,
),
0 | 11..=i32::MAX | i32::MIN..=-1 => (
SnsPolicyObservationStatus::Unassessable,
SnsPolicyObservationStatus::Unassessable,
),
1..=6 | 10 => (
SnsPolicyObservationStatus::ObservedSatisfied,
SnsPolicyObservationStatus::ObservedSatisfied,
),
}
}
#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct SnsNeuronDetailReport {
pub schema_version: u32,
pub network: String,
pub sns_wasm_canister_id: String,
pub fetched_at: String,
pub source_endpoint: String,
pub fetched_by: String,
pub id: usize,
pub name: String,
pub root_canister_id: String,
pub governance_canister_id: String,
pub neuron_id: String,
pub data_source: String,
pub detail: SnsNeuronDetail,
}
#[must_use]
pub const fn sns_neuron_permission_name(code: i32) -> &'static str {
match code {
0 => "unspecified",
1 => "configure_dissolve_state",
2 => "manage_principals",
3 => "submit_proposal",
4 => "vote",
5 => "disburse",
6 => "split",
7 => "merge_maturity",
8 => "disburse_maturity",
9 => "stake_maturity",
10 => "manage_voting_permission",
_ => "unknown",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn permission_names_cover_native_and_unknown_codes() {
for (code, expected) in [
(0, "unspecified"),
(1, "configure_dissolve_state"),
(2, "manage_principals"),
(3, "submit_proposal"),
(4, "vote"),
(5, "disburse"),
(6, "split"),
(7, "merge_maturity"),
(8, "disburse_maturity"),
(9, "stake_maturity"),
(10, "manage_voting_permission"),
(11, "unknown"),
(-1, "unknown"),
] {
assert_eq!(sns_neuron_permission_name(code), expected, "code {code}");
}
}
#[test]
fn policy_observations_fail_closed_and_prioritize_known_violations() {
let mut detail = SnsNeuronDetail {
neuron: SnsNeuronRow {
neuron_id: "00".repeat(32),
cached_neuron_stake_e8s: 0,
maturity_e8s_equivalent: 0,
staked_maturity_e8s_equivalent: None,
created_timestamp_seconds: 0,
created_at: "1970-01-01T00:00:00Z".to_string(),
source_nns_neuron_id: None,
auto_stake_maturity: None,
aging_since_timestamp_seconds: 0,
dissolve_state: None,
voting_power_percentage_multiplier: 100,
vesting_period_seconds: None,
neuron_fees_e8s: 0,
},
permissions: vec![SnsNeuronPermissionRow {
principal: Some("aaaaa-aa".to_string()),
permission_types: vec![SnsNeuronPermissionValue::from_code(11)],
}],
disburse_maturity_in_progress: Vec::new(),
followees: Vec::new(),
topic_followees: None,
maturity_mint_conversion_observed_disabled:
SnsPolicyObservationStatus::ObservedSatisfied,
manual_maturity_staking_observed_disabled:
SnsPolicyObservationStatus::ObservedSatisfied,
};
assert_eq!(
detail.derived_policy_observations(),
(
SnsPolicyObservationStatus::Unassessable,
SnsPolicyObservationStatus::Unassessable,
)
);
detail.permissions[0]
.permission_types
.push(SnsNeuronPermissionValue::from_code(7));
assert_eq!(
detail.derived_policy_observations(),
(
SnsPolicyObservationStatus::Violated,
SnsPolicyObservationStatus::Unassessable,
)
);
}
}