Files
khadhroony-bot3/kb-pipeline-demo-scenarios/src/spl/token_2022/metadata/validation.rs
2026-08-08 22:28:17 +02:00

211 lines
8.8 KiB
Rust

// file: kb-pipeline-demo-scenarios/src/spl/token_2022/metadata/validation.rs
// version: 3
//! Conservative validation matrix for the `0.4.8-pre.012` Token Metadata campaign.
/// Maximum evidence items retained by one campaign scenario.
pub const MAX_TOKEN_2022_METADATA_VALIDATION_EVIDENCE: usize = 16;
/// Exact network-validation status for one Token Metadata scenario.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Token2022MetadataValidationStatus {
/// No real Devnet execution has been recorded.
NotRun,
/// The scenario completed with all declared evidence.
Confirmed,
/// The scenario is unavailable for an explicit recorded reason.
Unavailable,
}
/// One bounded evidence item retained by the validation matrix.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Token2022MetadataValidationEvidence {
/// Stable evidence kind.
pub kind: std::string::String,
/// Bounded evidence value such as a signature, slot or diagnostic.
pub value: std::string::String,
}
/// One ordered Token Metadata scenario in the canonical Devnet matrix.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Token2022MetadataValidationScenario {
/// Stable campaign step identifier.
pub id: std::string::String,
/// Short Token Metadata interface operation name.
pub operation: std::string::String,
/// Current evidence-qualified network status.
pub status: crate::Token2022MetadataValidationStatus,
/// Evidence kinds required before promotion to `confirmed`.
pub required_evidence: std::vec::Vec<std::string::String>,
/// Bounded observed evidence.
pub evidence: std::vec::Vec<crate::Token2022MetadataValidationEvidence>,
}
/// Canonical `0.4.8-pre.012` Token Metadata Devnet validation matrix.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Token2022MetadataValidationMatrix {
/// Matrix schema version.
pub matrix_version: u32,
/// Milestone owning the campaign.
pub milestone: std::string::String,
/// Token-2022 program ID.
pub program_id: std::string::String,
/// Five scenarios in campaign order.
pub scenarios: std::vec::Vec<crate::Token2022MetadataValidationScenario>,
}
/// Loads and validates the canonical Token Metadata Devnet matrix.
pub fn load_token_2022_metadata_validation_matrix()
-> kb_core::Result<crate::Token2022MetadataValidationMatrix> {
let matrix = match serde_json::from_str::<crate::Token2022MetadataValidationMatrix>(
include_str!(
"../../../../../test-fixtures/contract-matrices/SPL_TOKEN_2022_METADATA_DEVNET_VALIDATION_MATRIX.json"
),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::new(
"token_2022_metadata_validation_matrix_invalid_json",
error.to_string(),
));
},
};
return match crate::validate_token_2022_metadata_validation_matrix(&matrix) {
std::result::Result::Ok(()) => std::result::Result::Ok(matrix),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
/// Validates campaign coverage and prevents promotion without complete evidence.
pub fn validate_token_2022_metadata_validation_matrix(
matrix: &crate::Token2022MetadataValidationMatrix,
) -> kb_core::Result<()> {
if matrix.matrix_version != 1
|| matrix.milestone != "0.4.8-pre.012"
|| matrix.program_id != kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID
{
return std::result::Result::Err(kb_core::Error::config(
"Token-2022 metadata validation matrix identity is invalid",
));
}
let expected = crate::token_2022_metadata_campaign_operation_names();
if matrix.scenarios.len() != expected.len() {
return std::result::Result::Err(kb_core::Error::config(format!(
"Token-2022 metadata validation matrix requires exactly {} scenarios",
expected.len()
)));
}
let mut ids = std::collections::BTreeSet::new();
for (index, scenario) in matrix.scenarios.iter().enumerate() {
if !ids.insert(scenario.id.as_str()) {
return std::result::Result::Err(kb_core::Error::config(format!(
"duplicate Token-2022 metadata validation scenario {}",
scenario.id
)));
}
if scenario.operation != expected[index] {
return std::result::Result::Err(kb_core::Error::config(format!(
"Token-2022 metadata scenario {} expected operation {}, got {}",
scenario.id, expected[index], scenario.operation
)));
}
if scenario.required_evidence.is_empty()
|| scenario.required_evidence.len() > MAX_TOKEN_2022_METADATA_VALIDATION_EVIDENCE
|| scenario.evidence.len() > MAX_TOKEN_2022_METADATA_VALIDATION_EVIDENCE
{
return std::result::Result::Err(kb_core::Error::config(format!(
"Token-2022 metadata scenario {} has invalid evidence bounds",
scenario.id
)));
}
let mut required = std::collections::BTreeSet::new();
if scenario
.required_evidence
.iter()
.any(|kind| return kind.trim().is_empty() || !required.insert(kind.as_str()))
{
return std::result::Result::Err(kb_core::Error::config(format!(
"Token-2022 metadata scenario {} has empty or duplicate required evidence",
scenario.id
)));
}
let mut observed = std::collections::BTreeSet::new();
for evidence in &scenario.evidence {
if evidence.kind.trim().is_empty()
|| evidence.value.trim().is_empty()
|| !observed.insert(evidence.kind.as_str())
{
return std::result::Result::Err(kb_core::Error::config(format!(
"Token-2022 metadata scenario {} has invalid observed evidence",
scenario.id
)));
}
}
match scenario.status {
crate::Token2022MetadataValidationStatus::NotRun => {
if !scenario.evidence.is_empty() {
return std::result::Result::Err(kb_core::Error::config(format!(
"not-run Token-2022 metadata scenario {} cannot claim evidence",
scenario.id
)));
}
},
crate::Token2022MetadataValidationStatus::Confirmed => {
if scenario
.required_evidence
.iter()
.any(|kind| return !observed.contains(kind.as_str()))
{
return std::result::Result::Err(kb_core::Error::config(format!(
"confirmed Token-2022 metadata scenario {} is missing required evidence",
scenario.id
)));
}
},
crate::Token2022MetadataValidationStatus::Unavailable => {
if !observed.contains("unavailable_reason") {
return std::result::Result::Err(kb_core::Error::config(format!(
"unavailable Token-2022 metadata scenario {} requires unavailable_reason",
scenario.id
)));
}
},
}
}
return std::result::Result::Ok(());
}
#[cfg(test)]
mod tests {
#[test]
fn canonical_matrix_is_closed_ordered_and_confirmed_after_network_execution() {
let matrix = crate::load_token_2022_metadata_validation_matrix()
.unwrap_or_else(|error| panic!("Token-2022 metadata matrix failed: {error}"));
assert_eq!(matrix.scenarios.len(), 5);
assert!(matrix.scenarios.iter().all(|scenario| {
let observed = scenario
.evidence
.iter()
.map(|evidence| return evidence.kind.as_str())
.collect::<std::collections::BTreeSet<_>>();
return scenario.status == crate::Token2022MetadataValidationStatus::Confirmed
&& scenario
.required_evidence
.iter()
.all(|kind| return observed.contains(kind.as_str()));
}));
assert_eq!(
matrix
.scenarios
.iter()
.map(|scenario| return scenario.operation.as_str())
.collect::<std::vec::Vec<_>>(),
crate::token_2022_metadata_campaign_operation_names().as_slice()
);
}
}