diff --git a/src/api/ledger/trustlines.js b/src/api/ledger/trustlines.js index 816b7067..4fd67e27 100644 --- a/src/api/ledger/trustlines.js +++ b/src/api/ledger/trustlines.js @@ -1,3 +1,5 @@ +/* @flow */ + 'use strict'; const _ = require('lodash'); const utils = require('./utils'); @@ -29,10 +31,12 @@ function getAccountLines(remote, address, ledgerVersion, options, marker, limit, }); } -/*:: type Options = {currency: string, counterparty: string, - limit: number, ledgerVersion: number} */ -function getTrustlines(account: string, options: Options, - callback: () => void): void { +function getTrustlines( + account: string, + options: {currency: string, counterparty: string, + limit: number, ledgerVersion: number}, + callback: () => void + ): void { validate.address(account); validate.options(options); diff --git a/src/core/amount.js b/src/core/amount.js index 41a41512..18a1baa2 100644 --- a/src/core/amount.js +++ b/src/core/amount.js @@ -3,30 +3,23 @@ // Represent Ripple amounts and currencies. // - Numbers in hex are big-endian. -var assert = require('assert'); -var extend = require('extend'); -var utils = require('./utils'); -var UInt160 = require('./uint160').UInt160; -var Seed = require('./seed').Seed; -var Currency = require('./currency').Currency; -var GlobalBigNumber = require('bignumber.js'); +const assert = require('assert'); +const extend = require('extend'); +const utils = require('./utils'); +const UInt160 = require('./uint160').UInt160; +const Seed = require('./seed').Seed; +const Currency = require('./currency').Currency; +const Value = require('./value').Value; +const IOUValue = require('./iouvalue').IOUValue; +const XRPValue = require('./xrpvalue').XRPValue; -var BigNumber = GlobalBigNumber.another({ - ROUNDING_MODE: GlobalBigNumber.ROUND_HALF_UP, - DECIMAL_PLACES: 40 -}); - - -function inverse(number) { - return (new BigNumber(number)).toPower(-1); -} - -function Amount() { +function Amount(value = new XRPValue(NaN)) { // Json format: // integer : XRP // { 'value' : ..., 'currency' : ..., 'issuer' : ...} + assert(value instanceof Value); - this._value = new BigNumber(NaN); + this._value = value; this._is_native = true; // Default to XRP. Only valid if value is not NaN. this._currency = new Currency(); this._issuer = new UInt160(); @@ -38,25 +31,17 @@ function Amount() { Amount.strict_mode = true; -var consts = { +const consts = { currency_xns: 0, currency_one: 1, xns_precision: 6, // bi_ prefix refers to "big integer" - // TODO: we shouldn't expose our BigNumber library publicly - bi_5: new BigNumber(5), - bi_7: new BigNumber(7), - bi_10: new BigNumber(10), - bi_1e14: new BigNumber(1e14), - bi_1e16: new BigNumber(1e16), - bi_1e17: new BigNumber(1e17), - bi_1e32: new BigNumber(1e32), - bi_man_max_value: new BigNumber('9999999999999999'), - bi_man_min_value: new BigNumber(1e15), - bi_xns_max: new BigNumber(1e17), - bi_xns_min: new BigNumber(-1e17), - bi_xns_unit: new BigNumber(1e6), + // man refers to mantissa + bi_man_max_value: '9999999999999999', + bi_man_min_value: Number(1e15).toString(), + bi_xns_max: Number(1e17).toString(), + bi_xns_min: Number(-1e17).toString(), cMinOffset: -96, cMaxOffset: 80, @@ -68,9 +53,11 @@ var consts = { min_value: '-1000000000000000e-96' }; -var MAX_XRP_VALUE = new BigNumber(1e11); -var MAX_IOU_VALUE = new BigNumber(consts.max_value); -var MIN_IOU_VALUE = (new BigNumber(consts.min_value)).abs(); +const MAX_XRP_VALUE = new XRPValue(1e11); +const MAX_IOU_VALUE = new IOUValue(consts.max_value); +const MIN_IOU_VALUE = new IOUValue(consts.min_value).abs(); + +const bi_xns_unit = new IOUValue(1e6); // Add constants to Amount class extend(Amount, consts); @@ -113,32 +100,36 @@ Amount.is_valid_full = function(j) { }; Amount.NaN = function() { - var result = new Amount(); - result._value = new BigNumber(NaN); // should have no effect + const result = new Amount(); + result._value = new IOUValue(NaN); // should have no effect return result; // but let's be careful }; // be sure that _is_native is set properly BEFORE calling _set_value -Amount.prototype._set_value = function(value, roundingMode) { - assert(value instanceof BigNumber); - this._value = value.isZero() && value.isNegative() ? value.negated() : value; - this.canonicalize(roundingMode); +Amount.prototype._set_value = function(value: Value) { + + this._value = value.isZero() && value.isNegative() ? + value.negate() : value; this._check_limits(); + }; // Returns a new value which is the absolute value of this. Amount.prototype.abs = function() { - return this.clone(this.is_negative()); + + return this._copy(this._value.abs()); + }; Amount.prototype.add = function(addend) { - var addendAmount = Amount.from_json(addend); + const addendAmount = Amount.from_json(addend); if (!this.is_comparable(addendAmount)) { - return new Amount(NaN); + return new Amount(); } - return this._copy(this._value.plus(addendAmount._value)); + return this._copy(this._value.add(addendAmount._value)); + }; Amount.prototype.subtract = function(subtrahend) { @@ -148,34 +139,21 @@ Amount.prototype.subtract = function(subtrahend) { // XXX Diverges from cpp. Amount.prototype.multiply = function(multiplicand) { - var multiplicandAmount = Amount.from_json(multiplicand); - // TODO: probably should just multiply by multiplicandAmount._value - var multiplyBy = multiplicandAmount.is_native() ? - multiplicandAmount._value.times(Amount.bi_xns_unit) - : multiplicandAmount._value; - return this._copy(this._value.times(multiplyBy)); + + const multiplicandAmount = Amount.from_json(multiplicand); + + return this._copy(this._value.multiply(multiplicandAmount._value)); + }; Amount.prototype.scale = function(scaleFactor) { - return this._copy(this._value.times(scaleFactor)); + return this.multiply(scaleFactor); }; Amount.prototype.divide = function(divisor) { - var divisorAmount = Amount.from_json(divisor); - if (!this.is_valid()) { - throw new Error('Invalid dividend'); - } - if (!divisorAmount.is_valid()) { - throw new Error('Invalid divisor'); - } - if (divisorAmount.is_zero()) { - throw new Error('divide by zero'); - } - // TODO: probably should just divide by divisorAmount._value - var divideBy = divisorAmount.is_native() ? - divisorAmount._value.times(Amount.bi_xns_unit) - : divisorAmount._value; - return this._copy(this._value.dividedBy(divideBy)); + const divisorAmount = Amount.from_json(divisor); + + return this._copy(this._value.divide(divisorAmount._value)); }; /** @@ -187,7 +165,7 @@ Amount.prototype.divide = function(divisor) { * price would be rendered as USD. * * @example - * var price = buy_amount.ratio_human(sell_amount); + * const price = buy_amount.ratio_human(sell_amount); * * @this {Amount} The numerator (top half) of the fraction. * @param {Amount} denominator The denominator (bottom half) of the fraction. @@ -198,12 +176,12 @@ Amount.prototype.divide = function(divisor) { * @return {Amount} The resulting ratio. Unit will be the same as numerator. */ -Amount.prototype.ratio_human = function(denominator, opts) { - opts = extend({ }, opts); +Amount.prototype.ratio_human = function(denom, opts) { + const options = extend({ }, opts); - var numerator = this.clone(); + const numerator = this.clone(); - denominator = Amount.from_json(denominator); + let denominator = Amount.from_json(denom); // If either operand is NaN, the result is NaN. if (!numerator.is_valid() || !denominator.is_valid()) { @@ -218,8 +196,8 @@ Amount.prototype.ratio_human = function(denominator, opts) { // // We only need to apply it to the second factor, because the currency unit of // the first factor will carry over into the result. - if (opts.reference_date) { - denominator = denominator.applyInterest(opts.reference_date); + if (options.reference_date) { + denominator = denominator.applyInterest(options.reference_date); } // Special case: The denominator is a native (XRP) amount. @@ -233,7 +211,7 @@ Amount.prototype.ratio_human = function(denominator, opts) { // // To compensate, we multiply the numerator by 10^xns_precision. if (denominator._is_native) { - numerator._set_value(numerator._value.times(Amount.bi_xns_unit)); + numerator._set_value(numerator.multiply(bi_xns_unit)); } return numerator.divide(denominator); @@ -249,7 +227,7 @@ Amount.prototype.ratio_human = function(denominator, opts) { * Intended use is to calculate something like: 10 USD * 10 XRP/USD = 100 XRP * * @example - * var sell_amount = buy_amount.product_human(price); + * let sell_amount = buy_amount.product_human(price); * * @see Amount#ratio_human * @@ -260,32 +238,32 @@ Amount.prototype.ratio_human = function(denominator, opts) { * for Ripple epoch. * @return {Amount} The product. Unit will be the same as the first factor. */ -Amount.prototype.product_human = function(factor, opts) { - opts = opts || {}; +Amount.prototype.product_human = function(factor, options = {}) { - factor = Amount.from_json(factor); + let fac = Amount.from_json(factor); // If either operand is NaN, the result is NaN. - if (!this.is_valid() || !factor.is_valid()) { - return new Amount(NaN); + if (!this.is_valid() || !fac.is_valid()) { + return new Amount(); } // Apply interest/demurrage // // We only need to apply it to the second factor, because the currency unit of // the first factor will carry over into the result. - if (opts.reference_date) { - factor = factor.applyInterest(opts.reference_date); + if (options.reference_date) { + fac = fac.applyInterest(options.reference_date); } - var product = this.multiply(factor); + const product = this.multiply(fac); // Special case: The second factor is a native (XRP) amount expressed as base // units (1 XRP = 10^xns_precision base units). // // See also Amount#ratio_human. - if (factor._is_native) { - product._set_value(product._value.dividedBy(Amount.bi_xns_unit)); + if (fac._is_native) { + const quotient = product.divide(bi_xns_unit.toString()); + product._set_value(quotient._value); } return product; @@ -298,7 +276,7 @@ Amount.prototype.product_human = function(factor, opts) { * @private */ Amount.prototype._invert = function() { - this._set_value(inverse(this._value)); + this._set_value(this._value.invert()); return this; }; @@ -313,7 +291,7 @@ Amount.prototype.invert = function() { }; /** - * Canonicalize amount value + * Canonicalize amount value is now taken care of in the Value classes * * Mirrors rippled's internal Amount representation * From https://github.com/ripple/rippled/blob/develop/src/ripple/data @@ -342,16 +320,6 @@ Amount.prototype.invert = function() { * bigger than supported */ -Amount.prototype.canonicalize = function(roundingMode) { - if (this._is_native) { - this._value = this._value.round(6, BigNumber.ROUND_DOWN); - } else if (roundingMode) { - this._value = new BigNumber(this._value.toPrecision(16, roundingMode)); - } else { - this._value = new BigNumber(this._value.toPrecision(16)); - } -}; - Amount.prototype._check_limits = function() { if (!Amount.strict_mode) { return this; @@ -359,7 +327,7 @@ Amount.prototype._check_limits = function() { if (this._value.isNaN() || this._value.isZero()) { return this; } - var absval = this._value.absoluteValue(); + const absval = this._value.abs(); if (this._is_native) { if (absval.greaterThan(MAX_XRP_VALUE)) { throw new Error('Exceeding max value of ' + MAX_XRP_VALUE.toString()); @@ -380,15 +348,15 @@ Amount.prototype.clone = function(negate) { }; Amount.prototype._copy = function(value) { - var copy = this.clone(); + const copy = this.clone(); copy._set_value(value); return copy; }; Amount.prototype.compareTo = function(to) { - var toAmount = Amount.from_json(to); + const toAmount = Amount.from_json(to); if (!this.is_comparable(toAmount)) { - return new Amount(NaN); + return new Amount(); } return this._value.comparedTo(toAmount._value); }; @@ -396,7 +364,7 @@ Amount.prototype.compareTo = function(to) { // Make d a copy of this. Returns d. // Modification of objects internally refered to is not allowed. Amount.prototype.copyTo = function(d, negate) { - d._value = negate ? this._value.negated() : this._value; + d._value = negate ? this._value.negate() : this._value; d._is_native = this._is_native; d._currency = this._currency; d._issuer = this._issuer; @@ -482,16 +450,16 @@ Amount.prototype.negate = function() { * $ */ -Amount.prototype.parse_human = function(j, opts) { - opts = opts || {}; +Amount.prototype.parse_human = function(j, options) { + const opts = options || {}; - var hex_RE = /^[a-fA-F0-9]{40}$/; - var currency_RE = /^([a-zA-Z]{3}|[0-9]{3})$/; + const hex_RE = /^[a-fA-F0-9]{40}$/; + const currency_RE = /^([a-zA-Z]{3}|[0-9]{3})$/; - var value; - var currency; + let value; + let currency; - var words = j.split(' ').filter(function(word) { + const words = j.split(' ').filter(function(word) { return word !== ''; }); @@ -507,7 +475,7 @@ Amount.prototype.parse_human = function(j, opts) { value = words[0].slice(0, -3); currency = words[0].slice(-3); if (!(isNumber(value) && currency.match(currency_RE))) { - return new Amount(NaN); + return new Amount(); } } } else if (words.length === 2) { @@ -521,21 +489,25 @@ Amount.prototype.parse_human = function(j, opts) { value = words[0]; currency = words[1]; } else { - return new Amount(NaN); + return new Amount(); } } else { - return new Amount(NaN); + return new Amount(); } currency = currency.toUpperCase(); this.set_currency(currency); this._is_native = (currency === 'XRP'); - this._set_value(new BigNumber(value)); + const newValue = + (this._is_native ? new XRPValue(value) : + new IOUValue(value)); + this._set_value(newValue); // Apply interest/demurrage if (opts.reference_date && this._currency.has_interest()) { - var interest = this._currency.get_interest_at(opts.reference_date); - this._set_value(this._value.dividedBy(interest.toString())); + const interest = this._currency.get_interest_at(opts.reference_date); + this._set_value( + this._value.divide(new IOUValue(interest.toString()))); } return this; @@ -582,16 +554,15 @@ Amount.prototype.parse_issuer = function(issuer) { */ Amount.prototype.parse_quality = function(quality, counterCurrency, counterIssuer, opts) { - opts = opts || {}; + const options = opts || {}; - var baseCurrency = Currency.from_json(opts.base_currency); + const baseCurrency = Currency.from_json(options.base_currency); - var mantissa_hex = quality.substring(quality.length - 14); - var offset_hex = quality.substring(quality.length - 16, quality.length - 14); - var mantissa = new BigNumber(mantissa_hex, 16); - var offset = parseInt(offset_hex, 16) - 100; - - var value = new BigNumber(mantissa.toString() + 'e' + offset.toString()); + const mantissa_hex = quality.substring(quality.length - 14); + const offset_hex = quality.substring( + quality.length - 16, quality.length - 14); + const mantissa = new IOUValue(mantissa_hex, null, 16); + const offset = parseInt(offset_hex, 16) - 100; this._currency = Currency.from_json(counterCurrency); this._issuer = UInt160.from_json(counterIssuer); @@ -614,10 +585,11 @@ function(quality, counterCurrency, counterIssuer, opts) { quality as stored : 5 USD / 3000000 drops inverted : 3000000 drops / 5 USD */ - var adjusted = opts.inverse ? inverse(value) : value; - var nativeAdjusted = adjusted; + const valueStr = mantissa.toString() + 'e' + offset.toString(); + let nativeAdjusted = new IOUValue(valueStr); + nativeAdjusted = options.inverse ? nativeAdjusted.invert() : nativeAdjusted; - if (!opts.xrp_as_drops) { + if (!options.xrp_as_drops) { // `In a currency exchange, the exchange rate is quoted as the units of the // counter currency in terms of a single unit of a base currency`. A // quality is how much taker must `pay` to get ONE `gets` unit thus: @@ -626,22 +598,26 @@ function(quality, counterCurrency, counterIssuer, opts) { if (this._is_native) { // pay:$price drops get:1 X // pay:($price / 1,000,000) XRP get:1 X - nativeAdjusted = adjusted.div(Amount.bi_xns_unit); + nativeAdjusted = nativeAdjusted.divide(bi_xns_unit); } else if (baseCurrency.is_valid() && baseCurrency.is_native()) { // pay:$price X get:1 drop // pay:($price * 1,000,000) X get:1 XRP - nativeAdjusted = adjusted.times(Amount.bi_xns_unit); + nativeAdjusted = nativeAdjusted.multiply(bi_xns_unit); } } - - this._set_value(nativeAdjusted); - - if (opts.reference_date && baseCurrency.is_valid() - && baseCurrency.has_interest()) { - var interest = baseCurrency.get_interest_at(opts.reference_date); - this._set_value(this._value.dividedBy(interest.toString())); + if (this._is_native) { + this._set_value( + new XRPValue(nativeAdjusted.round(6, Value.getBNRoundDown()).toString())); + } else { + this._set_value(nativeAdjusted); } + if (options.reference_date && baseCurrency.is_valid() + && baseCurrency.has_interest()) { + const interest = baseCurrency.get_interest_at(options.reference_date); + this._set_value( + this._value.divide(new IOUValue(interest.toString()))); + } return this; }; @@ -649,7 +625,7 @@ Amount.prototype.parse_number = function(n) { this._is_native = false; this._currency = Currency.from_json(1); this._issuer = UInt160.from_json(1); - this._set_value(new BigNumber(n)); + this._set_value(new IOUValue(n)); return this; }; @@ -659,7 +635,7 @@ Amount.prototype.parse_json = function(j) { case 'string': // .../.../... notation is not a wire format. But allowed for easier // testing. - var m = j.match(/^([^/]+)\/([^/]+)(?:\/(.+))?$/); + const m = j.match(/^([^/]+)\/([^/]+)(?:\/(.+))?$/); if (m) { this._currency = Currency.from_json(m[2]); @@ -700,7 +676,7 @@ Amount.prototype.parse_json = function(j) { break; default: - this._set_value(new BigNumber(NaN)); + this._set_value(new IOUValue(NaN)); } return this; @@ -715,11 +691,11 @@ Amount.prototype.parse_native = function(j) { if (j.indexOf('.') >= 0) { throw new Error('Native amounts must be specified in integer drops'); } - var value = new BigNumber(j); + const value = new XRPValue(j); this._is_native = true; - this._set_value(value.dividedBy(Amount.bi_xns_unit)); + this._set_value(value.divide(bi_xns_unit)); } else { - this._set_value(new BigNumber(NaN)); + this._set_value(new IOUValue(NaN)); } return this; @@ -729,7 +705,8 @@ Amount.prototype.parse_native = function(j) { // Requires _currency to be set! Amount.prototype.parse_value = function(j) { this._is_native = false; - this._set_value(new BigNumber(j), BigNumber.ROUND_DOWN); + const newValue = new IOUValue(j, Value.getBNRoundDown()); + this._set_value(newValue); return this; }; @@ -760,26 +737,27 @@ Amount.prototype.to_text = function() { } if (this._is_native) { - return this._value.times(Amount.bi_xns_unit).toString(); + return this._value.multiply(bi_xns_unit).toString(); } // not native - var offset = this._value.e - 15; - var sign = this._value.isNegative() ? '-' : ''; - var mantissa = utils.getMantissaDecimalString(this._value.absoluteValue()); + const offset = this._value.getExponent() - 15; + const sign = this._value.isNegative() ? '-' : ''; + const mantissa = utils.getMantissa16FromString( + this._value.abs().toString()); if (offset !== 0 && (offset < -25 || offset > -4)) { // Use e notation. // XXX Clamp output. return sign + mantissa.toString() + 'e' + offset.toString(); } - var val = '000000000000000000000000000' + const val = '000000000000000000000000000' + mantissa.toString() + '00000000000000000000000'; - var pre = val.substring(0, offset + 43); - var post = val.substring(offset + 43); - var s_pre = pre.match(/[1-9].*$/); // Everything but leading zeros. - var s_post = post.match(/[1-9]0*$/); // Last non-zero plus trailing zeros. + const pre = val.substring(0, offset + 43); + const post = val.substring(offset + 43); + const s_pre = pre.match(/[1-9].*$/); // Everything but leading zeros. + const s_post = post.match(/[1-9]0*$/); // Last non-zero plus trailing zeros. return sign + (s_pre ? s_pre[0] : '0') + (s_post ? '.' + post.substring(0, 1 + post.length - s_post[0].length) : ''); @@ -801,36 +779,37 @@ Amount.prototype.applyInterest = function(referenceDate) { if (!this._currency.has_interest()) { return this; } - var interest = this._currency.get_interest_at(referenceDate); - return this._copy(this._value.times(interest.toString())); + const interest = this._currency.get_interest_at(referenceDate); + return this._copy( + this._value.multiply(new IOUValue(interest.toString()))); }; /** * Format only value in a human-readable format. * * @example - * var pretty = amount.to_human({precision: 2}); + * let pretty = amount.to_human({precision: 2}); * - * @param {Object} opts Options for formatter. - * @param {Number} opts.precision Max. number of digits after decimal point. - * @param {Number} opts.min_precision Min. number of digits after dec. point. - * @param {Boolean} opts.skip_empty_fraction Don't show fraction if it is zero, - * even if min_precision is set. - * @param {Number} opts.max_sig_digits Maximum number of significant digits. + * @param {Object} options Options for formatter. + * @param {Number} options.precision Max. number of digits after decimal point. + * @param {Number} options.min_precision Min. number of digits after dec. point. + * @param {Boolean} options.skip_empty_fraction Don't show fraction if it + * is zero, even if min_precision is set. + * @param {Number} options.max_sig_digits Maximum number of significant digits. * Will cut fractional part, but never integer part. - * @param {Boolean|String} opts.group_sep Whether to show a separator every n + * @param {Boolean|String} options.group_sep Whether to show a separator every n * digits, if a string, that value will be used as the separator. Default: ',' - * @param {Number} opts.group_width How many numbers will be grouped together, - * default: 3. - * @param {Boolean|String} opts.signed Whether negative numbers will have a + * @param {Number} options.group_width How many numbers will be grouped + * together, default: 3. + * @param {Boolean|String} options.signed Whether negative numbers will have a * prefix. If String, that string will be used as the prefix. Default: '-' - * @param {Date|Number} opts.reference_date Date based on which + * @param {Date|Number} options.reference_date Date based on which * demurrage/interest should be applied. Can be given as JavaScript Date or int * for Ripple epoch. * @return {String} amount string */ -Amount.prototype.to_human = function(opts) { - opts = opts || {}; +Amount.prototype.to_human = function(options) { + const opts = options || {}; if (!this.is_valid()) { return 'NaN'; @@ -838,18 +817,18 @@ Amount.prototype.to_human = function(opts) { /* eslint-disable consistent-this */ // Apply demurrage/interest - var ref = this; + let ref = this; /* eslint-enable consistent-this */ if (opts.reference_date) { ref = this.applyInterest(opts.reference_date); } - var isNegative = ref._value.isNegative(); - var valueString = ref._value.abs().toFixed(); - var parts = valueString.split('.'); - var int_part = parts[0]; - var fraction_part = parts.length === 2 ? parts[1] : ''; + const isNegative = ref._value.isNegative(); + const valueString = ref._value.abs().toFixed(); + const parts = valueString.split('.'); + let int_part = parts[0]; + let fraction_part = parts.length === 2 ? parts[1] : ''; int_part = int_part.replace(/^0*/, ''); fraction_part = fraction_part.replace(/0*$/, ''); @@ -857,9 +836,9 @@ Amount.prototype.to_human = function(opts) { if (fraction_part.length || !opts.skip_empty_fraction) { // Enforce the maximum number of decimal digits (precision) if (typeof opts.precision === 'number') { - var precision = Math.max(0, opts.precision); + let precision = Math.max(0, opts.precision); precision = Math.min(precision, fraction_part.length); - var rounded = Number('0.' + fraction_part).toFixed(precision); + const rounded = Number('0.' + fraction_part).toFixed(precision); if (rounded < 1) { fraction_part = rounded.substring(2); @@ -877,18 +856,18 @@ Amount.prototype.to_human = function(opts) { if (typeof opts.max_sig_digits === 'number') { // First, we count the significant digits we have. // A zero in the integer part does not count. - var int_is_zero = Number(int_part) === 0; - var digits = int_is_zero ? 0 : int_part.length; + const int_is_zero = Number(int_part) === 0; + let digits = int_is_zero ? 0 : int_part.length; // Don't count leading zeros in the fractional part if the integer part is // zero. - var sig_frac = int_is_zero + const sig_frac = int_is_zero ? fraction_part.replace(/^0*/, '') : fraction_part; digits += sig_frac.length; // Now we calculate where we are compared to the maximum - var rounding = digits - opts.max_sig_digits; + let rounding = digits - opts.max_sig_digits; // If we're under the maximum we want to cut no (=0) digits rounding = Math.max(rounding, 0); @@ -913,12 +892,12 @@ Amount.prototype.to_human = function(opts) { } if (opts.group_sep !== false) { - var sep = (typeof opts.group_sep === 'string') ? opts.group_sep : ','; - var groups = utils.chunkString(int_part, opts.group_width || 3, true); + const sep = (typeof opts.group_sep === 'string') ? opts.group_sep : ','; + const groups = utils.chunkString(int_part, opts.group_width || 3, true); int_part = groups.join(sep); } - var formatted = ''; + let formatted = ''; if (isNegative && opts.signed !== false) { formatted += '-'; } @@ -929,12 +908,12 @@ Amount.prototype.to_human = function(opts) { return formatted; }; -Amount.prototype.to_human_full = function(opts) { - opts = opts || {}; - var value = this.to_human(opts); - var currency = this._currency.to_human(); - var issuer = this._issuer.to_json(opts); - var base = value + '/' + currency; +Amount.prototype.to_human_full = function(options) { + const opts = options || {}; + const value = this.to_human(opts); + const currency = this._currency.to_human(); + const issuer = this._issuer.to_json(opts); + const base = value + '/' + currency; return this.is_native() ? base : (base + '/' + issuer); }; @@ -943,7 +922,7 @@ Amount.prototype.to_json = function() { return this.to_text(); } - var amount_json = { + const amount_json = { value: this.to_text(), currency: this._currency.has_interest() ? this._currency.to_hex() : this._currency.to_json() @@ -981,8 +960,8 @@ Amount.prototype.not_equals_why = function(d, ignore_issuer) { return 'Native mismatch.'; } - var type = this._is_native ? 'XRP' : 'Non-XRP'; - if (!this._value.isZero() && this._value.negated().equals(d._value)) { + const type = this._is_native ? 'XRP' : 'Non-XRP'; + if (!this._value.isZero() && this._value.negate().equals(d._value)) { return type + ' sign differs.'; } if (!this._value.equals(d._value)) { diff --git a/src/core/iouvalue.js b/src/core/iouvalue.js new file mode 100644 index 00000000..babffecd --- /dev/null +++ b/src/core/iouvalue.js @@ -0,0 +1,56 @@ +/* @flow */ + +'use strict'; + +const Value = require('./value').Value; +const XRPValue = require('./xrpvalue').XRPValue; +const GlobalBigNumber = require('bignumber.js'); +const BigNumber = GlobalBigNumber.another({ + ROUNDING_MODE: GlobalBigNumber.ROUND_HALF_UP, + DECIMAL_PLACES: 40 +}); +const rippleUnits = new BigNumber(1e6); + +class IOUValue extends Value { + + constructor(value: string | BigNumber, roundingMode: ?number = null, + base: ?number = null) { + + super(new BigNumber(value, base).toDigits(16, roundingMode)); + } + + multiply(multiplicand: Value) { + if (multiplicand instanceof XRPValue) { + return super.multiply( + new IOUValue( + multiplicand._value.times(rippleUnits))); + } + return super.multiply(multiplicand); + } + + divide(divisor: Value) { + if (divisor instanceof XRPValue) { + return super.divide( + new IOUValue(divisor._value.times(rippleUnits))); + } + return super.divide(divisor); + } + + negate() { + return new IOUValue(this._value.neg()); + } + + _canonicalize(value) { + if (value.isNaN()) { + throw new Error('Invalid result'); + } + return new IOUValue(value.toPrecision(16)); + } + + equals(comparator) { + return (comparator instanceof IOUValue) + && this._value.equals(comparator._value); + } +} + +exports.IOUValue = IOUValue; diff --git a/src/core/orderbook.js b/src/core/orderbook.js index 400318e8..989f25bd 100644 --- a/src/core/orderbook.js +++ b/src/core/orderbook.js @@ -22,6 +22,7 @@ const Currency = require('./currency').Currency; const AutobridgeCalculator = require('./autobridgecalculator'); const OrderBookUtils = require('./orderbookutils'); const log = require('./log').internal.sub('orderbook'); +const IOUValue = require('./iouvalue').IOUValue; function assertValidNumber(number, message) { assert(!_.isNull(number) && !isNaN(number), message); @@ -448,11 +449,9 @@ OrderBook.prototype.applyTransferRate = function(balance) { assert(!isNaN(balance), 'Balance is invalid'); assertValidNumber(this._issuerTransferRate, 'Transfer rate is invalid'); - const adjustedBalance = OrderBookUtils.normalizeAmount(balance) - .divide(this._issuerTransferRate) - .multiply(Amount.from_json(OrderBook.DEFAULT_TRANSFER_RATE)) - .to_json() - .value; + const adjustedBalance = (new IOUValue(balance)) + .divide(new IOUValue(this._issuerTransferRate)) + .multiply(new IOUValue(OrderBook.DEFAULT_TRANSFER_RATE)).toString(); return adjustedBalance; }; diff --git a/src/core/orderbookutils.js b/src/core/orderbookutils.js index 8ab54fae..34705dcc 100644 --- a/src/core/orderbookutils.js +++ b/src/core/orderbookutils.js @@ -1,18 +1,50 @@ 'use strict'; -var _ = require('lodash'); -var assert = require('assert'); -var SerializedObject = require('./serializedobject').SerializedObject; -var Types = require('./serializedtypes'); -var Amount = require('./amount').Amount; - -var IOU_SUFFIX = '/000/rrrrrrrrrrrrrrrrrrrrrhoLvTp'; -var OrderBookUtils = {}; +const _ = require('lodash'); +const assert = require('assert'); +const SerializedObject = require('./serializedobject').SerializedObject; +const Types = require('./serializedtypes'); +const Amount = require('./amount').Amount; +const OrderBookUtils = {}; function assertValidNumber(number, message) { assert(!_.isNull(number) && !isNaN(number), message); } +/** +* Creates a new Amount from a JSON amount object using +* passed parameters for value, currency and counterparty +* +* @param amount of value, currency, counterparty +* @return JSON amount object +*/ + +function createAmount(value, currency, counterparty) { + const newJSON = + {'value': value, 'currency': currency, 'issuer': counterparty}; + return Amount.from_json(newJSON); +} + +/** +* Gets currency for getOfferTaker(Gets/Pays)Funded +* @param offer +* @return currency +*/ + +function getCurrencyFromOffer(offer) { + return offer.TakerPays.currency || offer.TakerGets.currency; +} + +/** +* Gets issuer for getOfferTaker(Gets/Pays)Funded +* @param offer +* @return issuer +*/ + +function getIssuerFromOffer(offer) { + return offer.TakerPays.issuer || offer.TakerGets.issuer; +} + /** * Casts and returns offer's taker gets funded amount as a default IOU amount * @@ -23,7 +55,10 @@ function assertValidNumber(number, message) { OrderBookUtils.getOfferTakerGetsFunded = function(offer) { assertValidNumber(offer.taker_gets_funded, 'Taker gets funded is invalid'); - return Amount.from_json(offer.taker_gets_funded + IOU_SUFFIX); + const currency = getCurrencyFromOffer(offer); + const issuer = getIssuerFromOffer(offer); + + return createAmount(offer.taker_gets_funded, currency, issuer); }; /** @@ -36,7 +71,10 @@ OrderBookUtils.getOfferTakerGetsFunded = function(offer) { OrderBookUtils.getOfferTakerPaysFunded = function(offer) { assertValidNumber(offer.taker_pays_funded, 'Taker gets funded is invalid'); - return Amount.from_json(offer.taker_pays_funded + IOU_SUFFIX); + const currency = getCurrencyFromOffer(offer); + const issuer = getIssuerFromOffer(offer); + + return createAmount(offer.taker_pays_funded, currency, issuer); }; /** @@ -50,7 +88,10 @@ OrderBookUtils.getOfferTakerPaysFunded = function(offer) { OrderBookUtils.getOfferTakerGets = function(offer) { assert(typeof offer, 'object', 'Offer is invalid'); - return Amount.from_json(offer.TakerGets + IOU_SUFFIX); + const currency = offer.TakerPays.currency; + const issuer = offer.TakerPays.issuer; + + return createAmount(offer.TakerGets, currency, issuer); }; /** @@ -61,7 +102,7 @@ OrderBookUtils.getOfferTakerGets = function(offer) { */ OrderBookUtils.getOfferQuality = function(offer, currencyGets) { - var amount; + let amount; if (currencyGets.has_interest()) { // XXX Should use Amount#from_quality @@ -71,7 +112,11 @@ OrderBookUtils.getOfferQuality = function(offer, currencyGets) { reference_date: new Date() }); } else { - amount = Amount.from_json(offer.quality + IOU_SUFFIX); + + const currency = getCurrencyFromOffer(offer); + const issuer = getIssuerFromOffer(offer); + + amount = createAmount(offer.quality, currency, issuer); } return amount; @@ -89,8 +134,8 @@ OrderBookUtils.getOfferQuality = function(offer, currencyGets) { OrderBookUtils.convertOfferQualityToHex = function(quality) { assert(quality instanceof Amount, 'Quality is not an amount'); - var so = new SerializedObject(); - Types.Quality.serialize(so, quality.to_text() + IOU_SUFFIX); + const so = new SerializedObject(); + Types.Quality.serialize(so, quality.to_text()); return so.to_hex(); }; @@ -100,7 +145,8 @@ OrderBookUtils.convertOfferQualityToHex = function(quality) { */ OrderBookUtils.normalizeAmount = function(value) { - return Amount.from_json(value + IOU_SUFFIX); + + return Amount.from_number(value); }; module.exports = OrderBookUtils; diff --git a/src/core/serializedtypes.js b/src/core/serializedtypes.js index 640d90dc..43edbdc7 100644 --- a/src/core/serializedtypes.js +++ b/src/core/serializedtypes.js @@ -8,22 +8,22 @@ * SerializedObject.parse() or SerializedObject.serialize(). */ -var assert = require('assert'); -var extend = require('extend'); -var GlobalBigNumber = require('bignumber.js'); -var Amount = require('./amount').Amount; -var Currency = require('./currency').Currency; -var binformat = require('./binformat'); -var utils = require('./utils'); -var sjcl = utils.sjcl; -var SJCL_BN = sjcl.bn; +const assert = require('assert'); +const extend = require('extend'); +const GlobalBigNumber = require('bignumber.js'); +const Amount = require('./amount').Amount; +const Currency = require('./currency').Currency; +const binformat = require('./binformat'); +const utils = require('./utils'); +const sjcl = utils.sjcl; +const SJCL_BN = sjcl.bn; -var UInt128 = require('./uint128').UInt128; -var UInt160 = require('./uint160').UInt160; -var UInt256 = require('./uint256').UInt256; -var Base = require('./base').Base; +const UInt128 = require('./uint128').UInt128; +const UInt160 = require('./uint160').UInt160; +const UInt256 = require('./uint256').UInt256; +const Base = require('./base').Base; -var BigNumber = GlobalBigNumber.another({ +const BigNumber = GlobalBigNumber.another({ ROUNDING_MODE: GlobalBigNumber.ROUND_HALF_UP, DECIMAL_PLACES: 40 }); @@ -45,7 +45,7 @@ function isHexInt64String(val) { } function serializeBits(so, bits, noLength) { - var byteData = sjcl.codec.bytes.fromBits(bits); + const byteData = sjcl.codec.bytes.fromBits(bits); if (!noLength) { SerializedType.serialize_varint(so, byteData.length); } @@ -68,18 +68,18 @@ function convertByteArrayToHex(byte_array) { } function convertHexToString(hexString) { - var bits = sjcl.codec.hex.toBits(hexString); + const bits = sjcl.codec.hex.toBits(hexString); return sjcl.codec.utf8String.fromBits(bits); } function sort_fields(keys) { function sort_field_compare(a, b) { - var a_field_coordinates = binformat.fieldsInverseMap[a]; - var a_type_bits = a_field_coordinates[0]; - var a_field_bits = a_field_coordinates[1]; - var b_field_coordinates = binformat.fieldsInverseMap[b]; - var b_type_bits = b_field_coordinates[0]; - var b_field_bits = b_field_coordinates[1]; + const a_field_coordinates = binformat.fieldsInverseMap[a]; + const a_type_bits = a_field_coordinates[0]; + const a_field_bits = a_field_coordinates[1]; + const b_field_coordinates = binformat.fieldsInverseMap[b]; + const b_type_bits = b_field_coordinates[0]; + const b_field_bits = b_field_coordinates[1]; // Sort by type id first, then by field id return a_type_bits !== b_type_bits @@ -91,26 +91,28 @@ function sort_fields(keys) { } SerializedType.serialize_varint = function(so, val) { - if (val < 0) { + let value = val; + if (value < 0) { throw new Error('Variable integers are unsigned.'); } - if (val <= 192) { - so.append([val]); - } else if (val <= 12480) { - val -= 193; - so.append([193 + (val >>> 8), val & 0xff]); - } else if (val <= 918744) { - val -= 12481; - so.append([241 + (val >>> 16), val >>> 8 & 0xff, val & 0xff]); + if (value <= 192) { + so.append([value]); + } else if (value <= 12480) { + value -= 193; + so.append([193 + (value >>> 8), value & 0xff]); + } else if (value <= 918744) { + value -= 12481; + so.append([241 + (value >>> 16), value >>> 8 & 0xff, value & 0xff]); } else { throw new Error('Variable integer overflow.'); } }; SerializedType.prototype.parse_varint = function(so) { - var b1 = so.read(1)[0], b2, b3; - var result; + const b1 = so.read(1)[0]; + let b2, b3; + let result; if (b1 > 254) { throw new Error('Invalid varint length indicator'); @@ -152,9 +154,9 @@ function convertIntegerToByteArray(val, bytes) { throw new Error('Value out of bounds '); } - var newBytes = [ ]; + const newBytes = [ ]; - for (var i = 0; i < bytes; i++) { + for (let i = 0; i < bytes; i++) { newBytes.unshift(val >>> (i * 8) & 0xff); } @@ -164,14 +166,14 @@ function convertIntegerToByteArray(val, bytes) { // Convert a certain number of bytes from the serialized object ('so') into an // integer. function readAndSum(so, bytes) { - var sum = 0; + let sum = 0; if (bytes > 4) { throw new Error('This function only supports up to four bytes.'); } - for (var i = 0; i < bytes; i++) { - var byte = so.read(1)[0]; + for (let i = 0; i < bytes; i++) { + const byte = so.read(1)[0]; sum += (byte << (8 * (bytes - i - 1))); } @@ -179,7 +181,7 @@ function readAndSum(so, bytes) { return sum >>> 0; } -var STInt8 = exports.Int8 = new SerializedType({ +const STInt8 = exports.Int8 = new SerializedType({ serialize: function(so, val) { so.append(convertIntegerToByteArray(val, 1)); }, @@ -194,21 +196,22 @@ function serialize(so, field_name, value) { // so: a byte-stream to serialize into. // field_name: a string for the field name ('LedgerEntryType' etc.) // value: the value of that field. - var field_coordinates = binformat.fieldsInverseMap[field_name]; - var type_bits = field_coordinates[0]; - var field_bits = field_coordinates[1]; - var tag_byte = (type_bits < 16 + const field_coordinates = binformat.fieldsInverseMap[field_name]; + const type_bits = field_coordinates[0]; + const field_bits = field_coordinates[1]; + const tag_byte = (type_bits < 16 ? type_bits << 4 : 0) | (field_bits < 16 ? field_bits : 0); + let val = value; - if (field_name === 'LedgerEntryType' && typeof value === 'string') { - value = binformat.ledger[value][0]; + if (field_name === 'LedgerEntryType' && typeof val === 'string') { + val = binformat.ledger[val][0]; } - if (field_name === 'TransactionResult' && typeof value === 'string') { - value = binformat.ter[value]; + if (field_name === 'TransactionResult' && typeof val === 'string') { + val = binformat.ter[val]; } STInt8.serialize(so, tag_byte); @@ -222,9 +225,9 @@ function serialize(so, field_name, value) { } // Get the serializer class (ST...) - var serialized_object_type; + let serialized_object_type; - if (field_name === 'Memo' && typeof value === 'object') { + if (field_name === 'Memo' && typeof val === 'object') { // for Memo we override the default behavior with our STMemo serializer serialized_object_type = exports.STMemo; } else { @@ -233,7 +236,7 @@ function serialize(so, field_name, value) { } try { - serialized_object_type.serialize(so, value); + serialized_object_type.serialize(so, val); } catch (e) { e.message += ' (' + field_name + ')'; throw e; @@ -246,15 +249,15 @@ exports.serialize = exports.serialize_whatever = serialize; // parsing of that. function parse(so) { - var tag_byte = so.read(1)[0]; - var type_bits = tag_byte >> 4; + const tag_byte = so.read(1)[0]; + let type_bits = tag_byte >> 4; if (type_bits === 0) { type_bits = so.read(1)[0]; } - var field_bits = tag_byte & 0x0f; - var field_name = (field_bits === 0) + const field_bits = tag_byte & 0x0f; + let field_name = (field_bits === 0) ? field_name = binformat.fields[type_bits][so.read(1)[0]] : field_name = binformat.fields[type_bits][field_bits]; @@ -262,7 +265,7 @@ function parse(so) { + tag_byte.toString(16)); // Get the parser class (ST...) for a field based on the type bits. - var type = (field_name === 'Memo') + const type = (field_name === 'Memo') ? exports.STMemo : exports[binformat.types[type_bits]]; @@ -273,7 +276,7 @@ function parse(so) { exports.parse = exports.parse_whatever = parse; -var STInt16 = exports.Int16 = new SerializedType({ +const STInt16 = exports.Int16 = new SerializedType({ serialize: function(so, val) { so.append(convertIntegerToByteArray(val, 2)); }, @@ -284,7 +287,7 @@ var STInt16 = exports.Int16 = new SerializedType({ STInt16.id = 1; -var STInt32 = exports.Int32 = new SerializedType({ +const STInt32 = exports.Int32 = new SerializedType({ serialize: function(so, val) { so.append(convertIntegerToByteArray(val, 4)); }, @@ -295,42 +298,43 @@ var STInt32 = exports.Int32 = new SerializedType({ STInt32.id = 2; -var STInt64 = exports.Int64 = new SerializedType({ +const STInt64 = exports.Int64 = new SerializedType({ serialize: function(so, val) { - var bigNumObject; + let bigNumObject; + let value = val; - if (isNumber(val)) { - val = Math.floor(val); - if (val < 0) { + if (isNumber(value)) { + value = Math.floor(value); + if (value < 0) { throw new Error('Negative value for unsigned Int64 is invalid.'); } - bigNumObject = new SJCL_BN(val, 10); - } else if (isString(val)) { - if (!isHexInt64String(val)) { + bigNumObject = new SJCL_BN(value, 10); + } else if (isString(value)) { + if (!isHexInt64String(value)) { throw new Error('Not a valid hex Int64.'); } - bigNumObject = new SJCL_BN(val, 16); - } else if (val instanceof SJCL_BN) { - if (!val.greaterEquals(0)) { + bigNumObject = new SJCL_BN(value, 16); + } else if (value instanceof SJCL_BN) { + if (!value.greaterEquals(0)) { throw new Error('Negative value for unsigned Int64 is invalid.'); } - bigNumObject = val; + bigNumObject = value; } else { throw new Error('Invalid type for Int64'); } serializeBits(so, bigNumObject.toBits(64), true); // noLength = true }, parse: function(so) { - var bytes = so.read(8); + const bytes = so.read(8); return SJCL_BN.fromBits(sjcl.codec.bytes.toBits(bytes)); } }); STInt64.id = 3; -var STHash128 = exports.Hash128 = new SerializedType({ +const STHash128 = exports.Hash128 = new SerializedType({ serialize: function(so, val) { - var hash = UInt128.from_json(val); + const hash = UInt128.from_json(val); if (!hash.is_valid()) { throw new Error('Invalid Hash128'); } @@ -343,9 +347,9 @@ var STHash128 = exports.Hash128 = new SerializedType({ STHash128.id = 4; -var STHash256 = exports.Hash256 = new SerializedType({ +const STHash256 = exports.Hash256 = new SerializedType({ serialize: function(so, val) { - var hash = UInt256.from_json(val); + const hash = UInt256.from_json(val); if (!hash.is_valid()) { throw new Error('Invalid Hash256'); } @@ -358,9 +362,9 @@ var STHash256 = exports.Hash256 = new SerializedType({ STHash256.id = 5; -var STHash160 = exports.Hash160 = new SerializedType({ +const STHash160 = exports.Hash160 = new SerializedType({ serialize: function(so, val) { - var hash = UInt160.from_json(val); + const hash = UInt160.from_json(val); if (!hash.is_valid()) { throw new Error('Invalid Hash160'); } @@ -374,9 +378,9 @@ var STHash160 = exports.Hash160 = new SerializedType({ STHash160.id = 17; // Internal -var STCurrency = new SerializedType({ +const STCurrency = new SerializedType({ serialize: function(so, val) { - var currencyData = val.to_bytes(); + const currencyData = val.to_bytes(); if (!currencyData) { throw new Error( @@ -386,8 +390,8 @@ var STCurrency = new SerializedType({ so.append(currencyData); }, parse: function(so) { - var bytes = so.read(20); - var currency = Currency.from_bytes(bytes); + const bytes = so.read(20); + const currency = Currency.from_bytes(bytes); // XXX Disabled check. Theoretically, the Currency class should support any // UInt160 value and consider it valid. But it doesn't, so for the // deserialization to be usable, we need to allow invalid results for @@ -408,23 +412,29 @@ var STCurrency = new SerializedType({ */ exports.Quality = new SerializedType({ serialize: function(so, val) { - var amount = Amount.from_json(val); + let value; + // if in format: amount/currency/issuer + if (val.includes('/')) { + const amount = Amount.from_json(val); - if (!amount.is_valid()) { - throw new Error('Not a valid Amount object.'); + if (!amount.is_valid()) { + throw new Error('Not a valid Amount object.'); + } + value = new BigNumber(amount.to_text()); + } else { + value = new BigNumber(val); } - var hi = 0, lo = 0; - var value = new BigNumber(amount.to_text()); - var offset = value.e - 15; + let hi = 0, lo = 0; - if (!amount.is_zero()) { + const offset = value.e - 15; + if (val !== 0) { // First eight bits: offset/exponent hi |= ((100 + offset) & 0xff) << 24; // Remaining 56 bits: mantissa - var mantissaDecimal = utils.getMantissaDecimalString(value.abs()); - var mantissaHex = (new BigNumber(mantissaDecimal)).toString(16); + const mantissaDecimal = utils.getMantissaDecimalString(value.abs()); + const mantissaHex = (new BigNumber(mantissaDecimal)).toString(16); assert(mantissaHex.length <= 16, 'Mantissa hex representation ' + mantissaHex + ' exceeds the maximum length of 16'); @@ -432,7 +442,7 @@ exports.Quality = new SerializedType({ lo = parseInt(mantissaHex.slice(-8), 16); } - var valueBytes = sjcl.codec.bytes.fromBits([hi, lo]); + const valueBytes = sjcl.codec.bytes.fromBits([hi, lo]); so.append(valueBytes); } @@ -442,22 +452,22 @@ exports.Quality = new SerializedType({ * Amount is encoded into 64 bits: * (1 bit non-native) (1 bit non-negative) (8 bits offset) (54 bits mantissa) */ -var STAmount = exports.Amount = new SerializedType({ +const STAmount = exports.Amount = new SerializedType({ serialize: function(so, val) { - var amount = Amount.from_json(val); + const amount = Amount.from_json(val); if (!amount.is_valid()) { throw new Error('Not a valid Amount object.'); } - var value = new BigNumber(amount.to_text()); - var offset = value.e - 15; + const value = new BigNumber(amount.to_text()); + const offset = value.e - 15; // Amount (64-bit integer) - var valueBytes = utils.arraySet(8, 0); + let valueBytes = utils.arraySet(8, 0); if (amount.is_native()) { - var valueHex = value.abs().toString(16); + let valueHex = value.abs().toString(16); if (Amount.strict_mode && value.abs().greaterThan(Amount.bi_xns_max)) { throw new Error('Value out of bounds'); @@ -482,7 +492,7 @@ var STAmount = exports.Amount = new SerializedType({ valueBytes[0] |= 0x40; } } else { - var hi = 0, lo = 0; + let hi = 0, lo = 0; // First bit: non-native hi |= 1 << 31; @@ -497,8 +507,8 @@ var STAmount = exports.Amount = new SerializedType({ hi |= ((97 + offset) & 0xff) << 22; // Remaining 54 bits: mantissa - var mantissaDecimal = utils.getMantissaDecimalString(value.abs()); - var mantissaHex = (new BigNumber(mantissaDecimal)).toString(16); + const mantissaDecimal = utils.getMantissaDecimalString(value.abs()); + const mantissaHex = (new BigNumber(mantissaDecimal)).toString(16); assert(mantissaHex.length <= 16, 'Mantissa hex representation ' + mantissaHex + ' exceeds the maximum length of 16'); @@ -513,7 +523,7 @@ var STAmount = exports.Amount = new SerializedType({ if (!amount.is_native()) { // Currency (160-bit hash) - var currency = amount.currency(); + const currency = amount.currency(); STCurrency.serialize(so, currency, true); // Issuer (160-bit hash) @@ -521,27 +531,28 @@ var STAmount = exports.Amount = new SerializedType({ } }, parse: function(so) { - var value_bytes = so.read(8); - var is_zero = !(value_bytes[0] & 0x7f); + const value_bytes = so.read(8); + let is_zero = !(value_bytes[0] & 0x7f); - for (var i = 1; i < 8; i++) { + for (let i = 1; i < 8; i++) { is_zero = is_zero && !value_bytes[i]; } - var is_negative = !is_zero && !(value_bytes[0] & 0x40); + const is_negative = !is_zero && !(value_bytes[0] & 0x40); if (value_bytes[0] & 0x80) { // non-native - var currency = STCurrency.parse(so); - var issuer_bytes = so.read(20); - var issuer = UInt160.from_bytes(issuer_bytes); + const currency = STCurrency.parse(so); + const issuer_bytes = so.read(20); + const issuer = UInt160.from_bytes(issuer_bytes); issuer.set_version(Base.VER_ACCOUNT_ID); - var offset = ((value_bytes[0] & 0x3f) << 2) + (value_bytes[1] >>> 6) - 97; - var mantissa_bytes = value_bytes.slice(1); + const offset = + ((value_bytes[0] & 0x3f) << 2) + (value_bytes[1] >>> 6) - 97; + const mantissa_bytes = value_bytes.slice(1); mantissa_bytes[0] &= 0x3f; - var mantissa = new BigNumber(utils.arrayToHex(mantissa_bytes), 16); - var sign = is_negative ? '-' : ''; - var valueString = sign + mantissa.toString() + 'e' + offset.toString(); + const mantissa = new BigNumber(utils.arrayToHex(mantissa_bytes), 16); + const sign = is_negative ? '-' : ''; + const valueString = sign + mantissa.toString() + 'e' + offset.toString(); return Amount.from_json({ currency: currency, @@ -551,17 +562,17 @@ var STAmount = exports.Amount = new SerializedType({ } // native - var integer_bytes = value_bytes.slice(); + const integer_bytes = value_bytes.slice(); integer_bytes[0] &= 0x3f; - var integer_hex = utils.arrayToHex(integer_bytes); - var value = new BigNumber(integer_hex, 16); + const integer_hex = utils.arrayToHex(integer_bytes); + const value = new BigNumber(integer_hex, 16); return Amount.from_json((is_negative ? '-' : '') + value.toString()); } }); STAmount.id = 6; -var STVL = exports.VariableLength = exports.VL = new SerializedType({ +const STVL = exports.VariableLength = exports.VL = new SerializedType({ serialize: function(so, val) { if (typeof val === 'string') { serializeHex(so, val); @@ -570,29 +581,29 @@ var STVL = exports.VariableLength = exports.VL = new SerializedType({ } }, parse: function(so) { - var len = this.parse_varint(so); + const len = this.parse_varint(so); return convertByteArrayToHex(so.read(len)); } }); STVL.id = 7; -var STAccount = exports.Account = new SerializedType({ +const STAccount = exports.Account = new SerializedType({ serialize: function(so, val) { - var account = UInt160.from_json(val); + const account = UInt160.from_json(val); if (!account.is_valid()) { throw new Error('Invalid account!'); } serializeBits(so, account.to_bits()); }, parse: function(so) { - var len = this.parse_varint(so); + const len = this.parse_varint(so); if (len !== 20) { throw new Error('Non-standard-length account ID'); } - var result = UInt160.from_bytes(so.read(len)); + const result = UInt160.from_bytes(so.read(len)); result.set_version(Base.VER_ACCOUNT_ID); if (false && !result.is_valid()) { @@ -605,23 +616,23 @@ var STAccount = exports.Account = new SerializedType({ STAccount.id = 8; -var STPathSet = exports.PathSet = new SerializedType({ +const STPathSet = exports.PathSet = new SerializedType({ typeBoundary: 0xff, typeEnd: 0x00, typeAccount: 0x01, typeCurrency: 0x10, typeIssuer: 0x20, serialize: function(so, val) { - for (var i = 0, l = val.length; i < l; i++) { + for (let i = 0, l = val.length; i < l; i++) { // Boundary if (i) { STInt8.serialize(so, this.typeBoundary); } - for (var j = 0, l2 = val[i].length; j < l2; j++) { - var entry = val[i][j]; + for (let j = 0, l2 = val[i].length; j < l2; j++) { + const entry = val[i][j]; // if (entry.hasOwnProperty('_value')) {entry = entry._value;} - var type = 0; + let type = 0; if (entry.account) { type |= this.typeAccount; @@ -640,7 +651,7 @@ var STPathSet = exports.PathSet = new SerializedType({ } if (entry.currency) { - var currency = Currency.from_json(entry.currency, entry.non_native); + const currency = Currency.from_json(entry.currency, entry.non_native); STCurrency.serialize(so, currency); } @@ -666,9 +677,9 @@ var STPathSet = exports.PathSet = new SerializedType({ amount, currency, issuer. */ - var path_list = []; - var current_path = []; - var tag_byte; + const path_list = []; + let current_path = []; + let tag_byte; /* eslint-disable no-cond-assign */ @@ -686,8 +697,8 @@ var STPathSet = exports.PathSet = new SerializedType({ } // It's an entry-begin tag. - var entry = {}; - var type = 0; + const entry = {}; + let type = 0; if (tag_byte & this.typeAccount) { entry.account = STHash160.parse(so); @@ -729,19 +740,19 @@ var STPathSet = exports.PathSet = new SerializedType({ STPathSet.id = 18; -var STVector256 = exports.Vector256 = new SerializedType({ +const STVector256 = exports.Vector256 = new SerializedType({ serialize: function(so, val) { // Assume val is an array of STHash256 objects. SerializedType.serialize_varint(so, val.length * 32); - for (var i = 0, l = val.length; i < l; i++) { + for (let i = 0, l = val.length; i < l; i++) { STHash256.serialize(so, val[i]); } }, parse: function(so) { - var length = this.parse_varint(so); - var output = []; + const length = this.parse_varint(so); + const output = []; // length is number of bytes not number of Hash256 - for (var i = 0; i < length / 32; i++) { + for (let i = 0; i < length / 32; i++) { output.push(STHash256.parse(so)); } return output; @@ -753,7 +764,7 @@ STVector256.id = 19; // Internal exports.STMemo = new SerializedType({ serialize: function(so, val, no_marker) { - var keys = []; + let keys = []; Object.keys(val).forEach(function(key) { // Ignore lowercase field names - they're non-serializable fields by @@ -782,37 +793,41 @@ exports.STMemo = new SerializedType({ } }, parse: function(so) { - var output = {}; + const output = {}; while (so.peek(1)[0] !== 0xe1) { - var keyval = parse(so); + const keyval = parse(so); output[keyval[0]] = keyval[1]; } if (output.MemoType !== undefined) { try { - var parsedType = convertHexToString(output.MemoType); + const parsedType = convertHexToString(output.MemoType); if (parsedType !== 'unformatted_memo') { output.parsed_memo_type = parsedType; } + /*eslint-disable no-empty*/ } catch (e) { // empty // we don't know what's in the binary, apparently it's not a UTF-8 // string // this is fine, we won't add the parsed_memo_type field } + /*eslint-enable no-empty*/ } if (output.MemoFormat !== undefined) { try { output.parsed_memo_format = convertHexToString(output.MemoFormat); + /*eslint-disable no-empty*/ } catch (e) { // empty // we don't know what's in the binary, apparently it's not a UTF-8 // string // this is fine, we won't add the parsed_memo_format field } + /*eslint-enable no-empty*/ } if (output.MemoData !== undefined) { @@ -827,6 +842,7 @@ exports.STMemo = new SerializedType({ // otherwise see if we can parse text output.parsed_memo_data = convertHexToString(output.MemoData); } + /*eslint-disable no-empty*/ } catch(e) { // empty // we'll fail in case the content does not match what the MemoFormat @@ -834,6 +850,7 @@ exports.STMemo = new SerializedType({ // this is fine, we won't add the parsed_memo_data, the user has to // parse themselves } + /*eslint-enable no-empty*/ } so.read(1); @@ -842,9 +859,9 @@ exports.STMemo = new SerializedType({ }); -var STObject = exports.Object = new SerializedType({ +const STObject = exports.Object = new SerializedType({ serialize: function(so, val, no_marker) { - var keys = []; + let keys = []; Object.keys(val).forEach(function(key) { // Ignore lowercase field names - they're non-serializable fields by @@ -863,7 +880,7 @@ var STObject = exports.Object = new SerializedType({ // Sort fields keys = sort_fields(keys); - for (var i = 0; i < keys.length; i++) { + for (let i = 0; i < keys.length; i++) { serialize(so, keys[i], val[keys[i]]); } @@ -874,9 +891,9 @@ var STObject = exports.Object = new SerializedType({ }, parse: function(so) { - var output = {}; + const output = {}; while (so.peek(1)[0] !== 0xe1) { - var keyval = parse(so); + const keyval = parse(so); output[keyval[0]] = keyval[1]; } so.read(1); @@ -886,18 +903,18 @@ var STObject = exports.Object = new SerializedType({ STObject.id = 14; -var STArray = exports.Array = new SerializedType({ +const STArray = exports.Array = new SerializedType({ serialize: function(so, val) { - for (var i = 0, l = val.length; i < l; i++) { - var keys = Object.keys(val[i]); + for (let i = 0, l = val.length; i < l; i++) { + const keys = Object.keys(val[i]); if (keys.length !== 1) { throw new Error( 'Cannot serialize an array containing non-single-key objects'); } - var field_name = keys[0]; - var value = val[i][field_name]; + const field_name = keys[0]; + const value = val[i][field_name]; serialize(so, field_name, value); } @@ -906,11 +923,11 @@ var STArray = exports.Array = new SerializedType({ }, parse: function(so) { - var output = [ ]; + const output = [ ]; while (so.peek(1)[0] !== 0xf1) { - var keyval = parse(so); - var obj = { }; + const keyval = parse(so); + const obj = { }; obj[keyval[0]] = keyval[1]; output.push(obj); } diff --git a/src/core/utils.js b/src/core/utils.js index e8fe8f3c..6341a6c3 100644 --- a/src/core/utils.js +++ b/src/core/utils.js @@ -1,16 +1,24 @@ 'use strict'; -function getMantissaDecimalString(bignum) { - let mantissa = bignum.toPrecision(16) - .replace(/\./, '') // remove decimal point +// returns the mantissa from the passed in string, +// adding zeros until it has 16 sd +function getMantissa16FromString(decimalString) { + let mantissa = decimalString.replace(/\./, '') // remove decimal point .replace(/e.*/, '') // remove scientific notation .replace(/^0*/, ''); // remove leading zeroes + if (mantissa.length > 16) { + return mantissa.substring(0, 16); + } while (mantissa.length < 16) { mantissa += '0'; // add trailing zeroes until length is 16 } return mantissa; } +function getMantissaDecimalString(bignum) { + return getMantissa16FromString(bignum.toPrecision(16)); +} + function trace(comment, func) { return function() { console.log('%s: %s', trace, arguments.toString); @@ -156,6 +164,7 @@ exports.arrayUnique = arrayUnique; exports.toTimestamp = toTimestamp; exports.fromTimestamp = fromTimestamp; exports.getMantissaDecimalString = getMantissaDecimalString; +exports.getMantissa16FromString = getMantissa16FromString; exports.sjcl = require('sjcl-extended'); diff --git a/src/core/value.js b/src/core/value.js new file mode 100644 index 00000000..eea11495 --- /dev/null +++ b/src/core/value.js @@ -0,0 +1,109 @@ +/* @flow */ + +'use strict'; + +const GlobalBigNumber = require('bignumber.js'); + +const BigNumber = GlobalBigNumber.another({ + ROUNDING_MODE: GlobalBigNumber.ROUND_HALF_UP, + DECIMAL_PLACES: 40 +}); + +const assert = require('assert'); + +class Value { + + constructor(value: string | BigNumber) { + if (this.constructor === 'Value') { + throw new Error( + 'Cannot instantiate Value directly, it is an abstract base class'); + } + this._value = new BigNumber(value); + } + + static getBNRoundDown() { + return BigNumber.ROUND_DOWN; + } + + abs() { + const result = this._value.abs(); + return this._canonicalize(result); + } + + add(addend: Value) { + assert(this.constructor === addend.constructor); + const result = this._value.plus(addend._value); + return this._canonicalize(result); + } + + subtract(subtrahend: Value) { + assert(this.constructor === subtrahend.constructor); + const result = this._value.minus(subtrahend._value); + return this._canonicalize(result); + } + + multiply(multiplicand: Value) { + const result = this._value.times(multiplicand._value); + return this._canonicalize(result); + } + + divide(divisor: Value) { + if (divisor.isZero()) { + throw new Error('divide by zero'); + } + const result = this._value.dividedBy(divisor._value); + return this._canonicalize(result); + } + + invert() { + const result = (new BigNumber(this._value)).toPower(-1); + return this._canonicalize(result); + } + + round(decimalPlaces: number, roundingMode: number) { + const result = this._value.round(decimalPlaces, roundingMode); + return this._canonicalize(result); + } + + toFixed(decimalPlaces: number, roundingMode: number) { + return this._value.toFixed(decimalPlaces, roundingMode); + } + + getExponent() { + return this._value.e; + } + + isNaN() { + return this._value.isNaN(); + } + + isZero() { + return this._value.isZero(); + } + + isNegative() { + return this._value.isNegative(); + } + + toString() { + return this._value.toString(); + } + + greaterThan(comparator: Value) { + assert(this.constructor === comparator.constructor); + return this._value.greaterThan(comparator._value); + } + + lessThan(comparator: Value) { + assert(this.constructor === comparator.constructor); + return this._value.lessThan(comparator._value); + } + + comparedTo(comparator: Value) { + assert(this.constructor === comparator.constructor); + return this._value.comparedTo(comparator._value); + } + +} + +exports.Value = Value; diff --git a/src/core/xrpvalue.js b/src/core/xrpvalue.js new file mode 100644 index 00000000..47107847 --- /dev/null +++ b/src/core/xrpvalue.js @@ -0,0 +1,59 @@ +/* @flow */ + +'use strict'; + +const GlobalBigNumber = require('bignumber.js'); +const BigNumber = GlobalBigNumber.another({ + ROUNDING_MODE: GlobalBigNumber.ROUND_HALF_UP, + DECIMAL_PLACES: 40 +}); + +const Value = require('./value').Value; +const rippleUnits = new BigNumber(1e6); + +class XRPValue extends Value { + + constructor(value: string | BigNumber) { + super(value); + if (this._value.dp() > 6) { + throw new Error( + 'Value has more than 6 digits of precision past the decimal point, ' + + 'an IOUValue may be being cast to an XRPValue' + ); + } + } + + multiply(multiplicand: Value) { + if (multiplicand instanceof XRPValue) { + return super.multiply( + new XRPValue(multiplicand._value.times(rippleUnits))); + } + return super.multiply(multiplicand); + } + + divide(divisor: Value) { + if (divisor instanceof XRPValue) { + return super.divide( + new XRPValue(divisor._value.times(rippleUnits))); + } + return super.divide(divisor); + } + + negate() { + return new XRPValue(this._value.neg()); + } + + _canonicalize(value) { + if (value.isNaN()) { + throw new Error('Invalid result'); + } + return new XRPValue(value.round(6, BigNumber.ROUND_DOWN)); + } + + equals(comparator) { + return (comparator instanceof XRPValue) + && this._value.equals(comparator._value); + } +} + +exports.XRPValue = XRPValue; diff --git a/test/amount-test.js b/test/amount-test.js index 72339c40..3deec477 100644 --- a/test/amount-test.js +++ b/test/amount-test.js @@ -1186,19 +1186,19 @@ describe('Amount', function() { describe('amount limits', function() { it('max JSON wire limite', function() { - assert.strictEqual(Amount.bi_xns_max.toString(), '100000000000000000'); + assert.strictEqual(Amount.bi_xns_max, '100000000000000000'); }); it('max JSON wire limite', function() { - assert.strictEqual(Amount.bi_xns_min.toString(), '-100000000000000000'); + assert.strictEqual(Amount.bi_xns_min, '-100000000000000000'); }); it('max mantissa value', function() { - assert.strictEqual(Amount.bi_man_max_value.toString(), '9999999999999999'); + assert.strictEqual(Amount.bi_man_max_value, '9999999999999999'); }); it('min mantissa value', function() { - assert.strictEqual(Amount.bi_man_min_value.toString(), '1000000000000000'); + assert.strictEqual(Amount.bi_man_min_value, '1000000000000000'); }); it('from_json minimum XRP', function() {