@ordinalsbot/bitcoin-fee-estimator
Version:
A library for calculating Bitcoin transaction fees
155 lines (140 loc) • 4.67 kB
text/typescript
import { getScriptTypeFromAddress } from "../common";
import { BatchMode, InscriptionFile, ScriptType } from "../types";
import { estimateTxFee } from "./baseTx";
export function estimateInscriptionFee(
fileSize: number,
count: number,
feeRate: number,
envelopeSize = 100,
batchMode: BatchMode = null,
): number {
const fileFee = Math.ceil((feeRate * (fileSize + envelopeSize)) / 4);
const commitCount = batchMode ? 1 : count;
let inputTypes = Array(2).fill(ScriptType.P2TR);
let outputTypes = Array(3).fill(ScriptType.P2TR);
const commitFee =
estimateTxFee(inputTypes, outputTypes, feeRate) * commitCount;
inputTypes = Array(1).fill(ScriptType.P2TR);
outputTypes = Array(1).fill(ScriptType.P2TR);
let revealFee = estimateTxFee(inputTypes, outputTypes, feeRate) * count;
if (batchMode === "separate-outputs") {
outputTypes = Array(count).fill(ScriptType.P2TR);
revealFee = estimateTxFee(inputTypes, outputTypes, feeRate);
} else if (batchMode === "shared-output") {
revealFee = estimateTxFee(inputTypes, outputTypes, feeRate);
}
return commitFee + revealFee + fileFee;
}
export function estimateDirectInscriptionFee(
feeRate: number,
receiverAddress: string,
files: InscriptionFile[] = [],
delegates = 0,
parentCount = 0,
delegatesAverageSize = 33,
parentAverageSize = 33,
): number {
const isFiles = files?.length > 0;
const isDelegates = !!delegates;
if ((isFiles && isDelegates) || (!isFiles && !isDelegates)) {
throw new Error("Use either 'files' or 'delegates'");
}
const receiverScriptType = getScriptTypeFromAddress(receiverAddress);
let envelopeSize = 0;
if (isFiles) {
for (let i = 0; i < files.length; i++) {
envelopeSize += getEnvelopeSize(
files[i],
delegates,
parentCount,
delegatesAverageSize,
parentAverageSize,
parentCount > 0 || i !== 0, // only false on first iter
);
}
}
if (isDelegates) {
for (let i = 0; i < delegates; i++) {
envelopeSize += getEnvelopeSize(
null,
delegates,
parentCount,
delegatesAverageSize,
parentAverageSize,
parentCount > 0 || i !== 0, // only false on first iter
);
}
}
const inscriptionFee = Math.ceil(((65 + envelopeSize) / 4) * feeRate);
const outputCount = (files?.length || 0) + parentCount + 1 + delegates; // add fee output
const inputTypes = [
ScriptType.P2TR,
...Array(parentCount).fill(ScriptType.P2TR_INSCRIPTION),
];
const outputTypes = Array(outputCount - 1).fill(receiverScriptType);
outputTypes.push(ScriptType.P2WPKH);
const revealFee = estimateTxFee(inputTypes, outputTypes, feeRate);
return Math.ceil(revealFee + inscriptionFee);
}
export function getEnvelopeSize(
file: InscriptionFile | null,
delegates = 0,
parentCount = 0,
delegatesAverageSize = 33,
parentAverageSize = 33,
addPointer = true,
): number {
if (!delegates && !file) {
throw new Error("must provide delegates or files");
}
const parentFieldsSize = parentCount * (1 + 1 + parentAverageSize); // 1 (tag) + 1 (push) + 33/32 (inscription ID)
const pointerFieldSize = addPointer ? 4 : 0;
// delegates
if (!file) {
const envelopeSize =
2 + // OP_0 OP_IF
4 + // "ord" push
parentFieldsSize +
pointerFieldSize +
1 + // tag
1 + // push
delegatesAverageSize + // delegate inscription ID
1; // OP_ENDIF
return envelopeSize;
}
const contentTypeSize = file.mimeType?.length || 0;
const contentTypeField = contentTypeSize > 0 ? 1 + 1 + contentTypeSize : 0;
const numFileChunks = Math.ceil(file.size / 520);
const lastChunkSize = file.size % 520 || 520;
let filePushOps = (numFileChunks - 1) * 3;
filePushOps +=
lastChunkSize <= 75
? 1 // OP_PUSHBYTES_X (1 byte)
: lastChunkSize <= 255
? 2 // OP_PUSHDATA1 (2 bytes)
: 3; // OP_PUSHDATA2 (3 bytes)
const metadataSize = file.metadataSize || 0;
const numMetaChunks = Math.ceil(metadataSize / 520);
const lastMetaChunkSize = metadataSize % 520 || 520;
let metadataPushOps = (numMetaChunks - 1) * 3;
metadataPushOps +=
lastMetaChunkSize <= 75
? 1 // OP_PUSHBYTES_X (1 byte)
: lastMetaChunkSize <= 255
? 2 // OP_PUSHDATA1 (2 bytes)
: 3; // OP_PUSHDATA2 (3 bytes)
const envelopeSize =
2 + // OP_FALSE OP_IF
4 + // "ord" push
parentFieldsSize +
numMetaChunks +
metadataPushOps +
metadataSize +
contentTypeField +
pointerFieldSize +
1 + // OP_PUSH 0
filePushOps +
file.size + // file content
1; // OP_ENDIF
return envelopeSize;
}