use core::ops::{Deref, DerefMut};
use std::collections::{BTreeSet, HashMap};
use amplify::confinement::KeyedCollection;
use amplify::MultiError;
use bpstd::psbt::{
Beneficiary, ConstructionError, DbcPsbtError, PsbtConstructor, PsbtMeta, TxParams,
UnfinalizedInputs,
};
use bpstd::seals::TxoSeal;
use bpstd::{Address, IdxBase, Psbt, Sats, Tx, Vout};
use rgb::invoice::{RgbBeneficiary, RgbInvoice};
use rgb::popls::bp::{
BundleError, Coinselect, FulfillError, IncludeError, OpRequestSet, PaymentScript, PrefabBundle,
RgbWallet, WalletProvider,
};
use rgb::{
AuthToken, CodexId, Contract, ContractId, Contracts, EitherSeal, Issuer, Pile, RgbSealDef,
Stock, Stockpile,
};
use rgpsbt::{RgbPsbt, RgbPsbtCsvError, RgbPsbtPrepareError, ScriptResolver};
use crate::CoinselectStrategy;
#[derive(Clone, Eq, PartialEq, Debug)]
pub struct Payment {
pub uncomit_psbt: Psbt,
pub psbt_meta: PsbtMeta,
pub bundle: PrefabBundle,
pub terminals: BTreeSet<AuthToken>,
}
pub struct RgbRuntime<
W,
Sp,
S = HashMap<CodexId, Issuer>,
C = HashMap<ContractId, Contract<<Sp as Stockpile>::Stock, <Sp as Stockpile>::Pile>>,
>(RgbWallet<W, Sp, S, C>)
where
W: WalletProvider,
Sp: Stockpile,
Sp::Pile: Pile<Seal = TxoSeal>,
S: KeyedCollection<Key = CodexId, Value = Issuer>,
C: KeyedCollection<Key = ContractId, Value = Contract<Sp::Stock, Sp::Pile>>;
impl<W, Sp, S, C> From<RgbWallet<W, Sp, S, C>> for RgbRuntime<W, Sp, S, C>
where
W: WalletProvider,
Sp: Stockpile,
Sp::Pile: Pile<Seal = TxoSeal>,
S: KeyedCollection<Key = CodexId, Value = Issuer>,
C: KeyedCollection<Key = ContractId, Value = Contract<Sp::Stock, Sp::Pile>>,
{
fn from(wallet: RgbWallet<W, Sp, S, C>) -> Self { Self(wallet) }
}
impl<W, Sp, S, C> Deref for RgbRuntime<W, Sp, S, C>
where
W: WalletProvider,
Sp: Stockpile,
Sp::Pile: Pile<Seal = TxoSeal>,
S: KeyedCollection<Key = CodexId, Value = Issuer>,
C: KeyedCollection<Key = ContractId, Value = Contract<Sp::Stock, Sp::Pile>>,
{
type Target = RgbWallet<W, Sp, S, C>;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl<W, Sp, S, C> DerefMut for RgbRuntime<W, Sp, S, C>
where
W: WalletProvider,
Sp: Stockpile,
Sp::Pile: Pile<Seal = TxoSeal>,
S: KeyedCollection<Key = CodexId, Value = Issuer>,
C: KeyedCollection<Key = ContractId, Value = Contract<Sp::Stock, Sp::Pile>>,
{
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
}
impl<W, Sp, S, C> RgbRuntime<W, Sp, S, C>
where
W: WalletProvider,
Sp: Stockpile,
Sp::Pile: Pile<Seal = TxoSeal>,
S: KeyedCollection<Key = CodexId, Value = Issuer>,
C: KeyedCollection<Key = ContractId, Value = Contract<Sp::Stock, Sp::Pile>>,
{
pub fn with_components(wallet: W, contracts: Contracts<Sp, S, C>) -> Self {
Self(RgbWallet::with_components(wallet, contracts))
}
pub fn into_rgb_wallet(self) -> RgbWallet<W, Sp, S, C> { self.0 }
pub fn into_components(self) -> (W, Contracts<Sp, S, C>) { self.0.into_components() }
}
impl<W, Sp, S, C> RgbRuntime<W, Sp, S, C>
where
W: PsbtConstructor + WalletProvider,
Sp: Stockpile,
Sp::Pile: Pile<Seal = TxoSeal>,
S: KeyedCollection<Key = CodexId, Value = Issuer>,
C: KeyedCollection<Key = ContractId, Value = Contract<Sp::Stock, Sp::Pile>>,
{
#[allow(clippy::type_complexity)]
pub fn pay_invoice(
&mut self,
invoice: &RgbInvoice<ContractId>,
strategy: impl Coinselect,
params: TxParams,
giveaway: Option<Sats>,
) -> Result<(Psbt, Payment), MultiError<PayError, <Sp::Stock as Stock>::Error>> {
let request = self
.fulfill(invoice, strategy, giveaway)
.map_err(MultiError::from_a)?;
let script = OpRequestSet::with(request.clone());
let (psbt, mut payment) = self
.transfer(script, params)
.map_err(MultiError::from_other_a)?;
let terminal = match invoice.auth {
RgbBeneficiary::Token(auth) => auth,
RgbBeneficiary::WitnessOut(wout) => request
.resolve_seal(wout, psbt.script_resolver())
.expect("witness out must be present in the PSBT")
.auth_token(),
};
payment.terminals.insert(terminal);
Ok((psbt, payment))
}
pub fn rbf(&mut self, payment: &Payment, fee: impl Into<Sats>) -> Result<Psbt, PayError> {
let mut psbt = payment.uncomit_psbt.clone();
let change = payment
.psbt_meta
.change
.expect("Can't RBF when no change is present");
let old_fee = psbt.fee().expect("Invalid PSBT with zero inputs");
let out = psbt
.output_mut(change.vout.into_usize())
.expect("invalid PSBT meta-information in the payment");
out.amount -= fee.into() - old_fee;
Ok(self.complete(psbt, &payment.bundle)?)
}
pub fn script(
&mut self,
invoice: &RgbInvoice<ContractId>,
strategy: CoinselectStrategy,
giveaway: Option<Sats>,
) -> Result<PaymentScript, PayError> {
let request = self.fulfill(invoice, strategy, giveaway)?;
Ok(OpRequestSet::with(request))
}
#[allow(clippy::type_complexity)]
pub fn transfer(
&mut self,
script: PaymentScript,
params: TxParams,
) -> Result<(Psbt, Payment), MultiError<TransferError, <Sp::Stock as Stock>::Error>> {
let payment = self.exec(script, params)?;
let psbt = self
.complete(payment.uncomit_psbt.clone(), &payment.bundle)
.map_err(MultiError::A)?;
Ok((psbt, payment))
}
pub fn compose_psbt(
&mut self,
bundle: &PaymentScript,
params: TxParams,
) -> Result<(Psbt, PsbtMeta), ConstructionError> {
let closes = bundle
.iter()
.flat_map(|params| ¶ms.using)
.map(|used| used.outpoint);
let network = self.wallet.network();
let beneficiaries = bundle
.iter()
.flat_map(|params| ¶ms.owned)
.filter_map(|assignment| match &assignment.state.seal {
EitherSeal::Alt(seal) => seal.as_ref(),
EitherSeal::Token(_) => None,
})
.map(|seal| {
let address = Address::with(&seal.wout.script_pubkey(), network)
.expect("script pubkey which is not representable as an address");
Beneficiary::new(address, seal.sats)
});
self.wallet.construct_psbt(closes, beneficiaries, params)
}
pub fn color_psbt(
&mut self,
mut psbt: Psbt,
mut meta: PsbtMeta,
script: PaymentScript,
) -> Result<Payment, MultiError<TransferError, <Sp::Stock as Stock>::Error>> {
let mut change_vout = meta.change.map(|c| c.vout);
let request = psbt
.rgb_resolve(script, &mut change_vout)
.map_err(MultiError::from_a)?;
if let Some(c) = meta.change.as_mut() {
if let Some(vout) = change_vout {
c.vout = vout
}
}
let bundle = self
.bundle(request, meta.change.map(|c| c.vout))
.map_err(MultiError::from_other_a)?;
psbt.rgb_fill_csv(&bundle).map_err(MultiError::from_a)?;
Ok(Payment {
uncomit_psbt: psbt,
psbt_meta: meta,
bundle,
terminals: none!(),
})
}
pub fn exec(
&mut self,
script: PaymentScript,
params: TxParams,
) -> Result<Payment, MultiError<TransferError, <Sp::Stock as Stock>::Error>> {
let (psbt, meta) = self
.compose_psbt(&script, params)
.map_err(MultiError::from_a)?;
self.color_psbt(psbt, meta, script)
}
pub fn complete(
&mut self,
mut psbt: Psbt,
bundle: &PrefabBundle,
) -> Result<Psbt, TransferError> {
let (mpc, dbc) = psbt.dbc_commit()?;
let tx = psbt.to_unsigned_tx();
let prevouts = psbt
.inputs()
.map(|inp| inp.previous_outpoint)
.collect::<Vec<_>>();
self.include(bundle, &tx.into(), mpc, dbc, &prevouts)?;
Ok(psbt)
}
#[allow(clippy::type_complexity)]
fn finalize_inner(
&mut self,
mut psbt: Psbt,
meta: PsbtMeta,
) -> Result<(Tx, Option<(Vout, u32, u32)>), FinalizeError<W::Error>> {
psbt.finalize(self.wallet.descriptor());
let tx = psbt.extract()?;
let change = meta.change.map(|change| {
(change.vout, change.terminal.keychain.index(), change.terminal.index.index())
});
Ok((tx, change))
}
#[cfg(not(feature = "async"))]
pub fn finalize(&mut self, psbt: Psbt, meta: PsbtMeta) -> Result<(), FinalizeError<W::Error>> {
let (tx, change) = self.finalize_inner(psbt, meta)?;
self.wallet
.broadcast(&tx, change)
.map_err(FinalizeError::Broadcast)?;
Ok(())
}
#[cfg(feature = "async")]
pub async fn finalize_async(
&mut self,
psbt: Psbt,
meta: PsbtMeta,
) -> Result<(), FinalizeError<W::Error>> {
let (tx, change) = self.finalize_inner(psbt, meta)?;
self.wallet
.broadcast_async(&tx, change)
.await
.map_err(FinalizeError::Broadcast)?;
Ok(())
}
}
#[derive(Debug, Display, Error, From)]
#[display(inner)]
pub enum PayError {
#[from]
Fulfill(FulfillError),
#[from]
Transfer(TransferError),
}
#[derive(Debug, Display, Error, From)]
#[display(inner)]
pub enum TransferError {
#[from]
PsbtConstruct(ConstructionError),
#[from]
PsbtRgbCsv(RgbPsbtCsvError),
#[from]
PsbtDbc(DbcPsbtError),
#[from]
PsbtPrepare(RgbPsbtPrepareError),
#[from]
Bundle(BundleError),
#[from]
Include(IncludeError),
}
#[derive(Debug, Display, Error, From)]
#[display(inner)]
pub enum FinalizeError<E: core::error::Error> {
#[from]
UnfinalizedPsbt(UnfinalizedInputs),
Broadcast(E),
}
#[cfg(feature = "fs")]
pub mod file {
use std::io;
use rgb_persist_fs::StockpileDir;
use super::*;
use crate::{FileHolder, Owner};
pub type RgbpRuntimeDir<R> = RgbRuntime<Owner<R, FileHolder>, StockpileDir<TxoSeal>>;
pub trait ConsignmentStream {
fn write(self, writer: impl io::Write) -> io::Result<()>;
}
pub struct Transfer<C: ConsignmentStream> {
pub psbt: Psbt,
pub consignment: C,
}
}