sonicapi/
articles.rs

1// SONIC: Standard library for formally-verifiable distributed contracts
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Designed in 2019-2025 by Dr Maxim Orlovsky <orlovsky@ubideco.org>
6// Written in 2024-2025 by Dr Maxim Orlovsky <orlovsky@ubideco.org>
7//
8// Copyright (C) 2019-2024 LNP/BP Standards Association, Switzerland.
9// Copyright (C) 2024-2025 Laboratories for Ubiquitous Deterministic Computing (UBIDECO),
10//                         Institute for Distributed and Cognitive Systems (InDCS), Switzerland.
11// Copyright (C) 2019-2025 Dr Maxim Orlovsky.
12// All rights under the above copyrights are reserved.
13//
14// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
15// in compliance with the License. You may obtain a copy of the License at
16//
17//        http://www.apache.org/licenses/LICENSE-2.0
18//
19// Unless required by applicable law or agreed to in writing, software distributed under the License
20// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
21// or implied. See the License for the specific language governing permissions and limitations under
22// the License.
23
24use std::io;
25
26use amplify::hex::ToHex;
27use strict_encoding::{
28    DecodeError, ReadRaw, StrictDecode, StrictEncode, StrictReader, StrictWriter, TypeName, WriteRaw,
29};
30use ultrasonic::{ContractId, Issue};
31
32use crate::sigs::ContentSigs;
33use crate::{Api, Schema, LIB_NAME_SONIC};
34
35pub const ARTICLES_MAGIC_NUMBER: [u8; 8] = *b"ARTICLES";
36pub const ARTICLES_VERSION: [u8; 2] = [0x00, 0x01];
37
38/// Articles contain the contract and all related codex and API information for interacting with it.
39#[derive(Clone, Eq, PartialEq, Debug)]
40#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
41#[strict_type(lib = LIB_NAME_SONIC)]
42#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))]
43pub struct Articles {
44    #[cfg_attr(feature = "serde", serde(flatten))]
45    pub schema: Schema,
46    pub contract_sigs: ContentSigs,
47    pub issue: Issue,
48}
49
50impl Articles {
51    pub fn contract_id(&self) -> ContractId { self.issue.contract_id() }
52
53    pub fn api(&self, name: &TypeName) -> &Api { self.schema.api(name) }
54
55    pub fn merge(&mut self, other: Self) -> Result<bool, MergeError> {
56        if self.contract_id() != other.contract_id() {
57            return Err(MergeError::ContractMismatch);
58        }
59
60        self.schema.merge(other.schema)?;
61        self.contract_sigs.merge(other.contract_sigs);
62
63        Ok(true)
64    }
65
66    pub fn decode(reader: &mut StrictReader<impl ReadRaw>) -> Result<Self, DecodeError> {
67        let magic_bytes = <[u8; 8]>::strict_decode(reader)?;
68        if magic_bytes != ARTICLES_MAGIC_NUMBER {
69            return Err(DecodeError::DataIntegrityError(format!(
70                "wrong contract articles magic bytes {}",
71                magic_bytes.to_hex()
72            )));
73        }
74        let version = <[u8; 2]>::strict_decode(reader)?;
75        if version != ARTICLES_VERSION {
76            return Err(DecodeError::DataIntegrityError(format!(
77                "unsupported contract articles version {}",
78                u16::from_be_bytes(version)
79            )));
80        }
81        Self::strict_decode(reader)
82    }
83
84    pub fn encode(&self, mut writer: StrictWriter<impl WriteRaw>) -> io::Result<()> {
85        // This is compatible with BinFile
86        writer = ARTICLES_MAGIC_NUMBER.strict_encode(writer)?;
87        // Version
88        writer = ARTICLES_VERSION.strict_encode(writer)?;
89        self.strict_encode(writer)?;
90        Ok(())
91    }
92}
93
94#[derive(Clone, Eq, PartialEq, Debug, Display, Error)]
95#[display(doc_comments)]
96pub enum MergeError {
97    /// contract id for the merged contract articles doesn't match
98    ContractMismatch,
99
100    /// codex id for the merged schema doesn't match
101    CodexMismatch,
102}
103
104#[cfg(feature = "std")]
105mod _fs {
106    use std::fs::File;
107    use std::io::{self, Read};
108    use std::path::Path;
109
110    use amplify::confinement::U24 as U24MAX;
111    use strict_encoding::{DeserializeError, StreamReader, StreamWriter, StrictReader, StrictWriter};
112
113    use super::Articles;
114
115    // TODO: Use BinFile
116    impl Articles {
117        pub fn load(path: impl AsRef<Path>) -> Result<Self, DeserializeError> {
118            let file = File::open(path)?;
119            let mut reader = StrictReader::with(StreamReader::new::<U24MAX>(file));
120            let me = Self::decode(&mut reader)?;
121            match reader.unbox().unconfine().read_exact(&mut [0u8; 1]) {
122                Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => Ok(me),
123                Err(e) => Err(e.into()),
124                Ok(()) => Err(DeserializeError::DataNotEntirelyConsumed),
125            }
126        }
127
128        pub fn save(&self, path: impl AsRef<Path>) -> io::Result<()> {
129            let file = File::create(path)?;
130            let writer = StrictWriter::with(StreamWriter::new::<U24MAX>(file));
131            self.encode(writer)
132        }
133    }
134}