UNPKG

@bpanel/bpanel-utils

Version:
747 lines (630 loc) 22.3 kB
'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;