Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve v0.12 payments workflows #279

Merged
merged 11 commits into from
Jan 25, 2025
Prev Previous commit
Next Next commit
psbt: avoid dependencies on bpstd when bp feature is not active
dr-orlovsky committed Jan 25, 2025
commit 97f780a161631134907a20fbb9adf14419842453
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@ nonasync = "0.1.2"
strict_encoding = "2.8.1"
strict_types = "2.8.1"
commit_verify = "=0.12.0-beta.4"
bp-core = "=0.12.0-beta.4"
bp-std = { version = "=0.12.0-beta.4", features = ["client-side-validation"] }
bp-electrum = "=0.12.0-beta.4"
bp-esplora = { version = "=0.12.0-beta.4", default-features = false }
2 changes: 1 addition & 1 deletion psbt/Cargo.toml
Original file line number Diff line number Diff line change
@@ -16,8 +16,8 @@ name = "rgpsbt"

[dependencies]
amplify = { workspace = true }
commit_verify = { workspace = true }
rgb-std = { workspace = true, features = ["bitcoin"] }
bp-core = { workspace = true }
bp-std = { workspace = true, optional = true }

[features]
9 changes: 6 additions & 3 deletions psbt/src/bp.rs
Original file line number Diff line number Diff line change
@@ -24,22 +24,23 @@

use amplify::ByteArray;
use bpstd::seals::{mmb, mpc};
use bpstd::{Psbt, Sats, ScriptPubkey, Unmodifiable, Vout};
use bpstd::{Psbt, Sats, ScriptPubkey, Vout};
use rgb::popls::bp::PrefabBundle;

use crate::common::RgbPsbtUnfinalizable;
use crate::{RgbPsbt, RgbPsbtError, ScriptResolver};

impl RgbPsbt for Psbt {
fn rgb_fill_csv(&mut self, bundle: &PrefabBundle) -> Result<(), RgbPsbtError> {

Check warning on line 34 in psbt/src/bp.rs

Codecov / codecov/patch

psbt/src/bp.rs#L34

Added line #L34 was not covered by tests
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 {

Check warning on line 39 in psbt/src/bp.rs

Codecov / codecov/patch

psbt/src/bp.rs#L39

Added line #L39 was not covered by tests
let input = self
.inputs_mut()
.find(|inp| inp.previous_outpoint == *outpoint)
.ok_or(RgbPsbtError::InputAbsent(*outpoint))?;

Check warning on line 43 in psbt/src/bp.rs

Codecov / codecov/patch

psbt/src/bp.rs#L42-L43

Added lines #L42 - L43 were not covered by tests
input.set_mmb_message(id, msg).map_err(|_| {
RgbPsbtError::InputAlreadyUsed(input.index(), prefab.operation.contract_id)
})?;
@@ -48,9 +49,11 @@
Ok(())
}

fn rgb_complete(&mut self) -> Result<(), Unmodifiable> {
fn rgb_complete(&mut self) -> Result<(), RgbPsbtUnfinalizable> {

Check warning on line 52 in psbt/src/bp.rs

Codecov / codecov/patch

psbt/src/bp.rs#L52

Added line #L52 was not covered by tests
if self.outputs().all(|out| !out.is_opret_host()) && self.opret_hosts().count() == 0 {
let host = self.construct_output(ScriptPubkey::op_return(&[]), Sats::ZERO)?;
let host = self
.construct_output(ScriptPubkey::op_return(&[]), Sats::ZERO)
.map_err(|_| RgbPsbtUnfinalizable)?;

Check warning on line 56 in psbt/src/bp.rs

Codecov / codecov/patch

psbt/src/bp.rs#L54-L56

Added lines #L54 - L56 were not covered by tests
host.set_opret_host().ok();
}
self.complete_construction();
10 changes: 8 additions & 2 deletions psbt/src/common.rs
Original file line number Diff line number Diff line change
@@ -22,15 +22,21 @@
// or implied. See the License for the specific language governing permissions and limitations under
// the License.

use bpstd::{ScriptPubkey, Unmodifiable, Vout};
use bp::{ScriptPubkey, Vout};
use rgb::popls::bp::PrefabBundle;
use rgb::{ContractId, Outpoint};

#[derive(Copy, Clone, Eq, PartialEq, Debug, Display, Error)]
#[display(
"in order to complete RGB processing the PSBT must have PSBT_GLOBAL_TX_MODIFIABLE flag set on"
)]
pub struct RgbPsbtUnfinalizable;

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_complete(&mut self) -> Result<(), Unmodifiable>;
fn rgb_complete(&mut self) -> Result<(), RgbPsbtUnfinalizable>;
}

/// Errors embedding RGB-related information.

Unchanged files with check annotations Beta

strategy: CoinselectStrategy,
/// Invoice to fulfill
invoice: RgbInvoice<ContractId>,

Check warning on line 214 in cli/src/cmd.rs

Codecov / codecov/patch

cli/src/cmd.rs#L214

Added line #L214 was not covered by tests
/// Fees for PSBT
fee: Sats,

Check warning on line 217 in cli/src/cmd.rs

Codecov / codecov/patch

cli/src/cmd.rs#L217

Added line #L217 was not covered by tests
/// File to save the produced PSBT
///
pub fn mound(&self) -> BpDirMound {
if self.init {
let _ = fs::create_dir_all(self.data_dir());

Check warning on line 312 in cli/src/cmd.rs

Codecov / codecov/patch

cli/src/cmd.rs#L312

Added line #L312 was not covered by tests
}
if !self.network.is_testnet() {
panic!("Non-testnet networks are not yet supported");
let state = if *all {
runtime.state_all(*contract).collect::<Vec<_>>()
} else {
runtime.state_own(*contract).collect()

Check warning on line 439 in cli/src/cmd.rs

Codecov / codecov/patch

cli/src/cmd.rs#L439

Added line #L439 was not covered by tests
};
for (contract_id, state) in state {
println!("{contract_id}");
}
}
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}");
}

