From a17e553a74b4ab7b31e71beac7b5e8fbf5fc75a2 Mon Sep 17 00:00:00 2001 From: BitcoinZavior Date: Sun, 16 Jul 2023 16:05:43 -0400 Subject: [PATCH 01/30] implement display trait for Error --- src/error.rs | 100 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 72 insertions(+), 28 deletions(-) diff --git a/src/error.rs b/src/error.rs index 45d9a27..cf90de4 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,36 +1,23 @@ -#[derive(Debug)] -pub struct ValidationError { - internal: InternalValidationError, -} +use std::fmt; -impl From for ValidationError { - fn from(value: InternalValidationError) -> Self { - ValidationError { internal: value } - } -} -impl From for InternalValidationError { - fn from(value: InputTypeError) -> Self { - InternalValidationError::InvalidInputType(value) - } -} -#[derive(Debug)] -pub(crate) enum InternalValidationError { - Psbt(bitcoin::psbt::PsbtParseError), - Io(std::io::Error), +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum Error { + Psbt(String), + Io(String), InvalidInput, - Type(InputTypeError), - InvalidProposedInput(crate::psbt::PrevTxOutError), + Type(String), + InvalidProposedInput(String), VersionsDontMatch { proposed: i32, original: i32, }, LockTimesDontMatch { - proposed: LockTime, - original: LockTime, + proposed: usize, + original: usize, }, SenderTxinSequenceChanged { - proposed: Sequence, - original: Sequence, + proposed: usize, + original: usize, }, SenderTxinContainsNonWitnessUtxo, SenderTxinContainsWitnessUtxo, @@ -42,8 +29,8 @@ pub(crate) enum InternalValidationError { ReceiverTxinMissingUtxoInfo, MixedSequence, MixedInputTypes { - proposed: InputType, - original: InputType, + proposed: String, + original: String, }, MissingOrShuffledInputs, TxOutContainsKeyPaths, @@ -51,9 +38,66 @@ pub(crate) enum InternalValidationError { DisallowedOutputSubstitution, OutputValueDecreased, MissingOrShuffledOutputs, - Inflation, AbsoluteFeeDecreased, PayeeTookContributedFee, - FeeContributionPaysOutputSizeIncrease, FeeRateBelowMinimum, + ReceiveError(String), + PjParseError(String), + ///Error that may occur when the request from sender is malformed. + ///This is currently opaque type because we aren’t sure which variants will stay. You can only display it. + RequestError(String), + ///Error that may occur when coin selection fails. + SelectionError(String), + ///Error returned when request could not be created. + ///This error can currently only happen due to programmer mistake. + CreateRequestError(String), +} +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::Psbt(e) => write!(f, "Psbt error:{}", e), + Error::Io(e) => write!(f, "Io error:{}", e), + Error::InvalidInput => write!(f, "Invalid input"), + Error::Type(e) => write!(f, "Type error:{}", e), + Error::InvalidProposedInput(e) => write!(f, "Invalid proposed input:{}", e), + Error::VersionsDontMatch { proposed, original } => + write!(f, "Version mismatch: proposed: {proposed}, original: {original} "), + Error::LockTimesDontMatch { proposed, original } => + write!(f, "LockTimes mismatch: proposed: {proposed}, original: {original} "), + Error::SenderTxinSequenceChanged { proposed, original } => + write!( + f, + "SenderTxinSequence changed: proposed: {proposed}, original: {original} " + ), + Error::SenderTxinContainsNonWitnessUtxo => + write!(f, "Sender txin contains non-witness utxo"), + Error::SenderTxinContainsWitnessUtxo => write!(f, "Sender txin contains witness utxo"), + Error::SenderTxinContainsFinalScriptSig => + write!(f, "Sender txin contains final script sig"), + Error::SenderTxinContainsFinalScriptWitness => + write!(f, "Sender txin contains final script witness"), + Error::TxInContainsKeyPaths => write!(f, "Txin contains keyP paths"), + Error::ContainsPartialSigs => write!(f, "Contains partial sigs"), + Error::ReceiverTxinNotFinalized => write!(f, "Receiver txin not finalized"), + Error::ReceiverTxinMissingUtxoInfo => write!(f, "Receiver txin missing utxo info"), + Error::MixedSequence => write!(f, "Missed sequence"), + Error::MixedInputTypes { proposed, original } => + write!(f, "Mixed input types: proposed: {proposed}, original: {original} "), + Error::MissingOrShuffledInputs => write!(f, "Missing or shuffled inputs"), + Error::TxOutContainsKeyPaths => write!(f, "Tx out contains key paths"), + Error::FeeContributionExceedsMaximum => write!(f, "Fee contribution exceeds maximum"), + Error::DisallowedOutputSubstitution => write!(f, "Output substited is not allowed"), + Error::OutputValueDecreased => write!(f, "Output value decreased"), + Error::MissingOrShuffledOutputs => write!(f, "Missing or shuffled outputs"), + Error::AbsoluteFeeDecreased => write!(f, "Absolute fee decreased"), + Error::PayeeTookContributedFee => write!(f, "The payee took the contribution fees"), + Error::FeeRateBelowMinimum => write!(f, "Fee rate is below minimum"), + Error::ReceiveError(e) => write!(f, "ReceiveError: {}", e), + Error::RequestError(e) => write!(f, "RequestError: {}", e), + Error::SelectionError(e) => write!(f, "SelectionError: {}", e), + Error::CreateRequestError(e) => write!(f, "CreateRequestError: {}", e), + Error::PjParseError(e) => write!(f, "PjParseError: {}", e), + } + } } +impl std::error::Error for Error {} From a3297f524e893eb84d6d581949e76c37ccf8f782 Mon Sep 17 00:00:00 2001 From: BitcoinZavior Date: Sat, 22 Jul 2023 22:11:09 -0400 Subject: [PATCH 02/30] Transaction & Error structs updated --- Cargo.toml | 3 +- src/bitcoind.rs | 30 ++++++++++++- src/error.rs | 103 +++++++++++---------------------------------- src/lib.rs | 21 +++------ src/receive.rs | 69 ++++++++++++++++++++++++++++++ src/transaction.rs | 28 ++++++++++++ 6 files changed, 158 insertions(+), 96 deletions(-) create mode 100644 src/receive.rs create mode 100644 src/transaction.rs diff --git a/Cargo.toml b/Cargo.toml index 3eeb642..fd848ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "payjoin-ffi" +name = "pdk-ffi" version = "0.1.0" license = "MIT OR Apache-2.0" edition = "2021" @@ -20,6 +20,7 @@ rcgen = { version = "0.11.1", optional = true } serde = { version = "1.0.160", features = ["derive"] } payjoin = { version = "0.9.0", features = ["send", "receive", "rand"] } bitcoin = "0.30.1" +serde_json = "1.0.103" [profile.release] panic = "abort" diff --git a/src/bitcoind.rs b/src/bitcoind.rs index 96f55f4..33db041 100644 --- a/src/bitcoind.rs +++ b/src/bitcoind.rs @@ -1,4 +1,4 @@ -use std::{ sync::{ Mutex, Arc, MutexGuard }, collections::HashMap }; +use std::{ sync::{ Mutex, Arc, MutexGuard }, collections::HashMap, str::FromStr }; use bitcoin::Amount; use serde::{ Deserialize, Serialize }; @@ -125,3 +125,31 @@ impl BitcoindClient { } } } + +pub struct Txid { + pub internal: String, +} +impl From for Txid { + fn from(value: bitcoin::hash_types::Txid) -> Self { + Txid { internal: value.to_string() } + } +} +impl From for bitcoin::hash_types::Txid { + fn from(value: Txid) -> Self { + bitcoin::hash_types::Txid::from_str(value.internal.as_str()).expect("Invalid Txid") + } +} +pub struct OutPoint { + pub txid: Txid, + pub vout: u32, +} +impl OutPoint { + pub fn new(txid: Txid, vout: u32) -> Self { + Self { txid: txid, vout: vout } + } +} +impl From for bitcoin::blockdata::transaction::OutPoint { + fn from(value: OutPoint) -> Self { + bitcoin::blockdata::transaction::OutPoint { txid: value.txid.into(), vout: value.vout } + } +} diff --git a/src/error.rs b/src/error.rs index cf90de4..0d38b6d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,48 +1,13 @@ use std::fmt; +use bitcoin::psbt::PsbtParseError; +use payjoin::receive::RequestError; + #[derive(Debug, PartialEq, Eq)] -pub(crate) enum Error { - Psbt(String), - Io(String), - InvalidInput, - Type(String), - InvalidProposedInput(String), - VersionsDontMatch { - proposed: i32, - original: i32, - }, - LockTimesDontMatch { - proposed: usize, - original: usize, - }, - SenderTxinSequenceChanged { - proposed: usize, - original: usize, - }, - SenderTxinContainsNonWitnessUtxo, - SenderTxinContainsWitnessUtxo, - SenderTxinContainsFinalScriptSig, - SenderTxinContainsFinalScriptWitness, - TxInContainsKeyPaths, - ContainsPartialSigs, - ReceiverTxinNotFinalized, - ReceiverTxinMissingUtxoInfo, - MixedSequence, - MixedInputTypes { - proposed: String, - original: String, - }, - MissingOrShuffledInputs, - TxOutContainsKeyPaths, - FeeContributionExceedsMaximum, - DisallowedOutputSubstitution, - OutputValueDecreased, - MissingOrShuffledOutputs, - AbsoluteFeeDecreased, - PayeeTookContributedFee, - FeeRateBelowMinimum, +pub enum Error { + /// Error encountered during PSBT decoding from Base64 string. + PsbtParseError(String), ReceiveError(String), - PjParseError(String), ///Error that may occur when the request from sender is malformed. ///This is currently opaque type because we aren’t sure which variants will stay. You can only display it. RequestError(String), @@ -51,53 +16,35 @@ pub(crate) enum Error { ///Error returned when request could not be created. ///This error can currently only happen due to programmer mistake. CreateRequestError(String), + PjParseError(String), + UnexpectedError(String), } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Error::Psbt(e) => write!(f, "Psbt error:{}", e), - Error::Io(e) => write!(f, "Io error:{}", e), - Error::InvalidInput => write!(f, "Invalid input"), - Error::Type(e) => write!(f, "Type error:{}", e), - Error::InvalidProposedInput(e) => write!(f, "Invalid proposed input:{}", e), - Error::VersionsDontMatch { proposed, original } => - write!(f, "Version mismatch: proposed: {proposed}, original: {original} "), - Error::LockTimesDontMatch { proposed, original } => - write!(f, "LockTimes mismatch: proposed: {proposed}, original: {original} "), - Error::SenderTxinSequenceChanged { proposed, original } => - write!( - f, - "SenderTxinSequence changed: proposed: {proposed}, original: {original} " - ), - Error::SenderTxinContainsNonWitnessUtxo => - write!(f, "Sender txin contains non-witness utxo"), - Error::SenderTxinContainsWitnessUtxo => write!(f, "Sender txin contains witness utxo"), - Error::SenderTxinContainsFinalScriptSig => - write!(f, "Sender txin contains final script sig"), - Error::SenderTxinContainsFinalScriptWitness => - write!(f, "Sender txin contains final script witness"), - Error::TxInContainsKeyPaths => write!(f, "Txin contains keyP paths"), - Error::ContainsPartialSigs => write!(f, "Contains partial sigs"), - Error::ReceiverTxinNotFinalized => write!(f, "Receiver txin not finalized"), - Error::ReceiverTxinMissingUtxoInfo => write!(f, "Receiver txin missing utxo info"), - Error::MixedSequence => write!(f, "Missed sequence"), - Error::MixedInputTypes { proposed, original } => - write!(f, "Mixed input types: proposed: {proposed}, original: {original} "), - Error::MissingOrShuffledInputs => write!(f, "Missing or shuffled inputs"), - Error::TxOutContainsKeyPaths => write!(f, "Tx out contains key paths"), - Error::FeeContributionExceedsMaximum => write!(f, "Fee contribution exceeds maximum"), - Error::DisallowedOutputSubstitution => write!(f, "Output substited is not allowed"), - Error::OutputValueDecreased => write!(f, "Output value decreased"), - Error::MissingOrShuffledOutputs => write!(f, "Missing or shuffled outputs"), - Error::AbsoluteFeeDecreased => write!(f, "Absolute fee decreased"), - Error::PayeeTookContributedFee => write!(f, "The payee took the contribution fees"), - Error::FeeRateBelowMinimum => write!(f, "Fee rate is below minimum"), Error::ReceiveError(e) => write!(f, "ReceiveError: {}", e), Error::RequestError(e) => write!(f, "RequestError: {}", e), Error::SelectionError(e) => write!(f, "SelectionError: {}", e), Error::CreateRequestError(e) => write!(f, "CreateRequestError: {}", e), Error::PjParseError(e) => write!(f, "PjParseError: {}", e), + Error::PsbtParseError(e) => write!(f, "PsbtParseError: {}", e), + Error::UnexpectedError(e) => write!(f, "UnexpectedError: {}", e), } } } impl std::error::Error for Error {} +impl From for Error { + fn from(value: RequestError) -> Self { + Error::RequestError(value.to_string()) + } +} +impl From for Error { + fn from(value: PsbtParseError) -> Self { + Error::PsbtParseError(value.to_string()) + } +} +impl From for Error { + fn from(value: payjoin::Error) -> Self { + Error::UnexpectedError(value.to_string()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 7253b70..f68f8f7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,12 @@ pub mod send; pub mod bitcoind; +pub mod error; +pub mod receive; +pub mod transaction; use std::{ sync::Mutex, collections::HashSet, fs::OpenOptions, str::FromStr }; -use bitcoin::{ Amount, psbt::PsbtParseError }; -use payjoin::bitcoin::psbt; +use bitcoin::{ Amount, psbt::{ PsbtParseError, self } }; + use serde::{ Deserialize, Serialize }; pub struct CachedOutputs { @@ -105,17 +108,3 @@ impl From<&Input> for bitcoincore_rpc::json::CreateRawTransactionInput { } } } - -pub struct PartiallySignedTransaction { - pub(crate) internal: Mutex, -} -impl PartiallySignedTransaction { - pub(crate) fn new(psbt_base64: String) -> Result { - let psbt: psbt::PartiallySignedTransaction = psbt::PartiallySignedTransaction::from_str( - &psbt_base64 - )?; - Ok(PartiallySignedTransaction { - internal: Mutex::new(psbt), - }) - } -} diff --git a/src/receive.rs b/src/receive.rs new file mode 100644 index 0000000..52c387e --- /dev/null +++ b/src/receive.rs @@ -0,0 +1,69 @@ +use anyhow::Ok; +use payjoin::receive::{ + MaybeInputsOwned as PdkMaybeInputsOwned, + MaybeMixedInputScripts as PdkMaybeMixedInputScripts, + MaybeInputsSeen as PdkMaybeInputsSeen, + OutputsUnknown as PdkOutputsUnknown, + PayjoinProposal as PdkPayjoinProposal, + UncheckedProposal as PdkUncheckedProposal, + Headers, +}; +use bitcoin::blockdata::transaction::Transaction as BitcoinTransaction; +use crate::{ bitcoind::OutPoint, error::Error, transaction::Transaction }; + +pub struct MaybeInputsOwned { + pub internal: PdkMaybeInputsOwned, +} + +pub struct MaybeMixedInputScripts { + pub internal: PdkMaybeMixedInputScripts, +} +///Typestate to validate that the Original PSBT has no inputs that have been seen before. +///Call check_no_inputs_seen to proceed. +pub struct MaybeInputsSeen { + pub internal: PdkMaybeInputsSeen, +} + +impl MaybeInputsSeen {} + +///The receiver has not yet identified which outputs belong to the receiver. +///Only accept PSBTs that send us money. Identify those outputs with identify_receiver_outputs() to proceed +pub struct OutputsUnknown { + pub internal: PdkOutputsUnknown, +} + +impl OutputsUnknown {} + +///A mutable checked proposal that the receiver may contribute inputs to to make a payjoin. +pub struct PayjoinProposal { + pub internal: PdkPayjoinProposal, +} +impl PayjoinProposal { + // pub fn utxos_to_be_locked(&self) -> impl '_ + Iterator {} +} + +pub struct UncheckedProposal { + pub internal: PdkUncheckedProposal, +} + +// impl UncheckedProposal { +// pub fn from_request( +// body: impl std::io::Read, +// query: String, +// headers: impl Headers +// ) -> Result { +// let res = PdkUncheckedProposal::from_request(body, query.as_str(), headers)?; +// Ok(UncheckedProposal { internal: res }) +// } +// pub fn get_transaction_to_schedule_broadcast(&self) -> Transaction { +// let res = self.internal.get_transaction_to_schedule_broadcast(); +// Transaction { internal: res } +// } +// // pub fn check_can_broadcast( +// // self, +// // can_broadcast: impl Fn(&BitcoinTransaction) -> Result +// // ) -> Result { +// // let res = self.internal.check_can_broadcast(can_broadcast)?; +// // Ok(MaybeInputsOwned { internal: res }) +// // } +// } diff --git a/src/transaction.rs b/src/transaction.rs new file mode 100644 index 0000000..43b149a --- /dev/null +++ b/src/transaction.rs @@ -0,0 +1,28 @@ +use std::{ sync::Mutex, str::FromStr }; +use bitcoin::blockdata::transaction::Transaction as BitcoinTransaction; +use bitcoin::psbt; + +use crate::error::Error; + +pub struct PartiallySignedTransaction { + pub(crate) internal: Mutex, +} +impl PartiallySignedTransaction { + pub(crate) fn new(psbt_base64: String) -> Result { + let psbt: psbt::PartiallySignedTransaction = psbt::PartiallySignedTransaction::from_str( + &psbt_base64 + )?; + Ok(PartiallySignedTransaction { + internal: Mutex::new(psbt), + }) + } +} +pub struct Transaction { + pub(crate) internal: BitcoinTransaction, +} + +// impl From for BitcoinTransaction { +// fn from(value: Transaction) -> Self { +// BitcoinTransaction:: +// } +// } From e5b40f0fb2db593a3a401db47b83ec6105440fbe Mon Sep 17 00:00:00 2001 From: BitcoinZavior Date: Tue, 25 Jul 2023 19:00:15 -0400 Subject: [PATCH 03/30] code clean up --- src/transaction.rs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/transaction.rs b/src/transaction.rs index 43b149a..63f35f0 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -1,28 +1,20 @@ use std::{ sync::Mutex, str::FromStr }; use bitcoin::blockdata::transaction::Transaction as BitcoinTransaction; -use bitcoin::psbt; +use bitcoin::psbt::PartiallySignedTransaction as BitcoinPsbt; use crate::error::Error; pub struct PartiallySignedTransaction { - pub(crate) internal: Mutex, + pub(crate) internal: Mutex, } impl PartiallySignedTransaction { pub(crate) fn new(psbt_base64: String) -> Result { - let psbt: psbt::PartiallySignedTransaction = psbt::PartiallySignedTransaction::from_str( - &psbt_base64 - )?; + let psbt: BitcoinPsbt = BitcoinPsbt::from_str(&psbt_base64)?; Ok(PartiallySignedTransaction { internal: Mutex::new(psbt), }) } } pub struct Transaction { - pub(crate) internal: BitcoinTransaction, + pub(crate) internal: Mutex, } - -// impl From for BitcoinTransaction { -// fn from(value: Transaction) -> Self { -// BitcoinTransaction:: -// } -// } From a4853b6257c879f257fecb31a78b32b2effc5951 Mon Sep 17 00:00:00 2001 From: BitcoinZavior Date: Wed, 26 Jul 2023 12:45:12 -0400 Subject: [PATCH 04/30] is_address_mine() exposed --- src/bitcoind.rs | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/bitcoind.rs b/src/bitcoind.rs index 33db041..a794abe 100644 --- a/src/bitcoind.rs +++ b/src/bitcoind.rs @@ -1,11 +1,11 @@ use std::{ sync::{ Mutex, Arc, MutexGuard }, collections::HashMap, str::FromStr }; use bitcoin::Amount; -use serde::{ Deserialize, Serialize }; +use serde::Deserialize; use bitcoincore_rpc::{ self, RpcApi }; use crate::{ CachedOutputs, Input, AddressType }; -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Clone)] pub struct BitcoindConfig { pub rpc_host: String, pub cookie: Option, @@ -13,9 +13,10 @@ pub struct BitcoindConfig { pub rpc_pass: String, pub cache_dir: String, } +#[derive(Clone)] pub struct BitcoindClient { pub config: BitcoindConfig, - pub bitcoind_mutex: Mutex, + pub bitcoind_mutex: Arc>, pub cached_outputs: Arc>, } impl BitcoindClient { @@ -38,7 +39,7 @@ impl BitcoindClient { } ).expect("Failed to connect to bitcoind"); let seen_input = Arc::new(Mutex::new(CachedOutputs::new(config.cache_dir.clone())?)); - let bitcoind_mutex = Mutex::new(bitcoind); + let bitcoind_mutex = Arc::new(Mutex::new(bitcoind)); Ok(Self { config, bitcoind_mutex, cached_outputs: seen_input }) } fn get_rpc_client(&self) -> MutexGuard { @@ -107,7 +108,6 @@ impl BitcoindClient { Err(e) => panic!("{:?}", e), }; } - pub fn get_new_address( &self, label: Option<&str>, @@ -124,6 +124,20 @@ impl BitcoindClient { Err(e) => panic!("{:?}", e), } } + pub fn is_address_mine( + self, + script: &bitcoin::Script, + network: bitcoin::Network + ) -> Result { + if let Ok(address) = bitcoin::Address::from_script(script, network) { + self.get_rpc_client() + .get_address_info(&address) + .map(|info| info.is_mine.unwrap_or(false)) + .map_err(|e| payjoin::Error::Server(e.into())) + } else { + Ok(false) + } + } } pub struct Txid { From 0a2d15f3afad26341fe6da3ed92dbd7ab94ce97e Mon Sep 17 00:00:00 2001 From: BitcoinZavior Date: Fri, 28 Jul 2023 07:08:51 -0400 Subject: [PATCH 05/30] UncheckedProposal exposed --- src/receive.rs | 99 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 70 insertions(+), 29 deletions(-) diff --git a/src/receive.rs b/src/receive.rs index 52c387e..b79bdf2 100644 --- a/src/receive.rs +++ b/src/receive.rs @@ -1,4 +1,6 @@ -use anyhow::Ok; +use std::sync::Mutex; + +use bitcoin::Script; use payjoin::receive::{ MaybeInputsOwned as PdkMaybeInputsOwned, MaybeMixedInputScripts as PdkMaybeMixedInputScripts, @@ -8,13 +10,78 @@ use payjoin::receive::{ UncheckedProposal as PdkUncheckedProposal, Headers, }; -use bitcoin::blockdata::transaction::Transaction as BitcoinTransaction; -use crate::{ bitcoind::OutPoint, error::Error, transaction::Transaction }; +use crate::{ PdkError, transaction::Transaction }; +use anyhow::anyhow; + +pub struct UncheckedProposal { + pub internal: PdkUncheckedProposal, +} +pub trait CanBroadcast { + fn test_mempool_accept( + &self, + tx_hex: Vec + ) -> Result< + Vec, + bitcoincore_rpc::Error + >; +} +impl UncheckedProposal { + pub fn from_request( + body: impl std::io::Read, + query: String, + headers: impl Headers + ) -> Result { + let res = PdkUncheckedProposal::from_request(body, query.as_str(), headers)?; + Ok(UncheckedProposal { internal: res }) + } + // fn get_internal(&self) -> MutexGuard { + // self.internal.lock().expect("PdkUncheckedProposal") + // } + pub fn get_transaction_to_schedule_broadcast(&self) -> Transaction { + let res = self.internal.get_transaction_to_schedule_broadcast(); + Transaction { internal: Mutex::new(res) } + } + pub fn check_can_broadcast( + self, + can_broadcast: Box + ) -> Result { + let res = self.internal.check_can_broadcast(|tx| { + let raw_tx = hex::encode(bitcoin::consensus::encode::serialize(&tx)); + let mempool_results = can_broadcast + .test_mempool_accept(vec![raw_tx]) + .map_err(|e| PdkError::Server(e.into()))?; + match mempool_results.first() { + Some(result) => Ok(result.allowed), + None => + Err( + PdkError::Server( + anyhow!("No mempool results returned on broadcast check").into() + ) + ), + } + })?; + Ok(MaybeInputsOwned { internal: res }) + } +} pub struct MaybeInputsOwned { pub internal: PdkMaybeInputsOwned, } +pub trait IsScriptOwned { + fn is_owned(&self, script: &Script) -> Result; +} +impl MaybeInputsOwned { + pub fn check_inputs_not_owned( + self, + is_owned: Box + ) -> Result { + match self.internal.check_inputs_not_owned(|input| is_owned.is_owned(input)) { + Ok(e) => Ok(MaybeMixedInputScripts { internal: e }), + Err(e) => Err(e), + } + } +} pub struct MaybeMixedInputScripts { pub internal: PdkMaybeMixedInputScripts, } @@ -41,29 +108,3 @@ pub struct PayjoinProposal { impl PayjoinProposal { // pub fn utxos_to_be_locked(&self) -> impl '_ + Iterator {} } - -pub struct UncheckedProposal { - pub internal: PdkUncheckedProposal, -} - -// impl UncheckedProposal { -// pub fn from_request( -// body: impl std::io::Read, -// query: String, -// headers: impl Headers -// ) -> Result { -// let res = PdkUncheckedProposal::from_request(body, query.as_str(), headers)?; -// Ok(UncheckedProposal { internal: res }) -// } -// pub fn get_transaction_to_schedule_broadcast(&self) -> Transaction { -// let res = self.internal.get_transaction_to_schedule_broadcast(); -// Transaction { internal: res } -// } -// // pub fn check_can_broadcast( -// // self, -// // can_broadcast: impl Fn(&BitcoinTransaction) -> Result -// // ) -> Result { -// // let res = self.internal.check_can_broadcast(can_broadcast)?; -// // Ok(MaybeInputsOwned { internal: res }) -// // } -// } From d2d65578ecc5221e0c298053a0dda0ffd4a5b35d Mon Sep 17 00:00:00 2001 From: BitcoinZavior Date: Fri, 28 Jul 2023 11:12:36 -0400 Subject: [PATCH 06/30] initial uniffi config added --- Cargo.toml | 22 +++++++++++++++++----- build.rs | 3 +++ src/pdk.udl | 3 +++ uniffi-bindgen.rs | 3 +++ uniffi.toml | 2 ++ 5 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 build.rs create mode 100644 src/pdk.udl create mode 100644 uniffi-bindgen.rs create mode 100644 uniffi.toml diff --git a/Cargo.toml b/Cargo.toml index fd848ce..83466c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,16 @@ version = "0.1.0" license = "MIT OR Apache-2.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +crate-type = ["lib", "staticlib", "cdylib"] +name = "pdkFfi" + +[build-dependencies] +uniffi = { version = "0.24.3", features = ["build"] } + +[dev-dependencies] +uniffi = { version = "0.24.3", features = ["bindgen-tests"] } +assert_matches = "1.5.0" [dependencies] anyhow = "1.0.70" @@ -21,9 +30,12 @@ serde = { version = "1.0.160", features = ["derive"] } payjoin = { version = "0.9.0", features = ["send", "receive", "rand"] } bitcoin = "0.30.1" serde_json = "1.0.103" +uniffi = "0.24.3" +hex = "0.4.3" -[profile.release] -panic = "abort" +[[bin]] +name = "uniffi-bindgen" +path = "uniffi-bindgen.rs" -[profile.dev] -panic = "abort" +[features] +default = ["uniffi/cli"] diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..89bd6d8 --- /dev/null +++ b/build.rs @@ -0,0 +1,3 @@ +fn main() { + uniffi::generate_scaffolding("./src/pdk.udl").unwrap(); +} diff --git a/src/pdk.udl b/src/pdk.udl new file mode 100644 index 0000000..77b91e0 --- /dev/null +++ b/src/pdk.udl @@ -0,0 +1,3 @@ +namespace pdk { + +}; diff --git a/uniffi-bindgen.rs b/uniffi-bindgen.rs new file mode 100644 index 0000000..f6cff6c --- /dev/null +++ b/uniffi-bindgen.rs @@ -0,0 +1,3 @@ +fn main() { + uniffi::uniffi_bindgen_main() +} diff --git a/uniffi.toml b/uniffi.toml new file mode 100644 index 0000000..7a65bcd --- /dev/null +++ b/uniffi.toml @@ -0,0 +1,2 @@ +[bindings.python] +cdylib_name = "pdkFfi" \ No newline at end of file From 939f0667ae08800492bb2c1711f2fd3353913bdc Mon Sep 17 00:00:00 2001 From: BitcoinZavior Date: Fri, 28 Jul 2023 14:41:34 -0400 Subject: [PATCH 07/30] code clean up --- src/lib.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f68f8f7..03fe291 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,12 +3,11 @@ pub mod bitcoind; pub mod error; pub mod receive; pub mod transaction; -use std::{ sync::Mutex, collections::HashSet, fs::OpenOptions, str::FromStr }; - -use bitcoin::{ Amount, psbt::{ PsbtParseError, self } }; - +use std::{ collections::HashSet, fs::OpenOptions, str::FromStr }; +use bitcoin::Amount; use serde::{ Deserialize, Serialize }; - +pub use payjoin::Error as PdkError; +uniffi::include_scaffolding!("pdk"); pub struct CachedOutputs { pub outputs: HashSet, pub file: std::fs::File, From e285a95f5ec826154c7333264c6ba38d4e2facaa Mon Sep 17 00:00:00 2001 From: BitcoinZavior Date: Mon, 31 Jul 2023 23:19:25 -0400 Subject: [PATCH 08/30] receive mod's wrapper code added --- src/receive.rs | 99 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 93 insertions(+), 6 deletions(-) diff --git a/src/receive.rs b/src/receive.rs index b79bdf2..26ceed6 100644 --- a/src/receive.rs +++ b/src/receive.rs @@ -1,6 +1,6 @@ use std::sync::Mutex; -use bitcoin::Script; +use bitcoin::{ Script, OutPoint, TxOut, Address }; use payjoin::receive::{ MaybeInputsOwned as PdkMaybeInputsOwned, MaybeMixedInputScripts as PdkMaybeMixedInputScripts, @@ -9,8 +9,9 @@ use payjoin::receive::{ PayjoinProposal as PdkPayjoinProposal, UncheckedProposal as PdkUncheckedProposal, Headers, + RequestError, }; -use crate::{ PdkError, transaction::Transaction }; +use crate::{ PdkError, transaction::{ Transaction, PartiallySignedTransaction } }; use anyhow::anyhow; pub struct UncheckedProposal { @@ -64,6 +65,9 @@ impl UncheckedProposal { } } +///Typestate to validate that the Original PSBT has no receiver-owned inputs. + +///Call check_no_receiver_owned_inputs() to proceed. pub struct MaybeInputsOwned { pub internal: PdkMaybeInputsOwned, } @@ -72,6 +76,9 @@ pub trait IsScriptOwned { fn is_owned(&self, script: &Script) -> Result; } impl MaybeInputsOwned { + ///Check that the Original PSBT has no receiver-owned inputs. Return original-psbt-rejected error or otherwise refuse to sign undesirable inputs. + + ///An attacker could try to spend receiver’s own inputs. This check prevents that. pub fn check_inputs_not_owned( self, is_owned: Box @@ -82,16 +89,43 @@ impl MaybeInputsOwned { } } } +///Typestate to validate that the Original PSBT has no inputs that have been seen before. +///Call check_no_inputs_seen to proceed. pub struct MaybeMixedInputScripts { pub internal: PdkMaybeMixedInputScripts, } +impl MaybeMixedInputScripts { + ///Verify the original transaction did not have mixed input types Call this after checking downstream. + ///Note: mixed spends do not necessarily indicate distinct wallet fingerprints. This check is intended to prevent some types of wallet fingerprinting. + pub fn check_no_mixed_input_scripts( + self + ) -> Result { + match self.internal.check_no_mixed_input_scripts() { + Ok(e) => Ok(MaybeInputsSeen { internal: e }), + Err(e) => Err(e), + } + } +} ///Typestate to validate that the Original PSBT has no inputs that have been seen before. + ///Call check_no_inputs_seen to proceed. pub struct MaybeInputsSeen { pub internal: PdkMaybeInputsSeen, } - -impl MaybeInputsSeen {} +pub trait IsOutoutKnown { + fn is_known(&self, output: &OutPoint) -> Result; +} +impl MaybeInputsSeen { + pub fn check_no_inputs_seen_before( + self, + is_known: Box + ) -> Result { + match self.internal.check_no_inputs_seen_before(|output| is_known.is_known(output)) { + Ok(e) => Ok(OutputsUnknown { internal: e }), + Err(e) => Err(e), + } + } +} ///The receiver has not yet identified which outputs belong to the receiver. ///Only accept PSBTs that send us money. Identify those outputs with identify_receiver_outputs() to proceed @@ -99,12 +133,65 @@ pub struct OutputsUnknown { pub internal: PdkOutputsUnknown, } -impl OutputsUnknown {} +impl OutputsUnknown { + ///Find which outputs belong to the receiver + pub fn identify_receiver_outputs( + self, + is_receiver_output: Box + ) -> Result { + match + self.internal.identify_receiver_outputs(|output_script| + is_receiver_output.is_owned(output_script) + ) + { + Ok(e) => Ok(PayjoinProposal { internal: e }), + Err(e) => Err(e), + } + } +} ///A mutable checked proposal that the receiver may contribute inputs to to make a payjoin. pub struct PayjoinProposal { pub internal: PdkPayjoinProposal, } impl PayjoinProposal { - // pub fn utxos_to_be_locked(&self) -> impl '_ + Iterator {} + pub fn is_output_substitution_disabled(&self) -> bool { + self.internal.is_output_substitution_disabled() + } + + //todo create outpoint and txout + pub fn contribute_witness_input(&mut self, txout: TxOut, outpoint: OutPoint) { + self.internal.contribute_witness_input(txout, outpoint) + } + pub fn contribute_non_witness_input(&mut self, tx: bitcoin::Transaction, outpoint: OutPoint) { + self.internal.contribute_non_witness_input(tx, outpoint) + } + pub fn substitute_output_address(&mut self, substitute_address: Address) { + self.internal.substitute_output_address(substitute_address) + } + + ///Apply additional fee contribution now that the receiver has contributed input this is kind of a “build_proposal” step before we sign and finalize and extract + + ///WARNING: DO NOT ALTER INPUTS OR OUTPUTS AFTER THIS STEP + pub fn apply_fee( + &mut self, + min_feerate_sat_per_vb: Option + ) -> Result { + match self.internal.apply_fee(min_feerate_sat_per_vb) { + Ok(e) => Ok(PartiallySignedTransaction { internal: e.to_owned() }), + Err(e) => Err(e), + } + } + ///Return a Payjoin Proposal PSBT that the sender will find acceptable. + ///This attempts to calculate any network fee owed by the receiver, subtract it from their output, and return a PSBT that can produce a consensus-valid transaction that the sender will accept. + ///wallet_process_psbt should sign and finalize receiver inputs + pub fn prepare_psbt( + self, + processed_psbt: PartiallySignedTransaction + ) -> Result { + match self.internal.prepare_psbt(processed_psbt.internal.to_owned()) { + Ok(e) => Ok(PartiallySignedTransaction { internal: e.to_owned() }), + Err(e) => Err(e), + } + } } From 33faba9fd7d2fa1eb2db5aa9ba6a257315b78a99 Mon Sep 17 00:00:00 2001 From: BitcoinZavior Date: Tue, 1 Aug 2023 22:34:51 -0400 Subject: [PATCH 09/30] removed mutex --- src/transaction.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/transaction.rs b/src/transaction.rs index 63f35f0..e72bc4a 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -5,13 +5,13 @@ use bitcoin::psbt::PartiallySignedTransaction as BitcoinPsbt; use crate::error::Error; pub struct PartiallySignedTransaction { - pub(crate) internal: Mutex, + pub(crate) internal: BitcoinPsbt, } impl PartiallySignedTransaction { pub(crate) fn new(psbt_base64: String) -> Result { let psbt: BitcoinPsbt = BitcoinPsbt::from_str(&psbt_base64)?; Ok(PartiallySignedTransaction { - internal: Mutex::new(psbt), + internal: psbt, }) } } From 7ad0e987b451a4c55690b38b7d3e5d3628c8a906 Mon Sep 17 00:00:00 2001 From: BitcoinZavior Date: Wed, 9 Aug 2023 23:36:21 -0400 Subject: [PATCH 10/30] Address exposed --- src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 03fe291..2c6a1b3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ pub mod transaction; use std::{ collections::HashSet, fs::OpenOptions, str::FromStr }; use bitcoin::Amount; use serde::{ Deserialize, Serialize }; +pub use bitcoin::Address as BitcoinAdrress; pub use payjoin::Error as PdkError; uniffi::include_scaffolding!("pdk"); pub struct CachedOutputs { @@ -107,3 +108,6 @@ impl From<&Input> for bitcoincore_rpc::json::CreateRawTransactionInput { } } } +pub struct Address { + pub internal: BitcoinAdrress, +} From f89f5b6bd218bae6762e2ef76e287128bcfb4e21 Mon Sep 17 00:00:00 2001 From: BitcoinZavior Date: Fri, 11 Aug 2023 17:30:25 -0400 Subject: [PATCH 11/30] removed mutex --- src/transaction.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transaction.rs b/src/transaction.rs index e72bc4a..cf6d57a 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -16,5 +16,5 @@ impl PartiallySignedTransaction { } } pub struct Transaction { - pub(crate) internal: Mutex, + pub(crate) internal: BitcoinTransaction, } From d747c498d84934d0c6ae4c42c7e7af0ab0d678fd Mon Sep 17 00:00:00 2001 From: BitcoinZavior Date: Sat, 12 Aug 2023 14:11:35 -0400 Subject: [PATCH 12/30] script exposed --- src/lib.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 2c6a1b3..419cffe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,7 @@ use bitcoin::Amount; use serde::{ Deserialize, Serialize }; pub use bitcoin::Address as BitcoinAdrress; pub use payjoin::Error as PdkError; +pub use bitcoin::blockdata::script::Script as BitcoinScript; uniffi::include_scaffolding!("pdk"); pub struct CachedOutputs { pub outputs: HashSet, @@ -40,6 +41,29 @@ impl From for bitcoin::OutPoint { } } } +impl From for OutPoint { + fn from(outpoint: bitcoin::OutPoint) -> Self { + OutPoint { + txid: outpoint.txid.to_string(), + vout: outpoint.vout, + } + } +} + +pub struct Script { + inner: Vec, +} + +impl Script { + fn new(raw_output_script: Vec) -> Self { + let script = BitcoinScript::from_bytes(raw_output_script.as_slice()); + Script { inner: script.to_bytes() } + } + + fn to_bytes(&self) -> Vec { + self.inner.to_owned() + } +} pub struct Uri { pub internal: String, From 4b94ef4bf3adbc2804ff8cd91e630863fa034c1b Mon Sep 17 00:00:00 2001 From: BitcoinZavior Date: Wed, 16 Aug 2023 23:11:35 -0400 Subject: [PATCH 13/30] TxOut exposed --- src/lib.rs | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 419cffe..5984fab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ pub mod error; pub mod receive; pub mod transaction; use std::{ collections::HashSet, fs::OpenOptions, str::FromStr }; -use bitcoin::Amount; +use bitcoin::{ Amount, ScriptBuf }; use serde::{ Deserialize, Serialize }; pub use bitcoin::Address as BitcoinAdrress; pub use payjoin::Error as PdkError; @@ -50,6 +50,7 @@ impl From for OutPoint { } } +#[derive(Debug, Clone)] pub struct Script { inner: Vec, } @@ -64,7 +65,32 @@ impl Script { self.inner.to_owned() } } +#[derive(Debug, Clone)] +pub struct TxOut { + /// The value of the output, in satoshis. + value: u64, + /// The address of the output. + script_pubkey: Script, +} +impl From for bitcoin::TxOut { + fn from(tx_out: TxOut) -> Self { + bitcoin::TxOut { + value: tx_out.value, + script_pubkey: ScriptBuf::from_bytes(tx_out.script_pubkey.inner), + } + } +} +impl From for TxOut { + fn from(tx_out: bitcoin::TxOut) -> Self { + TxOut { + value: tx_out.value, + script_pubkey: Script { + inner: tx_out.script_pubkey.to_bytes(), + }, + } + } +} pub struct Uri { pub internal: String, } From 881f6353b4777f3f7447368cde52196bdd227bb9 Mon Sep 17 00:00:00 2001 From: BitcoinZavior Date: Tue, 22 Aug 2023 21:55:12 -0400 Subject: [PATCH 14/30] Headers trait created; code clean up --- src/receive.rs | 65 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 17 deletions(-) diff --git a/src/receive.rs b/src/receive.rs index 26ceed6..ee3ad58 100644 --- a/src/receive.rs +++ b/src/receive.rs @@ -1,6 +1,3 @@ -use std::sync::Mutex; - -use bitcoin::{ Script, OutPoint, TxOut, Address }; use payjoin::receive::{ MaybeInputsOwned as PdkMaybeInputsOwned, MaybeMixedInputScripts as PdkMaybeMixedInputScripts, @@ -8,10 +5,16 @@ use payjoin::receive::{ OutputsUnknown as PdkOutputsUnknown, PayjoinProposal as PdkPayjoinProposal, UncheckedProposal as PdkUncheckedProposal, - Headers, RequestError, }; -use crate::{ PdkError, transaction::{ Transaction, PartiallySignedTransaction } }; +use crate::{ + PdkError, + transaction::{ Transaction, PartiallySignedTransaction }, + Address, + OutPoint, + Script, + TxOut, +}; use anyhow::anyhow; pub struct UncheckedProposal { @@ -26,11 +29,32 @@ pub trait CanBroadcast { bitcoincore_rpc::Error >; } +pub struct Headers { + pub length: String, +} + +impl Headers { + pub fn new(length: u64) -> Headers { + Headers { length: length.to_string() } + } +} + +impl payjoin::receive::Headers for Headers { + fn get_header(&self, key: &str) -> Option<&str> { + match key { + "content-length" => Some(&self.length), + "content-type" => Some("text/plain"), + _ => None, + } + } +} + impl UncheckedProposal { pub fn from_request( + //TODO; Find which type that implement Read trait is an appropriate option body: impl std::io::Read, query: String, - headers: impl Headers + headers: Headers ) -> Result { let res = PdkUncheckedProposal::from_request(body, query.as_str(), headers)?; Ok(UncheckedProposal { internal: res }) @@ -40,7 +64,7 @@ impl UncheckedProposal { // } pub fn get_transaction_to_schedule_broadcast(&self) -> Transaction { let res = self.internal.get_transaction_to_schedule_broadcast(); - Transaction { internal: Mutex::new(res) } + Transaction { internal: res } } pub fn check_can_broadcast( self, @@ -73,7 +97,7 @@ pub struct MaybeInputsOwned { } pub trait IsScriptOwned { - fn is_owned(&self, script: &Script) -> Result; + fn is_owned(&self, script: Script) -> Result; } impl MaybeInputsOwned { ///Check that the Original PSBT has no receiver-owned inputs. Return original-psbt-rejected error or otherwise refuse to sign undesirable inputs. @@ -83,7 +107,11 @@ impl MaybeInputsOwned { self, is_owned: Box ) -> Result { - match self.internal.check_inputs_not_owned(|input| is_owned.is_owned(input)) { + match + self.internal.check_inputs_not_owned(|input| + is_owned.is_owned(Script { inner: input.to_bytes() }) + ) + { Ok(e) => Ok(MaybeMixedInputScripts { internal: e }), Err(e) => Err(e), } @@ -113,14 +141,18 @@ pub struct MaybeInputsSeen { pub internal: PdkMaybeInputsSeen, } pub trait IsOutoutKnown { - fn is_known(&self, output: &OutPoint) -> Result; + fn is_known(&self, outpoint: OutPoint) -> Result; } impl MaybeInputsSeen { pub fn check_no_inputs_seen_before( self, is_known: Box ) -> Result { - match self.internal.check_no_inputs_seen_before(|output| is_known.is_known(output)) { + match + self.internal.check_no_inputs_seen_before(|outpoint| + is_known.is_known(outpoint.to_owned().into()) + ) + { Ok(e) => Ok(OutputsUnknown { internal: e }), Err(e) => Err(e), } @@ -141,7 +173,7 @@ impl OutputsUnknown { ) -> Result { match self.internal.identify_receiver_outputs(|output_script| - is_receiver_output.is_owned(output_script) + is_receiver_output.is_owned(Script { inner: output_script.to_bytes() }) ) { Ok(e) => Ok(PayjoinProposal { internal: e }), @@ -159,15 +191,14 @@ impl PayjoinProposal { self.internal.is_output_substitution_disabled() } - //todo create outpoint and txout pub fn contribute_witness_input(&mut self, txout: TxOut, outpoint: OutPoint) { - self.internal.contribute_witness_input(txout, outpoint) + self.internal.contribute_witness_input(txout.into(), outpoint.into()) } - pub fn contribute_non_witness_input(&mut self, tx: bitcoin::Transaction, outpoint: OutPoint) { - self.internal.contribute_non_witness_input(tx, outpoint) + pub fn contribute_non_witness_input(&mut self, tx: Transaction, outpoint: OutPoint) { + self.internal.contribute_non_witness_input(tx.internal, outpoint.into()) } pub fn substitute_output_address(&mut self, substitute_address: Address) { - self.internal.substitute_output_address(substitute_address) + self.internal.substitute_output_address(substitute_address.internal) } ///Apply additional fee contribution now that the receiver has contributed input this is kind of a “build_proposal” step before we sign and finalize and extract From 781be117e829ce5d88f27a2edad162454c998455 Mon Sep 17 00:00:00 2001 From: BitcoinZavior Date: Wed, 23 Aug 2023 19:59:28 -0400 Subject: [PATCH 15/30] Implement RwLock protection for Configuration --- src/send.rs | 124 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 74 insertions(+), 50 deletions(-) diff --git a/src/send.rs b/src/send.rs index cc2984e..d760267 100644 --- a/src/send.rs +++ b/src/send.rs @@ -1,74 +1,98 @@ -use std::{ sync::{ Arc }, string::ParseError }; +use std::{ + string::ParseError, + sync::{Arc, Mutex, RwLock}, +}; -pub use payjoin::send::{ Configuration as PdkConfiguration, Context as PdkContext }; +pub use payjoin::send::{Configuration as PdkConfiguration, Context as PdkContext}; ///Builder for sender-side payjoin parameters ///These parameters define how client wants to handle Payjoin. pub(crate) struct Configuration { - pub(crate) internal: PdkConfiguration, + pub(crate) internal: RwLock, } + impl Configuration { - ///Offer the receiver contribution to pay for his input. - ///These parameters will allow the receiver to take max_fee_contribution from given change output to pay for additional inputs. The recommended fee is size_of_one_input * fee_rate. - ///change_index specifies which output can be used to pay fee. If None is provided, then the output is auto-detected unless the supplied transaction has more than two outputs. - pub fn with_fee_contribution( - max_fee_contribution: u64, - change_index: Option - ) -> Arc { - let conf = PdkConfiguration::with_fee_contribution( - bitcoin::Amount::from_sat(max_fee_contribution), - change_index - ); - Arc::new(Self { internal: conf }) - } - ///Perform Payjoin without incentivizing the payee to cooperate. - ///While it’s generally better to offer some contribution some users may wish not to. This function disables contribution. - pub fn non_incentivizing() -> Arc { - Arc::new(Self { internal: PdkConfiguration::non_incentivizing() }) - } - ///Disable output substitution even if the receiver didn’t. - ///This forbids receiver switching output or decreasing amount. It is generally not recommended to set this as it may prevent the receiver from doing advanced operations such as opening LN channels and it also guarantees the receiver will not reward the sender with a discount. - pub fn always_disable_output_substitution(self, disable: bool) -> Arc { - let internal = self.internal; - Arc::new(Self { internal: internal.always_disable_output_substitution(disable) }) - } - ///Decrease fee contribution instead of erroring. + ///Offer the receiver contribution to pay for his input. + ///These parameters will allow the receiver to take max_fee_contribution from given change output to pay for additional inputs. The recommended fee is size_of_one_input * fee_rate. + ///change_index specifies which output can be used to pay fee. If None is provided, then the output is auto-detected unless the supplied transaction has more than two outputs. + pub fn with_fee_contribution(max_fee_contribution: u64, change_index: Option) -> Self { + let conf = PdkConfiguration::with_fee_contribution( + bitcoin::Amount::from_sat(max_fee_contribution), + change_index, + ); + Self { internal: RwLock::new(conf) } + } + ///Perform Payjoin without incentivizing the payee to cooperate. + ///While it’s generally better to offer some contribution some users may wish not to. This function disables contribution. + pub fn non_incentivizing() -> Arc { + Arc::new(Self { internal: RwLock::new(PdkConfiguration::non_incentivizing()) }) + } + ///Disable output substitution even if the receiver didn’t. + ///This forbids receiver switching output or decreasing amount. It is generally not recommended to set this as it may prevent the receiver from doing advanced operations such as opening LN channels and it also guarantees the receiver will not reward the sender with a discount. + pub fn always_disable_output_substitution(self, disable: bool) -> Self { + { + let mut data_guard = self.internal.write().unwrap(); + // Temporarily take out the Configuration and replace with a dummy value + let taken_config = std::mem::replace( + &mut *data_guard, + PdkConfiguration::with_fee_contribution(bitcoin::Amount::ZERO, None), + ); + *data_guard = taken_config.always_disable_output_substitution(disable); + } + self + } + ///Decrease fee contribution instead of erroring. - ///If this option is set and a transaction with change amount lower than fee contribution is provided then instead of returning error the fee contribution will be just lowered to match the change amount. - pub fn clamp_fee_contribution(self, clamp: bool) -> Arc { - let internal = self.internal; - Arc::new(Self { internal: internal.clamp_fee_contribution(clamp) }) - } - ///Sets minimum fee rate required by the sender. - pub fn min_fee_rate_sat_per_vb(self, fee_rate: u64) -> Arc { - let internal = self.internal; - Arc::new(Self { internal: internal.min_fee_rate_sat_per_vb(fee_rate) }) - } + ///If this option is set and a transaction with change amount lower than fee contribution is provided then instead of returning error the fee contribution will be just lowered to match the change amount. + pub fn clamp_fee_contribution(mut self, clamp: bool) -> Self { + { + let mut data_guard = self.internal.write().unwrap(); + // Temporarily take out the Configuration and replace with a dummy value + let taken_config = std::mem::replace( + &mut *data_guard, + PdkConfiguration::with_fee_contribution(bitcoin::Amount::ZERO, None), + ); + *data_guard = taken_config.clamp_fee_contribution(clamp); + } + self + } + ///Sets minimum fee rate required by the sender. + pub fn min_fee_rate_sat_per_vb(self, fee_rate: u64) -> Self { + { + let mut data_guard = self.internal.write().unwrap(); + // Temporarily take out the Configuration and replace with a dummy value + let taken_config = std::mem::replace( + &mut *data_guard, + PdkConfiguration::with_fee_contribution(bitcoin::Amount::ZERO, None), + ); + *data_guard = taken_config.min_fee_rate_sat_per_vb(fee_rate); + } + self + } } pub struct Context { - pub internal: PdkContext, + pub internal: PdkContext, } ///Represents data that needs to be transmitted to the receiver. ///You need to send this request over HTTP(S) to the receiver. -#[non_exhaustive] pub struct Request { - ///URL to send the request to. + ///URL to send the request to. - ///This is full URL with scheme etc - you can pass it right to reqwest or a similar library. - pub url: Url, - ///Bytes to be sent to the receiver. + ///This is full URL with scheme etc - you can pass it right to reqwest or a similar library. + pub url: Url, + ///Bytes to be sent to the receiver. - ///This is properly encoded PSBT, already in base64. You only need to make sure Content-Type is text/plain and Content-Length is body.len() (most libraries do the latter automatically). - pub body: Vec, + ///This is properly encoded PSBT, already in base64. You only need to make sure Content-Type is text/plain and Content-Length is body.len() (most libraries do the latter automatically). + pub body: Vec, } pub struct Url { - pub internal: String, + pub internal: String, } impl Url { - pub fn parse(input: String) -> Result { - Ok(Self { internal: input }) - } + pub fn parse(input: String) -> Result { + Ok(Self { internal: input }) + } } From 91c8b337f68aee1b450a651248f43a001b55b93a Mon Sep 17 00:00:00 2001 From: BitcoinZavior Date: Wed, 23 Aug 2023 23:37:54 -0400 Subject: [PATCH 16/30] Encapsulate internal variable inside Configuration with Option --- src/send.rs | 39 +++++++++++++-------------------------- 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/src/send.rs b/src/send.rs index d760267..76ae886 100644 --- a/src/send.rs +++ b/src/send.rs @@ -1,7 +1,4 @@ -use std::{ - string::ParseError, - sync::{Arc, Mutex, RwLock}, -}; +use std::{string::ParseError, sync::RwLock}; pub use payjoin::send::{Configuration as PdkConfiguration, Context as PdkContext}; @@ -9,7 +6,7 @@ pub use payjoin::send::{Configuration as PdkConfiguration, Context as PdkContext ///These parameters define how client wants to handle Payjoin. pub(crate) struct Configuration { - pub(crate) internal: RwLock, + pub(crate) internal: RwLock>, } impl Configuration { @@ -17,16 +14,16 @@ impl Configuration { ///These parameters will allow the receiver to take max_fee_contribution from given change output to pay for additional inputs. The recommended fee is size_of_one_input * fee_rate. ///change_index specifies which output can be used to pay fee. If None is provided, then the output is auto-detected unless the supplied transaction has more than two outputs. pub fn with_fee_contribution(max_fee_contribution: u64, change_index: Option) -> Self { - let conf = PdkConfiguration::with_fee_contribution( + let configuration = PdkConfiguration::with_fee_contribution( bitcoin::Amount::from_sat(max_fee_contribution), change_index, ); - Self { internal: RwLock::new(conf) } + Self { internal: RwLock::new(Some(configuration)) } } ///Perform Payjoin without incentivizing the payee to cooperate. ///While it’s generally better to offer some contribution some users may wish not to. This function disables contribution. - pub fn non_incentivizing() -> Arc { - Arc::new(Self { internal: RwLock::new(PdkConfiguration::non_incentivizing()) }) + pub fn non_incentivizing() -> Self { + Self { internal: RwLock::new(Some(PdkConfiguration::non_incentivizing())) } } ///Disable output substitution even if the receiver didn’t. ///This forbids receiver switching output or decreasing amount. It is generally not recommended to set this as it may prevent the receiver from doing advanced operations such as opening LN channels and it also guarantees the receiver will not reward the sender with a discount. @@ -34,26 +31,19 @@ impl Configuration { { let mut data_guard = self.internal.write().unwrap(); // Temporarily take out the Configuration and replace with a dummy value - let taken_config = std::mem::replace( - &mut *data_guard, - PdkConfiguration::with_fee_contribution(bitcoin::Amount::ZERO, None), - ); - *data_guard = taken_config.always_disable_output_substitution(disable); + let _config = std::mem::replace(&mut *data_guard, None); + *data_guard = Some(_config.unwrap().always_disable_output_substitution(disable)); } self } ///Decrease fee contribution instead of erroring. - ///If this option is set and a transaction with change amount lower than fee contribution is provided then instead of returning error the fee contribution will be just lowered to match the change amount. - pub fn clamp_fee_contribution(mut self, clamp: bool) -> Self { + pub fn clamp_fee_contribution(self, clamp: bool) -> Self { { let mut data_guard = self.internal.write().unwrap(); // Temporarily take out the Configuration and replace with a dummy value - let taken_config = std::mem::replace( - &mut *data_guard, - PdkConfiguration::with_fee_contribution(bitcoin::Amount::ZERO, None), - ); - *data_guard = taken_config.clamp_fee_contribution(clamp); + let _config = std::mem::replace(&mut *data_guard, None); + *data_guard = Some(_config.unwrap().clamp_fee_contribution(clamp)); } self } @@ -62,11 +52,8 @@ impl Configuration { { let mut data_guard = self.internal.write().unwrap(); // Temporarily take out the Configuration and replace with a dummy value - let taken_config = std::mem::replace( - &mut *data_guard, - PdkConfiguration::with_fee_contribution(bitcoin::Amount::ZERO, None), - ); - *data_guard = taken_config.min_fee_rate_sat_per_vb(fee_rate); + let _config = std::mem::replace(&mut *data_guard, None); + *data_guard = Some(_config.unwrap().min_fee_rate_sat_per_vb(fee_rate)); } self } From 6bba7ca7e00f9138d142abf6845795ee128aa2aa Mon Sep 17 00:00:00 2001 From: BitcoinZavior Date: Thu, 24 Aug 2023 04:22:39 -0400 Subject: [PATCH 17/30] Applied Option and Mutex wrappers to PayjoinProposal for nullability and synchronization --- src/receive.rs | 348 +++++++++++++++++++++++++------------------------ 1 file changed, 179 insertions(+), 169 deletions(-) diff --git a/src/receive.rs b/src/receive.rs index ee3ad58..5f38143 100644 --- a/src/receive.rs +++ b/src/receive.rs @@ -1,228 +1,238 @@ -use payjoin::receive::{ - MaybeInputsOwned as PdkMaybeInputsOwned, - MaybeMixedInputScripts as PdkMaybeMixedInputScripts, - MaybeInputsSeen as PdkMaybeInputsSeen, - OutputsUnknown as PdkOutputsUnknown, - PayjoinProposal as PdkPayjoinProposal, - UncheckedProposal as PdkUncheckedProposal, - RequestError, -}; +use std::sync::{Mutex, MutexGuard, RwLock}; + use crate::{ - PdkError, - transaction::{ Transaction, PartiallySignedTransaction }, - Address, - OutPoint, - Script, - TxOut, + transaction::{PartiallySignedTransaction, Transaction}, + Address, OutPoint, PdkError, Script, TxOut, }; use anyhow::anyhow; +use payjoin::receive::{ + MaybeInputsOwned as PdkMaybeInputsOwned, MaybeInputsSeen as PdkMaybeInputsSeen, + MaybeMixedInputScripts as PdkMaybeMixedInputScripts, OutputsUnknown as PdkOutputsUnknown, + PayjoinProposal as PdkPayjoinProposal, RequestError, UncheckedProposal as PdkUncheckedProposal, +}; pub struct UncheckedProposal { - pub internal: PdkUncheckedProposal, + pub internal: PdkUncheckedProposal, } pub trait CanBroadcast { - fn test_mempool_accept( - &self, - tx_hex: Vec - ) -> Result< - Vec, - bitcoincore_rpc::Error - >; + fn test_mempool_accept( + &self, tx_hex: Vec, + ) -> Result< + Vec, + bitcoincore_rpc::Error, + >; } pub struct Headers { - pub length: String, + pub length: String, } impl Headers { - pub fn new(length: u64) -> Headers { - Headers { length: length.to_string() } - } + pub fn new(length: u64) -> Headers { + Headers { length: length.to_string() } + } } impl payjoin::receive::Headers for Headers { - fn get_header(&self, key: &str) -> Option<&str> { - match key { - "content-length" => Some(&self.length), - "content-type" => Some("text/plain"), - _ => None, - } - } + fn get_header(&self, key: &str) -> Option<&str> { + match key { + "content-length" => Some(&self.length), + "content-type" => Some("text/plain"), + _ => None, + } + } } impl UncheckedProposal { - pub fn from_request( - //TODO; Find which type that implement Read trait is an appropriate option - body: impl std::io::Read, - query: String, - headers: Headers - ) -> Result { - let res = PdkUncheckedProposal::from_request(body, query.as_str(), headers)?; - Ok(UncheckedProposal { internal: res }) - } - // fn get_internal(&self) -> MutexGuard { - // self.internal.lock().expect("PdkUncheckedProposal") - // } - pub fn get_transaction_to_schedule_broadcast(&self) -> Transaction { - let res = self.internal.get_transaction_to_schedule_broadcast(); - Transaction { internal: res } - } - pub fn check_can_broadcast( - self, - can_broadcast: Box - ) -> Result { - let res = self.internal.check_can_broadcast(|tx| { - let raw_tx = hex::encode(bitcoin::consensus::encode::serialize(&tx)); - let mempool_results = can_broadcast - .test_mempool_accept(vec![raw_tx]) - .map_err(|e| PdkError::Server(e.into()))?; - match mempool_results.first() { - Some(result) => Ok(result.allowed), - None => - Err( - PdkError::Server( - anyhow!("No mempool results returned on broadcast check").into() - ) - ), - } - })?; - Ok(MaybeInputsOwned { internal: res }) - } + pub fn from_request( + //TODO; Find which type that implement Read trait is an appropriate option + body: impl std::io::Read, + query: String, + headers: Headers, + ) -> Result { + let res = PdkUncheckedProposal::from_request(body, query.as_str(), headers)?; + Ok(UncheckedProposal { internal: res }) + } + // fn get_internal(&self) -> MutexGuard { + // self.internal.lock().expect("PdkUncheckedProposal") + // } + pub fn get_transaction_to_schedule_broadcast(&self) -> Transaction { + let res = self.internal.get_transaction_to_schedule_broadcast(); + Transaction { internal: res } + } + pub fn check_can_broadcast( + self, can_broadcast: Box, + ) -> Result { + let res = self.internal.check_can_broadcast(|tx| { + let raw_tx = hex::encode(bitcoin::consensus::encode::serialize(&tx)); + let mempool_results = can_broadcast + .test_mempool_accept(vec![raw_tx]) + .map_err(|e| PdkError::Server(e.into()))?; + match mempool_results.first() { + Some(result) => Ok(result.allowed), + None => Err(PdkError::Server( + anyhow!("No mempool results returned on broadcast check").into(), + )), + } + })?; + Ok(MaybeInputsOwned { internal: res }) + } } ///Typestate to validate that the Original PSBT has no receiver-owned inputs. ///Call check_no_receiver_owned_inputs() to proceed. pub struct MaybeInputsOwned { - pub internal: PdkMaybeInputsOwned, + pub internal: PdkMaybeInputsOwned, } pub trait IsScriptOwned { - fn is_owned(&self, script: Script) -> Result; + fn is_owned(&self, script: Script) -> Result; } impl MaybeInputsOwned { - ///Check that the Original PSBT has no receiver-owned inputs. Return original-psbt-rejected error or otherwise refuse to sign undesirable inputs. - - ///An attacker could try to spend receiver’s own inputs. This check prevents that. - pub fn check_inputs_not_owned( - self, - is_owned: Box - ) -> Result { - match - self.internal.check_inputs_not_owned(|input| - is_owned.is_owned(Script { inner: input.to_bytes() }) - ) - { - Ok(e) => Ok(MaybeMixedInputScripts { internal: e }), - Err(e) => Err(e), - } - } + ///Check that the Original PSBT has no receiver-owned inputs. Return original-psbt-rejected error or otherwise refuse to sign undesirable inputs. + + ///An attacker could try to spend receiver’s own inputs. This check prevents that. + pub fn check_inputs_not_owned( + self, is_owned: Box, + ) -> Result { + match self + .internal + .check_inputs_not_owned(|input| is_owned.is_owned(Script { inner: input.to_bytes() })) + { + Ok(e) => Ok(MaybeMixedInputScripts { internal: e }), + Err(e) => Err(e), + } + } } ///Typestate to validate that the Original PSBT has no inputs that have been seen before. ///Call check_no_inputs_seen to proceed. pub struct MaybeMixedInputScripts { - pub internal: PdkMaybeMixedInputScripts, + pub internal: PdkMaybeMixedInputScripts, } impl MaybeMixedInputScripts { - ///Verify the original transaction did not have mixed input types Call this after checking downstream. - ///Note: mixed spends do not necessarily indicate distinct wallet fingerprints. This check is intended to prevent some types of wallet fingerprinting. - pub fn check_no_mixed_input_scripts( - self - ) -> Result { - match self.internal.check_no_mixed_input_scripts() { - Ok(e) => Ok(MaybeInputsSeen { internal: e }), - Err(e) => Err(e), - } - } + ///Verify the original transaction did not have mixed input types Call this after checking downstream. + ///Note: mixed spends do not necessarily indicate distinct wallet fingerprints. This check is intended to prevent some types of wallet fingerprinting. + pub fn check_no_mixed_input_scripts( + self, + ) -> Result { + match self.internal.check_no_mixed_input_scripts() { + Ok(e) => Ok(MaybeInputsSeen { internal: e }), + Err(e) => Err(e), + } + } } ///Typestate to validate that the Original PSBT has no inputs that have been seen before. ///Call check_no_inputs_seen to proceed. pub struct MaybeInputsSeen { - pub internal: PdkMaybeInputsSeen, + pub internal: PdkMaybeInputsSeen, } pub trait IsOutoutKnown { - fn is_known(&self, outpoint: OutPoint) -> Result; + fn is_known(&self, outpoint: OutPoint) -> Result; } impl MaybeInputsSeen { - pub fn check_no_inputs_seen_before( - self, - is_known: Box - ) -> Result { - match - self.internal.check_no_inputs_seen_before(|outpoint| - is_known.is_known(outpoint.to_owned().into()) - ) - { - Ok(e) => Ok(OutputsUnknown { internal: e }), - Err(e) => Err(e), - } - } + pub fn check_no_inputs_seen_before( + self, is_known: Box, + ) -> Result { + match self + .internal + .check_no_inputs_seen_before(|outpoint| is_known.is_known(outpoint.to_owned().into())) + { + Ok(e) => Ok(OutputsUnknown { internal: e }), + Err(e) => Err(e), + } + } } ///The receiver has not yet identified which outputs belong to the receiver. ///Only accept PSBTs that send us money. Identify those outputs with identify_receiver_outputs() to proceed pub struct OutputsUnknown { - pub internal: PdkOutputsUnknown, + pub internal: PdkOutputsUnknown, } impl OutputsUnknown { - ///Find which outputs belong to the receiver - pub fn identify_receiver_outputs( - self, - is_receiver_output: Box - ) -> Result { - match - self.internal.identify_receiver_outputs(|output_script| - is_receiver_output.is_owned(Script { inner: output_script.to_bytes() }) - ) - { - Ok(e) => Ok(PayjoinProposal { internal: e }), - Err(e) => Err(e), - } - } + ///Find which outputs belong to the receiver + pub fn identify_receiver_outputs( + self, is_receiver_output: Box, + ) -> Result { + match self.internal.identify_receiver_outputs(|output_script| { + is_receiver_output.is_owned(Script { inner: output_script.to_bytes() }) + }) { + Ok(e) => Ok(PayjoinProposal { internal: Mutex::new(Some(e)) }), + Err(e) => Err(e), + } + } } ///A mutable checked proposal that the receiver may contribute inputs to to make a payjoin. pub struct PayjoinProposal { - pub internal: PdkPayjoinProposal, + pub internal: Mutex>, } impl PayjoinProposal { - pub fn is_output_substitution_disabled(&self) -> bool { - self.internal.is_output_substitution_disabled() - } - - pub fn contribute_witness_input(&mut self, txout: TxOut, outpoint: OutPoint) { - self.internal.contribute_witness_input(txout.into(), outpoint.into()) - } - pub fn contribute_non_witness_input(&mut self, tx: Transaction, outpoint: OutPoint) { - self.internal.contribute_non_witness_input(tx.internal, outpoint.into()) - } - pub fn substitute_output_address(&mut self, substitute_address: Address) { - self.internal.substitute_output_address(substitute_address.internal) - } - - ///Apply additional fee contribution now that the receiver has contributed input this is kind of a “build_proposal” step before we sign and finalize and extract - - ///WARNING: DO NOT ALTER INPUTS OR OUTPUTS AFTER THIS STEP - pub fn apply_fee( - &mut self, - min_feerate_sat_per_vb: Option - ) -> Result { - match self.internal.apply_fee(min_feerate_sat_per_vb) { - Ok(e) => Ok(PartiallySignedTransaction { internal: e.to_owned() }), - Err(e) => Err(e), - } - } - ///Return a Payjoin Proposal PSBT that the sender will find acceptable. - ///This attempts to calculate any network fee owed by the receiver, subtract it from their output, and return a PSBT that can produce a consensus-valid transaction that the sender will accept. - ///wallet_process_psbt should sign and finalize receiver inputs - pub fn prepare_psbt( - self, - processed_psbt: PartiallySignedTransaction - ) -> Result { - match self.internal.prepare_psbt(processed_psbt.internal.to_owned()) { - Ok(e) => Ok(PartiallySignedTransaction { internal: e.to_owned() }), - Err(e) => Err(e), - } - } + pub(crate) fn get_proposal(&self) -> MutexGuard> { + self.internal.lock().expect("PayjoinProposal") + } + pub fn is_output_substitution_disabled(&self) -> bool { + if self.get_proposal().as_ref().is_none() { + //TODO CREATE CUSTOM ERROR + panic!("PayjoinProposal not initalized"); + } + self.get_proposal().as_mut().unwrap().is_output_substitution_disabled() + } + + pub fn contribute_witness_input(self, txout: TxOut, outpoint: OutPoint) -> Self { + if self.get_proposal().as_ref().is_none() { + panic!("PayjoinProposal not initalized"); + } + self.get_proposal() + .as_mut() + .unwrap() + .contribute_witness_input(txout.into(), outpoint.into()); + self + } + pub fn contribute_non_witness_input(self, tx: Transaction, outpoint: OutPoint) { + if self.get_proposal().as_ref().is_none() { + panic!("PayjoinProposal not initalized"); + } + self.get_proposal() + .as_mut() + .unwrap() + .contribute_non_witness_input(tx.internal, outpoint.into()) + } + pub fn substitute_output_address(self, substitute_address: Address) { + if self.get_proposal().as_ref().is_none() { + panic!("PayjoinProposal not initalized"); + } + self.get_proposal().as_mut().unwrap().substitute_output_address(substitute_address.internal) + } + + ///Apply additional fee contribution now that the receiver has contributed input this is kind of a “build_proposal” step before we sign and finalize and extract + + ///WARNING: DO NOT ALTER INPUTS OR OUTPUTS AFTER THIS STEP + pub fn apply_fee( + self, min_feerate_sat_per_vb: Option, + ) -> Result { + if self.get_proposal().as_ref().is_none() { + panic!("PayjoinProposal not initalized"); + } + + match self.get_proposal().as_mut().unwrap().apply_fee(min_feerate_sat_per_vb) { + Ok(e) => Ok(PartiallySignedTransaction { internal: e.to_owned() }), + Err(e) => Err(e), + } + } + ///Return a Payjoin Proposal PSBT that the sender will find acceptable. + ///This attempts to calculate any network fee owed by the receiver, subtract it from their output, and return a PSBT that can produce a consensus-valid transaction that the sender will accept. + ///wallet_process_psbt should sign and finalize receiver inputs + pub fn prepare_psbt( + self, processed_psbt: PartiallySignedTransaction, + ) -> Result { + let mut data_guard = self.get_proposal(); + // Temporarily take out the Configuration and replace with a dummy value + let taken_proposal = std::mem::replace(&mut *data_guard, None); + match taken_proposal.unwrap().prepare_psbt(processed_psbt.internal.to_owned()) { + Ok(e) => Ok(PartiallySignedTransaction { internal: e.to_owned() }), + Err(e) => Err(e), + } + } } From 1a9f2483dd4e4c2530cbee1ef82b584965b8f16e Mon Sep 17 00:00:00 2001 From: BitcoinZavior Date: Thu, 24 Aug 2023 07:10:43 -0400 Subject: [PATCH 18/30] cargo fmt --- Cargo.toml | 2 +- build.rs | 2 +- src/bitcoind.rs | 269 +++++++++++++++++++++------------------------ src/error.rs | 66 +++++------ src/lib.rs | 199 ++++++++++++++++----------------- src/transaction.rs | 16 ++- uniffi-bindgen.rs | 2 +- 7 files changed, 262 insertions(+), 294 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 83466c9..6e5b4e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" [lib] crate-type = ["lib", "staticlib", "cdylib"] -name = "pdkFfi" +name = "pdk_ffi" [build-dependencies] uniffi = { version = "0.24.3", features = ["build"] } diff --git a/build.rs b/build.rs index 89bd6d8..ad9a773 100644 --- a/build.rs +++ b/build.rs @@ -1,3 +1,3 @@ fn main() { - uniffi::generate_scaffolding("./src/pdk.udl").unwrap(); + uniffi::generate_scaffolding("./src/pdk.udl").unwrap(); } diff --git a/src/bitcoind.rs b/src/bitcoind.rs index a794abe..612c9ba 100644 --- a/src/bitcoind.rs +++ b/src/bitcoind.rs @@ -1,169 +1,152 @@ -use std::{ sync::{ Mutex, Arc, MutexGuard }, collections::HashMap, str::FromStr }; +use std::{ + collections::HashMap, + str::FromStr, + sync::{Arc, Mutex, MutexGuard}, +}; use bitcoin::Amount; +use bitcoincore_rpc::{self, RpcApi}; use serde::Deserialize; -use bitcoincore_rpc::{ self, RpcApi }; -use crate::{ CachedOutputs, Input, AddressType }; +use crate::{AddressType, CachedOutputs, Input}; #[derive(Debug, Deserialize, Clone)] pub struct BitcoindConfig { - pub rpc_host: String, - pub cookie: Option, - pub rpc_user: String, - pub rpc_pass: String, - pub cache_dir: String, + pub rpc_host: String, + pub cookie: Option, + pub rpc_user: String, + pub rpc_pass: String, + pub cache_dir: String, } #[derive(Clone)] pub struct BitcoindClient { - pub config: BitcoindConfig, - pub bitcoind_mutex: Arc>, - pub cached_outputs: Arc>, + pub config: BitcoindConfig, + pub bitcoind_mutex: Arc>, + pub cached_outputs: Arc>, } impl BitcoindClient { - pub fn new(config: BitcoindConfig) -> Result { - let bitcoind = ( - match &config.cookie { - Some(cookie) => - bitcoincore_rpc::Client::new( - &config.rpc_host, - bitcoincore_rpc::Auth::CookieFile(cookie.into()) - ), - None => - bitcoincore_rpc::Client::new( - &config.rpc_host, - bitcoincore_rpc::Auth::UserPass( - config.rpc_user.clone(), - config.rpc_pass.clone() - ) - ), - } - ).expect("Failed to connect to bitcoind"); - let seen_input = Arc::new(Mutex::new(CachedOutputs::new(config.cache_dir.clone())?)); - let bitcoind_mutex = Arc::new(Mutex::new(bitcoind)); - Ok(Self { config, bitcoind_mutex, cached_outputs: seen_input }) - } - fn get_rpc_client(&self) -> MutexGuard { - return self.bitcoind_mutex.lock().unwrap(); - } - pub fn load_wallet(&self, wallet_name: String) -> Result { - let client = self.get_rpc_client(); - match client.load_wallet(&wallet_name.as_str()) { - Ok(e) => { - return match e.warning { - Some(e) => panic!("{:?}", e), - None => Ok(wallet_name), - }; - } - Err(e) => panic!("{:?}", e), - }; - } - pub fn create_wallet( - &self, - wallet_name: String, - disable_private_keys: Option, - blank: Option, - passphrase: Option, - avoid_reuse: Option - ) -> Result { - let client = self.get_rpc_client(); - match - client.create_wallet( - &wallet_name.as_str(), - disable_private_keys, - blank, - passphrase.as_ref().map(|x| x.as_str()), - avoid_reuse - ) - { - Ok(e) => { - return match e.warning { - Some(e) => panic!("{:?}", e), - None => Ok(wallet_name), - }; - } - Err(e) => panic!("{:?}", e), - }; - } + pub fn new(config: BitcoindConfig) -> Result { + let bitcoind = (match &config.cookie { + Some(cookie) => bitcoincore_rpc::Client::new( + &config.rpc_host, + bitcoincore_rpc::Auth::CookieFile(cookie.into()), + ), + None => bitcoincore_rpc::Client::new( + &config.rpc_host, + bitcoincore_rpc::Auth::UserPass(config.rpc_user.clone(), config.rpc_pass.clone()), + ), + }) + .expect("Failed to connect to bitcoind"); + let seen_input = Arc::new(Mutex::new(CachedOutputs::new(config.cache_dir.clone())?)); + let bitcoind_mutex = Arc::new(Mutex::new(bitcoind)); + Ok(Self { config, bitcoind_mutex, cached_outputs: seen_input }) + } + fn get_rpc_client(&self) -> MutexGuard { + return self.bitcoind_mutex.lock().unwrap(); + } + pub fn load_wallet(&self, wallet_name: String) -> Result { + let client = self.get_rpc_client(); + match client.load_wallet(&wallet_name.as_str()) { + Ok(e) => { + return match e.warning { + Some(e) => panic!("{:?}", e), + None => Ok(wallet_name), + }; + } + Err(e) => panic!("{:?}", e), + }; + } + pub fn create_wallet( + &self, wallet_name: String, disable_private_keys: Option, blank: Option, + passphrase: Option, avoid_reuse: Option, + ) -> Result { + let client = self.get_rpc_client(); + match client.create_wallet( + &wallet_name.as_str(), + disable_private_keys, + blank, + passphrase.as_ref().map(|x| x.as_str()), + avoid_reuse, + ) { + Ok(e) => { + return match e.warning { + Some(e) => panic!("{:?}", e), + None => Ok(wallet_name), + }; + } + Err(e) => panic!("{:?}", e), + }; + } - pub fn create_psbt( - &self, - inputs: Vec, - outputs: HashMap, - locktime: Option, - replaceable: Option - ) -> Result { - let client = self.get_rpc_client(); - let pared_inputs = inputs - .iter() - .map(|x| x.into()) - .collect::>(); - let parsed_outputs = outputs - .into_iter() - .map(|(x, y)| (x, Amount::from_sat(y))) - .collect::>(); - return match - client.create_psbt(pared_inputs.as_slice(), &parsed_outputs, locktime, replaceable) - { - Ok(e) => Ok(e), - Err(e) => panic!("{:?}", e), - }; - } - pub fn get_new_address( - &self, - label: Option<&str>, - address_type: Option - ) -> Result { - let client = self.get_rpc_client(); - match - client.get_new_address( - label, - address_type.map(|x| x.into()) - ) - { - Ok(e) => Ok(e.assume_checked().to_string()), - Err(e) => panic!("{:?}", e), - } - } - pub fn is_address_mine( - self, - script: &bitcoin::Script, - network: bitcoin::Network - ) -> Result { - if let Ok(address) = bitcoin::Address::from_script(script, network) { - self.get_rpc_client() - .get_address_info(&address) - .map(|info| info.is_mine.unwrap_or(false)) - .map_err(|e| payjoin::Error::Server(e.into())) - } else { - Ok(false) - } - } + pub fn create_psbt( + &self, inputs: Vec, outputs: HashMap, locktime: Option, + replaceable: Option, + ) -> Result { + let client = self.get_rpc_client(); + let pared_inputs = inputs + .iter() + .map(|x| x.into()) + .collect::>(); + let parsed_outputs = outputs + .into_iter() + .map(|(x, y)| (x, Amount::from_sat(y))) + .collect::>(); + return match client.create_psbt( + pared_inputs.as_slice(), + &parsed_outputs, + locktime, + replaceable, + ) { + Ok(e) => Ok(e), + Err(e) => panic!("{:?}", e), + }; + } + pub fn get_new_address( + &self, label: Option<&str>, address_type: Option, + ) -> Result { + let client = self.get_rpc_client(); + match client.get_new_address(label, address_type.map(|x| x.into())) { + Ok(e) => Ok(e.assume_checked().to_string()), + Err(e) => panic!("{:?}", e), + } + } + pub fn is_address_mine( + self, script: &bitcoin::Script, network: bitcoin::Network, + ) -> Result { + if let Ok(address) = bitcoin::Address::from_script(script, network) { + self.get_rpc_client() + .get_address_info(&address) + .map(|info| info.is_mine.unwrap_or(false)) + .map_err(|e| payjoin::Error::Server(e.into())) + } else { + Ok(false) + } + } } pub struct Txid { - pub internal: String, + pub internal: String, } impl From for Txid { - fn from(value: bitcoin::hash_types::Txid) -> Self { - Txid { internal: value.to_string() } - } + fn from(value: bitcoin::hash_types::Txid) -> Self { + Txid { internal: value.to_string() } + } } impl From for bitcoin::hash_types::Txid { - fn from(value: Txid) -> Self { - bitcoin::hash_types::Txid::from_str(value.internal.as_str()).expect("Invalid Txid") - } + fn from(value: Txid) -> Self { + bitcoin::hash_types::Txid::from_str(value.internal.as_str()).expect("Invalid Txid") + } } pub struct OutPoint { - pub txid: Txid, - pub vout: u32, + pub txid: Txid, + pub vout: u32, } impl OutPoint { - pub fn new(txid: Txid, vout: u32) -> Self { - Self { txid: txid, vout: vout } - } + pub fn new(txid: Txid, vout: u32) -> Self { + Self { txid, vout } + } } impl From for bitcoin::blockdata::transaction::OutPoint { - fn from(value: OutPoint) -> Self { - bitcoin::blockdata::transaction::OutPoint { txid: value.txid.into(), vout: value.vout } - } + fn from(value: OutPoint) -> Self { + bitcoin::blockdata::transaction::OutPoint { txid: value.txid.into(), vout: value.vout } + } } diff --git a/src/error.rs b/src/error.rs index 0d38b6d..6d63722 100644 --- a/src/error.rs +++ b/src/error.rs @@ -5,46 +5,46 @@ use payjoin::receive::RequestError; #[derive(Debug, PartialEq, Eq)] pub enum Error { - /// Error encountered during PSBT decoding from Base64 string. - PsbtParseError(String), - ReceiveError(String), - ///Error that may occur when the request from sender is malformed. - ///This is currently opaque type because we aren’t sure which variants will stay. You can only display it. - RequestError(String), - ///Error that may occur when coin selection fails. - SelectionError(String), - ///Error returned when request could not be created. - ///This error can currently only happen due to programmer mistake. - CreateRequestError(String), - PjParseError(String), - UnexpectedError(String), + /// Error encountered during PSBT decoding from Base64 string. + PsbtParseError(String), + ReceiveError(String), + ///Error that may occur when the request from sender is malformed. + ///This is currently opaque type because we aren’t sure which variants will stay. You can only display it. + RequestError(String), + ///Error that may occur when coin selection fails. + SelectionError(String), + ///Error returned when request could not be created. + ///This error can currently only happen due to programmer mistake. + CreateRequestError(String), + PjParseError(String), + UnexpectedError(String), } impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Error::ReceiveError(e) => write!(f, "ReceiveError: {}", e), - Error::RequestError(e) => write!(f, "RequestError: {}", e), - Error::SelectionError(e) => write!(f, "SelectionError: {}", e), - Error::CreateRequestError(e) => write!(f, "CreateRequestError: {}", e), - Error::PjParseError(e) => write!(f, "PjParseError: {}", e), - Error::PsbtParseError(e) => write!(f, "PsbtParseError: {}", e), - Error::UnexpectedError(e) => write!(f, "UnexpectedError: {}", e), - } - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::ReceiveError(e) => write!(f, "ReceiveError: {}", e), + Error::RequestError(e) => write!(f, "RequestError: {}", e), + Error::SelectionError(e) => write!(f, "SelectionError: {}", e), + Error::CreateRequestError(e) => write!(f, "CreateRequestError: {}", e), + Error::PjParseError(e) => write!(f, "PjParseError: {}", e), + Error::PsbtParseError(e) => write!(f, "PsbtParseError: {}", e), + Error::UnexpectedError(e) => write!(f, "UnexpectedError: {}", e), + } + } } impl std::error::Error for Error {} impl From for Error { - fn from(value: RequestError) -> Self { - Error::RequestError(value.to_string()) - } + fn from(value: RequestError) -> Self { + Error::RequestError(value.to_string()) + } } impl From for Error { - fn from(value: PsbtParseError) -> Self { - Error::PsbtParseError(value.to_string()) - } + fn from(value: PsbtParseError) -> Self { + Error::PsbtParseError(value.to_string()) + } } impl From for Error { - fn from(value: payjoin::Error) -> Self { - Error::UnexpectedError(value.to_string()) - } + fn from(value: payjoin::Error) -> Self { + Error::UnexpectedError(value.to_string()) + } } diff --git a/src/lib.rs b/src/lib.rs index 5984fab..5b9fb2e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,163 +1,150 @@ -pub mod send; pub mod bitcoind; pub mod error; pub mod receive; +pub mod send; pub mod transaction; -use std::{ collections::HashSet, fs::OpenOptions, str::FromStr }; -use bitcoin::{ Amount, ScriptBuf }; -use serde::{ Deserialize, Serialize }; +pub use bitcoin::blockdata::script::Script as BitcoinScript; pub use bitcoin::Address as BitcoinAdrress; +use bitcoin::{Amount, ScriptBuf}; pub use payjoin::Error as PdkError; -pub use bitcoin::blockdata::script::Script as BitcoinScript; +use serde::{Deserialize, Serialize}; +use std::{collections::HashSet, fs::OpenOptions, str::FromStr}; uniffi::include_scaffolding!("pdk"); pub struct CachedOutputs { - pub outputs: HashSet, - pub file: std::fs::File, + pub outputs: HashSet, + pub file: std::fs::File, } impl CachedOutputs { - pub fn new(path: String) -> Result { - let mut file = OpenOptions::new().write(true).read(true).create(true).open(path)?; - let outputs = bitcoincore_rpc::jsonrpc::serde_json - ::from_reader(&mut file) - .unwrap_or_else(|_| HashSet::new()); - Ok(Self { outputs, file }) - } + pub fn new(path: String) -> Result { + let mut file = OpenOptions::new().write(true).read(true).create(true).open(path)?; + let outputs = bitcoincore_rpc::jsonrpc::serde_json::from_reader(&mut file) + .unwrap_or_else(|_| HashSet::new()); + Ok(Self { outputs, file }) + } } /// A reference to a transaction output. #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)] pub struct OutPoint { - /// The referenced transaction's txid. - pub txid: String, - /// The index of the referenced output in its transaction's vout. - pub vout: u32, + /// The referenced transaction's txid. + pub txid: String, + /// The index of the referenced output in its transaction's vout. + pub vout: u32, } impl From for bitcoin::OutPoint { - fn from(outpoint: OutPoint) -> Self { - bitcoin::OutPoint { - txid: bitcoin::Txid::from_str(&outpoint.txid).expect("Invalid txid"), - vout: outpoint.vout, - } - } + fn from(outpoint: OutPoint) -> Self { + bitcoin::OutPoint { + txid: bitcoin::Txid::from_str(&outpoint.txid).expect("Invalid txid"), + vout: outpoint.vout, + } + } } impl From for OutPoint { - fn from(outpoint: bitcoin::OutPoint) -> Self { - OutPoint { - txid: outpoint.txid.to_string(), - vout: outpoint.vout, - } - } + fn from(outpoint: bitcoin::OutPoint) -> Self { + OutPoint { txid: outpoint.txid.to_string(), vout: outpoint.vout } + } } #[derive(Debug, Clone)] pub struct Script { - inner: Vec, + inner: Vec, } impl Script { - fn new(raw_output_script: Vec) -> Self { - let script = BitcoinScript::from_bytes(raw_output_script.as_slice()); - Script { inner: script.to_bytes() } - } + fn new(raw_output_script: Vec) -> Self { + let script = BitcoinScript::from_bytes(raw_output_script.as_slice()); + Script { inner: script.to_bytes() } + } - fn to_bytes(&self) -> Vec { - self.inner.to_owned() - } + fn to_bytes(&self) -> Vec { + self.inner.to_owned() + } } #[derive(Debug, Clone)] pub struct TxOut { - /// The value of the output, in satoshis. - value: u64, - /// The address of the output. - script_pubkey: Script, + /// The value of the output, in satoshis. + value: u64, + /// The address of the output. + script_pubkey: Script, } impl From for bitcoin::TxOut { - fn from(tx_out: TxOut) -> Self { - bitcoin::TxOut { - value: tx_out.value, - script_pubkey: ScriptBuf::from_bytes(tx_out.script_pubkey.inner), - } - } + fn from(tx_out: TxOut) -> Self { + bitcoin::TxOut { + value: tx_out.value, + script_pubkey: ScriptBuf::from_bytes(tx_out.script_pubkey.inner), + } + } } impl From for TxOut { - fn from(tx_out: bitcoin::TxOut) -> Self { - TxOut { - value: tx_out.value, - script_pubkey: Script { - inner: tx_out.script_pubkey.to_bytes(), - }, - } - } + fn from(tx_out: bitcoin::TxOut) -> Self { + TxOut { + value: tx_out.value, + script_pubkey: Script { inner: tx_out.script_pubkey.to_bytes() }, + } + } } pub struct Uri { - pub internal: String, + pub internal: String, } impl Uri { - pub fn new( - amount: u64, - endpoint: String, - address: String - ) -> Result { - let addr = bitcoin::address::Address::from_str(address.as_str()).expect("Invalid address"); - let uri_str = format!( - "{:?}?amount={}&pj={}", - addr, - Amount::from_sat(amount).to_btc(), - endpoint - ); - let _ = payjoin::Uri - ::from_str(uri_str.as_ref()) - .map_err(|e| panic!("Constructed a bad URI string from args: {}", e)); - Ok(Self { internal: uri_str }) - } + pub fn new( + amount: u64, endpoint: String, address: String, + ) -> Result { + let addr = bitcoin::address::Address::from_str(address.as_str()).expect("Invalid address"); + let uri_str = + format!("{:?}?amount={}&pj={}", addr, Amount::from_sat(amount).to_btc(), endpoint); + let _ = payjoin::Uri::from_str(uri_str.as_ref()) + .map_err(|e| panic!("Constructed a bad URI string from args: {}", e)); + Ok(Self { internal: uri_str }) + } - pub fn try_from(bip21_str: String) -> Result { - match payjoin::Uri::from_str(bip21_str.as_ref()) { - Ok(e) => Ok(Self { internal: bip21_str }), - Err(e) => panic!("Constructed a bad URI string from args: {}", e), - } - } + pub fn try_from(bip21_str: String) -> Result { + match payjoin::Uri::from_str(bip21_str.as_ref()) { + Ok(e) => Ok(Self { internal: bip21_str }), + Err(e) => panic!("Constructed a bad URI string from args: {}", e), + } + } } #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)] pub enum AddressType { - Legacy, - P2shSegwit, - Bech32, - Bech32m, + Legacy, + P2shSegwit, + Bech32, + Bech32m, } impl From for bitcoincore_rpc::json::AddressType { - fn from(value: AddressType) -> Self { - return match value { - AddressType::Legacy => bitcoincore_rpc::json::AddressType::Legacy, - AddressType::P2shSegwit => bitcoincore_rpc::json::AddressType::P2shSegwit, - AddressType::Bech32 => bitcoincore_rpc::json::AddressType::Bech32, - AddressType::Bech32m => bitcoincore_rpc::json::AddressType::Bech32m, - }; - } + fn from(value: AddressType) -> Self { + return match value { + AddressType::Legacy => bitcoincore_rpc::json::AddressType::Legacy, + AddressType::P2shSegwit => bitcoincore_rpc::json::AddressType::P2shSegwit, + AddressType::Bech32 => bitcoincore_rpc::json::AddressType::Bech32, + AddressType::Bech32m => bitcoincore_rpc::json::AddressType::Bech32m, + }; + } } pub struct Input { - pub txid: String, - pub vout: u32, - pub sequence: Option, + pub txid: String, + pub vout: u32, + pub sequence: Option, } impl Input { - pub fn new(txid: String, vout: u32, sequence: Option) -> Self { - Self { txid, vout, sequence } - } + pub fn new(txid: String, vout: u32, sequence: Option) -> Self { + Self { txid, vout, sequence } + } } impl From<&Input> for bitcoincore_rpc::json::CreateRawTransactionInput { - fn from(value: &Input) -> Self { - bitcoincore_rpc::json::CreateRawTransactionInput { - txid: bitcoin::Txid::from_str(&value.txid).expect("Invalid Txid"), - vout: value.vout, - sequence: value.sequence, - } - } + fn from(value: &Input) -> Self { + bitcoincore_rpc::json::CreateRawTransactionInput { + txid: bitcoin::Txid::from_str(&value.txid).expect("Invalid Txid"), + vout: value.vout, + sequence: value.sequence, + } + } } pub struct Address { - pub internal: BitcoinAdrress, + pub internal: BitcoinAdrress, } diff --git a/src/transaction.rs b/src/transaction.rs index cf6d57a..e80ed12 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -1,20 +1,18 @@ -use std::{ sync::Mutex, str::FromStr }; use bitcoin::blockdata::transaction::Transaction as BitcoinTransaction; use bitcoin::psbt::PartiallySignedTransaction as BitcoinPsbt; +use std::{str::FromStr, sync::Mutex}; use crate::error::Error; pub struct PartiallySignedTransaction { - pub(crate) internal: BitcoinPsbt, + pub(crate) internal: BitcoinPsbt, } impl PartiallySignedTransaction { - pub(crate) fn new(psbt_base64: String) -> Result { - let psbt: BitcoinPsbt = BitcoinPsbt::from_str(&psbt_base64)?; - Ok(PartiallySignedTransaction { - internal: psbt, - }) - } + pub(crate) fn new(psbt_base64: String) -> Result { + let psbt: BitcoinPsbt = BitcoinPsbt::from_str(&psbt_base64)?; + Ok(PartiallySignedTransaction { internal: psbt }) + } } pub struct Transaction { - pub(crate) internal: BitcoinTransaction, + pub(crate) internal: BitcoinTransaction, } diff --git a/uniffi-bindgen.rs b/uniffi-bindgen.rs index f6cff6c..2aea967 100644 --- a/uniffi-bindgen.rs +++ b/uniffi-bindgen.rs @@ -1,3 +1,3 @@ fn main() { - uniffi::uniffi_bindgen_main() + uniffi::uniffi_bindgen_main() } From e0287c61f33a506c768c3c74db74206060e37b25 Mon Sep 17 00:00:00 2001 From: BitcoinZavior Date: Fri, 25 Aug 2023 19:23:18 -0400 Subject: [PATCH 19/30] serialize() exposed --- README.md | 6 +++++- src/transaction.rs | 15 +++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8778f66..e015728 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ # payjoin-ffi -A simplified Payjoin library built using [PDK](https://payjoindevkit.org/) . +# Bindings for PDK + +This repository creates the `libpdkffi` multi-language library for the Rust-based [PDK](https://payjoindevkit.org/) from the [Payjoin Dev Kit] project. + +Each supported language and the platform(s) it's packaged for has its own directory. The Rust code in this project is in the bdk-ffi directory and is a wrapper around the [bdk] library to expose its APIs in a uniform way using the [mozilla/uniffi-rs] bindings generator for each supported target language. diff --git a/src/transaction.rs b/src/transaction.rs index e80ed12..6995a43 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -1,16 +1,19 @@ use bitcoin::blockdata::transaction::Transaction as BitcoinTransaction; -use bitcoin::psbt::PartiallySignedTransaction as BitcoinPsbt; -use std::{str::FromStr, sync::Mutex}; +use payjoin::bitcoin::psbt::PartiallySignedTransaction as BitcoinPsbt; +use std::{str::FromStr, sync::Arc}; use crate::error::Error; - +#[derive(Debug, Clone)] pub struct PartiallySignedTransaction { - pub(crate) internal: BitcoinPsbt, + pub internal: Arc, } impl PartiallySignedTransaction { - pub(crate) fn new(psbt_base64: String) -> Result { + pub fn new(psbt_base64: String) -> Result { let psbt: BitcoinPsbt = BitcoinPsbt::from_str(&psbt_base64)?; - Ok(PartiallySignedTransaction { internal: psbt }) + Ok(PartiallySignedTransaction { internal: Arc::new(psbt) }) + } + pub fn serialize(&self) -> Vec { + self.internal.serialize() } } pub struct Transaction { From 0c438ef52abe98ec3235e2d6e49932ca799a8f19 Mon Sep 17 00:00:00 2001 From: BitcoinZavior Date: Sat, 26 Aug 2023 12:20:48 -0400 Subject: [PATCH 20/30] exposed Uri, PrjUri & Url --- src/uri.rs | 112 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/src/uri.rs b/src/uri.rs index e69de29..2ad2037 100644 --- a/src/uri.rs +++ b/src/uri.rs @@ -0,0 +1,112 @@ +use crate::{ + send::{Configuration, Context, Request}, + transaction::PartiallySignedTransaction, + Address, Network, +}; +use bitcoin::address::{NetworkChecked, NetworkUnchecked}; +use payjoin::{bitcoin, PjUriExt, UriExt}; +use std::str::FromStr; + +#[derive(Clone)] +pub enum PayjoinUri { + Unchecked(payjoin::Uri<'static, NetworkUnchecked>), + Checked(payjoin::Uri<'static, NetworkChecked>), +} +#[derive(Clone)] +pub struct Uri { + internal: PayjoinUri, +} + +impl Uri { + pub fn from_str(uri: String) -> Result { + match payjoin::Uri::from_str(uri.as_str()) { + Ok(e) => Ok(Uri { internal: PayjoinUri::Unchecked(e) }), + Err(e) => anyhow::bail!(e), + } + } + pub fn assume_checked(self) -> Self { + match self.internal { + PayjoinUri::Unchecked(e) => Self { internal: PayjoinUri::Checked(e.assume_checked()) }, + PayjoinUri::Checked(e) => Self { internal: PayjoinUri::Checked(e) }, + } + } + pub fn check_pj_supported(self) -> Result { + match self.internal { + PayjoinUri::Unchecked(_) => anyhow::bail!("Network Unchecked"), + PayjoinUri::Checked(e) => match e.check_pj_supported() { + Ok(e) => Ok(PrjUri { internal: e }), + Err(e) => anyhow::bail!(e), + }, + } + } + pub fn address(self) -> Address { + match self.internal { + PayjoinUri::Unchecked(e) => { + Address { internal: crate::BitcoinAddress::Unchecked(e.address) } + } + PayjoinUri::Checked(e) => { + Address { internal: crate::BitcoinAddress::Checked(e.address) } + } + } + } + pub fn amount(self) -> Option { + match self.internal { + PayjoinUri::Unchecked(e) => match e.amount { + Some(a) => Some(a.to_sat()), + None => None, + }, + PayjoinUri::Checked(e) => match e.amount { + Some(a) => Some(a.to_sat()), + None => None, + }, + } + } + pub fn require_network(self, network: Network) -> Result { + match self.internal { + PayjoinUri::Unchecked(e) => Ok(Uri { + internal: PayjoinUri::Checked( + e.require_network(network.into()).expect("Invalid Network"), + ), + }), + PayjoinUri::Checked(_) => anyhow::bail!("Network already checked"), + } + } +} +#[derive(Debug, Clone)] +pub struct PrjUri { + pub internal: payjoin::PjUri<'static>, +} +impl PrjUri { + pub fn create_pj_request( + self, psbt: PartiallySignedTransaction, params: Configuration, + ) -> Result<(Request, Context), anyhow::Error> { + let config = std::mem::replace(&mut *params.internal.lock().unwrap(), None); + match self.internal.create_pj_request(psbt.internal.as_ref().to_owned(), config.unwrap()) { + Ok(e) => Ok(( + Request { url: Url { internal: e.0.url }, body: e.0.body }, + Context { internal: e.1 }, + )), + Err(e) => anyhow::bail!(e), + } + } + + pub fn address(self) -> String { + // Address { internal: crate::BitcoinAddress::Checked() } + self.internal.address.to_string() + } + pub fn amount(self) -> Option { + self.internal.amount + } +} + +pub struct Url { + pub internal: url::Url, +} +impl Url { + pub fn parse(input: String) -> Result { + match url::Url::from_str(input.as_str()) { + Ok(e) => Ok(Self { internal: e }), + Err(e) => anyhow::bail!(e), + } + } +} From f1a4cce55bae0d650b112225a36db8a9d9f2e7e1 Mon Sep 17 00:00:00 2001 From: BitcoinZavior Date: Sun, 27 Aug 2023 22:45:21 -0400 Subject: [PATCH 21/30] add url, bitcoind & env_logger --- Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 6e5b4e7..cfc3601 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,9 @@ uniffi = { version = "0.24.3", features = ["build"] } [dev-dependencies] uniffi = { version = "0.24.3", features = ["bindgen-tests"] } assert_matches = "1.5.0" +env_logger = "0.10.0" +bitcoind = { version = "0.32.0", features = ["0_21_2"] } + [dependencies] anyhow = "1.0.70" @@ -32,6 +35,7 @@ bitcoin = "0.30.1" serde_json = "1.0.103" uniffi = "0.24.3" hex = "0.4.3" +url = "2.4.0" [[bin]] name = "uniffi-bindgen" From 35ab4ce49bc70e116fb2a210f3c7b5dfe541584f Mon Sep 17 00:00:00 2001 From: BitcoinZavior Date: Mon, 28 Aug 2023 12:15:27 -0400 Subject: [PATCH 22/30] code cleanup & exposed process_response --- .gitignore | 1 + src/send.rs | 175 ++++++++++++++++++++++++++++++++-------------------- 2 files changed, 110 insertions(+), 66 deletions(-) diff --git a/.gitignore b/.gitignore index af91b92..698de20 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html Cargo.lock + # These are backup files generated by rustfmt **/*.rs.bk .vscode/settings.json diff --git a/src/send.rs b/src/send.rs index 76ae886..4000d6e 100644 --- a/src/send.rs +++ b/src/send.rs @@ -1,85 +1,128 @@ -use std::{string::ParseError, sync::RwLock}; - -pub use payjoin::send::{Configuration as PdkConfiguration, Context as PdkContext}; +use crate::{ transaction::PartiallySignedTransaction, uri::Url }; +use payjoin::send::ValidationError; +pub use payjoin::send::{ Configuration as PdkConfiguration, Context as PdkContext }; +use std::sync::{ Arc, Mutex, MutexGuard }; ///Builder for sender-side payjoin parameters ///These parameters define how client wants to handle Payjoin. -pub(crate) struct Configuration { - pub(crate) internal: RwLock>, +pub struct Configuration { + pub internal: Mutex>, } impl Configuration { - ///Offer the receiver contribution to pay for his input. - ///These parameters will allow the receiver to take max_fee_contribution from given change output to pay for additional inputs. The recommended fee is size_of_one_input * fee_rate. - ///change_index specifies which output can be used to pay fee. If None is provided, then the output is auto-detected unless the supplied transaction has more than two outputs. - pub fn with_fee_contribution(max_fee_contribution: u64, change_index: Option) -> Self { - let configuration = PdkConfiguration::with_fee_contribution( - bitcoin::Amount::from_sat(max_fee_contribution), - change_index, - ); - Self { internal: RwLock::new(Some(configuration)) } - } - ///Perform Payjoin without incentivizing the payee to cooperate. - ///While it’s generally better to offer some contribution some users may wish not to. This function disables contribution. - pub fn non_incentivizing() -> Self { - Self { internal: RwLock::new(Some(PdkConfiguration::non_incentivizing())) } - } - ///Disable output substitution even if the receiver didn’t. - ///This forbids receiver switching output or decreasing amount. It is generally not recommended to set this as it may prevent the receiver from doing advanced operations such as opening LN channels and it also guarantees the receiver will not reward the sender with a discount. - pub fn always_disable_output_substitution(self, disable: bool) -> Self { - { - let mut data_guard = self.internal.write().unwrap(); - // Temporarily take out the Configuration and replace with a dummy value - let _config = std::mem::replace(&mut *data_guard, None); - *data_guard = Some(_config.unwrap().always_disable_output_substitution(disable)); - } - self - } - ///Decrease fee contribution instead of erroring. - ///If this option is set and a transaction with change amount lower than fee contribution is provided then instead of returning error the fee contribution will be just lowered to match the change amount. - pub fn clamp_fee_contribution(self, clamp: bool) -> Self { - { - let mut data_guard = self.internal.write().unwrap(); - // Temporarily take out the Configuration and replace with a dummy value - let _config = std::mem::replace(&mut *data_guard, None); - *data_guard = Some(_config.unwrap().clamp_fee_contribution(clamp)); - } - self - } - ///Sets minimum fee rate required by the sender. - pub fn min_fee_rate_sat_per_vb(self, fee_rate: u64) -> Self { - { - let mut data_guard = self.internal.write().unwrap(); - // Temporarily take out the Configuration and replace with a dummy value - let _config = std::mem::replace(&mut *data_guard, None); - *data_guard = Some(_config.unwrap().min_fee_rate_sat_per_vb(fee_rate)); - } - self - } + pub(crate) fn get_configuration_mutex(&self) -> MutexGuard> { + self.internal.lock().expect("PdkConfiguration") + } + ///Offer the receiver contribution to pay for his input. + ///These parameters will allow the receiver to take max_fee_contribution from given change output to pay for additional inputs. The recommended fee is size_of_one_input * fee_rate. + ///change_index specifies which output can be used to pay fee. If None is provided, then the output is auto-detected unless the supplied transaction has more than two outputs. + pub fn with_fee_contribution(max_fee_contribution: u64, change_index: Option) -> Self { + let configuration = PdkConfiguration::with_fee_contribution( + payjoin::bitcoin::Amount::from_sat(max_fee_contribution), + change_index + ); + Self { internal: Mutex::new(Some(configuration)) } + } + ///Perform Payjoin without incentivizing the payee to cooperate. + ///While it’s generally better to offer some contribution some users may wish not to. This function disables contribution. + pub fn non_incentivizing() -> Self { + Self { internal: Mutex::new(Some(PdkConfiguration::non_incentivizing())) } + } + ///Disable output substitution even if the receiver didn’t. + ///This forbids receiver switching output or decreasing amount. It is generally not recommended to set this as it may prevent the receiver from doing advanced operations such as opening LN channels and it also guarantees the receiver will not reward the sender with a discount. + pub fn always_disable_output_substitution(self, disable: bool) -> Self { + { + let mut data_guard = self.get_configuration_mutex(); + // Temporarily take out the Configuration and replace with a dummy value + let _config = std::mem::replace(&mut *data_guard, None); + *data_guard = Some(_config.unwrap().always_disable_output_substitution(disable)); + } + self + } + ///Decrease fee contribution instead of erroring. + ///If this option is set and a transaction with change amount lower than fee contribution is provided then instead of returning error the fee contribution will be just lowered to match the change amount. + pub fn clamp_fee_contribution(self, clamp: bool) -> Self { + { + let mut data_guard = self.get_configuration_mutex(); + // Temporarily take out the Configuration and replace with a dummy value + let _config = std::mem::replace(&mut *data_guard, None); + *data_guard = Some(_config.unwrap().clamp_fee_contribution(clamp)); + } + self + } + ///Sets minimum fee rate required by the sender. + pub fn min_fee_rate_sat_per_vb(self, fee_rate: u64) -> Self { + { + let mut data_guard = self.get_configuration_mutex(); + // Temporarily take out the Configuration and replace with a dummy value + let _config = std::mem::replace(&mut *data_guard, None); + *data_guard = Some(_config.unwrap().min_fee_rate_sat_per_vb(fee_rate)); + } + self + } } + +///Data required for validation of response. + +///This type is used to process the response. It is returned from PjUriExt::create_pj_request() method and you only need to call .process_response() on it to continue BIP78 flow. pub struct Context { - pub internal: PdkContext, + pub internal: PdkContext, +} + +impl Context { + ///Decodes and validates the response. + + ///Call this method with response from receiver to continue BIP78 flow. If the response is valid you will get appropriate PSBT that you should sign and broadcast. + pub fn process_response( + self, + response: &mut impl std::io::Read + ) -> Result { + match self.internal.process_response(response) { + Ok(e) => Ok(PartiallySignedTransaction { internal: Arc::new(e.to_owned()) }), + Err(e) => Err(e), + } + } } ///Represents data that needs to be transmitted to the receiver. ///You need to send this request over HTTP(S) to the receiver. pub struct Request { - ///URL to send the request to. + ///URL to send the request to. - ///This is full URL with scheme etc - you can pass it right to reqwest or a similar library. - pub url: Url, - ///Bytes to be sent to the receiver. + ///This is full URL with scheme etc - you can pass it right to reqwest or a similar library. + pub url: Url, + ///Bytes to be sent to the receiver. - ///This is properly encoded PSBT, already in base64. You only need to make sure Content-Type is text/plain and Content-Length is body.len() (most libraries do the latter automatically). - pub body: Vec, + ///This is properly encoded PSBT, already in base64. You only need to make sure Content-Type is text/plain and Content-Length is body.len() (most libraries do the latter automatically). + pub body: Vec, } -pub struct Url { - pub internal: String, -} -impl Url { - pub fn parse(input: String) -> Result { - Ok(Self { internal: input }) - } + +#[cfg(test)] +mod tests { + #[test] + fn official_vectors() { + use std::str::FromStr; + + use bitcoin::psbt::Psbt; + + let original_psbt = + "cHNidP8BAHMCAAAAAY8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////AtyVuAUAAAAAF6kUHehJ8GnSdBUOOv6ujXLrWmsJRDCHgIQeAAAAAAAXqRR3QJbbz0hnQ8IvQ0fptGn+votneofTAAAAAAEBIKgb1wUAAAAAF6kU3k4ekGHKWRNbA1rV5tR5kEVDVNCHAQcXFgAUx4pFclNVgo1WWAdN1SYNX8tphTABCGsCRzBEAiB8Q+A6dep+Rz92vhy26lT0AjZn4PRLi8Bf9qoB/CMk0wIgP/Rj2PWZ3gEjUkTlhDRNAQ0gXwTO7t9n+V14pZ6oljUBIQMVmsAaoNWHVMS02LfTSe0e388LNitPa1UQZyOihY+FFgABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUAAA="; + + let proposal = + "cHNidP8BAJwCAAAAAo8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////jye60aAl3JgZdaIERvjkeh72VYZuTGH/ps2I4l0IO4MBAAAAAP7///8CJpW4BQAAAAAXqRQd6EnwadJ0FQ46/q6NcutaawlEMIcACT0AAAAAABepFHdAltvPSGdDwi9DR+m0af6+i2d6h9MAAAAAAQEgqBvXBQAAAAAXqRTeTh6QYcpZE1sDWtXm1HmQRUNU0IcBBBYAFMeKRXJTVYKNVlgHTdUmDV/LaYUwIgYDFZrAGqDVh1TEtNi300ntHt/PCzYrT2tVEGcjooWPhRYYSFzWUDEAAIABAACAAAAAgAEAAAAAAAAAAAEBIICEHgAAAAAAF6kUyPLL+cphRyyI5GTUazV0hF2R2NWHAQcXFgAUX4BmVeWSTJIEwtUb5TlPS/ntohABCGsCRzBEAiBnu3tA3yWlT0WBClsXXS9j69Bt+waCs9JcjWtNjtv7VgIge2VYAaBeLPDB6HGFlpqOENXMldsJezF9Gs5amvDQRDQBIQJl1jz1tBt8hNx2owTm+4Du4isx0pmdKNMNIjjaMHFfrQABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUIgICygvBWB5prpfx61y1HDAwo37kYP3YRJBvAjtunBAur3wYSFzWUDEAAIABAACAAAAAgAEAAAABAAAAAAA="; + + let original_psbt = Psbt::from_str(original_psbt).unwrap(); + eprintln!("original: {:#?}", original_psbt); + let mut proposal = Psbt::from_str(proposal).unwrap(); + eprintln!("proposal: {:#?}", proposal); + for mut output in proposal.clone().outputs { + output.bip32_derivation.clear(); + } + for mut input in proposal.clone().inputs { + input.bip32_derivation.clear(); + } + proposal.inputs[0].witness_utxo = None; + } } From 9991c4a012c7ea94671a941df02e8b6a29e53a00 Mon Sep 17 00:00:00 2001 From: BitcoinZavior Date: Tue, 29 Aug 2023 21:34:31 -0400 Subject: [PATCH 23/30] Add Network and Address --- src/lib.rs | 283 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 179 insertions(+), 104 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5b9fb2e..b3a47db 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,149 +2,224 @@ pub mod bitcoind; pub mod error; pub mod receive; pub mod send; +#[cfg(test)] +mod test; pub mod transaction; -pub use bitcoin::blockdata::script::Script as BitcoinScript; -pub use bitcoin::Address as BitcoinAdrress; -use bitcoin::{Amount, ScriptBuf}; +pub mod uri; +pub use payjoin::bitcoin; +use payjoin::bitcoin::{ + address::{ NetworkChecked, NetworkUnchecked }, + Address as _BitcoinAdrress, + ScriptBuf as BitcoinScriptBuf, +}; pub use payjoin::Error as PdkError; -use serde::{Deserialize, Serialize}; -use std::{collections::HashSet, fs::OpenOptions, str::FromStr}; +use serde::{ Deserialize, Serialize }; +use std::{ collections::HashSet, fs::OpenOptions, str::FromStr }; uniffi::include_scaffolding!("pdk"); pub struct CachedOutputs { - pub outputs: HashSet, - pub file: std::fs::File, + pub outputs: HashSet, + pub file: std::fs::File, } impl CachedOutputs { - pub fn new(path: String) -> Result { - let mut file = OpenOptions::new().write(true).read(true).create(true).open(path)?; - let outputs = bitcoincore_rpc::jsonrpc::serde_json::from_reader(&mut file) - .unwrap_or_else(|_| HashSet::new()); - Ok(Self { outputs, file }) - } + pub fn new(path: String) -> Result { + let mut file = OpenOptions::new().write(true).read(true).create(true).open(path)?; + let outputs = bitcoincore_rpc::jsonrpc::serde_json + ::from_reader(&mut file) + .unwrap_or_else(|_| HashSet::new()); + Ok(Self { outputs, file }) + } } /// A reference to a transaction output. #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)] pub struct OutPoint { - /// The referenced transaction's txid. - pub txid: String, - /// The index of the referenced output in its transaction's vout. - pub vout: u32, + /// The referenced transaction's txid. + pub txid: String, + /// The index of the referenced output in its transaction's vout. + pub vout: u32, } impl From for bitcoin::OutPoint { - fn from(outpoint: OutPoint) -> Self { - bitcoin::OutPoint { - txid: bitcoin::Txid::from_str(&outpoint.txid).expect("Invalid txid"), - vout: outpoint.vout, - } - } + fn from(outpoint: OutPoint) -> Self { + bitcoin::OutPoint { + txid: bitcoin::Txid::from_str(&outpoint.txid).expect("Invalid txid"), + vout: outpoint.vout, + } + } } impl From for OutPoint { - fn from(outpoint: bitcoin::OutPoint) -> Self { - OutPoint { txid: outpoint.txid.to_string(), vout: outpoint.vout } - } + fn from(outpoint: bitcoin::OutPoint) -> Self { + OutPoint { txid: outpoint.txid.to_string(), vout: outpoint.vout } + } } -#[derive(Debug, Clone)] -pub struct Script { - inner: Vec, +//TODO; RECREATE ADDRESS STRUCTURE +#[derive(Debug, Clone, PartialEq)] +pub struct Address { + pub internal: BitcoinAddress, +} +#[derive(Debug, Clone, PartialEq)] +pub enum BitcoinAddress { + Unchecked(_BitcoinAdrress), + Checked(_BitcoinAdrress), } -impl Script { - fn new(raw_output_script: Vec) -> Self { - let script = BitcoinScript::from_bytes(raw_output_script.as_slice()); - Script { inner: script.to_bytes() } - } +impl Address { + pub fn from_script(script: ScriptBuf, network: Network) -> Result { + match _BitcoinAdrress::from_script(script.inner.as_script(), network.into()) { + Ok(e) => Ok(Address { internal: BitcoinAddress::Checked(e) }), + Err(e) => anyhow::bail!(e), + } + } + pub fn assume_checked(self) -> Result { + match self.internal { + BitcoinAddress::Unchecked(e) => { + Ok(Address { internal: BitcoinAddress::Checked(e.assume_checked()) }) + } + BitcoinAddress::Checked(e) => Ok(Address { internal: BitcoinAddress::Checked(e) }), + } + } + pub fn from_str(address: &str) -> Result { + match _BitcoinAdrress::from_str(&address) { + Ok(e) => Ok(Address { internal: BitcoinAddress::Unchecked(e) }), + Err(e) => anyhow::bail!(e), + } + } + pub fn to_string(self) -> String { + match self.internal { + BitcoinAddress::Unchecked(e) => serde_json::to_string(&e).unwrap(), + BitcoinAddress::Checked(e) => serde_json::to_string(&e).unwrap(), + } + } +} - fn to_bytes(&self) -> Vec { - self.inner.to_owned() - } +impl From
for bitcoin::Address { + fn from(value: Address) -> Self { + match value.internal { + BitcoinAddress::Unchecked(e) => e.assume_checked(), + BitcoinAddress::Checked(e) => e, + } + } } #[derive(Debug, Clone)] +pub struct ScriptBuf { + inner: BitcoinScriptBuf, +} +impl ScriptBuf { + pub fn new(raw_output_script: Vec) -> Self { + let buf = BitcoinScriptBuf::from_bytes(raw_output_script); + ScriptBuf { inner: buf } + } + + pub fn to_bytes(self) -> Vec { + self.get_internal().to_bytes() + } + pub fn to_hex_string(self) -> String { + self.get_internal().to_hex_string() + } + pub fn to_string(self) -> String { + self.get_internal().to_string() + } + pub fn to_asm_string(self) -> String { + self.get_internal().to_asm_string() + } + fn get_internal(self) -> BitcoinScriptBuf { + self.inner + } +} +impl From for bitcoin::ScriptBuf { + fn from(value: ScriptBuf) -> Self { + value.get_internal() + } +} +impl From for ScriptBuf { + fn from(value: bitcoin::ScriptBuf) -> Self { + ScriptBuf { inner: value } + } +} + +#[derive(Debug)] pub struct TxOut { - /// The value of the output, in satoshis. - value: u64, - /// The address of the output. - script_pubkey: Script, + /// The value of the output, in satoshis. + value: u64, + /// The address of the output. + script_pubkey: ScriptBuf, } impl From for bitcoin::TxOut { - fn from(tx_out: TxOut) -> Self { - bitcoin::TxOut { - value: tx_out.value, - script_pubkey: ScriptBuf::from_bytes(tx_out.script_pubkey.inner), - } - } + fn from(tx_out: TxOut) -> Self { + bitcoin::TxOut { value: tx_out.value, script_pubkey: tx_out.script_pubkey.get_internal() } + } } impl From for TxOut { - fn from(tx_out: bitcoin::TxOut) -> Self { - TxOut { - value: tx_out.value, - script_pubkey: Script { inner: tx_out.script_pubkey.to_bytes() }, - } - } -} -pub struct Uri { - pub internal: String, -} - -impl Uri { - pub fn new( - amount: u64, endpoint: String, address: String, - ) -> Result { - let addr = bitcoin::address::Address::from_str(address.as_str()).expect("Invalid address"); - let uri_str = - format!("{:?}?amount={}&pj={}", addr, Amount::from_sat(amount).to_btc(), endpoint); - let _ = payjoin::Uri::from_str(uri_str.as_ref()) - .map_err(|e| panic!("Constructed a bad URI string from args: {}", e)); - Ok(Self { internal: uri_str }) - } - - pub fn try_from(bip21_str: String) -> Result { - match payjoin::Uri::from_str(bip21_str.as_ref()) { - Ok(e) => Ok(Self { internal: bip21_str }), - Err(e) => panic!("Constructed a bad URI string from args: {}", e), - } - } + fn from(tx_out: bitcoin::TxOut) -> Self { + TxOut { + value: tx_out.value, + script_pubkey: ScriptBuf { inner: tx_out.script_pubkey.into() }, + } + } } #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)] pub enum AddressType { - Legacy, - P2shSegwit, - Bech32, - Bech32m, + Legacy, + P2shSegwit, + Bech32, + Bech32m, } impl From for bitcoincore_rpc::json::AddressType { - fn from(value: AddressType) -> Self { - return match value { - AddressType::Legacy => bitcoincore_rpc::json::AddressType::Legacy, - AddressType::P2shSegwit => bitcoincore_rpc::json::AddressType::P2shSegwit, - AddressType::Bech32 => bitcoincore_rpc::json::AddressType::Bech32, - AddressType::Bech32m => bitcoincore_rpc::json::AddressType::Bech32m, - }; - } + fn from(value: AddressType) -> Self { + return match value { + AddressType::Legacy => bitcoincore_rpc::json::AddressType::Legacy, + AddressType::P2shSegwit => bitcoincore_rpc::json::AddressType::P2shSegwit, + AddressType::Bech32 => bitcoincore_rpc::json::AddressType::Bech32, + AddressType::Bech32m => bitcoincore_rpc::json::AddressType::Bech32m, + }; + } } pub struct Input { - pub txid: String, - pub vout: u32, - pub sequence: Option, + pub txid: String, + pub vout: u32, + pub sequence: Option, } impl Input { - pub fn new(txid: String, vout: u32, sequence: Option) -> Self { - Self { txid, vout, sequence } - } + pub fn new(txid: String, vout: u32, sequence: Option) -> Self { + Self { txid, vout, sequence } + } } impl From<&Input> for bitcoincore_rpc::json::CreateRawTransactionInput { - fn from(value: &Input) -> Self { - bitcoincore_rpc::json::CreateRawTransactionInput { - txid: bitcoin::Txid::from_str(&value.txid).expect("Invalid Txid"), - vout: value.vout, - sequence: value.sequence, - } - } -} -pub struct Address { - pub internal: BitcoinAdrress, + fn from(value: &Input) -> Self { + bitcoincore_rpc::json::CreateRawTransactionInput { + txid: bitcoin::Txid::from_str(&value.txid).expect("Invalid Txid"), + vout: value.vout, + sequence: value.sequence, + } + } +} +#[derive(Clone)] +///The cryptocurrency to act on +pub enum Network { + ///Bitcoin’s testnet + Testnet, + ///Bitcoin’s regtest + Regtest, + ///Classic Bitcoin + Bitcoin, + ///Bitcoin’s signet + Signet, +} +impl Default for Network { + fn default() -> Self { + Network::Testnet + } +} +impl From for bitcoin::Network { + fn from(network: Network) -> Self { + match network { + Network::Signet => bitcoin::Network::Signet, + Network::Testnet => bitcoin::Network::Testnet, + Network::Regtest => bitcoin::Network::Regtest, + Network::Bitcoin => bitcoin::Network::Bitcoin, + } + } } From 126a5cff885a1e4a6a0d4a3884c6bb7291e9517e Mon Sep 17 00:00:00 2001 From: BitcoinZavior Date: Wed, 30 Aug 2023 12:01:31 -0400 Subject: [PATCH 24/30] Add tests & code clean up --- src/receive.rs | 458 +++++++++++++++++++++++++++++------------------- src/test/mod.rs | 233 ++++++++++++++++++++++++ 2 files changed, 506 insertions(+), 185 deletions(-) create mode 100644 src/test/mod.rs diff --git a/src/receive.rs b/src/receive.rs index 5f38143..d724f5a 100644 --- a/src/receive.rs +++ b/src/receive.rs @@ -1,238 +1,326 @@ -use std::sync::{Mutex, MutexGuard, RwLock}; - use crate::{ - transaction::{PartiallySignedTransaction, Transaction}, - Address, OutPoint, PdkError, Script, TxOut, + transaction::{ PartiallySignedTransaction, Transaction }, + Address, + OutPoint, + PdkError, + ScriptBuf, + TxOut, }; -use anyhow::anyhow; use payjoin::receive::{ - MaybeInputsOwned as PdkMaybeInputsOwned, MaybeInputsSeen as PdkMaybeInputsSeen, - MaybeMixedInputScripts as PdkMaybeMixedInputScripts, OutputsUnknown as PdkOutputsUnknown, - PayjoinProposal as PdkPayjoinProposal, RequestError, UncheckedProposal as PdkUncheckedProposal, + MaybeInputsOwned as PdkMaybeInputsOwned, + MaybeInputsSeen as PdkMaybeInputsSeen, + MaybeMixedInputScripts as PdkMaybeMixedInputScripts, + OutputsUnknown as PdkOutputsUnknown, + PayjoinProposal as PdkPayjoinProposal, + RequestError, + UncheckedProposal as PdkUncheckedProposal, }; +use std::{ collections::HashMap, sync::{ Arc, Mutex, MutexGuard } }; -pub struct UncheckedProposal { - pub internal: PdkUncheckedProposal, -} pub trait CanBroadcast { - fn test_mempool_accept( - &self, tx_hex: Vec, - ) -> Result< - Vec, - bitcoincore_rpc::Error, - >; -} -pub struct Headers { - pub length: String, + fn test_mempool_accept(&self, tx_hex: Vec) -> Result; } +pub struct Headers(pub HashMap); impl Headers { - pub fn new(length: u64) -> Headers { - Headers { length: length.to_string() } - } + pub fn from_vec(body: Vec) -> Headers { + let mut h = HashMap::new(); + h.insert("content-type".to_string(), "text/plain".to_string()); + h.insert("content-length".to_string(), body.len().to_string()); + Headers(h) + } } impl payjoin::receive::Headers for Headers { - fn get_header(&self, key: &str) -> Option<&str> { - match key { - "content-length" => Some(&self.length), - "content-type" => Some("text/plain"), - _ => None, - } - } + fn get_header(&self, key: &str) -> Option<&str> { + self.0.get(key).map(|e| e.as_str()) + } +} +pub struct UncheckedProposal { + pub internal: PdkUncheckedProposal, } - impl UncheckedProposal { - pub fn from_request( - //TODO; Find which type that implement Read trait is an appropriate option - body: impl std::io::Read, - query: String, - headers: Headers, - ) -> Result { - let res = PdkUncheckedProposal::from_request(body, query.as_str(), headers)?; - Ok(UncheckedProposal { internal: res }) - } - // fn get_internal(&self) -> MutexGuard { - // self.internal.lock().expect("PdkUncheckedProposal") - // } - pub fn get_transaction_to_schedule_broadcast(&self) -> Transaction { - let res = self.internal.get_transaction_to_schedule_broadcast(); - Transaction { internal: res } - } - pub fn check_can_broadcast( - self, can_broadcast: Box, - ) -> Result { - let res = self.internal.check_can_broadcast(|tx| { - let raw_tx = hex::encode(bitcoin::consensus::encode::serialize(&tx)); - let mempool_results = can_broadcast - .test_mempool_accept(vec![raw_tx]) - .map_err(|e| PdkError::Server(e.into()))?; - match mempool_results.first() { - Some(result) => Ok(result.allowed), - None => Err(PdkError::Server( - anyhow!("No mempool results returned on broadcast check").into(), - )), - } - })?; - Ok(MaybeInputsOwned { internal: res }) - } + pub fn from_request( + //TODO; Find which type that implement Read trait is an appropriate option + body: impl std::io::Read, + query: String, + headers: Headers + ) -> Result { + let res = PdkUncheckedProposal::from_request(body, query.as_str(), headers)?; + Ok(UncheckedProposal { internal: res }) + } + + pub fn get_transaction_to_schedule_broadcast(&self) -> Transaction { + let res = self.internal.get_transaction_to_schedule_broadcast(); + Transaction { internal: res } + } + pub fn check_can_broadcast( + self, + can_broadcast: Box + ) -> Result { + let res = self.internal.check_can_broadcast(|tx| { + let raw_tx = hex::encode(bitcoin::consensus::encode::serialize(&tx)); + let mempool_results = can_broadcast.test_mempool_accept(vec![raw_tx]); + match mempool_results { + Ok(e) => Ok(e), + Err(e) => Err(PdkError::Server(e.into())), + } + })?; + Ok(MaybeInputsOwned { internal: res }) + } + pub fn assume_interactive_receiver(self) -> MaybeInputsOwned { + MaybeInputsOwned { internal: self.internal.assume_interactive_receiver() } + } } ///Typestate to validate that the Original PSBT has no receiver-owned inputs. ///Call check_no_receiver_owned_inputs() to proceed. pub struct MaybeInputsOwned { - pub internal: PdkMaybeInputsOwned, + pub internal: PdkMaybeInputsOwned, } pub trait IsScriptOwned { - fn is_owned(&self, script: Script) -> Result; + fn is_owned(&self, script: ScriptBuf) -> Result; } impl MaybeInputsOwned { - ///Check that the Original PSBT has no receiver-owned inputs. Return original-psbt-rejected error or otherwise refuse to sign undesirable inputs. - - ///An attacker could try to spend receiver’s own inputs. This check prevents that. - pub fn check_inputs_not_owned( - self, is_owned: Box, - ) -> Result { - match self - .internal - .check_inputs_not_owned(|input| is_owned.is_owned(Script { inner: input.to_bytes() })) - { - Ok(e) => Ok(MaybeMixedInputScripts { internal: e }), - Err(e) => Err(e), - } - } + ///Check that the Original PSBT has no receiver-owned inputs. Return original-psbt-rejected error or otherwise refuse to sign undesirable inputs. + + ///An attacker could try to spend receiver’s own inputs. This check prevents that. + pub fn check_inputs_not_owned( + self, + is_owned: impl IsScriptOwned + ) -> Result { + match + self.internal.check_inputs_not_owned(|input| { + is_owned.is_owned(ScriptBuf { inner: input.to_owned() }) + }) + { + Ok(e) => Ok(MaybeMixedInputScripts { internal: e }), + Err(e) => Err(e), + } + } } ///Typestate to validate that the Original PSBT has no inputs that have been seen before. ///Call check_no_inputs_seen to proceed. pub struct MaybeMixedInputScripts { - pub internal: PdkMaybeMixedInputScripts, + pub internal: PdkMaybeMixedInputScripts, } impl MaybeMixedInputScripts { - ///Verify the original transaction did not have mixed input types Call this after checking downstream. - ///Note: mixed spends do not necessarily indicate distinct wallet fingerprints. This check is intended to prevent some types of wallet fingerprinting. - pub fn check_no_mixed_input_scripts( - self, - ) -> Result { - match self.internal.check_no_mixed_input_scripts() { - Ok(e) => Ok(MaybeInputsSeen { internal: e }), - Err(e) => Err(e), - } - } + ///Verify the original transaction did not have mixed input types Call this after checking downstream. + ///Note: mixed spends do not necessarily indicate distinct wallet fingerprints. This check is intended to prevent some types of wallet fingerprinting. + pub fn check_no_mixed_input_scripts( + self + ) -> Result { + match self.internal.check_no_mixed_input_scripts() { + Ok(e) => Ok(MaybeInputsSeen { internal: e }), + Err(e) => Err(e), + } + } } ///Typestate to validate that the Original PSBT has no inputs that have been seen before. +pub trait IsOutoutKnown { + fn is_known(&self, outpoint: OutPoint) -> Result; +} ///Call check_no_inputs_seen to proceed. pub struct MaybeInputsSeen { - pub internal: PdkMaybeInputsSeen, -} -pub trait IsOutoutKnown { - fn is_known(&self, outpoint: OutPoint) -> Result; + pub internal: PdkMaybeInputsSeen, } impl MaybeInputsSeen { - pub fn check_no_inputs_seen_before( - self, is_known: Box, - ) -> Result { - match self - .internal - .check_no_inputs_seen_before(|outpoint| is_known.is_known(outpoint.to_owned().into())) - { - Ok(e) => Ok(OutputsUnknown { internal: e }), - Err(e) => Err(e), - } - } + pub fn check_no_inputs_seen_before( + self, + is_known: impl IsOutoutKnown + ) -> Result { + match + self.internal.check_no_inputs_seen_before(|outpoint| + is_known.is_known(outpoint.to_owned().into()) + ) + { + Ok(e) => Ok(OutputsUnknown { internal: e }), + Err(e) => Err(e), + } + } } ///The receiver has not yet identified which outputs belong to the receiver. ///Only accept PSBTs that send us money. Identify those outputs with identify_receiver_outputs() to proceed pub struct OutputsUnknown { - pub internal: PdkOutputsUnknown, + pub internal: PdkOutputsUnknown, } impl OutputsUnknown { - ///Find which outputs belong to the receiver - pub fn identify_receiver_outputs( - self, is_receiver_output: Box, - ) -> Result { - match self.internal.identify_receiver_outputs(|output_script| { - is_receiver_output.is_owned(Script { inner: output_script.to_bytes() }) - }) { - Ok(e) => Ok(PayjoinProposal { internal: Mutex::new(Some(e)) }), - Err(e) => Err(e), - } - } + ///Find which outputs belong to the receiver + pub fn identify_receiver_outputs( + self, + is_receiver_output: impl IsScriptOwned + ) -> Result { + match + self.internal.identify_receiver_outputs(|output_script| { + is_receiver_output.is_owned(ScriptBuf { inner: output_script.to_owned() }) + }) + { + Ok(e) => Ok(PayjoinProposal { internal: Mutex::new(Some(e)) }), + Err(e) => Err(e), + } + } } ///A mutable checked proposal that the receiver may contribute inputs to to make a payjoin. pub struct PayjoinProposal { - pub internal: Mutex>, + pub internal: Mutex>, } impl PayjoinProposal { - pub(crate) fn get_proposal(&self) -> MutexGuard> { - self.internal.lock().expect("PayjoinProposal") - } - pub fn is_output_substitution_disabled(&self) -> bool { - if self.get_proposal().as_ref().is_none() { - //TODO CREATE CUSTOM ERROR - panic!("PayjoinProposal not initalized"); - } - self.get_proposal().as_mut().unwrap().is_output_substitution_disabled() - } - - pub fn contribute_witness_input(self, txout: TxOut, outpoint: OutPoint) -> Self { - if self.get_proposal().as_ref().is_none() { - panic!("PayjoinProposal not initalized"); - } - self.get_proposal() - .as_mut() - .unwrap() - .contribute_witness_input(txout.into(), outpoint.into()); - self - } - pub fn contribute_non_witness_input(self, tx: Transaction, outpoint: OutPoint) { - if self.get_proposal().as_ref().is_none() { - panic!("PayjoinProposal not initalized"); - } - self.get_proposal() - .as_mut() - .unwrap() - .contribute_non_witness_input(tx.internal, outpoint.into()) - } - pub fn substitute_output_address(self, substitute_address: Address) { - if self.get_proposal().as_ref().is_none() { - panic!("PayjoinProposal not initalized"); - } - self.get_proposal().as_mut().unwrap().substitute_output_address(substitute_address.internal) - } - - ///Apply additional fee contribution now that the receiver has contributed input this is kind of a “build_proposal” step before we sign and finalize and extract - - ///WARNING: DO NOT ALTER INPUTS OR OUTPUTS AFTER THIS STEP - pub fn apply_fee( - self, min_feerate_sat_per_vb: Option, - ) -> Result { - if self.get_proposal().as_ref().is_none() { - panic!("PayjoinProposal not initalized"); - } - - match self.get_proposal().as_mut().unwrap().apply_fee(min_feerate_sat_per_vb) { - Ok(e) => Ok(PartiallySignedTransaction { internal: e.to_owned() }), - Err(e) => Err(e), - } - } - ///Return a Payjoin Proposal PSBT that the sender will find acceptable. - ///This attempts to calculate any network fee owed by the receiver, subtract it from their output, and return a PSBT that can produce a consensus-valid transaction that the sender will accept. - ///wallet_process_psbt should sign and finalize receiver inputs - pub fn prepare_psbt( - self, processed_psbt: PartiallySignedTransaction, - ) -> Result { - let mut data_guard = self.get_proposal(); - // Temporarily take out the Configuration and replace with a dummy value - let taken_proposal = std::mem::replace(&mut *data_guard, None); - match taken_proposal.unwrap().prepare_psbt(processed_psbt.internal.to_owned()) { - Ok(e) => Ok(PartiallySignedTransaction { internal: e.to_owned() }), - Err(e) => Err(e), - } - } + pub(crate) fn get_proposal(&self) -> MutexGuard> { + self.internal.lock().expect("PayjoinProposal") + } + pub fn is_output_substitution_disabled(&self) -> bool { + if self.get_proposal().as_ref().is_none() { + //TODO CREATE CUSTOM ERROR + panic!("PayjoinProposal not initalized"); + } + self.get_proposal().as_mut().unwrap().is_output_substitution_disabled() + } + pub fn contribute_witness_input(&self, txout: TxOut, outpoint: OutPoint) -> &Self { + if self.get_proposal().as_ref().is_none() { + panic!("PayjoinProposal not initalized"); + } + self.get_proposal() + .as_mut() + .unwrap() + .contribute_witness_input(txout.into(), outpoint.into()); + self + } + pub fn contribute_non_witness_input(&self, tx: Transaction, outpoint: OutPoint) { + if self.get_proposal().as_ref().is_none() { + panic!("PayjoinProposal not initalized"); + } + self.get_proposal() + .as_mut() + .unwrap() + .contribute_non_witness_input(tx.internal, outpoint.into()) + } + pub fn substitute_output_address(&self, substitute_address: Address) { + if self.get_proposal().as_ref().is_none() { + panic!("PayjoinProposal not initalized"); + } + self.get_proposal().as_mut().unwrap().substitute_output_address(substitute_address.into()) + } + ///Apply additional fee contribution now that the receiver has contributed input this is kind of a “build_proposal” step before we sign and finalize and extract + ///WARNING: DO NOT ALTER INPUTS OR OUTPUTS AFTER THIS STEP + pub fn apply_fee( + &self, + min_feerate_sat_per_vb: Option + ) -> Result { + if self.get_proposal().as_ref().is_none() { + panic!("PayjoinProposal not initalized"); + } + + match self.get_proposal().as_mut().unwrap().apply_fee(min_feerate_sat_per_vb) { + Ok(e) => Ok(PartiallySignedTransaction { internal: Arc::new(e.to_owned()) }), + Err(e) => Err(e), + } + } + ///Return a Payjoin Proposal PSBT that the sender will find acceptable. + ///This attempts to calculate any network fee owed by the receiver, subtract it from their output, and return a PSBT that can produce a consensus-valid transaction that the sender will accept. + ///wallet_process_psbt should sign and finalize receiver inputs + pub fn prepare_psbt( + &self, + processed_psbt: PartiallySignedTransaction + ) -> Result { + let mut data_guard = self.get_proposal(); + let taken_proposal = std::mem::replace(&mut *data_guard, None); + match taken_proposal.unwrap().prepare_psbt(processed_psbt.internal.as_ref().to_owned()) { + Ok(e) => Ok(PartiallySignedTransaction { internal: Arc::new(e.to_owned()) }), + Err(e) => Err(e), + } + } + ///Select receiver input such that the payjoin avoids surveillance. Return the input chosen that has been applied to the Proposal. + ///Proper coin selection allows payjoin to resemble ordinary transactions. To ensure the resemblence, a number of heuristics must be avoided. + ///UIH “Unecessary input heuristic” is one class of them to avoid. We define UIH1 and UIH2 according to the BlockSci practice BlockSci UIH1 and UIH2: + pub fn try_preserving_privacy( + &self, + candidate_inputs: HashMap + ) -> Result { + let mut _candidate_inputs: HashMap = HashMap::new(); + for (key, value) in candidate_inputs.iter() { + _candidate_inputs.insert( + bitcoin::Amount::from_sat(key.to_owned()), + value.to_owned().into() + ); + } + let data_guard = self.get_proposal(); + match data_guard.as_ref().unwrap().try_preserving_privacy(_candidate_inputs) { + Ok(e) => Ok(OutPoint { txid: e.txid.to_string(), vout: e.vout }), + Err(e) => Err(e), + } + } + // TODO - pub fn utxos_to_be_locked(&self) +} + +#[cfg(test)] +mod test { + use crate::Network; + + use super::*; + + fn get_proposal_from_test_vector() -> Result { + // OriginalPSBT Test Vector from BIP + // | InputScriptType | Orginal PSBT Fee rate | maxadditionalfeecontribution | additionalfeeoutputindex| + // |-----------------|-----------------------|------------------------------|-------------------------| + // | P2SH-P2WPKH | 2 sat/vbyte | 0.00000182 | 0 | + let original_psbt = + "cHNidP8BAHMCAAAAAY8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////AtyVuAUAAAAAF6kUHehJ8GnSdBUOOv6ujXLrWmsJRDCHgIQeAAAAAAAXqRR3QJbbz0hnQ8IvQ0fptGn+votneofTAAAAAAEBIKgb1wUAAAAAF6kU3k4ekGHKWRNbA1rV5tR5kEVDVNCHAQcXFgAUx4pFclNVgo1WWAdN1SYNX8tphTABCGsCRzBEAiB8Q+A6dep+Rz92vhy26lT0AjZn4PRLi8Bf9qoB/CMk0wIgP/Rj2PWZ3gEjUkTlhDRNAQ0gXwTO7t9n+V14pZ6oljUBIQMVmsAaoNWHVMS02LfTSe0e388LNitPa1UQZyOihY+FFgABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUAAA="; + + let body = original_psbt.as_bytes(); + let headers = Headers::from_vec(body.to_vec()); + UncheckedProposal::from_request( + body, + "?maxadditionalfeecontribution=182?additionalfeeoutputindex=0".to_string(), + headers + ) + } + + #[test] + fn can_get_proposal_from_request() { + let proposal = get_proposal_from_test_vector(); + assert!(proposal.is_ok(), "OriginalPSBT should be a valid request"); + } + struct MockScriptOwned {} + struct MockOutputOwned {} + impl IsOutoutKnown for MockOutputOwned { + fn is_known(&self, _: OutPoint) -> Result { + Ok(false) + } + } + impl IsScriptOwned for MockScriptOwned { + fn is_owned(&self, script: ScriptBuf) -> Result { + { + let network = Network::Bitcoin; + Ok( + Address::from_script(script, network.clone()).unwrap() == + Address::from_str("3CZZi7aWFugaCdUCS15dgrUUViupmB8bVM").unwrap() + ) + } + // Ok(false) + } + } + + #[test] + fn unchecked_proposal_unlocks_after_checks() { + let proposal = get_proposal_from_test_vector().unwrap(); + let payjoin = proposal + .assume_interactive_receiver() + .check_inputs_not_owned(MockScriptOwned {}) + .expect("No inputs should be owned") + .check_no_mixed_input_scripts() + .expect("No mixed input scripts") + .check_no_inputs_seen_before(MockOutputOwned {}) + .expect("No inputs should be seen before") + .identify_receiver_outputs(MockScriptOwned {}) + .expect("Receiver output should be identified"); + let payjoin = payjoin.apply_fee(None); + + assert!(payjoin.is_ok(), "Payjoin should be a valid PSBT"); + } } diff --git a/src/test/mod.rs b/src/test/mod.rs new file mode 100644 index 0000000..1d701f8 --- /dev/null +++ b/src/test/mod.rs @@ -0,0 +1,233 @@ +use bitcoin::psbt::Psbt; +use bitcoind::bitcoincore_rpc; +use bitcoind::bitcoincore_rpc::core_rpc_json::AddressType; +use bitcoind::bitcoincore_rpc::RpcApi; +use log::{ debug, log_enabled, Level }; +use payjoin::bitcoin; +use payjoin::bitcoin::base64; +use std::collections::HashMap; +use std::str::FromStr; +use std::sync::Arc; + +use crate::receive::{ Headers, IsOutoutKnown, IsScriptOwned, UncheckedProposal }; +use crate::send::{ Configuration, Request }; +use crate::transaction::PartiallySignedTransaction; +use crate::uri::Uri; +use crate::{ Network, ScriptBuf }; + +#[test] +fn integration_test() { + let _ = env_logger::try_init(); + let bitcoind_exe = std::env + ::var("BITCOIND_EXE") + .ok() + .or_else(|| bitcoind::downloaded_exe_path().ok()) + .expect("version feature or env BITCOIND_EXE is required for tests"); + let mut conf = bitcoind::Conf::default(); + conf.view_stdout = log_enabled!(Level::Debug); + let bitcoind = bitcoind::BitcoinD::with_conf(bitcoind_exe, &conf).unwrap(); + let receiver = bitcoind.create_wallet("receiver").unwrap(); + let receiver_address = receiver + .get_new_address(None, Some(AddressType::Bech32)) + .unwrap() + .assume_checked(); + let sender = bitcoind.create_wallet("sender").unwrap(); + let sender_address = sender + .get_new_address(None, Some(AddressType::Bech32)) + .unwrap() + .assume_checked(); + bitcoind.client.generate_to_address(1, &receiver_address).unwrap(); + bitcoind.client.generate_to_address(101, &sender_address).unwrap(); + + assert_eq!( + payjoin::bitcoin::Amount::from_btc(50.0).unwrap(), + receiver.get_balances().unwrap().mine.trusted, + "receiver doesn't own bitcoin" + ); + + assert_eq!( + payjoin::bitcoin::Amount::from_btc(50.0).unwrap(), + sender.get_balances().unwrap().mine.trusted, + "sender doesn't own bitcoin" + ); + + // Receiver creates the payjoin URI + let pj_receiver_address = receiver.get_new_address(None, None).unwrap().assume_checked(); + let amount = payjoin::bitcoin::Amount::from_btc(1.0).unwrap(); + let pj_uri_string = format!( + "{}?amount={}&pj=https://example.com", + pj_receiver_address.to_qr_uri(), + amount.to_btc() + ); + let _uri = Uri::from_str(pj_uri_string).unwrap().assume_checked(); + let pj_uri = _uri.check_pj_supported().expect("Bad Uri"); + // Sender create a funded PSBT (not broadcasted) to address with amount given in the pj_uri + let mut outputs = HashMap::with_capacity(1); + outputs.insert(pj_uri.clone().address(), pj_uri.clone().amount().unwrap()); + debug!("outputs: {:?}", outputs); + let options = bitcoincore_rpc::json::WalletCreateFundedPsbtOptions { + lock_unspent: Some(true), + fee_rate: Some(payjoin::bitcoin::Amount::from_sat(2000)), + ..Default::default() + }; + let psbt = sender + .wallet_create_funded_psbt( + &[], // inputs + &outputs, + None, // locktime + Some(options), + None + ) + .expect("failed to create PSBT").psbt; + let psbt = sender.wallet_process_psbt(&psbt, None, None, None).unwrap().psbt; + let psbt = PartiallySignedTransaction::new(psbt).expect("Psbt new"); + debug!("Original psbt: {:#?}", psbt); + let pj_params = Configuration::with_fee_contribution(10000, None); + let (req, ctx) = pj_uri.create_pj_request(psbt, pj_params).unwrap(); + let headers = Headers::from_vec(req.body.clone()); + + // ********************** + // Inside the Receiver: + // this data would transit from one party to another over the network in production + let rec_clone = Arc::new(receiver); + let response = handle_pj_request(req, headers, rec_clone.clone()); + // this response would be returned as http response to the sender + + // ********************** + // Inside the Sender: + // Sender checks, signs, finalizes, extracts, and broadcasts + let checked_payjoin_proposal_psbt = ctx.process_response(&mut response.as_bytes()).unwrap(); + let payjoin_base64_string = base64::encode(&checked_payjoin_proposal_psbt.serialize()); + let payjoin_psbt = sender + .wallet_process_psbt(&payjoin_base64_string, None, None, None) + .unwrap().psbt; + let payjoin_psbt = sender.finalize_psbt(&payjoin_psbt, Some(false)).unwrap().psbt.unwrap(); + let payjoin_psbt = Psbt::from_str(&payjoin_psbt).unwrap(); + debug!("Sender's Payjoin PSBT: {:#?}", payjoin_psbt); + + let payjoin_tx = payjoin_psbt.extract_tx(); + bitcoind.client.send_raw_transaction(&payjoin_tx).unwrap(); +} + +// Receiver receive and process original_psbt from a sender +// In production it it will come in as an HTTP request (over ssl or onion) +fn handle_pj_request( + req: Request, + headers: Headers, + receiver: Arc +) -> String { + // Receiver receive payjoin proposal, IRL it will be an HTTP request (over ssl or onion) + let proposal = UncheckedProposal::from_request( + req.body.as_slice(), + req.url.internal.query().unwrap_or("").to_string(), + headers + ).unwrap(); + + // 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.get_transaction_to_schedule_broadcast(); + + let proposal = proposal + .check_can_broadcast(Box::new(TestBroadcast(receiver.clone()))) + .expect("Payjoin proposal should be broadcastable"); + + // Receive Check 2: receiver can't sign for proposal inputs + let proposal = proposal + .check_inputs_not_owned(MockScriptOwned(receiver.clone())) + .expect("Receiver should not own any of the inputs"); + + // Receive Check 3: receiver can't sign for proposal inputs + let proposal = proposal.check_no_mixed_input_scripts().unwrap(); + + // Receive Check 4: have we seen this input before? More of a check for non-interactive i.e. payment processor receivers. + let payjoin = Arc::new( + proposal + .check_no_inputs_seen_before(MockOutputOwned {}) + .unwrap() + .identify_receiver_outputs(MockScriptOwned(receiver.clone())) + .expect("Receiver should have at least one output") + ); + print!("payjoin: {}", payjoin.is_output_substitution_disabled()); + // Select receiver payjoin inputs. TODO Lock them. + let available_inputs = receiver.list_unspent(None, None, None, None, None).unwrap(); + let candidate_inputs: HashMap = available_inputs + .iter() + .map(|i| (i.amount.to_sat(), crate::OutPoint { txid: i.txid.to_string(), vout: i.vout })) + .collect(); + + let selected_outpoint = payjoin.try_preserving_privacy(candidate_inputs).expect("gg"); + let selected_utxo = available_inputs + .iter() + .find(|i| { + i.txid.to_string() == selected_outpoint.txid.to_string() && + i.vout == selected_outpoint.vout + }) + .unwrap(); + + // calculate receiver payjoin outputs given receiver payjoin inputs and original_psbt, + let txo_to_contribute = crate::TxOut { + value: selected_utxo.amount.to_sat(), + script_pubkey: ScriptBuf { inner: selected_utxo.script_pub_key.clone() }, + }; + let outpoint_to_contribute = crate::OutPoint { + txid: selected_utxo.txid.to_string(), + vout: selected_utxo.vout, + }; + payjoin.contribute_witness_input(txo_to_contribute, outpoint_to_contribute); + + let receiver_substitute_address = receiver + .get_new_address(None, None) + .unwrap() + .assume_checked(); + payjoin.substitute_output_address( + crate::Address + ::from_str(receiver_substitute_address.to_string().as_str()) + .expect("Invalid address") + ); + + let payjoin_proposal_psbt = payjoin.apply_fee(None).expect("Aplly fee"); + + // Sign payjoin psbt + let payjoin_base64_string = base64::encode(&payjoin_proposal_psbt.serialize()); + let payjoin_proposal_psbt = receiver + .wallet_process_psbt(&payjoin_base64_string, None, None, Some(false)) + .unwrap().psbt; + let payjoin_proposal_psbt = + PartiallySignedTransaction::new(payjoin_proposal_psbt).expect("Invalid psbt"); + + let payjoin_proposal_psbt = payjoin.prepare_psbt(payjoin_proposal_psbt).expect("Prepare psbt"); + debug!("Receiver's Payjoin proposal PSBT: {:#?}", payjoin_proposal_psbt); + + base64::encode(&payjoin_proposal_psbt.serialize()) +} + +struct TestBroadcast(Arc); +impl crate::receive::CanBroadcast for TestBroadcast { + fn test_mempool_accept(&self, tx_hex: Vec) -> Result { + match self.0.test_mempool_accept(&tx_hex) { + Ok(e) => + Ok(match e.first() { + Some(e) => e.allowed, + None => anyhow::bail!("No Mempool Result"), + }), + Err(e) => anyhow::bail!(e), + } + } +} +struct MockScriptOwned(Arc); +struct MockOutputOwned {} +impl IsOutoutKnown for MockOutputOwned { + fn is_known(&self, _: crate::OutPoint) -> Result { + Ok(false) + } +} +impl IsScriptOwned for MockScriptOwned { + fn is_owned(&self, script: ScriptBuf) -> Result { + { + let network = Network::Regtest; + + let address = crate::Address::from_script(script, network.clone()).unwrap(); + let addr: bitcoin::Address = address.into(); + Ok(self.0.get_address_info(&addr).unwrap().is_mine.unwrap()) + } + } +} From 0975377a098ad204a4a605a76fd986075eac10d6 Mon Sep 17 00:00:00 2001 From: BitcoinZavior Date: Thu, 31 Aug 2023 16:03:21 -0400 Subject: [PATCH 25/30] removed bitcoind functions --- src/bitcoind.rs | 152 ------------------------------------------------ 1 file changed, 152 deletions(-) delete mode 100644 src/bitcoind.rs diff --git a/src/bitcoind.rs b/src/bitcoind.rs deleted file mode 100644 index 612c9ba..0000000 --- a/src/bitcoind.rs +++ /dev/null @@ -1,152 +0,0 @@ -use std::{ - collections::HashMap, - str::FromStr, - sync::{Arc, Mutex, MutexGuard}, -}; - -use bitcoin::Amount; -use bitcoincore_rpc::{self, RpcApi}; -use serde::Deserialize; - -use crate::{AddressType, CachedOutputs, Input}; -#[derive(Debug, Deserialize, Clone)] -pub struct BitcoindConfig { - pub rpc_host: String, - pub cookie: Option, - pub rpc_user: String, - pub rpc_pass: String, - pub cache_dir: String, -} -#[derive(Clone)] -pub struct BitcoindClient { - pub config: BitcoindConfig, - pub bitcoind_mutex: Arc>, - pub cached_outputs: Arc>, -} -impl BitcoindClient { - pub fn new(config: BitcoindConfig) -> Result { - let bitcoind = (match &config.cookie { - Some(cookie) => bitcoincore_rpc::Client::new( - &config.rpc_host, - bitcoincore_rpc::Auth::CookieFile(cookie.into()), - ), - None => bitcoincore_rpc::Client::new( - &config.rpc_host, - bitcoincore_rpc::Auth::UserPass(config.rpc_user.clone(), config.rpc_pass.clone()), - ), - }) - .expect("Failed to connect to bitcoind"); - let seen_input = Arc::new(Mutex::new(CachedOutputs::new(config.cache_dir.clone())?)); - let bitcoind_mutex = Arc::new(Mutex::new(bitcoind)); - Ok(Self { config, bitcoind_mutex, cached_outputs: seen_input }) - } - fn get_rpc_client(&self) -> MutexGuard { - return self.bitcoind_mutex.lock().unwrap(); - } - pub fn load_wallet(&self, wallet_name: String) -> Result { - let client = self.get_rpc_client(); - match client.load_wallet(&wallet_name.as_str()) { - Ok(e) => { - return match e.warning { - Some(e) => panic!("{:?}", e), - None => Ok(wallet_name), - }; - } - Err(e) => panic!("{:?}", e), - }; - } - pub fn create_wallet( - &self, wallet_name: String, disable_private_keys: Option, blank: Option, - passphrase: Option, avoid_reuse: Option, - ) -> Result { - let client = self.get_rpc_client(); - match client.create_wallet( - &wallet_name.as_str(), - disable_private_keys, - blank, - passphrase.as_ref().map(|x| x.as_str()), - avoid_reuse, - ) { - Ok(e) => { - return match e.warning { - Some(e) => panic!("{:?}", e), - None => Ok(wallet_name), - }; - } - Err(e) => panic!("{:?}", e), - }; - } - - pub fn create_psbt( - &self, inputs: Vec, outputs: HashMap, locktime: Option, - replaceable: Option, - ) -> Result { - let client = self.get_rpc_client(); - let pared_inputs = inputs - .iter() - .map(|x| x.into()) - .collect::>(); - let parsed_outputs = outputs - .into_iter() - .map(|(x, y)| (x, Amount::from_sat(y))) - .collect::>(); - return match client.create_psbt( - pared_inputs.as_slice(), - &parsed_outputs, - locktime, - replaceable, - ) { - Ok(e) => Ok(e), - Err(e) => panic!("{:?}", e), - }; - } - pub fn get_new_address( - &self, label: Option<&str>, address_type: Option, - ) -> Result { - let client = self.get_rpc_client(); - match client.get_new_address(label, address_type.map(|x| x.into())) { - Ok(e) => Ok(e.assume_checked().to_string()), - Err(e) => panic!("{:?}", e), - } - } - pub fn is_address_mine( - self, script: &bitcoin::Script, network: bitcoin::Network, - ) -> Result { - if let Ok(address) = bitcoin::Address::from_script(script, network) { - self.get_rpc_client() - .get_address_info(&address) - .map(|info| info.is_mine.unwrap_or(false)) - .map_err(|e| payjoin::Error::Server(e.into())) - } else { - Ok(false) - } - } -} - -pub struct Txid { - pub internal: String, -} -impl From for Txid { - fn from(value: bitcoin::hash_types::Txid) -> Self { - Txid { internal: value.to_string() } - } -} -impl From for bitcoin::hash_types::Txid { - fn from(value: Txid) -> Self { - bitcoin::hash_types::Txid::from_str(value.internal.as_str()).expect("Invalid Txid") - } -} -pub struct OutPoint { - pub txid: Txid, - pub vout: u32, -} -impl OutPoint { - pub fn new(txid: Txid, vout: u32) -> Self { - Self { txid, vout } - } -} -impl From for bitcoin::blockdata::transaction::OutPoint { - fn from(value: OutPoint) -> Self { - bitcoin::blockdata::transaction::OutPoint { txid: value.txid.into(), vout: value.vout } - } -} From 00d07e6fc65dc8d85680f5a9e319cb45ff000bf2 Mon Sep 17 00:00:00 2001 From: BitcoinZavior Date: Fri, 1 Sep 2023 22:38:45 -0400 Subject: [PATCH 26/30] removed bitcoin dependency --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index cfc3601..85186a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,6 @@ reqwest = { version = "0.11.4", features = ["blocking"] } rcgen = { version = "0.11.1", optional = true } serde = { version = "1.0.160", features = ["derive"] } payjoin = { version = "0.9.0", features = ["send", "receive", "rand"] } -bitcoin = "0.30.1" serde_json = "1.0.103" uniffi = "0.24.3" hex = "0.4.3" From 8310960ba2ebe95f3d16a82cbfe64991dcbc7e7b Mon Sep 17 00:00:00 2001 From: BitcoinZavior Date: Sat, 2 Sep 2023 17:31:20 -0400 Subject: [PATCH 27/30] Address updated; code clean up --- src/lib.rs | 311 +++++++++++++++++++++++++---------------------------- 1 file changed, 146 insertions(+), 165 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b3a47db..2cf88a6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,3 @@ -pub mod bitcoind; pub mod error; pub mod receive; pub mod send; @@ -8,218 +7,200 @@ pub mod transaction; pub mod uri; pub use payjoin::bitcoin; use payjoin::bitcoin::{ - address::{ NetworkChecked, NetworkUnchecked }, - Address as _BitcoinAdrress, - ScriptBuf as BitcoinScriptBuf, + address::{NetworkChecked, NetworkUnchecked}, + Address as _BitcoinAdrress, ScriptBuf as BitcoinScriptBuf, }; pub use payjoin::Error as PdkError; -use serde::{ Deserialize, Serialize }; -use std::{ collections::HashSet, fs::OpenOptions, str::FromStr }; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; uniffi::include_scaffolding!("pdk"); -pub struct CachedOutputs { - pub outputs: HashSet, - pub file: std::fs::File, -} -impl CachedOutputs { - pub fn new(path: String) -> Result { - let mut file = OpenOptions::new().write(true).read(true).create(true).open(path)?; - let outputs = bitcoincore_rpc::jsonrpc::serde_json - ::from_reader(&mut file) - .unwrap_or_else(|_| HashSet::new()); - Ok(Self { outputs, file }) - } -} /// A reference to a transaction output. #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)] pub struct OutPoint { - /// The referenced transaction's txid. - pub txid: String, - /// The index of the referenced output in its transaction's vout. - pub vout: u32, + /// The referenced transaction's txid. + pub txid: String, + /// The index of the referenced output in its transaction's vout. + pub vout: u32, } impl From for bitcoin::OutPoint { - fn from(outpoint: OutPoint) -> Self { - bitcoin::OutPoint { - txid: bitcoin::Txid::from_str(&outpoint.txid).expect("Invalid txid"), - vout: outpoint.vout, - } - } + fn from(outpoint: OutPoint) -> Self { + bitcoin::OutPoint { + txid: bitcoin::Txid::from_str(&outpoint.txid).expect("Invalid txid"), + vout: outpoint.vout, + } + } } impl From for OutPoint { - fn from(outpoint: bitcoin::OutPoint) -> Self { - OutPoint { txid: outpoint.txid.to_string(), vout: outpoint.vout } - } + fn from(outpoint: bitcoin::OutPoint) -> Self { + OutPoint { txid: outpoint.txid.to_string(), vout: outpoint.vout } + } } //TODO; RECREATE ADDRESS STRUCTURE #[derive(Debug, Clone, PartialEq)] -pub struct Address { - pub internal: BitcoinAddress, -} -#[derive(Debug, Clone, PartialEq)] -pub enum BitcoinAddress { - Unchecked(_BitcoinAdrress), - Checked(_BitcoinAdrress), +pub struct Address +where + T: bitcoin::address::NetworkValidation, +{ + pub internal: _BitcoinAdrress, +} +impl Address { + pub fn from_script(script: ScriptBuf, network: Network) -> Result { + match _BitcoinAdrress::from_script(script.inner.as_script(), network.into()) { + Ok(e) => Ok(Address { internal: e }), + Err(e) => anyhow::bail!(e), + } + } + pub fn to_string(self) -> String { + self.internal.to_string() + } +} +impl Address { + pub fn assume_checked(self) -> Result, anyhow::Error> { + Ok(Address { internal: self.internal.assume_checked() }) + } + pub fn require_network( + self, network: Network, + ) -> Result, anyhow::Error> { + Ok(Address { + internal: self.internal.require_network(network.into()).expect("Invalid Network"), + }) + } + pub fn from_str(address: &str) -> Result { + match _BitcoinAdrress::from_str(&address) { + Ok(e) => Ok(Address { internal: e }), + Err(e) => anyhow::bail!(e), + } + } } -impl Address { - pub fn from_script(script: ScriptBuf, network: Network) -> Result { - match _BitcoinAdrress::from_script(script.inner.as_script(), network.into()) { - Ok(e) => Ok(Address { internal: BitcoinAddress::Checked(e) }), - Err(e) => anyhow::bail!(e), - } - } - pub fn assume_checked(self) -> Result { - match self.internal { - BitcoinAddress::Unchecked(e) => { - Ok(Address { internal: BitcoinAddress::Checked(e.assume_checked()) }) - } - BitcoinAddress::Checked(e) => Ok(Address { internal: BitcoinAddress::Checked(e) }), - } - } - pub fn from_str(address: &str) -> Result { - match _BitcoinAdrress::from_str(&address) { - Ok(e) => Ok(Address { internal: BitcoinAddress::Unchecked(e) }), - Err(e) => anyhow::bail!(e), - } - } - pub fn to_string(self) -> String { - match self.internal { - BitcoinAddress::Unchecked(e) => serde_json::to_string(&e).unwrap(), - BitcoinAddress::Checked(e) => serde_json::to_string(&e).unwrap(), - } - } +impl From> for bitcoin::Address { + fn from(value: Address) -> Self { + value.internal + } } -impl From
for bitcoin::Address { - fn from(value: Address) -> Self { - match value.internal { - BitcoinAddress::Unchecked(e) => e.assume_checked(), - BitcoinAddress::Checked(e) => e, - } - } -} #[derive(Debug, Clone)] pub struct ScriptBuf { - inner: BitcoinScriptBuf, + inner: BitcoinScriptBuf, } impl ScriptBuf { - pub fn new(raw_output_script: Vec) -> Self { - let buf = BitcoinScriptBuf::from_bytes(raw_output_script); - ScriptBuf { inner: buf } - } + pub fn new(raw_output_script: Vec) -> Self { + let buf = BitcoinScriptBuf::from_bytes(raw_output_script); + ScriptBuf { inner: buf } + } - pub fn to_bytes(self) -> Vec { - self.get_internal().to_bytes() - } - pub fn to_hex_string(self) -> String { - self.get_internal().to_hex_string() - } - pub fn to_string(self) -> String { - self.get_internal().to_string() - } - pub fn to_asm_string(self) -> String { - self.get_internal().to_asm_string() - } - fn get_internal(self) -> BitcoinScriptBuf { - self.inner - } + pub fn to_bytes(self) -> Vec { + self.get_internal().to_bytes() + } + pub fn to_hex_string(self) -> String { + self.get_internal().to_hex_string() + } + pub fn to_string(self) -> String { + self.get_internal().to_string() + } + pub fn to_asm_string(self) -> String { + self.get_internal().to_asm_string() + } + fn get_internal(self) -> BitcoinScriptBuf { + self.inner + } } impl From for bitcoin::ScriptBuf { - fn from(value: ScriptBuf) -> Self { - value.get_internal() - } + fn from(value: ScriptBuf) -> Self { + value.get_internal() + } } impl From for ScriptBuf { - fn from(value: bitcoin::ScriptBuf) -> Self { - ScriptBuf { inner: value } - } + fn from(value: bitcoin::ScriptBuf) -> Self { + ScriptBuf { inner: value } + } } #[derive(Debug)] pub struct TxOut { - /// The value of the output, in satoshis. - value: u64, - /// The address of the output. - script_pubkey: ScriptBuf, + /// The value of the output, in satoshis. + value: u64, + /// The address of the output. + script_pubkey: ScriptBuf, } impl From for bitcoin::TxOut { - fn from(tx_out: TxOut) -> Self { - bitcoin::TxOut { value: tx_out.value, script_pubkey: tx_out.script_pubkey.get_internal() } - } + fn from(tx_out: TxOut) -> Self { + bitcoin::TxOut { value: tx_out.value, script_pubkey: tx_out.script_pubkey.get_internal() } + } } impl From for TxOut { - fn from(tx_out: bitcoin::TxOut) -> Self { - TxOut { - value: tx_out.value, - script_pubkey: ScriptBuf { inner: tx_out.script_pubkey.into() }, - } - } + fn from(tx_out: bitcoin::TxOut) -> Self { + TxOut { + value: tx_out.value, + script_pubkey: ScriptBuf { inner: tx_out.script_pubkey.into() }, + } + } } #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)] pub enum AddressType { - Legacy, - P2shSegwit, - Bech32, - Bech32m, + Legacy, + P2shSegwit, + Bech32, + Bech32m, } impl From for bitcoincore_rpc::json::AddressType { - fn from(value: AddressType) -> Self { - return match value { - AddressType::Legacy => bitcoincore_rpc::json::AddressType::Legacy, - AddressType::P2shSegwit => bitcoincore_rpc::json::AddressType::P2shSegwit, - AddressType::Bech32 => bitcoincore_rpc::json::AddressType::Bech32, - AddressType::Bech32m => bitcoincore_rpc::json::AddressType::Bech32m, - }; - } -} -pub struct Input { - pub txid: String, - pub vout: u32, - pub sequence: Option, -} -impl Input { - pub fn new(txid: String, vout: u32, sequence: Option) -> Self { - Self { txid, vout, sequence } - } -} -impl From<&Input> for bitcoincore_rpc::json::CreateRawTransactionInput { - fn from(value: &Input) -> Self { - bitcoincore_rpc::json::CreateRawTransactionInput { - txid: bitcoin::Txid::from_str(&value.txid).expect("Invalid Txid"), - vout: value.vout, - sequence: value.sequence, - } - } -} + fn from(value: AddressType) -> Self { + return match value { + AddressType::Legacy => bitcoincore_rpc::json::AddressType::Legacy, + AddressType::P2shSegwit => bitcoincore_rpc::json::AddressType::P2shSegwit, + AddressType::Bech32 => bitcoincore_rpc::json::AddressType::Bech32, + AddressType::Bech32m => bitcoincore_rpc::json::AddressType::Bech32m, + }; + } +} +// pub struct Input { +// pub txid: String, +// pub vout: u32, +// pub sequence: Option, +// } +// impl Input { +// pub fn new(txid: String, vout: u32, sequence: Option) -> Self { +// Self { txid, vout, sequence } +// } +// } +// impl From<&Input> for bitcoincore_rpc::json::CreateRawTransactionInput { +// fn from(value: &Input) -> Self { +// bitcoincore_rpc::json::CreateRawTransactionInput { +// txid: bitcoin::Txid::from_str(&value.txid).expect("Invalid Txid"), +// vout: value.vout, +// sequence: value.sequence, +// } +// } +// } + #[derive(Clone)] -///The cryptocurrency to act on pub enum Network { - ///Bitcoin’s testnet - Testnet, - ///Bitcoin’s regtest - Regtest, - ///Classic Bitcoin - Bitcoin, - ///Bitcoin’s signet - Signet, + ///Bitcoin’s testnet + Testnet, + ///Bitcoin’s regtest + Regtest, + ///Classic Bitcoin + Bitcoin, + ///Bitcoin’s signet + Signet, } impl Default for Network { - fn default() -> Self { - Network::Testnet - } + fn default() -> Self { + Network::Testnet + } } impl From for bitcoin::Network { - fn from(network: Network) -> Self { - match network { - Network::Signet => bitcoin::Network::Signet, - Network::Testnet => bitcoin::Network::Testnet, - Network::Regtest => bitcoin::Network::Regtest, - Network::Bitcoin => bitcoin::Network::Bitcoin, - } - } + fn from(network: Network) -> Self { + match network { + Network::Signet => bitcoin::Network::Signet, + Network::Testnet => bitcoin::Network::Testnet, + Network::Regtest => bitcoin::Network::Regtest, + Network::Bitcoin => bitcoin::Network::Bitcoin, + } + } } From 6c0a89bf17c677a7cb24500a590885c9cdf68254 Mon Sep 17 00:00:00 2001 From: BitcoinZavior Date: Sun, 3 Sep 2023 20:51:30 -0400 Subject: [PATCH 28/30] Added comments considering autogen docs --- src/receive.rs | 567 ++++++++++++++++++++++++--------------------- src/send.rs | 188 +++++++-------- src/transaction.rs | 9 +- 3 files changed, 402 insertions(+), 362 deletions(-) diff --git a/src/receive.rs b/src/receive.rs index d724f5a..d24a582 100644 --- a/src/receive.rs +++ b/src/receive.rs @@ -1,326 +1,363 @@ use crate::{ - transaction::{ PartiallySignedTransaction, Transaction }, - Address, - OutPoint, - PdkError, - ScriptBuf, - TxOut, + transaction::{PartiallySignedTransaction, Transaction}, + Address, OutPoint, PdkError, ScriptBuf, TxOut, }; use payjoin::receive::{ - MaybeInputsOwned as PdkMaybeInputsOwned, - MaybeInputsSeen as PdkMaybeInputsSeen, - MaybeMixedInputScripts as PdkMaybeMixedInputScripts, - OutputsUnknown as PdkOutputsUnknown, - PayjoinProposal as PdkPayjoinProposal, - RequestError, - UncheckedProposal as PdkUncheckedProposal, + MaybeInputsOwned as PdkMaybeInputsOwned, MaybeInputsSeen as PdkMaybeInputsSeen, + MaybeMixedInputScripts as PdkMaybeMixedInputScripts, OutputsUnknown as PdkOutputsUnknown, + PayjoinProposal as PdkPayjoinProposal, UncheckedProposal as PdkUncheckedProposal, +}; +use std::{ + collections::HashMap, + sync::{Arc, Mutex, MutexGuard}, }; -use std::{ collections::HashMap, sync::{ Arc, Mutex, MutexGuard } }; pub trait CanBroadcast { - fn test_mempool_accept(&self, tx_hex: Vec) -> Result; + fn test_mempool_accept(&self, tx_hex: Vec) -> Result; } pub struct Headers(pub HashMap); impl Headers { - pub fn from_vec(body: Vec) -> Headers { - let mut h = HashMap::new(); - h.insert("content-type".to_string(), "text/plain".to_string()); - h.insert("content-length".to_string(), body.len().to_string()); - Headers(h) - } + pub fn from_vec(body: Vec) -> Headers { + let mut h = HashMap::new(); + h.insert("content-type".to_string(), "text/plain".to_string()); + h.insert("content-length".to_string(), body.len().to_string()); + Headers(h) + } } impl payjoin::receive::Headers for Headers { - fn get_header(&self, key: &str) -> Option<&str> { - self.0.get(key).map(|e| e.as_str()) - } + fn get_header(&self, key: &str) -> Option<&str> { + self.0.get(key).map(|e| e.as_str()) + } } +/// The sender’s original PSBT and optional parameters +/// +/// This type is used to proces the request. It is returned by UncheckedProposal::from_request(). +/// +/// If you are implementing an interactive payment processor, you should get extract the original transaction with get_transaction_to_schedule_broadcast() and schedule, followed by checking that the transaction can be broadcast with check_can_broadcast. Otherwise it is safe to call assume_interactive_receive to proceed with validation. pub struct UncheckedProposal { - pub internal: PdkUncheckedProposal, + pub internal: PdkUncheckedProposal, } impl UncheckedProposal { - pub fn from_request( - //TODO; Find which type that implement Read trait is an appropriate option - body: impl std::io::Read, - query: String, - headers: Headers - ) -> Result { - let res = PdkUncheckedProposal::from_request(body, query.as_str(), headers)?; - Ok(UncheckedProposal { internal: res }) - } + pub fn from_request( + //TODO; Find which type that implement Read trait is an appropriate option + body: impl std::io::Read, + query: String, + headers: Headers, + ) -> Result { + match PdkUncheckedProposal::from_request(body, query.as_str(), headers) { + Ok(e) => Ok(UncheckedProposal { internal: e }), + Err(e) => anyhow::bail!("{e}"), + } + } + + /// The Sender’s Original PSBT + pub fn get_transaction_to_schedule_broadcast(&self) -> Transaction { + let res = self.internal.get_transaction_to_schedule_broadcast(); + Transaction { internal: res } + } - pub fn get_transaction_to_schedule_broadcast(&self) -> Transaction { - let res = self.internal.get_transaction_to_schedule_broadcast(); - Transaction { internal: res } - } - pub fn check_can_broadcast( - self, - can_broadcast: Box - ) -> Result { - let res = self.internal.check_can_broadcast(|tx| { - let raw_tx = hex::encode(bitcoin::consensus::encode::serialize(&tx)); - let mempool_results = can_broadcast.test_mempool_accept(vec![raw_tx]); - match mempool_results { - Ok(e) => Ok(e), - Err(e) => Err(PdkError::Server(e.into())), - } - })?; - Ok(MaybeInputsOwned { internal: res }) - } - pub fn assume_interactive_receiver(self) -> MaybeInputsOwned { - MaybeInputsOwned { internal: self.internal.assume_interactive_receiver() } - } + /// Call after checking that the Original PSBT can be broadcast. + /// + /// Receiver MUST check that the Original PSBT from the sender can be broadcast, i.e. testmempoolaccept bitcoind rpc returns { “allowed”: true,.. } for get_transaction_to_check_broadcast() before calling this method. + /// + /// Do this check if you generate bitcoin uri to receive Payjoin on sender request without manual human approval, like a payment processor. Such so called “non-interactive” receivers are otherwise vulnerable to probing attacks. If a sender can make requests at will, they can learn which bitcoin the receiver owns at no cost. Broadcasting the Original PSBT after some time in the failure case makes incurs sender cost and prevents probing. + /// + /// Call this after checking downstream. + pub fn check_can_broadcast( + self, can_broadcast: Box, + ) -> Result { + let res = self.internal.check_can_broadcast(|tx| { + let raw_tx = hex::encode(payjoin::bitcoin::consensus::encode::serialize(&tx)); + let mempool_results = can_broadcast.test_mempool_accept(vec![raw_tx]); + match mempool_results { + Ok(e) => Ok(e), + Err(e) => Err(PdkError::Server(e.into())), + } + }); + match res { + Ok(e) => Ok(MaybeInputsOwned { internal: e }), + Err(e) => anyhow::bail!("{e}"), + } + } + + /// Call this method if the only way to initiate a Payjoin with this receiver requires manual intervention, as in most consumer wallets. + /// + /// So-called “non-interactive” receivers, like payment processors, that allow arbitrary requests are otherwise vulnerable to probing attacks. Those receivers call get_transaction_to_check_broadcast() and attest_tested_and_scheduled_broadcast() after making those checks downstream. + pub fn assume_interactive_receiver(self) -> MaybeInputsOwned { + MaybeInputsOwned { internal: self.internal.assume_interactive_receiver() } + } } -///Typestate to validate that the Original PSBT has no receiver-owned inputs. +/// Typestate to validate that the Original PSBT has no receiver-owned inputs. -///Call check_no_receiver_owned_inputs() to proceed. +/// Call check_no_receiver_owned_inputs() to proceed. pub struct MaybeInputsOwned { - pub internal: PdkMaybeInputsOwned, + pub internal: PdkMaybeInputsOwned, } pub trait IsScriptOwned { - fn is_owned(&self, script: ScriptBuf) -> Result; + fn is_owned(&self, script: ScriptBuf) -> Result; } impl MaybeInputsOwned { - ///Check that the Original PSBT has no receiver-owned inputs. Return original-psbt-rejected error or otherwise refuse to sign undesirable inputs. - - ///An attacker could try to spend receiver’s own inputs. This check prevents that. - pub fn check_inputs_not_owned( - self, - is_owned: impl IsScriptOwned - ) -> Result { - match - self.internal.check_inputs_not_owned(|input| { - is_owned.is_owned(ScriptBuf { inner: input.to_owned() }) - }) - { - Ok(e) => Ok(MaybeMixedInputScripts { internal: e }), - Err(e) => Err(e), - } - } + /// Check that the Original PSBT has no receiver-owned inputs. Return original-psbt-rejected error or otherwise refuse to sign undesirable inputs. + /// + /// An attacker could try to spend receiver’s own inputs. This check prevents that. + pub fn check_inputs_not_owned( + self, is_owned: impl IsScriptOwned, + ) -> Result { + match self.internal.check_inputs_not_owned(|input| { + let res = is_owned.is_owned(ScriptBuf { inner: input.to_owned() }); + match res { + Ok(e) => Ok(e), + Err(e) => Err(PdkError::Server(e.into())), + } + }) { + Ok(e) => Ok(MaybeMixedInputScripts { internal: e }), + Err(e) => anyhow::bail!("{e}"), + } + } } -///Typestate to validate that the Original PSBT has no inputs that have been seen before. -///Call check_no_inputs_seen to proceed. + +/// Typestate to validate that the Original PSBT has no inputs that have been seen before. +/// +/// Call check_no_inputs_seen to proceed. pub struct MaybeMixedInputScripts { - pub internal: PdkMaybeMixedInputScripts, + pub internal: PdkMaybeMixedInputScripts, } impl MaybeMixedInputScripts { - ///Verify the original transaction did not have mixed input types Call this after checking downstream. - ///Note: mixed spends do not necessarily indicate distinct wallet fingerprints. This check is intended to prevent some types of wallet fingerprinting. - pub fn check_no_mixed_input_scripts( - self - ) -> Result { - match self.internal.check_no_mixed_input_scripts() { - Ok(e) => Ok(MaybeInputsSeen { internal: e }), - Err(e) => Err(e), - } - } + /// Verify the original transaction did not have mixed input types Call this after checking downstream. + /// + /// Note: mixed spends do not necessarily indicate distinct wallet fingerprints. This check is intended to prevent some types of wallet fingerprinting. + pub fn check_no_mixed_input_scripts(self) -> Result { + match self.internal.check_no_mixed_input_scripts() { + Ok(e) => Ok(MaybeInputsSeen { internal: e }), + Err(e) => anyhow::bail!("{e}"), + } + } } -///Typestate to validate that the Original PSBT has no inputs that have been seen before. - pub trait IsOutoutKnown { - fn is_known(&self, outpoint: OutPoint) -> Result; + fn is_known(&self, outpoint: OutPoint) -> Result; } -///Call check_no_inputs_seen to proceed. + +/// Typestate to validate that the Original PSBT has no inputs that have been seen before. +/// +/// Call check_no_inputs_seen to proceed. pub struct MaybeInputsSeen { - pub internal: PdkMaybeInputsSeen, + pub internal: PdkMaybeInputsSeen, } impl MaybeInputsSeen { - pub fn check_no_inputs_seen_before( - self, - is_known: impl IsOutoutKnown - ) -> Result { - match - self.internal.check_no_inputs_seen_before(|outpoint| - is_known.is_known(outpoint.to_owned().into()) - ) - { - Ok(e) => Ok(OutputsUnknown { internal: e }), - Err(e) => Err(e), - } - } + /// Make sure that the original transaction inputs have never been seen before. This prevents probing attacks. This prevents reentrant Payjoin, where a sender proposes a Payjoin PSBT as a new Original PSBT for a new Payjoin. + pub fn check_no_inputs_seen_before( + self, is_known: impl IsOutoutKnown, + ) -> Result { + match self.internal.check_no_inputs_seen_before(|outpoint| { + let res = is_known.is_known(outpoint.to_owned().into()); + match res { + Ok(e) => Ok(e), + Err(e) => Err(PdkError::Server(e.into())), + } + }) { + Ok(e) => Ok(OutputsUnknown { internal: e }), + Err(e) => anyhow::bail!("{e}"), + } + } } +/// The receiver has not yet identified which outputs belong to the receiver. +/// +/// Only accept PSBTs that send us money. Identify those outputs with identify_receiver_outputs() to proceed -///The receiver has not yet identified which outputs belong to the receiver. -///Only accept PSBTs that send us money. Identify those outputs with identify_receiver_outputs() to proceed pub struct OutputsUnknown { - pub internal: PdkOutputsUnknown, + pub internal: PdkOutputsUnknown, } impl OutputsUnknown { - ///Find which outputs belong to the receiver - pub fn identify_receiver_outputs( - self, - is_receiver_output: impl IsScriptOwned - ) -> Result { - match - self.internal.identify_receiver_outputs(|output_script| { - is_receiver_output.is_owned(ScriptBuf { inner: output_script.to_owned() }) - }) - { - Ok(e) => Ok(PayjoinProposal { internal: Mutex::new(Some(e)) }), - Err(e) => Err(e), - } - } + /// Find which outputs belong to the receiver + pub fn identify_receiver_outputs( + self, is_receiver_output: impl IsScriptOwned, + ) -> Result { + match self.internal.identify_receiver_outputs(|output_script| { + let res = is_receiver_output.is_owned(ScriptBuf { inner: output_script.to_owned() }); + match res { + Ok(e) => Ok(e), + Err(e) => Err(PdkError::Server(e.into())), + } + }) { + Ok(e) => Ok(PayjoinProposal { internal: Mutex::new(Some(e)) }), + Err(e) => anyhow::bail!("{e}"), + } + } } -///A mutable checked proposal that the receiver may contribute inputs to to make a payjoin. +/// A mutable checked proposal that the receiver may contribute inputs to to make a payjoin. pub struct PayjoinProposal { - pub internal: Mutex>, + pub internal: Mutex>, } impl PayjoinProposal { - pub(crate) fn get_proposal(&self) -> MutexGuard> { - self.internal.lock().expect("PayjoinProposal") - } - pub fn is_output_substitution_disabled(&self) -> bool { - if self.get_proposal().as_ref().is_none() { - //TODO CREATE CUSTOM ERROR - panic!("PayjoinProposal not initalized"); - } - self.get_proposal().as_mut().unwrap().is_output_substitution_disabled() - } - pub fn contribute_witness_input(&self, txout: TxOut, outpoint: OutPoint) -> &Self { - if self.get_proposal().as_ref().is_none() { - panic!("PayjoinProposal not initalized"); - } - self.get_proposal() - .as_mut() - .unwrap() - .contribute_witness_input(txout.into(), outpoint.into()); - self - } - pub fn contribute_non_witness_input(&self, tx: Transaction, outpoint: OutPoint) { - if self.get_proposal().as_ref().is_none() { - panic!("PayjoinProposal not initalized"); - } - self.get_proposal() - .as_mut() - .unwrap() - .contribute_non_witness_input(tx.internal, outpoint.into()) - } - pub fn substitute_output_address(&self, substitute_address: Address) { - if self.get_proposal().as_ref().is_none() { - panic!("PayjoinProposal not initalized"); - } - self.get_proposal().as_mut().unwrap().substitute_output_address(substitute_address.into()) - } - ///Apply additional fee contribution now that the receiver has contributed input this is kind of a “build_proposal” step before we sign and finalize and extract - ///WARNING: DO NOT ALTER INPUTS OR OUTPUTS AFTER THIS STEP - pub fn apply_fee( - &self, - min_feerate_sat_per_vb: Option - ) -> Result { - if self.get_proposal().as_ref().is_none() { - panic!("PayjoinProposal not initalized"); - } + pub(crate) fn get_proposal(&self) -> MutexGuard> { + self.internal.lock().expect("PayjoinProposal") + } + + pub fn is_output_substitution_disabled(&self) -> bool { + if self.get_proposal().as_ref().is_none() { + //TODO CREATE CUSTOM ERROR + panic!("PayjoinProposal not initalized"); + } + self.get_proposal().as_mut().unwrap().is_output_substitution_disabled() + } + + pub fn contribute_witness_input(&self, txout: TxOut, outpoint: OutPoint) -> &Self { + if self.get_proposal().as_ref().is_none() { + panic!("PayjoinProposal not initalized"); + } + self.get_proposal() + .as_mut() + .unwrap() + .contribute_witness_input(txout.into(), outpoint.into()); + self + } + + pub fn contribute_non_witness_input(&self, tx: Transaction, outpoint: OutPoint) { + if self.get_proposal().as_ref().is_none() { + panic!("PayjoinProposal not initalized"); + } + self.get_proposal() + .as_mut() + .unwrap() + .contribute_non_witness_input(tx.internal, outpoint.into()) + } + + /// Just replace an output address with + pub fn substitute_output_address( + &self, substitute_address: Address, + ) { + if self.get_proposal().as_ref().is_none() { + panic!("PayjoinProposal not initalized"); + } + self.get_proposal().as_mut().unwrap().substitute_output_address(substitute_address.into()) + } + + /// Apply additional fee contribution now that the receiver has contributed input this is kind of a “build_proposal” step before we sign and finalize and extract + /// + /// WARNING: DO NOT ALTER INPUTS OR OUTPUTS AFTER THIS STEP + pub fn apply_fee( + &self, min_feerate_sat_per_vb: Option, + ) -> Result { + if self.get_proposal().as_ref().is_none() { + panic!("PayjoinProposal not initalized"); + } + + match self.get_proposal().as_mut().unwrap().apply_fee(min_feerate_sat_per_vb) { + Ok(e) => Ok(PartiallySignedTransaction { internal: Arc::new(e.to_owned()) }), + Err(e) => anyhow::bail!("{e}"), + } + } + + /// Return a Payjoin Proposal PSBT that the sender will find acceptable. + /// + /// This attempts to calculate any network fee owed by the receiver, subtract it from their output, and return a PSBT that can produce a consensus-valid transaction that the sender will accept. + /// + /// wallet_process_psbt should sign and finalize receiver inputs + pub fn prepare_psbt( + &self, processed_psbt: PartiallySignedTransaction, + ) -> Result { + let mut data_guard = self.get_proposal(); + let taken_proposal = std::mem::replace(&mut *data_guard, None); + match taken_proposal.unwrap().prepare_psbt(processed_psbt.internal.as_ref().to_owned()) { + Ok(e) => Ok(PartiallySignedTransaction { internal: Arc::new(e.to_owned()) }), + Err(e) => anyhow::bail!("{e}"), + } + } - match self.get_proposal().as_mut().unwrap().apply_fee(min_feerate_sat_per_vb) { - Ok(e) => Ok(PartiallySignedTransaction { internal: Arc::new(e.to_owned()) }), - Err(e) => Err(e), - } - } - ///Return a Payjoin Proposal PSBT that the sender will find acceptable. - ///This attempts to calculate any network fee owed by the receiver, subtract it from their output, and return a PSBT that can produce a consensus-valid transaction that the sender will accept. - ///wallet_process_psbt should sign and finalize receiver inputs - pub fn prepare_psbt( - &self, - processed_psbt: PartiallySignedTransaction - ) -> Result { - let mut data_guard = self.get_proposal(); - let taken_proposal = std::mem::replace(&mut *data_guard, None); - match taken_proposal.unwrap().prepare_psbt(processed_psbt.internal.as_ref().to_owned()) { - Ok(e) => Ok(PartiallySignedTransaction { internal: Arc::new(e.to_owned()) }), - Err(e) => Err(e), - } - } - ///Select receiver input such that the payjoin avoids surveillance. Return the input chosen that has been applied to the Proposal. - ///Proper coin selection allows payjoin to resemble ordinary transactions. To ensure the resemblence, a number of heuristics must be avoided. - ///UIH “Unecessary input heuristic” is one class of them to avoid. We define UIH1 and UIH2 according to the BlockSci practice BlockSci UIH1 and UIH2: - pub fn try_preserving_privacy( - &self, - candidate_inputs: HashMap - ) -> Result { - let mut _candidate_inputs: HashMap = HashMap::new(); - for (key, value) in candidate_inputs.iter() { - _candidate_inputs.insert( - bitcoin::Amount::from_sat(key.to_owned()), - value.to_owned().into() - ); - } - let data_guard = self.get_proposal(); - match data_guard.as_ref().unwrap().try_preserving_privacy(_candidate_inputs) { - Ok(e) => Ok(OutPoint { txid: e.txid.to_string(), vout: e.vout }), - Err(e) => Err(e), - } - } - // TODO - pub fn utxos_to_be_locked(&self) + /// Select receiver input such that the payjoin avoids surveillance. Return the input chosen that has been applied to the Proposal. + /// + /// Proper coin selection allows payjoin to resemble ordinary transactions. To ensure the resemblence, a number of heuristics must be avoided. + /// + /// UIH “Unecessary input heuristic” is one class of them to avoid. We define UIH1 and UIH2 according to the BlockSci practice BlockSci UIH1 and UIH2: + pub fn try_preserving_privacy( + &self, candidate_inputs: HashMap, + ) -> Result { + let mut _candidate_inputs: HashMap = + HashMap::new(); + for (key, value) in candidate_inputs.iter() { + _candidate_inputs.insert( + payjoin::bitcoin::Amount::from_sat(key.to_owned()), + value.to_owned().into(), + ); + } + let data_guard = self.get_proposal(); + match data_guard.as_ref().unwrap().try_preserving_privacy(_candidate_inputs) { + Ok(e) => Ok(OutPoint { txid: e.txid.to_string(), vout: e.vout }), + Err(_) => anyhow::bail!("Selection Error"), + } + } + // TODO - pub fn utxos_to_be_locked(&self) } #[cfg(test)] mod test { - use crate::Network; + use crate::Network; - use super::*; + use super::*; - fn get_proposal_from_test_vector() -> Result { - // OriginalPSBT Test Vector from BIP - // | InputScriptType | Orginal PSBT Fee rate | maxadditionalfeecontribution | additionalfeeoutputindex| - // |-----------------|-----------------------|------------------------------|-------------------------| - // | P2SH-P2WPKH | 2 sat/vbyte | 0.00000182 | 0 | - let original_psbt = + fn get_proposal_from_test_vector() -> Result { + // OriginalPSBT Test Vector from BIP + // | InputScriptType | Orginal PSBT Fee rate | maxadditionalfeecontribution | additionalfeeoutputindex| + // |-----------------|-----------------------|------------------------------|-------------------------| + // | P2SH-P2WPKH | 2 sat/vbyte | 0.00000182 | 0 | + let original_psbt = "cHNidP8BAHMCAAAAAY8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////AtyVuAUAAAAAF6kUHehJ8GnSdBUOOv6ujXLrWmsJRDCHgIQeAAAAAAAXqRR3QJbbz0hnQ8IvQ0fptGn+votneofTAAAAAAEBIKgb1wUAAAAAF6kU3k4ekGHKWRNbA1rV5tR5kEVDVNCHAQcXFgAUx4pFclNVgo1WWAdN1SYNX8tphTABCGsCRzBEAiB8Q+A6dep+Rz92vhy26lT0AjZn4PRLi8Bf9qoB/CMk0wIgP/Rj2PWZ3gEjUkTlhDRNAQ0gXwTO7t9n+V14pZ6oljUBIQMVmsAaoNWHVMS02LfTSe0e388LNitPa1UQZyOihY+FFgABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUAAA="; - let body = original_psbt.as_bytes(); - let headers = Headers::from_vec(body.to_vec()); - UncheckedProposal::from_request( - body, - "?maxadditionalfeecontribution=182?additionalfeeoutputindex=0".to_string(), - headers - ) - } + let body = original_psbt.as_bytes(); + let headers = Headers::from_vec(body.to_vec()); + UncheckedProposal::from_request( + body, + "?maxadditionalfeecontribution=182?additionalfeeoutputindex=0".to_string(), + headers, + ) + } - #[test] - fn can_get_proposal_from_request() { - let proposal = get_proposal_from_test_vector(); - assert!(proposal.is_ok(), "OriginalPSBT should be a valid request"); - } - struct MockScriptOwned {} - struct MockOutputOwned {} - impl IsOutoutKnown for MockOutputOwned { - fn is_known(&self, _: OutPoint) -> Result { - Ok(false) - } - } - impl IsScriptOwned for MockScriptOwned { - fn is_owned(&self, script: ScriptBuf) -> Result { - { - let network = Network::Bitcoin; - Ok( - Address::from_script(script, network.clone()).unwrap() == - Address::from_str("3CZZi7aWFugaCdUCS15dgrUUViupmB8bVM").unwrap() - ) - } - // Ok(false) - } - } + #[test] + fn can_get_proposal_from_request() { + let proposal = get_proposal_from_test_vector(); + assert!(proposal.is_ok(), "OriginalPSBT should be a valid request"); + } + struct MockScriptOwned {} + struct MockOutputOwned {} + impl IsOutoutKnown for MockOutputOwned { + fn is_known(&self, _: OutPoint) -> Result { + Ok(false) + } + } + impl IsScriptOwned for MockScriptOwned { + fn is_owned(&self, script: ScriptBuf) -> Result { + { + let network = Network::Bitcoin; + Ok(Address::from_script(script, network.clone()).unwrap() + == Address::from_str("3CZZi7aWFugaCdUCS15dgrUUViupmB8bVM") + .unwrap() + .assume_checked() + .unwrap()) + } + } + } - #[test] - fn unchecked_proposal_unlocks_after_checks() { - let proposal = get_proposal_from_test_vector().unwrap(); - let payjoin = proposal - .assume_interactive_receiver() - .check_inputs_not_owned(MockScriptOwned {}) - .expect("No inputs should be owned") - .check_no_mixed_input_scripts() - .expect("No mixed input scripts") - .check_no_inputs_seen_before(MockOutputOwned {}) - .expect("No inputs should be seen before") - .identify_receiver_outputs(MockScriptOwned {}) - .expect("Receiver output should be identified"); - let payjoin = payjoin.apply_fee(None); + #[test] + fn unchecked_proposal_unlocks_after_checks() { + let proposal = get_proposal_from_test_vector().unwrap(); + let payjoin = proposal + .assume_interactive_receiver() + .check_inputs_not_owned(MockScriptOwned {}) + .expect("No inputs should be owned") + .check_no_mixed_input_scripts() + .expect("No mixed input scripts") + .check_no_inputs_seen_before(MockOutputOwned {}) + .expect("No inputs should be seen before") + .identify_receiver_outputs(MockScriptOwned {}) + .expect("Receiver output should be identified"); + let payjoin = payjoin.apply_fee(None); - assert!(payjoin.is_ok(), "Payjoin should be a valid PSBT"); - } + assert!(payjoin.is_ok(), "Payjoin should be a valid PSBT"); + } } diff --git a/src/send.rs b/src/send.rs index 4000d6e..7e425a4 100644 --- a/src/send.rs +++ b/src/send.rs @@ -1,128 +1,130 @@ -use crate::{ transaction::PartiallySignedTransaction, uri::Url }; +use crate::{transaction::PartiallySignedTransaction, uri::Url}; use payjoin::send::ValidationError; -pub use payjoin::send::{ Configuration as PdkConfiguration, Context as PdkContext }; -use std::sync::{ Arc, Mutex, MutexGuard }; +pub use payjoin::send::{Configuration as PdkConfiguration, Context as PdkContext}; +use std::sync::{Arc, Mutex, MutexGuard}; ///Builder for sender-side payjoin parameters +/// ///These parameters define how client wants to handle Payjoin. pub struct Configuration { - pub internal: Mutex>, + pub internal: Mutex>, } impl Configuration { - pub(crate) fn get_configuration_mutex(&self) -> MutexGuard> { - self.internal.lock().expect("PdkConfiguration") - } - ///Offer the receiver contribution to pay for his input. - ///These parameters will allow the receiver to take max_fee_contribution from given change output to pay for additional inputs. The recommended fee is size_of_one_input * fee_rate. - ///change_index specifies which output can be used to pay fee. If None is provided, then the output is auto-detected unless the supplied transaction has more than two outputs. - pub fn with_fee_contribution(max_fee_contribution: u64, change_index: Option) -> Self { - let configuration = PdkConfiguration::with_fee_contribution( - payjoin::bitcoin::Amount::from_sat(max_fee_contribution), - change_index - ); - Self { internal: Mutex::new(Some(configuration)) } - } - ///Perform Payjoin without incentivizing the payee to cooperate. - ///While it’s generally better to offer some contribution some users may wish not to. This function disables contribution. - pub fn non_incentivizing() -> Self { - Self { internal: Mutex::new(Some(PdkConfiguration::non_incentivizing())) } - } - ///Disable output substitution even if the receiver didn’t. - ///This forbids receiver switching output or decreasing amount. It is generally not recommended to set this as it may prevent the receiver from doing advanced operations such as opening LN channels and it also guarantees the receiver will not reward the sender with a discount. - pub fn always_disable_output_substitution(self, disable: bool) -> Self { - { - let mut data_guard = self.get_configuration_mutex(); - // Temporarily take out the Configuration and replace with a dummy value - let _config = std::mem::replace(&mut *data_guard, None); - *data_guard = Some(_config.unwrap().always_disable_output_substitution(disable)); - } - self - } - ///Decrease fee contribution instead of erroring. - ///If this option is set and a transaction with change amount lower than fee contribution is provided then instead of returning error the fee contribution will be just lowered to match the change amount. - pub fn clamp_fee_contribution(self, clamp: bool) -> Self { - { - let mut data_guard = self.get_configuration_mutex(); - // Temporarily take out the Configuration and replace with a dummy value - let _config = std::mem::replace(&mut *data_guard, None); - *data_guard = Some(_config.unwrap().clamp_fee_contribution(clamp)); - } - self - } - ///Sets minimum fee rate required by the sender. - pub fn min_fee_rate_sat_per_vb(self, fee_rate: u64) -> Self { - { - let mut data_guard = self.get_configuration_mutex(); - // Temporarily take out the Configuration and replace with a dummy value - let _config = std::mem::replace(&mut *data_guard, None); - *data_guard = Some(_config.unwrap().min_fee_rate_sat_per_vb(fee_rate)); - } - self - } + pub(crate) fn get_configuration_mutex(&self) -> MutexGuard> { + self.internal.lock().expect("PdkConfiguration") + } + ///Offer the receiver contribution to pay for his input. + /// + ///These parameters will allow the receiver to take max_fee_contribution from given change output to pay for additional inputs. The recommended fee is size_of_one_input * fee_rate. + /// + ///change_index specifies which output can be used to pay fee. If None is provided, then the output is auto-detected unless the supplied transaction has more than two outputs. + pub fn with_fee_contribution(max_fee_contribution: u64, change_index: Option) -> Self { + let configuration = PdkConfiguration::with_fee_contribution( + payjoin::bitcoin::Amount::from_sat(max_fee_contribution), + change_index, + ); + Self { internal: Mutex::new(Some(configuration)) } + } + ///Perform Payjoin without incentivizing the payee to cooperate. + /// + ///While it’s generally better to offer some contribution some users may wish not to. This function disables contribution. + pub fn non_incentivizing() -> Self { + Self { internal: Mutex::new(Some(PdkConfiguration::non_incentivizing())) } + } + ///Disable output substitution even if the receiver didn’t. + /// + ///This forbids receiver switching output or decreasing amount. It is generally not recommended to set this as it may prevent the receiver from doing advanced operations such as opening LN channels and it also guarantees the receiver will not reward the sender with a discount. + pub fn always_disable_output_substitution(self, disable: bool) -> Self { + { + let mut data_guard = self.get_configuration_mutex(); + let _config = std::mem::replace(&mut *data_guard, None); + *data_guard = Some(_config.unwrap().always_disable_output_substitution(disable)); + } + self + } + ///Decrease fee contribution instead of erroring. + /// + ///If this option is set and a transaction with change amount lower than fee contribution is provided then instead of returning error the fee contribution will be just lowered to match the change amount. + pub fn clamp_fee_contribution(self, clamp: bool) -> Self { + { + let mut data_guard = self.get_configuration_mutex(); + let _config = std::mem::replace(&mut *data_guard, None); + *data_guard = Some(_config.unwrap().clamp_fee_contribution(clamp)); + } + self + } + ///Sets minimum fee rate required by the sender. + pub fn min_fee_rate_sat_per_vb(self, fee_rate: u64) -> Self { + { + let mut data_guard = self.get_configuration_mutex(); + let _config = std::mem::replace(&mut *data_guard, None); + *data_guard = Some(_config.unwrap().min_fee_rate_sat_per_vb(fee_rate)); + } + self + } } ///Data required for validation of response. ///This type is used to process the response. It is returned from PjUriExt::create_pj_request() method and you only need to call .process_response() on it to continue BIP78 flow. pub struct Context { - pub internal: PdkContext, + pub internal: PdkContext, } impl Context { - ///Decodes and validates the response. - - ///Call this method with response from receiver to continue BIP78 flow. If the response is valid you will get appropriate PSBT that you should sign and broadcast. - pub fn process_response( - self, - response: &mut impl std::io::Read - ) -> Result { - match self.internal.process_response(response) { - Ok(e) => Ok(PartiallySignedTransaction { internal: Arc::new(e.to_owned()) }), - Err(e) => Err(e), - } - } + ///Decodes and validates the response. + + ///Call this method with response from receiver to continue BIP78 flow. If the response is valid you will get appropriate PSBT that you should sign and broadcast. + pub fn process_response( + self, response: &mut impl std::io::Read, + ) -> Result { + match self.internal.process_response(response) { + Ok(e) => Ok(PartiallySignedTransaction { internal: Arc::new(e.to_owned()) }), + Err(e) => Err(e), + } + } } ///Represents data that needs to be transmitted to the receiver. ///You need to send this request over HTTP(S) to the receiver. pub struct Request { - ///URL to send the request to. - - ///This is full URL with scheme etc - you can pass it right to reqwest or a similar library. - pub url: Url, - ///Bytes to be sent to the receiver. - - ///This is properly encoded PSBT, already in base64. You only need to make sure Content-Type is text/plain and Content-Length is body.len() (most libraries do the latter automatically). - pub body: Vec, + ///URL to send the request to. + /// + ///This is full URL with scheme etc - you can pass it right to reqwest or a similar library. + pub url: Url, + ///Bytes to be sent to the receiver. + /// + ///This is properly encoded PSBT, already in base64. You only need to make sure Content-Type is text/plain and Content-Length is body.len() (most libraries do the latter automatically). + pub body: Vec, } #[cfg(test)] mod tests { - #[test] - fn official_vectors() { - use std::str::FromStr; + use std::str::FromStr; - use bitcoin::psbt::Psbt; + use bitcoincore_rpc::bitcoin::psbt::PartiallySignedTransaction; - let original_psbt = + #[test] + fn official_vectors() { + let original_psbt = "cHNidP8BAHMCAAAAAY8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////AtyVuAUAAAAAF6kUHehJ8GnSdBUOOv6ujXLrWmsJRDCHgIQeAAAAAAAXqRR3QJbbz0hnQ8IvQ0fptGn+votneofTAAAAAAEBIKgb1wUAAAAAF6kU3k4ekGHKWRNbA1rV5tR5kEVDVNCHAQcXFgAUx4pFclNVgo1WWAdN1SYNX8tphTABCGsCRzBEAiB8Q+A6dep+Rz92vhy26lT0AjZn4PRLi8Bf9qoB/CMk0wIgP/Rj2PWZ3gEjUkTlhDRNAQ0gXwTO7t9n+V14pZ6oljUBIQMVmsAaoNWHVMS02LfTSe0e388LNitPa1UQZyOihY+FFgABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUAAA="; - let proposal = + let proposal = "cHNidP8BAJwCAAAAAo8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////jye60aAl3JgZdaIERvjkeh72VYZuTGH/ps2I4l0IO4MBAAAAAP7///8CJpW4BQAAAAAXqRQd6EnwadJ0FQ46/q6NcutaawlEMIcACT0AAAAAABepFHdAltvPSGdDwi9DR+m0af6+i2d6h9MAAAAAAQEgqBvXBQAAAAAXqRTeTh6QYcpZE1sDWtXm1HmQRUNU0IcBBBYAFMeKRXJTVYKNVlgHTdUmDV/LaYUwIgYDFZrAGqDVh1TEtNi300ntHt/PCzYrT2tVEGcjooWPhRYYSFzWUDEAAIABAACAAAAAgAEAAAAAAAAAAAEBIICEHgAAAAAAF6kUyPLL+cphRyyI5GTUazV0hF2R2NWHAQcXFgAUX4BmVeWSTJIEwtUb5TlPS/ntohABCGsCRzBEAiBnu3tA3yWlT0WBClsXXS9j69Bt+waCs9JcjWtNjtv7VgIge2VYAaBeLPDB6HGFlpqOENXMldsJezF9Gs5amvDQRDQBIQJl1jz1tBt8hNx2owTm+4Du4isx0pmdKNMNIjjaMHFfrQABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUIgICygvBWB5prpfx61y1HDAwo37kYP3YRJBvAjtunBAur3wYSFzWUDEAAIABAACAAAAAgAEAAAABAAAAAAA="; - let original_psbt = Psbt::from_str(original_psbt).unwrap(); - eprintln!("original: {:#?}", original_psbt); - let mut proposal = Psbt::from_str(proposal).unwrap(); - eprintln!("proposal: {:#?}", proposal); - for mut output in proposal.clone().outputs { - output.bip32_derivation.clear(); - } - for mut input in proposal.clone().inputs { - input.bip32_derivation.clear(); - } - proposal.inputs[0].witness_utxo = None; - } + let original_psbt = PartiallySignedTransaction::from_str(original_psbt).unwrap(); + eprintln!("original: {:#?}", original_psbt); + let mut proposal = PartiallySignedTransaction::from_str(proposal).unwrap(); + eprintln!("proposal: {:#?}", proposal); + for mut output in proposal.clone().outputs { + output.bip32_derivation.clear(); + } + for mut input in proposal.clone().inputs { + input.bip32_derivation.clear(); + } + proposal.inputs[0].witness_utxo = None; + } } diff --git a/src/transaction.rs b/src/transaction.rs index 6995a43..4f5d05e 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -1,15 +1,16 @@ -use bitcoin::blockdata::transaction::Transaction as BitcoinTransaction; +use payjoin::bitcoin::blockdata::transaction::Transaction as BitcoinTransaction; use payjoin::bitcoin::psbt::PartiallySignedTransaction as BitcoinPsbt; use std::{str::FromStr, sync::Arc}; -use crate::error::Error; +/// +/// Partially signed transaction, commonly referred to as a PSBT. #[derive(Debug, Clone)] pub struct PartiallySignedTransaction { pub internal: Arc, } impl PartiallySignedTransaction { - pub fn new(psbt_base64: String) -> Result { - let psbt: BitcoinPsbt = BitcoinPsbt::from_str(&psbt_base64)?; + pub fn new(psbt_base64: String) -> Result { + let psbt = BitcoinPsbt::from_str(&psbt_base64)?; Ok(PartiallySignedTransaction { internal: Arc::new(psbt) }) } pub fn serialize(&self) -> Vec { From 6fcf0ef50be36e8468f03cc6cf076b6f714f349f Mon Sep 17 00:00:00 2001 From: BitcoinZavior Date: Mon, 4 Sep 2023 23:11:22 -0400 Subject: [PATCH 29/30] Uri struct updated; tests added --- src/uri.rs | 147 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 94 insertions(+), 53 deletions(-) diff --git a/src/uri.rs b/src/uri.rs index 2ad2037..9d78ab3 100644 --- a/src/uri.rs +++ b/src/uri.rs @@ -8,70 +8,44 @@ use payjoin::{bitcoin, PjUriExt, UriExt}; use std::str::FromStr; #[derive(Clone)] -pub enum PayjoinUri { - Unchecked(payjoin::Uri<'static, NetworkUnchecked>), - Checked(payjoin::Uri<'static, NetworkChecked>), -} -#[derive(Clone)] -pub struct Uri { - internal: PayjoinUri, +pub struct Uri +where + T: bitcoin::address::NetworkValidation, +{ + pub internal: payjoin::Uri<'static, T>, } -impl Uri { +impl Uri { pub fn from_str(uri: String) -> Result { match payjoin::Uri::from_str(uri.as_str()) { - Ok(e) => Ok(Uri { internal: PayjoinUri::Unchecked(e) }), + Ok(e) => Ok(Uri { internal: e }), Err(e) => anyhow::bail!(e), } } - pub fn assume_checked(self) -> Self { - match self.internal { - PayjoinUri::Unchecked(e) => Self { internal: PayjoinUri::Checked(e.assume_checked()) }, - PayjoinUri::Checked(e) => Self { internal: PayjoinUri::Checked(e) }, - } + pub fn assume_checked(self) -> Uri { + Uri { internal: self.internal.assume_checked() } } - pub fn check_pj_supported(self) -> Result { - match self.internal { - PayjoinUri::Unchecked(_) => anyhow::bail!("Network Unchecked"), - PayjoinUri::Checked(e) => match e.check_pj_supported() { - Ok(e) => Ok(PrjUri { internal: e }), - Err(e) => anyhow::bail!(e), - }, - } + pub fn require_network(self, network: Network) -> Result, anyhow::Error> { + Ok(Uri { + internal: self.internal.require_network(network.into()).expect("Invalid Network"), + }) } - pub fn address(self) -> Address { - match self.internal { - PayjoinUri::Unchecked(e) => { - Address { internal: crate::BitcoinAddress::Unchecked(e.address) } - } - PayjoinUri::Checked(e) => { - Address { internal: crate::BitcoinAddress::Checked(e.address) } - } - } +} +impl Uri { + pub fn address(&self) -> Address { + Address { internal: self.internal.address.to_owned() } } - pub fn amount(self) -> Option { - match self.internal { - PayjoinUri::Unchecked(e) => match e.amount { - Some(a) => Some(a.to_sat()), - None => None, - }, - PayjoinUri::Checked(e) => match e.amount { - Some(a) => Some(a.to_sat()), - None => None, - }, - } + pub fn amount(&self) -> u64 { + self.internal.amount.unwrap().to_sat() } - pub fn require_network(self, network: Network) -> Result { - match self.internal { - PayjoinUri::Unchecked(e) => Ok(Uri { - internal: PayjoinUri::Checked( - e.require_network(network.into()).expect("Invalid Network"), - ), - }), - PayjoinUri::Checked(_) => anyhow::bail!("Network already checked"), + pub fn check_pj_supported(self) -> Result { + match self.internal.check_pj_supported() { + Ok(e) => Ok(PrjUri { internal: e }), + Err(e) => anyhow::bail!(e), } } } + #[derive(Debug, Clone)] pub struct PrjUri { pub internal: payjoin::PjUri<'static>, @@ -90,9 +64,8 @@ impl PrjUri { } } - pub fn address(self) -> String { - // Address { internal: crate::BitcoinAddress::Checked() } - self.internal.address.to_string() + pub fn address(self) -> Address { + Address { internal: self.internal.address } } pub fn amount(self) -> Option { self.internal.amount @@ -110,3 +83,71 @@ impl Url { } } } +#[cfg(test)] +mod tests { + use payjoin::Uri; + use std::convert::TryFrom; + + #[test] + fn test_short() { + assert!(Uri::try_from("").is_err()); + assert!(Uri::try_from("bitcoin").is_err()); + assert!(Uri::try_from("bitcoin:").is_err()); + } + + #[ignore] + #[test] + fn test_todo_url_encoded() { + let uri = "bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX?amount=1&pj=https://example.com?ciao"; + assert!(Uri::try_from(uri).is_err(), "pj url should be url encoded"); + } + + #[test] + fn test_valid_url() { + let uri = "bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX?amount=1&pj=this_is_NOT_a_validURL"; + assert!(Uri::try_from(uri).is_err(), "pj is not a valid url"); + } + + #[test] + fn test_missing_amount() { + let uri = + "bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX?pj=https://testnet.demo.btcpayserver.org/BTC/pj"; + assert!(Uri::try_from(uri).is_ok(), "missing amount should be ok"); + } + + #[test] + fn test_unencrypted() { + let uri = "bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX?amount=1&pj=http://example.com"; + assert!(Uri::try_from(uri).is_err(), "unencrypted connection"); + + let uri = "bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX?amount=1&pj=ftp://foo.onion"; + assert!(Uri::try_from(uri).is_err(), "unencrypted connection"); + } + + #[test] + fn test_valid_uris() { + let https = "https://example.com"; + let onion = "http://vjdpwgybvubne5hda6v4c5iaeeevhge6jvo3w2cl6eocbwwvwxp7b7qd.onion"; + + let base58 = "bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX"; + let bech32_upper = "BITCOIN:TB1Q6D3A2W975YNY0ASUVD9A67NER4NKS58FF0Q8G4"; + let bech32_lower = "bitcoin:tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4"; + + for address in [base58, bech32_upper, bech32_lower].iter() { + for pj in [https, onion].iter() { + // TODO add with and without amount + // TODO shuffle params + let uri = format!("{}?amount=1&pj={}", address, pj); + assert!(Uri::try_from(&*uri).is_ok()); + } + } + } + + #[test] + fn test_unsupported() { + assert!(!Uri::try_from("bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX") + .unwrap() + .extras + .pj_is_supported()); + } +} From c65c4099fdcd74e8be5f77f49bb2261d6a18eeaf Mon Sep 17 00:00:00 2001 From: BitcoinZavior Date: Wed, 6 Sep 2023 15:36:22 -0400 Subject: [PATCH 30/30] Code cleanup --- README.md | 11 +- src/error.rs | 2 +- src/test/mod.rs | 391 +++++++++++++++++++++++------------------------- 3 files changed, 198 insertions(+), 206 deletions(-) diff --git a/README.md b/README.md index e015728..4a1da7f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,10 @@ -# payjoin-ffi +# Payjoin language bindings -# Bindings for PDK +This repository creates libraries for various programming languages, all using the Rust-based [Payjoin](https://github.com/payjoin/rust-payjoin) as the core implementation of BIP178, sourced from the [Payjoin Dev Kit](https://payjoindevkit.org/). -This repository creates the `libpdkffi` multi-language library for the Rust-based [PDK](https://payjoindevkit.org/) from the [Payjoin Dev Kit] project. +The primary focus of this project is to provide developers with cross-language libraries that seamlessly integrate with different platform languages. By offering support for multiple languages, we aim to enhance the accessibility and usability of Payjoin, empowering developers to incorporate this privacy-enhancing feature into their applications regardless of their preferred programming language. -Each supported language and the platform(s) it's packaged for has its own directory. The Rust code in this project is in the bdk-ffi directory and is a wrapper around the [bdk] library to expose its APIs in a uniform way using the [mozilla/uniffi-rs] bindings generator for each supported target language. +With a commitment to collaboration and interoperability, this repository strives to foster a more inclusive and diverse ecosystem around Payjoin and BIP178, contributing to the wider adoption of privacy-focused practices within the Bitcoin community. Join us in our mission to build a more private and secure future for Bitcoin transactions through Payjoin and BIP178! + +**Current Status:** +This is a pre-alpha stage and is currently in the design phase. The first language bindings available will be for Android followed by Swift. The ultimate goal is to have Payjoin implementations for Android, iOS, Python, Java, React Native, Flutter, C# and Golang. diff --git a/src/error.rs b/src/error.rs index 6d63722..ca6dae4 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,6 @@ use std::fmt; -use bitcoin::psbt::PsbtParseError; +use payjoin::bitcoin::psbt::PsbtParseError; use payjoin::receive::RequestError; #[derive(Debug, PartialEq, Eq)] diff --git a/src/test/mod.rs b/src/test/mod.rs index 1d701f8..b4e5754 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -2,232 +2,221 @@ use bitcoin::psbt::Psbt; use bitcoind::bitcoincore_rpc; use bitcoind::bitcoincore_rpc::core_rpc_json::AddressType; use bitcoind::bitcoincore_rpc::RpcApi; -use log::{ debug, log_enabled, Level }; +use log::{debug, log_enabled, Level}; use payjoin::bitcoin; use payjoin::bitcoin::base64; use std::collections::HashMap; use std::str::FromStr; use std::sync::Arc; -use crate::receive::{ Headers, IsOutoutKnown, IsScriptOwned, UncheckedProposal }; -use crate::send::{ Configuration, Request }; +use crate::receive::{Headers, IsOutoutKnown, IsScriptOwned, UncheckedProposal}; +use crate::send::{Configuration, Request}; use crate::transaction::PartiallySignedTransaction; use crate::uri::Uri; -use crate::{ Network, ScriptBuf }; +use crate::{Network, ScriptBuf}; #[test] fn integration_test() { - let _ = env_logger::try_init(); - let bitcoind_exe = std::env - ::var("BITCOIND_EXE") - .ok() - .or_else(|| bitcoind::downloaded_exe_path().ok()) - .expect("version feature or env BITCOIND_EXE is required for tests"); - let mut conf = bitcoind::Conf::default(); - conf.view_stdout = log_enabled!(Level::Debug); - let bitcoind = bitcoind::BitcoinD::with_conf(bitcoind_exe, &conf).unwrap(); - let receiver = bitcoind.create_wallet("receiver").unwrap(); - let receiver_address = receiver - .get_new_address(None, Some(AddressType::Bech32)) - .unwrap() - .assume_checked(); - let sender = bitcoind.create_wallet("sender").unwrap(); - let sender_address = sender - .get_new_address(None, Some(AddressType::Bech32)) - .unwrap() - .assume_checked(); - bitcoind.client.generate_to_address(1, &receiver_address).unwrap(); - bitcoind.client.generate_to_address(101, &sender_address).unwrap(); - - assert_eq!( - payjoin::bitcoin::Amount::from_btc(50.0).unwrap(), - receiver.get_balances().unwrap().mine.trusted, - "receiver doesn't own bitcoin" - ); - - assert_eq!( - payjoin::bitcoin::Amount::from_btc(50.0).unwrap(), - sender.get_balances().unwrap().mine.trusted, - "sender doesn't own bitcoin" - ); - - // Receiver creates the payjoin URI - let pj_receiver_address = receiver.get_new_address(None, None).unwrap().assume_checked(); - let amount = payjoin::bitcoin::Amount::from_btc(1.0).unwrap(); - let pj_uri_string = format!( - "{}?amount={}&pj=https://example.com", - pj_receiver_address.to_qr_uri(), - amount.to_btc() - ); - let _uri = Uri::from_str(pj_uri_string).unwrap().assume_checked(); - let pj_uri = _uri.check_pj_supported().expect("Bad Uri"); - // Sender create a funded PSBT (not broadcasted) to address with amount given in the pj_uri - let mut outputs = HashMap::with_capacity(1); - outputs.insert(pj_uri.clone().address(), pj_uri.clone().amount().unwrap()); - debug!("outputs: {:?}", outputs); - let options = bitcoincore_rpc::json::WalletCreateFundedPsbtOptions { - lock_unspent: Some(true), - fee_rate: Some(payjoin::bitcoin::Amount::from_sat(2000)), - ..Default::default() - }; - let psbt = sender - .wallet_create_funded_psbt( - &[], // inputs - &outputs, - None, // locktime - Some(options), - None - ) - .expect("failed to create PSBT").psbt; - let psbt = sender.wallet_process_psbt(&psbt, None, None, None).unwrap().psbt; - let psbt = PartiallySignedTransaction::new(psbt).expect("Psbt new"); - debug!("Original psbt: {:#?}", psbt); - let pj_params = Configuration::with_fee_contribution(10000, None); - let (req, ctx) = pj_uri.create_pj_request(psbt, pj_params).unwrap(); - let headers = Headers::from_vec(req.body.clone()); - - // ********************** - // Inside the Receiver: - // this data would transit from one party to another over the network in production - let rec_clone = Arc::new(receiver); - let response = handle_pj_request(req, headers, rec_clone.clone()); - // this response would be returned as http response to the sender - - // ********************** - // Inside the Sender: - // Sender checks, signs, finalizes, extracts, and broadcasts - let checked_payjoin_proposal_psbt = ctx.process_response(&mut response.as_bytes()).unwrap(); - let payjoin_base64_string = base64::encode(&checked_payjoin_proposal_psbt.serialize()); - let payjoin_psbt = sender - .wallet_process_psbt(&payjoin_base64_string, None, None, None) - .unwrap().psbt; - let payjoin_psbt = sender.finalize_psbt(&payjoin_psbt, Some(false)).unwrap().psbt.unwrap(); - let payjoin_psbt = Psbt::from_str(&payjoin_psbt).unwrap(); - debug!("Sender's Payjoin PSBT: {:#?}", payjoin_psbt); - - let payjoin_tx = payjoin_psbt.extract_tx(); - bitcoind.client.send_raw_transaction(&payjoin_tx).unwrap(); + let _ = env_logger::try_init(); + let bitcoind_exe = std::env::var("BITCOIND_EXE") + .ok() + .or_else(|| bitcoind::downloaded_exe_path().ok()) + .expect("version feature or env BITCOIND_EXE is required for tests"); + let mut conf = bitcoind::Conf::default(); + conf.view_stdout = log_enabled!(Level::Debug); + let bitcoind = bitcoind::BitcoinD::with_conf(bitcoind_exe, &conf).unwrap(); + let receiver = bitcoind.create_wallet("receiver").unwrap(); + let receiver_address = + receiver.get_new_address(None, Some(AddressType::Bech32)).unwrap().assume_checked(); + let sender = bitcoind.create_wallet("sender").unwrap(); + let sender_address = + sender.get_new_address(None, Some(AddressType::Bech32)).unwrap().assume_checked(); + bitcoind.client.generate_to_address(1, &receiver_address).unwrap(); + bitcoind.client.generate_to_address(101, &sender_address).unwrap(); + + assert_eq!( + payjoin::bitcoin::Amount::from_btc(50.0).unwrap(), + receiver.get_balances().unwrap().mine.trusted, + "receiver doesn't own bitcoin" + ); + + assert_eq!( + payjoin::bitcoin::Amount::from_btc(50.0).unwrap(), + sender.get_balances().unwrap().mine.trusted, + "sender doesn't own bitcoin" + ); + + // Receiver creates the payjoin URI + let pj_receiver_address = receiver.get_new_address(None, None).unwrap().assume_checked(); + let amount = payjoin::bitcoin::Amount::from_btc(1.0).unwrap(); + let pj_uri_string = format!( + "{}?amount={}&pj=https://example.com", + pj_receiver_address.to_qr_uri(), + amount.to_btc() + ); + let _uri = Uri::from_str(pj_uri_string).unwrap().assume_checked(); + let pj_uri = _uri.check_pj_supported().expect("Bad Uri"); + // Sender create a funded PSBT (not broadcasted) to address with amount given in the pj_uri + let mut outputs = HashMap::with_capacity(1); + outputs.insert(pj_uri.clone().address().to_string(), pj_uri.clone().amount().unwrap()); + debug!("outputs: {:?}", outputs); + let options = bitcoincore_rpc::json::WalletCreateFundedPsbtOptions { + lock_unspent: Some(true), + fee_rate: Some(payjoin::bitcoin::Amount::from_sat(2000)), + ..Default::default() + }; + let psbt = sender + .wallet_create_funded_psbt( + &[], // inputs + &outputs, + None, // locktime + Some(options), + None, + ) + .expect("failed to create PSBT") + .psbt; + let psbt = sender.wallet_process_psbt(&psbt, None, None, None).unwrap().psbt; + let psbt = PartiallySignedTransaction::new(psbt).expect("Psbt new"); + debug!("Original psbt: {:#?}", psbt); + let pj_params = Configuration::with_fee_contribution(10000, None); + let (req, ctx) = pj_uri.create_pj_request(psbt, pj_params).unwrap(); + let headers = Headers::from_vec(req.body.clone()); + + // ********************** + // Inside the Receiver: + // this data would transit from one party to another over the network in production + let rec_clone = Arc::new(receiver); + let response = handle_pj_request(req, headers, rec_clone.clone()); + // this response would be returned as http response to the sender + + // ********************** + // Inside the Sender: + // Sender checks, signs, finalizes, extracts, and broadcasts + let checked_payjoin_proposal_psbt = ctx.process_response(&mut response.as_bytes()).unwrap(); + let payjoin_base64_string = base64::encode(&checked_payjoin_proposal_psbt.serialize()); + let payjoin_psbt = + sender.wallet_process_psbt(&payjoin_base64_string, None, None, None).unwrap().psbt; + let payjoin_psbt = sender.finalize_psbt(&payjoin_psbt, Some(false)).unwrap().psbt.unwrap(); + let payjoin_psbt = Psbt::from_str(&payjoin_psbt).unwrap(); + debug!("Sender's Payjoin PSBT: {:#?}", payjoin_psbt); + + let payjoin_tx = payjoin_psbt.extract_tx(); + bitcoind.client.send_raw_transaction(&payjoin_tx).unwrap(); } // Receiver receive and process original_psbt from a sender // In production it it will come in as an HTTP request (over ssl or onion) fn handle_pj_request( - req: Request, - headers: Headers, - receiver: Arc + req: Request, headers: Headers, receiver: Arc, ) -> String { - // Receiver receive payjoin proposal, IRL it will be an HTTP request (over ssl or onion) - let proposal = UncheckedProposal::from_request( - req.body.as_slice(), - req.url.internal.query().unwrap_or("").to_string(), - headers - ).unwrap(); - - // 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.get_transaction_to_schedule_broadcast(); - - let proposal = proposal - .check_can_broadcast(Box::new(TestBroadcast(receiver.clone()))) - .expect("Payjoin proposal should be broadcastable"); - - // Receive Check 2: receiver can't sign for proposal inputs - let proposal = proposal - .check_inputs_not_owned(MockScriptOwned(receiver.clone())) - .expect("Receiver should not own any of the inputs"); - - // Receive Check 3: receiver can't sign for proposal inputs - let proposal = proposal.check_no_mixed_input_scripts().unwrap(); - - // Receive Check 4: have we seen this input before? More of a check for non-interactive i.e. payment processor receivers. - let payjoin = Arc::new( - proposal - .check_no_inputs_seen_before(MockOutputOwned {}) - .unwrap() - .identify_receiver_outputs(MockScriptOwned(receiver.clone())) - .expect("Receiver should have at least one output") - ); - print!("payjoin: {}", payjoin.is_output_substitution_disabled()); - // Select receiver payjoin inputs. TODO Lock them. - let available_inputs = receiver.list_unspent(None, None, None, None, None).unwrap(); - let candidate_inputs: HashMap = available_inputs - .iter() - .map(|i| (i.amount.to_sat(), crate::OutPoint { txid: i.txid.to_string(), vout: i.vout })) - .collect(); - - let selected_outpoint = payjoin.try_preserving_privacy(candidate_inputs).expect("gg"); - let selected_utxo = available_inputs - .iter() - .find(|i| { - i.txid.to_string() == selected_outpoint.txid.to_string() && - i.vout == selected_outpoint.vout - }) - .unwrap(); - - // calculate receiver payjoin outputs given receiver payjoin inputs and original_psbt, - let txo_to_contribute = crate::TxOut { - value: selected_utxo.amount.to_sat(), - script_pubkey: ScriptBuf { inner: selected_utxo.script_pub_key.clone() }, - }; - let outpoint_to_contribute = crate::OutPoint { - txid: selected_utxo.txid.to_string(), - vout: selected_utxo.vout, - }; - payjoin.contribute_witness_input(txo_to_contribute, outpoint_to_contribute); - - let receiver_substitute_address = receiver - .get_new_address(None, None) - .unwrap() - .assume_checked(); - payjoin.substitute_output_address( - crate::Address - ::from_str(receiver_substitute_address.to_string().as_str()) - .expect("Invalid address") - ); - - let payjoin_proposal_psbt = payjoin.apply_fee(None).expect("Aplly fee"); - - // Sign payjoin psbt - let payjoin_base64_string = base64::encode(&payjoin_proposal_psbt.serialize()); - let payjoin_proposal_psbt = receiver - .wallet_process_psbt(&payjoin_base64_string, None, None, Some(false)) - .unwrap().psbt; - let payjoin_proposal_psbt = - PartiallySignedTransaction::new(payjoin_proposal_psbt).expect("Invalid psbt"); - - let payjoin_proposal_psbt = payjoin.prepare_psbt(payjoin_proposal_psbt).expect("Prepare psbt"); - debug!("Receiver's Payjoin proposal PSBT: {:#?}", payjoin_proposal_psbt); - - base64::encode(&payjoin_proposal_psbt.serialize()) + // Receiver receive payjoin proposal, IRL it will be an HTTP request (over ssl or onion) + let proposal = UncheckedProposal::from_request( + req.body.as_slice(), + req.url.internal.query().unwrap_or("").to_string(), + headers, + ) + .unwrap(); + + // 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.get_transaction_to_schedule_broadcast(); + + let proposal = proposal + .check_can_broadcast(Box::new(TestBroadcast(receiver.clone()))) + .expect("Payjoin proposal should be broadcastable"); + + // Receive Check 2: receiver can't sign for proposal inputs + let proposal = proposal + .check_inputs_not_owned(MockScriptOwned(receiver.clone())) + .expect("Receiver should not own any of the inputs"); + + // Receive Check 3: receiver can't sign for proposal inputs + let proposal = proposal.check_no_mixed_input_scripts().unwrap(); + + // Receive Check 4: have we seen this input before? More of a check for non-interactive i.e. payment processor receivers. + let payjoin = Arc::new( + proposal + .check_no_inputs_seen_before(MockOutputOwned {}) + .unwrap() + .identify_receiver_outputs(MockScriptOwned(receiver.clone())) + .expect("Receiver should have at least one output"), + ); + print!("payjoin: {}", payjoin.is_output_substitution_disabled()); + // Select receiver payjoin inputs. TODO Lock them. + let available_inputs = receiver.list_unspent(None, None, None, None, None).unwrap(); + let candidate_inputs: HashMap = available_inputs + .iter() + .map(|i| (i.amount.to_sat(), crate::OutPoint { txid: i.txid.to_string(), vout: i.vout })) + .collect(); + + let selected_outpoint = payjoin.try_preserving_privacy(candidate_inputs).expect("gg"); + let selected_utxo = available_inputs + .iter() + .find(|i| { + i.txid.to_string() == selected_outpoint.txid.to_string() + && i.vout == selected_outpoint.vout + }) + .unwrap(); + + // calculate receiver payjoin outputs given receiver payjoin inputs and original_psbt, + let txo_to_contribute = crate::TxOut { + value: selected_utxo.amount.to_sat(), + script_pubkey: ScriptBuf { inner: selected_utxo.script_pub_key.clone() }, + }; + let outpoint_to_contribute = + crate::OutPoint { txid: selected_utxo.txid.to_string(), vout: selected_utxo.vout }; + payjoin.contribute_witness_input(txo_to_contribute, outpoint_to_contribute); + + let receiver_substitute_address = + receiver.get_new_address(None, None).unwrap().assume_checked(); + payjoin.substitute_output_address( + crate::Address::from_str(receiver_substitute_address.to_string().as_str()) + .expect("Invalid address") + .assume_checked() + .expect("error assume_checked"), + ); + + let payjoin_proposal_psbt = payjoin.apply_fee(None).expect("Aplly fee"); + + // Sign payjoin psbt + let payjoin_base64_string = base64::encode(&payjoin_proposal_psbt.serialize()); + let payjoin_proposal_psbt = + receiver.wallet_process_psbt(&payjoin_base64_string, None, None, Some(false)).unwrap().psbt; + let payjoin_proposal_psbt = + PartiallySignedTransaction::new(payjoin_proposal_psbt).expect("Invalid psbt"); + + let payjoin_proposal_psbt = payjoin.prepare_psbt(payjoin_proposal_psbt).expect("Prepare psbt"); + debug!("Receiver's Payjoin proposal PSBT: {:#?}", payjoin_proposal_psbt); + + base64::encode(&payjoin_proposal_psbt.serialize()) } struct TestBroadcast(Arc); impl crate::receive::CanBroadcast for TestBroadcast { - fn test_mempool_accept(&self, tx_hex: Vec) -> Result { - match self.0.test_mempool_accept(&tx_hex) { - Ok(e) => - Ok(match e.first() { - Some(e) => e.allowed, - None => anyhow::bail!("No Mempool Result"), - }), - Err(e) => anyhow::bail!(e), - } - } + fn test_mempool_accept(&self, tx_hex: Vec) -> Result { + match self.0.test_mempool_accept(&tx_hex) { + Ok(e) => Ok(match e.first() { + Some(e) => e.allowed, + None => anyhow::bail!("No Mempool Result"), + }), + Err(e) => anyhow::bail!(e), + } + } } struct MockScriptOwned(Arc); struct MockOutputOwned {} impl IsOutoutKnown for MockOutputOwned { - fn is_known(&self, _: crate::OutPoint) -> Result { - Ok(false) - } + fn is_known(&self, _: crate::OutPoint) -> Result { + Ok(false) + } } impl IsScriptOwned for MockScriptOwned { - fn is_owned(&self, script: ScriptBuf) -> Result { - { - let network = Network::Regtest; - - let address = crate::Address::from_script(script, network.clone()).unwrap(); - let addr: bitcoin::Address = address.into(); - Ok(self.0.get_address_info(&addr).unwrap().is_mine.unwrap()) - } - } + fn is_owned(&self, script: ScriptBuf) -> Result { + { + let network = Network::Regtest; + + let address = crate::Address::from_script(script, network.clone()).unwrap(); + let addr: bitcoin::Address = address.into(); + Ok(self.0.get_address_info(&addr).unwrap().is_mine.unwrap()) + } + } }