]> Untitled Git - bdk/commitdiff
[cli] Add a few commands to handle psbts
authorAlekos Filini <alekos.filini@gmail.com>
Sun, 17 May 2020 16:01:52 +0000 (18:01 +0200)
committerAlekos Filini <alekos.filini@gmail.com>
Sun, 17 May 2020 16:01:52 +0000 (18:01 +0200)
src/cli.rs
src/descriptor/mod.rs
src/wallet/mod.rs

index 3dd23c044344562a1ce4d97fa693a8336c87f633..a6f34625b14a6dfe8dc3c097df4f2293dd0d19ae 100644 (file)
@@ -7,6 +7,7 @@ use clap::{App, Arg, ArgMatches, SubCommand};
 use log::{debug, error, info, trace, LevelFilter};
 
 use bitcoin::consensus::encode::{deserialize, serialize, serialize_hex};
+use bitcoin::hashes::hex::{FromHex, ToHex};
 use bitcoin::util::psbt::PartiallySignedTransaction;
 use bitcoin::{Address, OutPoint};
 
@@ -147,16 +148,69 @@ pub fn make_cli_subcommands<'a, 'b>() -> App<'a, 'b> {
                 ))
         .subcommand(
             SubCommand::with_name("broadcast")
-                .about("Extracts the finalized transaction from a PSBT and broadcasts it to the network")
+                .about("Broadcasts a transaction to the network. Takes either a raw transaction or a PSBT to extract")
                 .arg(
                     Arg::with_name("psbt")
                         .long("psbt")
                         .value_name("BASE64_PSBT")
-                        .help("Sets the PSBT to broadcast")
+                        .help("Sets the PSBT to extract and broadcast")
+                        .takes_value(true)
+                        .required_unless("tx")
+                        .number_of_values(1))
+                .arg(
+                    Arg::with_name("tx")
+                        .long("tx")
+                        .value_name("RAWTX")
+                        .help("Sets the raw transaction to broadcast")
+                        .takes_value(true)
+                        .required_unless("psbt")
+                        .number_of_values(1))
+                )
+        .subcommand(
+            SubCommand::with_name("extract_psbt")
+                .about("Extracts a raw transaction from a PSBT")
+                .arg(
+                    Arg::with_name("psbt")
+                        .long("psbt")
+                        .value_name("BASE64_PSBT")
+                        .help("Sets the PSBT to extract")
+                        .takes_value(true)
+                        .required(true)
+                        .number_of_values(1))
+                )
+        .subcommand(
+            SubCommand::with_name("finalize_psbt")
+                .about("Finalizes a psbt")
+                .arg(
+                    Arg::with_name("psbt")
+                        .long("psbt")
+                        .value_name("BASE64_PSBT")
+                        .help("Sets the PSBT to finalize")
+                        .takes_value(true)
+                        .required(true)
+                        .number_of_values(1))
+                .arg(
+                    Arg::with_name("assume_height")
+                        .long("assume_height")
+                        .value_name("HEIGHT")
+                        .help("Assume the blockchain has reached a specific height")
                         .takes_value(true)
                         .number_of_values(1)
-                        .required(true),
-                ))
+                        .required(false))
+                )
+        .subcommand(
+            SubCommand::with_name("combine_psbt")
+                .about("Combines multiple PSBTs into one")
+                .arg(
+                    Arg::with_name("psbt")
+                        .long("psbt")
+                        .value_name("BASE64_PSBT")
+                        .help("Add one PSBT to comine. This option can be repeated multiple times, one for each PSBT")
+                        .takes_value(true)
+                        .number_of_values(1)
+                        .required(true)
+                        .multiple(true))
+                )
 }
 
 pub fn add_global_flags<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
@@ -240,8 +294,9 @@ where
         let addressees = sub_matches
             .values_of("to")
             .unwrap()
-            .map(|s| parse_addressee(s).unwrap())
-            .collect();
+            .map(|s| parse_addressee(s))
+            .collect::<Result<Vec<_>, _>>()
+            .map_err(|s| Error::Generic(s))?;
         let send_all = sub_matches.is_present("send_all");
         let fee_rate = sub_matches
             .value_of("fee_rate")
