use bitcoin::{Amount, Script};
use serde::{Deserialize, Serialize};
use crate::types::{ScriptType, TxNode};
#[must_use]
pub fn classify_script(script: &Script) -> ScriptType {
if script.is_p2pkh() {
ScriptType::P2pkh
} else if script.is_p2sh() {
ScriptType::P2sh
} else if script.is_p2wpkh() {
ScriptType::P2wpkh
} else if script.is_p2wsh() {
ScriptType::P2wsh
} else if script.is_p2tr() {
ScriptType::P2tr
} else if script.is_multisig() {
ScriptType::BareMultisig
} else if script.is_op_return() {
ScriptType::OpReturn
} else {
ScriptType::Unknown
}
}
#[must_use]
pub fn compute_fee(tx: &TxNode) -> Option<Amount> {
let total_in = tx
.inputs
.iter()
.try_fold(Amount::ZERO, |acc, input| acc.checked_add(input.value?))?;
let total_out = tx
.outputs
.iter()
.try_fold(Amount::ZERO, |acc, output| acc.checked_add(output.value))?;
total_in.checked_sub(total_out)
}
#[must_use]
pub fn compute_feerate(fee: Amount, vsize: u64) -> f64 {
if vsize == 0 {
return 0.0;
}
fee.to_sat() as f64 / vsize as f64
}
#[must_use]
pub fn is_rbf_signaling(tx: &TxNode) -> bool {
tx.inputs.iter().any(|input| input.sequence < 0xFFFFFFFE)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LocktimeInfo {
pub raw: u32,
pub kind: LocktimeKind,
pub active: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LocktimeKind {
Disabled,
BlockHeight,
Timestamp,
}
#[must_use]
pub fn locktime_info(locktime: u32, has_non_final_sequence: bool) -> LocktimeInfo {
if locktime == 0 {
return LocktimeInfo {
raw: 0,
kind: LocktimeKind::Disabled,
active: false,
};
}
let kind = if locktime < 500_000_000 {
LocktimeKind::BlockHeight
} else {
LocktimeKind::Timestamp
};
LocktimeInfo {
raw: locktime,
kind,
active: has_non_final_sequence,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_util::{make_input, make_output, make_tx_node};
use crate::types::TxInput;
#[test]
fn compute_fee_basic() {
let tx = make_tx_node(
vec![make_input(Some(5000), 0xFFFFFFFE)],
vec![make_output(3000)],
140,
);
let fee = compute_fee(&tx).unwrap();
assert_eq!(fee, Amount::from_sat(2000));
}
#[test]
fn compute_fee_multiple_inputs_outputs() {
let tx = make_tx_node(
vec![
make_input(Some(5000), 0xFFFFFFFE),
make_input(Some(3000), 0xFFFFFFFE),
],
vec![make_output(4000), make_output(2000)],
200,
);
let fee = compute_fee(&tx).unwrap();
assert_eq!(fee, Amount::from_sat(2000));
}
#[test]
fn compute_fee_returns_none_when_input_value_missing() {
let tx = make_tx_node(
vec![
make_input(Some(5000), 0xFFFFFFFE),
make_input(None, 0xFFFFFFFE),
],
vec![make_output(3000)],
140,
);
assert!(compute_fee(&tx).is_none());
}
#[test]
fn compute_fee_returns_none_for_coinbase() {
let tx = make_tx_node(
vec![TxInput {
prevout: None,
sequence: 0xFFFFFFFF,
value: None,
script_type: None,
}],
vec![make_output(50_0000_0000)],
140,
);
assert!(compute_fee(&tx).is_none());
}
#[test]
fn compute_feerate_basic() {
let rate = compute_feerate(Amount::from_sat(1000), 140);
let expected = 1000.0 / 140.0;
assert!((rate - expected).abs() < f64::EPSILON);
}
#[test]
fn compute_feerate_zero_vsize() {
let rate = compute_feerate(Amount::from_sat(1000), 0);
assert_eq!(rate, 0.0);
}
#[test]
fn rbf_signaling_with_non_final_sequence() {
let tx = make_tx_node(
vec![make_input(Some(5000), 0xFFFFFFFD)],
vec![make_output(3000)],
140,
);
assert!(is_rbf_signaling(&tx));
}
#[test]
fn no_rbf_signaling_when_sequence_is_final() {
let tx = make_tx_node(
vec![make_input(Some(5000), 0xFFFFFFFF)],
vec![make_output(3000)],
140,
);
assert!(!is_rbf_signaling(&tx));
}
#[test]
fn no_rbf_signaling_when_sequence_is_opt_out() {
let tx = make_tx_node(
vec![make_input(Some(5000), 0xFFFFFFFE)],
vec![make_output(3000)],
140,
);
assert!(!is_rbf_signaling(&tx));
}
#[test]
fn locktime_zero_is_disabled() {
let info = locktime_info(0, true);
assert_eq!(info.kind, LocktimeKind::Disabled);
assert!(!info.active);
}
#[test]
fn locktime_block_height() {
let info = locktime_info(800_000, true);
assert_eq!(info.kind, LocktimeKind::BlockHeight);
assert!(info.active);
}
#[test]
fn locktime_timestamp() {
let info = locktime_info(1_700_000_000, true);
assert_eq!(info.kind, LocktimeKind::Timestamp);
assert!(info.active);
}
#[test]
fn locktime_inactive_when_all_sequences_final() {
let info = locktime_info(800_000, false);
assert_eq!(info.kind, LocktimeKind::BlockHeight);
assert!(!info.active);
}
#[test]
fn classify_p2wpkh_script() {
let script = bitcoin::ScriptBuf::from_bytes(vec![
0x00, 0x14, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c,
0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14,
]);
assert_eq!(classify_script(script.as_script()), ScriptType::P2wpkh);
}
#[test]
fn classify_p2tr_script() {
let mut bytes = vec![0x51, 0x20];
bytes.extend_from_slice(&[0xAA; 32]);
let script = bitcoin::ScriptBuf::from_bytes(bytes);
assert_eq!(classify_script(script.as_script()), ScriptType::P2tr);
}
#[test]
fn classify_op_return_script() {
let script = bitcoin::ScriptBuf::from_bytes(vec![0x6a, 0x04, 0xde, 0xad, 0xbe, 0xef]);
assert_eq!(classify_script(script.as_script()), ScriptType::OpReturn);
}
#[test]
fn classify_unknown_script() {
let script = bitcoin::ScriptBuf::new();
assert_eq!(classify_script(script.as_script()), ScriptType::Unknown);
}
}