1use 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#[derive(Clone, Eq, PartialEq, Debug)]
53pub struct Payment {
56 pub uncomit_psbt: Psbt,
57 pub psbt_meta: PsbtMeta,
58 pub bundle: PrefabBundle,
59 pub terminals: BTreeSet<AuthToken>,
60}
61
62pub 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 #[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 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 #[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| ¶ms.using)
224 .map(|used| used.outpoint);
225
226 let network = self.wallet.network();
227 let beneficiaries = bundle
228 .iter()
229 .flat_map(|params| ¶ms.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 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 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 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 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 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 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}