#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use crate::builder::{AddressScheme, Seed64, WalletBuilder};
use crate::ordinals::types::{Inscription, Satpoint};
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;
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 spend_tx(prev: OutPoint, value: u64, script_pubkey: ScriptBuf) -> Transaction {
Transaction {
version: bdk_wallet::bitcoin::transaction::Version::TWO,
lock_time: bdk_wallet::bitcoin::absolute::LockTime::ZERO,
input: vec![TxIn {
previous_output: prev,
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 fund(wallet: &mut bdk_wallet::Wallet, txs: &[(&Transaction, Option<u32>)], last_index: u32) {
let mut graph = TxGraph::<ConfirmationBlockTime>::default();
let hash = BlockHash::all_zeros();
let mut cp = wallet.latest_checkpoint();
for (tx, height) in txs {
let _ = graph.insert_tx((*tx).clone());
match height {
Some(h) => {
let _ = graph.insert_anchor(
(*tx).compute_txid(),
ConfirmationBlockTime {
block_id: BlockId { height: *h, hash },
confirmation_time: 1000 + u64::from(*h),
},
);
cp = cp.insert(BlockId { height: *h, hash });
}
None => {
let _ = graph.insert_seen_at((*tx).compute_txid(), 5000);
}
}
}
let mut last = std::collections::BTreeMap::new();
last.insert(KeychainKind::External, last_index);
wallet
.apply_update(bdk_wallet::Update {
tx_update: graph.into(),
chain: Some(cp),
last_active_indices: last,
})
.unwrap();
}
fn unified() -> crate::builder::ZincWallet {
WalletBuilder::from_seed(Network::Regtest, Seed64::from_array([0u8; 64]))
.with_scheme(AddressScheme::Unified)
.build()
.unwrap()
}
#[test]
fn receive_tx_is_labeled_and_amounts_match() {
let mut w = unified();
let addr = w.vault_wallet.reveal_next_address(KeychainKind::External).address;
let tx = dummy_tx(50_000, addr.script_pubkey(), 1);
fund(&mut w.vault_wallet, &[(&tx, Some(100))], 0);
let txs = w.get_transactions(50);
assert_eq!(txs.len(), 1);
assert_eq!(txs[0].tx_type, "receive");
assert_eq!(txs[0].amount_sats, 50_000);
assert!(txs[0].confirmation_time.is_some());
assert!(!txs[0].parent_txids.is_empty(), "funding input recorded as a parent txid");
}
#[test]
fn spend_tx_is_labeled_send_with_negative_amount() {
let mut w = unified();
let addr = w.vault_wallet.reveal_next_address(KeychainKind::External).address;
let spk = addr.script_pubkey();
let other = WalletBuilder::from_seed(Network::Regtest, Seed64::from_array([9u8; 64]))
.with_scheme(AddressScheme::Unified)
.build()
.unwrap();
let ext_spk = other.peek_taproot_address(0).script_pubkey();
let fund_tx = dummy_tx(50_000, spk, 1);
let spend = spend_tx(OutPoint::new(fund_tx.compute_txid(), 0), 40_000, ext_spk);
fund(&mut w.vault_wallet, &[(&fund_tx, Some(100)), (&spend, Some(101))], 0);
let txs = w.get_transactions(50);
let send = txs
.iter()
.find(|t| t.txid == spend.compute_txid().to_string())
.expect("spend tx present");
assert_eq!(send.tx_type, "send");
assert!(send.amount_sats < 0, "spend reduces balance: {}", send.amount_sats);
}
#[test]
fn sorts_unconfirmed_first_then_newest_confirmed() {
let mut w = unified();
let addr = w.vault_wallet.reveal_next_address(KeychainKind::External).address;
let spk = addr.script_pubkey();
let old = dummy_tx(10_000, spk.clone(), 1); let new = dummy_tx(20_000, spk.clone(), 2); let pending = dummy_tx(30_000, spk, 3); fund(
&mut w.vault_wallet,
&[(&old, Some(100)), (&new, Some(200)), (&pending, None)],
0,
);
let txs = w.get_transactions(50);
assert_eq!(txs.len(), 3);
assert_eq!(txs[0].txid, pending.compute_txid().to_string(), "unconfirmed first");
assert!(txs[0].confirmation_time.is_none());
assert_eq!(txs[1].txid, new.compute_txid().to_string(), "newest confirmed next");
assert_eq!(txs[2].txid, old.compute_txid().to_string(), "oldest confirmed last");
}
#[test]
fn limit_caps_the_number_of_results() {
let mut w = unified();
let addr = w.vault_wallet.reveal_next_address(KeychainKind::External).address;
let spk = addr.script_pubkey();
let a = dummy_tx(10_000, spk.clone(), 1);
let b = dummy_tx(20_000, spk.clone(), 2);
let c = dummy_tx(30_000, spk, 3);
fund(&mut w.vault_wallet, &[(&a, Some(100)), (&b, Some(101)), (&c, Some(102))], 0);
assert_eq!(w.get_transactions(2).len(), 2);
}
#[test]
fn dedup_merges_same_txid_across_dual_wallets() {
let mut w = WalletBuilder::from_seed(Network::Regtest, Seed64::from_array([0u8; 64]))
.with_scheme(AddressScheme::Dual)
.build()
.unwrap();
let vault_spk = w
.vault_wallet
.reveal_next_address(KeychainKind::External)
.address
.script_pubkey();
let pay_spk = w
.payment_wallet
.as_mut()
.unwrap()
.reveal_next_address(KeychainKind::External)
.address
.script_pubkey();
let mut tx = dummy_tx(10_000, vault_spk, 1);
tx.output.push(TxOut {
value: Amount::from_sat(7_000),
script_pubkey: pay_spk,
});
fund(&mut w.vault_wallet, &[(&tx, Some(100))], 0);
fund(w.payment_wallet.as_mut().unwrap(), &[(&tx, Some(100))], 0);
let txs = w.get_transactions(50);
assert_eq!(txs.len(), 1, "same txid from both wallets is deduplicated");
assert_eq!(txs[0].amount_sats, 17_000, "amounts from both branches are summed");
}
#[test]
fn inscriptions_are_attached_to_their_transaction() {
let mut w = unified();
let addr = w.vault_wallet.reveal_next_address(KeychainKind::External).address;
let tx = dummy_tx(10_000, addr.script_pubkey(), 1);
fund(&mut w.vault_wallet, &[(&tx, Some(100))], 0);
let op = OutPoint::new(tx.compute_txid(), 0);
w.inscriptions.push(Inscription {
id: "insc-xyz".to_string(),
number: 7,
satpoint: Satpoint { outpoint: op, offset: 0 },
content_type: Some("text/plain".to_string()),
value: Some(10_000),
content_length: None,
timestamp: None,
});
let txs = w.get_transactions(50);
assert_eq!(txs[0].inscriptions.len(), 1);
assert_eq!(txs[0].inscriptions[0].id, "insc-xyz");
assert_eq!(txs[0].inscriptions[0].number, 7);
}
}