Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 69 additions & 23 deletions payjoin-test-utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,42 +124,88 @@ pub fn local_cert_key() -> (Vec<u8>, Vec<u8>) {
(cert_der, key_der)
}

pub fn init_bitcoind_sender_receiver(
sender_address_type: Option<AddressType>,
receiver_address_type: Option<AddressType>,
) -> Result<(bitcoind::BitcoinD, bitcoincore_rpc::Client, bitcoincore_rpc::Client), BoxError> {
pub fn init_bitcoind() -> Result<bitcoind::BitcoinD, BoxError> {
let bitcoind_exe = env::var("BITCOIND_EXE")
.ok()
.or_else(|| bitcoind::downloaded_exe_path().ok())
.expect("bitcoind not found");
let mut conf = bitcoind::Conf::default();
conf.view_stdout = log_enabled!(Level::Debug);
let bitcoind = bitcoind::BitcoinD::with_conf(bitcoind_exe, &conf)?;
let receiver = bitcoind.create_wallet("receiver")?;
let receiver_address = receiver.get_new_address(None, receiver_address_type)?.assume_checked();
let sender = bitcoind.create_wallet("sender")?;
let sender_address = sender.get_new_address(None, sender_address_type)?.assume_checked();
bitcoind.client.generate_to_address(1, &receiver_address)?;
bitcoind.client.generate_to_address(101, &sender_address)?;

assert_eq!(
Amount::from_btc(50.0)?,
receiver.get_balances()?.mine.trusted,
"receiver doesn't own bitcoin"
);

assert_eq!(
Amount::from_btc(50.0)?,
sender.get_balances()?.mine.trusted,
"sender doesn't own bitcoin"
);
Ok((bitcoind, sender, receiver))
Ok(bitcoind)
}

pub fn init_bitcoind_sender_receiver(
sender_address_type: Option<AddressType>,
receiver_address_type: Option<AddressType>,
) -> Result<(bitcoind::BitcoinD, bitcoincore_rpc::Client, bitcoincore_rpc::Client), BoxError> {
let bitcoind = init_bitcoind()?;
let mut wallets = create_and_fund_wallets(
&bitcoind,
vec![("receiver", receiver_address_type), ("sender", sender_address_type)],
)?;
let receiver = wallets.pop().expect("receiver to exist");
let sender = wallets.pop().expect("sender to exist");

Ok((bitcoind, receiver, sender))
}

fn create_and_fund_wallets<W: AsRef<str>>(
bitcoind: &bitcoind::BitcoinD,
wallets: Vec<(W, Option<AddressType>)>,
) -> Result<Vec<bitcoincore_rpc::Client>, BoxError> {
let mut funded_wallets = vec![];
let funding_wallet = bitcoind.create_wallet("funding_wallet")?;
let funding_address = funding_wallet.get_new_address(None, None)?.assume_checked();
// 100 blocks would work here, we add a extra block to cover fees between transfers
bitcoind.client.generate_to_address(101 + wallets.len() as u64, &funding_address)?;
for (wallet_name, address_type) in wallets {
let wallet = bitcoind.create_wallet(wallet_name)?;
let address = wallet.get_new_address(None, address_type)?.assume_checked();
funding_wallet.send_to_address(
&address,
Amount::from_btc(50.0)?,
None,
None,
None,
None,
None,
None,
)?;
funded_wallets.push(wallet);
}
// Mine the block which funds the different wallets
bitcoind.client.generate_to_address(1, &funding_address)?;

for wallet in funded_wallets.iter() {
let balances = wallet.get_balances()?;
assert_eq!(
balances.mine.trusted,
Amount::from_btc(50.0)?,
"wallet doesn't have expected amount of bitcoin"
);
}

Ok(funded_wallets)
}

pub fn http_agent(cert_der: Vec<u8>) -> Result<Client, BoxSendSyncError> {
Ok(http_agent_builder(cert_der).build()?)
}

pub fn init_bitcoind_multi_sender_single_reciever(
number_of_senders: usize,
) -> Result<(bitcoind::BitcoinD, Vec<bitcoincore_rpc::Client>, bitcoincore_rpc::Client), BoxError> {
let bitcoind = init_bitcoind()?;
let wallets_to_create =
(0..number_of_senders + 1).map(|i| (format!("sender_{}", i), None)).collect::<Vec<_>>();
let mut wallets = create_and_fund_wallets(&bitcoind, wallets_to_create)?;
let receiver = wallets.pop().expect("reciever to exist");
let senders = wallets;

Ok((bitcoind, senders, receiver))
}

fn http_agent_builder(cert_der: Vec<u8>) -> ClientBuilder {
ClientBuilder::new().danger_accept_invalid_certs(true).use_rustls_tls().add_root_certificate(
reqwest::tls::Certificate::from_der(cert_der.as_slice())
Expand Down
2 changes: 1 addition & 1 deletion payjoin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ default = ["v2"]
#[doc = "Core features for payjoin state machines"]
_core = ["bitcoin/rand", "serde_json", "url", "bitcoin_uri"]
directory = []
psbt-merge = []
v1 = ["_core"]
v2 = ["_core", "bitcoin/serde", "hpke", "dep:http", "bhttp", "ohttp", "serde", "url/serde", "directory"]
#[doc = "Functions to fetch OHTTP keys via CONNECT proxy using reqwest. Enables `v2` since only `v2` uses OHTTP."]
io = ["v2", "reqwest/rustls-tls"]
_danger-local-https = ["reqwest/rustls-tls", "rustls"]
_multiparty = ["v2"]

[dependencies]
bitcoin = { version = "0.32.5", features = ["base64"] }
Expand Down
9 changes: 7 additions & 2 deletions payjoin/src/psbt/merge.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
//! Utilities for merging unique v0 PSBTs
use bitcoin::Psbt;

#[allow(dead_code)]
/// Try to merge two PSBTs
/// PSBTs here should not have the same unsigned tx
/// if you do have the same unsigned tx, use `combine` instead
Expand All @@ -14,7 +13,13 @@ pub(crate) fn merge_unsigned_tx(acc: Psbt, psbt: Psbt) -> Psbt {
unsigned_tx.input.dedup_by_key(|input| input.previous_output);
unsigned_tx.output.extend(psbt.unsigned_tx.output);

Psbt::from_unsigned_tx(unsigned_tx).expect("pulling from unsigned tx above")
let mut merged_psbt =
Psbt::from_unsigned_tx(unsigned_tx).expect("pulling from unsigned tx above");
let zip = acc.inputs.iter().chain(psbt.inputs.iter()).collect::<Vec<_>>();
merged_psbt.inputs.iter_mut().enumerate().for_each(|(i, input)| {
input.witness_utxo = zip[i].witness_utxo.clone();
});
merged_psbt
}

#[cfg(test)]
Expand Down
2 changes: 1 addition & 1 deletion payjoin/src/psbt/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Utilities to make work with PSBTs easier

#[cfg(feature = "psbt-merge")]
#[cfg(feature = "_multiparty")]
pub(crate) mod merge;

use std::collections::BTreeMap;
Expand Down
2 changes: 2 additions & 0 deletions payjoin/src/receive/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ use crate::psbt::{InternalInputPair, InternalPsbtInputError, PsbtExt};
mod error;
pub(crate) mod optional_parameters;

#[cfg(feature = "_multiparty")]
pub mod multiparty;
#[cfg(feature = "v1")]
#[cfg_attr(docsrs, doc(cfg(feature = "v1")))]
pub mod v1;
Expand Down
56 changes: 56 additions & 0 deletions payjoin/src/receive/multiparty/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use core::fmt;
use std::error;

#[derive(Debug)]
pub struct MultipartyError(InternalMultipartyError);

#[derive(Debug)]
pub(crate) enum InternalMultipartyError {
/// Not enough proposals
NotEnoughProposals,
/// Proposal version not supported
ProposalVersionNotSupported(usize),
/// Optimistic merge not supported
OptimisticMergeNotSupported,
/// Bitcoin Internal Error
BitcoinExtractTxError(Box<bitcoin::psbt::ExtractTxError>),
/// Input in Finalized Proposal is missing witness or script_sig
InputMissingWitnessOrScriptSig,
/// Failed to combine psbts
FailedToCombinePsbts(bitcoin::psbt::Error),
}

impl From<InternalMultipartyError> for MultipartyError {
fn from(e: InternalMultipartyError) -> Self { MultipartyError(e) }
}

impl fmt::Display for MultipartyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self.0 {
InternalMultipartyError::NotEnoughProposals => write!(f, "Not enough proposals"),
InternalMultipartyError::ProposalVersionNotSupported(v) =>
write!(f, "Proposal version not supported: {}", v),
InternalMultipartyError::OptimisticMergeNotSupported =>
write!(f, "Optimistic merge not supported"),
InternalMultipartyError::BitcoinExtractTxError(e) =>
write!(f, "Bitcoin extract tx error: {:?}", e),
InternalMultipartyError::InputMissingWitnessOrScriptSig =>
write!(f, "Input in Finalized Proposal is missing witness or script_sig"),
InternalMultipartyError::FailedToCombinePsbts(e) =>
write!(f, "Failed to combine psbts: {:?}", e),
}
}
}

impl error::Error for MultipartyError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match &self.0 {
InternalMultipartyError::NotEnoughProposals => None,
InternalMultipartyError::ProposalVersionNotSupported(_) => None,
InternalMultipartyError::OptimisticMergeNotSupported => None,
InternalMultipartyError::BitcoinExtractTxError(e) => Some(e),
InternalMultipartyError::InputMissingWitnessOrScriptSig => None,
InternalMultipartyError::FailedToCombinePsbts(e) => Some(e),
}
}
}
Loading
Loading