diff --git a/django/finances/tx_math.py b/django/finances/tx_math.py new file mode 100644 index 0000000..d1bbccd --- /dev/null +++ b/django/finances/tx_math.py @@ -0,0 +1,226 @@ +# Constants +P2PKH_IN_SIZE = 148 +P2PKH_OUT_SIZE = 34 + +P2SH_OUT_SIZE = 32 +P2SH_P2WPKH_OUT_SIZE = 32 +P2SH_P2WSH_OUT_SIZE = 32 + +P2WPKH_OUT_SIZE = 31 +P2WSH_OUT_SIZE = P2TR_OUT_SIZE = 43 + +PUBKEY_SIZE = 33 +SIGNATURE_SIZE = 72 + + +def get_size_of_var_int(length): + if length <= 252: + return 1 + elif length <= 0xffff: + return 3 + elif length <= 0xffffffff: + return 5 + else: + return 9 + + +def get_size_of_script_length_element(length): + if length < 75: + return 1 + elif length <= 255: + return 2 + elif length <= 65535: + return 3 + elif length <= 4294967295: + return 5 + else: + raise ValueError("Script too large") + + +def get_input_size(input_attr): + """Return (base_size, witness_size) for a single input.""" + script_type = input_attr['input_script'] + m = input_attr.get('input_m', 0) + n = input_attr.get('input_n', 0) + + if script_type == "P2PKH": + return P2PKH_IN_SIZE, 0 + elif script_type == "P2SH": + # Redeem script: OP_m + n_pubkeys + OP_n + OP_CHECKMULTISIG + redeem_script_size = 1 + n * (1 + PUBKEY_SIZE) + 1 + 1 + # scriptSig: OP_0 + m_sigs + push_opcode + redeem_script + script_sig_size = 1 + m * (1 + SIGNATURE_SIZE) + get_size_of_script_length_element(redeem_script_size) + redeem_script_size + input_base_size = 32 + 4 + get_size_of_var_int(script_sig_size) + script_sig_size + 4 + return input_base_size, 0 + elif script_type == "P2SH-P2WPKH": + input_base_size = 32 + 4 + 1 + 23 + 4 + input_witness_size = 107 + return input_base_size, input_witness_size + elif script_type == "P2WPKH": + input_base_size = 32 + 4 + 1 + 4 + input_witness_size = 107 + return input_base_size, input_witness_size + elif script_type == "P2WSH": + input_base_size = 32 + 4 + 1 + 4 + witness_script_size = 1 + n * (1 + PUBKEY_SIZE) + 1 + 1 + num_stack_items = 1 + m + 1 # OP_0 + m sigs + witness script + input_witness_size = ( + get_size_of_var_int(num_stack_items) + + 1 + # OP_0 length + m * (1 + SIGNATURE_SIZE) + + get_size_of_var_int(witness_script_size) + + witness_script_size + ) + return input_base_size, input_witness_size + elif script_type == "P2TR": + input_base_size = 32 + 4 + 1 + 4 + input_witness_size = 65 + return input_base_size, input_witness_size + else: + raise ValueError(f"Unsupported input script type: {script_type}") + + +def calculate_transaction_size(inputs, output_counts): + """ + inputs: list of dicts with keys: input_script, input_m, input_n + output_counts: dict with counts per output type + """ + total_base = 0 + total_witness = 0 + + + # Total inputs / outputs + input_count = len(inputs) + output_count = sum(output_counts.values()) + + # Transaction overhead: version(4) + varints + locktime(4) + tx_base_size = 4 + get_size_of_var_int(input_count) + get_size_of_var_int(output_count) + 4 + + + # Segwit marker + flag if any input is segwit + has_witness = any(inp['input_script'] in ["P2SH-P2WPKH", "P2WPKH", "P2WSH", "P2TR"] for inp in inputs) + if has_witness: + total_witness += 2 # marker + flag + + for inp in inputs: + base, witness = get_input_size(inp) + total_base += base + total_witness += witness + + # Sum output sizes + output_size = (P2PKH_OUT_SIZE * output_counts.get('p2pkh', 0) + + P2SH_OUT_SIZE * output_counts.get('p2sh', 0) + + P2SH_P2WPKH_OUT_SIZE * output_counts.get('p2sh_p2wpkh', 0) + + P2SH_P2WSH_OUT_SIZE * output_counts.get('p2sh_p2wsh', 0) + + P2WPKH_OUT_SIZE * output_counts.get('p2wpkh', 0) + + P2WSH_OUT_SIZE * output_counts.get('p2wsh', 0) + + P2TR_OUT_SIZE * output_counts.get('p2tr', 0)) + + + # Total base size + tx_total_base_size = tx_base_size + total_base + output_size + + # Transaction weight and vbytes + tx_weight = tx_total_base_size * 4 + total_witness + tx_vbytes = tx_weight / 4 + + # Raw bytes (base + witness discounted by 1/4) + tx_bytes = tx_total_base_size + total_witness / 4 + + return { + 'txBytes': round(tx_bytes), + 'txVBytes': round(tx_vbytes), + 'txWeight': tx_weight + } + + + +def calculate_fee(tx_vbytes, fee_rate_sats_per_vbyte): + """ + tx_vbytes: virtual size from calculate_transaction_size() + fee_rate_sats_per_vbyte: fee rate in sats per vbyte + """ + return round(tx_vbytes * fee_rate_sats_per_vbyte) + + + +def run_tests(): + fee_rate = 20 # sats per vbyte + test_cases = [ + # 1. Single P2PKH input -> single P2PKH output + { + 'inputs': [{'input_script': 'P2PKH'}], + 'outputs': {'p2pkh': 1}, + 'description': 'Single P2PKH -> P2PKH' + }, + # 2. Two P2WPKH inputs -> two P2WPKH outputs + { + 'inputs': [{'input_script': 'P2WPKH'}, {'input_script': 'P2WPKH'}], + 'outputs': {'p2wpkh': 2}, + 'description': 'Two P2WPKH -> Two P2WPKH' + }, + # 3. Single P2SH 2-of-3 multisig input -> two P2PKH outputs + { + 'inputs': [{'input_script': 'P2SH', 'input_m': 2, 'input_n': 3}], + 'outputs': {'p2pkh': 2}, + 'description': 'P2SH 2-of-3 multisig -> 2x P2PKH' + }, + # 4. Mixed inputs: P2PKH + P2WPKH + P2TR -> P2PKH + P2WPKH + { + 'inputs': [ + {'input_script': 'P2PKH'}, + {'input_script': 'P2WPKH'}, + {'input_script': 'P2TR'} + ], + 'outputs': {'p2pkh': 1, 'p2wpkh': 1}, + 'description': 'Mixed inputs -> mixed outputs' + }, + # 5. Two P2WSH multisig inputs -> P2WSH outputs + { + 'inputs': [ + {'input_script': 'P2WSH', 'input_m': 2, 'input_n': 3}, + {'input_script': 'P2WSH', 'input_m': 1, 'input_n': 2} + ], + 'outputs': {'p2wsh': 2}, + 'description': 'Two P2WSH -> Two P2WSH' + } + ] + + for idx, test in enumerate(test_cases, 1): + # Step 1: calculate size + tx_size = calculate_transaction_size(test['inputs'], test['outputs']) + + # Step 2: calculate fee + fee_sats = calculate_fee(tx_size['txVBytes'], fee_rate) + + print(f"Test {idx}: {test['description']}") + print(f" txBytes: {tx_size['txBytes']}, txVBytes: {tx_size['txVBytes']}, txWeight: {tx_size['txWeight']}") + print(f" Fee (@ {fee_rate} sats/vbyte): {fee_sats} sats\n") + + + + + + +# Example usage +if __name__ == "__main__": + inputs = [ + {'input_script': 'P2PKH'}, + {'input_script': 'P2WPKH'}, + {'input_script': 'P2SH', 'input_m': 2, 'input_n': 3}, + ] + output_counts = { + 'p2pkh': 1, + 'p2sh': 0, + 'p2sh_p2wpkh': 1, + 'p2sh_p2wsh': 0, + 'p2wpkh': 0, + 'p2wsh': 0, + 'p2tr': 0 + } + + tx_size = calculate_transaction_size(inputs, output_counts) + print(tx_size) + + print("*"*80) + run_tests() diff --git a/django/finances/tx_math_tests.py b/django/finances/tx_math_tests.py new file mode 100644 index 0000000..4ad610e --- /dev/null +++ b/django/finances/tx_math_tests.py @@ -0,0 +1,48 @@ +def run_tests(): + test_cases = [ + # 1. Single P2PKH input -> single P2PKH output + { + 'inputs': [{'input_script': 'P2PKH'}], + 'outputs': {'p2pkh': 1}, + 'description': 'Single P2PKH -> P2PKH' + }, + # 2. Two P2WPKH inputs -> two P2WPKH outputs + { + 'inputs': [{'input_script': 'P2WPKH'}, {'input_script': 'P2WPKH'}], + 'outputs': {'p2wpkh': 2}, + 'description': 'Two P2WPKH -> Two P2WPKH' + }, + # 3. Single P2SH 2-of-3 multisig input -> two P2PKH outputs + { + 'inputs': [{'input_script': 'P2SH', 'input_m': 2, 'input_n': 3}], + 'outputs': {'p2pkh': 2}, + 'description': 'P2SH 2-of-3 multisig -> 2x P2PKH' + }, + # 4. Mixed inputs: P2PKH + P2WPKH + P2TR -> P2PKH + P2WPKH + { + 'inputs': [ + {'input_script': 'P2PKH'}, + {'input_script': 'P2WPKH'}, + {'input_script': 'P2TR'} + ], + 'outputs': {'p2pkh': 1, 'p2wpkh': 1}, + 'description': 'Mixed inputs -> mixed outputs' + }, + # 5. Two P2WSH multisig inputs -> P2WSH outputs + { + 'inputs': [ + {'input_script': 'P2WSH', 'input_m': 2, 'input_n': 3}, + {'input_script': 'P2WSH', 'input_m': 1, 'input_n': 2} + ], + 'outputs': {'p2wsh': 2}, + 'description': 'Two P2WSH -> Two P2WSH' + } + ] + + for idx, test in enumerate(test_cases, 1): + result = calculate_transaction_size(test['inputs'], test['outputs']) + print(f"Test {idx}: {test['description']}") + print(f" txBytes: {result['txBytes']}, txVBytes: {result['txVBytes']}, txWeight: {result['txWeight']}\n") + +if __name__ == "__main__": + run_tests()