use crate::ordinals::shield::{analyze_psbt, WarningLevel};
use bitcoin::psbt::{Input, Psbt};
use bitcoin::transaction::Transaction;
use bitcoin::{Amount, OutPoint, ScriptBuf, TxOut, Txid};
use std::collections::HashMap;
use std::str::FromStr;
fn create_dummy_psbt(inputs: &[(u64, Option<u64>)], outputs: &[u64]) -> Psbt {
let mut tx = Transaction {
version: bitcoin::transaction::Version::TWO,
lock_time: bitcoin::locktime::absolute::LockTime::ZERO,
input: vec![],
output: vec![],
};
let mut psbt_inputs = vec![];
for (i, (value, _inscription_offset)) in inputs.iter().enumerate() {
tx.input.push(bitcoin::TxIn {
previous_output: OutPoint {
txid: Txid::from_str(
"0000000000000000000000000000000000000000000000000000000000000000",
)
.unwrap(),
vout: u32::try_from(i).unwrap(),
},
script_sig: ScriptBuf::new(),
sequence: bitcoin::Sequence::MAX,
witness: bitcoin::Witness::default(),
});
let input = Input {
witness_utxo: Some(TxOut {
value: Amount::from_sat(*value),
script_pubkey: ScriptBuf::new(), }),
..Default::default()
};
psbt_inputs.push(input);
}
for value in outputs {
tx.output.push(TxOut {
value: Amount::from_sat(*value),
script_pubkey: ScriptBuf::new(), });
}
let mut psbt = Psbt::from_unsigned_tx(tx).unwrap();
psbt.inputs = psbt_inputs;
psbt
}
#[test]
fn test_tight_squeeze_burn() {
let inputs = vec![(10_000, Some(9_999))];
let outputs = vec![9_999];
let psbt = create_dummy_psbt(&inputs, &outputs);
let mut known_inscriptions = HashMap::new();
known_inscriptions.insert(
(psbt.unsigned_tx.input[0].previous_output.txid, 0),
vec![("Inscription 0".to_string(), 9_999_u64)],
);
let result = analyze_psbt(&psbt, &known_inscriptions, bitcoin::Network::Regtest).unwrap();
assert_eq!(
result.warning_level,
WarningLevel::Danger,
"Inscription at 9,999 should be burned if output is 9,999"
);
assert!(result
.inscriptions_burned
.contains(&"Inscription 0".to_string()));
}
#[test]
fn test_survivor_boundary() {
let inputs = vec![(10_000, Some(9_999))];
let outputs = vec![10_000];
let psbt = create_dummy_psbt(&inputs, &outputs);
let mut known_inscriptions = HashMap::new();
known_inscriptions.insert(
(psbt.unsigned_tx.input[0].previous_output.txid, 0),
vec![("Inscription 0".to_string(), 9_999_u64)],
);
let result = analyze_psbt(&psbt, &known_inscriptions, bitcoin::Network::Regtest).unwrap();
assert_eq!(result.warning_level, WarningLevel::Safe);
assert!(result.inscriptions_burned.is_empty());
}
#[test]
fn test_multi_inscription_utxo() {
let inputs = vec![(20_000, None)];
let outputs = vec![10_000, 10_000];
let psbt = create_dummy_psbt(&inputs, &outputs);
let mut known_inscriptions = HashMap::new();
known_inscriptions.insert(
(psbt.unsigned_tx.input[0].previous_output.txid, 0),
vec![
("Inscription 0".to_string(), 5_000),
("Inscription 1".to_string(), 15_000),
],
);
let result = analyze_psbt(&psbt, &known_inscriptions, bitcoin::Network::Regtest).unwrap();
let a_dest = result
.inscription_destinations
.get("Inscription 0")
.unwrap();
assert_eq!(a_dest.vout, Some(0));
let b_dest = result
.inscription_destinations
.get("Inscription 1")
.unwrap();
assert_eq!(b_dest.vout, Some(1));
}
#[test]
fn test_mixed_inputs_order() {
let inputs = vec![(5_000, None), (5_000, Some(0))];
let outputs = vec![6_000];
let psbt = create_dummy_psbt(&inputs, &outputs);
let mut known_inscriptions = HashMap::new();
known_inscriptions.insert(
(psbt.unsigned_tx.input[1].previous_output.txid, 1),
vec![("Inscription 0".to_string(), 0)],
);
let result = analyze_psbt(&psbt, &known_inscriptions, bitcoin::Network::Regtest).unwrap();
let dest = result
.inscription_destinations
.get("Inscription 0")
.unwrap();
assert_eq!(dest.vout, Some(0));
}
#[test]
fn test_blind_spot_missing_witness() {
let mut psbt = create_dummy_psbt(&[(10_000, None), (10_000, Some(0))], &[20_000]);
psbt.inputs[0].witness_utxo = None;
let mut known_inscriptions = HashMap::new();
known_inscriptions.insert(
(psbt.unsigned_tx.input[1].previous_output.txid, 1),
vec![("Inscription 0".to_string(), 0)],
);
let result = analyze_psbt(&psbt, &known_inscriptions, bitcoin::Network::Regtest);
assert!(result.is_err(), "Should fail if input value is unknown");
}
#[test]
fn test_burial_warning() {
let inputs = vec![(546, Some(0)), (100_000_000, None)];
let outputs = vec![100_000_546];
let psbt = create_dummy_psbt(&inputs, &outputs);
let mut known_inscriptions = HashMap::new();
known_inscriptions.insert(
(psbt.unsigned_tx.input[0].previous_output.txid, 0),
vec![("Inscription 0".to_string(), 0)],
);
let result = analyze_psbt(&psbt, &known_inscriptions, bitcoin::Network::Regtest).unwrap();
assert_eq!(
result.warning_level,
WarningLevel::Warn,
"Should warn when burying inscription"
);
}
#[test]
fn test_random_offset_pointer() {
let inputs = vec![(10_000, Some(3_500))];
let outputs = vec![3_500, 6_500];
let psbt = create_dummy_psbt(&inputs, &outputs);
let mut known_inscriptions = HashMap::new();
known_inscriptions.insert(
(psbt.unsigned_tx.input[0].previous_output.txid, 0),
vec![("Inscription 0".to_string(), 3_500)],
);
let result = analyze_psbt(&psbt, &known_inscriptions, bitcoin::Network::Regtest).unwrap();
let dest = result
.inscription_destinations
.get("Inscription 0")
.unwrap();
assert_eq!(dest.vout, Some(1));
assert_eq!(dest.offset, 0);
}
#[test]
fn test_split_transaction_safe() {
let inputs = vec![(100_000, None)];
let outputs = vec![10_000, 10_000, 79_000];
let psbt = create_dummy_psbt(&inputs, &outputs);
let known_inscriptions = HashMap::new();
let result = analyze_psbt(&psbt, &known_inscriptions, bitcoin::Network::Regtest)
.expect("Should analyze successfully even if no inscriptions");
assert_eq!(result.warning_level, WarningLevel::Safe);
assert!(result.inscription_destinations.is_empty());
assert!(result.inscriptions_burned.is_empty());
assert_eq!(result.fee_sats, 1_000);
}
#[test]
fn test_missing_witness_utxo_error() {
let mut psbt = create_dummy_psbt(&[(100_000, None)], &[99_000]);
psbt.inputs[0].witness_utxo = None;
let known_inscriptions = HashMap::new();
let result = analyze_psbt(&psbt, &known_inscriptions, bitcoin::Network::Regtest);
assert!(result.is_err());
let err = result.unwrap_err();
match err {
crate::ordinals::error::OrdError::RequestFailed(msg) => {
assert!(
msg.contains("missing witness_utxo data"),
"Error message should mention missing witness_utxo"
);
}
}
}
#[test]
fn test_fallback_to_non_witness_utxo() {
let mut psbt = create_dummy_psbt(&[(10_000, None)], &[9_000]);
psbt.inputs[0].witness_utxo = None;
let legacy_tx = Transaction {
version: bitcoin::transaction::Version::TWO,
lock_time: bitcoin::locktime::absolute::LockTime::ZERO,
input: vec![], output: vec![TxOut {
value: Amount::from_sat(10_000),
script_pubkey: ScriptBuf::new(),
}],
};
psbt.inputs[0].non_witness_utxo = Some(legacy_tx);
let known_inscriptions = HashMap::new();
let result = analyze_psbt(&psbt, &known_inscriptions, bitcoin::Network::Regtest)
.expect("Should succeed by falling back to non_witness_utxo");
assert_eq!(result.warning_level, WarningLevel::Safe);
assert_eq!(result.fee_sats, 1_000);
}
#[test]
fn test_analyze_sighash_warning() {
let inputs = vec![(10_000, None)];
let outputs = vec![9_000];
let mut psbt = create_dummy_psbt(&inputs, &outputs);
psbt.inputs[0].sighash_type = Some(bitcoin::psbt::PsbtSighashType::from_u32(2));
let known_inscriptions: HashMap<(Txid, u32), Vec<(String, u64)>> = HashMap::new();
let result = analyze_psbt(&psbt, &known_inscriptions, bitcoin::Network::Regtest)
.expect("Analysis should succeed");
assert_eq!(
result.warning_level,
WarningLevel::Danger,
"SIGHASH_NONE should trigger Danger"
);
assert!(!result.warnings.is_empty(), "Should have warnings");
assert!(
result.warnings[0].contains("SIGHASH_NONE"),
"Warning should mention SIGHASH_NONE"
);
}