@@ -308,11 +363,73 @@ where
 
         Ok(Some(res))
     } else if let Some(sub_matches) = matches.subcommand_matches("broadcast") {
-        let psbt = base64::decode(sub_matches.value_of("psbt").unwrap()).unwrap();
-        let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap();
-        let (txid, _) = wallet.broadcast(psbt).await?;
+        let tx = if sub_matches.value_of("psbt").is_some() {
+            let psbt = base64::decode(&sub_matches.value_of("psbt").unwrap()).unwrap();
+            let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap();
+            psbt.extract_tx()
+        } else if sub_matches.value_of("tx").is_some() {
+            deserialize(&Vec::<u8>::from_hex(&sub_matches.value_of("tx").unwrap()).unwrap())
+                .unwrap()
+        } else {
+            panic!("Missing `psbt` and `tx` option");
+        };
+
+        let txid = wallet.broadcast(tx).await?;
 
         Ok(Some(format!("TXID: {}", txid)))
+    } else if let Some(sub_matches) = matches.subcommand_matches("extract_psbt") {
+        let psbt = base64::decode(&sub_matches.value_of("psbt").unwrap()).unwrap();
+        let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap();
+
+        Ok(Some(format!(
+            "TX: {}",
+            serialize(&psbt.extract_tx()).to_hex()
+        )))
+    } else if let Some(sub_matches) = matches.subcommand_matches("finalize_psbt") {
+        let psbt = base64::decode(&sub_matches.value_of("psbt").unwrap()).unwrap();
+        let mut psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap();
+
+        let assume_height = sub_matches
+            .value_of("assume_height")
+            .and_then(|s| Some(s.parse().unwrap()));
+
+        let finalized = wallet.finalize_psbt(&mut psbt, assume_height)?;
+
+        let mut res = String::new();
+        res += &format!("PSBT: {}\n", base64::encode(&serialize(&psbt)));
+        res += &format!("Finalized: {}", finalized);
+        if finalized {
+            res += &format!("\nExtracted: {}", serialize_hex(&psbt.extract_tx()));
+        }
+
+        Ok(Some(res))
+    } else if let Some(sub_matches) = matches.subcommand_matches("combine_psbt") {
+        let mut psbts = sub_matches
+            .values_of("psbt")
+            .unwrap()
+            .map(|s| {
+                let psbt = base64::decode(&s).unwrap();
+                let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap();
+
+                psbt
+            })
+            .collect::<Vec<_>>();
+
+        let init_psbt = psbts.pop().unwrap();
+        let final_psbt = psbts
+            .into_iter()
+            .try_fold::<_, _, Result<PartiallySignedTransaction, Error>>(
+                init_psbt,
+                |mut acc, x| {
+                    acc.merge(x)?;
+                    Ok(acc)
+                },
+            )?;
+
+        Ok(Some(format!(
+            "PSBT: {}",
+            base64::encode(&serialize(&final_psbt))
+        )))
     } else {
         Ok(None)
     }
index a2db112d6f41b45addadc9fd8e0d08e3ba93de63..6f142a83be3211bd42791b158d08545de5636061 100644 (file)
@@ -194,11 +194,11 @@ impl ExtendedDescriptor {
             _ => return Err(Error::CantDeriveWithMiniscript),
         };
 
-        if !self.same_structure(&derived_desc) {
-            Err(Error::CantDeriveWithMiniscript)
-        } else {
-            Ok(derived_desc)
-        }
+        // if !self.same_structure(&derived_desc) {
+        //     Err(Error::CantDeriveWithMiniscript)
+        // } else {
+        Ok(derived_desc)
+        // }
     }
 
     pub fn derive_from_psbt_input(
index 7ebbdd995523e748f065d9bda949826e0705cfd8..7b2c7d4a5809924984bc6634a434b21a8aab9de8 100644 (file)
@@ -483,7 +483,7 @@ where
         }
 
         // attempt to finalize
-        let finalized = self.finalize_psbt(tx.clone(), &mut psbt, assume_height)?;
+        let finalized = self.finalize_psbt(&mut psbt, assume_height)?;
 
         Ok((psbt, finalized))
     }
@@ -507,6 +507,61 @@ where
         }
     }
 
