Skip to content

Commit

Permalink
Outline payjoin service
Browse files Browse the repository at this point in the history
  • Loading branch information
DanGould committed Jan 8, 2024
1 parent 02d22f6 commit 78733ca
Show file tree
Hide file tree
Showing 11 changed files with 427 additions and 1 deletion.
85 changes: 85 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,15 @@ futures = "0.3.30"
url = "2.5.0"
rand = "0.8.5"
bdk = "0.29.0"
payjoin = { version = "0.13.0", features = ["base64", "receive"] }
lazy_static = "1.4.0"
opentelemetry = { version = "0.21.0" }
opentelemetry_sdk = { version = "0.21.0", features = ["rt-tokio"] }
serde_with = "3.4.0"
electrum-client = "0.18.0"
reqwest = { version = "0.11.23", default-features = false, features = ["json", "rustls-tls"] }
hyper = { version = "0.14", features = ["full"] }
hyper-rustls = { version = "0.24", optional = true }
tonic_lnd = { version = "0.5.0", features = ["tracing"] }
async-trait = "0.1"
base64 = "0.21.5"
Expand Down
2 changes: 1 addition & 1 deletion src/api/server/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ impl From<PayoutQueue> for proto::PayoutQueue {
proto::payout_queue_config::Trigger::IntervalSecs(seconds.as_secs() as u32)
}
PayoutQueueTrigger::Manual => proto::payout_queue_config::Trigger::Manual(true),
PayoutQueueTrigger::Payjoin => proto::payout_queue_config::Trigger::Payjoin(true)
PayoutQueueTrigger::Payjoin => proto::payout_queue_config::Trigger::Payjoin(true),
};
let tx_priority: proto::TxPriority = payout_queue.config.tx_priority.into();
let config = Some(proto::PayoutQueueConfig {
Expand Down
8 changes: 8 additions & 0 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1047,6 +1047,14 @@ async fn run_cmd(
.context("Api server error")
});
}));
let payjoin_send = send.clone();
handles.push(tokio::spawn(async move {
let _ = payjoin_send.try_send(
super::payjoin::run(pool, app)
.await
.context("Payjoin server error")
);
}));

let reason = receive.recv().await.expect("Didn't receive msg");
for handle in handles {
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub mod fees;
mod job;
pub mod ledger;
mod outbox;
mod payjoin;
pub mod payout;
pub mod payout_queue;
pub mod primitives;
Expand Down
149 changes: 149 additions & 0 deletions src/payjoin/app.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
use std::{str::FromStr, collections::HashMap};

use anyhow::{anyhow, Result};
use payjoin::{receive::{PayjoinProposal, UncheckedProposal, ProvisionalProposal}, Error};
use tracing::instrument;

use super::error::*;
use crate::primitives::bitcoin;

const BOOTSTRAP_KEY_NAME: &str = "payjoin_bootstrap_key";

pub struct PayjoinApp {
// config: bitcoind ledger, wallet, pj_host, pj_endpoint
// ledger: Ledger,
// network: bitcoin::Network,
pool: sqlx::PgPool,
}

impl PayjoinApp {
pub fn new(pool: sqlx::PgPool) -> Self {
Self {
pool,
}
}

#[instrument(name = "payjoin_app.process_proposal", skip(self), err)]
fn process_proposal(&self, proposal: UncheckedProposal) -> Result<PayjoinProposal, Error> {
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
let _to_broadcast_in_failure_case = proposal.extract_tx_to_schedule_broadcast();

// The network is used for checks later
let network =
bitcoind.get_blockchain_info().map_err(|e| Error::Server(e.into())).and_then(
|info| bitcoin::Network::from_str(&info.chain).map_err(|e| Error::Server(e.into())),
)?;

// Receive Check 1: Can Broadcast
let proposal = proposal.check_broadcast_suitability(None, |tx| {
let raw_tx = bitcoin::consensus::encode::serialize_hex(&tx);
let mempool_results =
bitcoind.test_mempool_accept(&[raw_tx]).map_err(|e| Error::Server(e.into()))?;
match mempool_results.first() {
Some(result) => Ok(result.allowed),
None => Err(Error::Server(
anyhow!("No mempool results returned on broadcast check").into(),
)),
}
})?;
tracing::trace!("check1");

// Receive Check 2: receiver can't sign for proposal inputs
let proposal = proposal.check_inputs_not_owned(|input| {
if let Ok(address) = bitcoin::BdkAddress::from_script(input, network) {
bitcoind
.get_address_info(&address)
.map(|info| info.is_mine.unwrap_or(false))
.map_err(|e| Error::Server(e.into()))
} else {
Ok(false)
}
})?;
tracing::trace!("check2");
// Receive Check 3: receiver can't sign for proposal inputs
let proposal = proposal.check_no_mixed_input_scripts()?;
tracing::trace!("check3");

// Receive Check 4: have we seen this input before? More of a check for non-interactive i.e. payment processor receivers.
let payjoin = proposal.check_no_inputs_seen_before(|input| {
// TODO implement input_seen_before database check
// Ok(!self.insert_input_seen_before(*input).map_err(|e| Error::Server(e.into()))?)
Ok(false)
})?;
tracing::trace!("check4");

let mut provisional_payjoin = payjoin.identify_receiver_outputs(|output_script| {
if let Ok(address) = bitcoin::BdkAddress::from_script(output_script, network) {
bitcoind
.get_address_info(&address)
.map(|info| info.is_mine.unwrap_or(false))
.map_err(|e| Error::Server(e.into()))
} else {
Ok(false)
}
})?;

if !self.config.sub_only {
// Select receiver payjoin inputs.
_ = try_contributing_inputs(&mut provisional_payjoin, &bitcoind)
.map_err(|e| tracing::warn!("Failed to contribute inputs: {}", e));
}

let receiver_substitute_address = bitcoind
.get_new_address(None, None)
.map_err(|e| Error::Server(e.into()))?
.assume_checked();
provisional_payjoin.substitute_output_address(receiver_substitute_address);

let payjoin_proposal = provisional_payjoin.finalize_proposal(
|psbt: &bitcoin::psbt::Psbt| {
bitcoind
.wallet_process_psbt(&base64::encode(psbt.serialize()), None, None, Some(false))
.map(|res| bitcoin::psbt::Psbt::from_str(&res.psbt).map_err(|e| Error::Server(e.into())))
.map_err(|e| Error::Server(e.into()))?
},
None, // TODO set to bitcoin::FeeRate::MIN or similar
)?;
let payjoin_proposal_psbt = payjoin_proposal.psbt();
println!(
"Responded with Payjoin proposal {}",
payjoin_proposal_psbt.clone().extract_tx().txid()
);
Ok(payjoin_proposal)
}


fn try_contributing_inputs(
payjoin: &mut 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<bitcoin::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.")?;
tracing::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(())
}
}
18 changes: 18 additions & 0 deletions src/payjoin/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct PayjoinConfig {
#[serde(default = "default_port")]
pub listen_port: u16,
}
impl Default for PayjoinConfig {
fn default() -> Self {
Self {
listen_port: default_port(),
}
}
}

fn default_port() -> u16 {
8088
}
8 changes: 8 additions & 0 deletions src/payjoin/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
use thiserror::Error;

#[allow(clippy::large_enum_variant)]
#[derive(Error, Debug)]
pub enum PayjoinError {
#[error("PayjoinError - Error")]
Error,
}
Loading

0 comments on commit 78733ca

Please sign in to comment.