Check warning on line 518 in cli/src/cmd.rs

Codecov / codecov/patch

cli/src/cmd.rs#L504-L518

Added lines #L504 - L518 were not covered by tests
}
Cmd::Exec {
} => {
let mut runtime = self.runtime(wallet.as_deref());
let src = File::open(script).expect("Unable to open script file");
let items =
serde_yaml::from_reader::<_, OpRequestSet<Option<WoutAssignment>>>(src)?;

Check warning on line 532 in cli/src/cmd.rs

Codecov / codecov/patch

cli/src/cmd.rs#L531-L532

Added lines #L531 - L532 were not covered by tests
let (mut psbt, meta) = runtime
.wallet
.compose_psbt(&items, TxParams::with(*fee))

Check warning on line 536 in cli/src/cmd.rs

Codecov / codecov/patch

cli/src/cmd.rs#L535-L536

Added lines #L535 - L536 were not covered by tests
.expect("Unable to construct PSBT");
let mut psbt_file = File::create_new(
psbt_filename
// 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)?;

Check warning on line 551 in cli/src/cmd.rs

Codecov / codecov/patch

cli/src/cmd.rs#L550-L551

Added lines #L550 - L551 were not covered by tests
bundle
.strict_serialize_to_file::<{ usize::MAX }>(&bundle_filename)
.expect("Unable to write output file");
psbt.rgb_fill_csv(&bundle)

Check warning on line 556 in cli/src/cmd.rs

Codecov / codecov/patch

cli/src/cmd.rs#L556

Added line #L556 was not covered by tests
.expect("Unable to embed RGB information to PSBT");
psbt.encode(psbt.version, &mut psbt_file)
.expect("Unable to write PSBT");
let mut runtime = self.runtime(wallet.as_deref());
let (mpc, dbc) = psbt.dbc_commit()?;
let tx = psbt.to_unsigned_tx();
runtime.include(&bundle, &tx.into(), mpc, dbc, &prevouts)?;

Check warning on line 582 in cli/src/cmd.rs

Codecov / codecov/patch

cli/src/cmd.rs#L582

Added line #L582 was not covered by tests
psbt.encode(
psbt.version,
}
impl Coinselect for CoinselectStrategy {
fn coinselect(
&mut self,
invoiced_state: &StrictVal,
calc: &mut (impl StateCalc + ?Sized),
owned_state: Vec<(CellAddr, &StrictVal)>,
) -> Option<Vec<CellAddr>> {
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(),

Check warning on line 83 in src/coinselect.rs

Codecov / codecov/patch

src/coinselect.rs#L56-L83

Added lines #L56 - L83 were not covered by tests
};
if !calc.is_satisfied(invoiced_state) {
return None;
};
Some(res)
}

Check warning on line 89 in src/coinselect.rs

Codecov / codecov/patch

src/coinselect.rs#L85-L89

Added lines #L85 - L89 were not covered by tests
}
#[cfg(test)]
/// 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(

Check warning on line 68 in src/runtime.rs

Codecov / codecov/patch

src/runtime.rs#L68

Added line #L68 was not covered by tests
&mut self,
invoice: &RgbInvoice<ContractId>,
strategy: CoinselectStrategy,

Check warning on line 71 in src/runtime.rs

Codecov / codecov/patch

src/runtime.rs#L70-L71

Added lines #L70 - L71 were not covered by tests
params: TxParams,
giveaway: Option<Sats>,
) -> Result<Psbt, PayError> {
let request = self.fulfill(invoice, strategy, giveaway)?;
let bundle = OpRequestSet::with(request);
let psbt = self.transfer(bundle, params)?;
Ok(psbt)

Check warning on line 78 in src/runtime.rs

Codecov / codecov/patch

src/runtime.rs#L73-L78

Added lines #L73 - L78 were not covered by tests
}
/// 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: PaymentScript,
params: TxParams,
) -> Result<Psbt, TransferError> {
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)?;

Check warning on line 92 in src/runtime.rs

Codecov / codecov/patch

src/runtime.rs#L83-L92

Added lines #L83 - L92 were not covered by tests
psbt.rgb_fill_csv(&bundle)?;

Check warning on line 94 in src/runtime.rs

Codecov / codecov/patch

src/runtime.rs#L94

Added line #L94 was not covered by tests
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::<Vec<_>>();
self.include(&bundle, &tx.into(), mpc, dbc, &prevouts)?;

Check warning on line 105 in src/runtime.rs

Codecov / codecov/patch

src/runtime.rs#L96-L105

Added lines #L96 - L105 were not covered by tests
Ok(psbt)
}

Check warning on line 108 in src/runtime.rs

Codecov / codecov/patch

src/runtime.rs#L107-L108

Added lines #L107 - L108 were not covered by tests
}
#[derive(Clone, Debug, Display, Error, From)]
Wallet::load(provider, autosave).map(RgbWallet)
}
pub fn compose_psbt(
&mut self,
bundle: &PaymentScript,
params: TxParams,
) -> Result<(Psbt, PsbtMeta), ConstructionError> {
let closes = bundle
.iter()
.flat_map(|params| &params.using)
.map(|used| used.outpoint);
let network = self.network();
let beneficiaries = bundle
.iter()
.flat_map(|params| &params.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)
}

Check warning on line 130 in src/wallet.rs

Codecov / codecov/patch

src/wallet.rs#L107-L130

Added lines #L107 - L130 were not covered by tests
}