rgbstd/containers/
bindle.rs

1// RGB standard library for working with smart contracts on Bitcoin & Lightning
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Written in 2019-2023 by
6//     Dr Maxim Orlovsky <orlovsky@lnp-bp.org>
7//
8// Copyright (C) 2019-2023 LNP/BP Standards Association. All rights reserved.
9//
10// Licensed under the Apache License, Version 2.0 (the "License");
11// you may not use this file except in compliance with the License.
12// You may obtain a copy of the License at
13//
14//     http://www.apache.org/licenses/LICENSE-2.0
15//
16// Unless required by applicable law or agreed to in writing, software
17// distributed under the License is distributed on an "AS IS" BASIS,
18// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19// See the License for the specific language governing permissions and
20// limitations under the License.
21
22//! Bindle is a wrapper for different RGB containers, which can be serialized
23//! and optionally signed by the creator with certain id and send over to a
24//! remote party.
25
26use std::collections::BTreeMap;
27use std::fmt::{Debug, Display};
28use std::ops::Deref;
29use std::str::FromStr;
30
31#[cfg(feature = "fs")]
32pub use _fs::*;
33use amplify::confinement;
34use amplify::confinement::{Confined, TinyVec, U24};
35use baid58::Baid58ParseError;
36use rgb::{BundleId, ContractId, Schema, SchemaId, SchemaRoot};
37use strict_encoding::{
38    StrictDecode, StrictDeserialize, StrictDumb, StrictEncode, StrictSerialize, StrictType,
39};
40
41use crate::containers::transfer::TransferId;
42use crate::containers::{Cert, Contract, Transfer};
43use crate::interface::{Iface, IfaceId, IfaceImpl, ImplId};
44use crate::LIB_NAME_RGB_STD;
45
46// TODO: Move to UBIDECO crate
47pub trait BindleContent: StrictSerialize + StrictDeserialize + StrictDumb {
48    /// Magic bytes used in saving/restoring container from a file.
49    const MAGIC: [u8; 4];
50    /// String used in ASCII armored blocks
51    const PLATE_TITLE: &'static str;
52
53    type Id: Copy
54        + Eq
55        + Debug
56        + Display
57        + FromStr<Err = Baid58ParseError>
58        + StrictType
59        + StrictDumb
60        + StrictEncode
61        + StrictDecode;
62
63    fn bindle_id(&self) -> Self::Id;
64    fn bindle_headers(&self) -> BTreeMap<&'static str, String> { none!() }
65    fn bindle(self) -> Bindle<Self> { Bindle::new(self) }
66    fn bindle_mnemonic(&self) -> Option<String> { None }
67}
68
69impl<Root: SchemaRoot> BindleContent for Schema<Root> {
70    const MAGIC: [u8; 4] = *b"SCHM";
71    const PLATE_TITLE: &'static str = "RGB SCHEMA";
72    type Id = SchemaId;
73    fn bindle_id(&self) -> Self::Id { self.schema_id() }
74    fn bindle_mnemonic(&self) -> Option<String> { Some(self.schema_id().to_mnemonic()) }
75}
76
77impl BindleContent for Contract {
78    const MAGIC: [u8; 4] = *b"CNRC";
79    const PLATE_TITLE: &'static str = "RGB CONTRACT";
80    type Id = ContractId;
81    fn bindle_id(&self) -> Self::Id { self.contract_id() }
82    fn bindle_headers(&self) -> BTreeMap<&'static str, String> {
83        bmap! {
84            "Version" => self.version.to_string(),
85            "Terminals" => self.terminals
86                .keys()
87                .map(BundleId::to_string)
88                .collect::<Vec<_>>()
89                .join(",\n  "),
90        }
91    }
92}
93
94impl BindleContent for Transfer {
95    const MAGIC: [u8; 4] = *b"TRNS";
96    const PLATE_TITLE: &'static str = "RGB STATE TRANSFER";
97    type Id = TransferId;
98    fn bindle_id(&self) -> Self::Id { self.transfer_id() }
99    fn bindle_mnemonic(&self) -> Option<String> { Some(self.transfer_id().to_mnemonic()) }
100    fn bindle_headers(&self) -> BTreeMap<&'static str, String> {
101        bmap! {
102            "Version" => self.version.to_string(),
103            "ContractId" => self.contract_id().to_string(),
104            "Terminals" => self.terminals
105                .keys()
106                .map(BundleId::to_string)
107                .collect::<Vec<_>>()
108                .join(",\n  "),
109        }
110    }
111}
112
113impl BindleContent for Iface {
114    const MAGIC: [u8; 4] = *b"IFCE";
115    const PLATE_TITLE: &'static str = "RGB INTERFACE";
116    type Id = IfaceId;
117    fn bindle_id(&self) -> Self::Id { self.iface_id() }
118    fn bindle_mnemonic(&self) -> Option<String> { Some(self.iface_id().to_mnemonic()) }
119    fn bindle_headers(&self) -> BTreeMap<&'static str, String> {
120        bmap! {
121            "Name" => self.name.to_string()
122        }
123    }
124}
125
126impl BindleContent for IfaceImpl {
127    const MAGIC: [u8; 4] = *b"IMPL";
128    const PLATE_TITLE: &'static str = "RGB INTERFACE IMPLEMENTATION";
129    type Id = ImplId;
130    fn bindle_id(&self) -> Self::Id { self.impl_id() }
131    fn bindle_mnemonic(&self) -> Option<String> { Some(self.impl_id().to_mnemonic()) }
132    fn bindle_headers(&self) -> BTreeMap<&'static str, String> {
133        bmap! {
134            "IfaceId" => format!("{:-#}", self.iface_id),
135            "SchemaId" => format!("{:-#}", self.schema_id),
136        }
137    }
138}
139
140#[derive(Clone, PartialEq, Eq, Hash, Debug)]
141#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
142#[strict_type(lib = LIB_NAME_RGB_STD)]
143#[cfg_attr(
144    feature = "serde",
145    derive(Serialize, Deserialize),
146    serde(crate = "serde_crate", rename_all = "camelCase")
147)]
148pub struct Bindle<C: BindleContent> {
149    id: C::Id,
150    data: C,
151    sigs: TinyVec<Cert>,
152}
153
154impl<C: BindleContent> Deref for Bindle<C> {
155    type Target = C;
156    fn deref(&self) -> &Self::Target { &self.data }
157}
158
159impl<C: BindleContent> From<C> for Bindle<C> {
160    fn from(data: C) -> Self { Bindle::new(data) }
161}
162
163impl<C: BindleContent> Bindle<C> {
164    pub fn new(data: C) -> Self {
165        Bindle {
166            id: data.bindle_id(),
167            data,
168            sigs: empty!(),
169        }
170    }
171
172    pub fn id(&self) -> C::Id { self.id }
173
174    pub fn into_split(self) -> (C, TinyVec<Cert>) { (self.data, self.sigs) }
175    pub fn unbindle(self) -> C { self.data }
176}
177
178#[derive(Clone, PartialEq, Eq, Debug, Display, Error, From)]
179#[display(doc_comments)]
180pub enum BindleParseError<Id: Copy + Eq + Debug + Display> {
181    /// the provided text doesn't represent a recognizable ASCII-armored RGB
182    /// bindle encoding.
183    WrongStructure,
184
185    /// Id header of the bindle contains unparsable information. Details: {0}
186    InvalidId(Baid58ParseError),
187
188    /// the actual data doesn't match the provided id.
189    ///
190    /// Actual id: {actual}.
191    ///
192    /// Expected id: {expected}.
193    MismatchedId { actual: Id, expected: Id },
194
195    /// bindle data has invalid Base85 encoding (ASCII armoring).
196    #[from(base85::Error)]
197    Base85,
198
199    /// unable to decode the provided bindle data. Details: {0}
200    #[from]
201    Deserialize(strict_encoding::DeserializeError),
202
203    /// bindle contains more than 16MB of data.
204    #[from(confinement::Error)]
205    TooLarge,
206}
207
208impl<C: BindleContent> FromStr for Bindle<C> {
209    type Err = BindleParseError<C::Id>;
210
211    fn from_str(s: &str) -> Result<Self, Self::Err> {
212        let mut lines = s.lines();
213        let first = format!("-----BEGIN {}-----", C::PLATE_TITLE);
214        let last = format!("-----END {}-----", C::PLATE_TITLE);
215        if (lines.next(), lines.next_back()) != (Some(&first), Some(&last)) {
216            return Err(BindleParseError::WrongStructure);
217        }
218        let mut header_id = None;
219        for line in lines.by_ref() {
220            if line.is_empty() {
221                break;
222            }
223            if let Some(id_str) = line.strip_prefix("Id: ") {
224                header_id = Some(C::Id::from_str(id_str).map_err(BindleParseError::InvalidId)?);
225            }
226        }
227        let armor = lines.filter(|l| !l.is_empty()).collect::<String>();
228        let data = base85::decode(&armor)?;
229        let data = C::from_strict_serialized::<U24>(Confined::try_from(data)?)?;
230        let id = data.bindle_id();
231        if let Some(header_id) = header_id {
232            if header_id != id {
233                return Err(BindleParseError::MismatchedId {
234                    actual: id,
235                    expected: header_id,
236                });
237            }
238        }
239        // TODO: check mnemonic
240        // TODO: parse and validate sigs
241        Ok(Self {
242            id,
243            data,
244            sigs: none!(),
245        })
246    }
247}
248
249impl<C: BindleContent> Display for Bindle<C> {
250    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
251        writeln!(f, "-----BEGIN {}-----", C::PLATE_TITLE)?;
252        writeln!(f, "Id: {:-#}", self.id)?;
253        if let Some(mnemonic) = self.bindle_mnemonic() {
254            writeln!(f, "Mnemonic: {}", mnemonic)?;
255        }
256        for (header, value) in self.bindle_headers() {
257            writeln!(f, "{header}: {value}")?;
258        }
259        for cert in &self.sigs {
260            writeln!(f, "Signed-By: {}", cert.signer)?;
261        }
262        writeln!(f)?;
263
264        // TODO: Replace with streamed writer
265        let data = self.data.to_strict_serialized::<U24>().expect("in-memory");
266        let data = base85::encode(&data);
267        let mut data = data.as_str();
268        while data.len() >= 64 {
269            let (line, rest) = data.split_at(64);
270            writeln!(f, "{}", line)?;
271            data = rest;
272        }
273        writeln!(f, "{}", data)?;
274
275        writeln!(f, "\n-----END {}-----", C::PLATE_TITLE)?;
276        Ok(())
277    }
278}
279
280#[cfg(feature = "fs")]
281mod _fs {
282    use std::io::{Read, Write};
283    use std::path::Path;
284    use std::{fs, io};
285
286    use rgb::SubSchema;
287    use strict_encoding::{DecodeError, StrictEncode, StrictReader, StrictWriter};
288
289    use super::*;
290
291    #[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)]
292    #[display(doc_comments)]
293    pub enum LoadError {
294        /// invalid file data.
295        InvalidMagic,
296
297        #[display(inner)]
298        #[from]
299        #[from(io::Error)]
300        Decode(DecodeError),
301    }
302
303    #[derive(Clone, Debug, From)]
304    #[cfg_attr(
305        feature = "serde",
306        derive(Serialize, Deserialize),
307        serde(crate = "serde_crate", rename_all = "camelCase", tag = "type")
308    )]
309    pub enum UniversalBindle {
310        #[from]
311        #[cfg_attr(feature = "serde", serde(rename = "interface"))]
312        Iface(Bindle<Iface>),
313
314        #[from]
315        Schema(Bindle<SubSchema>),
316
317        #[from]
318        #[cfg_attr(feature = "serde", serde(rename = "implementation"))]
319        Impl(Bindle<IfaceImpl>),
320
321        #[from]
322        Contract(Bindle<Contract>),
323
324        #[from]
325        Transfer(Bindle<Transfer>),
326    }
327
328    impl<C: BindleContent> Bindle<C> {
329        pub fn load(path: impl AsRef<Path>) -> Result<Self, LoadError> {
330            let mut rgb = [0u8; 3];
331            let mut magic = [0u8; 4];
332            let mut file = fs::File::open(path)?;
333            file.read_exact(&mut rgb)?;
334            file.read_exact(&mut magic)?;
335            if rgb != *b"RGB" || magic != C::MAGIC {
336                return Err(LoadError::InvalidMagic);
337            }
338            let mut reader = StrictReader::with(usize::MAX, file);
339            let me = Self::strict_decode(&mut reader)?;
340            Ok(me)
341        }
342
343        pub fn save(&self, path: impl AsRef<Path>) -> Result<(), io::Error> {
344            let mut file = fs::File::create(path)?;
345            file.write_all(b"RGB")?;
346            file.write_all(&C::MAGIC)?;
347            let writer = StrictWriter::with(usize::MAX, file);
348            self.strict_encode(writer)?;
349            Ok(())
350        }
351    }
352
353    impl UniversalBindle {
354        pub fn load(path: impl AsRef<Path>) -> Result<Self, LoadError> {
355            let mut rgb = [0u8; 3];
356            let mut magic = [0u8; 4];
357            let mut file = fs::File::open(path)?;
358            file.read_exact(&mut rgb)?;
359            file.read_exact(&mut magic)?;
360            if rgb != *b"RGB" {
361                return Err(LoadError::InvalidMagic);
362            }
363            let mut reader = StrictReader::with(usize::MAX, file);
364            Ok(match magic {
365                x if x == Iface::MAGIC => Bindle::<Iface>::strict_decode(&mut reader)?.into(),
366                x if x == SubSchema::MAGIC => {
367                    Bindle::<SubSchema>::strict_decode(&mut reader)?.into()
368                }
369                x if x == IfaceImpl::MAGIC => {
370                    Bindle::<IfaceImpl>::strict_decode(&mut reader)?.into()
371                }
372                x if x == Contract::MAGIC => Bindle::<Contract>::strict_decode(&mut reader)?.into(),
373                x if x == Transfer::MAGIC => Bindle::<Transfer>::strict_decode(&mut reader)?.into(),
374                _ => return Err(LoadError::InvalidMagic),
375            })
376        }
377    }
378}