rgbstd/persistence/
stock.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
22use std::collections::{BTreeMap, BTreeSet};
23use std::convert::Infallible;
24use std::ops::{Deref, DerefMut};
25
26use amplify::confinement::{MediumOrdMap, MediumOrdSet, TinyOrdMap};
27use amplify::ByteArray;
28use bp::dbc::Anchor;
29use bp::Txid;
30use commit_verify::mpc::MerkleBlock;
31use rgb::validation::{Status, Validity, Warning};
32use rgb::{
33    validation, AnchorId, AnchoredBundle, Assign, AssignmentType, BundleId, ContractHistory,
34    ContractId, ContractState, ExposedState, Extension, Genesis, GenesisSeal, GraphSeal, OpId,
35    Operation, Opout, SecretSeal, SubSchema, Transition, TransitionBundle, TxoSeal, TypedAssigns,
36    WitnessAnchor,
37};
38use strict_encoding::{StrictDeserialize, StrictSerialize};
39
40use crate::containers::{Bindle, Cert, Consignment, ContentId, Contract, TerminalSeal, Transfer};
41use crate::interface::{
42    ContractIface, Iface, IfaceId, IfaceImpl, IfacePair, SchemaIfaces, TypedState,
43};
44use crate::persistence::inventory::{DataError, IfaceImplError, InventoryInconsistency};
45use crate::persistence::{
46    Hoard, Inventory, InventoryDataError, InventoryError, Stash, StashInconsistency,
47};
48use crate::resolvers::ResolveHeight;
49use crate::{Outpoint, LIB_NAME_RGB_STD};
50
51#[derive(Clone, Eq, PartialEq, Debug)]
52#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
53#[strict_type(lib = LIB_NAME_RGB_STD)]
54pub struct IndexedBundle(ContractId, BundleId);
55
56#[derive(Clone, Debug, Default)]
57#[derive(StrictType, StrictEncode, StrictDecode)]
58#[strict_type(lib = LIB_NAME_RGB_STD)]
59pub struct ContractIndex {
60    public_opouts: MediumOrdSet<Opout>,
61    outpoint_opouts: MediumOrdMap<Outpoint, MediumOrdSet<Opout>>,
62}
63
64/// Stock is an in-memory inventory (stash, index, contract state) useful for
65/// WASM implementations.
66///
67/// Can hold data about up to 256 contracts.
68#[derive(Clone, Debug, Getters)]
69#[getter(prefix = "debug_")]
70#[derive(StrictType, StrictEncode, StrictDecode)]
71#[strict_type(lib = LIB_NAME_RGB_STD)]
72pub struct Stock {
73    // stash
74    hoard: Hoard,
75    // state
76    history: TinyOrdMap<ContractId, ContractHistory>,
77    // index
78    bundle_op_index: MediumOrdMap<OpId, IndexedBundle>,
79    anchor_bundle_index: MediumOrdMap<BundleId, AnchorId>,
80    contract_index: TinyOrdMap<ContractId, ContractIndex>,
81    terminal_index: MediumOrdMap<SecretSeal, Opout>,
82    // secrets
83    seal_secrets: MediumOrdSet<GraphSeal>,
84}
85
86impl Default for Stock {
87    fn default() -> Self {
88        Stock {
89            hoard: Hoard::preset(),
90            history: empty!(),
91            bundle_op_index: empty!(),
92            anchor_bundle_index: empty!(),
93            contract_index: empty!(),
94            terminal_index: empty!(),
95            seal_secrets: empty!(),
96        }
97    }
98}
99
100impl StrictSerialize for Stock {}
101impl StrictDeserialize for Stock {}
102
103impl Deref for Stock {
104    type Target = Hoard;
105
106    fn deref(&self) -> &Self::Target { &self.hoard }
107}
108
109impl DerefMut for Stock {
110    fn deref_mut(&mut self) -> &mut Self::Target { &mut self.hoard }
111}
112
113#[allow(clippy::result_large_err)]
114impl Stock {
115    fn consume_consignment<R: ResolveHeight, const TYPE: bool>(
116        &mut self,
117        mut consignment: Consignment<TYPE>,
118        resolver: &mut R,
119        force: bool,
120    ) -> Result<validation::Status, InventoryError<Infallible>>
121    where
122        R::Error: 'static,
123    {
124        let mut status = validation::Status::new();
125        match consignment.validation_status() {
126            None => return Err(DataError::NotValidated.into()),
127            Some(status) if status.validity() == Validity::Invalid => {
128                return Err(DataError::Invalid(status.clone()).into());
129            }
130            Some(status) if status.validity() == Validity::UnresolvedTransactions && !force => {
131                return Err(DataError::UnresolvedTransactions.into());
132            }
133            Some(status) if status.validity() == Validity::UnminedTerminals && !force => {
134                return Err(DataError::TerminalsUnmined.into());
135            }
136            Some(s) if s.validity() == Validity::UnresolvedTransactions && !force => {
137                status.add_warning(Warning::Custom(s!(
138                    "contract contains unknown transactions and was forcefully imported"
139                )));
140            }
141            Some(s) if s.validity() == Validity::UnminedTerminals && !force => {
142                status.add_warning(Warning::Custom(s!("contract contains not yet mined final \
143                                                       transactions and was forcefully imported")));
144            }
145            _ => {}
146        }
147
148        let id = consignment.contract_id();
149
150        self.import_schema(consignment.schema.clone())?;
151        for IfacePair { iface, iimpl } in consignment.ifaces.values() {
152            self.import_iface(iface.clone())?;
153            self.import_iface_impl(iimpl.clone())?;
154        }
155
156        // clone needed due to borrow checker
157        for (bundle_id, terminal) in consignment.terminals.clone() {
158            for secret in terminal.seals.iter().filter_map(TerminalSeal::secret_seal) {
159                if let Some(seal) = self
160                    .seal_secrets
161                    .iter()
162                    .find(|s| s.to_concealed_seal() == secret)
163                {
164                    consignment.reveal_bundle_seal(bundle_id, *seal);
165                }
166            }
167        }
168
169        // Update existing contract state
170        let history = consignment
171            .update_history(self.history.get(&id), resolver)
172            .map_err(|err| DataError::HeightResolver(Box::new(err)))?;
173        self.history.insert(id, history)?;
174
175        let contract_id = consignment.contract_id();
176        if !self.contract_index.contains_key(&contract_id) {
177            self.contract_index.insert(contract_id, empty!())?;
178        }
179        self.index_genesis(contract_id, &consignment.genesis)?;
180        for extension in &consignment.extensions {
181            self.index_extension(contract_id, extension)?;
182        }
183        for AnchoredBundle { anchor, bundle } in &mut consignment.bundles {
184            let bundle_id = bundle.bundle_id();
185            let anchor_id = anchor.anchor_id(contract_id, bundle_id.into())?;
186            self.anchor_bundle_index.insert(bundle_id, anchor_id)?;
187            self.index_bundle(contract_id, bundle, anchor.txid)?;
188        }
189
190        self.hoard.consume_consignment(consignment)?;
191
192        Ok(status)
193    }
194
195    fn index_genesis(
196        &mut self,
197        id: ContractId,
198        genesis: &Genesis,
199    ) -> Result<(), InventoryError<<Self as Inventory>::Error>> {
200        let opid = genesis.id();
201        for (type_id, assign) in genesis.assignments.iter() {
202            match assign {
203                TypedAssigns::Declarative(vec) => {
204                    self.index_genesis_assignments(id, vec, opid, *type_id)?;
205                }
206                TypedAssigns::Fungible(vec) => {
207                    self.index_genesis_assignments(id, vec, opid, *type_id)?;
208                }
209                TypedAssigns::Structured(vec) => {
210                    self.index_genesis_assignments(id, vec, opid, *type_id)?;
211                }
212                TypedAssigns::Attachment(vec) => {
213                    self.index_genesis_assignments(id, vec, opid, *type_id)?;
214                }
215            }
216        }
217        Ok(())
218    }
219
220    fn index_extension(
221        &mut self,
222        id: ContractId,
223        extension: &Extension,
224    ) -> Result<(), InventoryError<<Self as Inventory>::Error>> {
225        let opid = extension.id();
226        for (type_id, assign) in extension.assignments.iter() {
227            match assign {
228                TypedAssigns::Declarative(vec) => {
229                    self.index_genesis_assignments(id, vec, opid, *type_id)?;
230                }
231                TypedAssigns::Fungible(vec) => {
232                    self.index_genesis_assignments(id, vec, opid, *type_id)?;
233                }
234                TypedAssigns::Structured(vec) => {
235                    self.index_genesis_assignments(id, vec, opid, *type_id)?;
236                }
237                TypedAssigns::Attachment(vec) => {
238                    self.index_genesis_assignments(id, vec, opid, *type_id)?;
239                }
240            }
241        }
242        Ok(())
243    }
244
245    fn index_bundle(
246        &mut self,
247        id: ContractId,
248        bundle: &TransitionBundle,
249        witness_txid: Txid,
250    ) -> Result<(), InventoryError<<Self as Inventory>::Error>> {
251        let bundle_id = bundle.bundle_id();
252        for (opid, item) in bundle.iter() {
253            if let Some(transition) = &item.transition {
254                self.bundle_op_index
255                    .insert(*opid, IndexedBundle(id, bundle_id))?;
256                for (type_id, assign) in transition.assignments.iter() {
257                    match assign {
258                        TypedAssigns::Declarative(vec) => {
259                            self.index_transition_assignments(
260                                id,
261                                vec,
262                                *opid,
263                                *type_id,
264                                witness_txid,
265                            )?;
266                        }
267                        TypedAssigns::Fungible(vec) => {
268                            self.index_transition_assignments(
269                                id,
270                                vec,
271                                *opid,
272                                *type_id,
273                                witness_txid,
274                            )?;
275                        }
276                        TypedAssigns::Structured(vec) => {
277                            self.index_transition_assignments(
278                                id,
279                                vec,
280                                *opid,
281                                *type_id,
282                                witness_txid,
283                            )?;
284                        }
285                        TypedAssigns::Attachment(vec) => {
286                            self.index_transition_assignments(
287                                id,
288                                vec,
289                                *opid,
290                                *type_id,
291                                witness_txid,
292                            )?;
293                        }
294                    }
295                }
296            }
297        }
298
299        Ok(())
300    }
301
302    fn index_genesis_assignments<State: ExposedState>(
303        &mut self,
304        contract_id: ContractId,
305        vec: &[Assign<State, GenesisSeal>],
306        opid: OpId,
307        type_id: AssignmentType,
308    ) -> Result<(), InventoryError<<Self as Inventory>::Error>> {
309        let index = self
310            .contract_index
311            .get_mut(&contract_id)
312            .ok_or(StashInconsistency::ContractAbsent(contract_id))?;
313
314        for (no, a) in vec.iter().enumerate() {
315            let opout = Opout::new(opid, type_id, no as u16);
316            if let Assign::ConfidentialState { seal, .. } | Assign::Revealed { seal, .. } = a {
317                let outpoint = seal.outpoint_or(seal.txid);
318                match index.outpoint_opouts.get_mut(&outpoint) {
319                    Some(opouts) => {
320                        opouts.push(opout)?;
321                    }
322                    None => {
323                        index
324                            .outpoint_opouts
325                            .insert(outpoint, confined_bset!(opout))?;
326                    }
327                }
328            }
329            if let Assign::Confidential { seal, .. } | Assign::ConfidentialSeal { seal, .. } = a {
330                self.terminal_index.insert(*seal, opout)?;
331            }
332        }
333        Ok(())
334    }
335
336    fn index_transition_assignments<State: ExposedState>(
337        &mut self,
338        contract_id: ContractId,
339        vec: &[Assign<State, GraphSeal>],
340        opid: OpId,
341        type_id: AssignmentType,
342        witness_txid: Txid,
343    ) -> Result<(), InventoryError<<Self as Inventory>::Error>> {
344        let index = self
345            .contract_index
346            .get_mut(&contract_id)
347            .ok_or(StashInconsistency::ContractAbsent(contract_id))?;
348
349        for (no, a) in vec.iter().enumerate() {
350            let opout = Opout::new(opid, type_id, no as u16);
351            if let Assign::ConfidentialState { seal, .. } | Assign::Revealed { seal, .. } = a {
352                let outpoint = seal.outpoint_or(witness_txid);
353                match index.outpoint_opouts.get_mut(&outpoint) {
354                    Some(opouts) => {
355                        opouts.push(opout)?;
356                    }
357                    None => {
358                        index
359                            .outpoint_opouts
360                            .insert(outpoint, confined_bset!(opout))?;
361                    }
362                }
363            }
364            if let Assign::Confidential { seal, .. } | Assign::ConfidentialSeal { seal, .. } = a {
365                self.terminal_index.insert(*seal, opout)?;
366            }
367        }
368        Ok(())
369    }
370}
371
372impl Inventory for Stock {
373    type Stash = Hoard;
374    // In-memory representation doesn't have connectivity errors
375    type Error = Infallible;
376
377    fn stash(&self) -> &Self::Stash { self }
378
379    fn import_sigs<I>(
380        &mut self,
381        content_id: ContentId,
382        sigs: I,
383    ) -> Result<(), InventoryDataError<Self::Error>>
384    where
385        I: IntoIterator<Item = Cert>,
386        I::IntoIter: ExactSizeIterator<Item = Cert>,
387    {
388        self.import_sigs_internal(content_id, sigs)?;
389        Ok(())
390    }
391
392    fn import_schema(
393        &mut self,
394        schema: impl Into<Bindle<SubSchema>>,
395    ) -> Result<validation::Status, InventoryDataError<Self::Error>> {
396        let bindle = schema.into();
397        let (schema, sigs) = bindle.into_split();
398        let id = schema.schema_id();
399
400        let mut status = schema.verify();
401        if !status.failures.is_empty() {
402            return Err(status.into());
403        }
404        if self.schemata.contains_key(&id) {
405            status.add_warning(Warning::Custom(format!("schema {id::<0} is already known")));
406        } else {
407            let schema_ifaces = SchemaIfaces::new(schema);
408            self.schemata.insert(id, schema_ifaces)?;
409        }
410
411        let content_id = ContentId::Schema(id);
412        // Do not bother if we can't import all the sigs
413        self.import_sigs_internal(content_id, sigs).ok();
414
415        Ok(status)
416    }
417
418    fn import_iface(
419        &mut self,
420        iface: impl Into<Bindle<Iface>>,
421    ) -> Result<validation::Status, InventoryDataError<Self::Error>> {
422        let bindle = iface.into();
423        let (iface, sigs) = bindle.into_split();
424        let id = iface.iface_id();
425
426        let mut status = validation::Status::new();
427
428        // TODO: Do interface check on internal consistency
429        if self.ifaces.insert(id, iface)?.is_some() {
430            status.add_warning(Warning::Custom(format!("interface {id::<0} is already known")));
431        }
432
433        let content_id = ContentId::Iface(id);
434        // Do not bother if we can't import all the sigs
435        self.import_sigs_internal(content_id, sigs).ok();
436
437        Ok(status)
438    }
439
440    fn import_iface_impl(
441        &mut self,
442        iimpl: impl Into<Bindle<IfaceImpl>>,
443    ) -> Result<validation::Status, InventoryDataError<Self::Error>> {
444        let bindle = iimpl.into();
445        let (iimpl, sigs) = bindle.into_split();
446        let iface_id = iimpl.iface_id;
447        let impl_id = iimpl.impl_id();
448
449        let mut status = validation::Status::new();
450
451        if !self.ifaces.contains_key(&iface_id) {
452            return Err(IfaceImplError::UnknownIface(iface_id).into());
453        }
454        let Some(schema_ifaces) = self.schemata.get_mut(&iimpl.schema_id) else {
455            return Err(IfaceImplError::UnknownSchema(iimpl.schema_id).into());
456        };
457        // TODO: Do interface check on internal consistency
458        if schema_ifaces.iimpls.insert(iface_id, iimpl)?.is_some() {
459            status.add_warning(Warning::Custom(format!(
460                "interface implementation {impl_id::<0} is already known",
461            )));
462        }
463
464        let content_id = ContentId::IfaceImpl(impl_id);
465        // Do not bother if we can't import all the sigs
466        self.import_sigs_internal(content_id, sigs).ok();
467
468        Ok(status)
469    }
470
471    fn import_contract<R: ResolveHeight>(
472        &mut self,
473        contract: Contract,
474        resolver: &mut R,
475    ) -> Result<validation::Status, InventoryError<Self::Error>>
476    where
477        R::Error: 'static,
478    {
479        self.consume_consignment(contract, resolver, false)
480    }
481
482    fn accept_transfer<R: ResolveHeight>(
483        &mut self,
484        transfer: Transfer,
485        resolver: &mut R,
486        force: bool,
487    ) -> Result<Status, InventoryError<Self::Error>>
488    where
489        R::Error: 'static,
490    {
491        self.consume_consignment(transfer, resolver, force)
492    }
493
494    fn consume_anchor(
495        &mut self,
496        anchor: Anchor<MerkleBlock>,
497    ) -> Result<(), InventoryError<Self::Error>> {
498        let anchor_id = anchor.anchor_id();
499        for (_, bundle_id) in anchor.mpc_proof.to_known_message_map() {
500            self.anchor_bundle_index
501                .insert(bundle_id.to_byte_array().into(), anchor_id)?;
502        }
503        self.hoard.consume_anchor(anchor)?;
504        Ok(())
505    }
506
507    fn consume_bundle(
508        &mut self,
509        contract_id: ContractId,
510        bundle: TransitionBundle,
511        witness_txid: Txid,
512    ) -> Result<(), InventoryError<<Self as Inventory>::Error>> {
513        self.index_bundle(contract_id, &bundle, witness_txid)?;
514        let history = self
515            .history
516            .get_mut(&contract_id)
517            .ok_or(InventoryInconsistency::StateAbsent(contract_id))?;
518        for item in bundle.values() {
519            if let Some(transition) = &item.transition {
520                let ord_txid = WitnessAnchor::from_mempool(witness_txid);
521                history.add_transition(transition, ord_txid);
522            }
523        }
524        self.hoard.consume_bundle(bundle)?;
525        Ok(())
526    }
527
528    unsafe fn import_contract_force<R: ResolveHeight>(
529        &mut self,
530        contract: Contract,
531        resolver: &mut R,
532    ) -> Result<validation::Status, InventoryError<Self::Error>>
533    where
534        R::Error: 'static,
535    {
536        self.consume_consignment(contract, resolver, true)
537    }
538
539    fn contract_iface(
540        &mut self,
541        contract_id: ContractId,
542        iface_id: IfaceId,
543    ) -> Result<ContractIface, InventoryError<Self::Error>> {
544        let history = self
545            .history
546            .get(&contract_id)
547            .ok_or(InventoryInconsistency::StateAbsent(contract_id))?
548            .clone();
549        let schema_id = history.schema_id();
550        let schema_ifaces = self
551            .schemata
552            .get(&schema_id)
553            .ok_or(StashInconsistency::SchemaAbsent(schema_id))?;
554        let state = ContractState {
555            schema: schema_ifaces.schema.clone(),
556            history,
557        };
558        let iimpl = schema_ifaces
559            .iimpls
560            .get(&iface_id)
561            .ok_or(StashInconsistency::IfaceImplAbsent(iface_id, schema_id))?
562            .clone();
563        Ok(ContractIface {
564            state,
565            iface: iimpl,
566        })
567    }
568
569    fn transition(&self, opid: OpId) -> Result<&Transition, InventoryError<Self::Error>> {
570        let IndexedBundle(_, bundle_id) = self
571            .bundle_op_index
572            .get(&opid)
573            .ok_or(InventoryInconsistency::BundleAbsent(opid))?;
574        let bundle = self.bundle(*bundle_id)?;
575        let item = bundle.get(&opid).ok_or(DataError::Concealed)?;
576        let transition = item.transition.as_ref().ok_or(DataError::Concealed)?;
577        Ok(transition)
578    }
579
580    fn anchored_bundle(&self, opid: OpId) -> Result<AnchoredBundle, InventoryError<Self::Error>> {
581        let IndexedBundle(contract_id, bundle_id) = self
582            .bundle_op_index
583            .get(&opid)
584            .ok_or(InventoryInconsistency::BundleAbsent(opid))?;
585
586        let anchor_id = self
587            .anchor_bundle_index
588            .get(bundle_id)
589            .ok_or(InventoryInconsistency::NoBundleAnchor(*bundle_id))?;
590
591        let bundle = self.bundle(*bundle_id)?.clone();
592        let anchor = self.anchor(*anchor_id)?;
593        let anchor = anchor.to_merkle_proof(*contract_id)?;
594        // TODO: Conceal all transitions except the one we need
595
596        Ok(AnchoredBundle { anchor, bundle })
597    }
598
599    fn contracts_by_outpoints(
600        &mut self,
601        outpoints: impl IntoIterator<Item = impl Into<Outpoint>>,
602    ) -> Result<BTreeSet<ContractId>, InventoryError<Self::Error>> {
603        let outpoints = outpoints
604            .into_iter()
605            .map(|o| o.into())
606            .collect::<BTreeSet<_>>();
607        let mut selected = BTreeSet::new();
608        for (contract_id, index) in &self.contract_index {
609            for outpoint in &outpoints {
610                if index.outpoint_opouts.contains_key(outpoint) {
611                    selected.insert(*contract_id);
612                }
613            }
614        }
615        Ok(selected)
616    }
617
618    fn public_opouts(
619        &mut self,
620        contract_id: ContractId,
621    ) -> Result<BTreeSet<Opout>, InventoryError<Self::Error>> {
622        let index = self
623            .contract_index
624            .get(&contract_id)
625            .ok_or(StashInconsistency::ContractAbsent(contract_id))?;
626        Ok(index.public_opouts.to_inner())
627    }
628
629    fn opouts_by_outpoints(
630        &mut self,
631        contract_id: ContractId,
632        outpoints: impl IntoIterator<Item = impl Into<Outpoint>>,
633    ) -> Result<BTreeSet<Opout>, InventoryError<Self::Error>> {
634        let index = self
635            .contract_index
636            .get(&contract_id)
637            .ok_or(StashInconsistency::ContractAbsent(contract_id))?;
638        let mut opouts = BTreeSet::new();
639        for outpoint in outpoints.into_iter().map(|o| o.into()) {
640            let set = index
641                .outpoint_opouts
642                .get(&outpoint)
643                .ok_or(DataError::OutpointUnknown(outpoint, contract_id))?;
644            opouts.extend(set)
645        }
646        Ok(opouts)
647    }
648
649    fn opouts_by_terminals(
650        &mut self,
651        terminals: impl IntoIterator<Item = SecretSeal>,
652    ) -> Result<BTreeSet<Opout>, InventoryError<Self::Error>> {
653        let terminals = terminals.into_iter().collect::<BTreeSet<_>>();
654        Ok(self
655            .terminal_index
656            .iter()
657            .filter(|(seal, _)| terminals.contains(*seal))
658            .map(|(_, opout)| *opout)
659            .collect())
660    }
661
662    fn state_for_outpoints(
663        &mut self,
664        contract_id: ContractId,
665        outpoints: impl IntoIterator<Item = impl Into<Outpoint>>,
666    ) -> Result<BTreeMap<Opout, TypedState>, InventoryError<Self::Error>> {
667        let outpoints = outpoints
668            .into_iter()
669            .map(|o| o.into())
670            .collect::<BTreeSet<_>>();
671
672        let history = self
673            .history
674            .get(&contract_id)
675            .ok_or(StashInconsistency::ContractAbsent(contract_id))?;
676
677        let mut res = BTreeMap::new();
678
679        for output in history.fungibles() {
680            if outpoints.contains(&output.seal) {
681                res.insert(output.opout, TypedState::Amount(output.state.value.as_u64()));
682            }
683        }
684
685        for output in history.data() {
686            if outpoints.contains(&output.seal) {
687                res.insert(output.opout, TypedState::Data(output.state.clone()));
688            }
689        }
690
691        for output in history.rights() {
692            if outpoints.contains(&output.seal) {
693                res.insert(output.opout, TypedState::Void);
694            }
695        }
696
697        for output in history.attach() {
698            if outpoints.contains(&output.seal) {
699                res.insert(output.opout, TypedState::Attachment(output.state.clone().into()));
700            }
701        }
702
703        Ok(res)
704    }
705
706    fn store_seal_secret(&mut self, seal: GraphSeal) -> Result<(), InventoryError<Self::Error>> {
707        self.seal_secrets.push(seal)?;
708        Ok(())
709    }
710
711    fn seal_secrets(&mut self) -> Result<BTreeSet<GraphSeal>, InventoryError<Self::Error>> {
712        Ok(self.seal_secrets.to_inner())
713    }
714}