use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
use crate::error::CompositionError;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum AnchorType {
#[serde(rename = "starknet-l3")]
StarknetL3,
#[serde(rename = "ethereum-l1")]
EthereumL1,
#[serde(rename = "bitcoin-opreturn")]
BitcoinOpReturn,
#[serde(rename = "x509-dsc")]
X509Dsc,
#[serde(rename = "dnssec")]
Dnssec,
#[serde(rename = "did-method")]
DidMethod,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AnchorEntry {
#[serde(rename = "type")]
pub anchor_type: AnchorType,
#[serde(with = "serde_bytes")]
pub r#ref: Vec<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub epoch: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none", with = "opt_bytes")]
pub nullifier: Option<Vec<u8>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub meta: Option<BTreeMap<String, serde_json::Value>>,
}
mod opt_bytes {
use alloc::vec::Vec;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub fn serialize<S: Serializer>(v: &Option<Vec<u8>>, s: S) -> Result<S::Ok, S::Error> {
match v {
Some(b) => serde_bytes::Bytes::new(b).serialize(s),
None => s.serialize_none(),
}
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Vec<u8>>, D::Error> {
let opt: Option<serde_bytes::ByteBuf> = Option::deserialize(d)?;
Ok(opt.map(serde_bytes::ByteBuf::into_vec))
}
}
impl AnchorEntry {
pub fn validate_shape(&self) -> Result<(), CompositionError> {
if let Some(n) = &self.nullifier {
if n.len() != 32 {
return Err(CompositionError::Invariant(
"anchor-entry.nullifier must be 32 bytes (Poseidon-felt252)",
));
}
}
match self.anchor_type {
AnchorType::StarknetL3 | AnchorType::EthereumL1 | AnchorType::BitcoinOpReturn => {
if self.r#ref.len() != 32 {
return Err(CompositionError::Invariant(
"anchor-entry.ref must be 32 bytes for chain anchors",
));
}
}
_ => {
if self.r#ref.is_empty() {
return Err(CompositionError::Invariant(
"anchor-entry.ref must be non-empty",
));
}
}
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Anchor(pub Vec<AnchorEntry>);
impl Anchor {
pub fn new(entries: Vec<AnchorEntry>) -> Result<Self, CompositionError> {
if entries.is_empty() {
return Err(CompositionError::Invariant(
"anchor list must contain at least one entry (I-4)",
));
}
for e in &entries {
e.validate_shape()?;
}
Ok(Self(entries))
}
pub fn uri(&self) -> &str {
match self.0.first() {
Some(e) => match e.anchor_type {
AnchorType::StarknetL3 => "starknet-l3",
AnchorType::EthereumL1 => "ethereum-l1",
AnchorType::BitcoinOpReturn => "bitcoin-opreturn",
AnchorType::X509Dsc => "x509-dsc",
AnchorType::Dnssec => "dnssec",
AnchorType::DidMethod => "did-method",
},
None => "unknown",
}
}
pub fn hash(&self) -> &[u8] { self.0.first().map_or(&[], |e| e.r#ref.as_slice()) }
pub fn epoch(&self) -> u64 { self.0.first().and_then(|e| e.epoch).unwrap_or(0) }
pub fn union(&self, other: &Self) -> Self {
let mut out = self.0.clone();
for e in &other.0 {
if !out.iter().any(|x| x == e) {
out.push(e.clone());
}
}
Self(out)
}
}