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

/*!
* 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