+    pub fn finalize_psbt(
+        &self,
+        psbt: &mut PSBT,
+        assume_height: Option<u32>,
+    ) -> Result<bool, Error> {
+        let mut tx = psbt.global.unsigned_tx.clone();
+
+        for (n, input) in tx.input.iter_mut().enumerate() {
+            // safe to run only on the descriptor because we assume the change descriptor also has
+            // the same structure
+            let desc = self.descriptor.derive_from_psbt_input(psbt, n);
+            debug!("{:?}", psbt.inputs[n].hd_keypaths);
+            debug!("reconstructed descriptor is {:?}", desc);
+
+            let desc = match desc {
+                Err(_) => return Ok(false),
+                Ok(desc) => desc,
+            };
+
+            // if the height is None in the database it means it's still unconfirmed, so consider
+            // that as a very high value
+            let create_height = self
+                .database
+                .borrow()
+                .get_tx(&input.previous_output.txid, false)?
+                .and_then(|tx| Some(tx.height.unwrap_or(std::u32::MAX)));
+            let current_height = assume_height.or(self.current_height);
+
+            debug!(
+                "Input #{} - {}, using `create_height` = {:?}, `current_height` = {:?}",
+                n, input.previous_output, create_height, current_height
+            );
+
+            // TODO: use height once we sync headers
+            let satisfier =
+                PSBTSatisfier::new(&psbt.inputs[n], false, create_height, current_height);
+
+            match desc.satisfy(input, satisfier) {
+                Ok(_) => continue,
+                Err(e) => {
+                    debug!("satisfy error {:?} for input {}", e, n);
+                    return Ok(false);
+                }
+            }
+        }
+
+        // consume tx to extract its input's script_sig and witnesses and move them into the psbt
+        for (input, psbt_input) in tx.input.into_iter().zip(psbt.inputs.iter_mut()) {
+            psbt_input.final_script_sig = Some(input.script_sig);
+            psbt_input.final_script_witness = Some(input.witness);
+        }
+
+        Ok(true)
+    }
+
     // Internals
 
     #[cfg(not(target_arch = "wasm32"))]
@@ -652,60 +707,6 @@ where
 
         Ok((answer, paths, selected_amount, fee_val))
     }
-
-    fn finalize_psbt(
-        &self,
-        mut tx: Transaction,
-        psbt: &mut PSBT,
-        assume_height: Option<u32>,
-    ) -> Result<bool, Error> {
-        for (n, input) in tx.input.iter_mut().enumerate() {
-            // safe to run only on the descriptor because we assume the change descriptor also has
-            // the same structure
-            let desc = self.descriptor.derive_from_psbt_input(psbt, n);
-            debug!("{:?}", psbt.inputs[n].hd_keypaths);
-            debug!("reconstructed descriptor is {:?}", desc);
-
-            let desc = match desc {
-                Err(_) => return Ok(false),
-                Ok(desc) => desc,
-            };
-
-            // if the height is None in the database it means it's still unconfirmed, so consider
-            // that as a very high value
-            let create_height = self
-                .database
-                .borrow()
-                .get_tx(&input.previous_output.txid, false)?
-                .and_then(|tx| Some(tx.height.unwrap_or(std::u32::MAX)));
-            let current_height = assume_height.or(self.current_height);
-
-            debug!(
-                "Input #{} - {}, using `create_height` = {:?}, `current_height` = {:?}",
-                n, input.previous_output, create_height, current_height
-            );
-
-            // TODO: use height once we sync headers
-            let satisfier =
-                PSBTSatisfier::new(&psbt.inputs[n], false, create_height, current_height);
-
-            match desc.satisfy(input, satisfier) {
-                Ok(_) => continue,
-                Err(e) => {
-                    debug!("satisfy error {:?} for input {}", e, n);
-                    return Ok(false);
-                }
-            }
-        }
-
-        // consume tx to extract its input's script_sig and witnesses and move them into the psbt
-        for (input, psbt_input) in tx.input.into_iter().zip(psbt.inputs.iter_mut()) {
-            psbt_input.final_script_sig = Some(input.script_sig);
-            psbt_input.final_script_witness = Some(input.witness);
-        }
-
-        Ok(true)
-    }
 }
 
 impl<B, D> Wallet<B, D>
@@ -828,10 +829,9 @@ where
             .await
     }
 
-    pub async fn broadcast(&self, psbt: PSBT) -> Result<(Txid, Transaction), Error> {
-        let extracted = psbt.extract_tx();
-        self.client.borrow_mut().broadcast(&extracted).await?;
+    pub async fn broadcast(&self, tx: Transaction) -> Result<Txid, Error> {
+        self.client.borrow_mut().broadcast(&tx).await?;
 
-        Ok((extracted.txid(), extracted))
+        Ok(tx.txid())
     }
 }