mirror of
https://github.com/Xahau/xahau.js.git
synced 2025-11-20 20:25:48 +00:00
Amount: Full demurrage support.
This commit is contained in:
@@ -847,9 +847,11 @@ Amount.prototype.to_text = function (allow_nan) {
|
|||||||
* default: 3.
|
* default: 3.
|
||||||
* @param opts.signed {Boolean|String} Whether negative numbers will have a
|
* @param opts.signed {Boolean|String} Whether negative numbers will have a
|
||||||
* prefix. If String, that string will be used as the prefix. Default: '-'
|
* 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) {
|
Amount.prototype.to_human = function (opts) {
|
||||||
var opts = opts || {};
|
opts = opts || {};
|
||||||
|
|
||||||
if (!this.is_valid()) return '';
|
if (!this.is_valid()) return '';
|
||||||
|
|
||||||
@@ -859,10 +861,29 @@ Amount.prototype.to_human = function (opts) {
|
|||||||
|
|
||||||
opts.group_width = opts.group_width || 3;
|
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 denominator = consts.bi_10.clone().pow(order);
|
||||||
var int_part = this._value.divide(denominator).toString();
|
var int_part = ref._value.divide(denominator).toString();
|
||||||
var fraction_part = this._value.mod(denominator).toString();
|
var fraction_part = ref._value.mod(denominator).toString();
|
||||||
|
|
||||||
// Add leading zeros to fraction
|
// Add leading zeros to fraction
|
||||||
while (fraction_part.length < order) {
|
while (fraction_part.length < order) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
var extend = require('extend');
|
var extend = require('extend');
|
||||||
|
|
||||||
var UInt160 = require('./uint160').UInt160;
|
var UInt160 = require('./uint160').UInt160;
|
||||||
|
var Float = require('./float').Float;
|
||||||
var utils = require('./utils');
|
var utils = require('./utils');
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -18,6 +19,8 @@ var Currency = extend(function () {
|
|||||||
// XXX Should support hex, C++ doesn't currently allow it.
|
// XXX Should support hex, C++ doesn't currently allow it.
|
||||||
|
|
||||||
this._value = NaN;
|
this._value = NaN;
|
||||||
|
|
||||||
|
this._update();
|
||||||
}, UInt160);
|
}, UInt160);
|
||||||
|
|
||||||
Currency.prototype = extend({}, UInt160.prototype);
|
Currency.prototype = extend({}, UInt160.prototype);
|
||||||
@@ -66,6 +69,7 @@ Currency.prototype.parse_json = function (j, shouldInterpretXrpAsIou) {
|
|||||||
case 'object':
|
case 'object':
|
||||||
if (j instanceof Currency) {
|
if (j instanceof Currency) {
|
||||||
this._value = j.copyTo({})._value;
|
this._value = j.copyTo({})._value;
|
||||||
|
this._update();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -73,6 +77,56 @@ Currency.prototype.parse_json = function (j, shouldInterpretXrpAsIou) {
|
|||||||
return this;
|
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?
|
// XXX Probably not needed anymore?
|
||||||
/*
|
/*
|
||||||
Currency.prototype.parse_bytes = function (byte_array) {
|
Currency.prototype.parse_bytes = function (byte_array) {
|
||||||
@@ -108,7 +162,24 @@ Currency.prototype.parse_bytes = function (byte_array) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
Currency.prototype.is_native = function () {
|
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.
|
// 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 () {
|
Currency.prototype.to_json = function () {
|
||||||
var bytes = this.to_bytes();
|
if (!this.is_valid()) {
|
||||||
|
// XXX This backwards compatible behavior, but probably not very good.
|
||||||
// is it 0 everywhere except 12, 13, 14?
|
|
||||||
var isZeroExceptInStandardPositions = true;
|
|
||||||
|
|
||||||
if (!bytes) {
|
|
||||||
return "XRP";
|
return "XRP";
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var i=0; i<20; i++) {
|
if (/^[A-Z0-9]{3}$/.test(this._iso_code)) {
|
||||||
isZeroExceptInStandardPositions = isZeroExceptInStandardPositions && (i===12 || i===13 || i===14 || bytes[i]===0);
|
return this._iso_code;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isZeroExceptInStandardPositions) {
|
// Fallback to returning the raw currency hex
|
||||||
var currencyCode = String.fromCharCode(bytes[12])
|
var currencyHex = this.to_hex();
|
||||||
+ 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();
|
|
||||||
|
|
||||||
// XXX This is to maintain backwards compatibility, but it is very, very odd
|
// 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
|
// behavior, so we should deprecate it and get rid of it as soon as
|
||||||
// possible.
|
// possible.
|
||||||
if (currencyHex === Currency.HEX_ONE) {
|
if (currencyHex === Currency.HEX_ONE) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
|
||||||
|
|
||||||
return currencyHex;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return currencyHex;
|
||||||
};
|
};
|
||||||
|
|
||||||
Currency.prototype.to_human = function () {
|
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() {
|
it('json_rewrite("NaN") == "XRP"', function() {
|
||||||
assert.strictEqual('XRP', currency.json_rewrite(NaN));
|
assert.strictEqual('XRP', currency.json_rewrite(NaN));
|
||||||
});
|
});
|
||||||
|
it('json_rewrite("015841551A748AD2C1F76FF6ECB0CCCD00000000") == "XAU"', function() {
|
||||||
|
assert.strictEqual('XAU', currency.json_rewrite("015841551A748AD2C1F76FF6ECB0CCCD00000000"));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
describe('from_json', function() {
|
describe('from_json', function() {
|
||||||
it('from_json(NaN).to_json() == "XRP"', 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());
|
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