Skip to content

Commit

Permalink
Upgrade to unreleased payjoin-0.21
Browse files Browse the repository at this point in the history
Remove support for v1 send,receive code. Separate uniffi exports where
wrappers are required in order to support payjoin-flutter and other
non-uniffi bindings. Use procedural macros to define uniffi bindings.

Irrelevant v1-to-v1 tests were removed.
  • Loading branch information
DanGould committed Nov 13, 2024
1 parent 7586ec6 commit c96d28c
Show file tree
Hide file tree
Showing 23 changed files with 2,184 additions and 2,594 deletions.
684 changes: 393 additions & 291 deletions Cargo.lock

Large diffs are not rendered by default.

15 changes: 8 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ uniffi = { version = "0.28.0", features = ["build"] }

[dev-dependencies]
uniffi = { version = "0.28.0", features = ["bindgen-tests"] }
bdk = { version = "0.29.0", features = ["all-keys", "use-esplora-ureq", "keys-bip39"] }
bdk = { version = "0.29.0", features = ["all-keys", "use-esplora-ureq", "keys-bip39", "rpc"] }
#bitcoin-ffi = { git = "https://github.com/bitcoindevkit/bitcoin-ffi" }
bitcoind = { version = "0.36.0", features = ["0_21_2"] }
bitcoincore-rpc = "0.19.0"
http = "1"
payjoin-directory = { git = "https://github.com/payjoin/rust-payjoin", features = ["danger-local-https"] }
payjoin-directory = { git = "https://github.com/payjoin/rust-payjoin", rev = "ef2ce55a57fe5270bc761bfcda58024ae45a93aa", features = ["danger-local-https"] }
ohttp-relay = "0.0.8"
rcgen = { version = "0.11" }
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
Expand All @@ -27,10 +29,10 @@ testcontainers-modules = { version = "0.1.3", features = ["redis"] }
tokio = { version = "1.12.0", features = ["full"] }
[dependencies]

payjoin = {version = "=0.20.0", features = ["send", "receive", "base64", "v2", "io"] }
uniffi = { version = "0.28.0" }
payjoin = { git = "https://github.com/payjoin/rust-payjoin", rev = "ef2ce55a57fe5270bc761bfcda58024ae45a93aa", features = ["send", "receive", "base64", "v2", "io"] }
uniffi = { version = "0.28.0", optional = true }
thiserror = "1.0.47"
ohttp = { version = "0.5.1" }
ohttp = { package = "bitcoin-ohttp", version = "0.6.0" }
url = "2.5.0"
base64 = "0.22.1"
hex = "0.4.3"
Expand All @@ -49,6 +51,5 @@ strip = true


[features]
default = ["uniffi/cli"]
uniffi = []
uniffi = ["uniffi/cli"]
danger-local-https = ["payjoin/danger-local-https"]
27 changes: 1 addition & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,37 +33,12 @@ pip install payjoin
```
## Running the Integration Test

First, we need to set up bitcoin core and esplora locally in the regtest network. If you don't have these, please refer to this [page](https://learn.saylor.org/mod/page/view.php?id=36347). Alternatively, you can install `Nigiri Bitcoin`, a tool designed to simplify the process of running local instances of Bitcoin and Liquid networks for development and testing purposes. Follow the instructions [here](https://github.com/vulpemventures/nigiri) to install it on your machine.

Once Nigiri Bitcoin is up and running, you need to mine a few blocks. Refer to this [link](https://developer.bitcoin.org/reference/rpc/generatetoaddress.html?highlight=generate) on how to mine blocks.

Before running the integration tests, replace the following snippet in `tests/bdk_integration_test.rs` and `tests/bitcoin_core_integration_test.rs` with your Nigiri Bitcoin Core credentials:

```rust
static RPC_USER: &str = "admin1";
static RPC_PASSWORD: &str = "123";
static RPC_HOST: &str = "localhost";
static RPC_PORT: &str = "18443";

