@ordinalsbot/bitcoin-fee-estimator
Version:
A library for calculating Bitcoin transaction fees
237 lines (214 loc) • 5.92 kB
text/typescript
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
}
};