use core::cmp::Ordering;
use core::fmt::Debug;
use core::hash::{Hash, Hasher};
use core::num::ParseIntError;
use aluvm::{Lib, LibId};
use amplify::confinement::{SmallOrdMap, SmallOrdSet, TinyOrdMap, TinyOrdSet, TinyString};
use amplify::num::u256;
use amplify::Bytes4;
use baid64::Baid64ParseError;
use commit_verify::{CommitEncode, CommitEngine, CommitId, StrictHash};
use indexmap::{indexset, IndexMap, IndexSet};
use sonic_callreq::{CallState, MethodName, StateName};
use strict_encoding::TypeName;
use strict_types::{SemId, StrictDecode, StrictDumb, StrictEncode, StrictVal, TypeSystem};
use ultrasonic::{CallId, Codex, CodexId, StateData, StateValue};
use crate::{
Aggregator, RawBuilder, RawConvertor, StateArithm, StateAtom, StateBuildError, StateBuilder, StateCalc,
StateConvertError, StateConvertor, LIB_NAME_SONIC,
};
#[derive(Debug, Display, Error, From)]
#[display(doc_comments)]
pub enum ParseVersionedError {
NoVersion(String),
NoChecksum(String),
Id(Baid64ParseError),
#[from]
Version(ParseIntError),
Checksum(Baid64ParseError),
}
#[derive(Wrapper, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, From)]
#[wrapper(Deref, BorrowSlice, Hex, Index, RangeOps)]
#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
#[strict_type(lib = LIB_NAME_SONIC)]
pub struct ApisChecksum(
#[from]
#[from([u8; 4])]
Bytes4,
);
mod _baid4 {
use core::fmt::{self, Display, Formatter};
use core::str::FromStr;
use amplify::ByteArray;
use baid64::{Baid64ParseError, DisplayBaid64, FromBaid64Str};
use commit_verify::{CommitmentId, DigestExt, Sha256};
use super::*;
impl DisplayBaid64<4> for ApisChecksum {
const HRI: &'static str = "api";
const CHUNKING: bool = false;
const PREFIX: bool = false;
const EMBED_CHECKSUM: bool = false;
const MNEMONIC: bool = false;
fn to_baid64_payload(&self) -> [u8; 4] { self.to_byte_array() }
}
impl FromBaid64Str<4> for ApisChecksum {}
impl FromStr for ApisChecksum {
type Err = Baid64ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> { Self::from_baid64_str(s) }
}
impl Display for ApisChecksum {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_baid64(f) }
}
impl From<Sha256> for ApisChecksum {
fn from(hasher: Sha256) -> Self {
let hash = hasher.finish();
Self::from_slice_checked(&hash[..4])
}
}
impl CommitmentId for ApisChecksum {
const TAG: &'static str = "urn:ubideco:sonic:apis#2025-05-25";
}
#[cfg(feature = "serde")]
ultrasonic::impl_serde_str_bin_wrapper!(ApisChecksum, Bytes4);
}
#[derive(Clone, Eq, Debug)]
#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
#[strict_type(lib = LIB_NAME_SONIC)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))]
pub struct Semantics {
pub version: u16,
pub default: Api,
pub custom: SmallOrdMap<TypeName, Api>,
pub codex_libs: SmallOrdSet<Lib>,
pub api_libs: SmallOrdSet<Lib>,
pub types: TypeSystem,
}
impl PartialEq for Semantics {
fn eq(&self, other: &Self) -> bool {
self.default.codex_id == other.default.codex_id
&& self.version == other.version
&& self.commit_id() == other.commit_id()
}
}
impl PartialOrd for Semantics {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
}
impl Ord for Semantics {
fn cmp(&self, other: &Self) -> Ordering {
match self.default.codex_id.cmp(&other.default.codex_id) {
Ordering::Equal => match self.version.cmp(&other.version) {
Ordering::Equal => self.commit_id().cmp(&other.commit_id()),
other => other,
},
other => other,
}
}
}
impl CommitEncode for Semantics {
type CommitmentId = ApisChecksum;
fn commit_encode(&self, e: &mut CommitEngine) {
e.commit_to_serialized(&self.version);
e.commit_to_hash(&self.default);
let apis = SmallOrdMap::from_iter_checked(
self.custom
.iter()
.map(|(name, api)| (name.clone(), api.api_id())),
);
e.commit_to_linear_map(&apis);
let libs = SmallOrdSet::from_iter_checked(self.api_libs.iter().map(Lib::lib_id));
e.commit_to_linear_set(&libs);
e.commit_to_serialized(&self.types.id());
}
}
impl Semantics {
pub fn apis_checksum(&self) -> ApisChecksum { self.commit_id() }
pub fn apis(&self) -> impl Iterator<Item = &Api> { [&self.default].into_iter().chain(self.custom.values()) }
pub fn check(&self, codex: &Codex) -> Result<(), SemanticError> {
let codex_id = codex.codex_id();
let mut ids = bset![];
for api in self.apis() {
if api.codex_id != codex_id {
return Err(SemanticError::CodexMismatch);
}
let api_id = api.api_id();
if !ids.insert(api_id) {
return Err(SemanticError::DuplicatedApi(api_id));
}
}
let lib_map = self
.codex_libs
.iter()
.map(|lib| (lib.lib_id(), lib))
.collect::<IndexMap<_, _>>();
let mut lib_ids = codex
.verifiers
.values()
.map(|entry| entry.lib_id)
.collect::<IndexSet<_>>();
let mut i = 0usize;
let mut count = lib_ids.len();
while i < count {
let id = lib_ids.get_index(i).expect("index is valid");
let lib = lib_map.get(id).ok_or(SemanticError::MissedCodexLib(*id))?;
lib_ids.extend(lib.libs.iter().copied());
count = lib_ids.len();
i += 1;
}
for id in lib_map.keys() {
if !lib_ids.contains(id) {
return Err(SemanticError::ExcessiveCodexLib(*id));
}
}
let lib_map = self
.api_libs
.iter()
.map(|lib| (lib.lib_id(), lib))
.collect::<IndexMap<_, _>>();
let mut lib_ids = indexset![];
for api in self.apis() {
for agg in api.aggregators.values() {
if let Aggregator::AluVM(entry) = agg {
lib_ids.insert(entry.lib_id);
}
}
for glob in api.global.values() {
if let StateConvertor::AluVM(entry) = glob.convertor {
lib_ids.insert(entry.lib_id);
}
if let StateBuilder::AluVM(entry) = glob.builder {
lib_ids.insert(entry.lib_id);
}
if let RawConvertor::AluVM(entry) = glob.raw_convertor {
lib_ids.insert(entry.lib_id);
}
if let RawBuilder::AluVM(entry) = glob.raw_builder {
lib_ids.insert(entry.lib_id);
}
}
for owned in api.owned.values() {
if let StateConvertor::AluVM(entry) = owned.convertor {
lib_ids.insert(entry.lib_id);
}
if let StateBuilder::AluVM(entry) = owned.builder {
lib_ids.insert(entry.lib_id);
}
if let StateBuilder::AluVM(entry) = owned.witness_builder {
lib_ids.insert(entry.lib_id);
}
if let StateArithm::AluVM(entry) = owned.arithmetics {
lib_ids.insert(entry.lib_id);
}
}
}
let mut i = 0usize;
let mut count = lib_ids.len();
while i < count {
let id = lib_ids.get_index(i).expect("index is valid");
let lib = lib_map.get(id).ok_or(SemanticError::MissedApiLib(*id))?;
lib_ids.extend(lib.libs.iter().copied());
count = lib_ids.len();
i += 1;
}
for id in lib_map.keys() {
if !lib_ids.contains(id) {
return Err(SemanticError::ExcessiveApiLib(*id));
}
}
Ok(())
}
}
#[derive(Getters, Clone, Debug)]
#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
#[strict_type(lib = LIB_NAME_SONIC)]
#[derive(CommitEncode)]
#[commit_encode(strategy = strict, id = StrictHash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase", bound = ""))]
pub struct Api {
#[getter(as_copy)]
pub codex_id: CodexId,
pub conforms: TinyOrdSet<u16>,
pub default_call: Option<CallState>,
pub global: TinyOrdMap<StateName, GlobalApi>,
pub owned: TinyOrdMap<StateName, OwnedApi>,
pub aggregators: TinyOrdMap<MethodName, Aggregator>,
pub verifiers: TinyOrdMap<MethodName, CallId>,
pub errors: TinyOrdMap<u256, TinyString>,
}
impl PartialEq for Api {
fn eq(&self, other: &Self) -> bool { self.cmp(other) == Ordering::Equal }
}
impl Eq for Api {}
impl PartialOrd for Api {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
}
impl Ord for Api {
fn cmp(&self, other: &Self) -> Ordering { self.api_id().cmp(&other.api_id()) }
}
impl Hash for Api {
fn hash<H: Hasher>(&self, state: &mut H) { self.api_id().hash(state); }
}
impl Api {
pub fn api_id(&self) -> StrictHash { self.commit_id() }
pub fn verifier(&self, method: impl Into<MethodName>) -> Option<CallId> {
self.verifiers.get(&method.into()).copied()
}
pub fn convert_global(
&self,
data: &StateData,
sys: &TypeSystem,
) -> Result<Option<(StateName, StateAtom)>, StateConvertError> {
for (name, api) in &self.global {
if let Some(verified) = api.convertor.convert(api.sem_id, data.value, sys)? {
let unverified =
if let Some(raw) = data.raw.as_ref() { Some(api.raw_convertor.convert(raw, sys)?) } else { None };
return Ok(Some((name.clone(), StateAtom { verified, unverified })));
}
}
Ok(None)
}
pub fn convert_owned(
&self,
value: StateValue,
sys: &TypeSystem,
) -> Result<Option<(StateName, StrictVal)>, StateConvertError> {
for (name, api) in &self.owned {
if let Some(atom) = api.convertor.convert(api.sem_id, value, sys)? {
return Ok(Some((name.clone(), atom)));
}
}
Ok(None)
}
#[allow(clippy::result_large_err)]
pub fn build_immutable(
&self,
name: impl Into<StateName>,
data: StrictVal,
raw: Option<StrictVal>,
sys: &TypeSystem,
) -> Result<StateData, StateBuildError> {
let name = name.into();
let api = self
.global
.get(&name)
.ok_or(StateBuildError::UnknownStateName(name))?;
let value = api.builder.build(api.sem_id, data, sys)?;
let raw = raw.map(|raw| api.raw_builder.build(raw, sys)).transpose()?;
Ok(StateData { value, raw })
}
#[allow(clippy::result_large_err)]
pub fn build_destructible(
&self,
name: impl Into<StateName>,
data: StrictVal,
sys: &TypeSystem,
) -> Result<StateValue, StateBuildError> {
let name = name.into();
let api = self
.owned
.get(&name)
.ok_or(StateBuildError::UnknownStateName(name))?;
api.builder.build(api.sem_id, data, sys)
}
#[allow(clippy::result_large_err)]
pub fn build_witness(
&self,
name: impl Into<StateName>,
data: StrictVal,
sys: &TypeSystem,
) -> Result<StateValue, StateBuildError> {
let name = name.into();
let api = self
.owned
.get(&name)
.ok_or(StateBuildError::UnknownStateName(name))?;
api.witness_builder.build(api.witness_sem_id, data, sys)
}
pub fn calculate(&self, name: impl Into<StateName>) -> Result<StateCalc, StateUnknown> {
let name = name.into();
let api = self.owned.get(&name).ok_or(StateUnknown(name))?;
Ok(api.arithmetics.calculator())
}
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
#[strict_type(lib = LIB_NAME_SONIC)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))]
pub struct GlobalApi {
pub sem_id: SemId,
pub published: bool,
pub convertor: StateConvertor,
pub builder: StateBuilder,
pub raw_convertor: RawConvertor,
pub raw_builder: RawBuilder,
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
#[strict_type(lib = LIB_NAME_SONIC)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))]
pub struct OwnedApi {
pub sem_id: SemId,
pub arithmetics: StateArithm,
pub convertor: StateConvertor,
pub builder: StateBuilder,
pub witness_sem_id: SemId,
pub witness_builder: StateBuilder,
}
#[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)]
#[display("unknown state name '{0}'")]
pub struct StateUnknown(pub StateName);
#[derive(Clone, Eq, PartialEq, Debug, Display, Error)]
#[display(doc_comments)]
pub enum SemanticError {
ContractMismatch,
CodexMismatch,
DuplicatedApi(StrictHash),
MissedCodexLib(LibId),
ExcessiveCodexLib(LibId),
MissedApiLib(LibId),
ExcessiveApiLib(LibId),
InvalidSignature,
}