[cli] Add a few commands to handle psbts
This commit is contained in:
parent
fb4abfb99c
commit
4f865ab2c0
133
src/cli.rs
133
src/cli.rs
@ -7,6 +7,7 @@ use clap::{App, Arg, ArgMatches, SubCommand};
|
|||||||
use log::{debug, error, info, trace, LevelFilter};
|
use log::{debug, error, info, trace, LevelFilter};
|
||||||
|
|
||||||
use bitcoin::consensus::encode::{deserialize, serialize, serialize_hex};
|
use bitcoin::consensus::encode::{deserialize, serialize, serialize_hex};
|
||||||
|
use bitcoin::hashes::hex::{FromHex, ToHex};
|
||||||
use bitcoin::util::psbt::PartiallySignedTransaction;
|
use bitcoin::util::psbt::PartiallySignedTransaction;
|
||||||
use bitcoin::{Address, OutPoint};
|
use bitcoin::{Address, OutPoint};
|
||||||
|
|
||||||
@ -147,16 +148,69 @@ pub fn make_cli_subcommands<'a, 'b>() -> App<'a, 'b> {
|
|||||||
))
|
))
|
||||||
.subcommand(
|
.subcommand(
|
||||||
SubCommand::with_name("broadcast")
|
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(
|
||||||
Arg::with_name("psbt")
|
Arg::with_name("psbt")
|
||||||
.long("psbt")
|
.long("psbt")
|
||||||
.value_name("BASE64_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)
|
.takes_value(true)
|
||||||
.number_of_values(1)
|
.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> {
|
pub fn add_global_flags<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
|
||||||
@ -240,8 +294,9 @@ where
|
|||||||
let addressees = sub_matches
|
let addressees = sub_matches
|
||||||
.values_of("to")
|
.values_of("to")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.map(|s| parse_addressee(s).unwrap())
|
.map(|s| parse_addressee(s))
|
||||||
.collect();
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map_err(|s| Error::Generic(s))?;
|
||||||
let send_all = sub_matches.is_present("send_all");
|
let send_all = sub_matches.is_present("send_all");
|
||||||
let fee_rate = sub_matches
|
let fee_rate = sub_matches
|
||||||
.value_of("fee_rate")
|
.value_of("fee_rate")
|
||||||
@ -308,11 +363,73 @@ where
|
|||||||
|
|
||||||
Ok(Some(res))
|
Ok(Some(res))
|
||||||
} else if let Some(sub_matches) = matches.subcommand_matches("broadcast") {
|
} else if let Some(sub_matches) = matches.subcommand_matches("broadcast") {
|
||||||
let psbt = base64::decode(sub_matches.value_of("psbt").unwrap()).unwrap();
|
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();
|
let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap();
|
||||||
let (txid, _) = wallet.broadcast(psbt).await?;
|
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)))
|
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 {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
@ -194,11 +194,11 @@ impl ExtendedDescriptor {
|
|||||||
_ => return Err(Error::CantDeriveWithMiniscript),
|
_ => return Err(Error::CantDeriveWithMiniscript),
|
||||||
};
|
};
|
||||||
|
|
||||||
if !self.same_structure(&derived_desc) {
|
// if !self.same_structure(&derived_desc) {
|
||||||
Err(Error::CantDeriveWithMiniscript)
|
// Err(Error::CantDeriveWithMiniscript)
|
||||||
} else {
|
// } else {
|
||||||
Ok(derived_desc)
|
Ok(derived_desc)
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn derive_from_psbt_input(
|
pub fn derive_from_psbt_input(
|
||||||
|
@ -483,7 +483,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
// attempt to finalize
|
// 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))
|
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
|
// Internals
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
@ -652,60 +707,6 @@ where
|
|||||||
|
|
||||||
Ok((answer, paths, selected_amount, fee_val))
|
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>
|
impl<B, D> Wallet<B, D>
|
||||||
@ -828,10 +829,9 @@ where
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn broadcast(&self, psbt: PSBT) -> Result<(Txid, Transaction), Error> {
|
pub async fn broadcast(&self, tx: Transaction) -> Result<Txid, Error> {
|
||||||
let extracted = psbt.extract_tx();
|
self.client.borrow_mut().broadcast(&tx).await?;
|
||||||
self.client.borrow_mut().broadcast(&extracted).await?;
|
|
||||||
|
|
||||||
Ok((extracted.txid(), extracted))
|
Ok(tx.txid())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user