Skip to content

Commit

Permalink
Make proposal with payjoin crate
Browse files Browse the repository at this point in the history
Co-authored-by: evanlinjin <hello@evanlinjin.me>
  • Loading branch information
DanGould and evanlinjin committed Aug 14, 2022
1 parent b3fb183 commit 07e301d
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 34 deletions.
110 changes: 104 additions & 6 deletions Cargo.lock

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

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ spec = "config_spec.toml"
test_paths = []

[dependencies]
bitcoin = { version = "0.27.0", features = ["use-serde"] }
bitcoin = { version = "0.28.1", features = ["use-serde"] }
bip78 = { git = "https://github.com/dangould/rust-payjoin", branch = "receive-logic-v3", features = ["sender", "receiver" ] }
url = "2.2.2"
hyper = "0.14.9"
tonic_lnd = "0.4.0"
tokio = { version = "1.7.1", features = ["rt-multi-thread"] }
Expand Down
83 changes: 56 additions & 27 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::fmt;
use std::sync::{Arc, Mutex};

use args::ArgError;
use bip78::receiver::*;
use bitcoin::util::address::Address;
use bitcoin::util::psbt::PartiallySignedTransaction;
use bitcoin::{Script, TxOut};
Expand Down Expand Up @@ -181,7 +182,7 @@ impl From<tonic_lnd::Error> for CheckError {
async fn ensure_connected(client: &mut tonic_lnd::Client, node: &P2PAddress) {
let pubkey = node.node_id.to_string();
let peer_addr =
tonic_lnd::rpc::LightningAddress { pubkey: pubkey, host: node.as_host_port().to_string() };
tonic_lnd::rpc::LightningAddress { pubkey, host: node.as_host_port().to_string() };

let connect_req =
tonic_lnd::rpc::ConnectPeerRequest { addr: Some(peer_addr), perm: true, timeout: 60 };
Expand Down Expand Up @@ -266,6 +267,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}

pub(crate) struct Headers(hyper::HeaderMap);
impl bip78::receiver::Headers for Headers {
fn get_header(&self, key: &str) -> Option<&str> { self.0.get(key)?.to_str().ok() }
}

async fn handle_web_req(
mut handler: Handler,
req: Request<Body>,
Expand Down Expand Up @@ -293,44 +299,60 @@ async fn handle_web_req(
dbg!(req.uri().query());

let mut lnd = handler.client;
let query = req
.uri()
.query()
.into_iter()
.flat_map(|query| query.split('&'))
.map(|kv| {
let eq_pos = kv.find('=').unwrap();
(&kv[..eq_pos], &kv[(eq_pos + 1)..])
})
.collect::<std::collections::HashMap<_, _>>();

if query.get("disableoutputsubstitution") == Some(&"1") {

let headers = Headers(req.headers().to_owned());
let query = {
let uri = req.uri();
if let Some(query) = uri.query() {
Some(&query.to_owned());
}
None
};
let body = req.into_body();
let bytes = hyper::body::to_bytes(body).await?;
dbg!(&bytes); // this is correct by my accounts
let reader = &*bytes;
let original_request = UncheckedProposal::from_request(reader, query, headers).unwrap();
if original_request.is_output_substitution_disabled() {
// TODO handle error for output substitution properly, don't panic
panic!("Output substitution must be enabled");
}
let base64_bytes = hyper::body::to_bytes(req.into_body()).await?;
let bytes = base64::decode(&base64_bytes).unwrap();
let mut reader = &*bytes;
let mut psbt = PartiallySignedTransaction::consensus_decode(&mut reader).unwrap();

let proposal = original_request
// This is interactive, NOT a Payment Processor, so we don't save original tx.
// Humans can solve the failure case out of band by trying again.
.assume_interactive_receive_endpoint()
.assume_no_inputs_owned() // TODO Check
.assume_no_mixed_input_scripts() // This check is silly and could be ignored
.assume_no_inputs_seen_before(); // TODO

let mut psbt = proposal.psbt().clone();
eprintln!("Received transaction: {:#?}", psbt);
for input in &mut psbt.global.unsigned_tx.input {
// clear signature
input.script_sig = bitcoin::blockdata::script::Script::new();
{
for input in &mut psbt.unsigned_tx.input {
// clear signature
input.script_sig = bitcoin::blockdata::script::Script::new();
}
}
// TODO: Handle with payjoin crate. Support multiple receiver outputs.
let (our_output, scheduled_payjoin) = handler
.payjoins
.find(&mut psbt.global.unsigned_tx.output)
.find(&mut psbt.unsigned_tx.output)
.expect("the transaction doesn't contain our output");
// TODO: replace with scheduled_payjoin.total_channel_amount()
let total_channel_amount: bitcoin::Amount = scheduled_payjoin
.channels
.iter()
.map(|channel| channel.amount)
.fold(bitcoin::Amount::ZERO, std::ops::Add::add);
// TODO: replace with sheduled_payjoin.fees()
let fees = calculate_fees(
scheduled_payjoin.channels.len() as u64,
scheduled_payjoin.fee_rate,
scheduled_payjoin.wallet_amount != bitcoin::Amount::ZERO,
);

// FIXME we shouldn't have anything that panics when handling an http request
assert_eq!(
our_output.value,
(total_channel_amount + scheduled_payjoin.wallet_amount + fees).as_sat()
Expand All @@ -341,9 +363,13 @@ async fn handle_web_req(
.map(|_| rand::random::<[u8; 32]>())
.collect::<Vec<_>>();

// these are channel-open txouts
// no collect() because of async
let mut txouts = Vec::with_capacity(scheduled_payjoin.channels.len());

// TODO: Creating `OpenChannelRequest`s should be it's own loop or functional iterator.
// Async calls into LND should be done in a step after.
// TODO: ❗️ Handle Channel open fails & timeouts. They corrupt the node.❗️
for (channel, chid) in scheduled_payjoin.channels.iter().zip(&chids) {
let psbt_shim = tonic_lnd::rpc::PsbtShim {
pending_chan_id: Vec::from(chid as &[_]),
Expand All @@ -354,8 +380,11 @@ async fn handle_web_req(
let funding_shim = tonic_lnd::rpc::funding_shim::Shim::PsbtShim(psbt_shim);
let funding_shim = tonic_lnd::rpc::FundingShim { shim: Some(funding_shim) };

// TODO wrap lnd in mutex. A mutable reference prevents the benefits from an async call in our context
// because we only have 1 client.
ensure_connected(&mut lnd, &channel.node).await;


let open_channel = tonic_lnd::rpc::OpenChannelRequest {
node_pubkey: channel.node.node_id.to_vec(),
local_funding_amount: channel
Expand Down Expand Up @@ -392,9 +421,9 @@ async fn handle_web_req(
let tx = PartiallySignedTransaction::consensus_decode(&mut bytes)
.unwrap();
eprintln!("PSBT received from LND: {:#?}", tx);
assert_eq!(tx.global.unsigned_tx.output.len(), 1);
assert_eq!(tx.unsigned_tx.output.len(), 1);

txouts.extend(tx.global.unsigned_tx.output);
txouts.extend(tx.unsigned_tx.output);
break;
}
// panic?
Expand All @@ -412,11 +441,11 @@ async fn handle_web_req(
*our_output = channel_output;
} else {
our_output.value = scheduled_payjoin.wallet_amount.as_sat();
psbt.global.unsigned_tx.output.push(channel_output)
psbt.unsigned_tx.output.push(channel_output)
}

psbt.global.unsigned_tx.output.extend(txouts);
psbt.outputs.resize_with(psbt.global.unsigned_tx.output.len(), Default::default);
psbt.unsigned_tx.output.extend(txouts);
psbt.outputs.resize_with(psbt.unsigned_tx.output.len(), Default::default);

eprintln!("PSBT to be given to LND: {:#?}", psbt);
let mut psbt_bytes = Vec::new();
Expand All @@ -441,7 +470,7 @@ async fn handle_web_req(
}

// Reset transaction state to be non-finalized
psbt = PartiallySignedTransaction::from_unsigned_tx(psbt.global.unsigned_tx)
let psbt = PartiallySignedTransaction::from_unsigned_tx(psbt.unsigned_tx.clone())
.expect("resetting tx failed");
let mut psbt_bytes = Vec::new();
eprintln!("PSBT that will be returned: {:#?}", psbt);
Expand Down

0 comments on commit 07e301d

Please # to comment.