Rust Taproot Transactions

This uses a bech32m encoded address and sends the amount from the UTXO to another bech32m address (- fees)

main.rs
use std::env;

use simple_wallet::p2tr_key::p2tr;
use structure::{constants::SEED, input_data::regtest_call::RegtestCall};

pub mod simple_wallet;
pub mod structure;

fn main() {
    env::set_var("RUST_BACKTRACE", "full");

    let client = RegtestCall::init(
        &vec!["bcrt1prnpxwf9tpjm4jll4ts72s2xscq66qxep6w9hf6sqnvwe9t4gvqasklfhyj"],
        "polar12_wallet",
        150,
    );

    p2tr(Some(SEED), client);
}

constants.rs
pub const NETWORK: bitcoin::Network = bitcoin::Network::Regtest;
pub const TIP: u64 = 2000;
pub const SEED: &str = "1d454c6ab705f999d97e6465300a79a9595fb5ae1186ae20e33e12bea606c094";
pub const LOG: bool = true;

regtest_call.rs
use std::{collections::BTreeMap, str::FromStr};

use bitcoin::{Address, BlockHash, OutPoint, Script, Transaction, TxIn, Txid, Witness};
use bitcoincore_rpc::{
    bitcoincore_rpc_json::{ImportMultiResult, LoadWalletResult},
    jsonrpc::serde_json::{json, Map, Value},
    Client, RpcApi,
};

use super::RpcCall;

pub struct RegtestCall {
    amount: u64,
    tx_in: Vec<TxIn>,
    previous_tx: Vec<Transaction>,
    address_list: Vec<Address>,
    client: Client,
}

impl RpcCall for RegtestCall {
    fn contract_source(&self) -> Vec<Transaction> {
        return self.previous_tx.clone();
    }

    fn prev_input(&self) -> Vec<TxIn> {
        return self.tx_in.clone();
    }

    fn script_get_balance(&self) -> u64 {
        return self.amount.clone();
    }

    fn fee(&self) -> u64 {
        return 100000;
    }
    fn broadcasts_transacton(&self, tx: &Transaction) {
        let tx_id = RegtestCall::get_client().send_raw_transaction(tx).unwrap();
        println!("transaction send transaction id is: {}", tx_id)
    }
}

