rgb/
contracts.rs

1// Standard Library for RGB smart contracts
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Designed in 2019-2025 by Dr Maxim Orlovsky <orlovsky@lnp-bp.org>
6// Written in 2024-2025 by Dr Maxim Orlovsky <orlovsky@lnp-bp.org>
7//
8// Copyright (C) 2019-2024 LNP/BP Standards Association, Switzerland.
9// Copyright (C) 2024-2025 LNP/BP Laboratories,
10//                         Institute for Distributed and Cognitive Systems (InDCS), Switzerland.
11// Copyright (C) 2025 RGB Consortium, Switzerland.
12// Copyright (C) 2019-2025 Dr Maxim Orlovsky.
13// All rights under the above copyrights are reserved.
14//
15// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
16// in compliance with the License. You may obtain a copy of the License at
17//
18//        http://www.apache.org/licenses/LICENSE-2.0
19//
20// Unless required by applicable law or agreed to in writing, software distributed under the License
21// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
22// or implied. See the License for the specific language governing permissions and limitations under
23// the License.
24
25use alloc::collections::BTreeMap;
26use core::borrow::Borrow;
27use core::cell::RefCell;
28use std::collections::HashMap;
29use std::io;
30
31use amplify::confinement::{KeyedCollection, SmallOrdMap};
32use amplify::MultiError;
33use commit_verify::StrictHash;
34use hypersonic::{
35    AcceptError, AuthToken, CallParams, CodexId, ContractId, ContractName, Opid, Stock,
36};
37use indexmap::{IndexMap, IndexSet};
38use rgb::RgbSeal;
39use strict_encoding::{
40    ReadRaw, StrictDecode, StrictDumb, StrictEncode, StrictReader, StrictWriter, WriteRaw,
41};
42use strict_types::StrictVal;
43
44use crate::{
45    parse_consignment, Articles, Consensus, Consignment, ConsumeError, Contract, ContractRef,
46    ContractState, CreateParams, Identity, ImmutableState, Issuer, Operation, OwnedState, Pile,
47    SigBlob, StateName, Stockpile, WitnessStatus,
48};
49
50pub const CONSIGN_VERSION: u16 = 0;
51#[cfg(feature = "binfile")]
52pub use _fs::CONSIGN_MAGIC_NUMBER;
53
54#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Display)]
55#[display("{contract_id}/{state_name}")]
56#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))]
57pub struct ContractStateName {
58    pub contract_id: ContractId,
59    pub state_name: StateName,
60}
61
62impl ContractStateName {
63    pub fn new(contract_id: ContractId, state_name: StateName) -> Self {
64        ContractStateName { contract_id, state_name }
65    }
66}
67
68#[derive(Clone, Eq, PartialEq, Debug)]
69#[cfg_attr(
70    feature = "serde",
71    derive(Serialize, Deserialize),
72    serde(
73        rename_all = "camelCase",
74        bound = "Seal: serde::Serialize + for<'d> serde::Deserialize<'d>"
75    )
76)]
77pub struct WalletState<Seal> {
78    pub immutable: BTreeMap<ContractStateName, Vec<ImmutableState>>,
79    pub owned: BTreeMap<ContractStateName, Vec<OwnedState<Seal>>>,
80    pub aggregated: BTreeMap<ContractStateName, StrictVal>,
81}
82
83impl<Seal> Default for WalletState<Seal> {
84    fn default() -> Self { Self { immutable: bmap! {}, owned: bmap! {}, aggregated: bmap! {} } }
85}
86
87impl<Seal> WalletState<Seal> {
88    pub fn from_contracts_state(
89        contracts: impl IntoIterator<Item = (ContractId, ContractState<Seal>)>,
90    ) -> Self {
91        let mut wallet_state = WalletState::default();
92        for (contract_id, contract_state) in contracts {
93            for (state_name, state) in contract_state.immutable {
94                wallet_state
95                    .immutable
96                    .insert(ContractStateName::new(contract_id, state_name), state);
97            }
98            for (state_name, state) in contract_state.owned {
99                wallet_state
100                    .owned
101                    .insert(ContractStateName::new(contract_id, state_name), state);
102            }
103            for (state_name, state) in contract_state.aggregated {
104                wallet_state
105                    .aggregated
106                    .insert(ContractStateName::new(contract_id, state_name), state);
107            }
108        }
109        wallet_state
110    }
111}
112
113/// Collection of RGB smart contracts and contract issuers, which can be cached in memory.
114///
115/// # Generics
116///
117/// - `S` provides a specific cache implementation for an in-mem copy of issuers,
118/// - `C` provides a specific cache implementation for an in-mem copy of contracts.
119#[derive(Clone, Debug)]
120pub struct Contracts<
121    Sp,
122    // TODO: Use IndexMap instead to be no_std-compatible
123    S = HashMap<CodexId, Issuer>,
124    C = HashMap<ContractId, Contract<<Sp as Stockpile>::Stock, <Sp as Stockpile>::Pile>>,
125> where
126    Sp: Stockpile,
127    S: KeyedCollection<Key = CodexId, Value = Issuer>,
128    C: KeyedCollection<Key = ContractId, Value = Contract<Sp::Stock, Sp::Pile>>,
129{
130    issuers: RefCell<S>,
131    contracts: RefCell<C>,
132    persistence: Sp,
133}
134
135impl<Sp, S, C> Contracts<Sp, S, C>
136where
137    Sp: Stockpile,
138    S: KeyedCollection<Key = CodexId, Value = Issuer>,
139    C: KeyedCollection<Key = ContractId, Value = Contract<Sp::Stock, Sp::Pile>>,
140{
141    pub fn load(persistence: Sp) -> Self
142    where
143        S: Default,
144        C: Default,
145    {
146        Self { issuers: none!(), contracts: none!(), persistence }
147    }
148
149    fn with_contract<R>(
150        &self,
151        id: ContractId,
152        f: impl FnOnce(&Contract<Sp::Stock, Sp::Pile>) -> R,
153        or: Option<R>,
154    ) -> R {
155        // We need this bullshit due to a failed rust `RefCell` implementation which panics if we do
156        // this block any other way.
157        if self.contracts.borrow().contains_key(&id) {
158            return f(self.contracts.borrow().get(&id).unwrap());
159        }
160        if let Some(contract) = self.persistence.contract(id) {
161            let res = f(&contract);
162            self.contracts.borrow_mut().insert(id, contract);
163            res
164        } else if let Some(or) = or {
165            or
166        } else {
167            panic!("Contract {id} not found")
168        }
169    }
170
171    fn with_contract_mut<R>(
172        &mut self,
173        id: ContractId,
174        f: impl FnOnce(&mut Contract<Sp::Stock, Sp::Pile>) -> R,
175    ) -> R {
176        // We need this bullshit due to a failed rust `RefCell` implementation which panics if we do
177        // this block any other way.
178        if self.contracts.borrow().contains_key(&id) {
179            return f(self.contracts.borrow_mut().get_mut(&id).unwrap());
180        }
181        if let Some(mut contract) = self.persistence.contract(id) {
182            let res = f(&mut contract);
183            self.contracts.borrow_mut().insert(id, contract);
184            res
185        } else {
186            panic!("Contract {id} not found")
187        }
188    }
189
190    #[cfg(feature = "async")]
191    #[allow(clippy::await_holding_refcell_ref)]
192    async fn with_contract_mut_async<R>(
193        &mut self,
194        id: ContractId,
195        f: impl AsyncFnOnce(&mut Contract<Sp::Stock, Sp::Pile>) -> R,
196    ) -> R {
197        // We need this bullshit due to a failed rust `RefCell` implementation which panics if we do
198        // this block any other way.
199        if self.contracts.borrow().contains_key(&id) {
200            return f(self.contracts.borrow_mut().get_mut(&id).unwrap()).await;
201        }
202        if let Some(mut contract) = self.persistence.contract(id) {
203            let res = f(&mut contract).await;
204            self.contracts.borrow_mut().insert(id, contract);
205            res
206        } else {
207            panic!("Contract {id} not found")
208        }
209    }
210}
211
212impl<Sp, S, C> Contracts<Sp, S, C>
213where
214    Sp: Stockpile,
215    S: KeyedCollection<Key = CodexId, Value = Issuer>,
216    C: KeyedCollection<Key = ContractId, Value = Contract<Sp::Stock, Sp::Pile>>,
217{
218    pub fn codex_ids(&self) -> impl Iterator<Item = CodexId> + use<'_, Sp, S, C> {
219        self.persistence.codex_ids()
220    }
221
222    pub fn issuers_count(&self) -> usize { self.persistence.issuers_count() }
223
224    pub fn has_issuer(&self, codex_id: CodexId) -> bool { self.persistence.has_issuer(codex_id) }
225
226    pub fn issuers(&self) -> impl Iterator<Item = (CodexId, Issuer)> + use<'_, Sp, S, C> {
227        self.persistence
228            .codex_ids()
229            .filter_map(|codex_id| self.issuer(codex_id).map(|schema| (codex_id, schema)))
230    }
231
232    pub fn issuer(&self, codex_id: CodexId) -> Option<Issuer> {
233        if let Some(issuer) = self.issuers.borrow().get(&codex_id) {
234            return Some(issuer.clone());
235        };
236        let issuer = self.persistence.issuer(codex_id)?;
237        self.issuers.borrow_mut().insert(codex_id, issuer);
238        self.issuers.borrow().get(&codex_id).cloned()
239    }
240
241    pub fn contracts_count(&self) -> usize { self.persistence.contracts_count() }
242
243    pub fn has_contract(&self, contract_id: ContractId) -> bool {
244        self.persistence.has_contract(contract_id)
245    }
246
247    pub fn contract_ids(&self) -> impl Iterator<Item = ContractId> + use<'_, Sp, S, C> {
248        self.persistence.contract_ids()
249    }
250
251    /// Get the contract state.
252    ///
253    /// The call does not recompute the contract state, but does a seal resolution,
254    /// taking into account the status of the witnesses in the whole history.
255    ///
256    /// # Panics
257    ///
258    /// If the contract id is not known.
259    pub fn contract_state(
260        &self,
261        contract_id: ContractId,
262    ) -> ContractState<<Sp::Pile as Pile>::Seal> {
263        self.with_contract(contract_id, |contract| contract.state(), None)
264    }
265
266    pub fn contract_articles(&self, contract_id: ContractId) -> Articles {
267        self.with_contract(contract_id, |contract| contract.articles().clone(), None)
268    }
269
270    pub fn find_contract_id(&self, r: impl Into<ContractRef>) -> Option<ContractId> {
271        match r.into() {
272            ContractRef::Id(id) if self.has_contract(id) => Some(id),
273            ContractRef::Id(_) => None,
274            ContractRef::Name(name) => {
275                let name = ContractName::Named(name);
276                if let Some(id) = self
277                    .contracts
278                    .borrow()
279                    .iter()
280                    .find(|(_, contract)| contract.articles().issue().meta.name == name)
281                    .map(|(id, _)| *id)
282                {
283                    return Some(id);
284                }
285                self.persistence
286                    .contract_ids()
287                    .filter_map(|id| self.persistence.contract(id))
288                    .find(|contract| contract.articles().issue().meta.name == name)
289                    .map(|contract| contract.contract_id())
290            }
291        }
292    }
293
294    pub fn import_issuer(&mut self, issuer: Issuer) -> Result<CodexId, Sp::Error> {
295        let codex_id = issuer.codex_id();
296        let schema = self.persistence.import_issuer(issuer)?;
297        self.issuers.borrow_mut().insert(codex_id, schema);
298        Ok(codex_id)
299    }
300
301    fn check_layer1(
302        &self,
303        consensus: Consensus,
304        testnet: bool,
305    ) -> Result<(), MultiError<IssuerError, <Sp::Stock as Stock>::Error, <Sp::Pile as Pile>::Error>>
306    {
307        if consensus != self.persistence.consensus() {
308            return Err(MultiError::A(IssuerError::ConsensusMismatch));
309        }
310        if testnet != self.persistence.is_testnet() {
311            Err(if testnet {
312                MultiError::A(IssuerError::TestnetMismatch)
313            } else {
314                MultiError::A(IssuerError::MainnetMismatch)
315            })
316        } else {
317            Ok(())
318        }
319    }
320
321    pub fn issue(
322        &mut self,
323        params: CreateParams<<<Sp::Pile as Pile>::Seal as RgbSeal>::Definition>,
324    ) -> Result<
325        ContractId,
326        MultiError<IssuerError, <Sp::Stock as Stock>::Error, <Sp::Pile as Pile>::Error>,
327    > {
328        self.check_layer1(params.consensus, params.testnet)?;
329        let contract = self.persistence.issue(params)?;
330        let id = contract.contract_id();
331        self.contracts.borrow_mut().insert(id, contract);
332        Ok(id)
333    }
334
335    /// Do a call to the contract method, creating and operation.
336    ///
337    /// The operation is automatically included in the contract history.
338    ///
339    /// The state of the contract is not automatically updated, but on the next update it will
340    /// reflect the call results.
341    ///
342    /// # Panics
343    ///
344    /// If the contract id is not known.
345    pub fn contract_call(
346        &mut self,
347        contract_id: ContractId,
348        call: CallParams,
349        seals: SmallOrdMap<u16, <<Sp::Pile as Pile>::Seal as RgbSeal>::Definition>,
350    ) -> Result<Operation, MultiError<AcceptError, <Sp::Stock as Stock>::Error>> {
351        self.with_contract_mut(contract_id, |contract| contract.call(call, seals))
352    }
353
354    #[cfg(not(feature = "async"))]
355    /// Update the status of all witnesses and single-use seal definitions.
356    ///
357    /// Applies rollbacks or forwards if required and recomputes the state of the affected
358    /// contracts.
359    pub fn update_witnesses<E: core::error::Error>(
360        &mut self,
361        resolver: impl Fn(<<Sp::Pile as Pile>::Seal as RgbSeal>::WitnessId) -> Result<WitnessStatus, E>,
362        last_block_height: u64,
363        min_conformations: u32,
364    ) -> Result<(), MultiError<SyncError<E>, <Sp::Stock as Stock>::Error>> {
365        let mut changed_statuses = IndexMap::<_, WitnessStatus>::new();
366        let contract_ids = self.persistence.contract_ids().collect::<IndexSet<_>>();
367        for contract_id in contract_ids {
368            self.with_contract_mut(
369                contract_id,
370                |contract| -> Result<(), MultiError<SyncError<E>, <Sp::Stock as Stock>::Error>> {
371                    for witness_id in contract.witness_ids() {
372                        let old_status = contract.witness_status(witness_id);
373                        if matches!(old_status, WitnessStatus::Mined(height) if last_block_height - height.get() > min_conformations as u64) {
374                            continue
375                        }
376                        let new_status = match changed_statuses.get(&witness_id) {
377                            None => resolver(witness_id)
378                                .map_err(SyncError::Status)
379                                .map_err(MultiError::A),
380                            Some(witness_id) => Ok(*witness_id),
381                        }?;
382                        if new_status != old_status {
383                            changed_statuses.insert(witness_id, new_status);
384                        }
385                    }
386                    contract
387                        .sync(changed_statuses.iter().map(|(id, status)| (*id, *status)))
388                        .map_err(MultiError::from_other_a)?;
389                    Ok(())
390                },
391            )?;
392        }
393        Ok(())
394    }
395
396    #[cfg(feature = "async")]
397    /// Update the status of all witnesses and single-use seal definitions.
398    ///
399    /// Applies rollbacks or forwards if required and recomputes the state of the affected
400    /// contracts.
401    pub async fn update_witnesses_async<E: core::error::Error>(
402        &mut self,
403        resolver: impl AsyncFn(
404            <<Sp::Pile as Pile>::Seal as RgbSeal>::WitnessId,
405        ) -> Result<WitnessStatus, E>,
406        last_block_height: u64,
407        min_conformations: u32,
408    ) -> Result<(), MultiError<SyncError<E>, <Sp::Stock as Stock>::Error>> {
409        let mut changed_statuses = IndexMap::<_, WitnessStatus>::new();
410        let contract_ids = self.persistence.contract_ids().collect::<IndexSet<_>>();
411        for contract_id in contract_ids {
412            self.with_contract_mut_async(
413                contract_id,
414                async |contract| -> Result<(), MultiError<SyncError<E>, <Sp::Stock as Stock>::Error>> {
415                    for witness_id in contract.witness_ids() {
416                        let old_status = contract.witness_status(witness_id);
417                        if matches!(old_status, WitnessStatus::Mined(height) if last_block_height - height.get() > min_conformations as u64) {
418                            continue
419                        }
420                        let new_status = match changed_statuses.get(&witness_id) {
421                            None => resolver(witness_id)
422                                .await
423                                .map_err(SyncError::Status)
424                                .map_err(MultiError::A),
425                            Some(witness_id) => Ok(*witness_id),
426                        }?;
427                        if new_status != old_status {
428                            changed_statuses.insert(witness_id, new_status);
429                        }
430                    }
431                    contract
432                        .sync(changed_statuses.iter().map(|(id, status)| (*id, *status)))
433                        .map_err(MultiError::from_other_a)?;
434                    Ok(())
435                },
436            ).await?;
437        }
438        Ok(())
439    }
440
441    /// Include an operation and its witness to the history of known operations and the contract
442    /// state.
443    ///
444    /// # Panics
445    ///
446    /// If the contract id is not known.
447    pub fn include(
448        &mut self,
449        contract_id: ContractId,
450        opid: Opid,
451        pub_witness: &<<Sp::Pile as Pile>::Seal as RgbSeal>::Published,
452        anchor: <<Sp::Pile as Pile>::Seal as RgbSeal>::Client,
453    ) {
454        self.with_contract_mut(contract_id, |contract| contract.include(opid, anchor, pub_witness))
455    }
456
457    /// Export a contract to a strictly encoded stream.
458    ///
459    /// # Panics
460    ///
461    /// If the contract id is not known.
462    ///
463    /// # Errors
464    ///
465    /// If the output stream failures, like when the stream cannot accept more data or got
466    /// disconnected.
467    pub fn export(
468        &self,
469        contract_id: ContractId,
470        writer: StrictWriter<impl WriteRaw>,
471    ) -> io::Result<()>
472    where
473        <<Sp::Pile as Pile>::Seal as RgbSeal>::Client: StrictDumb + StrictEncode,
474        <<Sp::Pile as Pile>::Seal as RgbSeal>::Published: StrictDumb + StrictEncode,
475        <<Sp::Pile as Pile>::Seal as RgbSeal>::WitnessId: StrictEncode,
476    {
477        self.with_contract(contract_id, |contract| contract.export(writer), None)
478    }
479
480    /// Purge a contract from the system.
481    pub fn purge(&mut self, contract_id: ContractId) -> Result<(), Sp::Error> {
482        self.contracts.borrow_mut().remove(&contract_id);
483        self.persistence.purge(contract_id)?;
484        Ok(())
485    }
486
487    /// Create a consignment with a history from the genesis to each of the `terminals`, and
488    /// serialize it to a strictly encoded stream `writer`.
489    ///
490    /// # Panics
491    ///
492    /// If the contract id is not known.
493    ///
494    /// # Errors
495    ///
496    /// If the output stream failures, like when the stream cannot accept more data or got
497    /// disconnected.
498    pub fn consign(
499        &mut self,
500        contract_id: ContractId,
501        terminals: impl IntoIterator<Item = impl Borrow<AuthToken>>,
502        writer: StrictWriter<impl WriteRaw>,
503    ) -> io::Result<()>
504    where
505        <<Sp::Pile as Pile>::Seal as RgbSeal>::Client: StrictDumb + StrictEncode,
506        <<Sp::Pile as Pile>::Seal as RgbSeal>::Published: StrictDumb + StrictEncode,
507        <<Sp::Pile as Pile>::Seal as RgbSeal>::WitnessId: StrictEncode,
508    {
509        self.with_contract_mut(contract_id, |contract| contract.consign(terminals, writer))
510    }
511
512    /// Consume a consignment stream.
513    ///
514    /// The method:
515    /// - validates the consignment;
516    /// - resolves auth tokens into seal definitions known to the current wallet (i.e., coming from
517    ///   the invoices produced by the wallet);
518    /// - checks the signature of the issuer over the contract articles;
519    ///
520    /// # Arguments
521    ///
522    /// - `allow_unknown`: allows importing a contract which was not known to the system;
523    /// - `reader`: the input stream;
524    /// - `seal_resolver`: lambda which knows about the seal definitions from the wallet-generated
525    ///   invoices;
526    /// - `sig_validator`: a validator for the signature of the issuer over the contract articles.
527    pub fn consume<E>(
528        &mut self,
529        allow_unknown: bool,
530        reader: &mut StrictReader<impl ReadRaw>,
531        seal_resolver: impl FnMut(
532            &Operation,
533        )
534            -> BTreeMap<u16, <<Sp::Pile as Pile>::Seal as RgbSeal>::Definition>,
535        sig_validator: impl FnOnce(StrictHash, &Identity, &SigBlob) -> Result<(), E>,
536    ) -> Result<
537        (),
538        MultiError<
539            ConsumeError<<<Sp::Pile as Pile>::Seal as RgbSeal>::Definition>,
540            <Sp::Stock as Stock>::Error,
541            <Sp::Pile as Pile>::Error,
542        >,
543    >
544    where
545        <Sp::Pile as Pile>::Conf: From<<Sp::Stock as Stock>::Conf>,
546        <<Sp::Pile as Pile>::Seal as RgbSeal>::Client: StrictDecode,
547        <<Sp::Pile as Pile>::Seal as RgbSeal>::Published: StrictDecode,
548        <<Sp::Pile as Pile>::Seal as RgbSeal>::WitnessId: StrictDecode,
549    {
550        // Checking version and getting contract id
551        let contract_id = parse_consignment(reader).map_err(MultiError::from_a)?;
552        if !self.has_contract(contract_id) {
553            if allow_unknown {
554                let consignment = Consignment::strict_decode(reader).map_err(MultiError::from_a)?;
555                // Here we do not check for the end of the stream,
556                // so in the future we can have arbitrary extensions
557                // put here with no backward compatibility issues.
558
559                let articles = consignment
560                    .articles(sig_validator)
561                    .map_err(MultiError::from_a)?;
562                self.check_layer1(
563                    articles.contract_meta().consensus,
564                    articles.contract_meta().testnet,
565                )
566                .map_err(MultiError::from_other_a)?;
567
568                let contract = self.persistence.import_contract(articles, consignment)?;
569                self.contracts.borrow_mut().insert(contract_id, contract);
570                Ok(())
571            } else {
572                Err(MultiError::A(ConsumeError::UnknownContract(contract_id)))
573            }
574        } else {
575            self.with_contract_mut(contract_id, |contract| {
576                contract.consume_internal(reader, seal_resolver, sig_validator)
577            })
578            .map_err(|err| match err {
579                MultiError::A(a) => MultiError::A(a),
580                MultiError::B(b) => MultiError::B(b),
581                MultiError::C(_) => unreachable!(),
582            })
583        }
584    }
585}
586
587#[derive(Clone, PartialEq, Eq, Debug, Display, Error, From)]
588#[display(doc_comments)]
589pub enum IssuerError {
590    /// the issuer version does not match the required one.
591    IssuerMismatch,
592    /// proof of publication layer mismatch.
593    ConsensusMismatch,
594    /// unable to consume a testnet contract for mainnet.
595    TestnetMismatch,
596    /// unable to consume a mainnet contract for testnet.
597    MainnetMismatch,
598    /// unknown codex for contract issue {0}.
599    UnknownCodex(CodexId),
600
601    #[from]
602    #[display(inner)]
603    Inner(hypersonic::IssueError),
604}
605
606#[cfg(feature = "binfile")]
607mod _fs {
608    use std::path::Path;
609
610    use binfile::BinFile;
611    use strict_encoding::StreamReader;
612
613    use super::*;
614
615    pub const CONSIGN_MAGIC_NUMBER: u64 = u64::from_be_bytes(*b"RGBCNSGN");
616
617    impl<Sp, S, C> Contracts<Sp, S, C>
618    where
619        Sp: Stockpile,
620        S: KeyedCollection<Key = CodexId, Value = Issuer>,
621        C: KeyedCollection<Key = ContractId, Value = Contract<Sp::Stock, Sp::Pile>>,
622    {
623        /// Export a contract to a file at `path`.
624        ///
625        /// # Panics
626        ///
627        /// If the contract id is not known.
628        ///
629        /// # Errors
630        ///
631        /// If writing to the file failures, like when the file already exists, there is no write
632        /// access to it, or no sufficient disk space.
633        pub fn export_to_file(
634            &self,
635            path: impl AsRef<Path>,
636            contract_id: ContractId,
637        ) -> io::Result<()>
638        where
639            <<Sp::Pile as Pile>::Seal as RgbSeal>::Client: StrictDumb + StrictEncode,
640            <<Sp::Pile as Pile>::Seal as RgbSeal>::Published: StrictDumb + StrictEncode,
641            <<Sp::Pile as Pile>::Seal as RgbSeal>::WitnessId: StrictEncode,
642        {
643            self.with_contract(contract_id, |contract| contract.export_to_file(path), None)
644        }
645
646        /// Create a consignment with a history from the genesis to each of the `terminals`, and
647        /// serialize it to a `file`.
648        ///
649        /// # Panics
650        ///
651        /// If the contract id is not known.
652        ///
653        /// # Errors
654        ///
655        /// If writing to the file failures, like when the file already exists, there is no write
656        /// access to it, or no sufficient disk space.
657        pub fn consign_to_file(
658            &self,
659            path: impl AsRef<Path>,
660            contract_id: ContractId,
661            terminals: impl IntoIterator<Item = impl Borrow<AuthToken>>,
662        ) -> io::Result<()>
663        where
664            <<Sp::Pile as Pile>::Seal as RgbSeal>::Client: StrictDumb + StrictEncode,
665            <<Sp::Pile as Pile>::Seal as RgbSeal>::Published: StrictDumb + StrictEncode,
666            <<Sp::Pile as Pile>::Seal as RgbSeal>::WitnessId: StrictEncode,
667        {
668            self.with_contract(
669                contract_id,
670                |contract| contract.consign_to_file(path, terminals),
671                None,
672            )
673        }
674
675        /// Consume a consignment from a `file`.
676        ///
677        /// The method:
678        /// - validates the consignment;
679        /// - resolves auth tokens into seal definitions known to the current wallet (i.e., coming
680        ///   from the invoices produced by the wallet);
681        /// - checks the signature of the issuer over the contract articles;
682        ///
683        /// # Arguments
684        ///
685        /// - `allow_unknown`: allows importing a contract which was not known to the system;
686        /// - `reader`: the input stream;
687        /// - `seal_resolver`: lambda which knows about the seal definitions from the
688        ///   wallet-generated invoices;
689        /// - `sig_validator`: a validator for the signature of the issuer over the contract
690        ///   articles.
691        pub fn consume_from_file<E>(
692            &mut self,
693            allow_unknown: bool,
694            path: impl AsRef<Path>,
695            seal_resolver: impl FnMut(
696                &Operation,
697            ) -> BTreeMap<
698                u16,
699                <<Sp::Pile as Pile>::Seal as RgbSeal>::Definition,
700            >,
701            sig_validator: impl FnOnce(StrictHash, &Identity, &SigBlob) -> Result<(), E>,
702        ) -> Result<
703            (),
704            MultiError<
705                ConsumeError<<<Sp::Pile as Pile>::Seal as RgbSeal>::Definition>,
706                <Sp::Stock as Stock>::Error,
707                <Sp::Pile as Pile>::Error,
708            >,
709        >
710        where
711            <Sp::Pile as Pile>::Conf: From<<Sp::Stock as Stock>::Conf>,
712            <<Sp::Pile as Pile>::Seal as RgbSeal>::Client: StrictDecode,
713            <<Sp::Pile as Pile>::Seal as RgbSeal>::Published: StrictDecode,
714            <<Sp::Pile as Pile>::Seal as RgbSeal>::WitnessId: StrictDecode,
715        {
716            let file = BinFile::<CONSIGN_MAGIC_NUMBER, CONSIGN_VERSION>::open(path)
717                .map_err(MultiError::from_a)?;
718            let mut reader = StrictReader::with(StreamReader::new::<{ usize::MAX }>(file));
719            self.consume(allow_unknown, &mut reader, seal_resolver, sig_validator)
720        }
721    }
722}
723
724#[derive(Debug, Display, Error, From)]
725#[display(doc_comments)]
726pub enum SyncError<E: core::error::Error> {
727    /// unable to synchronize wallet. Details: {0}
728    Wallet(E),
729
730    /// unable to retrieve the status of a witness id. Details: {0}
731    Status(E),
732
733    #[from]
734    #[display(inner)]
735    Forward(AcceptError),
736}