bmultisig
Version:
Bcoin wallet plugin for multi signature transaction proposals
1,198 lines (934 loc) • 27.3 kB
JavaScript
/*!
* proposaldb.js - proposal database
* Copyright (c) 2018, The Bcoin Developers (MIT License).
* https://github.com/bcoin-org/bcoin
*/
'use strict';
const assert = require('bsert');
const {enforce} = assert;
const {Struct} = require('bufio');
const {Outpoint, MTX} = require('bcoin');
const MultisigMTX = require('./primitives/mtx');
const Proposal = require('./primitives/proposal');
const Cosigner = require('./primitives/cosigner');
const {MapLock, Lock} = require('bmutex');
const {BufferMap} = require('buffer-map');
const layout = require('./layout').proposaldb;
/**
* Proposal DB
* @alias module:multisig.ProposalDB
* @property {MultisigDB} msdb
* @property {BDB} db
* @property {Number} wid
* @property {Bucket} bucket
* @property {MultisigWallet} wallet
* @property {Number} depth
*/
class ProposalDB {
/**
* Create ProposalsDB
* @constructor
* @param {MultisigDB} msdb
* @param {Number} [wid=0]
*/
constructor(msdb, wid) {
this.msdb = msdb;
this.db = msdb.db;
this.logger = msdb.logger;
this.wid = wid || 0;
this.bucket = null;
this.wallet = null;
this.depth = 0;
this.readLock = new MapLock();
this.writeLock = new Lock();
}
/**
* Open ProposalsDB
* @async
* @param {MultisigWallet} wallet
*/
async open(wallet) {
const prefix = layout.prefix.encode(wallet.wid);
this.bucket = this.db.bucket(prefix);
this.wid = wallet.wid;
this.wallet = wallet;
this.depth = await this.getDepth();
// TODO: handle locked coins after reorgs.
const lockedOutpoints = await this.getLockedOutpoints();
for (const outpoint of lockedOutpoints)
this.wallet.lockCoinTXDB(outpoint);
}
/**
* Emit proposal event.
* @param {String} event
* @param {Object} data
* @param {Object} details
*/
emit(event, ...args) {
this.msdb.emit(event, this.wallet, ...args);
this.wallet.emit(event, ...args);
}
/**
* Lock the coin in db and in txdb
* @async
* @param {bdb#Batch} b
* @param {Proposal} proposal
* @param {bcoin#Coin} coin
*/
lockCoin(b, proposal, coin) {
this.wallet.lockCoinTXDB(coin);
Proposal.lockCoin(b, proposal, coin);
Proposal.savePIDByCoin(b, coin, proposal.id);
}
/**
* Unlock the coin in db and in txdb
* @param {bdb#Batch} b
* @param {Proposal} proposal
* @param {bcoin#Outpoint} outpoint
*/
unlockCoin(b, proposal, outpoint) {
this.wallet.unlockCoinTXDB(outpoint);
Proposal.unlockCoin(b, proposal, outpoint);
Proposal.removePIDByCoin(b, outpoint);
this.emit('unlocked coin', proposal, outpoint);
}
/**
* Test if coin is locked.
* @param {Outpoint} outpoint
* @returns {Boolean}
*/
async isLocked(outpoint) {
return Proposal.hasOutpoint(this.bucket, outpoint);
}
/**
* Get locked coins
* @async
* @returns {Promise<Outpoint[]>}
*/
getLockedOutpoints() {
return Proposal.getOutpoints(this.bucket);
}
/**
* Get proposal depth
* @returns {Promise<Number>}
*/
async getDepth() {
const raw = await this.bucket.get(layout.D.encode());
if (!raw)
return 0;
assert(raw.length === 4);
return raw.readUInt32BE(0);
}
/**
* Increment depth
* @async
*/
async increment(b) {
ProposalDB.increment(b, this.depth);
this.depth += 1;
}
/**
* Resolve id
* TODO: Clean up, now we use only pid,
* so extra check is no longer necessary.
* @param {Number} id
* @returns {Promise<Number>}
*/
async ensurePID(id) {
assert(typeof id === 'number');
if (await Proposal.has(this.bucket, id))
return id;
return -1;
}
/**
* List all proposals
* @returns {Promise<Proposal[]>}
*/
getProposals() {
return Proposal.getProposals(this.bucket);
}
/**
* List all pending proposals
* @returns {Promise<Proposal[]>}
*/
async getPendingProposals() {
const pending = await Proposal.getPendingProposalIDs(this.bucket);
const proposals = [];
for (const pid of pending)
proposals.push(await this._getProposal(pid));
return proposals;
}
/**
* Get proposal with lock
* @param {Number} id
* @returns {Promise<Proposal>}
*/
async getProposal(id) {
const pid = await this.ensurePID(id);
if (pid === -1)
return null;
const unlock = await this.readLock.lock(pid);
try {
return await this._getProposal(pid);
} finally {
unlock();
}
}
/**
* Get proposal without lock
* @param {Number} pid
* @returns {Promise<Proposal>}
*/
async _getProposal(pid) {
const proposal = await Proposal.getProposal(this.bucket, pid);
proposal.m = this.wallet.m;
proposal.n = this.wallet.n;
return proposal;
}
/**
* Get proposal transaction with readLock
* @param {Number} id
* @returns {Promise<TX?>}
*/
async getTX(id) {
const pid = await this.ensurePID(id);
if (pid === -1)
return null;
const unlock = await this.readLock.lock(pid);
try {
return await this._getTX(pid);
} finally {
unlock();
}
}
/**
* Get proposal transaction without lock.
* @param {Number} pid
* @returns {Promise<TX?>}
*/
_getTX(pid) {
return Proposal.getTX(this.bucket, pid);
}
/**
* Get proposal transaction with coinview
* with lock.
* @param {Number} id
* @returns {Promise<MTX?>}
*/
async getMTX(id) {
const pid = await this.ensurePID(id);
if (pid === -1)
return null;
const unlock = await this.readLock.lock(pid);
try {
return await this._getMTX(id);
} finally {
unlock();
}
}
/**
* Get proposal transaction with coinview
* without lock.
* @param {Number} id
* @returns {Promise<MTX?>}
*/
async _getMTX(id) {
const tx = await this._getTX(id);
const view = await this.wallet.getCoinView(tx);
const mtx = MTX.fromTX(tx);
mtx.view = view;
return mtx;
}
/**
* Get proposal IDs by tx inputs. (no locks)
* @param {TX} tx
* @returns {Promise<Number[]>}
*/
async getPIDsByTX(tx) {
if (tx.isCoinbase())
return [];
const pids = new Set();
for (const {prevout} of tx.inputs) {
const pid = await this.getPIDByOutpoint(prevout);
if (pid !== -1)
pids.add(pid);
}
return Array.from(pids);
}
/**
* Get proposal by coin/outpoint
* @async
* @param {Outpoint} outpoint
* @returns {Promise<Proposal?>}
*/
async getProposalByOutpoint(outpoint) {
const pid = await this.getPIDByOutpoint(outpoint);
if (pid === -1)
return null;
return this.getProposal(pid);
}
/**
* Get proposal ID by coin, outpoint
* @async
* @param {Outpoint} outpoint
* @returns {Promise<Number>} Proposal ID
*/
getPIDByOutpoint(outpoint) {
return Proposal.getPIDByOutpoint(this.bucket, outpoint);
}
/**
* Get proposal coins (use only on pending proposals)
* @param {Number} id
* @throws {Error}
*/
async getProposalOutpoints(id) {
const pid = await this.ensurePID(id);
if (pid === -1)
return null;
return Proposal.getProposalOutpoints(this.bucket, pid);
}
/**
* Create proposal
* @param {Object} options
* @param {String} options.memo
* @param {Number} options.timestamp
* @param {Cosigner} cosigner
* @param {MTX} mtx
* @param {Signature} signature
* @returns {Promise<Proposal>}
*/
async createProposal(options, cosigner, mtx, signature) {
const unlock = await this.writeLock.lock();
try {
return await this._createProposal(options, cosigner, mtx, signature);
} finally {
unlock();
}
}
/**
* Create proposal without lock
* @param {Object} options - original options.
* @param {String} options.memo
* @param {Number} options.timestamp
* @param {Cosigner} cosigner
* @param {MTX} mtx
* @param {Buffer} signature
* @returns {Promise<Proposal>}
*/
async _createProposal(options, cosigner, mtx, signature) {
enforce(options && typeof options === 'object', 'options', 'object');
enforce(cosigner instanceof Cosigner, 'cosigner', 'Cosigner');
enforce(Buffer.isBuffer(signature), 'signature', 'buffer');
assert(signature.length === 65, 'signature must be 65 bytes.');
enforce(MTX.isMTX(mtx), 'mtx', 'MTX');
const b = this.bucket.batch();
const id = this.depth;
const [tx, view] = mtx.commit();
// Should we store empty MTX,
// we will need to clean up inputs
// Should we cache relevant information?
// Rings, Paths ? (We are storing coins for reorgs).
const proposal = Proposal.fromOptions({
id: id,
memo: options.memo,
timestamp: options.timestamp,
signature: signature,
author: cosigner.id,
m: this.wallet.m,
n: this.wallet.n,
options: options
});
const walletName = this.wallet.id;
if (!proposal.verifyCreateSignature(walletName, cosigner.authPubKey))
throw new Error('proposal signature is not valid.');
this.increment(b);
const statsDelta = new ProposalStats();
for (const input of tx.inputs) {
const coin = view.getCoinFor(input);
if (!coin)
continue;
statsDelta.addOwnLockedCoin(1);
statsDelta.addOwnLockedBalance(coin.value);
this.lockCoin(b, proposal, coin);
}
statsDelta.addPending(1);
statsDelta.addProposals(1);
Proposal.saveProposal(b, proposal);
Proposal.saveTX(b, proposal.id, tx);
await this._updateStats(b, statsDelta);
await b.write();
this.emit('proposal created', proposal, tx);
return proposal;
}
/**
* Reject proposal
* @param {Number} id
* @param {Cosigner} cosigner
* @param {Signature} signature
* @returns {Promise<Proposal>}
* @throws {Error}
*/
async rejectProposal(id, cosigner, signature) {
const pid = await this.ensurePID(id);
if (pid === -1)
throw new Error('Proposal not found.');
const unlock1 = await this.readLock.lock(pid);
const unlock2 = await this.writeLock.lock();
try {
return await this._rejectProposal(pid, cosigner, signature);
} finally {
unlock2();
unlock1();
}
}
/**
* Reject proposal without locks
* @param {Number} pid
* @param {Cosigner} cosigner
* @param {Signature} signature
* @returns {Promise<Proposal>}
*/
async _rejectProposal(pid, cosigner, signature) {
enforce(cosigner instanceof Cosigner, 'cosigner', 'Cosigner');
enforce(Buffer.isBuffer(signature), 'signature', 'buffer');
assert(signature.length === 65, 'signature must be 65 bytes.');
const proposal = await this._getProposal(pid);
const validSignature = proposal.verifyRejectSignature(
this.wallet.id,
signature,
cosigner.authPubKey
);
if (!validSignature)
throw new Error('rejection signature is not valid.');
// this will check the status of the proposal.
proposal.reject(cosigner, signature);
if (!proposal.isRejected()) {
const b = this.bucket.batch();
Proposal.saveProposal(b, proposal);
await b.write();
this.emit('proposal rejected', proposal, cosigner);
return proposal;
}
const outpoints = await Proposal.getProposalOutpoints(this.bucket, pid);
const b = this.bucket.batch();
const statsDelta = new ProposalStats();
statsDelta.addPending(-1);
statsDelta.addRejected(1);
for (const outpoint of outpoints) {
const coin = await this.wallet.getCoin(outpoint.hash, outpoint.index);
statsDelta.addOwnLockedCoin(-1);
statsDelta.addOwnLockedBalance(-coin.value);
this.unlockCoin(b, proposal, outpoint);
}
Proposal.saveProposal(b, proposal);
await this._updateStats(b, statsDelta);
await b.write();
this.emit('proposal rejected', proposal, cosigner);
return proposal;
}
/**
* Approve proposal
* @param {Number} id
* @param {Cosigner} cosigner
* @param {Buffer[]} signatures
* @returns {Promise<Proposal>}
* @throws {Error}
*/
async approveProposal(id, cosigner, signatures) {
const pid = await this.ensurePID(id);
if (pid === -1)
throw new Error('Proposal not found.');
const unlock1 = await this.readLock.lock(pid);
const unlock2 = await this.writeLock.lock();
try {
return await this._approveProposal(pid, cosigner, signatures);
} finally {
unlock2();
unlock1();
}
}
async _approveProposal(pid, cosigner, signatures) {
enforce(Cosigner.isCosigner(cosigner), 'cosigner', 'Cosigner');
enforce(Array.isArray(signatures), 'signatures', 'Array');
const statsDelta = new ProposalStats();
const proposal = await this._getProposal(pid);
assert(proposal.isPending(), 'Proposal is not pending.');
const mtx = await this._getMTX(pid);
const msMTX = MultisigMTX.fromMTX(mtx);
msMTX.view = mtx.view;
const rings = await this.wallet.deriveInputs(mtx);
const check = this.deriveRings(cosigner, rings);
const valid = msMTX.checkSignatures(rings, signatures);
if (valid !== check)
throw new Error('Signature(s) incorrect.');
proposal.approve(cosigner, signatures);
if (proposal.isPending()) {
const b = this.bucket.batch();
Proposal.saveProposal(b, proposal);
await b.write();
this.emit('proposal approved', proposal, cosigner);
return proposal;
}
for (const id of proposal.approvals.keys()) {
const cosigner = this.wallet.cosigners[id];
this.deriveRings(cosigner, rings);
const applied = proposal.applySignatures(id, msMTX, rings);
if (!applied)
throw new Error('Could not apply.');
}
const verify = msMTX.verify();
if (verify) {
statsDelta.addPending(-1);
statsDelta.addApproved(1);
const b = this.bucket.batch();
Proposal.saveProposal(b, proposal);
Proposal.saveTX(b, proposal.id, msMTX);
await this._updateStats(b, statsDelta);
await b.write();
this.emit('proposal approved', proposal, cosigner, msMTX);
return proposal;
}
// should not happen
const b = this.bucket.batch();
// TODO: reuse coins from MTX
const outpoints = await this.getProposalOutpoints(pid);
for (const outpoint of outpoints) {
statsDelta.addOwnLockedCoin(-1);
statsDelta.addOwnLockedBalance(-outpoint.value);
this.unlockCoin(b, proposal, outpoint);
}
proposal.status = Proposal.status.VERIFY;
Proposal.saveProposal(b, proposal);
statsDelta.addApproved(-1);
statsDelta.addRejected(1);
await this._updateStats(statsDelta);
await b.write();
this.emit('proposal rejected', proposal);
return proposal;
}
/**
* Derive rings for cosigner
* @param {Cosigner} cosigner
* @param {bcoin.KeyRing[]} rings
* @returns {Number} - number of rings
*/
deriveRings(cosigner, rings) {
let check = 0;
for (const ring of rings) {
if (!ring)
continue;
const pubkey = cosigner.deriveKey(ring.branch, ring.index).publicKey;
ring.publicKey = pubkey;
ring.witness = this.wallet.witness;
ring.nested = ring.branch === 2;
check++;
}
return check;
}
/**
* @private
* @param {Outpoint[]} outpoints
* @param {TX} tx
* @param {TXDetails} details
* @return {Map}
*/
async getCoinMap(outpoints, tx, details) {
const coinMap = new BufferMap();
for (const [i, input] of details.inputs.entries()) {
if (input.path) {
const op = tx.inputs[i].prevout;
coinMap.set(op.toKey(), input.value);
}
}
for (const [i, output] of details.outputs.entries()) {
if (output.path) {
const op = Outpoint.fromTX(tx, i);
coinMap.set(op.toKey(), output.value);
}
}
for (const outpoint of outpoints) {
const key = outpoint.toKey();
const value = coinMap.get(key);
if (value == null) {
const coin = await this.wallet.getCoin(outpoint.hash, outpoint.index);
coinMap.set(key, coin.value);
}
}
return coinMap;
}
/**
* Reject proposal if it is pending
* rejects double spent proposals
* @private
* @param {Number} pid
* @returns {Boolean}
*/
async _unlockProposalCoins(pid, tx, details) {
const proposal = await this._getProposal(pid);
if (!proposal || proposal.isRejected())
return false;
const outpoints = await Proposal.getProposalOutpoints(this.bucket, pid);
const coinMap = await this.getCoinMap(outpoints, tx, details);
if (proposal.isApproved()) {
const b = this.bucket.batch();
const statsDelta = new ProposalStats();
for (const outpoint of outpoints) {
const value = coinMap.get(outpoint.toKey());
assert(value != null);
statsDelta.addOwnLockedCoin(-1);
statsDelta.addOwnLockedBalance(-value);
this.unlockCoin(b, proposal, outpoint);
}
await this._updateStats(b, statsDelta);
await b.write();
return true;
}
assert(proposal.isPending());
proposal.forceReject(Proposal.status.DBLSPEND);
const b = this.bucket.batch();
const statsDelta = new ProposalStats();
statsDelta.addPending(-1);
statsDelta.addRejected(1);
for (const outpoint of outpoints) {
const value = coinMap.get(outpoint.toKey());
assert(value != null);
statsDelta.addOwnLockedCoin(-1);
statsDelta.addOwnLockedBalance(-value);
this.unlockCoin(b, proposal, outpoint);
}
Proposal.saveProposal(b, proposal);
await this._updateStats(b, statsDelta);
await b.write();
this.emit('proposal rejected', proposal);
return true;
}
/**
* Send approved proposal.
* @param {Number|String} id
* @returns {Promise<TX>}
* @throws {Error}
*/
async sendProposal(id) {
const pid = await this.ensurePID(id);
if (pid === -1)
throw new Error('Proposal not found.');
const unlock = await this.readLock.lock(pid);
try {
return await this._sendProposal(pid);
} finally {
unlock();
}
}
async _sendProposal(pid) {
const proposal = await this._getProposal(pid);
assert(proposal.isApproved(), 'Can only send approved proposal tx.');
const tx = await this._getTX(pid);
await this.wallet.send(tx);
return tx;
}
/**
* Reject proposals if transaction
* contains same coins and proposal
* is pending. Unlock coins if the proposal
* has been approved.
* @private
* @param {TX} tx
* @param {Details} details
*/
async _addTX(tx, details) {
if (tx.isCoinbase())
return;
const pids = await this.getPIDsByTX(tx);
for (const pid of pids)
await this._unlockProposalCoins(pid, tx, details);
}
/**
* Transaction was added in mempool.
* @private
* @param {bcoin#TX} tx
* @param {bcoin#TXDB#Details} details
*/
async addTX(tx, details) {
const pids = await this.getPIDsByTX(tx);
const readUnlocks = [];
const writeUnlock = await this.writeLock.lock();
for (const pid of pids)
readUnlocks.push(await this.readLock.lock(pid));
try {
return await this._addTX(tx, details);
} finally {
for (const unlock of readUnlocks)
unlock();
writeUnlock();
}
}
/**
* Transaction was confirmed.
* @private
* @param {bcoin#TX} tx
* @param {bcoin#TXDB#Details} details
*/
async confirmedTX(tx, details) {
const pids = await this.getPIDsByTX(tx);
const readUnlocks = [];
const writeUnlock = await this.writeLock.lock();
for (const pid of pids)
readUnlocks.push(await this.readLock.lock(pid));
try {
return await this._addTX(tx, details);
} finally {
for (const unlock of readUnlocks)
unlock();
writeUnlock();
}
}
/**
* Transaction was removed.
* Check if we have coins in removed transaction.
* Reject proposal if necessary.
* @private
* @param {bcoin#TX} tx
* @param {bcoin#TXDB#Details} details
*/
async removeTX(tx, details) {
const pids = new Set();
for (let i = 0; i < tx.outputs.length; i++) {
const outpoint = Outpoint.fromTX(tx, i);
const pid = await this.getPIDByOutpoint(outpoint);
if (pid !== -1)
pids.add(pid);
}
const readUnlocks = [];
const writeUnlock = await this.writeLock.lock();
for (const pid of pids)
readUnlocks.push(await this.readLock.lock(pid));
try {
for (const pid of pids) {
await this._unlockProposalCoins(pid, tx, details);
}
} finally {
for (const unlock of readUnlocks)
unlock();
writeUnlock();
}
}
/**
* Force reject proposal with locks.
* @param {Number} id
* @param {status} [status = status.FORCE]
* @returns {Promise<Proposal>}
*/
async forceRejectProposal(id, status = Proposal.status.FORCE) {
const pid = await this.ensurePID(id);
const unlock1 = await this.readLock.lock(pid);
const unlock2 = await this.writeLock.lock();
try {
return await this._forceRejectProposal(pid, status);
} finally {
unlock2();
unlock1();
}
}
/**
* Force reject proposal.
* @param {Number} pid
* @returns {Promise<Proposal>}
*/
async _forceRejectProposal(pid, status) {
const proposal = await this._getProposal(pid);
assert(proposal, 'Proposal not found.');
assert(proposal.isPending(), 'Proposal is not pending.');
const outpoints = await this.getProposalOutpoints(pid);
const statsDelta = new ProposalStats();
const b = this.bucket.batch();
proposal.forceReject(status);
statsDelta.addPending(-1);
statsDelta.addRejected(1);
for (const outpoint of outpoints) {
const coin = await this.wallet.getCoin(outpoint.hash, outpoint.index);
statsDelta.addOwnLockedCoin(-1);
statsDelta.addOwnLockedBalance(-coin.value);
this.unlockCoin(b, proposal, outpoint);
}
Proposal.saveProposal(b, proposal);
await this._updateStats(b, statsDelta);
await b.write();
this.emit('proposal rejected', proposal);
return proposal;
}
/**
* Get proposal db stats
*/
async getStats() {
const raw = await this.bucket.get(layout.S.encode());
if (!raw)
return new ProposalStats();
return ProposalStats.decode(raw);
}
/**
* Update proposal stats.
* @param {bdb#Batch} b
* @param {ProposalStats} statsDelta
* @returns {Promise<ProposalStats>} - current stats
*/
async updateStats(b, statsDelta) {
const unlock = this.writeLock.lock();
try {
return await this._updateStats(b, statsDelta);
} finally {
unlock();
}
}
async _updateStats(b, statsDelta) {
assert(statsDelta instanceof ProposalStats);
const currentStats = await this.getStats();
currentStats.apply(statsDelta);
ProposalDB.saveStats(b, currentStats);
return currentStats;
}
/*
* layout
*/
/**
* Increment proposal depth
* @param {bdb#Batch} b
* @param {Number} depth
*/
static increment(b, depth) {
b.put(layout.D.encode(), fromU32BE(depth + 1));
}
/**
* Save stats
* @param {bdb#Batch} b
* @param {ProposalStats} stats
*/
static saveStats(b, stats) {
b.put(layout.S.encode(), stats.toRaw());
}
}
/**
* General information about proposals.
* Locked coins is number of coins we have locked.
* Locked balance is sum of all coins.
* @alias module:multisig.ProposalStats
* @property {Number} lockedOwnCoins - Locked coins that belong to wallet.
* @property {Number} lockedOwnBalance - Sum of all lockedOwnCoins.
* @property {Number} proposals - Number of proposals(should be same as Depth)
* @property {Number} pending - Number of pending proposals.
* @property {Number} approved - Number of approved proposals.
* @property {Number} rejected - NUmber of rejected proposals.
*/
class ProposalStats extends Struct {
constructor() {
super();
this.lockedOwnCoins = 0;
this.lockedOwnBalance = 0;
this.proposals = 0;
this.pending = 0;
this.approved = 0;
this.rejected = 0;
}
size() {
return 32;
}
write(bw) {
bw.writeU64(this.lockedOwnCoins);
bw.writeU64(this.lockedOwnBalance);
bw.writeU32(this.proposals);
bw.writeU32(this.pending);
bw.writeU32(this.approved);
bw.writeU32(this.rejected);
return bw;
}
read(br) {
this.lockedOwnCoins = br.readU64();
this.lockedOwnBalance = br.readU64();
this.proposals = br.readU32();
this.pending = br.readU32();
this.approved = br.readU32();
this.rejected = br.readU32();
return this;
}
fromJSON(json) {
enforce(json, 'json', 'object');
enforce(Number.isSafeInteger(json.lockedOwnCoins),
'json.lockedOwnCoins', 'u64');
enforce(Number.isSafeInteger(json.lockedOwnBalance),
'json.lockedOwnBalance', 'u64');
enforce((json.proposals >>> 0) === json.proposals,
'json.proposals', 'u32');
enforce((json.pending >>> 0) === json.pending,
'json.pending', 'u32');
enforce((json.approved >>> 0) === json.approved,
'json.approved', 'u32');
enforce((json.rejected >>> 0) === json.rejected,
'json.rejected', 'u32');
this.lockedOwnCoins = json.lockedOwnCoins;
this.lockedOwnBalance = json.lockedOwnBalance;
this.proposals = json.proposals;
this.pending = json.pending;
this.approved = json.approved;
this.rejected = json.rejected;
return this;
}
getJSON() {
return {
lockedOwnCoins: this.lockedOwnCoins,
lockedOwnBalance: this.lockedOwnBalance,
proposals: this.proposals,
pending: this.pending,
approved: this.approved,
rejected: this.rejected
};
}
/**
* Apply another info to the current one.
* We use proposal info to track existing
* progress on the fly and apply in the end.
* @param {ProposalStats} info
* @returns {ProposalStats}
*/
apply(info) {
this.lockedOwnCoins += info.lockedOwnCoins;
this.lockedOwnBalance += info.lockedOwnBalance;
this.proposals += info.proposals;
this.pending += info.pending;
this.approved += info.approved;
this.rejected += info.rejected;
assert(this.lockedOwnCoins >= 0);
assert(this.lockedOwnBalance >= 0);
assert(this.proposals >= 0);
assert(this.pending >= 0);
assert(this.approved >= 0);
assert(this.rejected >= 0);
return this;
}
addOwnLockedCoin(value) {
this.lockedOwnCoins += value;
}
addOwnLockedBalance(value) {
this.lockedOwnBalance += value;
}
addProposals(value) {
this.proposals += value;
}
addPending(value) {
this.pending += value;
}
addApproved(value) {
this.approved += value;
}
addRejected(value) {
this.rejected += value;
}
}
/*
* Helpers
*/
function fromU32BE(num) {
const data = Buffer.allocUnsafe(4);
data.writeUInt32BE(num, 0);
return data;
}
ProposalDB.ProposalStats = ProposalStats;
module.exports = ProposalDB;