Skip to content

Commit

Permalink
Add new typestates for output and input contributions
Browse files Browse the repository at this point in the history
This effectively splits the `ProvisionalProposal` state into three
states that must be completed in order:
- `WantsOutputs` allows the receiver to substitute or add output(s) and
produces a WantsInputs
- `WantsInputs` allows the receiver to contribute input(s) as needed to
fund their outputs and produces a ProvisionalProposal
- `ProvisionalProposal` is only responsible for finalizing the payjoin
proposal and producing a PSBT that will be acceptable to the sender
  • Loading branch information
spacebear21 committed Jul 30, 2024
1 parent 8d4196f commit 65720a7
Show file tree
Hide file tree
Showing 6 changed files with 277 additions and 110 deletions.
32 changes: 0 additions & 32 deletions payjoin-cli/src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,38 +94,6 @@ pub trait App {
}
}

fn try_contributing_inputs(
payjoin: &mut payjoin::receive::ProvisionalProposal,
bitcoind: &bitcoincore_rpc::Client,
) -> Result<()> {
use bitcoin::OutPoint;

let available_inputs = bitcoind
.list_unspent(None, None, None, None, None)
.context("Failed to list unspent from bitcoind")?;
let candidate_inputs: HashMap<Amount, OutPoint> = available_inputs
.iter()
.map(|i| (i.amount, OutPoint { txid: i.txid, 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 == selected_outpoint.txid && i.vout == selected_outpoint.vout)
.context("This shouldn't happen. Failed to retrieve the privacy preserving utxo from those we provided to the seclector.")?;
log::debug!("selected utxo: {:#?}", selected_utxo);

// calculate receiver payjoin outputs given receiver payjoin inputs and original_psbt,
let txo_to_contribute = bitcoin::TxOut {
value: selected_utxo.amount.to_sat(),
script_pubkey: selected_utxo.script_pub_key.clone(),
};
let outpoint_to_contribute =
bitcoin::OutPoint { txid: selected_utxo.txid, vout: selected_utxo.vout };
payjoin.contribute_witness_input(txo_to_contribute, outpoint_to_contribute);
Ok(())
}

struct Headers<'a>(&'a hyper::HeaderMap);
impl payjoin::receive::Headers for Headers<'_> {
fn get_header(&self, key: &str) -> Option<&str> {
Expand Down
57 changes: 43 additions & 14 deletions payjoin-cli/src/app/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use payjoin::{Error, PjUriBuilder, Uri, UriExt};

use super::config::AppConfig;
use super::App as AppTrait;
use crate::app::{http_agent, try_contributing_inputs, Headers};
use crate::app::{http_agent, Headers};
use crate::db::Database;
#[cfg(feature = "danger-local-https")]
pub const LOCAL_CERT_FILE: &str = "localhost.der";
Expand Down Expand Up @@ -297,7 +297,7 @@ impl App {
})?;
log::trace!("check4");

let mut provisional_payjoin = payjoin.identify_receiver_outputs(|output_script| {
let provisional_payjoin = payjoin.identify_receiver_outputs(|output_script| {
if let Ok(address) = bitcoin::Address::from_script(output_script, network) {
bitcoind
.get_address_info(&address)
Expand All @@ -308,19 +308,17 @@ impl App {
}
})?;

_ = try_contributing_inputs(&mut provisional_payjoin, &bitcoind)
.map_err(|e| log::warn!("Failed to contribute inputs: {}", e));
let provisional_payjoin = provisional_payjoin.try_substitute_receiver_output(|| {
Ok(bitcoind
.get_new_address(None, None)
.map_err(|e| Error::Server(e.into()))?
.require_network(network)
.map_err(|e| Error::Server(e.into()))?
.script_pubkey())
})?;

_ = provisional_payjoin
.try_substitute_receiver_output(|| {
Ok(bitcoind
.get_new_address(None, None)
.map_err(|e| Error::Server(e.into()))?
.require_network(network)
.map_err(|e| Error::Server(e.into()))?
.script_pubkey())
})
.map_err(|e| log::warn!("Failed to substitute output: {}", e));
let provisional_payjoin = try_contributing_inputs(provisional_payjoin, &bitcoind)
.map_err(|e| Error::Server(e.into()))?;

let payjoin_proposal = provisional_payjoin.finalize_proposal(
|psbt: &Psbt| {
Expand All @@ -339,3 +337,34 @@ impl App {
Ok(payjoin_proposal)
}
}

fn try_contributing_inputs(
payjoin: payjoin::receive::WantsInputs,
bitcoind: &bitcoincore_rpc::Client,
) -> Result<payjoin::receive::ProvisionalProposal> {
use bitcoin::OutPoint;

let available_inputs = bitcoind
.list_unspent(None, None, None, None, None)
.context("Failed to list unspent from bitcoind")?;
let candidate_inputs: HashMap<Amount, OutPoint> = available_inputs
.iter()
.map(|i| (i.amount, OutPoint { txid: i.txid, 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 == selected_outpoint.txid && i.vout == selected_outpoint.vout)
.context("This shouldn't happen. Failed to retrieve the privacy preserving utxo from those we provided to the seclector.")?;
log::debug!("selected utxo: {:#?}", selected_utxo);

// calculate receiver payjoin outputs given receiver payjoin inputs and original_psbt,
let txo_to_contribute = bitcoin::TxOut {
value: selected_utxo.amount.to_sat(),
script_pubkey: selected_utxo.script_pub_key.clone(),
};
let outpoint_to_contribute =
bitcoin::OutPoint { txid: selected_utxo.txid, vout: selected_utxo.vout };
Ok(payjoin.contribute_witness_input(txo_to_contribute, outpoint_to_contribute))
}
42 changes: 37 additions & 5 deletions payjoin-cli/src/app/v2.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::Arc;

Expand Down Expand Up @@ -272,8 +273,6 @@ impl App {
&self,
proposal: payjoin::receive::v2::UncheckedProposal,
) -> Result<payjoin::receive::v2::PayjoinProposal, Error> {
use crate::app::try_contributing_inputs;

let bitcoind = self.bitcoind().map_err(|e| Error::Server(e.into()))?;

// in a payment processor where the sender could go offline, this is where you schedule to broadcast the original_tx
Expand Down Expand Up @@ -323,7 +322,7 @@ impl App {
})?;
log::trace!("check4");

let mut provisional_payjoin = payjoin.identify_receiver_outputs(|output_script| {
let provisional_payjoin = payjoin.identify_receiver_outputs(|output_script| {
if let Ok(address) = bitcoin::Address::from_script(output_script, network) {
bitcoind
.get_address_info(&address)
Expand All @@ -334,8 +333,10 @@ impl App {
}
})?;

_ = try_contributing_inputs(&mut provisional_payjoin.inner, &bitcoind)
.map_err(|e| log::warn!("Failed to contribute inputs: {}", e));
let provisional_payjoin = provisional_payjoin.try_substitute_receiver_outputs(None)?;

let provisional_payjoin = try_contributing_inputs(provisional_payjoin, &bitcoind)
.map_err(|e| Error::Server(e.into()))?;

let payjoin_proposal = provisional_payjoin.finalize_proposal(
|psbt: &Psbt| {
Expand All @@ -352,6 +353,37 @@ impl App {
}
}

fn try_contributing_inputs(
payjoin: payjoin::receive::v2::WantsInputs,
bitcoind: &bitcoincore_rpc::Client,
) -> Result<payjoin::receive::v2::ProvisionalProposal> {
use bitcoin::OutPoint;

let available_inputs = bitcoind
.list_unspent(None, None, None, None, None)
.context("Failed to list unspent from bitcoind")?;
let candidate_inputs: HashMap<Amount, OutPoint> = available_inputs
.iter()
.map(|i| (i.amount, OutPoint { txid: i.txid, 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 == selected_outpoint.txid && i.vout == selected_outpoint.vout)
.context("This shouldn't happen. Failed to retrieve the privacy preserving utxo from those we provided to the seclector.")?;
log::debug!("selected utxo: {:#?}", selected_utxo);

// calculate receiver payjoin outputs given receiver payjoin inputs and original_psbt,
let txo_to_contribute = bitcoin::TxOut {
value: selected_utxo.amount.to_sat(),
script_pubkey: selected_utxo.script_pub_key.clone(),
};
let outpoint_to_contribute =
bitcoin::OutPoint { txid: selected_utxo.txid, vout: selected_utxo.vout };
Ok(payjoin.contribute_witness_input(txo_to_contribute, outpoint_to_contribute))
}

async fn unwrap_ohttp_keys_or_else_fetch(config: &AppConfig) -> Result<payjoin::OhttpKeys> {
if let Some(keys) = config.ohttp_keys.clone() {
println!("Using OHTTP Keys from config");
Expand Down
Loading

0 comments on commit 65720a7

Please sign in to comment.