diff --git a/Cargo.lock b/Cargo.lock index ec19076..50608d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,11 +56,32 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf9ff0bbfd639f15c74af777d81383cf53efb7c93613f6cab67c6c11e05bbf8b" +[[package]] +name = "bip21" +version = "0.1.2" +source = "git+https://github.com/DanGould/bip21.git?rev=f74dd0f#f74dd0fb452f7d1e25f2b2d892a170f76f59f752" +dependencies = [ + "bitcoin", + "percent-encoding-rfc3986", +] + +[[package]] +name = "bip78" +version = "0.2.0-preview" +source = "git+https://github.com/dangould/rust-payjoin?branch=receive-logic-v3#ff17f1b3619eaba19c847af39bfb6b17c6c2c259" +dependencies = [ + "base64", + "bip21", + "bitcoin", + "rand", + "url", +] + [[package]] name = "bitcoin" -version = "0.27.1" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a41df6ad9642c5c15ae312dd3d074de38fd3eb7cc87ad4ce10f90292a83fe4d" +checksum = "05bba324e6baf655b882df672453dbbc527bc938cadd27750ae510aaccc3a66a" dependencies = [ "bech32", "bitcoin_hashes", @@ -179,6 +200,16 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + [[package]] name = "futures-channel" version = "0.3.19" @@ -348,6 +379,17 @@ dependencies = [ "tokio-io-timeout", ] +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "indexmap" version = "1.8.0" @@ -433,6 +475,7 @@ name = "loptos" version = "0.1.0" dependencies = [ "base64", + "bip78", "bitcoin", "configure_me", "configure_me_codegen", @@ -444,6 +487,7 @@ dependencies = [ "serde_json", "tokio", "tonic_lnd", + "url", ] [[package]] @@ -455,6 +499,12 @@ dependencies = [ "roff", ] +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + [[package]] name = "memchr" version = "2.4.1" @@ -526,6 +576,12 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +[[package]] +name = "percent-encoding-rfc3986" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3637c05577168127568a64e9dc5a6887da720efef07b3d9472d45f63ab191166" + [[package]] name = "petgraph" version = "0.5.1" @@ -785,9 +841,9 @@ dependencies = [ [[package]] name = "secp256k1" -version = "0.20.3" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d03ceae636d0fed5bae6a7f4f664354c5f4fcedf6eef053fef17e49f837d0a" +checksum = "26947345339603ae8395f68e2f3d85a6b0a8ddfe6315818e80b8504415099db0" dependencies = [ "secp256k1-sys", "serde", @@ -795,9 +851,9 @@ dependencies = [ [[package]] name = "secp256k1-sys" -version = "0.4.2" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957da2573cde917463ece3570eab4a0b3f19de6f1646cde62e6fd3868f566036" +checksum = "152e20a0fd0519390fc43ab404663af8a0b794273d2a91d60ad4a39f13ffe110" dependencies = [ "cc", ] @@ -880,6 +936,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + [[package]] name = "tokio" version = "1.16.1" @@ -1104,6 +1175,21 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +[[package]] +name = "unicode-bidi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + +[[package]] +name = "unicode-normalization" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" version = "1.8.0" @@ -1122,6 +1208,18 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + [[package]] name = "void" version = "1.0.2" diff --git a/Cargo.toml b/Cargo.toml index bea4263..d8a0a05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } diff --git a/src/main.rs b/src/main.rs index ed62503..fccbd0b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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}; @@ -181,7 +182,7 @@ impl From 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 }; @@ -266,6 +267,11 @@ async fn main() -> Result<(), Box> { 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, @@ -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::>(); - - 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() @@ -341,9 +363,13 @@ async fn handle_web_req( .map(|_| rand::random::<[u8; 32]>()) .collect::>(); + // 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 &[_]), @@ -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 @@ -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? @@ -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(); @@ -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);