use super::SnsNeuronRow;
#[cfg(feature = "sns-host")]
use crate::hex::is_lowercase_hex_32_bytes;
use crate::report::ReportDataSource;
use crate::sns::report::SnsReportContext;
use serde::{Deserialize as SerdeDeserialize, Serialize};
#[cfg(feature = "sns-host")]
use std::collections::HashSet;
#[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 SnsNeuronPermissionRow {
pub principal: Option<String>,
pub permission_types: Vec<i32>,
}
#[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>>,
}
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) 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().copied() {
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,
),
_ if permission_code_is_assessable(code) => (
SnsPolicyObservationStatus::ObservedSatisfied,
SnsPolicyObservationStatus::ObservedSatisfied,
),
_ => (
SnsPolicyObservationStatus::Unassessable,
SnsPolicyObservationStatus::Unassessable,
),
}
}
pub(in crate::sns::report::model) const fn permission_code_is_assessable(code: i32) -> bool {
matches!(code, 1..=10)
}
#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct SnsNeuronDetailReport {
pub schema_version: u32,
#[serde(flatten)]
pub context: SnsReportContext,
pub neuron_id: String,
pub data_source: ReportDataSource,
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(feature = "sns-host")]
pub(in crate::sns::report) fn validate_neuron_permissions(
permissions: &[SnsNeuronPermissionRow],
) -> Result<(), String> {
let mut principals = HashSet::new();
for permission in permissions {
let principal = match permission.principal.as_deref() {
Some(principal) => {
validate_canonical_principal(principal, "permission principal")?;
if !principals.insert(principal) {
return Err(format!("duplicate permission principal {principal}"));
}
principal
}
None => {
return Err("permission principal is missing".to_string());
}
};
let mut codes = HashSet::new();
for code in &permission.permission_types {
if !codes.insert(*code) {
return Err(format!(
"{principal} contains duplicate permission code {code}"
));
}
}
}
Ok(())
}
#[cfg(feature = "sns-host")]
pub(in crate::sns::report) fn validate_maturity_disbursements(
disbursements: &[SnsMaturityDisbursementRow],
) -> Result<(), String> {
for disbursement in disbursements {
if let Some(account) = disbursement.account_to_disburse_to.as_ref() {
if let Some(owner) = account.owner.as_deref() {
validate_canonical_principal(owner, "pending disbursement account owner")?;
}
if let Some(subaccount) = account.subaccount_hex.as_deref()
&& !is_lowercase_hex_32_bytes(subaccount)
{
return Err(
"pending disbursement subaccount is not 32-byte lowercase hexadecimal text"
.to_string(),
);
}
}
}
Ok(())
}
#[cfg(feature = "sns-host")]
pub(super) fn validate_canonical_principal(value: &str, field: &str) -> Result<(), String> {
crate::principal_text::validate_canonical(
value,
|reason| format!("{field} {value} is invalid: {reason}"),
|_| format!("{field} {value} is not canonical principal text"),
)
}
#[cfg(test)]
mod tests;