/*!
* coinview.js - coin viewpoint object for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const {BufferMap} = require('buffer-map');
const Coins = require('./coins');
const UndoCoins = require('./undocoins');
const CoinEntry = require('./coinentry');
const View = require('../covenants/view');
const {BitView} = require('../covenants/bitfield');
/** @typedef {import('bufio').BufferReader} BufferReader */
/** @typedef {import('../types').Hash} Hash */
/** @typedef {import('../types').BufioWriter} BufioWriter */
/** @typedef {import('../primitives/coin')} Coin */
/** @typedef {import('../primitives/input')} Input */
/** @typedef {import('../primitives/output')} Output */
/** @typedef {import('../primitives/tx')} TX */
/** @typedef {import('../primitives/outpoint')} Outpoint */
/** @typedef {import('../wallet/path')} Path */
/** @typedef {import('../blockchain/chaindb')} ChainDB */
/**
* Coin View
* Represents a coin viewpoint:
* a snapshot of {@link Coins} objects.
* @alias module:coins.CoinView
* @property {Object} map
* @property {UndoCoins} undo
*/
class CoinView extends View {
/**
* Create a coin view.
* @constructor
*/
constructor() {
super();
this.map = new BufferMap();
this.undo = new UndoCoins();
this.bits = new BitView();
}
/**
* Get coins.
* @param {Hash} hash
* @returns {Coins} coins
*/
get(hash) {
return this.map.get(hash);
}
/**
* Test whether the view has an entry.
* @param {Hash} hash
* @returns {Boolean}
*/
has(hash) {
return this.map.has(hash);
}
/**
* Add coins to the collection.
* @param {Hash} hash
* @param {Coins} coins
* @returns {Coins}
*/
add(hash, coins) {
this.map.set(hash, coins);
return coins;
}
/**
* Ensure existence of coins object in the collection.
* @param {Hash} hash
* @returns {Coins}
*/
ensure(hash) {
const coins = this.map.get(hash);
if (coins)
return coins;
return this.add(hash, new Coins());
}
/**
* Remove coins from the collection.
* @param {Hash} hash
* @returns {Coins?}
*/
remove(hash) {
const coins = this.map.get(hash);
if (!coins)
return null;
this.map.delete(hash);
return coins;
}
/**
* Add a tx to the collection.
* @param {TX} tx
* @param {Number} height
* @returns {Coins}
*/
addTX(tx, height) {
const hash = tx.hash();
const coins = Coins.fromTX(tx, height);
return this.add(hash, coins);
}
/**
* Remove a tx from the collection.
* @param {TX} tx
* @param {Number} height
* @returns {Coins}
*/
removeTX(tx, height) {
const hash = tx.hash();
const coins = Coins.fromTX(tx, height);
for (const coin of coins.outputs.values())
coin.spent = true;
return this.add(hash, coins);
}
/**
* Add an entry to the collection.
* @param {Outpoint} prevout
* @param {CoinEntry} coin
* @returns {CoinEntry|null}
*/
addEntry(prevout, coin) {
const {hash, index} = prevout;
const coins = this.ensure(hash);
return coins.add(index, coin);
}
/**
* Add a coin to the collection.
* @param {Coin} coin
* @returns {CoinEntry|null}
*/
addCoin(coin) {
const coins = this.ensure(coin.hash);
return coins.addCoin(coin);
}
/**
* Add an output to the collection.
* @param {Outpoint} prevout
* @param {Output} output
* @returns {CoinEntry|null}
*/
addOutput(prevout, output) {
const {hash, index} = prevout;
const coins = this.ensure(hash);
return coins.addOutput(index, output);
}
/**
* Add an output to the collection by output index.
* @param {TX} tx
* @param {Number} index
* @param {Number} height
* @returns {CoinEntry|null}
*/
addIndex(tx, index, height) {
const hash = tx.hash();
const coins = this.ensure(hash);
return coins.addIndex(tx, index, height);
}
/**
* Spend an output.
* @param {Outpoint} prevout
* @returns {CoinEntry|null}
*/
spendEntry(prevout) {
const {hash, index} = prevout;
const coins = this.get(hash);
if (!coins)
return null;
const coin = coins.spend(index);
if (!coin)
return null;
this.undo.push(coin);
return coin;
}
/**
* Remove an output.
* @param {Outpoint} prevout
* @returns {CoinEntry|null}
*/
removeEntry(prevout) {
const {hash, index} = prevout;
const coins = this.get(hash);
if (!coins)
return null;
return coins.remove(index);
}
/**
* Test whether the view has an entry by prevout.
* @param {Outpoint} prevout
* @returns {Boolean}
*/
hasEntry(prevout) {
const {hash, index} = prevout;
const coins = this.get(hash);
if (!coins)
return false;
return coins.has(index);
}
/**
* Get a single entry by prevout.
* @param {Outpoint} prevout
* @returns {CoinEntry|null}
*/
getEntry(prevout) {
const {hash, index} = prevout;
const coins = this.get(hash);
if (!coins)
return null;
return coins.get(index);
}
/**
* Test whether an entry has been spent by prevout.
* @param {Outpoint} prevout
* @returns {Boolean}
*/
isUnspent(prevout) {
const {hash, index} = prevout;
const coins = this.get(hash);
if (!coins)
return false;
return coins.isUnspent(index);
}
/**
* Get a single coin by prevout.
* @param {Outpoint} prevout
* @returns {Coin|null}
*/
getCoin(prevout) {
const coins = this.get(prevout.hash);
if (!coins)
return null;
return coins.getCoin(prevout);
}
/**
* Get a single output by prevout.
* @param {Outpoint} prevout
* @returns {Output|null}
*/
getOutput(prevout) {
const {hash, index} = prevout;
const coins = this.get(hash);
if (!coins)
return null;
return coins.getOutput(index);
}
/**
* Get an HD path by prevout.
* Implemented in {@link WalletCoinView}.
* @param {Outpoint} prevout
* @returns {null}
*/
getPath(prevout) {
return null;
}
/**
* Get coins height by prevout.
* @param {Outpoint} prevout
* @returns {Number}
*/
getHeight(prevout) {
const coin = this.getEntry(prevout);
if (!coin)
return -1;
return coin.height;
}
/**
* Get coins coinbase flag by prevout.
* @param {Outpoint} prevout
* @returns {Boolean}
*/
isCoinbase(prevout) {
const coin = this.getEntry(prevout);
if (!coin)
return false;
return coin.coinbase;
}
/**
* Test whether the view has an entry by input.
* @param {Input} input
* @returns {Boolean}
*/
hasEntryFor(input) {
return this.hasEntry(input.prevout);
}
/**
* Get a single entry by input.
* @param {Input} input
* @returns {CoinEntry|null}
*/
getEntryFor(input) {
return this.getEntry(input.prevout);
}
/**
* Test whether an entry has been spent by input.
* @param {Input} input
* @returns {Boolean}
*/
isUnspentFor(input) {
return this.isUnspent(input.prevout);
}
/**
* Get a single coin by input.
* @param {Input} input
* @returns {Coin|null}
*/
getCoinFor(input) {
return this.getCoin(input.prevout);
}
/**
* Get a single output by input.
* @param {Input} input
* @returns {Output|null}
*/
getOutputFor(input) {
return this.getOutput(input.prevout);
}
/**
* Get a single path by input.
* Implemented in {@link WalletCoinView}.
* @param {Input} input
* @returns {null}
*/
getPathFor(input) {
return null;
}
/**
* Add an HD path to the collection.
* Implemented in {@link WalletCoinView}
* @param {Outpoint} prevout
* @param {Path} path
* @returns {Path|null}
*/
addPath(prevout, path) {
return null;
}
/**
* Get coins height by input.
* @param {Input} input
* @returns {Number}
*/
getHeightFor(input) {
return this.getHeight(input.prevout);
}
/**
* Get coins coinbase flag by input.
* @param {Input} input
* @returns {Boolean}
*/
isCoinbaseFor(input) {
return this.isCoinbase(input.prevout);
}
/**
* Retrieve coins from database.
* @method
* @param {ChainDB} db
* @param {Outpoint} prevout
* @returns {Promise} - Returns {@link CoinEntry}.
*/
async readCoin(db, prevout) {
const cache = this.getEntry(prevout);
if (cache)
return cache;
const coin = await db.readCoin(prevout);
if (!coin)
return null;
return this.addEntry(prevout, coin);
}
/**
* Read all input coins into unspent map.
* @method
* @param {ChainDB} db
* @param {TX} tx
* @returns {Promise} - Returns {Boolean}.
*/
async readInputs(db, tx) {
let found = true;
for (const {prevout} of tx.inputs) {
if (!await this.readCoin(db, prevout))
found = false;
}
return found;
}
/**
* Spend coins for transaction.
* @method
* @param {ChainDB} db
* @param {TX} tx
* @returns {Promise} - Returns {Boolean}.
*/
async spendInputs(db, tx) {
let i = 0;
while (i < tx.inputs.length) {
const len = Math.min(i + 4, tx.inputs.length);
const jobs = [];
for (; i < len; i++) {
const {prevout} = tx.inputs[i];
jobs.push(this.readCoin(db, prevout));
}
const coins = await Promise.all(jobs);
for (const coin of coins) {
if (!coin || coin.spent)
return false;
coin.spent = true;
this.undo.push(coin);
}
}
return true;
}
/**
* Calculate serialization size.
* @param {TX} tx
* @returns {Number}
*/
getSize(tx) {
let size = 0;
size += tx.inputs.length;
for (const {prevout} of tx.inputs) {
const coin = this.getEntry(prevout);
if (!coin)
continue;
size += coin.getSize();
}
return size;
}
/**
* Write coin data to buffer writer
* as it pertains to a transaction.
* @param {BufioWriter} bw
* @param {TX} tx
* @returns {BufioWriter}
*/
write(bw, tx) {
for (const {prevout} of tx.inputs) {
const coin = this.getEntry(prevout);
if (!coin) {
bw.writeU8(0);
continue;
}
bw.writeU8(1);
coin.write(bw);
}
return bw;
}
/**
* Read serialized view data from a buffer
* reader as it pertains to a transaction.
* @param {BufferReader} br
* @param {TX} tx
*/
read(br, tx) {
for (const {prevout} of tx.inputs) {
if (br.readU8() === 0)
continue;
const coin = CoinEntry.read(br);
this.addEntry(prevout, coin);
}
return this;
}
/**
* Read serialized view data from a buffer
* reader as it pertains to a transaction.
* @param {BufferReader} br
* @param {TX} tx
* @returns {CoinView}
*/
static read(br, tx) {
return new this().read(br, tx);
}
}
/*
* Expose
*/
module.exports = CoinView;