1use core::cmp::Ordering;
37use core::fmt::Debug;
38use core::hash::{Hash, Hasher};
39use core::num::ParseIntError;
40
41use aluvm::{Lib, LibId};
42use amplify::confinement::{SmallOrdMap, SmallOrdSet, TinyOrdMap, TinyOrdSet, TinyString};
43use amplify::num::u256;
44use amplify::Bytes4;
45use baid64::Baid64ParseError;
46use commit_verify::{CommitEncode, CommitEngine, CommitId, StrictHash};
47use indexmap::{indexset, IndexMap, IndexSet};
48use sonic_callreq::{CallState, MethodName, StateName};
49use strict_encoding::TypeName;
50use strict_types::{SemId, StrictDecode, StrictDumb, StrictEncode, StrictVal, TypeSystem};
51use ultrasonic::{CallId, Codex, CodexId, StateData, StateValue};
52
53use crate::{
54 Aggregator, RawBuilder, RawConvertor, StateArithm, StateAtom, StateBuildError, StateBuilder, StateCalc,
55 StateConvertError, StateConvertor, LIB_NAME_SONIC,
56};
57
58#[derive(Debug, Display, Error, From)]
60#[display(doc_comments)]
61pub enum ParseVersionedError {
62 NoVersion(String),
65 NoChecksum(String),
68 Id(Baid64ParseError),
70 #[from]
71 Version(ParseIntError),
73 Checksum(Baid64ParseError),
75}
76
77#[derive(Wrapper, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, From)]
87#[wrapper(Deref, BorrowSlice, Hex, Index, RangeOps)]
88#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
89#[strict_type(lib = LIB_NAME_SONIC)]
90pub struct ApisChecksum(
91 #[from]
92 #[from([u8; 4])]
93 Bytes4,
94);
95
96mod _baid4 {
97 use core::fmt::{self, Display, Formatter};
98 use core::str::FromStr;
99
100 use amplify::ByteArray;
101 use baid64::{Baid64ParseError, DisplayBaid64, FromBaid64Str};
102 use commit_verify::{CommitmentId, DigestExt, Sha256};
103
104 use super::*;
105
106 impl DisplayBaid64<4> for ApisChecksum {
107 const HRI: &'static str = "api";
108 const CHUNKING: bool = false;
109 const PREFIX: bool = false;
110 const EMBED_CHECKSUM: bool = false;
111 const MNEMONIC: bool = false;
112 fn to_baid64_payload(&self) -> [u8; 4] { self.to_byte_array() }
113 }
114 impl FromBaid64Str<4> for ApisChecksum {}
115 impl FromStr for ApisChecksum {
116 type Err = Baid64ParseError;
117 fn from_str(s: &str) -> Result<Self, Self::Err> { Self::from_baid64_str(s) }
118 }
119 impl Display for ApisChecksum {
120 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_baid64(f) }
121 }
122
123 impl From<Sha256> for ApisChecksum {
124 fn from(hasher: Sha256) -> Self {
125 let hash = hasher.finish();
126 Self::from_slice_checked(&hash[..4])
127 }
128 }
129
130 impl CommitmentId for ApisChecksum {
131 const TAG: &'static str = "urn:ubideco:sonic:apis#2025-05-25";
132 }
133
134 #[cfg(feature = "serde")]
135 ultrasonic::impl_serde_str_bin_wrapper!(ApisChecksum, Bytes4);
136}
137
138#[derive(Clone, Eq, Debug)]
145#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
146#[strict_type(lib = LIB_NAME_SONIC)]
147#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))]
148pub struct Semantics {
149 pub version: u16,
154 pub default: Api,
156 pub custom: SmallOrdMap<TypeName, Api>,
163 pub codex_libs: SmallOrdSet<Lib>,
165 pub api_libs: SmallOrdSet<Lib>,
167 pub types: TypeSystem,
169}
170
171impl PartialEq for Semantics {
172 fn eq(&self, other: &Self) -> bool {
173 self.default.codex_id == other.default.codex_id
174 && self.version == other.version
175 && self.commit_id() == other.commit_id()
176 }
177}
178impl PartialOrd for Semantics {
179 fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
180}
181impl Ord for Semantics {
182 fn cmp(&self, other: &Self) -> Ordering {
183 match self.default.codex_id.cmp(&other.default.codex_id) {
184 Ordering::Equal => match self.version.cmp(&other.version) {
185 Ordering::Equal => self.commit_id().cmp(&other.commit_id()),
186 other => other,
187 },
188 other => other,
189 }
190 }
191}
192
193impl CommitEncode for Semantics {
194 type CommitmentId = ApisChecksum;
195 fn commit_encode(&self, e: &mut CommitEngine) {
196 e.commit_to_serialized(&self.version);
197 e.commit_to_hash(&self.default);
198 let apis = SmallOrdMap::from_iter_checked(
203 self.custom
204 .iter()
205 .map(|(name, api)| (name.clone(), api.api_id())),
206 );
207 e.commit_to_linear_map(&apis);
208 let libs = SmallOrdSet::from_iter_checked(self.api_libs.iter().map(Lib::lib_id));
209 e.commit_to_linear_set(&libs);
210 e.commit_to_serialized(&self.types.id());
211 }
212}
213
214impl Semantics {
215 pub fn apis_checksum(&self) -> ApisChecksum { self.commit_id() }
216
217 pub fn apis(&self) -> impl Iterator<Item = &Api> { [&self.default].into_iter().chain(self.custom.values()) }
219
220 pub fn check(&self, codex: &Codex) -> Result<(), SemanticError> {
222 let codex_id = codex.codex_id();
223
224 let mut ids = bset![];
225 for api in self.apis() {
226 if api.codex_id != codex_id {
227 return Err(SemanticError::CodexMismatch);
228 }
229 let api_id = api.api_id();
230 if !ids.insert(api_id) {
231 return Err(SemanticError::DuplicatedApi(api_id));
232 }
233 }
234
235 let lib_map = self
237 .codex_libs
238 .iter()
239 .map(|lib| (lib.lib_id(), lib))
240 .collect::<IndexMap<_, _>>();
241
242 let mut lib_ids = codex
243 .verifiers
244 .values()
245 .map(|entry| entry.lib_id)
246 .collect::<IndexSet<_>>();
247 let mut i = 0usize;
248 let mut count = lib_ids.len();
249 while i < count {
250 let id = lib_ids.get_index(i).expect("index is valid");
251 let lib = lib_map.get(id).ok_or(SemanticError::MissedCodexLib(*id))?;
252 lib_ids.extend(lib.libs.iter().copied());
253 count = lib_ids.len();
254 i += 1;
255 }
256 for id in lib_map.keys() {
257 if !lib_ids.contains(id) {
258 return Err(SemanticError::ExcessiveCodexLib(*id));
259 }
260 }
261
262 let lib_map = self
264 .api_libs
265 .iter()
266 .map(|lib| (lib.lib_id(), lib))
267 .collect::<IndexMap<_, _>>();
268
269 let mut lib_ids = indexset![];
270 for api in self.apis() {
271 for agg in api.aggregators.values() {
272 if let Aggregator::AluVM(entry) = agg {
273 lib_ids.insert(entry.lib_id);
274 }
275 }
276 for glob in api.global.values() {
277 if let StateConvertor::AluVM(entry) = glob.convertor {
278 lib_ids.insert(entry.lib_id);
279 }
280 if let StateBuilder::AluVM(entry) = glob.builder {
281 lib_ids.insert(entry.lib_id);
282 }
283 if let RawConvertor::AluVM(entry) = glob.raw_convertor {
284 lib_ids.insert(entry.lib_id);
285 }
286 if let RawBuilder::AluVM(entry) = glob.raw_builder {
287 lib_ids.insert(entry.lib_id);
288 }
289 }
290 for owned in api.owned.values() {
291 if let StateConvertor::AluVM(entry) = owned.convertor {
292 lib_ids.insert(entry.lib_id);
293 }
294 if let StateBuilder::AluVM(entry) = owned.builder {
295 lib_ids.insert(entry.lib_id);
296 }
297 if let StateBuilder::AluVM(entry) = owned.witness_builder {
298 lib_ids.insert(entry.lib_id);
299 }
300 if let StateArithm::AluVM(entry) = owned.arithmetics {
301 lib_ids.insert(entry.lib_id);
302 }
303 }
304 }
305 let mut i = 0usize;
306 let mut count = lib_ids.len();
307 while i < count {
308 let id = lib_ids.get_index(i).expect("index is valid");
309 let lib = lib_map.get(id).ok_or(SemanticError::MissedApiLib(*id))?;
310 lib_ids.extend(lib.libs.iter().copied());
311 count = lib_ids.len();
312 i += 1;
313 }
314 for id in lib_map.keys() {
315 if !lib_ids.contains(id) {
316 return Err(SemanticError::ExcessiveApiLib(*id));
317 }
318 }
319
320 Ok(())
321 }
322}
323
324#[derive(Getters, Clone, Debug)]
333#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
334#[strict_type(lib = LIB_NAME_SONIC)]
335#[derive(CommitEncode)]
336#[commit_encode(strategy = strict, id = StrictHash)]
337#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase", bound = ""))]
338pub struct Api {
339 #[getter(as_copy)]
341 pub codex_id: CodexId,
342
343 pub conforms: TinyOrdSet<u16>,
345
346 pub default_call: Option<CallState>,
348
349 pub global: TinyOrdMap<StateName, GlobalApi>,
352
353 pub owned: TinyOrdMap<StateName, OwnedApi>,
356
357 pub aggregators: TinyOrdMap<MethodName, Aggregator>,
363
364 pub verifiers: TinyOrdMap<MethodName, CallId>,
369
370 pub errors: TinyOrdMap<u256, TinyString>,
373}
374
375impl PartialEq for Api {
376 fn eq(&self, other: &Self) -> bool { self.cmp(other) == Ordering::Equal }
377}
378impl Eq for Api {}
379impl PartialOrd for Api {
380 fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
381}
382impl Ord for Api {
383 fn cmp(&self, other: &Self) -> Ordering { self.api_id().cmp(&other.api_id()) }
384}
385impl Hash for Api {
386 fn hash<H: Hasher>(&self, state: &mut H) { self.api_id().hash(state); }
387}
388
389impl Api {
390 pub fn api_id(&self) -> StrictHash { self.commit_id() }
391
392 pub fn verifier(&self, method: impl Into<MethodName>) -> Option<CallId> {
393 self.verifiers.get(&method.into()).copied()
394 }
395
396 pub fn convert_global(
397 &self,
398 data: &StateData,
399 sys: &TypeSystem,
400 ) -> Result<Option<(StateName, StateAtom)>, StateConvertError> {
401 for (name, api) in &self.global {
406 if let Some(verified) = api.convertor.convert(api.sem_id, data.value, sys)? {
407 let unverified =
408 if let Some(raw) = data.raw.as_ref() { Some(api.raw_convertor.convert(raw, sys)?) } else { None };
409 return Ok(Some((name.clone(), StateAtom { verified, unverified })));
410 }
411 }
412 Ok(None)
414 }
415
416 pub fn convert_owned(
417 &self,
418 value: StateValue,
419 sys: &TypeSystem,
420 ) -> Result<Option<(StateName, StrictVal)>, StateConvertError> {
421 for (name, api) in &self.owned {
426 if let Some(atom) = api.convertor.convert(api.sem_id, value, sys)? {
427 return Ok(Some((name.clone(), atom)));
428 }
429 }
430 Ok(None)
432 }
433
434 #[allow(clippy::result_large_err)]
435 pub fn build_immutable(
436 &self,
437 name: impl Into<StateName>,
438 data: StrictVal,
439 raw: Option<StrictVal>,
440 sys: &TypeSystem,
441 ) -> Result<StateData, StateBuildError> {
442 let name = name.into();
443 let api = self
444 .global
445 .get(&name)
446 .ok_or(StateBuildError::UnknownStateName(name))?;
447 let value = api.builder.build(api.sem_id, data, sys)?;
448 let raw = raw.map(|raw| api.raw_builder.build(raw, sys)).transpose()?;
449 Ok(StateData { value, raw })
450 }
451
452 #[allow(clippy::result_large_err)]
453 pub fn build_destructible(
454 &self,
455 name: impl Into<StateName>,
456 data: StrictVal,
457 sys: &TypeSystem,
458 ) -> Result<StateValue, StateBuildError> {
459 let name = name.into();
460 let api = self
461 .owned
462 .get(&name)
463 .ok_or(StateBuildError::UnknownStateName(name))?;
464
465 api.builder.build(api.sem_id, data, sys)
466 }
467
468 #[allow(clippy::result_large_err)]
469 pub fn build_witness(
470 &self,
471 name: impl Into<StateName>,
472 data: StrictVal,
473 sys: &TypeSystem,
474 ) -> Result<StateValue, StateBuildError> {
475 let name = name.into();
476 let api = self
477 .owned
478 .get(&name)
479 .ok_or(StateBuildError::UnknownStateName(name))?;
480
481 api.witness_builder.build(api.witness_sem_id, data, sys)
482 }
483
484 pub fn calculate(&self, name: impl Into<StateName>) -> Result<StateCalc, StateUnknown> {
485 let name = name.into();
486 let api = self.owned.get(&name).ok_or(StateUnknown(name))?;
487
488 Ok(api.arithmetics.calculator())
489 }
490}
491
492#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
499#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
500#[strict_type(lib = LIB_NAME_SONIC)]
501#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))]
502pub struct GlobalApi {
503 pub sem_id: SemId,
505
506 pub published: bool,
508
509 pub convertor: StateConvertor,
512
513 pub builder: StateBuilder,
516
517 pub raw_convertor: RawConvertor,
520
521 pub raw_builder: RawBuilder,
524}
525
526#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
533#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
534#[strict_type(lib = LIB_NAME_SONIC)]
535#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))]
536pub struct OwnedApi {
537 pub sem_id: SemId,
539
540 pub arithmetics: StateArithm,
542
543 pub convertor: StateConvertor,
546
547 pub builder: StateBuilder,
550
551 pub witness_sem_id: SemId,
553
554 pub witness_builder: StateBuilder,
558}
559
560#[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)]
562#[display("unknown state name '{0}'")]
563pub struct StateUnknown(pub StateName);
564
565#[derive(Clone, Eq, PartialEq, Debug, Display, Error)]
568#[display(doc_comments)]
569pub enum SemanticError {
570 ContractMismatch,
572
573 CodexMismatch,
575
576 DuplicatedApi(StrictHash),
578
579 MissedCodexLib(LibId),
581
582 ExcessiveCodexLib(LibId),
584
585 MissedApiLib(LibId),
587
588 ExcessiveApiLib(LibId),
590
591 InvalidSignature,
593}