2020-05-28 18:39:45 +07:00
|
|
|
import { Transaction, Vin } from './interfaces/electrs.interface';
|
|
|
|
|
|
|
|
const P2SH_P2WPKH_COST = 21 * 4; // the WU cost for the non-witness part of P2SH-P2WPKH
|
|
|
|
const P2SH_P2WSH_COST = 35 * 4; // the WU cost for the non-witness part of P2SH-P2WSH
|
|
|
|
|
|
|
|
export function calcSegwitFeeGains(tx: Transaction) {
|
|
|
|
// calculated in weight units
|
2022-07-26 16:29:42 +02:00
|
|
|
let realizedSegwitGains = 0;
|
|
|
|
let potentialSegwitGains = 0;
|
|
|
|
let potentialP2shSegwitGains = 0;
|
2022-07-24 00:08:53 +02:00
|
|
|
let potentialTaprootGains = 0;
|
|
|
|
let realizedTaprootGains = 0;
|
2020-05-28 18:39:45 +07:00
|
|
|
|
|
|
|
for (const vin of tx.vin) {
|
|
|
|
if (!vin.prevout) { continue; }
|
|
|
|
|
2022-07-24 00:08:53 +02:00
|
|
|
const isP2pk = vin.prevout.scriptpubkey_type === 'p2pk';
|
|
|
|
// const isBareMultisig = vin.prevout.scriptpubkey_type === 'multisig'; // type will be unknown, so use the multisig helper from the address labels
|
|
|
|
const isBareMultisig = !!parseMultisigScript(vin.prevout.scriptpubkey_asm);
|
|
|
|
const isP2pkh = vin.prevout.scriptpubkey_type === 'p2pkh';
|
|
|
|
const isP2sh = vin.prevout.scriptpubkey_type === 'p2sh';
|
|
|
|
const isP2wsh = vin.prevout.scriptpubkey_type === 'v0_p2wsh';
|
|
|
|
const isP2wpkh = vin.prevout.scriptpubkey_type === 'v0_p2wpkh';
|
|
|
|
const isP2tr = vin.prevout.scriptpubkey_type === 'v1_p2tr';
|
2020-05-28 18:39:45 +07:00
|
|
|
|
|
|
|
const op = vin.scriptsig ? vin.scriptsig_asm.split(' ')[0] : null;
|
2022-07-26 16:29:42 +02:00
|
|
|
const isP2shP2Wpkh = isP2sh && !!vin.witness && op === 'OP_PUSHBYTES_22';
|
|
|
|
const isP2shP2Wsh = isP2sh && !!vin.witness && op === 'OP_PUSHBYTES_34';
|
2020-05-28 18:39:45 +07:00
|
|
|
|
|
|
|
switch (true) {
|
2022-07-26 16:29:42 +02:00
|
|
|
// Native Segwit - P2WPKH/P2WSH/P2TR
|
2020-05-28 18:39:45 +07:00
|
|
|
case isP2wpkh:
|
|
|
|
case isP2wsh:
|
2021-11-10 15:05:45 +04:00
|
|
|
case isP2tr:
|
2020-05-28 18:39:45 +07:00
|
|
|
// maximal gains: the scriptSig is moved entirely to the witness part
|
2022-07-26 16:29:42 +02:00
|
|
|
// if taproot is used savings are 42 WU higher because it produces smaller signatures and doesn't require a pubkey in the witness
|
|
|
|
// this number is explained above `realizedTaprootGains += 42;`
|
|
|
|
realizedSegwitGains += (witnessSize(vin) + (isP2tr ? 42 : 0)) * 3;
|
2020-05-28 18:39:45 +07:00
|
|
|
// XXX P2WSH output creation is more expensive, should we take this into consideration?
|
|
|
|
break;
|
|
|
|
|
|
|
|
// Backward compatible Segwit - P2SH-P2WPKH
|
2022-07-26 16:29:42 +02:00
|
|
|
case isP2shP2Wpkh:
|
|
|
|
// the scriptSig is moved to the witness, but we have extra 21 extra non-witness bytes (84 WU)
|
|
|
|
realizedSegwitGains += witnessSize(vin) * 3 - P2SH_P2WPKH_COST;
|
|
|
|
potentialSegwitGains += P2SH_P2WPKH_COST;
|
2020-05-28 18:39:45 +07:00
|
|
|
break;
|
|
|
|
|
|
|
|
// Backward compatible Segwit - P2SH-P2WSH
|
2022-07-26 16:29:42 +02:00
|
|
|
case isP2shP2Wsh:
|
|
|
|
// the scriptSig is moved to the witness, but we have extra 35 extra non-witness bytes (140 WU)
|
|
|
|
realizedSegwitGains += witnessSize(vin) * 3 - P2SH_P2WSH_COST;
|
|
|
|
potentialSegwitGains += P2SH_P2WSH_COST;
|
2020-05-28 18:39:45 +07:00
|
|
|
break;
|
|
|
|
|
2022-07-24 00:08:53 +02:00
|
|
|
// Non-segwit P2PKH/P2SH/P2PK/bare multisig
|
2020-05-28 18:39:45 +07:00
|
|
|
case isP2pkh:
|
|
|
|
case isP2sh:
|
2022-07-24 00:08:53 +02:00
|
|
|
case isP2pk:
|
|
|
|
case isBareMultisig: {
|
2022-07-26 16:29:42 +02:00
|
|
|
let fullGains = scriptSigSize(vin) * 3;
|
|
|
|
if (isBareMultisig) {
|
|
|
|
// a _bare_ multisig has the keys in the output script, but P2SH and P2WSH require them to be in the scriptSig/scriptWitness
|
|
|
|
fullGains -= vin.prevout.scriptpubkey.length / 2;
|
|
|
|
}
|
|
|
|
potentialSegwitGains += fullGains;
|
|
|
|
potentialP2shSegwitGains += fullGains - (isP2pkh ? P2SH_P2WPKH_COST : P2SH_P2WSH_COST);
|
2020-05-28 18:39:45 +07:00
|
|
|
break;
|
2022-07-24 00:08:53 +02:00
|
|
|
}
|
|
|
|
}
|
2020-05-28 18:39:45 +07:00
|
|
|
|
2022-07-24 00:08:53 +02:00
|
|
|
if (isP2tr) {
|
2022-07-24 18:44:53 +02:00
|
|
|
if (vin.witness.length === 1) {
|
2022-07-24 00:08:53 +02:00
|
|
|
// key path spend
|
|
|
|
// we don't know if this was a multisig or single sig (the goal of taproot :)),
|
|
|
|
// so calculate fee savings by comparing to the cheapest single sig input type: P2WPKH and say "saved at least ...%"
|
|
|
|
// the witness size of P2WPKH is 1 (stack size) + 1 (size) + 72 (low s signature) + 1 (size) + 33 (pubkey) = 108 WU
|
|
|
|
// the witness size of key path P2TR is 1 (stack size) + 1 (size) + 64 (signature) = 66 WU
|
|
|
|
realizedTaprootGains += 42;
|
|
|
|
} else {
|
|
|
|
// script path spend
|
|
|
|
// complex scripts with multiple spending paths can often be made around 2x to 3x smaller with the Taproot script tree
|
|
|
|
// because only the hash of the alternative spending path has the be in the witness data, not the entire script,
|
|
|
|
// but only assumptions can be made because the scripts themselves are unknown (again, the goal of taproot :))
|
|
|
|
// TODO maybe add some complex scripts that are specified somewhere, so that size is known, such as lightning scripts
|
|
|
|
}
|
|
|
|
} else {
|
2022-07-26 16:29:42 +02:00
|
|
|
const script = isP2shP2Wsh || isP2wsh ? vin.inner_witnessscript_asm : vin.inner_redeemscript_asm;
|
2022-07-24 00:08:53 +02:00
|
|
|
let replacementSize: number;
|
|
|
|
if (
|
|
|
|
// single sig
|
2022-07-26 16:29:42 +02:00
|
|
|
isP2pk || isP2pkh || isP2wpkh || isP2shP2Wpkh ||
|
2022-07-24 00:08:53 +02:00
|
|
|
// multisig
|
|
|
|
isBareMultisig || parseMultisigScript(script)
|
|
|
|
) {
|
|
|
|
// the scriptSig and scriptWitness can all be replaced by a 66 witness WU with taproot
|
|
|
|
replacementSize = 66;
|
|
|
|
} else if (script) {
|
|
|
|
// not single sig, not multisig: the complex scripts
|
|
|
|
// rough calculations on spending paths
|
|
|
|
// every OP_IF and OP_NOTIF indicates an _extra_ spending path, so add 1
|
|
|
|
const spendingPaths = script.split(' ').filter(op => /^(OP_IF|OP_NOTIF)$/g.test(op)).length + 1;
|
|
|
|
// now assume the script could have been split in ${spendingPaths} equal tapleaves
|
|
|
|
replacementSize = script.length / 2 / spendingPaths +
|
|
|
|
// but account for the leaf and branch hashes and internal key in the control block
|
|
|
|
32 * Math.log2((spendingPaths - 1) || 1) + 33;
|
|
|
|
}
|
|
|
|
potentialTaprootGains += witnessSize(vin) + scriptSigSize(vin) * 4 - replacementSize;
|
2020-05-28 18:39:45 +07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// returned as percentage of the total tx weight
|
2022-07-24 00:08:53 +02:00
|
|
|
return {
|
2022-07-26 16:29:42 +02:00
|
|
|
realizedSegwitGains: realizedSegwitGains / (tx.weight + realizedSegwitGains), // percent of the pre-segwit tx size
|
|
|
|
potentialSegwitGains: potentialSegwitGains / tx.weight,
|
|
|
|
potentialP2shSegwitGains: potentialP2shSegwitGains / tx.weight,
|
2022-07-24 00:08:53 +02:00
|
|
|
potentialTaprootGains: potentialTaprootGains / tx.weight,
|
2022-07-26 16:29:42 +02:00
|
|
|
realizedTaprootGains: realizedTaprootGains / (tx.weight + realizedTaprootGains)
|
2022-07-24 00:08:53 +02:00
|
|
|
};
|
2020-05-28 18:39:45 +07:00
|
|
|
}
|
|
|
|
|
2022-07-24 18:44:27 +02:00
|
|
|
/** extracts m and n from a multisig script (asm), returns nothing if it is not a multisig script */
|
|
|
|
export function parseMultisigScript(script: string): void | { m: number, n: number } {
|
|
|
|
if (!script) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const ops = script.split(' ');
|
|
|
|
if (ops.length < 3 || ops.pop() !== 'OP_CHECKMULTISIG') {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const opN = ops.pop();
|
|
|
|
if (!opN.startsWith('OP_PUSHNUM_')) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const n = parseInt(opN.match(/[0-9]+/)[0], 10);
|
|
|
|
if (ops.length < n * 2 + 1) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// pop n public keys
|
|
|
|
for (let i = 0; i < n; i++) {
|
|
|
|
if (!/^0((2|3)\w{64}|4\w{128})$/.test(ops.pop())) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!/^OP_PUSHBYTES_(33|65)$/.test(ops.pop())) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const opM = ops.pop();
|
|
|
|
if (!opM.startsWith('OP_PUSHNUM_')) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const m = parseInt(opM.match(/[0-9]+/)[0], 10);
|
|
|
|
|
|
|
|
if (ops.length) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
return { m, n };
|
|
|
|
}
|
|
|
|
|
2020-09-29 03:54:56 +07:00
|
|
|
// https://github.com/shesek/move-decimal-point
|
|
|
|
export function moveDec(num: number, n: number) {
|
|
|
|
let frac, int, neg, ref;
|
|
|
|
if (n === 0) {
|
2022-02-13 00:46:42 +04:00
|
|
|
return num.toString();
|
2020-09-29 03:54:56 +07:00
|
|
|
}
|
|
|
|
ref = ('' + num).split('.'), int = ref[0], frac = ref[1];
|
|
|
|
int || (int = '0');
|
|
|
|
frac || (frac = '0');
|
|
|
|
neg = (int[0] === '-' ? '-' : '');
|
|
|
|
if (neg) {
|
|
|
|
int = int.slice(1);
|
|
|
|
}
|
|
|
|
if (n > 0) {
|
|
|
|
if (n > frac.length) {
|
|
|
|
frac += zeros(n - frac.length);
|
|
|
|
}
|
|
|
|
int += frac.slice(0, n);
|
|
|
|
frac = frac.slice(n);
|
|
|
|
} else {
|
|
|
|
n = n * -1;
|
|
|
|
if (n > int.length) {
|
|
|
|
int = (zeros(n - int.length)) + int;
|
|
|
|
}
|
|
|
|
frac = int.slice(n * -1) + frac;
|
|
|
|
int = int.slice(0, n * -1);
|
|
|
|
}
|
|
|
|
while (int[0] === '0') {
|
|
|
|
int = int.slice(1);
|
|
|
|
}
|
|
|
|
while (frac[frac.length - 1] === '0') {
|
|
|
|
frac = frac.slice(0, -1);
|
|
|
|
}
|
|
|
|
return neg + (int || '0') + (frac.length ? '.' + frac : '');
|
|
|
|
}
|
|
|
|
|
2022-07-26 16:29:42 +02:00
|
|
|
function zeros(n: number) {
|
2020-09-29 03:54:56 +07:00
|
|
|
return new Array(n + 1).join('0');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Formats a number for display. Treats the number as a string to avoid rounding errors.
|
2022-07-26 16:29:42 +02:00
|
|
|
export const formatNumber = (s: number | string, precision: number | null = null) => {
|
2020-09-29 03:54:56 +07:00
|
|
|
let [ whole, dec ] = s.toString().split('.');
|
|
|
|
|
|
|
|
// divide numbers into groups of three separated with a thin space (U+202F, "NARROW NO-BREAK SPACE"),
|
|
|
|
// but only when there are more than a total of 5 non-decimal digits.
|
|
|
|
if (whole.length >= 5) {
|
|
|
|
whole = whole.replace(/\B(?=(\d{3})+(?!\d))/g, '\u202F');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (precision != null && precision > 0) {
|
|
|
|
if (dec == null) {
|
|
|
|
dec = '0'.repeat(precision);
|
|
|
|
}
|
|
|
|
else if (dec.length < precision) {
|
|
|
|
dec += '0'.repeat(precision - dec.length);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return whole + (dec != null ? '.' + dec : '');
|
|
|
|
};
|
|
|
|
|
2020-05-28 18:39:45 +07:00
|
|
|
// Utilities for segwitFeeGains
|
2022-07-24 00:08:53 +02:00
|
|
|
const witnessSize = (vin: Vin) => vin.witness ? vin.witness.reduce((S, w) => S + (w.length / 2), 0) : 0;
|
2020-05-28 18:39:45 +07:00
|
|
|
const scriptSigSize = (vin: Vin) => vin.scriptsig ? vin.scriptsig.length / 2 : 0;
|
2022-02-21 15:55:27 +09:00
|
|
|
|
|
|
|
// Power of ten wrapper
|
2023-12-04 09:56:56 +01:00
|
|
|
export function selectPowerOfTen(val: number, multiplier = 1): { divider: number, unit: string } {
|
2022-02-21 15:55:27 +09:00
|
|
|
const powerOfTen = {
|
|
|
|
exa: Math.pow(10, 18),
|
|
|
|
peta: Math.pow(10, 15),
|
2022-07-26 16:29:42 +02:00
|
|
|
tera: Math.pow(10, 12),
|
2022-02-21 15:55:27 +09:00
|
|
|
giga: Math.pow(10, 9),
|
|
|
|
mega: Math.pow(10, 6),
|
|
|
|
kilo: Math.pow(10, 3),
|
|
|
|
};
|
|
|
|
|
2022-07-26 16:29:42 +02:00
|
|
|
let selectedPowerOfTen: { divider: number, unit: string };
|
2023-12-04 09:56:56 +01:00
|
|
|
if (val < powerOfTen.kilo * multiplier) {
|
2022-02-21 15:55:27 +09:00
|
|
|
selectedPowerOfTen = { divider: 1, unit: '' }; // no scaling
|
2023-12-04 09:56:56 +01:00
|
|
|
} else if (val < powerOfTen.mega * multiplier) {
|
2022-04-15 00:21:38 +09:00
|
|
|
selectedPowerOfTen = { divider: powerOfTen.kilo, unit: 'k' };
|
2023-12-04 09:56:56 +01:00
|
|
|
} else if (val < powerOfTen.giga * multiplier) {
|
2022-02-21 15:55:27 +09:00
|
|
|
selectedPowerOfTen = { divider: powerOfTen.mega, unit: 'M' };
|
2023-12-04 09:56:56 +01:00
|
|
|
} else if (val < powerOfTen.tera * multiplier) {
|
2022-02-21 15:55:27 +09:00
|
|
|
selectedPowerOfTen = { divider: powerOfTen.giga, unit: 'G' };
|
2023-12-04 09:56:56 +01:00
|
|
|
} else if (val < powerOfTen.peta * multiplier) {
|
2022-07-26 16:29:42 +02:00
|
|
|
selectedPowerOfTen = { divider: powerOfTen.tera, unit: 'T' };
|
2023-12-04 09:56:56 +01:00
|
|
|
} else if (val < powerOfTen.exa * multiplier) {
|
2022-02-21 15:55:27 +09:00
|
|
|
selectedPowerOfTen = { divider: powerOfTen.peta, unit: 'P' };
|
|
|
|
} else {
|
|
|
|
selectedPowerOfTen = { divider: powerOfTen.exa, unit: 'E' };
|
|
|
|
}
|
|
|
|
|
|
|
|
return selectedPowerOfTen;
|
2022-07-24 00:08:53 +02:00
|
|
|
}
|
2023-03-14 13:02:50 +09:00
|
|
|
|
|
|
|
const featureActivation = {
|
|
|
|
mainnet: {
|
|
|
|
rbf: 399701,
|
|
|
|
segwit: 477120,
|
|
|
|
taproot: 709632,
|
|
|
|
},
|
|
|
|
testnet: {
|
|
|
|
rbf: 720255,
|
|
|
|
segwit: 872730,
|
|
|
|
taproot: 2032291,
|
|
|
|
},
|
|
|
|
signet: {
|
|
|
|
rbf: 0,
|
|
|
|
segwit: 0,
|
|
|
|
taproot: 0,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
export function isFeatureActive(network: string, height: number, feature: 'rbf' | 'segwit' | 'taproot'): boolean {
|
|
|
|
const activationHeight = featureActivation[network || 'mainnet']?.[feature];
|
|
|
|
if (activationHeight != null) {
|
|
|
|
return height >= activationHeight;
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
2023-07-22 17:51:45 +09:00
|
|
|
|
|
|
|
export async function calcScriptHash$(script: string): Promise<string> {
|
2023-07-23 13:55:52 +09:00
|
|
|
if (!/^[0-9a-fA-F]*$/.test(script) || script.length % 2 !== 0) {
|
|
|
|
throw new Error('script is not a valid hex string');
|
|
|
|
}
|
|
|
|
const buf = Uint8Array.from(script.match(/.{2}/g).map((byte) => parseInt(byte, 16)));
|
2023-07-22 17:51:45 +09:00
|
|
|
const hashBuffer = await crypto.subtle.digest('SHA-256', buf);
|
|
|
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
|
|
return hashArray
|
|
|
|
.map((bytes) => bytes.toString(16).padStart(2, '0'))
|
|
|
|
.join('');
|
|
|
|
}
|