Skip to content

Commit

Permalink
Replace PjUriBuilder with mutable PjUri
Browse files Browse the repository at this point in the history
The builder pattern was feature gated and confusing when there were
really only two distinct ways to build Uris:

1. From plain parameters in v1, from which to add amount, label, etc.
2. From a v2 receiver, from which to add amount, label, etc.

amount, label, message, etc. can be set by mutating the bitcoin_uri::Uri
struct directly, and we weren't enforcing anything special like pjos
in the builder that we could not in the v2::Receiver directly. So we can
delete code and have a simple interface that gets us closer to additive
v1/v2 features.
  • Loading branch information
DanGould committed Jan 8, 2025
1 parent 1baa8a0 commit daee12d
Show file tree
Hide file tree
Showing 7 changed files with 79 additions and 197 deletions.
7 changes: 4 additions & 3 deletions payjoin-cli/src/app/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use payjoin::bitcoin::psbt::Psbt;
use payjoin::bitcoin::{self, FeeRate};
use payjoin::receive::{PayjoinProposal, UncheckedProposal};
use payjoin::send::v1::SenderBuilder;
use payjoin::{Error, PjUriBuilder, Uri, UriExt};
use payjoin::{Error, Uri, UriExt};
use tokio::net::TcpListener;

use super::config::AppConfig;
Expand Down Expand Up @@ -137,7 +137,8 @@ impl App {
let pj_part = payjoin::Url::parse(pj_part)
.map_err(|e| anyhow!("Failed to parse pj_endpoint: {}", e))?;

let pj_uri = PjUriBuilder::new(pj_receiver_address, pj_part).amount(amount).build();
let mut pj_uri = payjoin::receive::build_v1_pj_uri(&pj_receiver_address, &pj_part, false);
pj_uri.amount = Some(amount);

Ok(pj_uri.to_string())
}
Expand Down Expand Up @@ -263,7 +264,7 @@ impl App {
} else {
format!("{}?pj={}", address.to_qr_uri(), self.config.pj_endpoint)
};
let uri = payjoin::Uri::try_from(uri_string.clone())
let uri = Uri::try_from(uri_string.clone())
.map_err(|_| Error::Server(anyhow!("Could not parse payjoin URI string.").into()))?;
let _ = uri.assume_checked(); // we just got it from bitcoind above

Expand Down
7 changes: 2 additions & 5 deletions payjoin-cli/src/app/v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,8 @@ impl App {
amount: Option<Amount>,
) -> Result<()> {
println!("Receive session established");
let mut pj_uri_builder = session.pj_uri_builder();
if let Some(amount) = amount {
pj_uri_builder = pj_uri_builder.amount(amount);
}
let pj_uri = pj_uri_builder.build();
let mut pj_uri = session.pj_uri();
pj_uri.amount = amount;

println!("Request Payjoin by sharing this Payjoin Uri:");
println!("{}", pj_uri);
Expand Down
2 changes: 1 addition & 1 deletion payjoin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,5 @@ mod uri;

#[cfg(feature = "base64")]
pub use bitcoin::base64;
pub use uri::{PjParseError, PjUri, PjUriBuilder, Uri, UriExt};
pub use uri::{PjParseError, PjUri, Uri, UriExt};
pub use url::{ParseError, Url};
12 changes: 11 additions & 1 deletion payjoin/src/receive/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//! Usage is pretty simple:
//!
//! 1. Generate a pj_uri [BIP 21](https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki)
//! using [`payjoin::Uri`](crate::Uri)::from_str
//! using [`build_v1_pj_uri`]
//! 2. Listen for a sender's request on the `pj` endpoint
//! 3. Parse the request using
//! [`UncheckedProposal::from_request()`](crate::receive::UncheckedProposal::from_request())
Expand Down Expand Up @@ -92,6 +92,16 @@ impl<'a> From<&'a InputPair> for InternalInputPair<'a> {
fn from(pair: &'a InputPair) -> Self { Self { psbtin: &pair.psbtin, txin: &pair.txin } }
}

pub fn build_v1_pj_uri<'a>(
address: &bitcoin::Address,
endpoint: &url::Url,
disable_output_substitution: bool,
) -> crate::uri::PjUri<'a> {
let extras =
crate::uri::PayjoinExtras { endpoint: endpoint.clone(), disable_output_substitution };
bitcoin_uri::Uri::with_extras(address.clone(), extras)
}

