#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use crate::builder::{AddressScheme, DerivationMode, Seed64, WalletBuilder, ZincWallet};
use bdk_wallet::bitcoin::hashes::Hash;
use bdk_wallet::bitcoin::{
Amount, BlockHash, Network, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid,
};
use bdk_wallet::chain::{BlockId, ConfirmationBlockTime, TxGraph};
use bdk_wallet::KeychainKind;
use std::collections::HashSet;
fn dummy_tx(value: u64, script_pubkey: ScriptBuf, uid: u8) -> Transaction {
let mut hash_bytes = [0u8; 32];
hash_bytes[31] = uid;
Transaction {
version: bdk_wallet::bitcoin::transaction::Version::TWO,
lock_time: bdk_wallet::bitcoin::absolute::LockTime::ZERO,
input: vec![TxIn {
previous_output: OutPoint::new(Txid::from_byte_array(hash_bytes), 0),
script_sig: ScriptBuf::new(),
sequence: bdk_wallet::bitcoin::Sequence::ENABLE_RBF_NO_LOCKTIME,
witness: bdk_wallet::bitcoin::Witness::default(),
}],
output: vec![TxOut {
value: Amount::from_sat(value),
script_pubkey,
}],
}
}
fn unified() -> ZincWallet {
WalletBuilder::from_seed(Network::Regtest, Seed64::from_array([0u8; 64]))
.with_scheme(AddressScheme::Unified)
.build()
.unwrap()
}
fn fund(wallet: &mut ZincWallet, entries: &[(u64, bool)]) -> Vec<OutPoint> {
let spk = wallet
.vault_wallet
.reveal_next_address(KeychainKind::External)
.address
.script_pubkey();
let mut graph = TxGraph::<ConfirmationBlockTime>::default();
let hash = BlockHash::all_zeros();
let mut cp = wallet.vault_wallet.latest_checkpoint();
let mut ops = Vec::new();
for (i, (value, confirmed)) in entries.iter().enumerate() {
let tx = dummy_tx(*value, spk.clone(), u8::try_from(i).unwrap() + 1);
let _ = graph.insert_tx(tx.clone());
if *confirmed {
let height = 100 + u32::try_from(i).unwrap();
let _ = graph.insert_anchor(
tx.compute_txid(),
ConfirmationBlockTime {
block_id: BlockId { height, hash },
confirmation_time: 1000 + u64::from(height),
},
);
cp = cp.insert(BlockId { height, hash });
} else {
let _ = graph.insert_seen_at(tx.compute_txid(), 5000);
}
ops.push(OutPoint::new(tx.compute_txid(), 0));
}
let mut last = std::collections::BTreeMap::new();
last.insert(KeychainKind::External, 0);
wallet
.vault_wallet
.apply_update(bdk_wallet::Update {
tx_update: graph.into(),
chain: Some(cp),
last_active_indices: last,
})
.unwrap();
ops
}
#[test]
fn get_balance_excludes_inscribed_from_spendable() {
let mut w = unified();
let ops = fund(&mut w, &[(10_000, true), (50_000, true)]);
w.inscribed_utxos.insert(ops[0]);
let bal = w.get_balance();
assert_eq!(
bal.total.confirmed.to_sat(),
60_000,
"raw total counts everything"
);
assert_eq!(
bal.spendable.confirmed.to_sat(),
50_000,
"spendable excludes the inscribed UTXO"
);
assert_eq!(bal.inscribed, 10_000, "inscribed value = total - spendable");
}
#[test]
fn get_balance_counts_unconfirmed_as_trusted_pending() {
let mut w = unified();
fund(&mut w, &[(30_000, false)]);
let bal = w.get_balance();
assert_eq!(bal.spendable.confirmed.to_sat(), 0);
assert_eq!(bal.spendable.trusted_pending.to_sat(), 30_000);
}
#[test]
fn dapp_candidates_include_known_utxos_before_active_index() {
let mut w = WalletBuilder::from_seed(Network::Regtest, Seed64::from_array([9u8; 64]))
.with_scheme(AddressScheme::Unified)
.with_derivation_mode(DerivationMode::Index)
.with_account_index(5)
.build()
.unwrap();
let _revealed: Vec<_> = w
.vault_wallet
.reveal_addresses_to(KeychainKind::External, 2)
.collect();
let funded_address = w
.vault_wallet
.peek_address(KeychainKind::External, 2)
.address
.to_string();
let tx = dummy_tx(
42_000,
w.vault_wallet
.peek_address(KeychainKind::External, 2)
.address
.script_pubkey(),
42,
);
let mut graph = TxGraph::<ConfirmationBlockTime>::default();
let hash = BlockHash::all_zeros();
let _ = graph.insert_tx(tx.clone());
let _ = graph.insert_anchor(
tx.compute_txid(),
ConfirmationBlockTime {
block_id: BlockId { height: 120, hash },
confirmation_time: 1_200,
},
);
let mut last = std::collections::BTreeMap::new();
last.insert(KeychainKind::External, 2);
w.vault_wallet
.apply_update(bdk_wallet::Update {
tx_update: graph.into(),
chain: None,
last_active_indices: last,
})
.unwrap();
w.apply_verified_ordinals_update(Vec::new(), HashSet::new(), Vec::new());
let funded = w
.dapp_address_candidates(Some(3))
.into_iter()
.find(|candidate| candidate.address == funded_address)
.expect("funded older address should be selectable");
assert_eq!(funded.derivation_index, 2);
assert_eq!(funded.clean_btc_sats, 42_000);
assert!(funded.eligible_payment);
assert!(w.sign_message(&funded.address, "connect").is_ok());
}
#[test]
fn reset_sync_state_rebuilds_empty_wallet() {
let mut w = unified();
fund(&mut w, &[(40_000, true)]);
assert_eq!(w.get_balance().total.confirmed.to_sat(), 40_000);
w.reset_sync_state().expect("reset");
assert_eq!(
w.get_balance().total.confirmed.to_sat(),
0,
"reset rebuilds wallets from descriptors, dropping synced txs"
);
}
#[test]
fn export_changeset_succeeds_for_funded_wallet() {
let mut w = unified();
fund(&mut w, &[(40_000, true)]);
assert!(w.export_changeset().is_ok());
}
#[test]
fn public_key_getters_return_valid_hex() {
let w = unified();
let tap = w.get_taproot_public_key(0).expect("taproot pubkey");
assert!(hex::decode(&tap).is_ok(), "taproot pubkey is hex: {tap}");
assert!(
matches!(tap.len(), 64 | 66),
"x-only/compressed pubkey hex: {tap}"
);
let dual = WalletBuilder::from_seed(Network::Regtest, Seed64::from_array([0u8; 64]))
.with_scheme(AddressScheme::Dual)
.build()
.unwrap();
let pay = dual.get_payment_public_key(0).expect("payment pubkey");
assert!(hex::decode(&pay).is_ok(), "payment pubkey is hex: {pay}");
}
#[test]
fn pairing_secret_key_is_hex64() {
let w = unified();
let secret = w.get_pairing_secret_key_hex().expect("pairing secret");
assert_eq!(secret.len(), 64);
assert!(hex::decode(&secret).is_ok());
}
#[test]
fn sign_message_signs_own_address_and_rejects_foreign() {
let w = unified();
let own = w.peek_taproot_address(0).to_string();
assert!(
w.sign_message(&own, "gm").is_ok(),
"wallet signs its own address"
);
let other = WalletBuilder::from_seed(Network::Regtest, Seed64::from_array([7u8; 64]))
.with_scheme(AddressScheme::Unified)
.build()
.unwrap();
let foreign = other.peek_taproot_address(0).to_string();
assert!(
w.sign_message(&foreign, "gm").is_err(),
"cannot sign a foreign address"
);
}
#[test]
fn bip322_simple_signature_is_hex() {
let w = unified();
let own = w.peek_taproot_address(0).to_string();
let sig = w
.sign_bip322_simple_hex(&own, "hello bip322")
.expect("bip322 sig");
assert!(hex::decode(&sig).is_ok() && !sig.is_empty());
}
#[test]
fn fresh_wallet_requires_full_scan_and_reveals_taproot_address() {
let mut w = unified();
assert!(w.needs_full_scan(), "fresh wallet needs a full scan");
let addr = w.next_taproot_address().expect("address");
assert_eq!(
addr.address_type(),
Some(bdk_wallet::bitcoin::AddressType::P2tr)
);
}
#[test]
fn prepare_requests_forces_full_scan_when_requested() {
use crate::builder::SyncRequestType;
let w = unified();
let reqs = w.prepare_requests(true);
assert!(matches!(reqs.taproot, SyncRequestType::Full(_)));
}
}