impl<'a> RegtestCall {
    pub fn get_client() -> Client {
        return Client::new(
            "http://127.0.0.1:18447",
            bitcoincore_rpc::Auth::UserPass("polaruser".to_string(), "polarpass".to_owned()),
        )
        .unwrap();
    }

    pub fn init(address_list: &Vec<&str>, wallet_name: &str, mine: u8) -> Self {
        let client = RegtestCall::get_client();

        address_list.iter().for_each(|address| {
            let mut addr = "addr(".to_owned();
            addr.push_str(&address);
            addr.push_str(")");

            client
                .get_descriptor_info(&addr)
                .map(|desc| {
                    let descriptor = desc.descriptor;
                    println!("assigned a descriptor {} ", descriptor);
                    create_wallet(&client, wallet_name, mine, &descriptor)
                })
                .unwrap();
        });
        return RegtestCall::from_string(address_list);
    }

    pub fn generatetodescriptor(
        client: &Client,
        block_num: u64,
        address: &Address,
    ) -> Vec<BlockHash> {
        return client.generate_to_address(block_num, address).unwrap();
    }

    pub fn transaction_broadcast(&self, tx: &Transaction) -> Txid {
        let tx_id = RegtestCall::get_client().send_raw_transaction(tx).unwrap();
        println!("transaction id: {}", &tx_id);
        return tx_id;
    }

    pub fn from_string(address_list: &'a Vec<&str>) -> RegtestCall {
        return RegtestCall::from_address(
            address_list
                .iter()
                .map(|addr| Address::from_str(addr).unwrap())
                .collect::<Vec<Address>>(),
        );
    }

    pub fn update(&self) -> Self {
        let tx_in = RegtestCall::get_txin(&self.client, &self.address_list).to_vec();
        let previous_tx = RegtestCall::get_previous_tx(&self.client, &tx_in);

        let amt = RegtestCall::get_amount(&previous_tx, &self.address_list);
        return RegtestCall {
            amount: amt,
            tx_in,
            previous_tx,
            address_list: self.address_list.clone(),
            client: RegtestCall::get_client(),
        };
    }

    fn get_txin(client: &Client, address_list: &Vec<Address>) -> Vec<TxIn> {
        return client
            .list_unspent(
                None,
                None,
                Some(&address_list.clone().iter().collect::<Vec<&Address>>()),
                None,
                None,
            )
            .unwrap()
            .iter()
            .map(|entry| {
                return TxIn {
                    previous_output: OutPoint::new(entry.txid, entry.vout),
                    script_sig: Script::new(),
                    sequence: bitcoin::Sequence(0xFFFFFFFF),
                    witness: Witness::default(),
                };
            })
            .collect::<Vec<TxIn>>();
    }

    fn get_previous_tx(client: &Client, tx_in: &Vec<TxIn>) -> Vec<Transaction> {
        return tx_in
            .iter()
            .map(|tx_id| {
                let result = client
                    .get_transaction(&tx_id.previous_output.txid, Some(true))
                    .unwrap()
                    .transaction()
                    .unwrap();
                return result;
            })
            .collect::<Vec<Transaction>>();
    }

    fn get_amount(previous_tx: &Vec<Transaction>, address_list: &Vec<Address>) -> u64 {
        return previous_tx
            .iter()
            .map(|tx| {
                tx.output
                    .iter()
                    .filter(|p| {
                        address_list
                            .clone()
                            .iter()
                            .map(|addr| addr.script_pubkey())
                            .collect::<Vec<Script>>()
                            .contains(&p.script_pubkey)
                    })
                    .map(|output_tx| output_tx.value)
                    .sum::<u64>()
            })
            .sum::<u64>();
    }

    pub fn from_address(address_list: Vec<Address>) -> Self {
        let client = RegtestCall::get_client();
        let tx_in = RegtestCall::get_txin(&client, &address_list).to_vec();
        let previous_tx = RegtestCall::get_previous_tx(&client, &tx_in);
        let amt = RegtestCall::get_amount(&previous_tx, &address_list);
        return RegtestCall {
            amount: amt,
            tx_in,
            previous_tx,
            address_list,
            client,
        };
    }
}

fn create_wallet(client: &Client, wallet_name: &str, mine: u8, desc: &String) {
    if client
        .list_wallets()
        .unwrap()
        .contains(&wallet_name.to_owned())
    {
        importdescriptors(client, desc, mine);
        return;
    }
    client
        .create_wallet(wallet_name, Some(true), Some(true), None, Some(false))
        .map(|load_wallet| {
            println!("wallet {} created successfully ", load_wallet.name);

            if let Some(msg) = load_wallet.warning {
                println!("Warning! {}", msg)
            }

            println!("wallet {} created successfully ", load_wallet.name);

            importdescriptors(client, desc, mine);
        })
        .unwrap();
}

fn importdescriptors(client: &Client, desc: &String, mine: u8) {
    let mut params = Map::new();
    params.insert("desc".to_owned(), Value::String(desc.to_string()));
    params.insert("timestamp".to_owned(), Value::String("now".to_owned()));

    client
        .call::<Vec<ImportMultiResult>>(
            "importdescriptors",
            &[Value::Array([Value::Object(params)].to_vec())],
        )
        .map(|import| {
            import.iter().for_each(|result| {
                if result.success {
                    println!("descriptor successfully imported");
                }
                result.error.iter().for_each(|err| {
                    panic!("error importing wallet {:#?}", err);
                })
            });

            mine_to_descriptors(client, mine, desc);
        })
        .unwrap()
}

fn mine_to_descriptors(client: &Client, mine: u8, desc: &String) {
    client
        .call::<Vec<BlockHash>>("generatetodescriptor", &[json!(mine), json!(desc)])
        .unwrap();
    println!("successfully mined blocks");
}

p2tr_keys
use std::str::FromStr;

use bitcoin::{
    psbt::{Input, PartiallySignedTransaction, Prevouts},
    schnorr::TapTweak,
    secp256k1::{All, Message, Scalar, Secp256k1, SecretKey},
    util::{
        bip32::{ExtendedPrivKey, ExtendedPubKey},
        sighash::SighashCache,
    },
    Address, KeyPair, PackedLockTime, SchnorrSig, Transaction, TxOut,
};
use miniscript::psbt::PsbtExt;

