use std::collections::BTreeMap;
use std::fmt;
use std::str::FromStr;
use bitcoin::bip32::{ChildNumber, DerivationPath, Fingerprint, IntoDerivationPath, Xpriv, Xpub};
use bitcoin::consensus::encode;
use bitcoin::locktime::absolute;
use bitcoin::psbt::{self, Input, Psbt, PsbtSighashType};
use bitcoin::secp256k1::{Secp256k1, Signing, Verification};
use bitcoin::{
transaction, Address, Amount, CompressedPublicKey, Network, OutPoint, ScriptBuf, Sequence,
Transaction, TxIn, TxOut, Witness,
};
type Result<T> = std::result::Result<T, Error>;
const EXTENDED_MASTER_PRIVATE_KEY: &str = "tprv8ZgxMBicQKsPeSHZFZWT8zxie2dXWcwemnTkf4grVzMvP2UABUxqbPTCHzZ4ztwhBghpfFw27sJqEgW6y1ZTZcfvCUdtXE1L6qMF7TBdbqQ";
const INPUT_UTXO_TXID: &str = "295f06639cde6039bf0c3dbf4827f0e3f2b2c2b476408e2f9af731a8d7a9c7fb";
const INPUT_UTXO_VOUT: u32 = 0;
const INPUT_UTXO_SCRIPT_PUBKEY: &str = "00149891eeb8891b3e80a2a1ade180f143add23bf5de";
const INPUT_UTXO_VALUE: &str = "50 BTC";
const INPUT_UTXO_DERIVATION_PATH: &str = "0h/0h/0h";
const RECEIVE_ADDRESS: &str = "bcrt1qcmnpjjjw78yhyjrxtql6lk7pzpujs3h244p7ae";
const OUTPUT_AMOUNT_BTC: &str = "1 BTC";
const CHANGE_AMOUNT_BTC: &str = "48.99999 BTC";
const NETWORK: Network = Network::Regtest;
fn main() -> Result<()> {
let secp = Secp256k1::new();
let (offline, fingerprint, account_0_xpub, input_xpub) =
ColdStorage::new(&secp, EXTENDED_MASTER_PRIVATE_KEY)?;
let online = WatchOnly::new(account_0_xpub, input_xpub, fingerprint);
let created = online.create_psbt(&secp)?;
let updated = online.update_psbt(created)?;
let signed = offline.sign_psbt(&secp, updated)?;
let finalized = online.finalize_psbt(signed)?;
let tx = finalized.extract_tx_unchecked_fee_rate();
tx.verify(|_| Some(previous_output())).expect("failed to verify transaction");
let hex = encode::serialize_hex(&tx);
println!("You should now be able to broadcast the following transaction: \n\n{}", hex);
Ok(())
}
struct ColdStorage {
master_xpriv: Xpriv,
master_xpub: Xpub,
}
type ExportData = (ColdStorage, Fingerprint, Xpub, Xpub);
impl ColdStorage {
fn new<C: Signing>(secp: &Secp256k1<C>, xpriv: &str) -> Result<ExportData> {
let master_xpriv = Xpriv::from_str(xpriv)?;
let master_xpub = Xpub::from_priv(secp, &master_xpriv);
let path = "84h/0h/0h".into_derivation_path()?;
let account_0_xpriv = master_xpriv.derive_priv(secp, &path)?;
let account_0_xpub = Xpub::from_priv(secp, &account_0_xpriv);
let path = INPUT_UTXO_DERIVATION_PATH.into_derivation_path()?;
let input_xpriv = master_xpriv.derive_priv(secp, &path)?;
let input_xpub = Xpub::from_priv(secp, &input_xpriv);
let wallet = ColdStorage { master_xpriv, master_xpub };
let fingerprint = wallet.master_fingerprint();
Ok((wallet, fingerprint, account_0_xpub, input_xpub))
}
fn master_fingerprint(&self) -> Fingerprint { self.master_xpub.fingerprint() }
fn sign_psbt<C: Signing + Verification>(
&self,
secp: &Secp256k1<C>,
mut psbt: Psbt,
) -> Result<Psbt> {
match psbt.sign(&self.master_xpriv, secp) {
Ok(keys) => assert_eq!(keys.len(), 1),
Err((_, e)) => {
let e = e.get(&0).expect("at least one error");
return Err(e.clone().into());
}
};
Ok(psbt)
}
}
struct WatchOnly {
account_0_xpub: Xpub,
input_xpub: Xpub,
master_fingerprint: Fingerprint,
}
impl WatchOnly {
fn new(account_0_xpub: Xpub, input_xpub: Xpub, master_fingerprint: Fingerprint) -> Self {
WatchOnly { account_0_xpub, input_xpub, master_fingerprint }
}
fn create_psbt<C: Verification>(&self, secp: &Secp256k1<C>) -> Result<Psbt> {
let to_address = Address::from_str(RECEIVE_ADDRESS)?.require_network(Network::Regtest)?;
let to_amount = Amount::from_str(OUTPUT_AMOUNT_BTC)?;
let (_, change_address, _) = self.change_address(secp)?;
let change_amount = Amount::from_str(CHANGE_AMOUNT_BTC)?;
let tx = Transaction {
version: transaction::Version::TWO,
lock_time: absolute::LockTime::ZERO,
input: vec![TxIn {
previous_output: OutPoint { txid: INPUT_UTXO_TXID.parse()?, vout: INPUT_UTXO_VOUT },
script_sig: ScriptBuf::new(),
sequence: Sequence::MAX, witness: Witness::default(),
}],
output: vec![
TxOut { value: to_amount, script_pubkey: to_address.script_pubkey() },
TxOut { value: change_amount, script_pubkey: change_address.script_pubkey() },
],
};
let psbt = Psbt::from_unsigned_tx(tx)?;
Ok(psbt)
}
fn update_psbt(&self, mut psbt: Psbt) -> Result<Psbt> {
let mut input = Input { witness_utxo: Some(previous_output()), ..Default::default() };
let pk = self.input_xpub.to_pub();
let wpkh = pk.wpubkey_hash();
let redeem_script = ScriptBuf::new_p2wpkh(&wpkh);
input.redeem_script = Some(redeem_script);
let fingerprint = self.master_fingerprint;
let path = input_derivation_path()?;
let mut map = BTreeMap::new();
map.insert(pk.0, (fingerprint, path));
input.bip32_derivation = map;
let ty = PsbtSighashType::from_str("SIGHASH_ALL")?;
input.sighash_type = Some(ty);
psbt.inputs = vec![input];
Ok(psbt)
}
fn finalize_psbt(&self, mut psbt: Psbt) -> Result<Psbt> {
if psbt.inputs.is_empty() {
return Err(psbt::SignError::MissingInputUtxo.into());
}
let sigs: Vec<_> = psbt.inputs[0].partial_sigs.values().collect();
let mut script_witness: Witness = Witness::new();
script_witness.push(sigs[0].to_vec());
script_witness.push(self.input_xpub.to_pub().to_bytes());
psbt.inputs[0].final_script_witness = Some(script_witness);
psbt.inputs[0].partial_sigs = BTreeMap::new();
psbt.inputs[0].sighash_type = None;
psbt.inputs[0].redeem_script = None;
psbt.inputs[0].witness_script = None;
psbt.inputs[0].bip32_derivation = BTreeMap::new();
Ok(psbt)
}
fn change_address<C: Verification>(
&self,
secp: &Secp256k1<C>,
) -> Result<(CompressedPublicKey, Address, DerivationPath)> {
let path = [ChildNumber::from_normal_idx(1)?, ChildNumber::from_normal_idx(0)?];
let derived = self.account_0_xpub.derive_pub(secp, &path)?;
let pk = derived.to_pub();
let addr = Address::p2wpkh(&pk, NETWORK);
let path = path.into_derivation_path()?;
Ok((pk, addr, path))
}
}
fn input_derivation_path() -> Result<DerivationPath> {
let path = INPUT_UTXO_DERIVATION_PATH.into_derivation_path()?;
Ok(path)
}
fn previous_output() -> TxOut {
let script_pubkey = ScriptBuf::from_hex(INPUT_UTXO_SCRIPT_PUBKEY)
.expect("failed to parse input utxo scriptPubkey");
let amount = Amount::from_str(INPUT_UTXO_VALUE).expect("failed to parse input utxo value");
TxOut { value: amount, script_pubkey }
}
struct Error(Box<dyn std::error::Error>);
impl<T: std::error::Error + 'static> From<T> for Error {
fn from(e: T) -> Self { Error(Box::new(e)) }
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Debug::fmt(&self.0, f) }
}