@bpanel/bpanel-utils
Version:
Utilities for bpanel
747 lines (630 loc) • 22.3 kB
JavaScript
'use strict';
exports.__esModule = true;
exports.UXTXOptions = exports.UXTX = undefined;
var _getIterator2 = require('babel-runtime/core-js/get-iterator');
var _getIterator3 = _interopRequireDefault(_getIterator2);
var _entries = require('babel-runtime/core-js/object/entries');
var _entries2 = _interopRequireDefault(_entries);
var _typeof2 = require('babel-runtime/helpers/typeof');
var _typeof3 = _interopRequireDefault(_typeof2);
var _bcoin = require('bcoin');
var _primitives = require('./primitives');
var _bsert = require('bsert');
var _bsert2 = _interopRequireDefault(_bsert);
var _moment = require('moment');
var _moment2 = _interopRequireDefault(_moment);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/**
* User Experience Transaction
* NOTE: this class is not meant to be mutable
*
* instantiate with the static method
* fromOptions to allow for dynamic subclassing
*
* @alias module:bpanel-utils.UXTX
*/
// TODO: replace Amount with currency tool
class UXTX {
constructor(options, SuperClass) {
/*
* this class inherits from bcoin like
* TX primitive, for dynamic subclassing,
* pass in a SuperClass or automatically
* determine SuperClass based on chain
*/
if (!SuperClass) {
var _chain = void 0;
if (options && options.chain) _chain = options.chain;else _chain = 'bitcoin';
SuperClass = _primitives.primitives.TX[_chain];
(0, _bsert2.default)(SuperClass, 'must use valid chain');
}
class TX extends SuperClass {
/**
* Create a UXTX
* User Experience Transaction
* @constructor
* @param options
*
* TODO: handle these cases:
* wallet/account -> same wallet/account
* wallet/account -> same wallet/different account
* coinjoin transactions
*
* TODO: overwrite TX.inspect
*/
constructor(options) {
super();
// these are used as keys to look up
// in options.labels for toJSON labels
this.TYPES = {
COINBASE: 'COINBASE',
WITHDRAW: 'WITHDRAW',
DEPOSIT: 'DEPOSIT',
UNKNOWN: 'UNKNOWN'
};
this.DATE_FORMAT = null;
this._UXType = null;
this._labels = null;
this._json = null;
this._wallet = null;
this._account = null;
this._accounts = null;
this._counterparty = null;
this._recipients = null;
this._chain = null;
if (options) this.fromOptions(options);
}
/**
* Inject properties from options object.
* @private
* @param {Object} options
* @param {Object} options.json - json encoded transaction
* @param {Object} options.json.wallet - wallet the tx belongs to
* @param {Object} options.constants - for calculating human readable info
* @param {Object} options.constants.DATE_FORMAT - moment.js date format string
* @param {Object} options.labels - human readable labels
* @param {String} options.wallet - wallet that the txs belong to
* @param {String} options.chain - chain transaction is valid on (bitcoin or bitcoincash)
*/
fromOptions(options) {
super.fromOptions(options);
(0, _bsert2.default)(options.json, 'tx object is required');
(0, _bsert2.default)((0, _typeof3.default)(options.json) === 'object', 'tx json must be an object');
this._json = options.json;
if (options.constants.DATE_FORMAT) {
(0, _bsert2.default)(typeof options.constants.DATE_FORMAT === 'string');
this.DATE_FORMAT = options.constants.DATE_FORMAT;
}
if (options.labels) {
(0, _bsert2.default)((0, _typeof3.default)(options.labels) === 'object');
for (var _iterator = (0, _entries2.default)(options.labels), _isArray = Array.isArray(_iterator), _i = 0, _iterator = _isArray ? _iterator : (0, _getIterator3.default)(_iterator);;) {
var _ref2;
if (_isArray) {
if (_i >= _iterator.length) break;
_ref2 = _iterator[_i++];
} else {
_i = _iterator.next();
if (_i.done) break;
_ref2 = _i.value;
}
var _ref = _ref2;
var key = _ref[0];
var val = _ref[1];
(0, _bsert2.default)(typeof val === 'string');
}this._labels = options.labels;
}
if (options.wallet) {
(0, _bsert2.default)(typeof options.wallet === 'string');
this._wallet = options.wallet;
}
// prioritize wallet information on individual
// transactions over global wallet information
if (options.json && options.json.wallet) {
(0, _bsert2.default)(typeof options.json.wallet === 'string');
this._wallet = options.json.wallet;
}
if (options.chain) {
(0, _bsert2.default)(typeof options.chain === 'string');
(0, _bsert2.default)(options.chain === 'bitcoin' || options.chain === 'bitcoincash' || options.chain === 'handshake', 'must pass a supported chain');
this._chain = options.chain;
}
return this;
}
/**
* Return user experience transaction
* types, these differentiate the
* labels to be displayed to a user
*
* @returns {Object}
*/
getTypes() {
return this.TYPES;
}
/**
* Attempt to calculate if transaction is
* a withdrawal from a known account.
* Withdrawal defined by:
* more than 1 known input
* at least 1 known change output
*
* NOTE: not all withdrawals should actually have change
* but as of bcoin@1.0.2 they all do
*
* @returns {Boolean}
*/
isWithdraw() {
var json = this.getJSON();
var knownOutputs = this.getKnownCoins('outputs');
var knownInputs = this.getKnownCoins('inputs');
var changeOutputs = this.getChangeCoins(knownOutputs);
if (knownInputs.length > 0 && changeOutputs.length > 0) return true;
return false;
}
/*
* Attempt to calculate if transaction is
* a deposit.
* deposit defined by:
* no known inputs
* no change outputs
* @returns {Boolean}
*/
isDeposit() {
var json = this.getJSON();
var knownInputs = this.getKnownCoins('inputs');
var changeOutputs = this.getChangeCoins(json.outputs);
if (knownInputs.length === 0 && changeOutputs.length === 0) return true;
return false;
}
/*
* Get known coins
* known coin defined by:
* coin.path is not null
* type can be inputs, outputs
* undefined type gets both
* @param {String} type
* @returns {Boolean}
*
* TODO: cache and partially cache
* TODO: replace string input with enum
*/
getKnownCoins(type) {
var json = this.getJSON();
if (type === 'inputs') return json.inputs.filter(function (i) {
return i.path;
});
if (type === 'outputs') return json.outputs.filter(function (o) {
return o.path;
});
// destructuring into new array
return [].concat(json.inputs.filter(function (i) {
return i.path;
}), json.outputs.filter(function (o) {
return o.path;
}));
}
/*
* Get external coins
* external coin defined by:
* control by a private key
* external of the consumer's wallet
* @param {String} type
* @returns {Boolean}
*
* TODO: implement
* TODO: replace string input with enum
*/
getExternalCoins(type) {
throw new Error('Not Implemented Error');
}
/*
* Get change coins
* change coin defined by:
* coin assigned to change address
* @param {Object[]} coins
* @param {Object} coins[].path
* @param {Boolean} coins[].path.change
* @returns {Object[]}
*/
getChangeCoins(coins) {
return coins.filter(function (o) {
return o.path && o.path.change;
});
}
/*
* Get value for inputs, outputs or both
* Convert from satoshis to unit
* see bcoin.Amount for accepted amounts
* @param {String} type
* @param {String} [unit='btc']
* @returns {Number}
*/
getAmount(type) {
var unit = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'btc';
var coins = void 0;
var json = this.getJSON();
if (type === 'inputs') coins = json.inputs;else if (type === 'outputs') coins = json.outputs;else coins = [].concat(json.inputs, json.outputs);
var value = coins.reduce(function (a, o) {
return a + o.value;
}, 0);
var amount = new _bcoin.Amount(value, 'sat');
return amount.to(unit);
}
/*
* Get value from known coins
* and return as string for pretty printing
* Convert from satoshis to unit
* see bcoin.Amount for accepted amounts
* @param {String} [unit='btc']
* @param {Boolean} [formatted=false] - add prefix '+'/'-'
* @returns {String}
*/
getKnownAmount() {
var unit = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'btc';
var formatted = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
var coins = void 0;
var prefix = '';
switch (this.getUXType()) {
case this.TYPES.DEPOSIT:
coins = this.getKnownCoins('outputs');
prefix = '+';
break;
case this.TYPES.WITHDRAW:
coins = this.getKnownCoins('inputs');
prefix = '-';
break;
case this.TYPES.COINBASE:
coins = this.getJSON().outputs;
prefix = '+';
break;
case this.TYPES.UNKNOWN:
default:
// something went wrong...
coins = [];
}
var value = coins.reduce(function (a, c) {
return a + c.value;
}, 0);
var amount = new _bcoin.Amount(value, 'sat');
amount = amount.to(unit);
if (formatted) return '' + prefix + amount;
return '' + amount;
}
/*
* Get bcoin.TX json
* @returns {Object}
*
* TODO: rename this.getJSON to
* this.getTXJSON
*/
getJSON() {
(0, _bsert2.default)(this._json);
return this._json;
}
/*
* Get wallet name
* @returns {String}
*/
getWallet() {
return this._wallet;
}
/*
* Get chain type - bitcoin,bitcoincash
* @returns {String}
*/
getChain() {
return this._chain;
}
/*
* Get account names
* @returns {[]String}
*/
getAccounts() {
if (this._accounts) return this._accounts;
var accounts = void 0;
var labels = this.getLabels();
var json = this.getJSON();
switch (this.getUXType()) {
case this.TYPES.DEPOSIT:
// receive transaction
accounts = this.getKnownCoins('inputs').filter(function (i) {
return !i.path.change;
}) // filter out change utxo
.map(function (i) {
return i.path.name;
});
break;
case this.TYPES.WITHDRAW:
// send transaction
accounts = json.outputs.filter(function (o) {
// only will have path property
// if controlled by wallet
if (o.path) return !o.path.change;
// handle sending to controlled wallet
return true; // all remaining outputs included
}).map(function (o) {
if (o.path) // known outputs will have an account name
return o.path.name;
return null;
});
break;
case this.TYPES.COINBASE:
// null account when not controlled wallet
if (json.outputs[0].path) accounts = [json.outputs[0].path.name];else accounts = [null];
break;
case this.TYPES.UNKNOWN:
default:
accounts = [];
}
this._accounts = accounts;
return accounts;
}
/*
* Get displayed account name
* @returns {String}
*/
getAccount() {
if (this._account) return this._account;
var account = void 0;
var knownOutputs = this.getKnownCoins('outputs');
var knownInputs = this.getKnownCoins('inputs');
var json = this.getJSON();
var labels = this.getLabels();
switch (this.getUXType()) {
case this.TYPES.DEPOSIT:
// account that received coin
if (knownOutputs.length > 1) account = labels.MULTIPLE_ACCOUNT;else if (knownOutputs.length === 1) account = knownOutputs[0].path.name;else account = labels.UNKNOWN_ACCOUNT;
break;
case this.TYPES.WITHDRAW:
// account sending coin
if (knownInputs.length > 1) account = labels.MULTIPLE_ACCOUNT;else if (knownInputs.length === 1) account = knownInputs[0].path.name;else account = labels.UNKNOWN_ACCOUNT;
break;
case this.TYPES.COINBASE:
// account that received coin
// look before you leap
if (json.outputs.length === 1 && json.outputs[0].path) account = json.outputs[0].path.name;else account = labels.UNKNOWN_ACCOUNT;
break;
case this.TYPES.UNKNOWN:
default:
account = labels.UNKNOWN_ACCOUNT;
break;
}
this._account = account;
return account;
}
/*
* Get tx user experience type
* Determines which human readable labels
* are parsed
* @returns {String}
*/
getUXType() {
if (this._UXType) return this._UXType;
var UXType = void 0;
if (this.isCoinbase()) UXType = this.TYPES.COINBASE;else if (this.isDeposit()) UXType = this.TYPES.DEPOSIT;else if (this.isWithdraw()) UXType = this.TYPES.WITHDRAW;else UXType = this.TYPES.UNKNOWN;
this._UXType = UXType;
return UXType;
}
/*
* Get tx counterparty
* @returns {String}
*
* TODO: move accounts parsing out of this
* and into this.getAccounts
*
* TODO: move recipients parsing out of this
* and into this.getRecipients
*/
getCounterparty() {
var counterparty = void 0;
var accounts = void 0;
var labels = this.getLabels();
var json = this.getJSON();
switch (this.getUXType()) {
case this.TYPES.DEPOSIT:
// receive transaction
// parse counterparty
if (json.inputs.length > 1) counterparty = labels.MULTIPLE_ADDRESS;else if (json.inputs.length === 1) counterparty = json.inputs[0].address;
// TODO: this can be a null value
else counterparty = labels.UNKNOWN_ADDRESS;
break;
case this.TYPES.WITHDRAW:
// send transaction
// TODO: remove concept of recipients
var outputs = json.outputs.filter(function (o) {
// only will have path property
// if controlled by wallet
if (o.path) return !o.path.change;
// handle sending to controlled wallet
return true; // all remaining outputs included
});
var recipients = outputs.map(function (o) {
return o.address;
});
// parse counterpary based on number of recipients
if (recipients.length === 1) counterparty = recipients[0];else if (recipients.length > 1) counterparty = labels.MULTIPLE_ADDRESS;else counterparty = labels.UNKNOWN_ADDRESS;
break;
case this.TYPES.COINBASE:
// assume one input and one output
counterparty = json.outputs[0].address;
break;
case this.TYPES.UNKNOWN:
default:
counterparty = labels.UNKNOWN_ADDRESS;
}
this._counterparty = counterparty;
return counterparty;
}
/*
* Get tx recipients
* @returns {String}
*/
getRecipients() {
if (this._recipients) return this._recipients;
var recipients = void 0;
var labels = this.getLabels();
var json = this.getJSON();
switch (this.getUXType()) {
case this.TYPES.DEPOSIT:
// receive transaction
// parse recipients
// TODO: catch edge cases here around more complex txns
recipients = json.inputs.map(function (i) {
return i.address;
});
break;
case this.TYPES.WITHDRAW:
// send transaction
// parse recipients
var outputs = json.outputs.filter(function (o) {
// only will have path property
// if controlled by wallet
if (o.path) return !o.path.change;
// handle sending to controlled wallet
return true; // all remaining outputs included
}).map(function (o) {
return o.address;
});
break;
case this.TYPES.COINBASE:
// assume one input and one output
recipients = [json.outputs[0].address];
break;
case this.TYPES.UNKNOWN:
default:
recipients = [];
}
this._recipients = recipients;
return recipients;
}
/*
* Get human readable labels
* These can be configured with this.fromOptions
* @param {String} label - return a specific labels value
* @returns {Object|String}
*/
getLabels(label) {
if (label) return this._labels[label];
return this._labels;
}
/*
* Get human readable labels
* These can be configured with this.fromOptions
* @static
* @param {String|Buffer} data - raw tx data
* @param {String} enc - encoding raw tx data is in
* @param {Objects} options - UXTX options
* @returns {Object|String}
*/
static fromRaw(data, enc, options) {
if (typeof data === 'string') data = Buffer.from(data, enc);
return new this(options).fromRaw(data);
}
/*
* Get JSON with human readable values in it
* @returns {Object}
*
* TODO: consolidate output values to make most flexible
* TODO: don't hardcode segwit and coinbase labels
*/
toJSON() {
var json = this.getJSON();
var date = (0, _moment2.default)(json.date).format(this.DATE_FORMAT);
var uxtype = this.getUXType();
var counterparty = this.getCounterparty();
var recipients = this.getRecipients();
var accounts = this.getAccounts();
var wallet = this.getWallet();
var amount = this.getKnownAmount('btc', true);
var chain = this.getChain();
var account = this.getAccount();
var uxtypeLabel = this.getLabels(uxtype);
var isSegwit = undefined;
var weight = undefined;
if (chain !== 'bitcoincash') {
isSegwit = this.hasWitness();
weight = this.getWeight();
}
var isCoinbase = this.isCoinbase();
var inputAmount = this.getAmount('inputs');
var outputAmount = this.getAmount('outputs');
return {
hash: json.hash,
fee: json.fee,
rate: json.rate,
size: json.size,
block: json.block, // block hash
isSegwit: isSegwit,
isCoinbase: isCoinbase,
tx: json.tx,
height: json.height,
inputs: json.inputs,
outputs: json.outputs,
inputAmount: inputAmount,
outputAmount: outputAmount,
inputCount: json.inputs.length,
outputCount: json.outputs.length,
weight: weight,
mdate: json.mdate,
date: date,
amount: amount,
wallet: wallet, // must be provided by fn consumer
accounts: accounts,
confirmations: json.confirmations,
recipients: recipients,
addressLabel: counterparty,
accountLabel: account,
segwitLabel: isSegwit ? 'Yes' : 'No',
coinbaseLabel: isCoinbase ? 'Yes' : 'No',
chain: chain,
uxtype: uxtypeLabel
};
}
}
this._txClass = TX;
if (options) this.fromOptions(options);
}
static fromOptions(options, SuperClass) {
return new this(null, SuperClass).fromOptions(options);
}
// allow for dynamic subclassing
fromOptions(options, SuperClass) {
return new this._txClass(options).fromOptions(options);
}
static fromRaw(data, enc, options) {
if (typeof data === 'string') data = Buffer.from(data, enc);
return new this(options).fromRaw(data, enc, options);
}
fromRaw(data, enc, options) {
if (typeof data === 'string') data = Buffer.from(data, enc);
return this._txClass.fromRaw(data, enc, options);
}
}
// initial label values
// NOTE: many are the same, but
// they are decoupled
var labels = {
WITHDRAW: 'Sent',
DEPOSIT: 'Received',
COINBASE: 'Coinbase',
MULTIPLE_OUTPUT: 'Multiple',
MULTIPLE_ADDRESS: 'Multiple',
MULTIPLE_ACCOUNT: 'Multiple',
UNKNOWN_ADDRESS: 'Unknown',
UNKNOWN_ACCOUNT: 'Unknown'
};
// initial constants
var constants = {
DATE_FORMAT: 'MM/DD/YY hh:mm a'
};
// valid networks:
// bitcoin, bitcoincash, handshake
var chain = null;
// additional options for UXTX
var UXTXOptions = {
constants: constants,
labels: labels,
chain: chain,
json: null // tx json
};
exports.UXTX = UXTX;
exports.UXTXOptions = UXTXOptions;