From abb8b3b7e821e94c09c8c9763bcb29d002668dce Mon Sep 17 00:00:00 2001 From: willowens14 Date: Thu, 15 Jun 2023 13:56:31 -0600 Subject: [PATCH] Add preliminary send payjoin support --- Cargo.lock | 82 +++++++++++++++++++--------- Cargo.toml | 9 ++- src/commands.rs | 8 ++- src/handlers.rs | 142 +++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 213 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b2b2d0c..c1d121e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,7 +74,7 @@ checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", ] [[package]] @@ -190,8 +190,10 @@ dependencies = [ "getrandom", "js-sys", "log", + "payjoin", "rand 0.6.5", "regex", + "reqwest", "rustyline", "secp256k1 0.22.2", "serde", @@ -232,6 +234,16 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +[[package]] +name = "bip21" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1998475af29ccfb7c761bb624a16e501fc321510366012bc9cce267bc134aedc" +dependencies = [ + "bitcoin", + "percent-encoding-rfc3986", +] + [[package]] name = "bip39" version = "1.2.0" @@ -381,11 +393,12 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.79" +version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "51f1226cd9da55587234753d1245dd5b132343ea240f26b6a9003d68706141ba" dependencies = [ "jobserver", + "libc", ] [[package]] @@ -635,9 +648,9 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" dependencies = [ "errno-dragonfly", "libc", @@ -830,7 +843,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", ] [[package]] @@ -1189,9 +1202,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" +checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" [[package]] name = "lock_api" @@ -1398,7 +1411,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", ] [[package]] @@ -1480,6 +1493,19 @@ dependencies = [ "proc-macro-hack", ] +[[package]] +name = "payjoin" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e076c820f8780985db6a2b1f6c0914350becc79aa3e071872bcd4bc17e7e0f06" +dependencies = [ + "base64 0.13.1", + "bip21", + "bitcoin", + "log", + "url", +] + [[package]] name = "pbkdf2" version = "0.11.0" @@ -1498,6 +1524,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +[[package]] +name = "percent-encoding-rfc3986" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3637c05577168127568a64e9dc5a6887da720efef07b3d9472d45f63ab191166" + [[package]] name = "pin-project-lite" version = "0.2.10" @@ -1823,9 +1855,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" +checksum = "b7b6d6190b7594385f61bd3911cd1be99dfddcfc365a4160cc2ab5bff4aed294" dependencies = [ "aho-corasick", "memchr", @@ -1944,7 +1976,7 @@ checksum = "79ea77c539259495ce8ca47f53e66ae0330a8819f67e23ac96ca02f50e7b7d36" dependencies = [ "log", "ring", - "rustls-webpki 0.101.1", + "rustls-webpki 0.101.2", "sct", ] @@ -1960,9 +1992,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.101.1" +version = "0.101.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15f36a6828982f422756984e47912a7a51dcbc2a197aa791158f8ca61cd8204e" +checksum = "513722fd73ad80a71f72b61009ea1b584bcfa1483ca93949c8f290298837fa59" dependencies = [ "ring", "untrusted", @@ -2087,29 +2119,29 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.175" +version = "1.0.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d25439cd7397d044e2748a6fe2432b5e85db703d6d097bd014b3c0ad1ebff0b" +checksum = "0ea67f183f058fe88a4e3ec6e2788e003840893b91bac4559cabedd00863b3ed" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.175" +version = "1.0.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b23f7ade6f110613c0d63858ddb8b94c1041f550eab58a16b371bdf2c9c80ab4" +checksum = "24e744d7782b686ab3b73267ef05697159cc0e5abbed3f47f9933165e5219036" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", ] [[package]] name = "serde_json" -version = "1.0.103" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b" +checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" dependencies = [ "itoa", "ryu", @@ -2248,9 +2280,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.27" +version = "2.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0" +checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" dependencies = [ "proc-macro2", "quote", @@ -2313,7 +2345,7 @@ checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", ] [[package]] @@ -2360,7 +2392,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 48a8603..97a2527 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,8 @@ keywords = ["bitcoin", "wallet", "descriptor", "psbt", "taproot"] readme = "README.md" license = "MIT" -[dependencies] +[dependencies] +# right here set path bdk = { version = "0.27.1", default-features = false, features = ["all-keys"] } bdk-macros = "0.6" clap = { version = "3.2.22", features = ["derive"] } @@ -21,6 +22,12 @@ zeroize = "<1.4.0" dirs-next = "2.0" env_logger = "0.7" base64 = "^0.13" +# payjoin dependencies +payjoin = { version = "=0.8.0", features = ["send"] } +# reqwest +reqwest = { version = "0.11.4", features = ["blocking"] } + + # Optional dependencies rustyline = { version = "~9.0", optional = true } diff --git a/src/commands.rs b/src/commands.rs index 12d8fe9..981488e 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -527,6 +527,12 @@ pub enum OnlineWalletSubCommand { #[clap(name = "CONFIRMATIONS", long = "confirmations", default_value = "6")] confirmations: u32, }, + /// Sends a Payjoin Transaction. Takes a valid payjoin bip21 uri. + SendPayjoin { + /// Sets the bip21 uri to send to. + #[clap(name = "URI", long = "uri")] + uri: String, + }, } /// Subcommands for Key operations. @@ -622,7 +628,7 @@ mod test { feature = "compact_filters", feature = "rpc" ))] - use super::OnlineWalletSubCommand::{Broadcast, Sync}; + use super::OnlineWalletSubCommand::{Broadcast, SendPayjoin, Sync}; use super::WalletSubCommand::OfflineWalletSubCommand; #[cfg(any( feature = "electrum", diff --git a/src/handlers.rs b/src/handlers.rs index c21afc0..3742851 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -87,6 +87,10 @@ use rustyline::error::ReadlineError; use rustyline::Editor; use serde_json::json; use std::str::FromStr; +use crate::bitcoin::psbt::Input; +// Import some modules for payjoin functionality from payjoin crate +use payjoin::{PjUriExt, UriExt}; +use std::convert::TryFrom; /// Execute an offline wallet sub-command /// @@ -321,7 +325,7 @@ where B: Blockchain, D: BatchDatabase, { - use bdk::SyncOptions; + use bdk::{signer::InputSigner, wallet::tx_builder, SyncOptions}; match online_subcommand { Sync => { @@ -393,6 +397,142 @@ where maybe_await!(wallet.verify_proof(&psbt, &msg, max_confirmation_height))?; Ok(json!({ "spendable": spendable })) } + + // Payjoin Logic goes here + SendPayjoin { uri } => { + // convert the bip21 uri into a payjoin uri, and handle error if necessary + let uri = payjoin::Uri::try_from(uri).map_err(|e| { + Error::Generic(format!("Unable to convert BIP21 into Payjoin URI: {}", e)) + })?; + + // ensure uri is payjoin capable + let uri = uri + .check_pj_supported() + .map_err(|e| Error::Generic(format!("Payjoin not supported: {}", e)))?; + + // ensure amount of satoshis is specified in uri, handle error if not + let sats = match uri.amount { + Some(amount) => Ok(amount.to_sat()), + None => Err(Error::Generic("URI was empty".to_string())), + }?; + + // call wallet build tx to create psbt + let mut tx_builder = wallet.build_tx(); + + // assuming uri address is of type address + let scrm = uri.address.script_pubkey(); + + // create tx with payjoin uri + tx_builder + .add_recipient(scrm.clone(), sats) + .fee_rate(FeeRate::from_sat_per_vb(1.0)) + .enable_rbf(); + + // create original psbt + let (mut original_psbt, _tx_details) = tx_builder + .finish() + .map_err(|e| Error::Generic(format!("Unable to build transaction: {}", e)))?; + + // check + sign psbt + let finalized = wallet.sign(&mut original_psbt, SignOptions::default())?; + + // handle error if the psbt is not finalized + if !finalized { + return Err(Error::Generic("PSBT not finalized".to_string())); + } + + // set payjoin params + // TODO use fee_rate + let pj_params = payjoin::send::Configuration::with_fee_contribution( + payjoin::bitcoin::Amount::from_sat(100), + None, + ); + + // save a clone for after processing the response + let mut ocean_psbt = original_psbt.clone(); + + // Construct the request with the PSBT and parameters + let (req, ctx) = uri + .create_pj_request(original_psbt, pj_params) // not clone here + .map_err(|e| Error::Generic(format!("Error building payjoin request: {}", e)))?; + + // reqwest client + let client = reqwest::blocking::Client::builder() + .danger_accept_invalid_certs(true) + .build() + .map_err(|e| Error::Generic(format!("Error building payjoin client: {}", e)))?; + + // send the request, and receive response + let response = client + .post(req.url) + .body(req.body) + .header("Content-Type", "text/plain") + .send() + .map_err(|e| Error::Generic(format!("Error with HTTP request: {}", e)))?; + + // process the response + let mut payjoin_psbt = ctx + .process_response(response) + .map_err(|e| Error::Generic(format!("Error processing payjoin response: {}", e)))?; + + // need to reintroduce utxo from original psbt + fn input_pairs(psbt: &mut PartiallySignedTransaction) -> Box + '_> { + Box::new( + psbt.unsigned_tx + .input + .iter() + .zip(&mut psbt.inputs) + ) + } + + // get original inputs from original psbt clone (ocean_psbt) + let mut original_inputs = input_pairs(&mut ocean_psbt).peekable(); + + + for (proposed_txin, mut proposed_psbtin) in input_pairs(&mut payjoin_psbt) { + if let Some((original_txin, original_psbtin)) = original_inputs.peek() { + if proposed_txin.previous_output == original_txin.previous_output { + proposed_psbtin.witness_utxo = original_psbtin.witness_utxo.clone(); + proposed_psbtin.non_witness_utxo = original_psbtin.non_witness_utxo.clone(); + } + original_inputs.next(); + } + } + + + // basic flow for broadcasting: sign, extract tx, and then broadcast + + // sign + wallet.sign(&mut payjoin_psbt, SignOptions::default())?; + + // finalize + let finalized = wallet.finalize_psbt(&mut payjoin_psbt, SignOptions::default())?; + + match finalized { + true => { + + // Signing was successful + } + false => { + // signing failed + return Err(Error::Generic( + "Signing the payjoin transaction failed.".to_string(), + )); + } + } + + // Extract the finalized transaction + let finalized_tx = payjoin_psbt.extract_tx(); + dbg!("Finalized Transaction:"); + dbg!(finalized_tx.clone()); + + // Broadcast the transaction + blockchain.broadcast(&finalized_tx)?; + + Ok(serde_json::Value::String( + "Successfully sent payjoin transaction".to_string(), + )) + } } }