You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
200 lines
5.3 KiB
200 lines
5.3 KiB
/*!
|
|
* tracker/transactions-bundle.js
|
|
* Copyright © 2019 – Katana Cryptographic Ltd. All Rights Reserved.
|
|
*/
|
|
'use strict'
|
|
|
|
const _ = require('lodash')
|
|
const LRU = require('lru-cache')
|
|
const util = require('../lib/util')
|
|
const db = require('../lib/db/mysql-db-wrapper')
|
|
const addrHelper = require('../lib/bitcoin/addresses-helper')
|
|
|
|
|
|
/**
|
|
* A base class defining a set of transactions (mempool, block)
|
|
*/
|
|
class TransactionsBundle {
|
|
|
|
/**
|
|
* Constructor
|
|
* @param {object[]} txs - array of bitcoin transaction objects
|
|
*/
|
|
constructor(txs) {
|
|
// List of transactions
|
|
this.transactions = (txs == null) ? [] : txs
|
|
}
|
|
|
|
/**
|
|
* Adds a transaction
|
|
* @param {object} tx - transaction object
|
|
*/
|
|
addTransaction(tx) {
|
|
if (tx) {
|
|
this.transactions.push(tx)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear the bundle
|
|
*/
|
|
clear() {
|
|
this.transactions = []
|
|
}
|
|
|
|
/**
|
|
* Return the bundle as an array of transactions
|
|
* @returns {object[]}
|
|
*/
|
|
toArray() {
|
|
return this.transactions.slice()
|
|
}
|
|
|
|
/**
|
|
* Get the size of the bundle
|
|
* @returns {integer} return the number of transactions stored in the bundle
|
|
*/
|
|
size() {
|
|
return this.transactions.length
|
|
}
|
|
|
|
/**
|
|
* Find the transactions of interest
|
|
* based on theirs inputs
|
|
* @returns {object[]} returns an array of transactions objects
|
|
*/
|
|
async prefilterByInputs() {
|
|
// Process transactions by slices of 5000 transactions
|
|
const MAX_NB_TXS = 5000
|
|
const lists = util.splitList(this.transactions, MAX_NB_TXS)
|
|
const results = await util.parallelCall(lists, txs => this._prefilterByInputs(txs))
|
|
return _.flatten(results)
|
|
}
|
|
|
|
/**
|
|
* Find the transactions of interest
|
|
* based on theirs outputs
|
|
* @returns {object[]} returns an array of transactions objects
|
|
*/
|
|
async prefilterByOutputs() {
|
|
// Process transactions by slices of 5000 transactions
|
|
const MAX_NB_TXS = 5000
|
|
const lists = util.splitList(this.transactions, MAX_NB_TXS)
|
|
const results = await util.parallelCall(lists, txs => this._prefilterByOutputs(txs))
|
|
return _.flatten(results)
|
|
}
|
|
|
|
/**
|
|
* Find the transactions of interest
|
|
* based on theirs outputs (internal implementation)
|
|
* @params {object[]} txs - array of transactions objects
|
|
* @returns {object[]} returns an array of transactions objects
|
|
*/
|
|
async _prefilterByOutputs(txs) {
|
|
let addresses = []
|
|
let filteredIdxTxs = []
|
|
let indexedOutputs = {}
|
|
|
|
// Index the transaction outputs
|
|
for (const i in txs) {
|
|
const tx = txs[i]
|
|
const txid = tx.getId()
|
|
|
|
if (TransactionsBundle.cache.has(txid))
|
|
continue
|
|
|
|
for (const j in tx.outs) {
|
|
try {
|
|
const script = tx.outs[j].script
|
|
const address = addrHelper.outputScript2Address(script)
|
|
addresses.push(address)
|
|
if (!indexedOutputs[address])
|
|
indexedOutputs[address] = []
|
|
indexedOutputs[address].push(i)
|
|
} catch (e) {}
|
|
}
|
|
}
|
|
|
|
// Prefilter
|
|
const outRes = await db.getUngroupedHDAccountsByAddresses(addresses)
|
|
for (const i in outRes) {
|
|
const key = outRes[i].addrAddress
|
|
const idxTxs = indexedOutputs[key]
|
|
if (idxTxs) {
|
|
for (const idxTx of idxTxs)
|
|
if (filteredIdxTxs.indexOf(idxTx) == -1)
|
|
filteredIdxTxs.push(idxTx)
|
|
}
|
|
}
|
|
|
|
return filteredIdxTxs.map(x => txs[x])
|
|
}
|
|
|
|
/**
|
|
* Find the transactions of interest
|
|
* based on theirs inputs (internal implementation)
|
|
* @params {object[]} txs - array of transactions objects
|
|
* @returns {object[]} returns an array of transactions objects
|
|
*/
|
|
async _prefilterByInputs(txs) {
|
|
let inputs = []
|
|
let filteredIdxTxs = []
|
|
let indexedInputs = {}
|
|
|
|
for (const i in txs) {
|
|
const tx = txs[i]
|
|
const txid = tx.getId()
|
|
|
|
if (TransactionsBundle.cache.has(txid))
|
|
continue
|
|
|
|
for (const j in tx.ins) {
|
|
const spendHash = tx.ins[j].hash
|
|
const spendTxid = Buffer.from(spendHash).reverse().toString('hex')
|
|
const spendIdx = tx.ins[j].index
|
|
inputs.push({txid: spendTxid, index: spendIdx})
|
|
const key = spendTxid + '-' + spendIdx
|
|
if (!indexedInputs[key])
|
|
indexedInputs[key] = []
|
|
indexedInputs[key].push(i)
|
|
}
|
|
}
|
|
|
|
// Prefilter
|
|
const lists = util.splitList(inputs, 1000)
|
|
const results = await util.parallelCall(lists, list => db.getOutputSpends(list))
|
|
const inRes = _.flatten(results)
|
|
for (const i in inRes) {
|
|
const key = inRes[i].txnTxid + '-' + inRes[i].outIndex
|
|
const idxTxs = indexedInputs[key]
|
|
if (idxTxs) {
|
|
for (const idxTx of idxTxs)
|
|
if (filteredIdxTxs.indexOf(idxTx) == -1)
|
|
filteredIdxTxs.push(idxTx)
|
|
}
|
|
}
|
|
|
|
return filteredIdxTxs.map(x => txs[x])
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Cache of txids, for avoiding triple-check behavior.
|
|
* ZMQ sends the transaction twice:
|
|
* 1. When it enters the mempool
|
|
* 2. When it leaves the mempool (mined or orphaned)
|
|
* Additionally, the transaction comes in a block
|
|
* Orphaned transactions are deleted during the routine check
|
|
*/
|
|
TransactionsBundle.cache = LRU({
|
|
// Maximum number of txids to store in cache
|
|
max: 100000,
|
|
// Function used to compute length of item
|
|
length: (n, key) => 1,
|
|
// Maximum age for items in the cache. Items do not expire
|
|
maxAge: Infinity
|
|
})
|
|
|
|
|
|
module.exports = TransactionsBundle
|
|
|