rgbp/
runtime.rs

1// Wallet 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 core::ops::{Deref, DerefMut};
26use std::collections::{BTreeSet, HashMap};
27
28use amplify::confinement::KeyedCollection;
29use amplify::MultiError;
30use bpstd::psbt::{
31    Beneficiary, ConstructionError, DbcPsbtError, PsbtConstructor, PsbtMeta, TxParams,
32    UnfinalizedInputs,
33};
34use bpstd::seals::TxoSeal;
35use bpstd::{Address, IdxBase, Psbt, Sats, Tx, Vout};
36use rgb::invoice::{RgbBeneficiary, RgbInvoice};
37use rgb::popls::bp::{
38    BundleError, Coinselect, FulfillError, IncludeError, OpRequestSet, PaymentScript, PrefabBundle,
39    RgbWallet, WalletProvider,
40};
41use rgb::{
42    AuthToken, CodexId, Contract, ContractId, Contracts, EitherSeal, Issuer, Pile, RgbSealDef,
43    Stock, Stockpile,
44};
45use rgpsbt::{RgbPsbt, RgbPsbtCsvError, RgbPsbtPrepareError, ScriptResolver};
46
47use crate::CoinselectStrategy;
48
49/// Payment structure is used in the process of RBF (replace by fee). It is returned by
50/// [`RgbRuntime::pay_invoice`] method when the original transaction is created, and then must be
51/// provided to [`RgbRuntime::rbf`] to do the RBF transaction.
52#[derive(Clone, Eq, PartialEq, Debug)]
53// TODO: Add Deserialize once implemented in Psbt
54//#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
55pub struct Payment {
56    pub uncomit_psbt: Psbt,
57    pub psbt_meta: PsbtMeta,
58    pub bundle: PrefabBundle,
59    pub terminals: BTreeSet<AuthToken>,
60}
61
62/// RGB Runtime is a lightweight stateless layer integrating some wallet provider (`Wallet` generic
63/// parameter) and RGB stockpile (`Sp` generic parameter).
64///
65/// It provides
66/// - synchronization for the history of witness transactions, extending the main wallet UTXO set
67///   synchronization ([`Self::sync`]);
68/// - low-level methods for working with PSBTs using `bp-std` library (these methods utilize
69///   [`rgb-psbt`] crate) - like [`Self::compose_psbt`] and [`Self::color_psbt`];
70/// - high-level payment methods ([`Self::pay`], [`Self::rbf`]) relaying on the above.
71pub struct RgbRuntime<
72    W,
73    Sp,
74    S = HashMap<CodexId, Issuer>,
75    C = HashMap<ContractId, Contract<<Sp as Stockpile>::Stock, <Sp as Stockpile>::Pile>>,
76>(RgbWallet<W, Sp, S, C>)
77where
78    W: WalletProvider,
79    Sp: Stockpile,
80    Sp::Pile: Pile<Seal = TxoSeal>,
81    S: KeyedCollection<Key = CodexId, Value = Issuer>,
82    C: KeyedCollection<Key = ContractId, Value = Contract<Sp::Stock, Sp::Pile>>;
83
84impl<W, Sp, S, C> From<RgbWallet<W, Sp, S, C>> for RgbRuntime<W, Sp, S, C>
85where
86    W: WalletProvider,
87    Sp: Stockpile,
88    Sp::Pile: Pile<Seal = TxoSeal>,
89    S: KeyedCollection<Key = CodexId, Value = Issuer>,
90    C: KeyedCollection<Key = ContractId, Value = Contract<Sp::Stock, Sp::Pile>>,
91{
92    fn from(wallet: RgbWallet<W, Sp, S, C>) -> Self { Self(wallet) }
93}
94
95impl<W, Sp, S, C> Deref for RgbRuntime<W, Sp, S, C>
96where
97    W: WalletProvider,
98    Sp: Stockpile,
99    Sp::Pile: Pile<Seal = TxoSeal>,
100    S: KeyedCollection<Key = CodexId, Value = Issuer>,
101    C: KeyedCollection<Key = ContractId, Value = Contract<Sp::Stock, Sp::Pile>>,
102{
103    type Target = RgbWallet<W, Sp, S, C>;
104    fn deref(&self) -> &Self::Target { &self.0 }
105}
106impl<W, Sp, S, C> DerefMut for RgbRuntime<W, Sp, S, C>
107where
108    W: WalletProvider,
109    Sp: Stockpile,
110    Sp::Pile: Pile<Seal = TxoSeal>,
111    S: KeyedCollection<Key = CodexId, Value = Issuer>,
112    C: KeyedCollection<Key = ContractId, Value = Contract<Sp::Stock, Sp::Pile>>,
113{
114    fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
115}
116
117impl<W, Sp, S, C> RgbRuntime<W, Sp, S, C>
118where
119    W: WalletProvider,
120    Sp: Stockpile,
121    Sp::Pile: Pile<Seal = TxoSeal>,
122    S: KeyedCollection<Key = CodexId, Value = Issuer>,
123    C: KeyedCollection<Key = ContractId, Value = Contract<Sp::Stock, Sp::Pile>>,
124{
125    pub fn with_components(wallet: W, contracts: Contracts<Sp, S, C>) -> Self {
126        Self(RgbWallet::with_components(wallet, contracts))
127    }
128    pub fn into_rgb_wallet(self) -> RgbWallet<W, Sp, S, C> { self.0 }
129    pub fn into_components(self) -> (W, Contracts<Sp, S, C>) { self.0.into_components() }
130}
131
132impl<W, Sp, S, C> RgbRuntime<W, Sp, S, C>
133where
134    W: PsbtConstructor + WalletProvider,
135    Sp: Stockpile,
136    Sp::Pile: Pile<Seal = TxoSeal>,
137    S: KeyedCollection<Key = CodexId, Value = Issuer>,
138    C: KeyedCollection<Key = ContractId, Value = Contract<Sp::Stock, Sp::Pile>>,
139{
140    /// Pay an invoice producing PSBT ready to be signed.
141    ///
142    /// Should not be used in multi-party protocols like coinjoins, when a PSBT may need to be
143    /// modified in the number of inputs or outputs. Use the `construct_psbt` method for such
144    /// scenarios.
145    ///
146    /// If you need more flexibility in constructing payments (do multiple payments with multiple
147    /// contracts, use global state etc.) in a single PSBT, please use `pay_custom` APIs and
148    /// [`PrefabBundleSet`] instead of this simplified API.
149    #[allow(clippy::type_complexity)]
150    pub fn pay_invoice(
151        &mut self,
152        invoice: &RgbInvoice<ContractId>,
153        strategy: impl Coinselect,
154        params: TxParams,
155        giveaway: Option<Sats>,
156    ) -> Result<(Psbt, Payment), MultiError<PayError, <Sp::Stock as Stock>::Error>> {
157        let request = self
158            .fulfill(invoice, strategy, giveaway)
159            .map_err(MultiError::from_a)?;
160        let script = OpRequestSet::with(request.clone());
161        let (psbt, mut payment) = self
162            .transfer(script, params)
163            .map_err(MultiError::from_other_a)?;
164        let terminal = match invoice.auth {
165            RgbBeneficiary::Token(auth) => auth,
166            RgbBeneficiary::WitnessOut(wout) => request
167                .resolve_seal(wout, psbt.script_resolver())
168                .expect("witness out must be present in the PSBT")
169                .auth_token(),
170        };
171        payment.terminals.insert(terminal);
172        Ok((psbt, payment))
173    }
174
175    pub fn rbf(&mut self, payment: &Payment, fee: impl Into<Sats>) -> Result<Psbt, PayError> {
176        let mut psbt = payment.uncomit_psbt.clone();
177        let change = payment
178            .psbt_meta
179            .change
180            .expect("Can't RBF when no change is present");
181        let old_fee = psbt.fee().expect("Invalid PSBT with zero inputs");
182        let out = psbt
183            .output_mut(change.vout.into_usize())
184            .expect("invalid PSBT meta-information in the payment");
185        out.amount -= fee.into() - old_fee;
186
187        Ok(self.complete(psbt, &payment.bundle)?)
188    }
189
190    /// Convert invoice into a payment script.
191    pub fn script(
192        &mut self,
193        invoice: &RgbInvoice<ContractId>,
194        strategy: CoinselectStrategy,
195        giveaway: Option<Sats>,
196    ) -> Result<PaymentScript, PayError> {
197        let request = self.fulfill(invoice, strategy, giveaway)?;
198        Ok(OpRequestSet::with(request))
199    }
200
201    /// Construct transfer, consisting of PSBT and a consignment stream
202    // TODO: Return a dedicated Transfer object which can stream a consignment
203    #[allow(clippy::type_complexity)]
204    pub fn transfer(
205        &mut self,
206        script: PaymentScript,
207        params: TxParams,
208    ) -> Result<(Psbt, Payment), MultiError<TransferError, <Sp::Stock as Stock>::Error>> {
209        let payment = self.exec(script, params)?;
210        let psbt = self
211            .complete(payment.uncomit_psbt.clone(), &payment.bundle)
212            .map_err(MultiError::A)?;
213        Ok((psbt, payment))
214    }
215
216    pub fn compose_psbt(
217        &mut self,
218        bundle: &PaymentScript,
219        params: TxParams,
220    ) -> Result<(Psbt, PsbtMeta), ConstructionError> {
221        let closes = bundle
222            .iter()
223            .flat_map(|params| &params.using)
224            .map(|used| used.outpoint);
225
226        let network = self.wallet.network();
227        let beneficiaries = bundle
228            .iter()
229            .flat_map(|params| &params.owned)
230            .filter_map(|assignment| match &assignment.state.seal {
231                EitherSeal::Alt(seal) => seal.as_ref(),
232                EitherSeal::Token(_) => None,
233            })
234            .map(|seal| {
235                let address = Address::with(&seal.wout.script_pubkey(), network)
236                    .expect("script pubkey which is not representable as an address");
237                Beneficiary::new(address, seal.sats)
238            });
239        self.wallet.construct_psbt(closes, beneficiaries, params)
240    }
241
242    /// Fill in RGB information into a pre-composed PSBT, aligning it with the provided payment
243    /// script.
244    ///
245    /// This procedure internally calls [`RgbWallet::bundle`], ensuring all other RGB data (from
246    /// other contracts) which were assigned to the UTXOs spent by this RGB, are not lost and
247    /// re-assigned to the change output(s) of the PSBT.
248    pub fn color_psbt(
249        &mut self,
250        mut psbt: Psbt,
251        mut meta: PsbtMeta,
252        script: PaymentScript,
253    ) -> Result<Payment, MultiError<TransferError, <Sp::Stock as Stock>::Error>> {
254        // From this moment the transaction becomes unmodifiable
255        let mut change_vout = meta.change.map(|c| c.vout);
256        let request = psbt
257            .rgb_resolve(script, &mut change_vout)
258            .map_err(MultiError::from_a)?;
259        if let Some(c) = meta.change.as_mut() {
260            if let Some(vout) = change_vout {
261                c.vout = vout
262            }
263        }
264
265        let bundle = self
266            .bundle(request, meta.change.map(|c| c.vout))
267            .map_err(MultiError::from_other_a)?;
268
269        psbt.rgb_fill_csv(&bundle).map_err(MultiError::from_a)?;
270
271        Ok(Payment {
272            uncomit_psbt: psbt,
273            psbt_meta: meta,
274            bundle,
275            terminals: none!(),
276        })
277    }
278
279    /// Execute payment script creating PSBT and prefabricated operation bundle.
280    ///
281    /// The returned PSBT contains only anonymous client-side validation information and is
282    /// not modifiable, since it contains RGB data.
283    pub fn exec(
284        &mut self,
285        script: PaymentScript,
286        params: TxParams,
287    ) -> Result<Payment, MultiError<TransferError, <Sp::Stock as Stock>::Error>> {
288        let (psbt, meta) = self
289            .compose_psbt(&script, params)
290            .map_err(MultiError::from_a)?;
291        self.color_psbt(psbt, meta, script)
292    }
293
294    /// Completes PSBT and includes the prefabricated bundle into the contract.
295    pub fn complete(
296        &mut self,
297        mut psbt: Psbt,
298        bundle: &PrefabBundle,
299    ) -> Result<Psbt, TransferError> {
300        let (mpc, dbc) = psbt.dbc_commit()?;
301        let tx = psbt.to_unsigned_tx();
302
303        let prevouts = psbt
304            .inputs()
305            .map(|inp| inp.previous_outpoint)
306            .collect::<Vec<_>>();
307        self.include(bundle, &tx.into(), mpc, dbc, &prevouts)?;
308
309        Ok(psbt)
310    }
311
312    #[allow(clippy::type_complexity)]
313    fn finalize_inner(
314        &mut self,
315        mut psbt: Psbt,
316        meta: PsbtMeta,
317    ) -> Result<(Tx, Option<(Vout, u32, u32)>), FinalizeError<W::Error>> {
318        psbt.finalize(self.wallet.descriptor());
319        let tx = psbt.extract()?;
320        let change = meta.change.map(|change| {
321            (change.vout, change.terminal.keychain.index(), change.terminal.index.index())
322        });
323        Ok((tx, change))
324    }
325
326    #[cfg(not(feature = "async"))]
327    /// Finalizes PSBT, extracts the signed transaction, broadcasts it and updates wallet UTXO set
328    /// accordingly.
329    pub fn finalize(&mut self, psbt: Psbt, meta: PsbtMeta) -> Result<(), FinalizeError<W::Error>> {
330        let (tx, change) = self.finalize_inner(psbt, meta)?;
331        self.wallet
332            .broadcast(&tx, change)
333            .map_err(FinalizeError::Broadcast)?;
334        Ok(())
335    }
336
337    #[cfg(feature = "async")]
338    /// Finalizes PSBT, extracts the signed transaction, broadcasts it and updates wallet UTXO set
339    /// accordingly.
340    pub async fn finalize_async(
341        &mut self,
342        psbt: Psbt,
343        meta: PsbtMeta,
344    ) -> Result<(), FinalizeError<W::Error>> {
345        let (tx, change) = self.finalize_inner(psbt, meta)?;
346        self.wallet
347            .broadcast_async(&tx, change)
348            .await
349            .map_err(FinalizeError::Broadcast)?;
350        Ok(())
351    }
352}
353
354#[derive(Debug, Display, Error, From)]
355#[display(inner)]
356pub enum PayError {
357    #[from]
358    Fulfill(FulfillError),
359    #[from]
360    Transfer(TransferError),
361}
362
363#[derive(Debug, Display, Error, From)]
364#[display(inner)]
365pub enum TransferError {
366    #[from]
367    PsbtConstruct(ConstructionError),
368
369    #[from]
370    PsbtRgbCsv(RgbPsbtCsvError),
371
372    #[from]
373    PsbtDbc(DbcPsbtError),
374
375    #[from]
376    PsbtPrepare(RgbPsbtPrepareError),
377
378    #[from]
379    Bundle(BundleError),
380
381    #[from]
382    Include(IncludeError),
383}
384
385#[derive(Debug, Display, Error, From)]
386#[display(inner)]
387pub enum FinalizeError<E: core::error::Error> {
388    #[from]
389    UnfinalizedPsbt(UnfinalizedInputs),
390    Broadcast(E),
391}
392
393#[cfg(feature = "fs")]
394pub mod file {
395    use std::io;
396
397    use rgb_persist_fs::StockpileDir;
398
399    use super::*;
400    use crate::{FileHolder, Owner};
401
402    pub type RgbpRuntimeDir<R> = RgbRuntime<Owner<R, FileHolder>, StockpileDir<TxoSeal>>;
403
404    pub trait ConsignmentStream {
405        fn write(self, writer: impl io::Write) -> io::Result<()>;
406    }
407
408    pub struct Transfer<C: ConsignmentStream> {
409        pub psbt: Psbt,
410        pub consignment: C,
411    }
412}