/*! * records.js - walletdb records * Copyright (c) 2014-2017, Christopher Jeffrey (MIT License). * https://github.com/bcoin-org/bcoin */ 'use strict'; /** * @module wallet/records */ var assert = require('assert'); var util = require('../utils/util'); var encoding = require('../utils/encoding'); var BufferReader = require('../utils/reader'); var StaticWriter = require('../utils/staticwriter'); var TX = require('../primitives/tx'); /** * Chain State * @constructor */ function ChainState() { if (!(this instanceof ChainState)) return new ChainState(); this.startHeight = -1; this.startHash = encoding.NULL_HASH; this.height = -1; this.marked = false; } /** * Clone the state. * @returns {ChainState} */ ChainState.prototype.clone = function clone() { var state = new ChainState(); state.startHeight = this.startHeight; state.startHash = this.startHash; state.height = this.height; state.marked = this.marked; return state; }; /** * Inject properties from serialized data. * @private * @param {Buffer} data */ ChainState.prototype.fromRaw = function fromRaw(data) { var br = new BufferReader(data); this.startHeight = br.readU32(); this.startHash = br.readHash('hex'); this.height = br.readU32(); this.marked = true; if (br.left() > 0) this.marked = br.readU8() === 1; return this; }; /** * Instantiate chain state from serialized data. * @param {Hash} hash * @param {Buffer} data * @returns {ChainState} */ ChainState.fromRaw = function fromRaw(data) { return new ChainState().fromRaw(data); }; /** * Serialize the chain state. * @returns {Buffer} */ ChainState.prototype.toRaw = function toRaw() { var bw = new StaticWriter(41); bw.writeU32(this.startHeight); bw.writeHash(this.startHash); bw.writeU32(this.height); bw.writeU8(this.marked ? 1 : 0); return bw.render(); }; /** * Block Meta * @constructor * @param {Hash} hash * @param {Number} height * @param {Number} ts */ function BlockMeta(hash, height, ts) { if (!(this instanceof BlockMeta)) return new BlockMeta(hash, height, ts); this.hash = hash || encoding.NULL_HASH; this.height = height != null ? height : -1; this.ts = ts || 0; } /** * Clone the block. * @returns {BlockMeta} */ BlockMeta.prototype.clone = function clone() { return new BlockMeta(this.hash, this.height, this.ts); }; /** * Get block meta hash as a buffer. * @returns {Buffer} */ BlockMeta.prototype.toHash = function toHash() { return new Buffer(this.hash, 'hex'); }; /** * Instantiate block meta from chain entry. * @private * @param {ChainEntry} entry */ BlockMeta.prototype.fromEntry = function fromEntry(entry) { this.hash = entry.hash; this.height = entry.height; this.ts = entry.ts; return this; }; /** * Instantiate block meta from json object. * @private * @param {Object} json */ BlockMeta.prototype.fromJSON = function fromJSON(json) { this.hash = util.revHex(json.hash); this.height = json.height; this.ts = json.ts; return this; }; /** * Instantiate block meta from serialized tip data. * @private * @param {Buffer} data */ BlockMeta.prototype.fromRaw = function fromRaw(data) { var br = new BufferReader(data); this.hash = br.readHash('hex'); this.height = br.readU32(); this.ts = br.readU32(); return this; }; /** * Instantiate block meta from chain entry. * @param {ChainEntry} entry * @returns {BlockMeta} */ BlockMeta.fromEntry = function fromEntry(entry) { return new BlockMeta().fromEntry(entry); }; /** * Instantiate block meta from json object. * @param {Object} json * @returns {BlockMeta} */ BlockMeta.fromJSON = function fromJSON(json) { return new BlockMeta().fromJSON(json); }; /** * Instantiate block meta from serialized data. * @param {Hash} hash * @param {Buffer} data * @returns {BlockMeta} */ BlockMeta.fromRaw = function fromRaw(data) { return new BlockMeta().fromRaw(data); }; /** * Serialize the block meta. * @returns {Buffer} */ BlockMeta.prototype.toRaw = function toRaw() { var bw = new StaticWriter(42); bw.writeHash(this.hash); bw.writeU32(this.height); bw.writeU32(this.ts); return bw.render(); }; /** * Convert the block meta to a more json-friendly object. * @returns {Object} */ BlockMeta.prototype.toJSON = function toJSON() { return { hash: util.revHex(this.hash), height: this.height, ts: this.ts }; }; /** * Wallet Block * @constructor * @param {Hash} hash * @param {Number} height */ function BlockMapRecord(height) { if (!(this instanceof BlockMapRecord)) return new BlockMapRecord(height); this.height = height != null ? height : -1; this.txs = []; this.index = {}; } /** * Instantiate wallet block from serialized data. * @private * @param {Hash} hash * @param {Buffer} data */ BlockMapRecord.prototype.fromRaw = function fromRaw(data) { var br = new BufferReader(data); var count = br.readU32(); var i, hash, tx; for (i = 0; i < count; i++) { hash = br.readHash('hex'); tx = TXMapRecord.fromReader(hash, br); this.txs.push(tx); this.index[tx.hash] = tx; } return this; }; /** * Instantiate wallet block from serialized data. * @param {Hash} hash * @param {Buffer} data * @returns {BlockMapRecord} */ BlockMapRecord.fromRaw = function fromRaw(height, data) { return new BlockMapRecord(height).fromRaw(data); }; /** * Calculate serialization size. * @returns {Number} */ BlockMapRecord.prototype.getSize = function getSize() { var size = 0; var i, tx; size += 4; for (i = 0; i < this.txs.length; i++) { tx = this.txs[i]; size += 32; size += tx.getSize(); } return size; }; /** * Serialize the wallet block as a block. * Contains matching transaction hashes. * @returns {Buffer} */ BlockMapRecord.prototype.toRaw = function toRaw() { var size = this.getSize(); var bw = new StaticWriter(size); var i, tx; bw.writeU32(this.txs.length); for (i = 0; i < this.txs.length; i++) { tx = this.txs[i]; bw.writeHash(tx.hash); tx.toWriter(bw); } return bw.render(); }; /** * Add a hash and wid pair to the block. * @param {Hash} hash * @param {WalletID} wid * @returns {Boolean} */ BlockMapRecord.prototype.add = function add(hash, wid) { var tx = this.index[hash]; if (!tx) { tx = new TXMapRecord(hash); tx.wids.push(wid); this.txs.push(tx); this.index[tx.hash] = tx; return true; } return tx.add(wid); }; /** * Remove a hash and wid pair from the block. * @param {Hash} hash * @param {WalletID} wid * @returns {Boolean} */ BlockMapRecord.prototype.remove = function remove(hash, wid) { var tx = this.index[hash]; var result; if (!tx) return false; if (!tx.remove(wid)) return false; if (tx.wids.length === 0) { result = util.binaryRemove(this.txs, tx, cmpid); assert(result); delete this.index[tx.hash]; } return true; }; /** * TX Hash * @constructor */ function TXMapRecord(hash, wids) { this.hash = hash || encoding.NULL_HASH; this.wids = wids || []; this.id = TXMapRecord.id++; } TXMapRecord.id = 0; TXMapRecord.prototype.add = function add(wid) { return util.binaryInsert(this.wids, wid, cmp, true) !== -1; }; TXMapRecord.prototype.remove = function remove(wid) { return util.binaryRemove(this.wids, wid, cmp); }; TXMapRecord.prototype.toWriter = function toWriter(bw) { return serializeWallets(bw, this.wids); }; TXMapRecord.prototype.getSize = function getSize() { return sizeWallets(this.wids); }; TXMapRecord.prototype.toRaw = function toRaw() { var size = this.getSize(); return this.toWriter(new StaticWriter(size)).render(); }; TXMapRecord.prototype.fromReader = function fromReader(br) { this.wids = parseWallets(br); return this; }; TXMapRecord.prototype.fromRaw = function fromRaw(data) { return this.fromReader(new BufferReader(data)); }; TXMapRecord.fromReader = function fromReader(hash, br) { return new TXMapRecord(hash).fromReader(br); }; TXMapRecord.fromRaw = function fromRaw(hash, data) { return new TXMapRecord(hash).fromRaw(data); }; /** * Outpoint Map * @constructor */ function OutpointMapRecord(hash, index, wids) { this.hash = hash || encoding.NULL_HASH; this.index = index != null ? index : -1; this.wids = wids || []; } OutpointMapRecord.prototype.add = function add(wid) { return util.binaryInsert(this.wids, wid, cmp, true) !== -1; }; OutpointMapRecord.prototype.remove = function remove(wid) { return util.binaryRemove(this.wids, wid, cmp); }; OutpointMapRecord.prototype.toWriter = function toWriter(bw) { return serializeWallets(bw, this.wids); }; OutpointMapRecord.prototype.getSize = function getSize() { return sizeWallets(this.wids); }; OutpointMapRecord.prototype.toRaw = function toRaw() { var size = this.getSize(); return this.toWriter(new StaticWriter(size)).render(); }; OutpointMapRecord.prototype.fromReader = function fromReader(br) { this.wids = parseWallets(br); return this; }; OutpointMapRecord.prototype.fromRaw = function fromRaw(data) { return this.fromReader(new BufferReader(data)); }; OutpointMapRecord.fromReader = function fromReader(hash, index, br) { return new OutpointMapRecord(hash, index).fromReader(br); }; OutpointMapRecord.fromRaw = function fromRaw(hash, index, data) { return new OutpointMapRecord(hash, index).fromRaw(data); }; /** * Path Record * @constructor */ function PathMapRecord(hash, wids) { this.hash = hash || encoding.NULL_HASH; this.wids = wids || []; } PathMapRecord.prototype.add = function add(wid) { return util.binaryInsert(this.wids, wid, cmp, true) !== -1; }; PathMapRecord.prototype.remove = function remove(wid) { return util.binaryRemove(this.wids, wid, cmp); }; PathMapRecord.prototype.toWriter = function toWriter(bw) { return serializeWallets(bw, this.wids); }; PathMapRecord.prototype.getSize = function getSize() { return sizeWallets(this.wids); }; PathMapRecord.prototype.toRaw = function toRaw() { var size = this.getSize(); return this.toWriter(new StaticWriter(size)).render(); }; PathMapRecord.prototype.fromReader = function fromReader(br) { this.wids = parseWallets(br); return this; }; PathMapRecord.prototype.fromRaw = function fromRaw(data) { return this.fromReader(new BufferReader(data)); }; PathMapRecord.fromReader = function fromReader(hash, br) { return new PathMapRecord(hash).fromReader(br); }; PathMapRecord.fromRaw = function fromRaw(hash, data) { return new PathMapRecord(hash).fromRaw(data); }; /** * TXRecord * @constructor * @param {TX} tx * @param {BlockMeta?} block */ function TXRecord(tx, block) { if (!(this instanceof TXRecord)) return new TXRecord(tx, block); this.tx = null; this.hash = null; this.ps = util.now(); this.height = -1; this.block = null; this.index = -1; this.ts = 0; if (tx) this.fromTX(tx, block); } /** * Inject properties from tx and block. * @private * @param {TX} tx * @param {Block?} block * @returns {TXRecord} */ TXRecord.prototype.fromTX = function fromTX(tx, block) { this.tx = tx; this.hash = tx.hash('hex'); if (block) this.setBlock(block); return this; }; /** * Instantiate tx record from tx and block. * @param {TX} tx * @param {Block?} block * @returns {TXRecord} */ TXRecord.fromTX = function fromTX(tx, block) { return new TXRecord().fromTX(tx, block); }; /** * Set block data (confirm). * @param {BlockMeta} block */ TXRecord.prototype.setBlock = function setBlock(block) { this.height = block.height; this.block = block.hash; this.ts = block.ts; }; /** * Unset block (unconfirm). */ TXRecord.prototype.unsetBlock = function unsetBlock() { this.height = -1; this.block = null; this.ts = 0; }; /** * Convert tx record to a block meta. * @returns {BlockMeta} */ TXRecord.prototype.getBlock = function getBlock() { if (this.height === -1) return; return new BlockMeta(this.block, this.height, this.ts); }; /** * Calculate current number of transaction confirmations. * @param {Number} height - Current chain height. * @returns {Number} confirmations */ TXRecord.prototype.getDepth = function getDepth(height) { assert(typeof height === 'number', 'Must pass in height.'); if (this.height === -1) return 0; if (height < this.height) return 0; return height - this.height + 1; }; /** * Get serialization size. * @returns {Number} */ TXRecord.prototype.getSize = function getSize() { var size = 0; size += this.tx.getSize(); size += 4; if (this.block) { size += 1; size += 32; size += 4 * 3; } else { size += 1; } return size; }; /** * Serialize a transaction to "extended format". * @returns {Buffer} */ TXRecord.prototype.toRaw = function toRaw() { var size = this.getSize(); var bw = new StaticWriter(size); var index = this.index; this.tx.toWriter(bw); bw.writeU32(this.ps); if (this.block) { if (index === -1) index = 0x7fffffff; bw.writeU8(1); bw.writeHash(this.block); bw.writeU32(this.height); bw.writeU32(this.ts); bw.writeU32(index); } else { bw.writeU8(0); } return bw.render(); }; /** * Inject properties from "extended" format. * @private * @param {Buffer} data */ TXRecord.prototype.fromRaw = function fromRaw(data) { var br = new BufferReader(data); this.tx = new TX(); this.tx.fromReader(br); this.hash = this.tx.hash('hex'); this.ps = br.readU32(); if (br.readU8() === 1) { this.block = br.readHash('hex'); this.height = br.readU32(); this.ts = br.readU32(); this.index = br.readU32(); if (this.index === 0x7fffffff) this.index = -1; } return this; }; /** * Instantiate a transaction from a buffer * in "extended" serialization format. * @param {Buffer} data * @returns {TX} */ TXRecord.fromRaw = function fromRaw(data) { return new TXRecord().fromRaw(data); }; /* * Helpers */ function cmp(a, b) { return a - b; } function cmpid(a, b) { return a.id - b.id; } function parseWallets(br) { var count = br.readU32(); var wids = []; var i; for (i = 0; i < count; i++) wids.push(br.readU32()); return wids; } function sizeWallets(wids) { return 4 + wids.length * 4; } function serializeWallets(bw, wids) { var i, wid; bw.writeU32(wids.length); for (i = 0; i < wids.length; i++) { wid = wids[i]; bw.writeU32(wid); } return bw; } /* * Expose */ exports.ChainState = ChainState; exports.BlockMeta = BlockMeta; exports.BlockMapRecord = BlockMapRecord; exports.TXMapRecord = TXMapRecord; exports.OutpointMapRecord = OutpointMapRecord; exports.PathMapRecord = PathMapRecord; exports.TXRecord = TXRecord; module.exports = exports;