zinc-core 0.4.0

Core Rust library for Zinc Bitcoin + Ordinals wallet
Documentation
use crate::offer::OfferEnvelopeV1;
use bdk_wallet::bitcoin::secp256k1::{Keypair, Secp256k1, SecretKey, XOnlyPublicKey};
use std::str::FromStr;

fn sample_offer(seller_pubkey_hex: &str) -> OfferEnvelopeV1 {
    OfferEnvelopeV1 {
        version: 1,
        seller_pubkey_hex: seller_pubkey_hex.to_string(),
        network: "regtest".to_string(),
        inscription_id: "6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0"
            .to_string(),
        seller_outpoint: "6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799:0"
            .to_string(),
        ask_sats: 100_000,
        fee_rate_sat_vb: 2,
        psbt_base64: "cHNidP8BAHECAAAAAf//////////////////////////////////////////AAAAAAD9////AqCGAQAAAAAAIgAgx0Jv4z2frfr6f3Ff9rR9lSxDgP3UzrA1n6g0bHTqfQAAAAAAAAAA".to_string(),
        created_at_unix: 1_710_000_000,
        expires_at_unix: 1_710_086_400,
        nonce: 42,
    }
}

fn test_secret_key() -> SecretKey {
    SecretKey::from_str("0001020304050607080900010203040506070809000102030405060708090001")
        .expect("valid test secret key")
}

fn test_public_key_hex() -> String {
    let secp = Secp256k1::new();
    let secret_key = test_secret_key();
    let keypair = Keypair::from_secret_key(&secp, &secret_key);
    let (xonly, _) = XOnlyPublicKey::from_keypair(&keypair);
    xonly.to_string()
}

#[test]
fn canonical_json_is_stable_for_same_payload() {
    let pubkey = test_public_key_hex();
    let offer_a = sample_offer(&pubkey);
    let offer_b = sample_offer(&pubkey);

    let json_a = offer_a.canonical_json().expect("canonical json A");
    let json_b = offer_b.canonical_json().expect("canonical json B");

    assert_eq!(json_a, json_b);
}

#[test]
fn offer_id_changes_when_payload_changes() {
    let pubkey = test_public_key_hex();
    let offer_a = sample_offer(&pubkey);
    let mut offer_b = sample_offer(&pubkey);
    offer_b.ask_sats += 1;

    let hash_a = offer_a.offer_id_hex().expect("offer id A");
    let hash_b = offer_b.offer_id_hex().expect("offer id B");

    assert_ne!(hash_a, hash_b);
}

#[test]
fn schnorr_signature_verifies_for_matching_pubkey() {
    let pubkey = test_public_key_hex();
    let offer = sample_offer(&pubkey);
    let signature = offer
        .sign_schnorr_hex("0001020304050607080900010203040506070809000102030405060708090001")
        .expect("signed");

    offer
        .verify_schnorr_hex(&signature)
        .expect("signature should verify");
}

#[test]
fn schnorr_signature_rejects_tampered_payload() {
    let pubkey = test_public_key_hex();
    let offer = sample_offer(&pubkey);
    let signature = offer
        .sign_schnorr_hex("0001020304050607080900010203040506070809000102030405060708090001")
        .expect("signed");

    let mut tampered = sample_offer(&pubkey);
    tampered.ask_sats = 150_000;

    assert!(tampered.verify_schnorr_hex(&signature).is_err());
}

#[test]
fn schnorr_signature_rejects_wrong_pubkey() {
    let original_pubkey = test_public_key_hex();
    let offer = sample_offer(&original_pubkey);
    let signature = offer
        .sign_schnorr_hex("0001020304050607080900010203040506070809000102030405060708090001")
        .expect("signed");

    let wrong_secret =
        SecretKey::from_str("abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd")
            .expect("valid secret");
    let secp = Secp256k1::new();
    let wrong_keypair = Keypair::from_secret_key(&secp, &wrong_secret);
    let (wrong_pubkey, _) = XOnlyPublicKey::from_keypair(&wrong_keypair);

    let wrong_offer = sample_offer(&wrong_pubkey.to_string());
    assert!(wrong_offer.verify_schnorr_hex(&signature).is_err());
}