211 lines
8.8 KiB
Rust
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()
|
|
);
|
|
}
|
|
}
|