277 lines
6.5 KiB
JavaScript
277 lines
6.5 KiB
JavaScript
/*!
|
|
* addrindexer.js - address indexer for bcoin
|
|
* 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 layout = require('./layout');
|
|
const Address = require('../primitives/address');
|
|
const Indexer = require('./indexer');
|
|
|
|
/*
|
|
* AddrIndexer Database Layout:
|
|
* A[addr-prefix][addr-hash][height][index] -> dummy (tx by address)
|
|
* C[height][index] -> hash (tx hash by height and index)
|
|
* c[hash]-> height + index (tx height and index by hash)
|
|
*
|
|
* The database layout is organized so that transactions are
|
|
* sorted in the same order as the blocks using the block height
|
|
* and transaction index. This provides the ability to query for
|
|
* sets of transactions within that order. For a wallet that would
|
|
* like to synchronize or rescan, this could be a query for all of
|
|
* the latest transactions, but not for earlier transactions that
|
|
* are already known.
|
|
*
|
|
* To be able to query for all transactions in multiple sets without
|
|
* reference to height and index, there is a mapping from tx hash to
|
|
* the height and index as an entry point.
|
|
*
|
|
* A mapping of height and index is kept for each transaction
|
|
* hash so that the tx hash is not repeated for every address within
|
|
* a transaction.
|
|
*/
|
|
|
|
Object.assign(layout, {
|
|
A: bdb.key('A', ['uint8', 'hash', 'uint32', 'uint32']),
|
|
C: bdb.key('C', ['uint32', 'uint32']),
|
|
c: bdb.key('c', ['hash256'])
|
|
});
|
|
|
|
/**
|
|
* Count
|
|
*/
|
|
|
|
class Count {
|
|
/**
|
|
* Create count record.
|
|
* @constructor
|
|
* @param {Number} height
|
|
* @param {Number} index
|
|
*/
|
|
|
|
constructor(height, index) {
|
|
this.height = height || 0;
|
|
this.index = index || 0;
|
|
|
|
assert((this.height >>> 0) === this.height);
|
|
assert((this.index >>> 0) === this.index);
|
|
}
|
|
|
|
/**
|
|
* Serialize.
|
|
* @returns {Buffer}
|
|
*/
|
|
|
|
toRaw() {
|
|
const bw = bio.write(8);
|
|
|
|
bw.writeU32(this.height);
|
|
bw.writeU32(this.index);
|
|
|
|
return bw.render();
|
|
}
|
|
|
|
/**
|
|
* Deserialize.
|
|
* @private
|
|
* @param {Buffer} data
|
|
*/
|
|
|
|
fromRaw(data) {
|
|
const br = bio.read(data);
|
|
|
|
this.height = br.readU32();
|
|
this.index = 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;
|
|
}
|
|
|
|
/**
|
|
* Index transactions by address.
|
|
* @private
|
|
* @param {BlockMeta} meta
|
|
* @param {Block} block
|
|
* @param {CoinView} view
|
|
*/
|
|
|
|
async indexBlock(meta, block, view) {
|
|
const height = meta.height;
|
|
|
|
for (let i = 0; i < block.txs.length; i++) {
|
|
const tx = block.txs[i];
|
|
const hash = tx.hash();
|
|
const count = new Count(height, i);
|
|
|
|
let hasAddress = false;
|
|
|
|
for (const addr of tx.getAddresses(view)) {
|
|
const prefix = addr.getPrefix(this.network);
|
|
|
|
if (prefix < 0)
|
|
continue;
|
|
|
|
const addrHash = addr.getHash();
|
|
|
|
this.put(layout.A.encode(prefix, addrHash, height, i), null);
|
|
|
|
hasAddress = true;
|
|
}
|
|
|
|
if (hasAddress) {
|
|
this.put(layout.C.encode(height, i), hash);
|
|
this.put(layout.c.encode(hash), count.toRaw());
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove addresses from index.
|
|
* @private
|
|
* @param {BlockMeta} meta
|
|
* @param {Block} block
|
|
* @param {CoinView} view
|
|
*/
|
|
|
|
async unindexBlock(meta, block, view) {
|
|
const height = meta.height;
|
|
|
|
for (let i = 0; i < block.txs.length; i++) {
|
|
const tx = block.txs[i];
|
|
const hash = tx.hash();
|
|
|
|
let hasAddress = false;
|
|
|
|
for (const addr of tx.getAddresses(view)) {
|
|
const prefix = addr.getPrefix(this.network);
|
|
|
|
if (prefix < 0)
|
|
continue;
|
|
|
|
const addrHash = addr.getHash();
|
|
|
|
this.del(layout.A.encode(prefix, addrHash, height, i));
|
|
|
|
hasAddress = true;
|
|
}
|
|
|
|
if (hasAddress) {
|
|
this.del(layout.C.encode(height, i));
|
|
this.del(layout.c.encode(hash));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get transaction hashes to an address in ascending or descending
|
|
* order. If the `after` argument is supplied, results will be given
|
|
* _after_ that transaction hash. The default order is ascending from
|
|
* oldest to latest.
|
|
* @param {Address} addr
|
|
* @param {Object} options
|
|
* @param {Buffer} options.after - A transaction hash
|
|
* @param {Number} options.limit
|
|
* @param {Boolean} options.reverse
|
|
* @returns {Promise} - Returns {@link Hash}[].
|
|
*/
|
|
|
|
async getHashesByAddress(addr, options = {}) {
|
|
const {after, 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);
|
|
const prefix = addr.getPrefix(this.network);
|
|
|
|
const opts = {
|
|
limit,
|
|
reverse,
|
|
parse: (key) => {
|
|
const [,, height, index] = layout.A.decode(key);
|
|
return [height, index];
|
|
}
|
|
};
|
|
|
|
// Determine if the hash -> height + index mapping exists.
|
|
const hasAfter = (after && await this.db.has(layout.c.encode(after)));
|
|
|
|
// Check to see if results should be skipped because
|
|
// the after hash is expected to be within a following
|
|
// mempool query.
|
|
const skip = (after && !hasAfter && !reverse);
|
|
if (skip)
|
|
return [];
|
|
|
|
if (after && hasAfter) {
|
|
// Give results starting from after
|
|
// the tx hash for the address.
|
|
const raw = await this.db.get(layout.c.encode(after));
|
|
const count = Count.fromRaw(raw);
|
|
const {height, index} = count;
|
|
|
|
if (!reverse) {
|
|
opts.gt = layout.A.min(prefix, hash, height, index);
|
|
opts.lte = layout.A.max(prefix, hash);
|
|
} else {
|
|
opts.gte = layout.A.min(prefix, hash);
|
|
opts.lt = layout.A.max(prefix, hash, height, index);
|
|
}
|
|
} else {
|
|
// Give earliest or latest results
|
|
// for the address.
|
|
opts.gte = layout.A.min(prefix, hash);
|
|
opts.lte = layout.A.max(prefix, hash);
|
|
}
|
|
|
|
const txs = await this.db.keys(opts);
|
|
const hashes = [];
|
|
|
|
for (const [height, index] of txs)
|
|
hashes.push(await this.db.get(layout.C.encode(height, index)));
|
|
|
|
return hashes;
|
|
}
|
|
}
|
|
|
|
module.exports = AddrIndexer;
|