2023-05-29 15:56:29 -04:00
|
|
|
import { TransactionExtended, MempoolTransactionExtended, TransactionMinerInfo } from '../mempool.interfaces';
|
2020-12-28 04:47:22 +07:00
|
|
|
import { IEsploraApi } from './bitcoin/esplora-api.interface';
|
2021-12-31 02:28:40 +04:00
|
|
|
import { Common } from './common';
|
2023-01-08 11:24:23 +01:00
|
|
|
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
|
2023-05-29 15:56:29 -04:00
|
|
|
import * as bitcoinjs from 'bitcoinjs-lib';
|
2020-12-21 23:08:34 +07:00
|
|
|
|
|
|
|
class TransactionUtils {
|
|
|
|
constructor() { }
|
|
|
|
|
|
|
|
public stripCoinbaseTransaction(tx: TransactionExtended): TransactionMinerInfo {
|
|
|
|
return {
|
|
|
|
vin: [{
|
|
|
|
scriptsig: tx.vin[0].scriptsig || tx.vin[0]['coinbase']
|
|
|
|
}],
|
|
|
|
vout: tx.vout
|
|
|
|
.map((vout) => ({
|
|
|
|
scriptpubkey_address: vout.scriptpubkey_address,
|
2023-02-17 21:21:21 +09:00
|
|
|
scriptpubkey_asm: vout.scriptpubkey_asm,
|
2020-12-21 23:08:34 +07:00
|
|
|
value: vout.value
|
|
|
|
}))
|
|
|
|
.filter((vout) => vout.value)
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-01-07 11:17:30 +01:00
|
|
|
/**
|
2023-05-29 15:56:29 -04:00
|
|
|
* @param txId
|
|
|
|
* @param addPrevouts
|
|
|
|
* @param lazyPrevouts
|
2023-01-07 11:17:30 +01:00
|
|
|
* @param forceCore - See https://github.com/mempool/mempool/issues/2904
|
|
|
|
*/
|
2023-05-29 15:56:29 -04:00
|
|
|
public async $getTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false, forceCore = false, addMempoolData = false): Promise<TransactionExtended> {
|
2023-01-07 11:17:30 +01:00
|
|
|
let transaction: IEsploraApi.Transaction;
|
|
|
|
if (forceCore === true) {
|
2023-01-08 11:24:23 +01:00
|
|
|
transaction = await bitcoinCoreApi.$getRawTransaction(txId, true);
|
2023-01-07 11:17:30 +01:00
|
|
|
} else {
|
|
|
|
transaction = await bitcoinApi.$getRawTransaction(txId, false, addPrevouts, lazyPrevouts);
|
|
|
|
}
|
2023-05-29 15:56:29 -04:00
|
|
|
if (addMempoolData || !transaction?.status?.confirmed) {
|
|
|
|
return this.extendMempoolTransaction(transaction);
|
|
|
|
} else {
|
|
|
|
return this.extendTransaction(transaction);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public async $getMempoolTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false, forceCore = false): Promise<MempoolTransactionExtended> {
|
|
|
|
return (await this.$getTransactionExtended(txId, addPrevouts, lazyPrevouts, forceCore, true)) as MempoolTransactionExtended;
|
2020-12-21 23:08:34 +07:00
|
|
|
}
|
|
|
|
|
2020-12-28 20:17:32 +07:00
|
|
|
private extendTransaction(transaction: IEsploraApi.Transaction): TransactionExtended {
|
2021-03-19 19:10:11 +07:00
|
|
|
// @ts-ignore
|
|
|
|
if (transaction.vsize) {
|
|
|
|
// @ts-ignore
|
|
|
|
return transaction;
|
|
|
|
}
|
2021-12-31 02:28:40 +04:00
|
|
|
const feePerVbytes = Math.max(Common.isLiquid() ? 0.1 : 1,
|
2021-12-31 01:26:45 +04:00
|
|
|
(transaction.fee || 0) / (transaction.weight / 4));
|
2020-12-28 20:17:32 +07:00
|
|
|
const transactionExtended: TransactionExtended = Object.assign({
|
|
|
|
vsize: Math.round(transaction.weight / 4),
|
2021-03-18 23:47:40 +07:00
|
|
|
feePerVsize: feePerVbytes,
|
|
|
|
effectiveFeePerVsize: feePerVbytes,
|
2020-12-28 20:17:32 +07:00
|
|
|
}, transaction);
|
2023-05-29 15:56:29 -04:00
|
|
|
if (!transaction?.status?.confirmed) {
|
|
|
|
transactionExtended.firstSeen = Math.round((new Date().getTime() / 1000));
|
|
|
|
}
|
|
|
|
return transactionExtended;
|
|
|
|
}
|
|
|
|
|
|
|
|
public extendMempoolTransaction(transaction: IEsploraApi.Transaction): MempoolTransactionExtended {
|
|
|
|
const vsize = Math.ceil(transaction.weight / 4);
|
|
|
|
const sigops = this.countSigops(transaction);
|
|
|
|
// https://github.com/bitcoin/bitcoin/blob/e9262ea32a6e1d364fb7974844fadc36f931f8c6/src/policy/policy.cpp#L295-L298
|
|
|
|
const adjustedVsize = Math.max(vsize, sigops * 5); // adjusted vsize = Max(weight, sigops * bytes_per_sigop) / witness_scale_factor
|
|
|
|
const feePerVbytes = Math.max(Common.isLiquid() ? 0.1 : 1,
|
|
|
|
(transaction.fee || 0) / vsize);
|
|
|
|
const adjustedFeePerVsize = Math.max(Common.isLiquid() ? 0.1 : 1,
|
|
|
|
(transaction.fee || 0) / adjustedVsize);
|
|
|
|
const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, {
|
|
|
|
vsize: Math.round(transaction.weight / 4),
|
|
|
|
adjustedVsize,
|
|
|
|
sigops,
|
|
|
|
feePerVsize: feePerVbytes,
|
|
|
|
adjustedFeePerVsize: adjustedFeePerVsize,
|
|
|
|
effectiveFeePerVsize: adjustedFeePerVsize,
|
|
|
|
});
|
|
|
|
if (!transaction?.status?.confirmed) {
|
2020-12-28 20:17:32 +07:00
|
|
|
transactionExtended.firstSeen = Math.round((new Date().getTime() / 1000));
|
|
|
|
}
|
|
|
|
return transactionExtended;
|
|
|
|
}
|
2022-01-05 15:41:14 +09:00
|
|
|
|
|
|
|
public hex2ascii(hex: string) {
|
|
|
|
let str = '';
|
|
|
|
for (let i = 0; i < hex.length; i += 2) {
|
|
|
|
str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
|
|
|
|
}
|
|
|
|
return str;
|
|
|
|
}
|
2023-05-29 15:56:29 -04:00
|
|
|
|
|
|
|
public countScriptSigops(script: string, isRawScript: boolean = false, witness: boolean = false): number {
|
|
|
|
let sigops = 0;
|
|
|
|
// count OP_CHECKSIG and OP_CHECKSIGVERIFY
|
|
|
|
sigops += (script.match(/OP_CHECKSIG/g)?.length || 0);
|
|
|
|
|
|
|
|
// count OP_CHECKMULTISIG and OP_CHECKMULTISIGVERIFY
|
|
|
|
if (isRawScript) {
|
|
|
|
// in scriptPubKey or scriptSig, always worth 20
|
|
|
|
sigops += 20 * (script.match(/OP_CHECKMULTISIG/g)?.length || 0);
|
|
|
|
} else {
|
|
|
|
// in redeem scripts and witnesses, worth N if preceded by OP_N, 20 otherwise
|
|
|
|
const matches = script.matchAll(/(?:OP_(\d+))? OP_CHECKMULTISIG/g);
|
|
|
|
for (const match of matches) {
|
|
|
|
const n = parseInt(match[1]);
|
|
|
|
if (Number.isInteger(n)) {
|
|
|
|
sigops += n;
|
|
|
|
} else {
|
|
|
|
sigops += 20;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return witness ? sigops : (sigops * 4);
|
|
|
|
}
|
|
|
|
|
|
|
|
public countSigops(transaction: IEsploraApi.Transaction): number {
|
|
|
|
let sigops = 0;
|
|
|
|
|
|
|
|
for (const input of transaction.vin) {
|
|
|
|
if (input.scriptsig_asm) {
|
|
|
|
sigops += this.countScriptSigops(input.scriptsig_asm, true);
|
|
|
|
}
|
|
|
|
if (input.prevout) {
|
|
|
|
switch (true) {
|
|
|
|
case input.prevout.scriptpubkey_type === 'p2sh' && input.witness?.length === 2 && input.scriptsig && input.scriptsig.startsWith('160014'):
|
|
|
|
case input.prevout.scriptpubkey_type === 'v0_p2wpkh':
|
|
|
|
sigops += 1;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case input.prevout?.scriptpubkey_type === 'p2sh' && input.witness?.length && input.scriptsig && input.scriptsig.startsWith('220020'):
|
|
|
|
case input.prevout.scriptpubkey_type === 'v0_p2wsh':
|
|
|
|
if (input.witness?.length) {
|
|
|
|
sigops += this.countScriptSigops(bitcoinjs.script.toASM(Buffer.from(input.witness[input.witness.length - 1], 'hex')), false, true);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const output of transaction.vout) {
|
|
|
|
if (output.scriptpubkey_asm) {
|
|
|
|
sigops += this.countScriptSigops(output.scriptpubkey_asm, true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return sigops;
|
|
|
|
}
|
2020-12-21 23:08:34 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
export default new TransactionUtils();
|