zinc-core 0.3.0

Core Rust library for Zinc Bitcoin + Ordinals wallet
Documentation
use crate::builder::{AddressScheme, CreatePsbtRequest, PaymentAddressType, Seed64, SignOptions, WalletBuilder};
use base64::Engine;
use bdk_wallet::bitcoin::hashes::{hash160, Hash};
use bdk_wallet::bitcoin::{Amount, Network, ScriptBuf, Transaction, TxOut, Txid};
use bdk_wallet::chain::ConfirmationBlockTime;
use bdk_wallet::KeychainKind;
use bitcoin::psbt::Psbt;

fn create_dummy_tx(output_value: u64, script_pubkey: ScriptBuf, uid: u8) -> Transaction {
    let mut hash_bytes = [0u8; 32];
    hash_bytes[31] = uid;
    let dummy_txid = Txid::from_byte_array(hash_bytes);

    let dummy_input = bdk_wallet::bitcoin::TxIn {
        previous_output: bdk_wallet::bitcoin::OutPoint::new(dummy_txid, 0),
        script_sig: bdk_wallet::bitcoin::ScriptBuf::new(),
        sequence: bdk_wallet::bitcoin::Sequence::ENABLE_RBF_NO_LOCKTIME,
        witness: bdk_wallet::bitcoin::Witness::default(),
    };

    Transaction {
        version: bdk_wallet::bitcoin::transaction::Version::TWO,
        lock_time: bdk_wallet::bitcoin::absolute::LockTime::ZERO,
        input: vec![dummy_input],
        output: vec![TxOut {
            value: Amount::from_sat(output_value),
            script_pubkey,
        }],
    }
}

fn fund_payment_wallet(wallet: &mut crate::builder::ZincWallet, value: u64, uid: u8) -> TxOut {
    let payment_wallet = wallet
        .payment_wallet
        .as_mut()
        .expect("dual wallet should include payment wallet");
    let payment_script = payment_wallet
        .reveal_next_address(KeychainKind::External)
        .address
        .script_pubkey();

    let tx = create_dummy_tx(value, payment_script, uid);
    let prevout = tx.output[0].clone();
    let mut graph = bdk_wallet::chain::TxGraph::default();
    let block_hash = bdk_wallet::bitcoin::BlockHash::all_zeros();

    let _ = graph.insert_tx(tx.clone());
    let _ = graph.insert_anchor(
        tx.compute_txid(),
        ConfirmationBlockTime {
            block_id: bdk_wallet::chain::BlockId {
                height: 100,
                hash: block_hash,
            },
            confirmation_time: 1_000,
        },
    );

    let mut last_active = std::collections::BTreeMap::new();
    last_active.insert(KeychainKind::External, 0);
    let update = bdk_wallet::Update {
        tx_update: graph.into(),
        chain: Default::default(),
        last_active_indices: last_active,
    };

    payment_wallet.apply_update(update).expect("payment update");
    prevout
}

fn finalized_pubkey_matches_native_witness(prevout: &TxOut, witness: &bitcoin::Witness) -> bool {
    let script = prevout.script_pubkey.as_bytes();
    if script.len() != 22 || script[0] != 0x00 || script[1] != 0x14 {
        return false;
    }
    let pubkey = match witness.iter().nth(1) {
        Some(bytes) => bytes,
        None => return false,
    };
    let pubkey_hash = hash160::Hash::hash(pubkey);
    script[2..22] == pubkey_hash[..]
}

fn finalized_pubkey_matches_nested_witness(
    prevout: &TxOut,
    script_sig: &bitcoin::ScriptBuf,
    witness: &bitcoin::Witness,
) -> bool {
    let prevout_script = prevout.script_pubkey.as_bytes();
    if prevout_script.len() != 23
        || prevout_script[0] != 0xa9
        || prevout_script[1] != 0x14
        || prevout_script[22] != 0x87
    {
        return false;
    }

    let script_sig_bytes = script_sig.as_bytes();
    if script_sig_bytes.len() != 23 || script_sig_bytes[0] != 0x16 || script_sig_bytes[1] != 0x00 || script_sig_bytes[2] != 0x14 {
        return false;
    }

    let redeem_script = &script_sig_bytes[1..23];
    let redeem_hash = hash160::Hash::hash(redeem_script);
    if prevout_script[2..22] != redeem_hash[..] {
        return false;
    }

    let pubkey = match witness.iter().nth(1) {
        Some(bytes) => bytes,
        None => return false,
    };
    let pubkey_hash = hash160::Hash::hash(pubkey);
    redeem_script[2..22] == pubkey_hash[..]
}

fn assert_payment_send_finalizes_correctly(payment_type: PaymentAddressType) {
    let seed = [11u8; 64];
    let mut wallet = WalletBuilder::from_seed(Network::Regtest, Seed64::from_array(seed))
        .with_scheme(AddressScheme::Dual)
        .with_payment_address_type(payment_type)
        .build()
        .expect("dual wallet");

    wallet.apply_verified_ordinals_update(vec![], std::collections::HashSet::new(), vec![]);
    let prevout = fund_payment_wallet(&mut wallet, 100_000, 3);

    let recipient = WalletBuilder::from_seed(Network::Regtest, Seed64::from_array([12u8; 64]))
        .with_scheme(AddressScheme::Dual)
        .with_payment_address_type(payment_type)
        .build()
        .expect("recipient wallet")
        .peek_payment_address(0)
        .expect("recipient payment address")
        .to_string();

    let request = CreatePsbtRequest::from_parts(&recipient, 40_000, 1).expect("request");
    let unsigned_psbt = wallet.create_psbt_base64(&request).expect("create send psbt");
    let signed_psbt = wallet
        .sign_psbt(
            &unsigned_psbt,
            Some(SignOptions {
                sign_inputs: None,
                sighash: None,
                finalize: true,
            }),
        )
        .expect("sign and finalize");

    let signed_bytes = base64::engine::general_purpose::STANDARD
        .decode(signed_psbt)
        .expect("decode signed psbt");
    let signed = Psbt::deserialize(&signed_bytes).expect("signed psbt");
    let extracted = signed.extract_tx().expect("extract tx");

    assert_eq!(extracted.input.len(), 1, "expected a single funded input");
    let input = &extracted.input[0];

    let is_valid = match payment_type {
        PaymentAddressType::NativeSegwit => {
            finalized_pubkey_matches_native_witness(&prevout, &input.witness)
        }
        PaymentAddressType::NestedSegwit => {
            finalized_pubkey_matches_nested_witness(&prevout, &input.script_sig, &input.witness)
        }
        PaymentAddressType::Legacy => true,
    };

    assert!(
        is_valid,
        "finalized witness/script does not match the payment prevout for {:?}: prevout_script={}, script_sig={}, witness={:?}",
        payment_type,
        prevout.script_pubkey,
        input.script_sig,
        input.witness
    );
}

#[test]
fn test_dual_native_payment_send_finalizes_with_matching_witness() {
    assert_payment_send_finalizes_correctly(PaymentAddressType::NativeSegwit);
}

#[test]
fn test_dual_nested_payment_send_finalizes_with_matching_witness() {
    assert_payment_send_finalizes_correctly(PaymentAddressType::NestedSegwit);
}