fcoin/lib/indexer/addrindexer.js
2019-05-15 12:02:48 -07:00

435 lines
9.6 KiB
JavaScript

/*!
* addrindexer.js - addr indexer
* Copyright (c) 2018, the bcoin developers (MIT License).
* https://github.com/bcoin-org/bcoin
*/
'use strict';
const assert = require('assert');
const bdb = require('bdb');
const bio = require('bufio');
const {BufferSet} = require('buffer-map');
const layout = require('./layout');
const Address = require('../primitives/address');
const Indexer = require('./indexer');
/*
* AddrIndexer Database Layout:
* T[addr-hash][height][tx-index][hash] -> dummy (tx by address)
* C[addr-hash][height][tx-index][hash][coin-index] -> dummy (coin by address)
* x[addr-hash][hash] -> height and tx-index for tx
* y[addr-hash][hash][index] -> height, tx-index and coin-index for coin
*/
Object.assign(layout, {
T: bdb.key('T', ['hash', 'uint32', 'uint32', 'hash256']),
C: bdb.key('C', ['hash', 'uint32', 'uint32', 'hash256', 'uint32']),
x: bdb.key('x', ['hash', 'hash256']),
y: bdb.key('y', ['hash', 'hash256', 'uint32'])
});
/**
* Count
*/
class Count {
/**
* Create count record.
* @constructor
* @param {Number} height
* @param {Number} index
*/
constructor(height, index, coin) {
this.height = height >= 0 ? height : 0;
this.index = index >= 0 ? index : 0;
this.coin = coin >= 0 ? coin : -1;
assert((this.height >>> 0) === this.height);
assert((this.index >>> 0) === this.index);
if (coin)
assert((this.coin >>> 0) === this.coin);
}
/**
* Serialize.
* @returns {Buffer}
*/
toRaw() {
let len = 8;
if (this.coin >= 0)
len += 4;
const bw = bio.write(len);
bw.writeU32(this.height);
bw.writeU32(this.index);
if (this.coin >= 0)
bw.writeU32(this.coin);
return bw.render();
}
/**
* Deserialize.
* @private
* @param {Buffer} data
*/
fromRaw(data) {
const br = bio.read(data);
this.height = br.readU32();
this.index = br.readU32();
if (br.left() >= 4)
this.coin = br.readU32();
return this;
}
/**
* Instantiate a count from a buffer.
* @param {Buffer} data
* @returns {Count}
*/
static fromRaw(data) {
return new this().fromRaw(data);
}
}
/**
* AddrIndexer
* @alias module:indexer.AddrIndexer
* @extends Indexer
*/
class AddrIndexer extends Indexer {
/**
* Create a indexer
* @constructor
* @param {Object} options
*/
constructor(options) {
super('addr', options);
this.db = bdb.create(this.options);
this.maxTxs = options.maxTxs || 100;
this.maxCoins = options.maxCoins || 500;
}
/**
* Index transactions by address.
* @private
* @param {ChainEntry} entry
* @param {Block} block
* @param {CoinView} view
*/
async indexBlock(entry, block, view) {
const b = this.db.batch();
const height = entry.height;
for (let i = 0; i < block.txs.length; i++) {
const tx = block.txs[i];
const hash = tx.hash();
for (const addr of tx.getHashes(view)) {
const count = new Count(height, i);
b.put(layout.T.encode(addr, height, i, hash), null);
b.put(layout.x.encode(addr, hash), count.toRaw());
}
if (!tx.isCoinbase()) {
for (const {prevout} of tx.inputs) {
const {hash, index} = prevout;
const coin = view.getOutput(prevout);
assert(coin);
const addr = coin.getHash();
if (!addr)
continue;
b.del(layout.C.encode(addr, height, i, hash, index));
b.del(layout.y.encode(addr, hash, index));
}
}
for (let j = 0; j < tx.outputs.length; j++) {
const output = tx.outputs[j];
const addr = output.getHash();
if (!addr)
continue;
const count = new Count(height, i, j);
b.put(layout.C.encode(addr, height, i, hash, j), null);
b.put(layout.y.encode(addr, hash, j), count.toRaw());
}
}
return b.write();
}
/**
* Remove addresses from index.
* @private
* @param {ChainEntry} entry
* @param {Block} block
* @param {CoinView} view
*/
async unindexBlock(entry, block, view) {
const b = this.db.batch();
const height = entry.height;
for (let i = 0; i < block.txs.length; i++) {
const tx = block.txs[i];
const hash = tx.hash();
for (const addr of tx.getHashes(view)) {
b.del(layout.T.encode(addr, height, i, hash));
b.del(layout.x.encode(addr, hash));
}
if (!tx.isCoinbase()) {
for (const {prevout} of tx.inputs) {
const {hash, index} = prevout;
const coin = view.getOutput(prevout);
assert(coin);
const addr = coin.getHash();
if (!addr)
continue;
const count = new Count(height, i);
b.put(layout.C.encode(addr, height, i, hash, index), null);
b.put(layout.y.encode(addr, hash, index), count.toRaw());
}
}
for (let j = 0; j < tx.outputs.length; j++) {
const output = tx.outputs[j];
const addr = output.getHash();
if (!addr)
continue;
b.del(layout.C.encode(addr, height, i, hash, j));
b.del(layout.y.encode(addr, hash, j));
}
}
return b.write();
}
/**
* Get all coins pertinent to an address.
* @param {Address} addr
* @param {Object} options
* @param {Boolean} options.reverse
* @param {Boolean} options.limit
* @returns {Promise} - Returns {@link Coin}[].
*/
async getCoinsByAddress(addr, options = {}) {
const coins = [];
const {reverse} = options;
let {limit} = options;
if (!limit)
limit = this.maxCoins;
if (limit > this.maxCoins)
throw new Error('Limit above max of ${this.maxCoins}.');
const hash = Address.getHash(addr);
const keys = await this.db.keys({
gte: layout.C.min(hash),
lte: layout.C.max(hash),
limit,
reverse,
parse: (key) => {
const [,,, txid, index] = layout.C.decode(key);
return [txid, index];
}
});
for (const [hash, index] of keys) {
const coin = await this.chain.getCoin(hash, index);
assert(coin);
coins.push(coin);
}
return coins;
}
/**
* Get all coins pertinent to an address after a
* specific txid and output/coin index.
* @param {Address} addr
* @param {Object} options
* @param {Buffer} options.txid
* @param {Number} options.index
* @param {Boolean} options.limit
* @param {Boolean} options.reverse
* @returns {Promise} - Returns {@link Coin}[].
*/
async getCoinsByAddressAfter(addr, options = {}) {
const coins = [];
const {txid, index, reverse} = options;
let {limit} = options;
if (!limit)
limit = this.maxCoins;
if (limit > this.maxCoins)
throw new Error('Limit above max of ${this.maxCoins}.');
const hash = Address.getHash(addr);
const raw = await this.db.get(layout.y.encode(hash, txid, index));
if (!raw)
return coins;
const count = Count.fromRaw(raw);
const opts = {
limit,
reverse,
parse: (key) => {
const [,,, txid, index] = layout.C.decode(key);
return [txid, index];
}
};
if (!reverse) {
opts.gt = layout.C.min(hash, count.height, count.index, txid, count.coin);
opts.lte = layout.C.max(hash);
} else {
opts.gte = layout.C.min(hash);
opts.lt = layout.C.max(hash, count.height, count.index, txid, count.coin);
}
const keys = await this.db.keys(opts);
for (const [hash, index] of keys) {
const coin = await this.chain.getCoin(hash, index);
assert(coin);
coins.push(coin);
}
return coins;
}
/**
* Get all transaction hashes to an address.
* @param {Address} addr
* @param {Object} options
* @param {Boolean} options.limit
* @param {Boolean} options.reverse
* @returns {Promise} - Returns {@link Hash}[].
*/
async getHashesByAddress(addr, options = {}) {
const set = new BufferSet();
const {reverse} = options;
let {limit} = options;
if (!limit)
limit = this.maxTxs;
if (limit > this.maxTxs)
throw new Error('Limit above max of ${this.maxTxs}.');
const hash = Address.getHash(addr);
await this.db.keys({
gte: layout.T.min(hash),
lte: layout.T.max(hash),
limit,
reverse,
parse: (key) => {
const [,,, txid] = layout.T.decode(key);
set.add(txid);
}
});
return set.toArray();
}
/**
* Get all transaction hashes to an address after
* a specific txid.
* @param {Address} addr
* @param {Object} options
* @param {Buffer} options.txid
* @param {Boolean} options.limit
* @param {Boolean} options.reverse
* @returns {Promise} - Returns {@link Hash}[].
*/
async getHashesByAddressAfter(addr, options = {}) {
const set = new BufferSet();
const hash = Address.getHash(addr);
const {txid, reverse} = options;
let {limit} = options;
if (!limit)
limit = this.maxTxs;
if (limit > this.maxTxs)
throw new Error('Limit above max of ${this.maxTxs}.');
const raw = await this.db.get(layout.x.encode(hash, txid));
if (!raw)
return [];
const count = Count.fromRaw(raw);
const {height, index} = count;
const opts = {
limit,
reverse,
parse: (key) => {
const [,,, txid] = layout.T.decode(key);
set.add(txid);
}
};
if (!reverse) {
opts.gt = layout.T.min(hash, height, index, txid);
opts.lte = layout.T.max(hash);
} else {
opts.gte = layout.T.min(hash);
opts.lt = layout.T.max(hash, height, index, txid);
}
await this.db.keys(opts);
return set.toArray();
}
}
module.exports = AddrIndexer;