/// The sender's original PSBT and optional parameters
///
/// This type is used to process the request. It is returned by
Expand Down
54 changes: 41 additions & 13 deletions payjoin/src/receive/v2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use crate::psbt::PsbtExt;
use crate::receive::optional_parameters::Params;
use crate::receive::InputPair;
use crate::uri::ShortId;
use crate::{PjUriBuilder, Request};
use crate::Request;

pub(crate) mod error;

Expand Down Expand Up @@ -140,7 +140,7 @@ impl Receiver {
([u8; crate::ohttp::ENCAPSULATED_MESSAGE_BYTES], ohttp::ClientResponse),
OhttpEncapsulationError,
> {
let fallback_target = self.pj_url();
let fallback_target = self.subdir();
ohttp_encapsulate(&mut self.context.ohttp_keys, "GET", fallback_target.as_str(), None)
}

Expand Down Expand Up @@ -185,19 +185,20 @@ impl Receiver {
Ok(UncheckedProposal { inner, context: self.context.clone() })
}

pub fn pj_uri_builder(&self) -> PjUriBuilder {
PjUriBuilder::new(
self.context.address.clone(),
self.pj_url(),
Some(self.context.s.public_key().clone()),
Some(self.context.ohttp_keys.clone()),
Some(self.context.expiry),
)
/// Build a V2 Payjoin URI from the receiver's context
pub fn pj_uri<'a>(&self) -> crate::PjUri<'a> {
use crate::uri::{PayjoinExtras, UrlExt};
let mut pj = self.subdir().clone();
pj.set_receiver_pubkey(self.context.s.public_key().clone());
pj.set_ohttp(self.context.ohttp_keys.clone());
pj.set_exp(self.context.expiry);
let extras = PayjoinExtras { endpoint: pj, disable_output_substitution: false };
bitcoin_uri::Uri::with_extras(self.context.address.clone(), extras)
}

// The contents of the `&pj=` query parameter.
// This identifies a session at the payjoin directory server.
pub fn pj_url(&self) -> Url {
/// The subdirectory for this Payjoin receiver session.
/// It consists of a directory URL and the session ShortID in the path.
pub fn subdir(&self) -> Url {
let mut url = self.context.directory.clone();
{
let mut path_segments =
Expand Down Expand Up @@ -568,4 +569,31 @@ mod test {
let deserialized: Receiver = serde_json::from_str(&serialized).unwrap();
assert_eq!(session, deserialized);
}

#[test]
fn test_v2_pj_uri() {
let address = bitcoin::Address::from_str("12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX")
.unwrap()
.assume_checked();
let receiver_keys = crate::hpke::HpkeKeyPair::gen_keypair();
let ohttp_keys =
OhttpKeys::from_str("OH1QYPM5JXYNS754Y4R45QWE336QFX6ZR8DQGVQCULVZTV20TFVEYDMFQC")
.expect("Invalid OhttpKeys");
let arbitrary_url = Url::parse("https://example.com").unwrap();
let uri = Receiver {
context: SessionContext {
address,
directory: arbitrary_url.clone(),
subdirectory: None,
ohttp_keys,
ohttp_relay: arbitrary_url.clone(),
expiry: SystemTime::now() + Duration::from_secs(60),
s: receiver_keys,
e: None,
},
}
.pj_uri();
assert_ne!(uri.extras.endpoint, arbitrary_url);
assert!(!uri.extras.disable_output_substitution);
}
}
139 changes: 0 additions & 139 deletions payjoin/src/uri/mod.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
use std::borrow::Cow;

use bitcoin::address::NetworkChecked;
use bitcoin::{Address, Amount};
pub use error::PjParseError;
use url::Url;

#[cfg(feature = "v2")]
use crate::hpke::HpkePublicKey;
use crate::uri::error::InternalPjParseError;
#[cfg(feature = "v2")]
pub(crate) use crate::uri::url_ext::UrlExt;
#[cfg(feature = "v2")]
use crate::OhttpKeys;

pub mod error;
#[cfg(feature = "v2")]
Expand Down Expand Up @@ -141,97 +136,6 @@ impl<'a> UriExt<'a> for Uri<'a, NetworkChecked> {
}
}

/// Build a valid `PjUri`.
///
/// Payjoin receiver can use this builder to create a payjoin
/// uri to send to the sender.
#[derive(Clone)]
pub struct PjUriBuilder {
/// Address you want to receive funds to.
address: Address,
/// Amount you want to receive.
///
/// If `None` the amount will be left unspecified.
amount: Option<Amount>,
/// Message
message: Option<String>,
/// Label
label: Option<String>,
/// Payjoin endpoint url listening for payjoin requests.
pj: Url,
/// Whether or not payjoin output substitution is allowed
pjos: bool,
}

impl PjUriBuilder {
/// Create a new `PjUriBuilder` with required parameters.
///
/// ## Parameters
/// - `address`: Represents a bitcoin address.
/// - `origin`: Represents either the payjoin endpoint in v1 or the directory in v2.
/// - `ohttp_keys`: Optional OHTTP keys for v2 (only available if the "v2" feature is enabled).
/// - `expiry`: Optional non-default expiry for the payjoin session (only available if the "v2" feature is enabled).
pub fn new(
address: Address,
origin: Url,
#[cfg(feature = "v2")] receiver_pubkey: Option<HpkePublicKey>,
#[cfg(feature = "v2")] ohttp_keys: Option<OhttpKeys>,
#[cfg(feature = "v2")] expiry: Option<std::time::SystemTime>,
) -> Self {
#[allow(unused_mut)]
let mut pj = origin;
#[cfg(feature = "v2")]
if let Some(receiver_pubkey) = receiver_pubkey {
pj.set_receiver_pubkey(receiver_pubkey);
}
#[cfg(feature = "v2")]
if let Some(ohttp_keys) = ohttp_keys {
pj.set_ohttp(ohttp_keys);
}
#[cfg(feature = "v2")]
if let Some(expiry) = expiry {
pj.set_exp(expiry);
}
Self { address, amount: None, message: None, label: None, pj, pjos: false }
}
/// Set the amount you want to receive.
pub fn amount(mut self, amount: Amount) -> Self {
self.amount = Some(amount);
self
}

/// Set the message.
pub fn message(mut self, message: String) -> Self {
self.message = Some(message);
self
}

/// Set the label.
pub fn label(mut self, label: String) -> Self {
self.label = Some(label);
self
}

/// Set whether or not payjoin output substitution is allowed.
pub fn pjos(mut self, pjos: bool) -> Self {
self.pjos = pjos;
self
}

/// Build payjoin URI.
///
/// Constructs a `bitcoin_uri::Uri` with PayjoinParams from the
/// parameters set in the builder.
pub fn build<'a>(self) -> PjUri<'a> {
let extras = PayjoinExtras { endpoint: self.pj, disable_output_substitution: self.pjos };
let mut pj_uri = bitcoin_uri::Uri::with_extras(self.address, extras);
pj_uri.amount = self.amount;
pj_uri.label = self.label.map(Into::into);
pj_uri.message = self.message.map(Into::into);
pj_uri
}
}

impl PayjoinExtras {
pub fn is_output_substitution_disabled(&self) -> bool { self.disable_output_substitution }
}
Expand Down Expand Up @@ -417,47 +321,4 @@ mod tests {
.extras
.pj_is_supported());
}

#[test]
fn test_builder() {
use std::str::FromStr;

use url::Url;
use PjUriBuilder;
let https = "https://example.com/";
let onion = "http://vjdpwgybvubne5hda6v4c5iaeeevhge6jvo3w2cl6eocbwwvwxp7b7qd.onion/";
let base58 = "12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX";
let bech32_upper = "TB1Q6D3A2W975YNY0ASUVD9A67NER4NKS58FF0Q8G4";
let bech32_lower = "tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4";

for address in [base58, bech32_upper, bech32_lower] {
for pj in [https, onion] {
let address = bitcoin::Address::from_str(address).unwrap().assume_checked();
let amount = bitcoin::Amount::ONE_BTC;
let builder = PjUriBuilder::new(
address.clone(),
Url::parse(pj).unwrap(),
#[cfg(feature = "v2")]
None,
#[cfg(feature = "v2")]
None,
#[cfg(feature = "v2")]
None,
)
.amount(amount)
.message("message".to_string())
.label("label".to_string())
.pjos(true);
let uri = builder.build();
assert_eq!(uri.address, address);
assert_eq!(uri.amount.unwrap(), bitcoin::Amount::ONE_BTC);
let label: Cow<'_, str> = uri.label.clone().unwrap().try_into().unwrap();
let message: Cow<'_, str> = uri.message.clone().unwrap().try_into().unwrap();
assert_eq!(label, "label");
assert_eq!(message, "message");
assert!(uri.extras.disable_output_substitution);
assert_eq!(uri.extras.endpoint.to_string(), pj.to_string());
}
}
}
}
Loading

0 comments on commit daee12d

Please sign in to comment.