From f678f47155c4b011f9c590467d3ec305b20346d9 Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Sat, 25 Jan 2014 11:31:56 -0800 Subject: [PATCH] Amount: Full demurrage support. --- src/js/ripple/amount.js | 29 ++++++++-- src/js/ripple/currency.js | 115 ++++++++++++++++++++++++++++---------- src/js/ripple/float.js | 56 +++++++++++++++++++ test/currency-test.js | 36 ++++++++++++ 4 files changed, 202 insertions(+), 34 deletions(-) create mode 100644 src/js/ripple/float.js diff --git a/src/js/ripple/amount.js b/src/js/ripple/amount.js index 145a5c2c..d042e8f5 100644 --- a/src/js/ripple/amount.js +++ b/src/js/ripple/amount.js @@ -847,9 +847,11 @@ Amount.prototype.to_text = function (allow_nan) { * default: 3. * @param opts.signed {Boolean|String} Whether negative numbers will have a * prefix. If String, that string will be used as the prefix. Default: '-' + * @param opts.reference_date {Date|Number} Date based on which demurrage/interest + * should be applied. Can be given as JavaScript Date or int for Ripple epoch. */ Amount.prototype.to_human = function (opts) { - var opts = opts || {}; + opts = opts || {}; if (!this.is_valid()) return ''; @@ -859,10 +861,29 @@ Amount.prototype.to_human = function (opts) { opts.group_width = opts.group_width || 3; - var order = this._is_native ? consts.xns_precision : -this._offset; + // Apply demurrage/interest + var ref = this; + if (opts.reference_date && this._currency.has_interest()) { + var interest = this._currency.get_interest_at(opts.reference_date); + + // XXX Because the Amount parsing routines don't support some of the things + // that JavaScript can output when casting a float to a string, the + // following call sometimes does not produce a valid Amount. + // + // The correct way to solve this is probably to switch to a proper + // BigDecimal for our internal representation and then use that across + // the board instead of instantiating these dummy Amount objects. + var interestTempAmount = Amount.from_json(""+interest+"/1/1"); + + if (interestTempAmount.is_valid()) { + ref = this.multiply(interestTempAmount); + } + } + + var order = ref._is_native ? consts.xns_precision : -ref._offset; var denominator = consts.bi_10.clone().pow(order); - var int_part = this._value.divide(denominator).toString(); - var fraction_part = this._value.mod(denominator).toString(); + var int_part = ref._value.divide(denominator).toString(); + var fraction_part = ref._value.mod(denominator).toString(); // Add leading zeros to fraction while (fraction_part.length < order) { diff --git a/src/js/ripple/currency.js b/src/js/ripple/currency.js index 4dcf51b0..12e05a84 100644 --- a/src/js/ripple/currency.js +++ b/src/js/ripple/currency.js @@ -2,6 +2,7 @@ var extend = require('extend'); var UInt160 = require('./uint160').UInt160; +var Float = require('./float').Float; var utils = require('./utils'); // @@ -18,6 +19,8 @@ var Currency = extend(function () { // XXX Should support hex, C++ doesn't currently allow it. this._value = NaN; + + this._update(); }, UInt160); Currency.prototype = extend({}, UInt160.prototype); @@ -66,6 +69,7 @@ Currency.prototype.parse_json = function (j, shouldInterpretXrpAsIou) { case 'object': if (j instanceof Currency) { this._value = j.copyTo({})._value; + this._update(); } break; } @@ -73,6 +77,56 @@ Currency.prototype.parse_json = function (j, shouldInterpretXrpAsIou) { return this; }; +/** + * Recalculate internal representation. + * + * You should never need to call this. + */ +Currency.prototype._update = function () { + var bytes = this.to_bytes(); + + // is it 0 everywhere except 12, 13, 14? + var isZeroExceptInStandardPositions = true; + + if (!bytes) { + return "XRP"; + } + + this._native = false; + this._type = -1; + this._interest_start = new Date(); + this._interest_period = NaN; + this._iso_code = ''; + + for (var i=0; i<20; i++) { + isZeroExceptInStandardPositions = isZeroExceptInStandardPositions && (i===12 || i===13 || i===14 || bytes[i]===0); + } + + if (isZeroExceptInStandardPositions) { + this._iso_code = String.fromCharCode(bytes[12]) + + String.fromCharCode(bytes[13]) + + String.fromCharCode(bytes[14]); + + if (this._iso_code === "\0\0\0") { + this._native = true; + this._iso_code = "XRP"; + } + + this._type = 0; + } else if (bytes[0] === 0x01) { // Demurrage currency + this._iso_code = String.fromCharCode(bytes[1]) + + String.fromCharCode(bytes[2]) + + String.fromCharCode(bytes[3]); + + this._type = 1; + this._interest_start = (bytes[4] << 24) + + (bytes[5] << 16) + + (bytes[6] << 8) + + (bytes[7] ); + this._interest_period = Float.fromBytes(bytes.slice(8, 16)); + } +}; + // XXX Probably not needed anymore? /* Currency.prototype.parse_bytes = function (byte_array) { @@ -108,7 +162,24 @@ Currency.prototype.parse_bytes = function (byte_array) { */ Currency.prototype.is_native = function () { - return !isNaN(this._value) && this.is_zero(); + return this._native; +}; + +/** + * Whether this currency is an interest-bearing/demurring currency. + */ +Currency.prototype.has_interest = function () { + return this._type === 1 && this._interest_start && !isNaN(this._interest_period); +}; + +Currency.prototype.get_interest_at = function (referenceDate) { + if (!this.has_interest) return 1; + + if (referenceDate instanceof Date) { + referenceDate = utils.fromTimestamp(referenceDate.getTime()); + } + + return Math.pow(Math.E, (referenceDate - this._interest_start) / this._interest_period); }; // XXX Currently we inherit UInt.prototype.is_valid, which is mostly fine. @@ -121,42 +192,26 @@ Currency.prototype.is_native = function () { //}; Currency.prototype.to_json = function () { - var bytes = this.to_bytes(); - - // is it 0 everywhere except 12, 13, 14? - var isZeroExceptInStandardPositions = true; - - if (!bytes) { + if (!this.is_valid()) { + // XXX This backwards compatible behavior, but probably not very good. return "XRP"; } - for (var i=0; i<20; i++) { - isZeroExceptInStandardPositions = isZeroExceptInStandardPositions && (i===12 || i===13 || i===14 || bytes[i]===0); + if (/^[A-Z0-9]{3}$/.test(this._iso_code)) { + return this._iso_code; } - if (isZeroExceptInStandardPositions) { - var currencyCode = String.fromCharCode(bytes[12]) - + String.fromCharCode(bytes[13]) - + String.fromCharCode(bytes[14]); - if (/^[A-Z0-9]{3}$/.test(currencyCode) && currencyCode !== "XRP" ) { - return currencyCode; - } else if (currencyCode === "\0\0\0") { - return "XRP"; - } else { - return "XRP"; - } - } else { - var currencyHex = this.to_hex(); + // Fallback to returning the raw currency hex + var currencyHex = this.to_hex(); - // XXX This is to maintain backwards compatibility, but it is very, very odd - // behavior, so we should deprecate it and get rid of it as soon as - // possible. - if (currencyHex === Currency.HEX_ONE) { - return 1; - } - - return currencyHex; + // XXX This is to maintain backwards compatibility, but it is very, very odd + // behavior, so we should deprecate it and get rid of it as soon as + // possible. + if (currencyHex === Currency.HEX_ONE) { + return 1; } + + return currencyHex; }; Currency.prototype.to_human = function () { diff --git a/src/js/ripple/float.js b/src/js/ripple/float.js new file mode 100644 index 00000000..23aab200 --- /dev/null +++ b/src/js/ripple/float.js @@ -0,0 +1,56 @@ +/** + * IEEE 754 floating-point. + * + * Supports single- or double-precision + */ +var Float = exports.Float = {}; + +var allZeros = /^0+$/; +var allOnes = /^1+$/; + +Float.fromBytes = function (bytes) { + // Render in binary. Hackish. + var b = ""; + for (var i = 0, n = bytes.length; i < n; i++) { + var bits = (bytes[i] & 0xff).toString(2); + while (bits.length < 8) bits = "0" + bits; + b += bits; + } + + // Determine configuration. This could have all been precomputed but it is fast enough. + var exponentBits = bytes.length === 4 ? 4 : 11; + var mantissaBits = (bytes.length * 8) - exponentBits - 1; + var bias = Math.pow(2, exponentBits - 1) - 1; + var minExponent = 1 - bias - mantissaBits; + + // Break up the binary representation into its pieces for easier processing. + var s = b[0]; + var e = b.substring(1, exponentBits + 1); + var m = b.substring(exponentBits + 1); + + var value = 0; + var multiplier = (s === "0" ? 1 : -1); + + if (allZeros.test(e)) { + // Zero or denormalized + if (allZeros.test(m)) { + // Value is zero + } else { + value = parseInt(m, 2) * Math.pow(2, minExponent); + } + } else if (allOnes.test(e)) { + // Infinity or NaN + if (allZeros.test(m)) { + value = Infinity; + } else { + value = NaN; + } + } else { + // Normalized + var exponent = parseInt(e, 2) - bias; + var mantissa = parseInt(m, 2); + value = (1 + (mantissa * Math.pow(2, -mantissaBits))) * Math.pow(2, exponent); + } + + return value * multiplier; +}; diff --git a/test/currency-test.js b/test/currency-test.js index c8ccd550..65a83168 100644 --- a/test/currency-test.js +++ b/test/currency-test.js @@ -10,6 +10,9 @@ describe('Currency', function() { it('json_rewrite("NaN") == "XRP"', function() { assert.strictEqual('XRP', currency.json_rewrite(NaN)); }); + it('json_rewrite("015841551A748AD2C1F76FF6ECB0CCCD00000000") == "XAU"', function() { + assert.strictEqual('XAU', currency.json_rewrite("015841551A748AD2C1F76FF6ECB0CCCD00000000")); + }); }); describe('from_json', function() { it('from_json(NaN).to_json() == "XRP"', function() { @@ -52,4 +55,37 @@ describe('Currency', function() { assert.strictEqual('XRP', currency.from_json('XRP').to_human()); }); }); + describe('has_interest', function() { + it('should be true for type 1 currency codes', function() { + assert(currency.from_hex('015841551A748AD2C1F76FF6ECB0CCCD00000000').has_interest()); + assert(currency.from_json('015841551A748AD2C1F76FF6ECB0CCCD00000000').has_interest()); + }); + it('should be false for type 0 currency codes', function() { + assert(!currency.from_hex('0000000000000000000000005553440000000000').has_interest()); + assert(!currency.from_json('USD').has_interest()); + }); + }); + function precision(num, precision) { + return +(Math.round(num + "e+"+precision) + "e-"+precision); + } + describe('get_interest_at', function() { + it('returns demurred value for demurrage currency', function() { + var cur = currency.from_json('015841551A748AD2C1F76FF6ECB0CCCD00000000'); + + // At start, no demurrage should occur + assert.equal(1, cur.get_interest_at(443845330)); + + // After one year, 0.5% should have occurred + assert.equal(0.995, precision(cur.get_interest_at(443845330 + 31536000), 14)); + + // After one demurrage period, 1/e should have occurred + assert.equal(1/Math.E, cur.get_interest_at(443845330 + 6291418827.05)); + + // One year before start, it should be (roughly) 0.5% higher. + assert.equal(1.005, precision(cur.get_interest_at(443845330 - 31536000), 4)); + + // One demurrage period before start, rate should be e + assert.equal(Math.E, cur.get_interest_at(443845330 - 6291418827.05)); + }); + }); });