rgb/
contract.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::error::Error;
28use core::marker::PhantomData;
29use std::io;
30
31use amplify::confinement::SmallOrdMap;
32use amplify::{IoError, MultiError};
33use chrono::{DateTime, Utc};
34use commit_verify::{ReservedBytes, StrictHash};
35use hypersonic::{
36    AcceptError, Articles, AuthToken, CallParams, CellAddr, Codex, Consensus, ContractId,
37    CoreParams, DataCell, EffectiveState, IssueError, IssueParams, Ledger, LibRepo, Memory,
38    MethodName, NamedState, Operation, Opid, SemanticError, Semantics, SigBlob, StateAtom,
39    StateName, Stock, Transition,
40};
41use indexmap::{IndexMap, IndexSet};
42use rgb::{
43    ContractApi, ContractVerify, OperationSeals, ReadOperation, RgbSeal, RgbSealDef,
44    VerificationError,
45};
46use single_use_seals::{ClientSideWitness, PublishedWitness, SealWitness};
47use strict_encoding::{
48    DecodeError, ReadRaw, StrictDecode, StrictDumb, StrictEncode, StrictReader, StrictWriter,
49    TypeName, TypedRead, WriteRaw,
50};
51use strict_types::StrictVal;
52
53use crate::{
54    parse_consignment, Consignment, ContractMeta, Identity, Issue, Issuer, IssuerError, IssuerSpec,
55    OpRels, Pile, VerifiedOperation, Witness, WitnessStatus,
56};
57
58#[derive(Copy, Clone, PartialEq, Eq, Debug, From)]
59#[cfg_attr(
60    feature = "serde",
61    derive(Serialize, Deserialize),
62    serde(untagged, bound = "Seal: serde::Serialize + for<'d> serde::Deserialize<'d>")
63)]
64pub enum EitherSeal<Seal> {
65    Alt(Seal),
66
67    #[from]
68    Token(AuthToken),
69}
70
71impl<Seal> EitherSeal<Seal> {
72    pub fn auth_token(&self) -> AuthToken
73    where Seal: RgbSealDef {
74        match self {
75            EitherSeal::Alt(seal) => seal.auth_token(),
76            EitherSeal::Token(auth) => *auth,
77        }
78    }
79
80    pub fn to_explicit(&self) -> Option<Seal>
81    where Seal: Clone {
82        match self {
83            EitherSeal::Alt(seal) => Some(seal.clone()),
84            EitherSeal::Token(_) => None,
85        }
86    }
87}
88
89#[derive(Clone, PartialEq, Eq, Debug)]
90#[cfg_attr(
91    feature = "serde",
92    derive(Serialize, Deserialize),
93    serde(bound = "Seal: serde::Serialize + for<'d> serde::Deserialize<'d>")
94)]
95pub struct Assignment<Seal> {
96    pub seal: Seal,
97    pub data: StrictVal,
98}
99
100impl<Seal> Assignment<Seal> {
101    pub fn new(seal: Seal, data: impl Into<StrictVal>) -> Self { Self { seal, data: data.into() } }
102}
103
104impl<Seal> Assignment<EitherSeal<Seal>> {
105    pub fn new_external(auth: AuthToken, data: impl Into<StrictVal>) -> Self {
106        Self { seal: EitherSeal::Token(auth), data: data.into() }
107    }
108    pub fn new_internal(seal: Seal, data: impl Into<StrictVal>) -> Self {
109        Self { seal: EitherSeal::Alt(seal), data: data.into() }
110    }
111}
112
113/// Element of the contract owned state, carrying information about its confirmation status.
114#[derive(Clone, PartialEq, Eq, Debug)]
115#[cfg_attr(
116    feature = "serde",
117    derive(Serialize, Deserialize),
118    serde(bound = "Seal: serde::Serialize + for<'d> serde::Deserialize<'d>")
119)]
120pub struct OwnedState<Seal> {
121    /// Operation output defining this element of owned state.
122    pub addr: CellAddr,
123
124    /// State assignment.
125    #[cfg_attr(feature = "serde", serde(flatten))]
126    pub assignment: Assignment<Seal>,
127
128    /// Status of the state: the maximal confirmation depth for the whole history of operations
129    /// leading to this status, since genesis.
130    ///
131    /// If any operation has known multiple witnesses (related to RBF'ed transactions or lightning
132    /// channels), the best confirmation is used for that specific operation (i.e., the deepest
133    /// mined).
134    ///
135    /// Formally, if $H$ is a set of all operations in the history between genesis and this state,
136    /// and $W_o$ is a set of all witnesses for operation $o \in H$, the status here is defined as
137    /// $max_{o \in H} min_{w \in W_o} status(w)$.
138    pub status: WitnessStatus,
139}
140
141#[derive(Clone, PartialEq, Eq, Debug)]
142#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
143pub struct ImmutableState {
144    pub addr: CellAddr,
145    #[cfg_attr(feature = "serde", serde(flatten))]
146    pub data: StateAtom,
147    pub status: WitnessStatus,
148}
149
150#[derive(Clone, Eq, PartialEq, Debug, Default)]
151#[cfg_attr(
152    feature = "serde",
153    derive(Serialize, Deserialize),
154    serde(
155        rename_all = "camelCase",
156        bound = "Seal: serde::Serialize + for<'d> serde::Deserialize<'d>"
157    )
158)]
159pub struct ContractState<Seal> {
160    pub immutable: BTreeMap<StateName, Vec<ImmutableState>>,
161    pub owned: BTreeMap<StateName, Vec<OwnedState<Seal>>>,
162    pub aggregated: BTreeMap<StateName, StrictVal>,
163}
164
165impl<Seal> ContractState<Seal> {
166    pub fn map<To>(self, f: impl Fn(Seal) -> To) -> ContractState<To> {
167        ContractState {
168            immutable: self.immutable,
169            owned: self
170                .owned
171                .into_iter()
172                .map(|(name, map)| {
173                    let map = map
174                        .into_iter()
175                        .map(|owned| OwnedState {
176                            addr: owned.addr,
177                            assignment: Assignment {
178                                seal: f(owned.assignment.seal),
179                                data: owned.assignment.data,
180                            },
181                            status: owned.status,
182                        })
183                        .collect();
184                    (name, map)
185                })
186                .collect(),
187            aggregated: self.aggregated,
188        }
189    }
190
191    pub fn filter_map<To>(self, f: impl Fn(Seal) -> Option<To>) -> ContractState<To> {
192        ContractState {
193            immutable: self.immutable,
194            owned: self
195                .owned
196                .into_iter()
197                .map(|(name, map)| {
198                    let map = map
199                        .into_iter()
200                        .filter_map(|owned| {
201                            Some(OwnedState {
202                                addr: owned.addr,
203                                assignment: Assignment {
204                                    seal: f(owned.assignment.seal)?,
205                                    data: owned.assignment.data,
206                                },
207                                status: owned.status,
208                            })
209                        })
210                        .collect();
211                    (name, map)
212                })
213                .collect(),
214            aggregated: self.aggregated,
215        }
216    }
217}
218
219/// Parameters used by RGB for contract creation operations.
220///
221/// Differs from [`IssueParams`] in the fact that it uses full seal data instead of
222/// [`hypersonic::AuthTokens`] for output definitions.
223#[derive(Clone, Debug)]
224#[cfg_attr(
225    feature = "serde",
226    derive(Serialize, Deserialize),
227    serde(
228        rename_all = "camelCase",
229        bound = "Seal: serde::Serialize + for<'d> serde::Deserialize<'d>"
230    )
231)]
232pub struct CreateParams<Seal: Clone> {
233    pub issuer: IssuerSpec,
234    pub consensus: Consensus,
235    pub testnet: bool,
236    pub method: MethodName,
237    pub name: TypeName,
238    pub timestamp: Option<DateTime<Utc>>,
239    pub global: Vec<NamedState<StateAtom>>,
240    pub owned: Vec<NamedState<Assignment<EitherSeal<Seal>>>>,
241}
242
243impl<Seal: Clone> CreateParams<Seal> {
244    pub fn new_testnet(
245        issuer: impl Into<IssuerSpec>,
246        consensus: Consensus,
247        name: impl Into<TypeName>,
248    ) -> Self {
249        Self {
250            issuer: issuer.into(),
251            consensus,
252            testnet: true,
253            method: vname!("issue"),
254            name: name.into(),
255            timestamp: None,
256            global: none![],
257            owned: none![],
258        }
259    }
260
261    pub fn with_global_verified(
262        mut self,
263        name: impl Into<StateName>,
264        data: impl Into<StrictVal>,
265    ) -> Self {
266        self.global
267            .push(NamedState { name: name.into(), state: StateAtom::new_verified(data) });
268        self
269    }
270
271    pub fn push_owned_unlocked(
272        &mut self,
273        name: impl Into<StateName>,
274        assignment: Assignment<EitherSeal<Seal>>,
275    ) {
276        self.owned
277            .push(NamedState { name: name.into(), state: assignment });
278    }
279}
280
281#[derive(Clone, Debug)]
282pub struct Contract<S: Stock, P: Pile> {
283    /// Cached contract id
284    contract_id: ContractId,
285    ledger: Ledger<S>,
286    pile: P,
287}
288
289impl<S: Stock, P: Pile> Contract<S, P> {
290    /// Initializes contract from contract articles, consignment and a persistence configuration.
291    pub fn with(
292        articles: Articles,
293        consignment: Consignment<P::Seal>,
294        conf: S::Conf,
295    ) -> Result<Self, MultiError<ConsumeError<<P::Seal as RgbSeal>::Definition>, S::Error, P::Error>>
296    where
297        P::Conf: From<S::Conf>,
298        <P::Seal as RgbSeal>::Client: StrictDecode,
299        <P::Seal as RgbSeal>::Published: StrictDecode,
300        <P::Seal as RgbSeal>::WitnessId: StrictDecode,
301    {
302        let contract_id = articles.contract_id();
303        let genesis_opid = articles.genesis_opid();
304        let ledger = Ledger::new(articles, conf)
305            .map_err(MultiError::with_third)
306            .map_err(MultiError::from_other_a)?;
307        let conf: S::Conf = ledger.config();
308        let mut pile = P::new(conf.into()).map_err(MultiError::C)?;
309        pile.add_seals(genesis_opid, none!());
310        let mut contract = Self { ledger, pile, contract_id };
311        contract
312            .evaluate_commit(consignment.into_operations())
313            .map_err(MultiError::from_a)?;
314        Ok(contract)
315    }
316
317    pub fn issue(
318        issuer: Issuer,
319        params: CreateParams<<P::Seal as RgbSeal>::Definition>,
320        conf: impl FnOnce(&Articles) -> Result<S::Conf, S::Error>,
321    ) -> Result<Self, MultiError<IssuerError, S::Error, P::Error>>
322    where
323        P::Conf: From<S::Conf>,
324    {
325        if !params.issuer.check(issuer.issuer_id()) {
326            return Err(MultiError::A(IssuerError::IssuerMismatch));
327        }
328
329        let seals = SmallOrdMap::try_from_iter(params.owned.iter().enumerate().filter_map(
330            |(pos, assignment)| {
331                assignment
332                    .state
333                    .seal
334                    .to_explicit()
335                    .map(|seal| (pos as u16, seal))
336            },
337        ))
338        .expect("too many outputs");
339        let params = IssueParams {
340            issuer: params.issuer,
341            name: params.name,
342            consensus: params.consensus,
343            testnet: params.testnet,
344            timestamp: params.timestamp,
345            core: CoreParams {
346                method: params.method,
347                global: params.global,
348                owned: params
349                    .owned
350                    .into_iter()
351                    .map(|assignment| NamedState {
352                        name: assignment.name,
353                        state: DataCell {
354                            auth: assignment.state.seal.auth_token(),
355                            data: assignment.state.data,
356                            lock: None,
357                        },
358                    })
359                    .collect(),
360            },
361        };
362
363        let articles = issuer.issue(params);
364        let conf = conf(&articles).map_err(MultiError::B)?;
365        let ledger = Ledger::new(articles, conf)
366            .map_err(MultiError::with_third)
367            .map_err(MultiError::from_other_a)?;
368        let conf: S::Conf = ledger.config();
369        let contract_id = ledger.contract_id();
370
371        // Init seals
372        let mut pile = P::new(conf.into()).map_err(MultiError::C)?;
373        pile.add_seals(ledger.articles().genesis_opid(), seals);
374
375        Ok(Self { ledger, pile, contract_id })
376    }
377
378    pub fn load(
379        stock_conf: S::Conf,
380        pile_conf: P::Conf,
381    ) -> Result<Self, MultiError<S::Error, P::Error>> {
382        let ledger = Ledger::load(stock_conf).map_err(MultiError::A)?;
383        let contract_id = ledger.contract_id();
384        let pile = P::load(pile_conf).map_err(MultiError::B)?;
385        Ok(Self { ledger, pile, contract_id })
386    }
387
388    #[inline]
389    pub(crate) fn witness_status(&self, wid: <P::Seal as RgbSeal>::WitnessId) -> WitnessStatus {
390        self.pile.witness_status(wid)
391    }
392
393    /// Get the best mining status for a given operation ("best" means "the most deeply mined").
394    fn best_op_status(&self, opid: Opid) -> WitnessStatus {
395        self.pile
396            .op_witness_ids(opid)
397            .map(|wid| self.witness_status(wid))
398            // "best" means "the most deeply mined"
399            .reduce(|best, other| best.best(other))
400            .unwrap_or(WitnessStatus::Genesis)
401    }
402
403    fn retrieve(&self, opid: Opid) -> Option<SealWitness<P::Seal>> {
404        let (status, wid) = self
405            .pile
406            .op_witness_ids(opid)
407            .map(|wid| (self.pile.witness_status(wid), wid))
408            .reduce(|best, other| if best.0.is_better(other.0) { best } else { other })?;
409        if !status.is_valid() {
410            return None;
411        }
412        let client = self.pile.cli_witness(wid);
413        let published = self.pile.pub_witness(wid);
414        Some(SealWitness::new(published, client))
415    }
416
417    pub fn contract_id(&self) -> ContractId { self.contract_id }
418
419    pub fn articles(&self) -> &Articles { self.ledger.articles() }
420
421    /// # Nota bene
422    ///
423    /// Does not include genesis
424    pub fn operations(
425        &self,
426    ) -> impl Iterator<Item = (Opid, Operation, OpRels<P::Seal>)> + use<'_, S, P> {
427        self.ledger.operations().map(|(opid, op)| {
428            let rels = self.pile.op_relations(opid, op.destructible_out.len_u16());
429            (opid, op, rels)
430        })
431    }
432
433    pub fn trace(&self) -> impl Iterator<Item = (Opid, Transition)> + use<'_, S, P> {
434        self.ledger.trace()
435    }
436
437    pub fn witness_ids(
438        &self,
439    ) -> impl Iterator<Item = <P::Seal as RgbSeal>::WitnessId> + use<'_, S, P> {
440        self.pile.witness_ids()
441    }
442
443    pub fn witnesses(&self) -> impl Iterator<Item = Witness<P::Seal>> + use<'_, S, P> {
444        self.pile.witnesses()
445    }
446
447    pub fn ops_by_witness_id(
448        &self,
449        wid: <P::Seal as RgbSeal>::WitnessId,
450    ) -> impl Iterator<Item = Opid> + use<'_, S, P> {
451        self.pile.ops_by_witness_id(wid)
452    }
453
454    pub fn op_seals(&self, opid: Opid, up_to: u16) -> OpRels<P::Seal> {
455        self.pile.op_relations(opid, up_to)
456    }
457
458    pub fn seal(&self, seal: &<P::Seal as RgbSeal>::Definition) -> Option<CellAddr> {
459        let auth = seal.auth_token();
460        self.ledger.state().raw.auth.get(&auth).copied()
461    }
462
463    /// Get the contract state.
464    ///
465    /// The call does not recompute the contract state, but does a seal resolution,
466    /// taking into account the status of the witnesses in the whole history.
467    pub fn state(&self) -> ContractState<P::Seal> {
468        let mut cache = bmap! {};
469        let mut ancestor_cache = bmap! {};
470        let mut cached_status = |opid: Opid| {
471            *cache
472                .entry(opid)
473                .or_insert_with(|| self.best_op_status(opid))
474        };
475        let mut get_status = |opid: Opid, or: WitnessStatus| {
476            (*ancestor_cache.entry(opid).or_insert_with(|| {
477                self.ledger
478                    .ancestors([opid])
479                    .map(|ancestor| self.best_op_status(ancestor))
480                    .fold(WitnessStatus::Genesis, |worst, other| worst.worst(other))
481            }))
482            .worst(or)
483        };
484        let state = self.ledger.state().main.clone();
485        let mut owned = bmap! {};
486        for (name, map) in state.owned {
487            let mut state = vec![];
488            for (addr, data) in map {
489                let Some(seal) = self.pile.seal(addr) else {
490                    continue;
491                };
492                if let Some(seal) = seal.to_src() {
493                    state.push(OwnedState {
494                        addr,
495                        assignment: Assignment { seal, data },
496                        status: get_status(addr.opid, cached_status(addr.opid)),
497                    });
498                } else {
499                    // We insert a copy of state for each of the witnesses created for the operation
500                    for wid in self.pile.op_witness_ids(addr.opid) {
501                        state.push(OwnedState {
502                            addr,
503                            assignment: Assignment { seal: seal.resolve(wid), data: data.clone() },
504                            status: get_status(addr.opid, self.pile.witness_status(wid)),
505                        });
506                    }
507                }
508            }
509            owned.insert(name, state);
510        }
511        let mut immutable = bmap! {};
512        for (name, map) in state.global {
513            let mut state = vec![];
514            for (addr, data) in map {
515                let status = get_status(addr.opid, cached_status(addr.opid));
516                state.push(ImmutableState { addr, data, status });
517            }
518            immutable.insert(name, state);
519        }
520        ContractState { immutable, owned, aggregated: state.aggregated }
521    }
522
523    pub fn full_state(&self) -> &EffectiveState { self.ledger.state() }
524
525    /// Synchronize the status of all witnesses and single-use seal definitions.
526    ///
527    /// # Panics
528    ///
529    /// If the contract id is not known.
530    pub fn sync(
531        &mut self,
532        changed: impl IntoIterator<Item = (<P::Seal as RgbSeal>::WitnessId, WitnessStatus)>,
533    ) -> Result<(), MultiError<AcceptError, S::Error>> {
534        // Step 1: Sanitize the list of changed wids
535        let mut affected_wids = IndexMap::new();
536        for (wid, status) in changed {
537            if !self.pile.has_witness(wid) {
538                continue;
539            }
540            let prev_status = self.pile.witness_status(wid);
541            if status == prev_status {
542                continue;
543            }
544
545            let old_status = affected_wids.insert(wid, status);
546            debug_assert!(
547                old_status.is_none() || old_status == Some(status),
548                "the same transaction with different status passed to a sync operation"
549            );
550        }
551
552        // Step 2: Select opdis which may be affected by the changed witnesses and their pre-sync
553        // operation status
554        let mut affected_ops = IndexMap::new();
555        for wid in affected_wids.keys() {
556            for opid in self.pile.ops_by_witness_id(*wid) {
557                let op_status = self.best_op_status(opid);
558                let old = affected_ops.insert(opid, op_status);
559                debug_assert!(old.is_none() || old == Some(op_status));
560            }
561        }
562
563        // Step 3: Update witness status.
564        // NB: This cannot be done at the same time as step 2 due to many-to-many relation between
565        // witnesses and operations, such that one witness change may affect other operation witness
566        // status.
567        for (wid, status) in affected_wids {
568            self.pile.update_witness_status(wid, status);
569        }
570
571        // Step 4: Filter opids and leave only those whose status has changed after the witness
572        // update
573        let mut roll_back = IndexSet::new();
574        let mut forward = IndexSet::new();
575        for (opid, old_status) in affected_ops {
576            let new_status = self.best_op_status(opid);
577            if old_status.is_valid() == new_status.is_valid() {
578                continue;
579            }
580            if new_status.is_valid() {
581                forward.insert(opid);
582            } else {
583                roll_back.insert(opid);
584            }
585        }
586        debug_assert_eq!(forward.intersection(&roll_back).count(), 0);
587
588        // Step 5: Perform rollback and forward operations
589        self.ledger.rollback(roll_back).map_err(MultiError::B)?;
590        // Ledger has already committed as a part of `rollback`
591        self.pile.commit_transaction();
592
593        self.ledger.forward(forward)?;
594        // Ledger has already committed as a part of `forward`
595        self.pile.commit_transaction();
596
597        Ok(())
598    }
599
600    /// Do a call to the contract method, creating and operation.
601    ///
602    /// The operation is automatically included in the contract history.
603    ///
604    /// The state of the contract is not automatically updated, but on the next update it will
605    /// reflect the call results.
606    pub fn call(
607        &mut self,
608        call: CallParams,
609        seals: SmallOrdMap<u16, <P::Seal as RgbSeal>::Definition>,
610    ) -> Result<Operation, MultiError<AcceptError, S::Error>> {
611        let opid = self.ledger.call(call)?;
612        let operation = self.ledger.operation(opid);
613        debug_assert_eq!(operation.opid(), opid);
614        self.pile.add_seals(opid, seals);
615        debug_assert_eq!(operation.contract_id, self.contract_id());
616        Ok(operation)
617    }
618
619    /// Include an operation and its witness to the history of known operations and the contract
620    /// state.
621    pub fn include(
622        &mut self,
623        opid: Opid,
624        anchor: <P::Seal as RgbSeal>::Client,
625        published: &<P::Seal as RgbSeal>::Published,
626    ) {
627        let wid = published.pub_id();
628        let anchor = if self.pile.has_witness(wid) {
629            let mut prev_anchor = self.pile.cli_witness(wid);
630            if prev_anchor != anchor {
631                prev_anchor.merge(anchor).expect(
632                    "the existing anchor is not compatible with the new one; this indicates \
633                     either a bug in the RGB standard library or a compromised storage",
634                );
635            }
636            prev_anchor
637        } else {
638            anchor
639        };
640        self.pile
641            .add_witness(opid, wid, published, &anchor, WitnessStatus::Tentative);
642        self.pile.commit_transaction();
643    }
644
645    fn aux<W: WriteRaw>(
646        &self,
647        opid: Opid,
648        op: &Operation,
649        mut writer: StrictWriter<W>,
650    ) -> io::Result<StrictWriter<W>> {
651        // Write seal definitions
652        let seals = self.pile.seals(opid, op.destructible_out.len_u16());
653        writer = seals.strict_encode(writer)?;
654
655        // Write witnesses
656        let witness = self.retrieve(opid);
657        writer = witness.is_some().strict_encode(writer)?;
658        if let Some(witness) = witness {
659            writer = witness.strict_encode(writer)?;
660        }
661
662        Ok(writer)
663    }
664
665    /// Export a contract to a strictly encoded stream.
666    ///
667    /// # Errors
668    ///
669    /// If the output stream failures, like when the stream cannot accept more data or got
670    /// disconnected.
671    pub fn export(&self, writer: StrictWriter<impl WriteRaw>) -> io::Result<()>
672    where
673        <P::Seal as RgbSeal>::Client: StrictDumb + StrictEncode,
674        <P::Seal as RgbSeal>::Published: StrictDumb + StrictEncode,
675        <P::Seal as RgbSeal>::WitnessId: StrictEncode,
676    {
677        self.ledger
678            .export_all_aux(writer, |opid, op, writer| self.aux(opid, op, writer))
679    }
680
681    /// Create a consignment with a history from the genesis to each of the `terminals`, and
682    /// serialize it to a strictly encoded stream `writer`.
683    ///
684    /// # Errors
685    ///
686    /// If the output stream failures, like when the stream cannot accept more data or got
687    /// disconnected.
688    pub fn consign(
689        &self,
690        terminals: impl IntoIterator<Item = impl Borrow<AuthToken>>,
691        writer: StrictWriter<impl WriteRaw>,
692    ) -> io::Result<()>
693    where
694        <P::Seal as RgbSeal>::Client: StrictDumb + StrictEncode,
695        <P::Seal as RgbSeal>::Published: StrictDumb + StrictEncode,
696        <P::Seal as RgbSeal>::WitnessId: StrictEncode,
697    {
698        self.ledger
699            .export_aux(terminals, writer, |opid, op, writer| self.aux(opid, op, writer))
700    }
701
702    /// Consume a consignment stream.
703    ///
704    /// The method:
705    /// - validates the consignment;
706    /// - resolves auth tokens into seal definitions known to the current wallet (i.e., coming from
707    ///   the invoices produced by the wallet);
708    /// - checks the signature of the issuer over the contract articles;
709    ///
710    /// # Arguments
711    ///
712    /// - `allow_unknown`: allows importing a contract which was not known to the system;
713    /// - `reader`: the input stream;
714    /// - `seal_resolver`: lambda which knows about the seal definitions from the wallet-generated
715    ///   invoices;
716    /// - `sig_validator`: a validator for the signature of the issuer over the contract articles.
717    pub fn consume<E>(
718        &mut self,
719        reader: &mut StrictReader<impl ReadRaw>,
720        seal_resolver: impl FnMut(&Operation) -> BTreeMap<u16, <P::Seal as RgbSeal>::Definition>,
721        sig_validator: impl FnOnce(StrictHash, &Identity, &SigBlob) -> Result<(), E>,
722    ) -> Result<(), MultiError<ConsumeError<<P::Seal as RgbSeal>::Definition>, S::Error>>
723    where
724        <P::Seal as RgbSeal>::Client: StrictDecode,
725        <P::Seal as RgbSeal>::Published: StrictDecode,
726        <P::Seal as RgbSeal>::WitnessId: StrictDecode,
727    {
728        let contract_id = parse_consignment(reader).map_err(MultiError::from_a)?;
729        if contract_id != self.contract_id() {
730            return Err(MultiError::A(ConsumeError::UnknownContract(contract_id)));
731        }
732        self.consume_internal(reader, seal_resolver, sig_validator)
733    }
734
735    pub(crate) fn consume_internal<E>(
736        &mut self,
737        reader: &mut StrictReader<impl ReadRaw>,
738        seal_resolver: impl FnMut(&Operation) -> BTreeMap<u16, <P::Seal as RgbSeal>::Definition>,
739        sig_validator: impl FnOnce(StrictHash, &Identity, &SigBlob) -> Result<(), E>,
740    ) -> Result<(), MultiError<ConsumeError<<P::Seal as RgbSeal>::Definition>, S::Error>>
741    where
742        <P::Seal as RgbSeal>::Client: StrictDecode,
743        <P::Seal as RgbSeal>::Published: StrictDecode,
744        <P::Seal as RgbSeal>::WitnessId: StrictDecode,
745    {
746        let articles = (|| -> Result<Articles, ConsumeError<_>> {
747            // Read and ignore the extension block
748            let ext_blocks = u8::strict_decode(reader)?;
749            for _ in 0..ext_blocks {
750                let len = u16::strict_decode(reader)?;
751                let r = unsafe { reader.raw_reader() };
752                let _ = r.read_raw::<{ u16::MAX as usize }>(len as usize)?;
753            }
754
755            // We need to read articles field by field since we have to evaluate genesis separately
756            let semantics = Semantics::strict_decode(reader)?;
757            let sig = Option::<SigBlob>::strict_decode(reader)?;
758
759            let issue_version = ReservedBytes::<1>::strict_decode(reader)?;
760            let meta = ContractMeta::strict_decode(reader)?;
761            let codex = Codex::strict_decode(reader)?;
762
763            let op_reader = OpReader {
764                stream: reader,
765                seal_resolver,
766                // We start with this hardcoded value to signal that we need to read the actual
767                // count right after the genesis (first operation).
768                count: u32::MAX,
769                _phantom: PhantomData,
770            };
771            self.evaluate_commit(op_reader)?;
772
773            // We need to clone due to a borrow checker.
774            let genesis = self.ledger.articles().genesis().clone();
775            let issue = Issue { version: issue_version, meta, codex, genesis };
776            let articles = Articles::with(semantics, issue, sig, sig_validator)?;
777
778            Ok(articles)
779        })()
780        .map_err(MultiError::A)?;
781
782        // Here we do not check for the end of the stream,
783        // so in the future we can have arbitrary extensions
784        // put here with no backward compatibility issues.
785
786        self.ledger
787            .upgrade_apis(articles)
788            .map_err(MultiError::from_other_a)?;
789        Ok(())
790    }
791
792    pub(crate) fn evaluate_commit<R: ReadOperation<Seal = P::Seal>>(
793        &mut self,
794        reader: R,
795    ) -> Result<(), VerificationError<P::Seal>>
796    where
797        <P::Seal as RgbSeal>::Client: StrictDecode,
798        <P::Seal as RgbSeal>::Published: StrictDecode,
799        <P::Seal as RgbSeal>::WitnessId: StrictDecode,
800    {
801        self.evaluate(reader)?;
802        self.ledger.commit_transaction();
803        self.pile.commit_transaction();
804        Ok(())
805    }
806}
807
808pub struct OpReader<
809    'r,
810    Seal: RgbSeal,
811    R: ReadRaw,
812    F: FnMut(&Operation) -> BTreeMap<u16, Seal::Definition>,
813> {
814    stream: &'r mut StrictReader<R>,
815    count: u32,
816    seal_resolver: F,
817    _phantom: PhantomData<Seal>,
818}
819
820impl<'r, Seal: RgbSeal, R: ReadRaw, F: FnMut(&Operation) -> BTreeMap<u16, Seal::Definition>>
821    ReadOperation for OpReader<'r, Seal, R, F>
822{
823    type Seal = Seal;
824
825    fn read_operation(
826        &mut self,
827    ) -> Result<Option<OperationSeals<Self::Seal>>, impl Error + 'static> {
828        if self.count == 0 {
829            return Result::<_, DecodeError>::Ok(None);
830        }
831        let operation = Operation::strict_decode(self.stream)?;
832
833        let mut defined_seals = SmallOrdMap::strict_decode(self.stream)?;
834        defined_seals
835            .extend((self.seal_resolver)(&operation))
836            .map_err(|_| {
837                DecodeError::DataIntegrityError(format!(
838                    "too many seals defined for the operation {}",
839                    operation.opid()
840                ))
841            })?;
842
843        let witness = Option::<SealWitness<Seal>>::strict_decode(self.stream)?;
844
845        // We start with this hardcoded value to signal that we need to read the actual
846        // count right after the genesis (first operation).
847        if self.count == u32::MAX {
848            self.count = u32::strict_decode(self.stream)?;
849        } else {
850            self.count -= 1;
851        }
852
853        Ok(Some(OperationSeals { operation, defined_seals, witness }))
854    }
855}
856
857impl<S: Stock, P: Pile> ContractApi<P::Seal> for Contract<S, P> {
858    fn contract_id(&self) -> ContractId { self.ledger.contract_id() }
859
860    fn codex(&self) -> &Codex { self.ledger.articles().codex() }
861
862    fn repo(&self) -> &impl LibRepo { self.ledger.articles() }
863
864    fn memory(&self) -> &impl Memory { &self.ledger.state().raw }
865
866    fn is_known(&self, opid: Opid) -> bool { self.ledger.is_valid(opid) }
867
868    fn apply_operation(&mut self, op: VerifiedOperation) {
869        self.ledger.apply(op).expect("unable to apply operation");
870    }
871
872    fn apply_seals(
873        &mut self,
874        opid: Opid,
875        seals: SmallOrdMap<u16, <P::Seal as RgbSeal>::Definition>,
876    ) {
877        self.pile.add_seals(opid, seals);
878    }
879
880    fn apply_witness(&mut self, opid: Opid, witness: SealWitness<P::Seal>) {
881        self.include(opid, witness.client, &witness.published)
882    }
883}
884
885#[derive(Debug, Display, Error, From)]
886#[display(inner)]
887pub enum ConsumeError<Seal: RgbSealDef> {
888    #[from]
889    #[from(io::Error)]
890    Io(IoError),
891
892    /// unknown {0} can't be consumed; please import contract articles first.
893    #[display(doc_comments)]
894    UnknownContract(ContractId),
895
896    #[from]
897    Semantics(SemanticError),
898
899    #[from]
900    Decode(DecodeError),
901
902    #[from]
903    Verify(VerificationError<Seal::Src>),
904
905    #[from]
906    #[from(IssueError)]
907    // FIXME
908    Issue(IssuerError),
909}
910
911#[cfg(feature = "binfile")]
912mod fs {
913    use std::path::Path;
914
915    use binfile::BinFile;
916    use strict_encoding::{StreamWriter, StrictDumb, StrictEncode};
917
918    use super::*;
919    use crate::{CONSIGN_MAGIC_NUMBER, CONSIGN_VERSION};
920
921    impl<S: Stock, P: Pile> Contract<S, P> {
922        /// Export a contract to a file at `path`.
923        ///
924        /// # Errors
925        ///
926        /// If writing to the file failures, like when the file already exists, there is no write
927        /// access to it, or no sufficient disk space.
928        pub fn export_to_file(&self, path: impl AsRef<Path>) -> io::Result<()>
929        where
930            <P::Seal as RgbSeal>::Client: StrictDumb + StrictEncode,
931            <P::Seal as RgbSeal>::Published: StrictDumb + StrictEncode,
932            <P::Seal as RgbSeal>::WitnessId: StrictEncode,
933        {
934            let file = BinFile::<CONSIGN_MAGIC_NUMBER, CONSIGN_VERSION>::create_new(path)?;
935            let writer = StrictWriter::with(StreamWriter::new::<{ usize::MAX }>(file));
936            self.export(writer)
937        }
938
939        /// Create a consignment with a history from the genesis to each of the `terminals`, and
940        /// serialize it to a `file`.
941        ///
942        /// # Errors
943        ///
944        /// If writing to the file failures, like when the file already exists, there is no write
945        /// access to it, or no sufficient disk space.
946        pub fn consign_to_file(
947            &self,
948            path: impl AsRef<Path>,
949            terminals: impl IntoIterator<Item = impl Borrow<AuthToken>>,
950        ) -> io::Result<()>
951        where
952            <P::Seal as RgbSeal>::Client: StrictDumb + StrictEncode,
953            <P::Seal as RgbSeal>::Published: StrictDumb + StrictEncode,
954            <P::Seal as RgbSeal>::WitnessId: StrictEncode,
955        {
956            let file = BinFile::<CONSIGN_MAGIC_NUMBER, CONSIGN_VERSION>::create_new(path)?;
957            let writer = StrictWriter::with(StreamWriter::new::<{ usize::MAX }>(file));
958            self.consign(terminals, writer)
959        }
960    }
961}