use chia_protocol::Bytes32;
use serde::{Deserialize, Serialize};
use thiserror::Error as ThisError;
pub const CHIP0007_FORMAT: &str = "CHIP-0007";
#[derive(Debug, ThisError, PartialEq, Eq)]
pub enum MetadataError {
#[error("invalid format: expected \"{CHIP0007_FORMAT}\", got {0:?}")]
BadFormat(String),
#[error("missing required field: {0}")]
MissingField(&'static str),
#[error("{which} hash mismatch: bytes hash to {computed} but on-chain hash is {expected}")]
HashMismatch {
which: &'static str,
computed: String,
expected: String,
},
#[error("series_number ({number}) exceeds series_total ({total})")]
BadSeries { number: u64, total: u64 },
#[error("json error: {0}")]
Json(String),
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct Attribute {
pub trait_type: String,
pub value: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct CollectionAttribute {
#[serde(rename = "type", alias = "trait_type")]
pub kind: String,
pub value: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct CollectionRef {
pub id: String,
pub name: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub attributes: Vec<CollectionAttribute>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct Chip0007Metadata {
pub format: String,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub sensitive_content: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub collection: Option<CollectionRef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub attributes: Vec<Attribute>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub series_number: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub series_total: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub minting_tool: Option<String>,
}
impl Chip0007Metadata {
pub fn new(name: impl Into<String>) -> Self {
Self {
format: CHIP0007_FORMAT.to_string(),
name: name.into(),
description: None,
sensitive_content: false,
collection: None,
attributes: Vec::new(),
series_number: None,
series_total: None,
minting_tool: None,
}
}
pub fn to_canonical_json(&self) -> Result<String, MetadataError> {
serde_json::to_string(self).map_err(|e| MetadataError::Json(e.to_string()))
}
pub fn compute_metadata_hash(&self) -> Result<Bytes32, MetadataError> {
Ok(sha256(self.to_canonical_json()?.as_bytes()))
}
pub fn validate_schema(&self) -> Result<(), MetadataError> {
if self.format != CHIP0007_FORMAT {
return Err(MetadataError::BadFormat(self.format.clone()));
}
if self.name.trim().is_empty() {
return Err(MetadataError::MissingField("name"));
}
if let (Some(number), Some(total)) = (self.series_number, self.series_total) {
if number > total {
return Err(MetadataError::BadSeries { number, total });
}
}
Ok(())
}
}
pub fn sha256(bytes: &[u8]) -> Bytes32 {
let mut h = chia_sha2::Sha256::new();
h.update(bytes);
Bytes32::new(h.finalize())
}
pub fn validate_uri_hash(
which: &'static str,
bytes: &[u8],
expected: Bytes32,
) -> Result<(), MetadataError> {
let computed = sha256(bytes);
if computed != expected {
return Err(MetadataError::HashMismatch {
which,
computed: format!("{computed}"),
expected: format!("{expected}"),
});
}
Ok(())
}