```

NB: The default nigiri credentials would be the following

```
rpc_user = "admin1"
rpc_password = "123"
rpc_host = "localhost"
rpc_port = "18443"
```

Now, run the integration tests:

The integration tests illustrates and verify integration using bitcoin core and with bitcoin dev kit(bdk).
The integration tests illustrates and verify integration using bitcoin core and bdk.

```shell

# Run the integration test
cargo test --package payjoin_ffi --test bitcoin_core_integration_test v1_to_v1_full_cycle
cargo test --package payjoin_ffi --test bdk_integration_test v1_to_v1_full_cycle
cargo test --package payjoin_ffi --test bdk_integration_test v2_to_v2_full_cycle --features danger-local-https

Expand Down
1 change: 1 addition & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
fn main() {
#[cfg(feature = "uniffi")]
uniffi::generate_scaffolding("src/payjoin_ffi.udl").unwrap();
}
89 changes: 89 additions & 0 deletions src/bitcoin.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use std::str::FromStr;
use std::sync::Arc;

use payjoin::bitcoin;

/// A reference to a transaction output.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
pub struct OutPoint {
/// The referenced transaction's txid.
pub txid: String,
Expand All @@ -27,6 +29,64 @@ impl From<bitcoin::OutPoint> for OutPoint {
}

#[derive(Debug, Clone)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
pub struct PsbtInput {
pub witness_utxo: Option<TxOut>,
pub redeem_script: Option<Arc<Script>>,
pub witness_script: Option<Arc<Script>>,
}

impl PsbtInput {
pub fn new(
witness_utxo: Option<TxOut>,
redeem_script: Option<Arc<Script>>,
witness_script: Option<Arc<Script>>,
) -> Self {
Self { witness_utxo, redeem_script, witness_script }
}
}

impl From<bitcoin::psbt::Input> for PsbtInput {
fn from(psbt_input: bitcoin::psbt::Input) -> Self {
Self {
witness_utxo: psbt_input.witness_utxo.map(|s| s.into()),
redeem_script: psbt_input.redeem_script.clone().map(|s| Arc::new(s.into())),
witness_script: psbt_input.witness_script.clone().map(|s| Arc::new(s.into())),
}
}
}

impl From<PsbtInput> for bitcoin::psbt::Input {
fn from(psbt_input: PsbtInput) -> Self {
Self {
witness_utxo: psbt_input.witness_utxo.map(|s| s.into()),
redeem_script: psbt_input.redeem_script.map(|s| Arc::unwrap_or_clone(s).into()),
witness_script: psbt_input.witness_script.map(|s| Arc::unwrap_or_clone(s).into()),
..Default::default()
}
}
}

#[derive(Debug, Clone)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
pub struct TxIn {
pub previous_output: OutPoint,
}

impl From<TxIn> for bitcoin::TxIn {
fn from(tx_in: TxIn) -> Self {
bitcoin::TxIn { previous_output: tx_in.previous_output.into(), ..Default::default() }
}
}

impl From<bitcoin::TxIn> for TxIn {
fn from(tx_in: bitcoin::TxIn) -> Self {
TxIn { previous_output: tx_in.previous_output.into() }
}
}

#[derive(Debug, Clone)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
pub struct TxOut {
/// The value of the output, in satoshis.
pub value: u64,
Expand All @@ -50,6 +110,7 @@ impl From<bitcoin::TxOut> for TxOut {
}

#[derive(Clone, Default)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
pub enum Network {
///Bitcoin’s testnet
Testnet,
Expand All @@ -72,3 +133,31 @@ impl From<Network> for bitcoin::Network {
}
}
}

#[derive(Clone, Debug)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
pub struct Script(pub payjoin::bitcoin::ScriptBuf);

#[cfg_attr(feature = "uniffi", uniffi::export)]
impl Script {
#[cfg_attr(feature = "uniffi", uniffi::constructor)]
pub fn new(script: Vec<u8>) -> Self {
Self(payjoin::bitcoin::ScriptBuf::from_bytes(script))
}

pub fn to_bytes(&self) -> Vec<u8> {
self.0.to_bytes()
}
}

impl From<payjoin::bitcoin::ScriptBuf> for Script {
fn from(value: payjoin::bitcoin::ScriptBuf) -> Self {
Self(value)
}
}

impl From<Script> for payjoin::bitcoin::ScriptBuf {
fn from(value: Script) -> Self {
value.0
}
}
34 changes: 30 additions & 4 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use std::fmt::Debug;

use payjoin::bitcoin::psbt::PsbtParseError;
use payjoin::receive::{RequestError, SelectionError};
use payjoin::receive::{
InputContributionError, OutputSubstitutionError, PsbtInputError, RequestError, SelectionError,
};
use payjoin::send::{CreateRequestError, ResponseError as PdkResponseError, ValidationError};

#[derive(Debug, PartialEq, Eq, thiserror::Error)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Error))]
pub enum PayjoinError {
#[error("Error while parsing the string: {message} ")]
InvalidAddress { message: String },
Expand All @@ -15,7 +18,7 @@ pub enum PayjoinError {
#[error("Error encountered while decoding PSBT: {message} ")]
PsbtParseError { message: String },

#[error("Response error: {message}")]
#[error("Response error: {message:?}")]
ResponseError { message: String },

///Error that may occur when the request from sender is malformed.
Expand Down Expand Up @@ -62,6 +65,15 @@ pub enum PayjoinError {

#[error("{message}")]
IoError { message: String },

#[error("{message}")]
OutputSubstitutionError { message: String },

#[error("{message}")]
InputContributionError { message: String },

#[error("{message}")]
InputPairError { message: String },
}

macro_rules! impl_from_error {
Expand All @@ -82,10 +94,24 @@ impl_from_error! {
payjoin::bitcoin::consensus::encode::Error => TransactionError,
payjoin::bitcoin::address::ParseError => InvalidAddress,
RequestError => RequestError,
PdkResponseError => ResponseError,
ValidationError => ValidationError,
CreateRequestError => CreateRequestError,
uniffi::UnexpectedUniFFICallbackError => UnexpectedError,
OutputSubstitutionError => OutputSubstitutionError,
InputContributionError => InputContributionError,
PsbtInputError => InputPairError,
}

#[cfg(feature = "uniffi")]
impl From<uniffi::UnexpectedUniFFICallbackError> for PayjoinError {
fn from(value: uniffi::UnexpectedUniFFICallbackError) -> Self {
PayjoinError::UnexpectedError { message: value.to_string() }
}
}

impl From<PdkResponseError> for PayjoinError {
fn from(value: PdkResponseError) -> Self {
PayjoinError::ResponseError { message: format!("{:?}", value) }
}
}

impl From<SelectionError> for PayjoinError {
Expand Down
34 changes: 26 additions & 8 deletions src/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@ use crate::error::PayjoinError;
use crate::ohttp::OhttpKeys;
use crate::uri::Url;

/// Fetch the ohttp keys from the specified payjoin directory via proxy.
///
/// * `ohttp_relay`: The http CONNNECT method proxy to request the ohttp keys from a payjoin
/// directory. Proxying requests for ohttp keys ensures a client IP address is never revealed to
/// the payjoin directory.
///
/// * `payjoin_directory`: The payjoin directory from which to fetch the ohttp keys. This
/// directory stores and forwards payjoin client payloads.
#[cfg(not(feature = "danger-local-https"))]
pub async fn fetch_ohttp_keys(
ohttp_relay: Url,
payjoin_directory: Url,
) -> Result<OhttpKeys, PayjoinError> {
payjoin::io::fetch_ohttp_keys(ohttp_relay.into(), payjoin_directory.into())
.await
.map(|e| e.into())
.map_err(|e| e.into())
}

/// Fetch the ohttp keys from the specified payjoin directory via proxy.
///
/// * `ohttp_relay`: The http CONNNECT method proxy to request the ohttp keys from a payjoin
Expand All @@ -11,16 +30,15 @@ use crate::uri::Url;
/// * `payjoin_directory`: The payjoin directory from which to fetch the ohttp keys. This
/// directory stores and forwards payjoin client payloads.
///
/// * `cert_der` (optional): The DER-encoded certificate to use for local HTTPS connections. This
/// parameter is only available when the "danger-local-https" feature is enabled.
/// * `cert_der`: The DER-encoded certificate to use for local HTTPS connections.
#[cfg(feature = "danger-local-https")]
pub async fn fetch_ohttp_keys(
ohttp_relay: Url,
payjoin_directory: Url,
#[cfg(feature = "danger-local-https")] cert_der: Vec<u8>,
cert_der: Vec<u8>,
) -> Result<OhttpKeys, PayjoinError> {
#[cfg(not(feature = "danger-local-https"))]
let res = payjoin::io::fetch_ohttp_keys(ohttp_relay.into(), payjoin_directory.into());
#[cfg(feature = "danger-local-https")]
let res = payjoin::io::fetch_ohttp_keys(ohttp_relay.into(), payjoin_directory.into(), cert_der);
res.await.map(|e| e.into()).map_err(|e| e.into())
payjoin::io::fetch_ohttp_keys(ohttp_relay.into(), payjoin_directory.into(), cert_der)
.await
.map(|e| e.into())
.map_err(|e| e.into())
}
36 changes: 8 additions & 28 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,18 @@ pub mod error;
pub mod io;
pub mod ohttp;
pub mod receive;
pub mod request;
pub mod send;
pub mod uri;

pub use crate::bitcoin::*;
use crate::error::PayjoinError;
pub use crate::error::PayjoinError;
pub use crate::ohttp::*;
pub use crate::receive::v1::*;
pub use crate::receive::v2::*;
pub use crate::send::v1::*;
pub use crate::send::v2::*;
#[cfg(feature = "uniffi")]
pub use crate::receive::uni::*;
pub use crate::request::Request;
#[cfg(feature = "uniffi")]
pub use crate::send::uni::*;
pub use crate::uri::{PjUri, PjUriBuilder, Uri, Url};

#[cfg(feature = "uniffi")]
uniffi::include_scaffolding!("payjoin_ffi");

use std::sync::Arc;
///Represents data that needs to be transmitted to the receiver.
///You need to send this request over HTTP(S) to the receiver.
#[derive(Clone, Debug)]
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: Arc<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<u8>,
}

impl From<payjoin::Request> for Request {
fn from(value: payjoin::Request) -> Self {
Self { url: Arc::new(value.url.into()), body: value.body }
}
}
uniffi::setup_scaffolding!();
22 changes: 22 additions & 0 deletions src/ohttp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,33 @@ impl From<OhttpKeys> for payjoin::OhttpKeys {
value.0
}
}
#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
#[derive(Debug, Clone)]
pub struct OhttpKeys(pub payjoin::OhttpKeys);

#[cfg_attr(feature = "uniffi", uniffi::export)]
impl OhttpKeys {
/// Decode an OHTTP KeyConfig
#[cfg_attr(feature = "uniffi", uniffi::constructor)]
pub fn decode(bytes: Vec<u8>) -> Result<Self, PayjoinError> {
payjoin::OhttpKeys::decode(bytes.as_slice()).map(|e| e.into()).map_err(|e| e.into())
}
}

use std::sync::Mutex;

#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
pub struct ClientResponse(Mutex<Option<ohttp::ClientResponse>>);

impl From<&ClientResponse> for ohttp::ClientResponse {
fn from(value: &ClientResponse) -> Self {
let mut data_guard = value.0.lock().unwrap();
Option::take(&mut *data_guard).expect("ClientResponse moved out of memory")
}
}

impl From<ohttp::ClientResponse> for ClientResponse {
fn from(value: ohttp::ClientResponse) -> Self {
Self(Mutex::new(Some(value)))
}
}
Loading

0 comments on commit c96d28c

Please sign in to comment.