UNPKG

@ordinalsbot/bitcoin-fee-estimator

Version:

A library for calculating Bitcoin transaction fees

237 lines (214 loc) 5.92 kB
import { ScriptType } from "../types"; import { P2PKH_INPUT_SIZE, P2PKH_OUTPUT_SIZE, P2SH_OUTPUT_SIZE, P2SH_P2WPKH_INPUT_SIZE, P2WPKH_INPUT_SIZE, P2WSH_OUTPUT_SIZE, P2TR_OUTPUT_SIZE, P2TR_INPUT_SIZE, P2TR_INSCRIPTION_INPUT_SIZE, SIGNATURE_SIZE, MULTISIG_REDEEM_SCRIPT_SIZE, OUTPOINT_SIZE, SEQUENCE_SIZE, P2WPKH_OUTPUT_SIZE, P2SH_P2WPKH_OUTPUT_SIZE, } from "../constants"; import { encodingLength } from "varuint-bitcoin"; import { bech32ConvertWords, bech32Decode } from "../utils/bech32"; import { bs58Decode } from "../utils/bs58"; export function getScriptLengthSize(length: number): number { if (length < 75) return 1; if (length <= 255) return 2; if (length <= 65535) return 3; if (length <= 4294967295) return 5; throw new Error("Script size too large"); } export function getScriptTypeFromAddress(address: string): ScriptType { if (!address || typeof address !== "string") { return ScriptType.UNKNOWN; } const addressLower = address.toLowerCase(); if (addressLower.startsWith("bc1") || addressLower.startsWith("tb1")) { try { const decoded = bech32Decode(addressLower); if (!decoded) return ScriptType.UNKNOWN; const data = bech32ConvertWords(decoded.words, 5, 8, false); const version = decoded.words[0]; if ( decoded.encoding === "bech32m" && version === 1 && data.length === 33 ) { return ScriptType.P2TR; } if (decoded.encoding === "bech32" && version === 0) { if (data.length === 20) return ScriptType.P2WPKH; if (data.length === 33) return ScriptType.P2WSH; } } catch { return ScriptType.UNKNOWN; } return ScriptType.UNKNOWN; } try { const decoded = bs58Decode(address); const version = decoded[0]; if (version === 0x00 || version === 0x6f) return ScriptType.P2PKH; if (version === 0x05 || version === 0xc4) return ScriptType.P2SH; return ScriptType.UNKNOWN; } catch { return ScriptType.UNKNOWN; } } export const detectOutputScriptType = ( scriptPubKey: Buffer, redeemScript?: string | Buffer, ): ScriptType => { if (!scriptPubKey || scriptPubKey.length === 0) { return ScriptType.UNKNOWN; } const scriptLength = scriptPubKey.length; // OP_RETURN if (scriptPubKey[0] === 0x6a) { return ScriptType.OP_RETURN; } // P2PK (compressed or uncompressed) if ( scriptLength === 35 || (scriptLength === 67 && scriptPubKey[scriptLength - 1] === 0xac) ) { return ScriptType.P2PK; } // P2PKH if (scriptLength === 25) { if ( scriptPubKey[0] === 0x76 && // OP_DUP scriptPubKey[1] === 0xa9 && // OP_HASH160 scriptPubKey[2] === 0x14 && // OP_PUSH20 scriptPubKey[23] === 0x88 && // OP_EQUALVERIFY scriptPubKey[24] === 0xac // OP_CHECKSIG ) { return ScriptType.P2PKH; } return ScriptType.UNKNOWN; } // P2SH if (scriptLength === 23) { if ( scriptPubKey[0] === 0xa9 && // OP_HASH160 scriptPubKey[1] === 0x14 && // OP_PUSH20 scriptPubKey[22] === 0x87 // OP_EQUAL ) { if (redeemScript) { const redeemBuffer = typeof redeemScript === "string" ? Buffer.from(redeemScript, "hex") : redeemScript; // redeem script: P2WPKH if ( redeemBuffer.length === 22 && redeemBuffer[0] === 0x00 && redeemBuffer[1] === 0x14 ) { return ScriptType.P2SH_P2WPKH; } // redeem script: P2WSH) if ( redeemBuffer.length === 34 && redeemBuffer[0] === 0x00 && redeemBuffer[1] === 0x20 ) { return ScriptType.P2SH_P2WSH; } } return ScriptType.P2SH; } return ScriptType.UNKNOWN; } // P2WPKH if (scriptLength === 22) { if ( scriptPubKey[0] === 0x00 && // OP_0 scriptPubKey[1] === 0x14 // OP_PUSH20 ) { return ScriptType.P2WPKH; } return ScriptType.UNKNOWN; } // P2TR if ( scriptLength === 34 && scriptPubKey[0] === 0x51 && // OP_1 scriptPubKey[1] === 0x20 // OP_PUSH32 ) { return ScriptType.P2TR; } // P2WSH if (scriptLength === 34) { if ( scriptPubKey[0] === 0x00 && // OP_0 scriptPubKey[1] === 0x20 // OP_PUSH32 ) { return ScriptType.P2WSH; } } return ScriptType.UNKNOWN; }; export const estimateInputSize = (inputScriptType: ScriptType): number => { switch (inputScriptType) { case "P2PKH": return P2PKH_INPUT_SIZE; case "P2SH-P2WPKH": return P2SH_P2WPKH_INPUT_SIZE; case "P2WPKH": return P2WPKH_INPUT_SIZE; case "P2TR": return P2TR_INPUT_SIZE; case "P2TR-INSCRIPTION": return P2TR_INSCRIPTION_INPUT_SIZE; case "P2SH": { const scriptSigSize = 1 + // OP_0 1 * (1 + SIGNATURE_SIZE) + // Signature with length getScriptLengthSize(MULTISIG_REDEEM_SCRIPT_SIZE) + MULTISIG_REDEEM_SCRIPT_SIZE; return ( OUTPOINT_SIZE + encodingLength(scriptSigSize) + scriptSigSize + SEQUENCE_SIZE ); } case "P2SH-P2WSH": return P2SH_P2WPKH_INPUT_SIZE; case "P2WSH": return OUTPOINT_SIZE + SEQUENCE_SIZE; default: throw new Error(`Unsupported script type: ${inputScriptType}`); } }; export const getOutputSize = (outputScript: ScriptType): number => { switch (outputScript) { case "P2PKH": return P2PKH_OUTPUT_SIZE; case "P2SH": return P2SH_OUTPUT_SIZE; case "P2SH-P2WPKH": return P2SH_P2WPKH_OUTPUT_SIZE; case "P2SH-P2WSH": return P2SH_OUTPUT_SIZE; case "P2WPKH": return P2WPKH_OUTPUT_SIZE; case "P2TR": return P2TR_OUTPUT_SIZE; case "P2WSH": return P2WSH_OUTPUT_SIZE; case "OP_RETURN": return 0; default: return 67; // safe value } };