rgb/popls/
bp.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
25//! Implementation of RGB standard library types for Bitcoin protocol, covering Bitcoin and Liquid
26//! proof of publication layer 1.
27
28use alloc::collections::btree_map::Entry;
29use alloc::collections::{btree_set, BTreeMap, BTreeSet};
30use alloc::vec;
31use core::mem;
32use std::collections::HashMap;
33
34use amplify::confinement::{
35    Collection, KeyedCollection, NonEmptyVec, SmallOrdMap, SmallOrdSet, U8 as U8MAX,
36};
37use amplify::{confinement, ByteArray, Bytes32, MultiError, Wrapper};
38use bp::dbc::tapret::TapretProof;
39pub use bp::seals;
40use bp::seals::{mmb, Anchor, Noise, TxoSeal, TxoSealExt, WOutpoint, WTxoSeal};
41use bp::{Outpoint, Sats, ScriptPubkey, Tx, Txid, Vout};
42use commit_verify::mpc::ProtocolId;
43use commit_verify::{mpc, Digest, DigestExt, Sha256, StrictHash};
44use hypersonic::{
45    AcceptError, AuthToken, CallParams, CellAddr, ContractId, CoreParams, DataCell, MethodName,
46    NamedState, Operation, Satisfaction, StateAtom, StateCalc, StateCalcError, StateName,
47    StateUnknown, Stock,
48};
49use invoice::bp::{Address, WitnessOut};
50use invoice::{RgbBeneficiary, RgbInvoice};
51use rgb::RgbSealDef;
52use rgbcore::LIB_NAME_RGB;
53use strict_encoding::{ReadRaw, StrictDecode, StrictReader, TypeName};
54use strict_types::StrictVal;
55
56use crate::contracts::SyncError;
57use crate::{
58    Assignment, CodexId, Consensus, ConsumeError, Contract, ContractState, Contracts, CreateParams,
59    EitherSeal, Identity, Issuer, IssuerError, OwnedState, Pile, SigBlob, Stockpile, WalletState,
60    WitnessStatus,
61};
62
63/// Trait abstracting a specific implementation of a bitcoin wallet.
64pub trait WalletProvider {
65    type Error: core::error::Error;
66
67    fn has_utxo(&self, outpoint: Outpoint) -> bool;
68    fn utxos(&self) -> impl Iterator<Item = Outpoint>;
69
70    #[cfg(not(feature = "async"))]
71    fn update_utxos(&mut self) -> Result<(), Self::Error>;
72    #[cfg(feature = "async")]
73    async fn update_utxos_async(&mut self) -> Result<(), Self::Error>;
74
75    fn register_seal(&mut self, seal: WTxoSeal);
76    fn resolve_seals(
77        &self,
78        seals: impl Iterator<Item = AuthToken>,
79    ) -> impl Iterator<Item = WTxoSeal>;
80
81    fn noise_seed(&self) -> Bytes32;
82    fn next_address(&mut self) -> Address;
83    fn next_nonce(&mut self) -> u64;
84
85    #[cfg(not(feature = "async"))]
86    /// Returns a closure which can retrieve a witness status of an arbitrary transaction id
87    /// (including the ones that are not related to the wallet).
88    fn txid_resolver(&self) -> impl Fn(Txid) -> Result<WitnessStatus, Self::Error>;
89    #[cfg(feature = "async")]
90    /// Returns a closure which can retrieve a witness status of an arbitrary transaction id
91    /// (including the ones that are not related to the wallet).
92    fn txid_resolver_async(&self) -> impl AsyncFn(Txid) -> Result<WitnessStatus, Self::Error>;
93
94    #[cfg(not(feature = "async"))]
95    /// Returns the height of the last known block.
96    fn last_block_height(&self) -> Result<u64, Self::Error>;
97    #[cfg(feature = "async")]
98    /// Returns the height of the last known block.
99    async fn last_block_height_async(&self) -> Result<u64, Self::Error>;
100
101    #[cfg(not(feature = "async"))]
102    /// Broadcasts the transaction, also updating UTXO set accordingly.
103    fn broadcast(&mut self, tx: &Tx, change: Option<(Vout, u32, u32)>) -> Result<(), Self::Error>;
104    #[cfg(feature = "async")]
105    /// Broadcasts the transaction, also updating UTXO set accordingly.
106    async fn broadcast_async(
107        &mut self,
108        tx: &Tx,
109        change: Option<(Vout, u32, u32)>,
110    ) -> Result<(), Self::Error>;
111}
112
113pub trait Coinselect {
114    fn coinselect<'a>(
115        &mut self,
116        invoiced_state: &StrictVal,
117        calc: &mut StateCalc,
118        // Sorted vector by values
119        owned_state: impl IntoIterator<
120            Item = &'a OwnedState<Outpoint>,
121            IntoIter: DoubleEndedIterator<Item = &'a OwnedState<Outpoint>>,
122        >,
123    ) -> Option<Vec<(CellAddr, Outpoint)>>;
124}
125
126pub const BP_BLANK_METHOD: &str = "_";
127
128#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)]
129#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))]
130pub struct PrefabSeal {
131    pub vout: Vout,
132    pub noise: Option<Noise>,
133}
134
135#[derive(Clone, Eq, PartialEq, Hash, Debug, Display)]
136#[display("{wout}/{sats}")]
137#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))]
138pub struct WoutAssignment {
139    #[cfg_attr(feature = "serde", serde(with = "serde_with::rust::display_fromstr"))]
140    pub wout: WitnessOut,
141    pub sats: Sats,
142}
143
144impl From<WoutAssignment> for ScriptPubkey {
145    fn from(val: WoutAssignment) -> Self { val.script_pubkey() }
146}
147
148impl WoutAssignment {
149    pub fn script_pubkey(&self) -> ScriptPubkey { self.wout.script_pubkey() }
150}
151
152impl<T> EitherSeal<T> {
153    pub fn map<U>(self, f: impl FnOnce(T) -> U) -> EitherSeal<U> {
154        match self {
155            Self::Alt(seal) => EitherSeal::Alt(f(seal)),
156            Self::Token(auth) => EitherSeal::Token(auth),
157        }
158    }
159}
160
161impl EitherSeal<Outpoint> {
162    pub fn transform(self, noise_engine: Sha256, nonce: u64) -> EitherSeal<WTxoSeal> {
163        match self {
164            EitherSeal::Alt(seal) => {
165                EitherSeal::Alt(WTxoSeal::no_fallback(seal, noise_engine, nonce))
166            }
167            EitherSeal::Token(auth) => EitherSeal::Token(auth),
168        }
169    }
170}
171
172impl CreateParams<Outpoint> {
173    pub fn transform(self, mut noise_engine: Sha256) -> CreateParams<WTxoSeal> {
174        noise_engine.input_raw(self.issuer.codex_id().as_slice());
175        noise_engine.input_raw(&[self.consensus as u8]);
176        noise_engine.input_raw(self.method.as_bytes());
177        noise_engine.input_raw(self.name.as_bytes());
178        noise_engine.input_raw(&self.timestamp.unwrap_or_default().timestamp().to_le_bytes());
179        CreateParams {
180            issuer: self.issuer,
181            consensus: self.consensus,
182            testnet: self.testnet,
183            method: self.method,
184            name: self.name,
185            timestamp: self.timestamp,
186            global: self.global,
187            owned: self
188                .owned
189                .into_iter()
190                .enumerate()
191                .map(|(nonce, assignment)| NamedState {
192                    name: assignment.name,
193                    state: Assignment {
194                        seal: assignment
195                            .state
196                            .seal
197                            .transform(noise_engine.clone(), nonce as u64),
198                        data: assignment.state.data,
199                    },
200                })
201                .collect(),
202        }
203    }
204}
205
206#[derive(Clone, Debug)]
207#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))]
208pub struct UsedState {
209    pub addr: CellAddr,
210    pub outpoint: Outpoint,
211    pub satisfaction: Option<Satisfaction>,
212}
213
214pub type PaymentScript = OpRequestSet<Option<WoutAssignment>>;
215
216/// A set of multiple operation requests (see [`OpRequests`]) under single or multiple contracts.
217#[derive(Wrapper, WrapperMut, Clone, Debug, From)]
218#[wrapper(Deref)]
219#[wrapper_mut(DerefMut)]
220#[cfg_attr(
221    feature = "serde",
222    derive(Serialize, Deserialize),
223    serde(
224        rename_all = "camelCase",
225        bound = "T: serde::Serialize + for<'d> serde::Deserialize<'d>"
226    )
227)]
228pub struct OpRequestSet<T>(NonEmptyVec<OpRequest<T>, U8MAX>);
229
230impl<T> IntoIterator for OpRequestSet<T> {
231    type Item = OpRequest<T>;
232    type IntoIter = vec::IntoIter<OpRequest<T>>;
233
234    fn into_iter(self) -> Self::IntoIter { self.0.into_iter() }
235}
236
237impl<T> OpRequestSet<T> {
238    pub fn with(request: OpRequest<T>) -> Self { Self(NonEmptyVec::with(request)) }
239}
240
241impl OpRequestSet<Option<WoutAssignment>> {
242    pub fn resolve_seals(
243        self,
244        resolver: impl Fn(&ScriptPubkey) -> Option<Vout>,
245        change: Option<Vout>,
246    ) -> Result<OpRequestSet<PrefabSeal>, UnresolvedSeal> {
247        let mut items = Vec::with_capacity(self.0.len());
248        for request in self.0 {
249            let mut owned = Vec::with_capacity(request.owned.len());
250            for assignment in request.owned {
251                let seal = match assignment.state.seal {
252                    EitherSeal::Alt(Some(seal)) => {
253                        let spk = seal.script_pubkey();
254                        let vout = resolver(&spk).ok_or(UnresolvedSeal::Spk(spk))?;
255                        let seal = PrefabSeal { vout, noise: Some(seal.wout.noise()) };
256                        EitherSeal::Alt(seal)
257                    }
258                    EitherSeal::Alt(None) => {
259                        let change = change.ok_or(UnresolvedSeal::Change)?;
260                        let seal = PrefabSeal { vout: change, noise: None };
261                        EitherSeal::Alt(seal)
262                    }
263                    EitherSeal::Token(auth) => EitherSeal::Token(auth),
264                };
265                owned.push(NamedState {
266                    name: assignment.name,
267                    state: Assignment { seal, data: assignment.state.data },
268                });
269            }
270            items.push(OpRequest {
271                contract_id: request.contract_id,
272                method: request.method,
273                reading: request.reading,
274                using: request.using,
275                global: request.global,
276                owned,
277            });
278        }
279        Ok(OpRequestSet(NonEmptyVec::from_iter_checked(items)))
280    }
281}
282
283#[derive(Clone, PartialEq, Eq, Debug, Display, Error)]
284#[display(doc_comments)]
285pub enum UnresolvedSeal {
286    /// unable to resolve seal witness output seal definition for script pubkey {0:x}.
287    Spk(ScriptPubkey),
288
289    /// seal requires assignment to a change output, but the transaction lacks change.
290    Change,
291}
292
293/// Request to construct RGB operations.
294///
295/// NB: [`OpRequest`] must contain pre-computed information about the change; otherwise the
296/// excessive state will be lost. Change information allows wallet to construct complex transactions
297/// with multiple changes etc. Use [`OpRequest::check`] method to verify that request includes
298/// necessary change.
299///
300/// Differs from [`CallParams`] in the fact that it uses [`EitherSeal`]s instead of
301/// [`hypersonic::AuthTokens`] for output definitions.
302#[derive(Clone, Debug)]
303#[cfg_attr(
304    feature = "serde",
305    derive(Serialize, Deserialize),
306    serde(
307        rename_all = "camelCase",
308        bound = "T: serde::Serialize + for<'d> serde::Deserialize<'d>"
309    )
310)]
311pub struct OpRequest<T> {
312    pub contract_id: ContractId,
313    pub method: MethodName,
314    pub reading: Vec<CellAddr>,
315    pub using: Vec<UsedState>,
316    pub global: Vec<NamedState<StateAtom>>,
317    pub owned: Vec<NamedState<Assignment<EitherSeal<T>>>>,
318}
319
320impl OpRequest<Option<WoutAssignment>> {
321    pub fn resolve_seal(
322        &self,
323        wout: WitnessOut,
324        resolver: impl Fn(&ScriptPubkey) -> Option<Vout>,
325    ) -> Option<WTxoSeal> {
326        for assignment in &self.owned {
327            if let EitherSeal::Alt(Some(assignment)) = &assignment.state.seal {
328                if assignment.wout == wout {
329                    let spk = assignment.script_pubkey();
330                    let vout = resolver(&spk)?;
331                    let primary = WOutpoint::Wout(vout);
332                    let seal = WTxoSeal { primary, secondary: TxoSealExt::Noise(wout.noise()) };
333                    return Some(seal);
334                }
335            }
336        }
337        None
338    }
339}
340
341#[derive(Clone, PartialEq, Eq, Debug, Display, Error, From)]
342#[display(doc_comments)]
343pub enum UnmatchedState {
344    /// neither invoice nor contract API contains information about the state name.
345    #[from(StateUnknown)]
346    StateNameUnknown,
347
348    #[from]
349    #[display(inner)]
350    StateCalc(StateCalcError),
351
352    /// the operation request doesn't re-assign all of `{0}` state, leading to the state loss.
353    NotEnoughChange(StateName),
354}
355
356/// Prefabricated operation, which includes information on the contract id and closed seals
357/// (previous outputs).
358#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
359#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
360#[strict_type(lib = LIB_NAME_RGB)]
361#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))]
362pub struct Prefab {
363    pub closes: SmallOrdSet<Outpoint>,
364    pub defines: SmallOrdSet<Vout>,
365    pub operation: Operation,
366}
367
368/// A bundle of prefabricated operations related to the same witness transaction.
369///
370/// The pack should cover all contracts assigning state to the witness transaction previous outputs.
371/// It is used to add seal closing commitment to the witness transaction PSBT.
372#[derive(Wrapper, WrapperMut, Clone, Eq, PartialEq, Debug, Default, From)]
373#[wrapper(Deref)]
374#[wrapper_mut(DerefMut)]
375#[derive(StrictType, StrictEncode, StrictDecode)]
376#[strict_type(lib = LIB_NAME_RGB)]
377#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(transparent))]
378pub struct PrefabBundle(SmallOrdSet<Prefab>);
379
380impl IntoIterator for PrefabBundle {
381    type Item = Prefab;
382    type IntoIter = btree_set::IntoIter<Prefab>;
383
384    fn into_iter(self) -> Self::IntoIter { self.0.into_iter() }
385}
386
387impl<'a> IntoIterator for &'a PrefabBundle {
388    type Item = &'a Prefab;
389    type IntoIter = btree_set::Iter<'a, Prefab>;
390
391    fn into_iter(self) -> Self::IntoIter { self.0.iter() }
392}
393
394impl PrefabBundle {
395    pub fn new(items: impl IntoIterator<Item = Prefab>) -> Result<Self, confinement::Error> {
396        let items = SmallOrdSet::try_from_iter(items.into_iter())?;
397        Ok(Self(items))
398    }
399
400    pub fn closes(&self) -> impl Iterator<Item = Outpoint> + use<'_> {
401        self.0.iter().flat_map(|item| item.closes.iter().copied())
402    }
403
404    pub fn defines(&self) -> impl Iterator<Item = Vout> + use<'_> {
405        self.0.iter().flat_map(|item| item.defines.iter().copied())
406    }
407}
408
409/// RGB wallet contains a bunch of RGB contracts, which are held by a single owner (a wallet);
410/// such that when a new operation under any of the contracts happens, it may affect other contracts
411/// sharing the same UTXOs.
412pub struct RgbWallet<
413    W,
414    Sp,
415    // TODO: Replace with IndexMap
416    S = HashMap<CodexId, Issuer>,
417    C = HashMap<ContractId, Contract<<Sp as Stockpile>::Stock, <Sp as Stockpile>::Pile>>,
418> where
419    W: WalletProvider,
420    Sp: Stockpile,
421    Sp::Pile: Pile<Seal = TxoSeal>,
422    S: KeyedCollection<Key = CodexId, Value = Issuer>,
423    C: KeyedCollection<Key = ContractId, Value = Contract<Sp::Stock, Sp::Pile>>,
424{
425    pub wallet: W,
426    pub contracts: Contracts<Sp, S, C>,
427}
428
429impl<W, Sp, S, C> RgbWallet<W, Sp, S, C>
430where
431    W: WalletProvider,
432    Sp: Stockpile,
433    Sp::Pile: Pile<Seal = TxoSeal>,
434    S: KeyedCollection<Key = CodexId, Value = Issuer>,
435    C: KeyedCollection<Key = ContractId, Value = Contract<Sp::Stock, Sp::Pile>>,
436{
437    pub fn with_components(wallet: W, contracts: Contracts<Sp, S, C>) -> Self {
438        Self { wallet, contracts }
439    }
440
441    pub fn into_components(self) -> (W, Contracts<Sp, S, C>) { (self.wallet, self.contracts) }
442
443    pub fn switch_wallet(&mut self, new: W) -> W { mem::replace(&mut self.wallet, new) }
444
445    pub fn issue(
446        &mut self,
447        params: CreateParams<Outpoint>,
448    ) -> Result<
449        ContractId,
450        MultiError<IssuerError, <Sp::Stock as Stock>::Error, <Sp::Pile as Pile>::Error>,
451    > {
452        self.contracts.issue(params.transform(self.noise_engine()))
453    }
454
455    pub fn auth_token(&mut self, nonce: Option<u64>) -> Option<AuthToken> {
456        let outpoint = self.wallet.utxos().next()?;
457        let nonce = nonce.unwrap_or_else(|| self.wallet.next_nonce());
458        let seal = WTxoSeal::no_fallback(outpoint, self.noise_engine(), nonce);
459        let auth = seal.auth_token();
460        self.wallet.register_seal(seal);
461        Some(auth)
462    }
463
464    pub fn wout(&mut self, nonce: Option<u64>) -> WitnessOut {
465        let address = self.wallet.next_address();
466        let nonce = nonce.unwrap_or_else(|| self.wallet.next_nonce());
467        WitnessOut::new(address.payload, nonce)
468    }
469
470    pub fn wallet_state(&self) -> WalletState<TxoSeal> {
471        let iter = self
472            .contracts
473            .contract_ids()
474            .map(|id| (id, self.contracts.contract_state(id)));
475        WalletState::from_contracts_state(iter)
476    }
477
478    pub fn wallet_contract_state(&self, contract_id: ContractId) -> ContractState<Outpoint> {
479        self.contracts
480            .contract_state(contract_id)
481            .clone()
482            .filter_map(
483                |seal| {
484                    if self.wallet.has_utxo(seal.primary) {
485                        Some(seal.primary)
486                    } else {
487                        None
488                    }
489                },
490            )
491    }
492
493    pub fn contract_state_full(
494        &self,
495        contract_id: ContractId,
496    ) -> ContractState<<Sp::Pile as Pile>::Seal> {
497        self.contracts.contract_state(contract_id)
498    }
499
500    fn noise_engine(&self) -> Sha256 {
501        let noise_seed = self.wallet.noise_seed();
502        let mut noise_engine = Sha256::new();
503        noise_engine.input_raw(noise_seed.as_ref());
504        noise_engine
505    }
506
507    pub fn fulfill(
508        &mut self,
509        invoice: &RgbInvoice<ContractId>,
510        mut coinselect: impl Coinselect,
511        giveaway: Option<Sats>,
512    ) -> Result<OpRequest<Option<WoutAssignment>>, FulfillError> {
513        let contract_id = invoice.scope;
514
515        // Determine method
516        let articles = self.contracts.contract_articles(contract_id);
517        let api = articles.default_api();
518        let call = invoice
519            .call
520            .as_ref()
521            .or(api.default_call.as_ref())
522            .ok_or(FulfillError::CallStateUnknown)?;
523        let method = call.method.clone();
524        let state_name = call.owned.clone().ok_or(FulfillError::StateNameUnknown)?;
525        let mut calc = api.calculate(state_name.clone())?;
526
527        let value = invoice.data.as_ref().ok_or(FulfillError::ValueMissed)?;
528
529        // Do coinselection
530        let state = self.wallet_contract_state(contract_id);
531        let state = state
532            .owned
533            .get(&state_name)
534            .ok_or(FulfillError::StateUnavailable)?;
535        // NB: we do state accumulation with `calc` inside coinselect
536        let mut using = coinselect
537            .coinselect(value, &mut calc, state)
538            .ok_or(FulfillError::StateInsufficient)?;
539        // Now we need to include all other allocations under the same contract that use the
540        // selected UTXOs.
541        let (addrs, outpoints) = using
542            .iter()
543            .copied()
544            .unzip::<_, _, BTreeSet<_>, BTreeSet<_>>();
545        using.extend(
546            state
547                .iter()
548                .filter(|s| outpoints.contains(&s.assignment.seal) && !addrs.contains(&s.addr))
549                .filter_map(|s| {
550                    calc.accumulate(&s.assignment.data).ok()?;
551                    Some((s.addr, s.assignment.seal))
552                }),
553        );
554        let using = using
555            .into_iter()
556            .map(|(addr, outpoint)| UsedState { addr, outpoint, satisfaction: None })
557            .collect();
558
559        // Add beneficiaries
560        let seal = match invoice.auth {
561            RgbBeneficiary::Token(auth) => EitherSeal::Token(auth),
562            RgbBeneficiary::WitnessOut(wout) => {
563                let wout = WoutAssignment {
564                    wout,
565                    sats: giveaway.ok_or(FulfillError::WoutRequiresGiveaway)?,
566                };
567                EitherSeal::Alt(Some(wout))
568            }
569        };
570        calc.lessen(value)?;
571        let assignment = Assignment { seal, data: value.clone() };
572        let state = NamedState { name: state_name.clone(), state: assignment };
573        let mut owned = vec![state];
574
575        // Add change
576        let diff = calc.diff()?;
577        let seal = EitherSeal::Alt(None);
578        for data in diff {
579            let assignment = Assignment { seal: seal.clone(), data };
580            let state = NamedState { name: state_name.clone(), state: assignment };
581            owned.push(state);
582        }
583
584        // Construct operation request
585        Ok(OpRequest {
586            contract_id,
587            method,
588            reading: none!(),
589            global: none!(),
590            using,
591            owned,
592        })
593    }
594
595    /// Check whether all state used in a request is properly re-distributed to new owners, and
596    /// non-distributed state is used in the change.
597    pub fn check_request<T>(&self, request: &OpRequest<T>) -> Result<(), UnmatchedState> {
598        let contract_id = request.contract_id;
599        let state = self.contracts.contract_state(contract_id);
600        let articles = self.contracts.contract_articles(contract_id);
601        let api = articles.default_api();
602        let mut calcs = BTreeMap::new();
603
604        for inp in &request.using {
605            let (state_name, val) = state
606                .owned
607                .iter()
608                .find_map(|(state_name, map)| {
609                    map.iter()
610                        .find(|owned| owned.addr == inp.addr)
611                        .map(|owned| (state_name, owned))
612                })
613                .expect("unknown state included in the contract stock");
614            let calc = match calcs.entry(state_name.clone()) {
615                Entry::Vacant(entry) => {
616                    let calc = api.calculate(state_name.clone())?;
617                    entry.insert(calc)
618                }
619                Entry::Occupied(entry) => entry.into_mut(),
620            };
621            calc.accumulate(&val.assignment.data)?;
622        }
623        for out in &request.owned {
624            let calc = match calcs.entry(out.name.clone()) {
625                Entry::Vacant(entry) => {
626                    let calc = api.calculate(out.name.clone())?;
627                    entry.insert(calc)
628                }
629                Entry::Occupied(entry) => entry.into_mut(),
630            };
631            calc.lessen(&out.state.data)?;
632        }
633        for (state_name, calc) in calcs {
634            if !calc.diff()?.is_empty() {
635                return Err(UnmatchedState::NotEnoughChange(state_name.clone()));
636            }
637        }
638        Ok(())
639    }
640
641    /// Creates a single operation basing on the provided construction parameters.
642    pub fn prefab(
643        &mut self,
644        request: OpRequest<PrefabSeal>,
645    ) -> Result<Prefab, MultiError<PrefabError, <Sp::Stock as Stock>::Error>> {
646        self.check_request(&request).map_err(MultiError::from_a)?;
647
648        // convert ConstructParams into CallParams
649        let (closes, using) = request
650            .using
651            .into_iter()
652            .map(|used| (used.outpoint, (used.addr, used.satisfaction)))
653            .unzip();
654        let closes =
655            SmallOrdSet::try_from(closes).map_err(|_| MultiError::A(PrefabError::TooManyInputs))?;
656        let mut defines = SmallOrdSet::new();
657
658        let mut seals = SmallOrdMap::new();
659        let mut noise_engine = self.noise_engine();
660        noise_engine.input_raw(request.contract_id.as_slice());
661
662        let mut owned = Vec::with_capacity(request.owned.len());
663        for (opout_no, assignment) in request.owned.into_iter().enumerate() {
664            let auth = match assignment.state.seal {
665                EitherSeal::Alt(seal) => {
666                    defines
667                        .push(seal.vout)
668                        .map_err(|_| MultiError::A(PrefabError::TooManyOutputs))?;
669                    let primary = WOutpoint::Wout(seal.vout);
670                    let noise = seal.noise.unwrap_or_else(|| {
671                        Noise::with(primary, noise_engine.clone(), opout_no as u64)
672                    });
673                    let seal = WTxoSeal { primary, secondary: TxoSealExt::Noise(noise) };
674                    seals.insert(opout_no as u16, seal).expect("checked above");
675                    seal.auth_token()
676                }
677                EitherSeal::Token(auth) => auth,
678            };
679            let state = DataCell { data: assignment.state.data, auth, lock: None };
680            let named_state = NamedState { name: assignment.name, state };
681            owned.push(named_state);
682        }
683
684        let call = CallParams {
685            core: CoreParams { method: request.method, global: request.global, owned },
686            using,
687            reading: request.reading,
688        };
689
690        let operation = self
691            .contracts
692            .contract_call(request.contract_id, call, seals)
693            .map_err(MultiError::from_other_a)?;
694
695        Ok(Prefab { closes, defines, operation })
696    }
697
698    /// Complete creation of a prefabricated operation bundle from operation requests, adding blank
699    /// operations if necessary. Operation requests can be multiple.
700    ///
701    /// A set of operations is either a collection of them or an [`OpRequestSet`] - any structure
702    /// that implements the [`IntoIterator`] trait.
703    ///
704    /// # Arguments
705    ///
706    /// - `requests`: a set of instructions to create non-blank operations (potentially under
707    ///   multiple contracts);
708    /// - `seal`: a single-use seal definition where all blank outputs will be assigned to.
709    pub fn bundle(
710        &mut self,
711        requests: impl IntoIterator<Item = OpRequest<PrefabSeal>>,
712        change: Option<Vout>,
713    ) -> Result<PrefabBundle, MultiError<BundleError, <Sp::Stock as Stock>::Error>> {
714        let ops = requests.into_iter().map(|params| self.prefab(params));
715
716        let mut outpoints = BTreeSet::<Outpoint>::new();
717        let mut contracts = BTreeSet::new();
718        let mut prefabs = BTreeSet::new();
719        for prefab in ops {
720            let prefab = prefab.map_err(MultiError::from_other_a)?;
721            contracts.insert(prefab.operation.contract_id);
722            outpoints.extend(&prefab.closes);
723            prefabs.insert(prefab);
724        }
725
726        // Constructing blank operation requests
727        let mut blank_requests = Vec::new();
728        let root_noise_engine = self.noise_engine();
729        for contract_id in self.contracts.contract_ids() {
730            if contracts.contains(&contract_id) {
731                continue;
732            }
733            // We need to clone here not to conflict with mutable calls below
734            let owned = self.contracts.contract_state(contract_id).owned.clone();
735            let (using, prev): (Vec<_>, Vec<_>) = owned
736                .iter()
737                .flat_map(|(name, map)| map.iter().map(move |owned| (name, owned)))
738                .filter_map(|(name, owned)| {
739                    let outpoint = owned.assignment.seal.primary;
740                    if !outpoints.contains(&outpoint) {
741                        return None;
742                    }
743                    let prevout = UsedState { addr: owned.addr, outpoint, satisfaction: None };
744                    Some((prevout, (name.clone(), owned)))
745                })
746                .unzip();
747
748            if using.is_empty() {
749                continue;
750            };
751
752            let articles = self.contracts.contract_articles(contract_id);
753            let api = articles.default_api();
754            let mut calcs = BTreeMap::<StateName, StateCalc>::new();
755            for (name, state) in prev {
756                let calc = match calcs.entry(name.clone()) {
757                    Entry::Vacant(entry) => {
758                        let calc = api.calculate(name).map_err(MultiError::from_a)?;
759                        entry.insert(calc)
760                    }
761                    Entry::Occupied(entry) => entry.into_mut(),
762                };
763                calc.accumulate(&state.assignment.data)
764                    .map_err(MultiError::from_a)?;
765            }
766
767            let mut owned = Vec::new();
768            let mut nonce = 0;
769            let mut noise_engine = root_noise_engine.clone();
770            noise_engine.input_raw(contract_id.as_slice());
771            for (name, calc) in calcs {
772                for data in calc.diff().map_err(MultiError::from_a)? {
773                    let vout = change.ok_or(MultiError::A(BundleError::ChangeRequired))?;
774                    let noise =
775                        Some(Noise::with(WOutpoint::Wout(vout), noise_engine.clone(), nonce));
776                    let change = PrefabSeal { vout, noise };
777                    nonce += 1;
778                    let state = NamedState {
779                        name: name.clone(),
780                        state: Assignment { seal: EitherSeal::Alt(change), data },
781                    };
782                    owned.push(state);
783                }
784            }
785
786            let params = OpRequest {
787                contract_id,
788                method: MethodName::from(BP_BLANK_METHOD),
789                global: none!(),
790                reading: none!(),
791                using,
792                owned,
793            };
794            blank_requests.push(params);
795        }
796
797        for request in blank_requests {
798            let prefab = self.prefab(request).map_err(|err| match err {
799                MultiError::A(e) => MultiError::A(BundleError::Blank(e)),
800                MultiError::B(e) => MultiError::B(e),
801                MultiError::C(_) => unreachable!(),
802            })?;
803            prefabs.push(prefab);
804        }
805
806        Ok(PrefabBundle(
807            SmallOrdSet::try_from(prefabs)
808                .map_err(|_| MultiError::A(BundleError::TooManyBlanks))?,
809        ))
810    }
811
812    /// Include a prefab bundle, creating the necessary anchors on the fly.
813    pub fn include(
814        &mut self,
815        bundle: &PrefabBundle,
816        witness: &Tx,
817        mpc: mpc::MerkleBlock,
818        dbc: Option<TapretProof>,
819        prevouts: &[Outpoint],
820    ) -> Result<(), IncludeError> {
821        for prefab in bundle {
822            let protocol_id = ProtocolId::from(prefab.operation.contract_id.to_byte_array());
823            let opid = prefab.operation.opid();
824            let mut map = bmap! {};
825            for prevout in &prefab.closes {
826                let pos = prevouts
827                    .iter()
828                    .position(|p| p == prevout)
829                    .ok_or(IncludeError::MissingPrevout(*prevout))?;
830                map.insert(pos as u32, mmb::Message::from_byte_array(opid.to_byte_array()));
831            }
832            let anchor = Anchor {
833                mmb_proof: mmb::BundleProof { map: SmallOrdMap::from_checked(map) },
834                mpc_protocol: protocol_id,
835                mpc_proof: mpc.to_merkle_proof(protocol_id)?,
836                dbc_proof: dbc.clone(),
837                fallback_proof: default!(),
838            };
839            self.contracts
840                .include(prefab.operation.contract_id, opid, witness, anchor);
841        }
842        Ok(())
843    }
844
845    /// Consume consignment.
846    ///
847    /// The method:
848    /// - validates the consignment;
849    /// - resolves auth tokens into seal definitions known to the current wallet (i.e., coming from
850    ///   the invoices produced by the wallet);
851    /// - checks the signature of the issuer over the contract articles;
852    ///
853    /// # Arguments
854    ///
855    /// - `allow_unknown`: allows importing a contract which was not known to the system;
856    /// - `reader`: the input stream;
857    /// - `sig_validator`: a validator for the signature of the issuer over the contract articles.
858    #[allow(clippy::result_large_err)]
859    pub fn consume<E>(
860        &mut self,
861        allow_unknown: bool,
862        reader: &mut StrictReader<impl ReadRaw>,
863        sig_validator: impl FnOnce(StrictHash, &Identity, &SigBlob) -> Result<(), E>,
864    ) -> Result<
865        (),
866        MultiError<ConsumeError<WTxoSeal>, <Sp::Stock as Stock>::Error, <Sp::Pile as Pile>::Error>,
867    >
868    where
869        <Sp::Pile as Pile>::Conf: From<<Sp::Stock as Stock>::Conf>,
870    {
871        let seal_resolver = |op: &Operation| {
872            self.wallet
873                .resolve_seals(op.destructible_out.iter().map(|cell| cell.auth))
874                .map(|seal| {
875                    let auth = seal.auth_token();
876                    let op_out =
877                        op.destructible_out
878                            .iter()
879                            .position(|cell| cell.auth == auth)
880                            .expect("invalid wallet implementation") as u16;
881                    (op_out, seal)
882                })
883                .collect()
884        };
885        self.contracts
886            .consume(allow_unknown, reader, seal_resolver, sig_validator)
887    }
888
889    #[cfg(not(feature = "async"))]
890    /// Update a wallet UTXO set and the status of all witnesses and single-use seal
891    /// definitions.
892    ///
893    /// Applies rollbacks or forwards if required and recomputes the state of the affected
894    /// contracts.
895    pub fn update(
896        &mut self,
897        min_conformations: u32,
898    ) -> Result<(), MultiError<SyncError<W::Error>, <Sp::Stock as Stock>::Error>> {
899        self.wallet
900            .update_utxos()
901            .map_err(SyncError::Wallet)
902            .map_err(MultiError::from_a)?;
903        let last_height = self
904            .wallet
905            .last_block_height()
906            .map_err(SyncError::Wallet)
907            .map_err(MultiError::from_a)?;
908        self.contracts
909            .update_witnesses(self.wallet.txid_resolver(), last_height, min_conformations)
910            .map_err(MultiError::from_other_a)
911    }
912
913    #[cfg(feature = "async")]
914    /// Update a wallet UTXO set and the status of all witnesses and single-use seal
915    /// definitions.
916    ///
917    /// Applies rollbacks or forwards if required and recomputes the state of the affected
918    /// contracts.
919    pub async fn update_async(
920        &mut self,
921        min_conformations: u32,
922    ) -> Result<(), MultiError<SyncError<W::Error>, <Sp::Stock as Stock>::Error>>
923    where
924        Sp::Stock: 'static,
925        Sp::Pile: 'static,
926    {
927        self.wallet
928            .update_utxos_async()
929            .await
930            .map_err(SyncError::Wallet)
931            .map_err(MultiError::from_a)?;
932        let last_height = self
933            .wallet
934            .last_block_height_async()
935            .await
936            .map_err(SyncError::Wallet)
937            .map_err(MultiError::from_a)?;
938        self.contracts
939            .update_witnesses_async(
940                self.wallet.txid_resolver_async(),
941                last_height,
942                min_conformations,
943            )
944            .await
945            .map_err(MultiError::from_other_a)
946    }
947}
948
949impl CreateParams<Outpoint> {
950    pub fn new_bitcoin_testnet(codex_id: CodexId, name: impl Into<TypeName>) -> Self {
951        Self::new_testnet(codex_id, Consensus::Bitcoin, name)
952    }
953}
954
955#[derive(Debug, Display, Error, From)]
956#[display(doc_comments)]
957pub enum PrefabError {
958    /// operation request contains too many inputs (maximum number of inputs is 64k).
959    TooManyInputs,
960
961    /// operation request contains too many outputs (maximum number of outputs is 64k).
962    TooManyOutputs,
963
964    #[from]
965    #[display(inner)]
966    UnmatchedState(UnmatchedState),
967
968    #[from]
969    #[display(inner)]
970    Accept(AcceptError),
971}
972
973#[derive(Debug, Display, Error, From)]
974#[display(doc_comments)]
975pub enum BundleError {
976    #[from]
977    #[display(inner)]
978    Prefab(PrefabError),
979
980    /// blank {0}
981    Blank(PrefabError),
982
983    /// the requested set of operations requires creation of blank operations for other contracts,
984    /// which in turn require transaction to contain a change output.
985    ChangeRequired,
986
987    /// neither invoice nor contract API contains information about the state name.
988    #[from(StateUnknown)]
989    StateNameUnknown,
990
991    #[from]
992    #[display(inner)]
993    StateCalc(StateCalcError),
994
995    /// one or multiple outputs used in operation requests contain too many contracts; it is
996    /// impossible to create a bundle with more than 64k of operations.
997    TooManyBlanks,
998}
999
1000#[derive(Copy, Clone, Eq, PartialEq, Debug, Display, Error, From)]
1001#[display(doc_comments)]
1002pub enum FulfillError {
1003    /// neither invoice nor contract API contains information about the transfer method.
1004    CallStateUnknown,
1005
1006    /// neither invoice nor contract API contains information about the state name.
1007    #[from(StateUnknown)]
1008    StateNameUnknown,
1009
1010    /// the wallet doesn't own any state to fulfill the invoice.
1011    StateUnavailable,
1012
1013    /// the state owned by the wallet is not enough to fulfill the invoice.
1014    StateInsufficient,
1015
1016    #[from]
1017    #[display(inner)]
1018    StateCalc(StateCalcError),
1019
1020    /// the invoice asks to create an UTXO for the receiver, but method call doesn't provide
1021    /// information on how many sats can be put there (`giveaway` argument in `Barrow::fulfill`
1022    /// call must not be set to None).
1023    WoutRequiresGiveaway,
1024
1025    /// the invoice misses the value, and the method call also doesn't provide one
1026    ValueMissed,
1027}
1028
1029#[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)]
1030#[display(doc_comments)]
1031pub enum IncludeError {
1032    /// prefab bundle references unknown previous output {0}.
1033    MissingPrevout(Outpoint),
1034
1035    /// multi-protocol commitment proof is invalid; {0}
1036    #[from]
1037    Mpc(mpc::LeafNotKnown),
1038}
1039
1040#[cfg(feature = "binfile")]
1041mod _fs {
1042    use std::io;
1043    use std::path::Path;
1044
1045    use amplify::confinement::U24 as U24MAX;
1046    use binfile::BinFile;
1047    use commit_verify::StrictHash;
1048    use strict_encoding::{DecodeError, StreamReader, StreamWriter, StrictEncode};
1049
1050    use super::*;
1051    use crate::{Identity, SigBlob, CONSIGN_MAGIC_NUMBER, CONSIGN_VERSION};
1052
1053    /// The magic number used in storing issuer as a binary file.
1054    pub const PREFAB_MAGIC_NUMBER: u64 = u64::from_be_bytes(*b"PREFABND");
1055    /// The issuer encoding version used in storing issuer as a binary file.
1056    pub const PREFAB_VERSION: u16 = 0;
1057
1058    impl<W, Sp, S, C> RgbWallet<W, Sp, S, C>
1059    where
1060        W: WalletProvider,
1061        Sp: Stockpile,
1062        Sp::Pile: Pile<Seal = TxoSeal>,
1063        S: KeyedCollection<Key = CodexId, Value = Issuer>,
1064        C: KeyedCollection<Key = ContractId, Value = Contract<Sp::Stock, Sp::Pile>>,
1065    {
1066        #[allow(clippy::result_large_err)]
1067        pub fn consume_from_file<E>(
1068            &mut self,
1069            allow_unknown: bool,
1070            path: impl AsRef<Path>,
1071            sig_validator: impl FnOnce(StrictHash, &Identity, &SigBlob) -> Result<(), E>,
1072        ) -> Result<
1073            (),
1074            MultiError<
1075                ConsumeError<WTxoSeal>,
1076                <Sp::Stock as Stock>::Error,
1077                <Sp::Pile as Pile>::Error,
1078            >,
1079        >
1080        where
1081            <Sp::Pile as Pile>::Conf: From<<Sp::Stock as Stock>::Conf>,
1082        {
1083            let file = BinFile::<CONSIGN_MAGIC_NUMBER, CONSIGN_VERSION>::open(path)
1084                .map_err(MultiError::from_a)?;
1085            let mut reader = StrictReader::with(StreamReader::new::<{ usize::MAX }>(file));
1086            self.consume(allow_unknown, &mut reader, sig_validator)
1087        }
1088    }
1089
1090    impl PrefabBundle {
1091        pub fn load(path: impl AsRef<Path>) -> Result<Self, DecodeError> {
1092            let file = BinFile::<PREFAB_MAGIC_NUMBER, PREFAB_VERSION>::open(path)?;
1093            let reader = StreamReader::new::<U24MAX>(file);
1094            Self::strict_read(reader)
1095            // We do not check for the end of file to allow backwards-compatible extensions
1096        }
1097
1098        pub fn save(&self, path: impl AsRef<Path>) -> io::Result<()> {
1099            let file = BinFile::<PREFAB_MAGIC_NUMBER, PREFAB_VERSION>::create_new(path)?;
1100            let writer = StreamWriter::new::<U24MAX>(file);
1101            self.strict_write(writer)
1102        }
1103    }
1104}