use crate::structure::{
    constants::NETWORK,
    input_data::{regtest_call::RegtestCall, RpcCall},
};

pub fn p2tr(secret_string: Option<&str>, client: impl RpcCall) {
    let secp = Secp256k1::new();
    let secret = match secret_string {
        Some(sec_str) => SecretKey::from_str(&sec_str).unwrap(),
        None => {
            let scalar = Scalar::random();
            let secret_key = SecretKey::from_slice(&scalar.to_be_bytes()).unwrap();
            println!("secret_key: {}", secret_key.display_secret());
            secret_key
        }
    };

    let key_pair = KeyPair::from_secret_key(&secp, &secret);

    let (x_only, _) = key_pair.x_only_public_key();

    let address = Address::p2tr(&secp, x_only, None, NETWORK);

    println!("address {}", address.to_string());

    let ext_pub = ExtendedPubKey::from_priv(
        &secp,
        &ExtendedPrivKey::new_master(NETWORK, &secret.secret_bytes()).unwrap(),
    );

    println!("xpub {}", ext_pub.to_string());

    if (secret_string.is_none()) {
        return;
    }

    let tx_in_list = client.prev_input();

    let transaction_list = client.contract_source();

    let prevouts = transaction_list
        .iter()
        .flat_map(|tx| tx.output.clone())
        .filter(|p| address.script_pubkey().eq(&p.script_pubkey))
        .collect::<Vec<TxOut>>();

    let total: u64 = prevouts.iter().map(|tx_out| tx_out.value).sum();

    let out_put = create_output(total, &client);

    let unsigned_tx = Transaction {
        version: 2,
        lock_time: PackedLockTime(0),
        input: tx_in_list,
        output: out_put,
    };

    let mut psbt = PartiallySignedTransaction::from_unsigned_tx(unsigned_tx.clone()).unwrap();
    psbt.inputs = sign_all_unsigned_tx(&secp, &prevouts, &unsigned_tx, &key_pair);

    let tx = psbt.finalize(&secp).unwrap().extract_tx();

    client.broadcasts_transacton(&tx);
}

fn create_output<'a>(total: u64, client: &'a impl RpcCall) -> Vec<TxOut> {
    let out_put = vec![TxOut {
        value: total - client.fee(),
        script_pubkey: Address::from_str(
            "bcrt1prnpxwf9tpjm4jll4ts72s2xscq66qxep6w9hf6sqnvwe9t4gvqasklfhyj",
        )
        .unwrap()
        .script_pubkey(),
    }];
    out_put
}

fn sign_all_unsigned_tx(
    secp: &Secp256k1<All>,
    prevouts: &Vec<TxOut>,
    unsigned_tx: &Transaction,
    key_pair: &KeyPair,
) -> Vec<Input> {
    return prevouts
        .iter()
        .enumerate()
        .map(|(index, tx_out)| {
            sign_tx(secp, index, unsigned_tx, &prevouts, key_pair, tx_out).clone()
        })
        .collect();
}

fn sign_tx(
    secp: &Secp256k1<All>,
    index: usize,
    unsigned_tx: &Transaction,
    prevouts: &Vec<TxOut>,
    key_pair: &KeyPair,
    tx_out: &TxOut,
) -> Input {
    let sighash = SighashCache::new(&mut unsigned_tx.clone())
        .taproot_key_spend_signature_hash(
            index,
            &Prevouts::All(&prevouts),
            bitcoin::SchnorrSighashType::AllPlusAnyoneCanPay,
        )
        .unwrap();

    let message = Message::from_slice(&sighash).unwrap();

    let tweaked_key_pair = key_pair.tap_tweak(&secp, None);

    let sig = secp.sign_schnorr(&message, &tweaked_key_pair.to_inner());

    secp.verify_schnorr(
        &sig,
        &message,
        &tweaked_key_pair.to_inner().x_only_public_key().0,
    )
    .unwrap();

    let schnorr_sig = SchnorrSig {
        sig,
        hash_ty: bitcoin::SchnorrSighashType::AllPlusAnyoneCanPay,
    };

    let mut input = Input::default();

    input.witness_script = Some(tx_out.script_pubkey.clone());

    input.tap_key_sig = Some(schnorr_sig);

    input.witness_utxo = Some(tx_out.clone());

    return input;
}

Last updated