mirror of
https://github.com/Xahau/xahau.js.git
synced 2025-11-20 04:05:52 +00:00
Amount: Full demurrage support.
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
56
src/js/ripple/float.js
Normal file
56
src/js/ripple/float.js
Normal file
@@ -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;
|
||||
};
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user