From 74be135368ccd3bc026afccf9fb9555e9d4f8184 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Sat, 18 Jan 2025 18:49:27 +0100 Subject: [PATCH] payments workflows --- Cargo.lock | 12 +++-- Cargo.toml | 3 ++ cli/src/cmd.rs | 36 +++++++++++-- psbt/src/bp.rs | 8 +-- psbt/src/common.rs | 2 +- src/coinselect.rs | 51 +++++++++++++++++-- src/lib.rs | 4 +- src/runtime.rs | 123 ++++++++++++++++++++++++++++++++++++--------- src/wallet.rs | 33 ++++++++++-- 9 files changed, 224 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4a8de99..b7adffb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -930,7 +930,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hypersonic" version = "0.12.0-beta.4" -source = "git+https://github.com/AluVM/sonic?branch=master#02fea6d4472bfada05555f2a01348787498add23" +source = "git+https://github.com/AluVM/sonic?branch=master#9c26410b570b9d51f54bb92f2909333631b16a01" dependencies = [ "aluvm", "amplify", @@ -1496,7 +1496,7 @@ dependencies = [ [[package]] name = "rgb-invoice" version = "0.12.0-beta.4" -source = "git+https://github.com/RGB-WG/rgb-std?branch=payments#688aa844b36aeb8fde9c18c9aec27d318f3941f2" +source = "git+https://github.com/RGB-WG/rgb-std?branch=payments#d19bfefb813240e2e6edbcb99dfb1abd0358166c" dependencies = [ "amplify", "baid64", @@ -1533,9 +1533,11 @@ dependencies = [ "log", "nonasync", "rand", + "rgb-psbt", "rgb-std", "serde", "serde_yaml", + "strict_types", "wasm-bindgen", "wasm-bindgen-test", ] @@ -1543,7 +1545,7 @@ dependencies = [ [[package]] name = "rgb-std" version = "0.12.0-beta.4" -source = "git+https://github.com/RGB-WG/rgb-std?branch=payments#688aa844b36aeb8fde9c18c9aec27d318f3941f2" +source = "git+https://github.com/RGB-WG/rgb-std?branch=payments#d19bfefb813240e2e6edbcb99dfb1abd0358166c" dependencies = [ "amplify", "bp-core", @@ -1848,7 +1850,7 @@ dependencies = [ [[package]] name = "sonic-api" version = "0.12.0-beta.4" -source = "git+https://github.com/AluVM/sonic?branch=master#02fea6d4472bfada05555f2a01348787498add23" +source = "git+https://github.com/AluVM/sonic?branch=master#9c26410b570b9d51f54bb92f2909333631b16a01" dependencies = [ "aluvm", "amplify", @@ -1867,7 +1869,7 @@ dependencies = [ [[package]] name = "sonic-callreq" version = "0.12.0-beta.4" -source = "git+https://github.com/AluVM/sonic?branch=master#02fea6d4472bfada05555f2a01348787498add23" +source = "git+https://github.com/AluVM/sonic?branch=master#9c26410b570b9d51f54bb92f2909333631b16a01" dependencies = [ "amplify", "baid64", diff --git a/Cargo.toml b/Cargo.toml index 37ca44f..f2e1ef7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ license = "Apache-2.0" amplify = "4.8.0" nonasync = "0.1.2" strict_encoding = "2.8.1" +strict_types = "2.8.1" commit_verify = "=0.12.0-beta.4" bp-std = { version = "=0.12.0-beta.4", features = ["client-side-validation"] } bp-electrum = "=0.12.0-beta.4" @@ -51,10 +52,12 @@ crate-type = ["cdylib", "rlib"] [dependencies] amplify = { workspace = true } commit_verify = { workspace = true } +strict_types = { workspace = true } nonasync = { workspace = true } bp-std = { workspace = true } bp-wallet = { workspace = true } rgb-std = { workspace = true, features = ["bitcoin"] } +rgb-psbt.workspace = true indexmap = { workspace = true } serde = { workspace = true, optional = true } serde_yaml = { workspace = true, optional = true } diff --git a/cli/src/cmd.rs b/cli/src/cmd.rs index 4423125..9d7a6da 100644 --- a/cli/src/cmd.rs +++ b/cli/src/cmd.rs @@ -210,8 +210,17 @@ pub enum Cmd { #[clap(short, long, default_value = "aggregate", env = RGB_COINSELECT_STRATEGY_ENV)] strategy: CoinselectStrategy, - /// Invoice to fylfill - invoice: RgbInvoice, + /// Invoice to fulfill + invoice: RgbInvoice, + + /// Fees for PSBT + fee: Sats, + + /// File to save the produced PSBT + /// + /// If not provided, prints PSBT to standard output. + #[clap(value_hint = ValueHint::FilePath)] + psbt: Option, }, /// Execute a script, producing prefabricated operation bundle and PSBT @@ -492,6 +501,23 @@ impl Args { } } + Cmd::Pay { wallet, strategy, invoice, fee, psbt: psbt_filename } => { + let mut runtime = self.runtime(wallet.wallet.as_deref()); + // TODO: sync wallet if needed + // TODO: Add params and giveway to arguments + let params = TxParams::with(*fee); + let giveaway = Some(Sats::from(500u16)); + let psbt = runtime.pay_invoice(invoice, *strategy, params, giveaway)?; + if let Some(psbt_filename) = psbt_filename { + psbt.encode( + psbt.version, + &mut File::create(psbt_filename).expect("Unable to write PSBT"), + )?; + } else { + println!("{psbt}"); + } + } + Cmd::Exec { wallet, script, @@ -506,7 +532,8 @@ impl Args { serde_yaml::from_reader::<_, OpRequestSet>>(src)?; let (mut psbt, meta) = runtime - .construct_psbt(&items, TxParams::with(*fee)) + .wallet + .compose_psbt(&items, TxParams::with(*fee)) .expect("Unable to construct PSBT"); let mut psbt_file = File::create_new( psbt_filename @@ -519,13 +546,14 @@ impl Args { // Here we send PSBT to other payjoin parties so they add their inputs and outputs, // or even re-order existing ones + // TODO: Replace this with `color` function let items = items.resolve_seals(psbt.script_resolver(), meta.change_vout)?; let bundle = runtime.bundle(items, meta.change_vout)?; bundle .strict_serialize_to_file::<{ usize::MAX }>(&bundle_filename) .expect("Unable to write output file"); - psbt.rgb_fill_csv(bundle) + psbt.rgb_fill_csv(&bundle) .expect("Unable to embed RGB information to PSBT"); psbt.encode(psbt.version, &mut psbt_file) .expect("Unable to write PSBT"); diff --git a/psbt/src/bp.rs b/psbt/src/bp.rs index 8c97bb2..5900469 100644 --- a/psbt/src/bp.rs +++ b/psbt/src/bp.rs @@ -30,16 +30,16 @@ use rgb::popls::bp::PrefabBundle; use crate::{RgbPsbt, RgbPsbtError, ScriptResolver}; impl RgbPsbt for Psbt { - fn rgb_fill_csv(&mut self, bundle: PrefabBundle) -> Result<(), RgbPsbtError> { + fn rgb_fill_csv(&mut self, bundle: &PrefabBundle) -> Result<(), RgbPsbtError> { for prefab in bundle { let id = mpc::ProtocolId::from_byte_array(prefab.operation.contract_id.to_byte_array()); let opid = prefab.operation.opid(); let msg = mmb::Message::from_byte_array(opid.to_byte_array()); - for outpoint in prefab.closes { + for outpoint in &prefab.closes { let input = self .inputs_mut() - .find(|inp| inp.previous_outpoint == outpoint) - .ok_or(RgbPsbtError::InputAbsent(outpoint))?; + .find(|inp| inp.previous_outpoint == *outpoint) + .ok_or(RgbPsbtError::InputAbsent(*outpoint))?; input.set_mmb_message(id, msg).map_err(|_| { RgbPsbtError::InputAlreadyUsed(input.index(), prefab.operation.contract_id) })?; diff --git a/psbt/src/common.rs b/psbt/src/common.rs index 5ed1bf5..4e440a4 100644 --- a/psbt/src/common.rs +++ b/psbt/src/common.rs @@ -28,7 +28,7 @@ use rgb::{ContractId, Outpoint}; pub trait RgbPsbt { // TODO: Add rgb_embed to embed operations for hardware signers - fn rgb_fill_csv(&mut self, bundle: PrefabBundle) -> Result<(), RgbPsbtError>; + fn rgb_fill_csv(&mut self, bundle: &PrefabBundle) -> Result<(), RgbPsbtError>; fn rgb_complete(&mut self) -> Result<(), Unmodifiable>; } diff --git a/src/coinselect.rs b/src/coinselect.rs index ece933c..4557c90 100644 --- a/src/coinselect.rs +++ b/src/coinselect.rs @@ -22,14 +22,22 @@ // or implied. See the License for the specific language governing permissions and limitations under // the License. -use std::str::FromStr; +use core::str::FromStr; + +use rgb::popls::bp::Coinselect; +use rgb::{CellAddr, StateCalc}; +use strict_types::StrictVal; #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Display, Default)] #[display(lowercase)] pub enum CoinselectStrategy { + /// Collect them most small outputs unless the invoiced value if reached #[default] Aggregate, - Size, + + /// Collect the minimum number of outputs (with the large value) to reduce the resulting input + /// count + SmallSize, } impl FromStr for CoinselectStrategy { @@ -38,8 +46,45 @@ impl FromStr for CoinselectStrategy { fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "aggregate" => Ok(CoinselectStrategy::Aggregate), - "size" => Ok(CoinselectStrategy::Size), + "smallsize" => Ok(CoinselectStrategy::SmallSize), s => Err(s.to_string()), } } } + +impl Coinselect for CoinselectStrategy { + fn coinselect( + &mut self, + invoiced_state: &StrictVal, + calc: &mut (impl StateCalc + ?Sized), + owned_state: Vec<(CellAddr, &StrictVal)>, + ) -> Option> { + let res = match self { + CoinselectStrategy::Aggregate => owned_state + .into_iter() + .take_while(|(_, val)| { + if calc.is_satisfied(invoiced_state) { + return false; + } + calc.accumulate(*val).is_ok() + }) + .map(|(addr, _)| addr) + .collect(), + CoinselectStrategy::SmallSize => owned_state + .into_iter() + .rev() + .take_while(|(_, val)| { + if calc.is_satisfied(invoiced_state) { + return false; + } + calc.accumulate(*val).is_ok() + }) + .map(|(addr, _)| addr) + .collect(), + }; + if !calc.is_satisfied(invoiced_state) { + return None; + }; + Some(res) + } +} diff --git a/src/lib.rs b/src/lib.rs index bc212d9..9fceccb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,6 +39,6 @@ mod runtime; pub use coinselect::CoinselectStrategy; #[cfg(feature = "fs")] -pub use runtime::file::RgbDirRuntime; -pub use runtime::RgbRuntime; +pub use runtime::file::{ConsignmentStream, RgbDirRuntime, Transfer}; +pub use runtime::{PayError, RgbRuntime, TransferError}; pub use wallet::RgbWallet; diff --git a/src/runtime.rs b/src/runtime.rs index 0cdff9c..0549fc7 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -24,13 +24,18 @@ use std::ops::{Deref, DerefMut}; -use bpstd::psbt::{Beneficiary, ConstructionError, PsbtConstructor, PsbtMeta, TxParams}; +use bpstd::psbt::{ConstructionError, DbcPsbtError, TxParams}; use bpstd::seals::TxoSeal; -use bpstd::{Address, Psbt}; -use rgb::popls::bp::{Barrow, OpRequestSet, WoutAssignment}; -use rgb::{EitherSeal, Excavate, Pile, Supply}; +use bpstd::{Psbt, Sats}; +use rgb::invoice::RgbInvoice; +use rgb::popls::bp::{ + Barrow, BundleError, FulfillError, IncludeError, OpRequestSet, WoutAssignment, +}; +use rgb::{ContractId, Excavate, Pile, Supply}; +use rgpsbt::{RgbPsbt, RgbPsbtError, ScriptResolver}; use crate::wallet::RgbWallet; +use crate::CoinselectStrategy; pub struct RgbRuntime, X: Excavate>( Barrow, @@ -51,37 +56,105 @@ impl, X: Excavate> DerefMut for RgbRunt } impl, X: Excavate> RgbRuntime { - pub fn construct_psbt( + /// Pays an invoice producing PSBT ready to be signed + /// + /// Should not be used in multi-party protocols like coinjoins, when a PSBT may needs to be + /// modified in the number of inputs or outputs. Use `construct_psbt` method for such + /// scenarios. + /// + /// If you need more flexibility in constructing payments (do multiple payments with multiple + /// contracts, use global state etc.) in a single PSBT, please use `pay_custom` APIs and + /// [`PrefabBundleSet`] stead of this simplified API. + pub fn pay_invoice( &mut self, - bundle: &OpRequestSet>, + invoice: &RgbInvoice, + strategy: CoinselectStrategy, params: TxParams, - ) -> Result<(Psbt, PsbtMeta), ConstructionError> { - let closes = bundle - .iter() - .flat_map(|params| ¶ms.using) - .map(|used| used.outpoint); - let network = self.0.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.amount) - }); - self.0.wallet.construct_psbt(closes, beneficiaries, params) + giveaway: Option, + ) -> Result { + let request = self.fulfill(invoice, strategy, giveaway)?; + let bundle = OpRequestSet::with(request); + let psbt = self.transfer(bundle, params)?; + Ok(psbt) } + + /// Constructs transfer, consisting of PSBT and a consignment stream + // TODO: Return a dedicated Transfer object which can stream a consignment + pub fn transfer( + &mut self, + set: OpRequestSet>, + params: TxParams, + ) -> Result { + let (mut psbt, meta) = self.0.wallet.compose_psbt(&set, params)?; + let items = set + .resolve_seals(psbt.script_resolver(), meta.change_vout) + .map_err(|_| TransferError::ChangeRequired)?; + let bundle = self.bundle(items, meta.change_vout)?; + + psbt.rgb_fill_csv(&bundle)?; + + psbt.rgb_complete() + .expect("PSBT is modifiable since it is just constructed"); + let (mpc, dbc) = psbt.dbc_commit()?; + let tx = psbt.to_unsigned_tx(); + + let prevouts = psbt + .inputs() + .map(|inp| inp.previous_outpoint) + .collect::>(); + self.include(&bundle, &tx.into(), mpc, dbc, &prevouts)?; + + Ok(psbt) + } +} + +#[derive(Clone, Debug, Display, Error, From)] +#[display(inner)] +pub enum PayError { + #[from] + Fulfill(FulfillError), + #[from] + Transfer(TransferError), +} + +#[derive(Clone, Debug, Display, Error, From)] +#[display(inner)] +pub enum TransferError { + #[from] + PsbtConstruct(ConstructionError), + + #[from] + PsbtRgb(RgbPsbtError), + + #[from] + PsbtDbc(DbcPsbtError), + + #[from] + Bundle(BundleError), + + #[from] + Include(IncludeError), + + #[display("transfer doesn't create BTC change output, which is required for RGB change")] + ChangeRequired, } #[cfg(feature = "fs")] pub mod file { + use std::io; + use rgb::{DirExcavator, FilePile, FileSupply}; use super::*; pub type RgbDirRuntime = RgbRuntime, DirExcavator>; + + pub trait ConsignmentStream { + fn write(self, writer: impl io::Write) -> io::Result<()>; + } + + pub struct Transfer { + pub psbt: Psbt, + pub consignment: C, + } } diff --git a/src/wallet.rs b/src/wallet.rs index 74092f0..cb74844 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -25,13 +25,13 @@ use std::convert::Infallible; use amplify::Bytes32; -use bpstd::psbt::PsbtConstructor; +use bpstd::psbt::{Beneficiary, ConstructionError, PsbtConstructor, PsbtMeta, TxParams}; use bpstd::seals::TxoSeal; -use bpstd::{Address, Keychain, Network, Outpoint, XpubDerivable}; +use bpstd::{Address, Keychain, Network, Outpoint, Psbt, XpubDerivable}; use bpwallet::{Layer2Empty, NoLayer2, Wallet, WalletCache, WalletData, WalletDescr}; use nonasync::persistence::{PersistenceError, PersistenceProvider}; -use rgb::popls::bp::WalletProvider; -use rgb::{AuthToken, SealAuthToken}; +use rgb::popls::bp::{OpRequestSet, WalletProvider, WoutAssignment}; +use rgb::{AuthToken, EitherSeal, SealAuthToken}; use crate::descriptor::RgbDescr; @@ -103,4 +103,29 @@ impl RgbWallet { + 'static { Wallet::load(provider, autosave).map(RgbWallet) } + + pub fn compose_psbt( + &mut self, + bundle: &OpRequestSet>, + params: TxParams, + ) -> Result<(Psbt, PsbtMeta), ConstructionError> { + let closes = bundle + .iter() + .flat_map(|params| ¶ms.using) + .map(|used| used.outpoint); + let network = self.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.amount) + }); + self.construct_psbt(closes, beneficiaries, params) + } }