diff --git a/payjoin-cli/src/app/mod.rs b/payjoin-cli/src/app/mod.rs index 468a4b66..e29e7f8b 100644 --- a/payjoin-cli/src/app/mod.rs +++ b/payjoin-cli/src/app/mod.rs @@ -7,6 +7,7 @@ use bitcoin::TxIn; use bitcoincore_rpc::bitcoin::Amount; use bitcoincore_rpc::RpcApi; use payjoin::bitcoin::psbt::Psbt; +use payjoin::receive::InputPair; use payjoin::send::Sender; use payjoin::{bitcoin, PjUri}; @@ -124,8 +125,8 @@ fn read_local_cert() -> Result> { } pub fn input_pair_from_list_unspent( - utxo: &bitcoincore_rpc::bitcoincore_rpc_json::ListUnspentResultEntry, -) -> (PsbtInput, TxIn) { + utxo: bitcoincore_rpc::bitcoincore_rpc_json::ListUnspentResultEntry, +) -> InputPair { let psbtin = PsbtInput { // NOTE: non_witness_utxo is not necessary because bitcoin-cli always supplies // witness_utxo, even for non-witness inputs @@ -141,5 +142,5 @@ pub fn input_pair_from_list_unspent( previous_output: bitcoin::OutPoint { txid: utxo.txid, vout: utxo.vout }, ..Default::default() }; - (psbtin, txin) + InputPair::new(txin, psbtin).expect("Input pair should be valid") } diff --git a/payjoin-cli/src/app/v1.rs b/payjoin-cli/src/app/v1.rs index a2d98cc2..29c85e8c 100644 --- a/payjoin-cli/src/app/v1.rs +++ b/payjoin-cli/src/app/v1.rs @@ -375,28 +375,18 @@ fn try_contributing_inputs( payjoin: payjoin::receive::WantsInputs, bitcoind: &bitcoincore_rpc::Client, ) -> Result { - use bitcoin::OutPoint; - - let available_inputs = bitcoind + let candidate_inputs = bitcoind .list_unspent(None, None, None, None, None) - .context("Failed to list unspent from bitcoind")?; - let candidate_inputs: HashMap = available_inputs - .iter() - .map(|i| (i.amount, OutPoint { txid: i.txid, vout: i.vout })) - .collect(); - - let selected_outpoint = payjoin + .context("Failed to list unspent from bitcoind")? + .into_iter() + .map(input_pair_from_list_unspent); + let selected_input = payjoin .try_preserving_privacy(candidate_inputs) .map_err(|e| anyhow!("Failed to make privacy preserving selection: {}", e))?; - let selected_utxo = available_inputs - .iter() - .find(|i| i.txid == selected_outpoint.txid && i.vout == selected_outpoint.vout) - .context("This shouldn't happen. Failed to retrieve the privacy preserving utxo from those we provided to the seclector.")?; - log::debug!("selected utxo: {:#?}", selected_utxo); - let input_pair = input_pair_from_list_unspent(selected_utxo); + log::debug!("selected input: {:#?}", selected_input); Ok(payjoin - .contribute_inputs(vec![input_pair]) + .contribute_inputs(vec![selected_input]) .expect("This shouldn't happen. Failed to contribute inputs.") .commit_inputs()) } diff --git a/payjoin-cli/src/app/v2.rs b/payjoin-cli/src/app/v2.rs index 49a1e26b..3dce6a98 100644 --- a/payjoin-cli/src/app/v2.rs +++ b/payjoin-cli/src/app/v2.rs @@ -1,4 +1,3 @@ -use std::collections::HashMap; use std::str::FromStr; use std::sync::Arc; @@ -365,28 +364,18 @@ fn try_contributing_inputs( payjoin: payjoin::receive::v2::WantsInputs, bitcoind: &bitcoincore_rpc::Client, ) -> Result { - use bitcoin::OutPoint; - - let available_inputs = bitcoind + let candidate_inputs = bitcoind .list_unspent(None, None, None, None, None) - .context("Failed to list unspent from bitcoind")?; - let candidate_inputs: HashMap = available_inputs - .iter() - .map(|i| (i.amount, OutPoint { txid: i.txid, vout: i.vout })) - .collect(); - - let selected_outpoint = payjoin + .context("Failed to list unspent from bitcoind")? + .into_iter() + .map(input_pair_from_list_unspent); + let selected_input = payjoin .try_preserving_privacy(candidate_inputs) .map_err(|e| anyhow!("Failed to make privacy preserving selection: {}", e))?; - let selected_utxo = available_inputs - .iter() - .find(|i| i.txid == selected_outpoint.txid && i.vout == selected_outpoint.vout) - .context("This shouldn't happen. Failed to retrieve the privacy preserving utxo from those we provided to the seclector.")?; - log::debug!("selected utxo: {:#?}", selected_utxo); - let input_pair = input_pair_from_list_unspent(selected_utxo); + log::debug!("selected input: {:#?}", selected_input); Ok(payjoin - .contribute_inputs(vec![input_pair]) + .contribute_inputs(vec![selected_input]) .expect("This shouldn't happen. Failed to contribute inputs.") .commit_inputs()) } diff --git a/payjoin/src/psbt.rs b/payjoin/src/psbt.rs index 25a028e4..52ed3af5 100644 --- a/payjoin/src/psbt.rs +++ b/payjoin/src/psbt.rs @@ -35,7 +35,7 @@ pub(crate) trait PsbtExt: Sized { ) -> &mut BTreeMap; fn proprietary_mut(&mut self) -> &mut BTreeMap>; fn unknown_mut(&mut self) -> &mut BTreeMap>; - fn input_pairs(&self) -> Box> + '_>; + fn input_pairs(&self) -> Box> + '_>; // guarantees that length of psbt input matches that of unsigned_tx inputs and same /// thing for outputs. fn validate(self) -> Result; @@ -59,13 +59,13 @@ impl PsbtExt for Psbt { fn unknown_mut(&mut self) -> &mut BTreeMap> { &mut self.unknown } - fn input_pairs(&self) -> Box> + '_> { + fn input_pairs(&self) -> Box> + '_> { Box::new( self.unsigned_tx .input .iter() .zip(&self.inputs) - .map(|(txin, psbtin)| InputPair { txin, psbtin }), + .map(|(txin, psbtin)| InternalInputPair { txin, psbtin }), ) } @@ -106,12 +106,13 @@ fn redeem_script(script_sig: &Script) -> Option<&Script> { // https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#p2wpkh-nested-in-bip16-p2sh const NESTED_P2WPKH_MAX: InputWeightPrediction = InputWeightPrediction::from_slice(23, &[72, 33]); -pub(crate) struct InputPair<'a> { +#[derive(Clone, Debug)] +pub(crate) struct InternalInputPair<'a> { pub txin: &'a TxIn, pub psbtin: &'a psbt::Input, } -impl<'a> InputPair<'a> { +impl<'a> InternalInputPair<'a> { /// Returns TxOut associated with the input pub fn previous_txout(&self) -> Result<&TxOut, PrevTxOutError> { match (&self.psbtin.non_witness_utxo, &self.psbtin.witness_utxo) { @@ -132,10 +133,13 @@ impl<'a> InputPair<'a> { } } - pub fn validate_utxo(&self, treat_missing_as_error: bool) -> Result<(), PsbtInputError> { + pub fn validate_utxo( + &self, + treat_missing_as_error: bool, + ) -> Result<(), InternalPsbtInputError> { match (&self.psbtin.non_witness_utxo, &self.psbtin.witness_utxo) { (None, None) if treat_missing_as_error => - Err(PsbtInputError::PrevTxOut(PrevTxOutError::MissingUtxoInformation)), + Err(InternalPsbtInputError::PrevTxOut(PrevTxOutError::MissingUtxoInformation)), (None, None) => Ok(()), (Some(tx), None) if tx.compute_txid() == self.txin.previous_output.txid => tx .output @@ -153,7 +157,7 @@ impl<'a> InputPair<'a> { .into() }) .map(drop), - (Some(_), None) => Err(PsbtInputError::UnequalTxid), + (Some(_), None) => Err(InternalPsbtInputError::UnequalTxid), (None, Some(_)) => Ok(()), (Some(tx), Some(witness_txout)) if tx.compute_txid() == self.txin.previous_output.txid => @@ -173,10 +177,10 @@ impl<'a> InputPair<'a> { if witness_txout == non_witness_txout { Ok(()) } else { - Err(PsbtInputError::SegWitTxOutMismatch) + Err(InternalPsbtInputError::SegWitTxOutMismatch) } } - (Some(_), Some(_)) => Err(PsbtInputError::UnequalTxid), + (Some(_), Some(_)) => Err(InternalPsbtInputError::UnequalTxid), } } @@ -245,41 +249,66 @@ impl fmt::Display for PrevTxOutError { impl std::error::Error for PrevTxOutError {} #[derive(Debug)] -pub(crate) enum PsbtInputError { +pub(crate) enum InternalPsbtInputError { PrevTxOut(PrevTxOutError), UnequalTxid, /// TxOut provided in `segwit_utxo` doesn't match the one in `non_segwit_utxo` SegWitTxOutMismatch, + AddressType(AddressTypeError), + NoRedeemScript, } -impl fmt::Display for PsbtInputError { +impl fmt::Display for InternalPsbtInputError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - PsbtInputError::PrevTxOut(_) => write!(f, "invalid previous transaction output"), - PsbtInputError::UnequalTxid => write!(f, "transaction ID of previous transaction doesn't match one specified in input spending it"), - PsbtInputError::SegWitTxOutMismatch => write!(f, "transaction output provided in SegWit UTXO field doesn't match the one in non-SegWit UTXO field"), + Self::PrevTxOut(_) => write!(f, "invalid previous transaction output"), + Self::UnequalTxid => write!(f, "transaction ID of previous transaction doesn't match one specified in input spending it"), + Self::SegWitTxOutMismatch => write!(f, "transaction output provided in SegWit UTXO field doesn't match the one in non-SegWit UTXO field"), + Self::AddressType(_) => write!(f, "invalid address type"), + Self::NoRedeemScript => write!(f, "provided p2sh PSBT input is missing a redeem_script"), } } } -impl std::error::Error for PsbtInputError { +impl std::error::Error for InternalPsbtInputError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { - PsbtInputError::PrevTxOut(error) => Some(error), - PsbtInputError::UnequalTxid => None, - PsbtInputError::SegWitTxOutMismatch => None, + Self::PrevTxOut(error) => Some(error), + Self::UnequalTxid => None, + Self::SegWitTxOutMismatch => None, + Self::AddressType(error) => Some(error), + Self::NoRedeemScript => None, } } } -impl From for PsbtInputError { - fn from(value: PrevTxOutError) -> Self { PsbtInputError::PrevTxOut(value) } +impl From for InternalPsbtInputError { + fn from(value: PrevTxOutError) -> Self { InternalPsbtInputError::PrevTxOut(value) } +} + +impl From for InternalPsbtInputError { + fn from(value: AddressTypeError) -> Self { Self::AddressType(value) } +} + +#[derive(Debug)] +pub struct PsbtInputError(InternalPsbtInputError); + +impl From for PsbtInputError { + fn from(e: InternalPsbtInputError) -> Self { PsbtInputError(e) } +} + +impl fmt::Display for PsbtInputError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.0) } +} + +impl std::error::Error for PsbtInputError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { Some(&self.0) } } #[derive(Debug)] pub struct PsbtInputsError { index: usize, - error: PsbtInputError, + error: InternalPsbtInputError, } impl fmt::Display for PsbtInputsError { diff --git a/payjoin/src/receive/error.rs b/payjoin/src/receive/error.rs index a8479db6..08e90ede 100644 --- a/payjoin/src/receive/error.rs +++ b/payjoin/src/receive/error.rs @@ -289,8 +289,6 @@ pub struct InputContributionError(InternalInputContributionError); #[derive(Debug)] pub(crate) enum InternalInputContributionError { - /// Missing previous txout information - PrevTxOut(crate::psbt::PrevTxOutError), /// The address type could not be determined AddressType(crate::psbt::AddressTypeError), /// The original PSBT has no inputs @@ -304,8 +302,6 @@ pub(crate) enum InternalInputContributionError { impl fmt::Display for InputContributionError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self.0 { - InternalInputContributionError::PrevTxOut(e) => - write!(f, "Missing previous txout information: {}", e), InternalInputContributionError::AddressType(e) => write!(f, "The address type could not be determined: {}", e), InternalInputContributionError::NoSenderInputs => diff --git a/payjoin/src/receive/mod.rs b/payjoin/src/receive/mod.rs index 96174278..a7cad41a 100644 --- a/payjoin/src/receive/mod.rs +++ b/payjoin/src/receive/mod.rs @@ -28,8 +28,8 @@ use std::cmp::{max, min}; use bitcoin::base64::prelude::BASE64_STANDARD; use bitcoin::base64::Engine; -use bitcoin::psbt::{Input as PsbtInput, Psbt}; -use bitcoin::{Amount, FeeRate, OutPoint, Script, TxIn, TxOut, Weight}; +use bitcoin::psbt::Psbt; +use bitcoin::{psbt, AddressType, Amount, FeeRate, OutPoint, Script, TxIn, TxOut, Weight}; mod error; mod optional_parameters; @@ -47,12 +47,51 @@ use error::{ }; use optional_parameters::Params; -use crate::psbt::{InputPair, PsbtExt}; +pub use crate::psbt::PsbtInputError; +use crate::psbt::{InternalInputPair, InternalPsbtInputError, PsbtExt}; pub trait Headers { fn get_header(&self, key: &str) -> Option<&str>; } +/// Helper to construct a pair of (txin, psbtin) with some built-in validation +/// Use with [`InputPair::new`] to contribute receiver inputs. +#[derive(Clone, Debug)] +pub struct InputPair { + pub(crate) txin: TxIn, + pub(crate) psbtin: psbt::Input, +} + +impl InputPair { + pub fn new(txin: TxIn, psbtin: psbt::Input) -> Result { + let input_pair = Self { txin, psbtin }; + let raw = InternalInputPair::from(&input_pair); + raw.validate_utxo(true)?; + let address_type = raw.address_type().map_err(InternalPsbtInputError::AddressType)?; + if address_type == AddressType::P2sh && input_pair.psbtin.redeem_script.is_none() { + return Err(InternalPsbtInputError::NoRedeemScript.into()); + } + Ok(input_pair) + } + + pub(crate) fn address_type(&self) -> AddressType { + InternalInputPair::from(self) + .address_type() + .expect("address type should have been validated in InputPair::new") + } + + pub(crate) fn previous_txout(&self) -> TxOut { + InternalInputPair::from(self) + .previous_txout() + .expect("UTXO information should have been validated in InputPair::new") + .clone() + } +} + +impl<'a> From<&'a InputPair> for InternalInputPair<'a> { + fn from(pair: &'a InputPair) -> Self { Self { psbtin: &pair.psbtin, txin: &pair.txin } } +} + /// The sender's original PSBT and optional parameters /// /// This type is used to process the request. It is returned by @@ -459,8 +498,8 @@ impl WantsInputs { /// A simple consolidation is otherwise chosen if available. pub fn try_preserving_privacy( &self, - candidate_inputs: impl IntoIterator, - ) -> Result { + candidate_inputs: impl IntoIterator, + ) -> Result { let mut candidate_inputs = candidate_inputs.into_iter().peekable(); if candidate_inputs.peek().is_none() { return Err(InternalSelectionError::Empty.into()); @@ -486,8 +525,8 @@ impl WantsInputs { /// https://eprint.iacr.org/2022/589.pdf fn avoid_uih( &self, - candidate_inputs: impl IntoIterator, - ) -> Result { + candidate_inputs: impl IntoIterator, + ) -> Result { let min_original_out_sats = self .payjoin_psbt .unsigned_tx @@ -506,15 +545,15 @@ impl WantsInputs { let prior_payment_sats = self.payjoin_psbt.unsigned_tx.output[self.change_vout].value; - for candidate in candidate_inputs { - let candidate_sats = candidate.0; + for input_pair in candidate_inputs { + let candidate_sats = input_pair.previous_txout().value; let candidate_min_out = min(min_original_out_sats, prior_payment_sats + candidate_sats); let candidate_min_in = min(min_original_in_sats, candidate_sats); if candidate_min_in > candidate_min_out { // The candidate avoids UIH2 but conforms to UIH1: Optimal change heuristic. // It implies the smallest output is the sender's change address. - return Ok(candidate.1); + return Ok(input_pair); } } @@ -524,19 +563,16 @@ impl WantsInputs { fn select_first_candidate( &self, - candidate_inputs: impl IntoIterator, - ) -> Result { - candidate_inputs - .into_iter() - .next() - .map_or(Err(InternalSelectionError::NotFound.into()), |(_, outpoint)| Ok(outpoint)) + candidate_inputs: impl IntoIterator, + ) -> Result { + candidate_inputs.into_iter().next().ok_or(InternalSelectionError::NotFound.into()) } /// Add the provided list of inputs to the transaction. /// Any excess input amount is added to the change_vout output indicated previously. pub fn contribute_inputs( self, - inputs: impl IntoIterator, + inputs: impl IntoIterator, ) -> Result { let mut payjoin_psbt = self.payjoin_psbt.clone(); // The payjoin proposal must not introduce mixed input sequence numbers @@ -552,26 +588,20 @@ impl WantsInputs { // Insert contributions at random indices for privacy let mut rng = rand::thread_rng(); let mut receiver_input_amount = Amount::ZERO; - for (psbtin, txin) in inputs.into_iter() { - let input_pair = InputPair { txin: &txin, psbtin: &psbtin }; - let input_type = - input_pair.address_type().map_err(InternalInputContributionError::AddressType)?; - + for input_pair in inputs.into_iter() { + let input_type = input_pair.address_type(); if self.params.v == 1 { // v1 payjoin proposals must not introduce mixed input script types self.check_mixed_input_types(input_type, uniform_sender_input_type)?; } - receiver_input_amount += input_pair - .previous_txout() - .map_err(InternalInputContributionError::PrevTxOut)? - .value; + receiver_input_amount += input_pair.previous_txout().value; let index = rng.gen_range(0..=self.payjoin_psbt.unsigned_tx.input.len()); - payjoin_psbt.inputs.insert(index, psbtin); + payjoin_psbt.inputs.insert(index, input_pair.psbtin); payjoin_psbt .unsigned_tx .input - .insert(index, TxIn { sequence: original_sequence, ..txin }); + .insert(index, TxIn { sequence: original_sequence, ..input_pair.txin }); } // Add the receiver change amount to the receiver change output, if applicable @@ -968,8 +998,10 @@ mod test { proposal.params.min_feerate = FeeRate::from_sat_per_vb_unchecked(1000); // Input contribution for the receiver, from the BIP78 test vector let proposal_psbt = Psbt::from_str("cHNidP8BAJwCAAAAAo8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////jye60aAl3JgZdaIERvjkeh72VYZuTGH/ps2I4l0IO4MBAAAAAP7///8CJpW4BQAAAAAXqRQd6EnwadJ0FQ46/q6NcutaawlEMIcACT0AAAAAABepFHdAltvPSGdDwi9DR+m0af6+i2d6h9MAAAAAAAEBIICEHgAAAAAAF6kUyPLL+cphRyyI5GTUazV0hF2R2NWHAQcXFgAUX4BmVeWSTJIEwtUb5TlPS/ntohABCGsCRzBEAiBnu3tA3yWlT0WBClsXXS9j69Bt+waCs9JcjWtNjtv7VgIge2VYAaBeLPDB6HGFlpqOENXMldsJezF9Gs5amvDQRDQBIQJl1jz1tBt8hNx2owTm+4Du4isx0pmdKNMNIjjaMHFfrQAAAA==").unwrap(); - let input: (PsbtInput, TxIn) = - (proposal_psbt.inputs[1].clone(), proposal_psbt.unsigned_tx.input[1].clone()); + let input = InputPair { + txin: proposal_psbt.unsigned_tx.input[1].clone(), + psbtin: proposal_psbt.inputs[1].clone(), + }; let mut payjoin = proposal .assume_interactive_receiver() .check_inputs_not_owned(|_| Ok(false)) diff --git a/payjoin/src/receive/v2/mod.rs b/payjoin/src/receive/v2/mod.rs index 1447c899..2f897ba5 100644 --- a/payjoin/src/receive/v2/mod.rs +++ b/payjoin/src/receive/v2/mod.rs @@ -3,8 +3,8 @@ use std::time::{Duration, SystemTime}; use bitcoin::base64::prelude::BASE64_URL_SAFE_NO_PAD; use bitcoin::base64::Engine; -use bitcoin::psbt::{Input as PsbtInput, Psbt}; -use bitcoin::{Address, Amount, FeeRate, OutPoint, Script, TxIn, TxOut}; +use bitcoin::psbt::Psbt; +use bitcoin::{Address, FeeRate, OutPoint, Script, TxOut}; use serde::de::Deserializer; use serde::{Deserialize, Serialize}; use url::Url; @@ -18,6 +18,7 @@ use crate::hpke::{decrypt_message_a, encrypt_message_b, HpkeKeyPair, HpkePublicK use crate::ohttp::{ohttp_decapsulate, ohttp_encapsulate, OhttpEncapsulationError, OhttpKeys}; use crate::psbt::PsbtExt; use crate::receive::optional_parameters::Params; +use crate::receive::InputPair; use crate::{PjUriBuilder, Request}; pub(crate) mod error; @@ -387,8 +388,8 @@ impl WantsInputs { /// https://eprint.iacr.org/2022/589.pdf pub fn try_preserving_privacy( &self, - candidate_inputs: impl IntoIterator, - ) -> Result { + candidate_inputs: impl IntoIterator, + ) -> Result { self.inner.try_preserving_privacy(candidate_inputs) } @@ -396,7 +397,7 @@ impl WantsInputs { /// Any excess input amount is added to the change_vout output indicated previously. pub fn contribute_inputs( self, - inputs: impl IntoIterator, + inputs: impl IntoIterator, ) -> Result { let inner = self.inner.contribute_inputs(inputs)?; Ok(WantsInputs { inner, context: self.context }) diff --git a/payjoin/tests/integration.rs b/payjoin/tests/integration.rs index ce304c00..e4701792 100644 --- a/payjoin/tests/integration.rs +++ b/payjoin/tests/integration.rs @@ -12,6 +12,7 @@ mod integration { use bitcoind::bitcoincore_rpc::{self, RpcApi}; use log::{log_enabled, Level}; use once_cell::sync::{Lazy, OnceCell}; + use payjoin::receive::InputPair; use payjoin::send::SenderBuilder; use payjoin::{PjUri, PjUriBuilder, Request, Uri}; use tracing_subscriber::{EnvFilter, FmtSubscriber}; @@ -560,7 +561,7 @@ mod integration { // POST payjoin let proposal = session.process_res(response.bytes().await?.to_vec().as_slice(), ctx)?.unwrap(); - let inputs = receiver_utxos.iter().map(input_pair_from_list_unspent).collect(); + let inputs = receiver_utxos.into_iter().map(input_pair_from_list_unspent).collect(); let mut payjoin_proposal = handle_directory_proposal(&receiver, proposal, Some(inputs)); assert!(!payjoin_proposal.is_output_substitution_disabled()); @@ -831,7 +832,7 @@ mod integration { fn handle_directory_proposal( receiver: &bitcoincore_rpc::Client, proposal: UncheckedProposal, - custom_inputs: Option>, + custom_inputs: Option>, ) -> PayjoinProposal { // in a payment processor where the sender could go offline, this is where you schedule to broadcast the original_tx let _to_broadcast_in_failure_case = proposal.extract_tx_to_schedule_broadcast(); @@ -874,24 +875,18 @@ mod integration { let inputs = match custom_inputs { Some(inputs) => inputs, None => { - let available_inputs = - receiver.list_unspent(None, None, None, None, None).unwrap(); - let candidate_inputs: HashMap = available_inputs - .iter() - .map(|i| (i.amount, OutPoint { txid: i.txid, vout: i.vout })) - .collect(); - - let selected_outpoint = payjoin + let candidate_inputs = receiver + .list_unspent(None, None, None, None, None) + .unwrap() + .into_iter() + .map(input_pair_from_list_unspent); + let selected_input = payjoin .try_preserving_privacy(candidate_inputs) - .expect("Failed to make privacy preserving selection"); - let selected_utxo = available_inputs - .iter() - .find(|i| { - i.txid == selected_outpoint.txid && i.vout == selected_outpoint.vout + .map_err(|e| { + format!("Failed to make privacy preserving selection: {:?}", e) }) .unwrap(); - let input_pair = input_pair_from_list_unspent(selected_utxo); - vec![input_pair] + vec![selected_input] } }; let payjoin = payjoin.contribute_inputs(inputs).unwrap().commit_inputs(); @@ -1043,7 +1038,7 @@ mod integration { .script_pubkey(), }]; let drain_script = outputs[0].script_pubkey.clone(); - let inputs = receiver_utxos.iter().map(input_pair_from_list_unspent).collect(); + let inputs = receiver_utxos.into_iter().map(input_pair_from_list_unspent).collect(); let response = handle_v1_pj_request( req, headers, @@ -1246,7 +1241,7 @@ mod integration { receiver: &bitcoincore_rpc::Client, custom_outputs: Option>, drain_script: Option<&bitcoin::Script>, - custom_inputs: Option>, + custom_inputs: Option>, ) -> Result { // Receiver receive payjoin proposal, IRL it will be an HTTP request (over ssl or onion) let proposal = payjoin::receive::UncheckedProposal::from_request( @@ -1267,7 +1262,7 @@ mod integration { receiver: &bitcoincore_rpc::Client, custom_outputs: Option>, drain_script: Option<&bitcoin::Script>, - custom_inputs: Option>, + custom_inputs: Option>, ) -> Result { // in a payment processor where the sender could go offline, this is where you schedule to broadcast the original_tx let _to_broadcast_in_failure_case = proposal.extract_tx_to_schedule_broadcast(); @@ -1309,21 +1304,14 @@ mod integration { let inputs = match custom_inputs { Some(inputs) => inputs, None => { - let available_inputs = receiver.list_unspent(None, None, None, None, None)?; - let candidate_inputs: HashMap = available_inputs - .iter() - .map(|i| (i.amount, OutPoint { txid: i.txid, vout: i.vout })) - .collect(); - - let selected_outpoint = payjoin + let candidate_inputs = receiver + .list_unspent(None, None, None, None, None)? + .into_iter() + .map(input_pair_from_list_unspent); + let selected_input = payjoin .try_preserving_privacy(candidate_inputs) .map_err(|e| format!("Failed to make privacy preserving selection: {:?}", e))?; - let selected_utxo = available_inputs - .iter() - .find(|i| i.txid == selected_outpoint.txid && i.vout == selected_outpoint.vout) - .unwrap(); - let input_pair = input_pair_from_list_unspent(selected_utxo); - vec![input_pair] + vec![selected_input] } }; let payjoin = payjoin @@ -1388,8 +1376,8 @@ mod integration { } fn input_pair_from_list_unspent( - utxo: &bitcoind::bitcoincore_rpc::bitcoincore_rpc_json::ListUnspentResultEntry, - ) -> (PsbtInput, TxIn) { + utxo: bitcoind::bitcoincore_rpc::bitcoincore_rpc_json::ListUnspentResultEntry, + ) -> InputPair { let psbtin = PsbtInput { // NOTE: non_witness_utxo is not necessary because bitcoin-cli always supplies // witness_utxo, even for non-witness inputs @@ -1405,7 +1393,7 @@ mod integration { previous_output: OutPoint { txid: utxo.txid, vout: utxo.vout }, ..Default::default() }; - (psbtin, txin) + InputPair::new(txin, psbtin).expect("Input pair should be valid") } struct HeaderMock(HashMap);