mirror of
https://github.com/Xahau/xahau.js.git
synced 2025-11-27 07:35:52 +00:00
Fix currency parsing of non-alphanumeric and no-currency currencies
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
var extend = require('extend');
|
||||
'use strict';
|
||||
|
||||
var extend = require('extend');
|
||||
var UInt160 = require('./uint160').UInt160;
|
||||
var utils = require('./utils');
|
||||
var Float = require('./ieee754').Float;
|
||||
@@ -16,8 +18,7 @@ var Currency = extend(function() {
|
||||
// 3-letter code: ...
|
||||
// XXX Should support hex, C++ doesn't currently allow it.
|
||||
|
||||
this._value = NaN;
|
||||
|
||||
this._value = NaN;
|
||||
this._update();
|
||||
}, UInt160);
|
||||
|
||||
@@ -32,25 +33,37 @@ Currency.HEX_CURRENCY_BAD = '0000000000000000000000005852500000000000';
|
||||
* Examples:
|
||||
*
|
||||
* USD => currency
|
||||
* USD - Dollar => currency with optional full currency name
|
||||
* XAU (-0.5%pa) => XAU with 0.5% effective demurrage rate per year
|
||||
* USD - Dollar => currency with optional full currency
|
||||
* name
|
||||
* XAU (-0.5%pa) => XAU with 0.5% effective demurrage rate
|
||||
* per year
|
||||
* XAU - Gold (-0.5%pa) => Optionally allowed full currency name
|
||||
* USD (1%pa) => US dollars with 1% effective interest per year
|
||||
* USD (1%pa) => US dollars with 1% effective interest
|
||||
* per year
|
||||
* INR - Indian Rupees => Optional full currency name with spaces
|
||||
* TYX - 30-Year Treasuries => Optional full currency with numbers and a dash
|
||||
* TYX - 30-Year Treasuries (1.5%pa) => Optional full currency with numbers, dash and interest rate
|
||||
* TYX - 30-Year Treasuries => Optional full currency with numbers
|
||||
* and a dash
|
||||
* TYX - 30-Year Treasuries (1.5%pa) => Optional full currency with numbers,
|
||||
* dash and interest rate
|
||||
*
|
||||
* The regular expression below matches above cases, broken down for better understanding:
|
||||
* The regular expression below matches above cases, broken down for better
|
||||
* understanding:
|
||||
*
|
||||
* ^\s* // start with any amount of whitespace
|
||||
* ([a-zA-Z]{3}|[0-9]{3}) // either 3 letter alphabetic currency-code or 3 digit numeric currency-code. See ISO 4217
|
||||
* (\s*-\s*[- \w]+) // optional full currency name following the dash after currency code,
|
||||
* full currency code can contain letters, numbers and dashes
|
||||
* (\s*\(-?\d+\.?\d*%pa\))? // optional demurrage rate, has optional - and . notation (-0.5%pa)
|
||||
* ([a-zA-Z]{3}|[0-9]{3}) // either 3 letter alphabetic currency-code or 3
|
||||
* digit numeric currency-code. See ISO 4217
|
||||
* (\s*-\s*[- \w]+) // optional full currency name following the dash
|
||||
* after currency code, full currency code can
|
||||
* contain letters, numbers and dashes
|
||||
* (\s*\(-?\d+\.?\d*%pa\))? // optional demurrage rate, has optional - and
|
||||
* . notation (-0.5%pa)
|
||||
* \s*$ // end with any amount of whitespace
|
||||
*
|
||||
*/
|
||||
Currency.prototype.human_RE = /^\s*([a-zA-Z0-9]{3})(\s*-\s*[- \w]+)?(\s*\(-?\d+\.?\d*%pa\))?\s*$/;
|
||||
|
||||
/*eslint-disable max-len*/
|
||||
Currency.prototype.human_RE = /^\s*([a-zA-Z0-9\<\>\(\)\{\}\[\]\|\?\!\@\#\$\%\^\&]{3})(\s*-\s*[- \w]+)?(\s*\(-?\d+\.?\d*%pa\))?\s*$/;
|
||||
/*eslint-enable max-len*/
|
||||
|
||||
Currency.from_json = function(j, shouldInterpretXrpAsIou) {
|
||||
return (new Currency()).parse_json(j, shouldInterpretXrpAsIou);
|
||||
@@ -58,39 +71,65 @@ Currency.from_json = function(j, shouldInterpretXrpAsIou) {
|
||||
|
||||
Currency.from_human = function(j, opts) {
|
||||
return (new Currency().parse_human(j, opts));
|
||||
}
|
||||
};
|
||||
|
||||
// this._value = NaN on error.
|
||||
Currency.prototype.parse_json = function(j, shouldInterpretXrpAsIou) {
|
||||
this._value = NaN;
|
||||
|
||||
switch (typeof j) {
|
||||
case 'string':
|
||||
if (j instanceof Currency) {
|
||||
this._value = j.copyTo({})._value;
|
||||
this._update();
|
||||
return this;
|
||||
}
|
||||
|
||||
// if an empty string is given, fall back to XRP
|
||||
switch (typeof j) {
|
||||
case 'number':
|
||||
if (!isNaN(j)) {
|
||||
this.parse_number(j);
|
||||
}
|
||||
break;
|
||||
case 'string':
|
||||
if (!j || j === '0') {
|
||||
this.parse_hex(shouldInterpretXrpAsIou ? Currency.HEX_CURRENCY_BAD : Currency.HEX_ZERO);
|
||||
// Empty string or XRP
|
||||
this.parse_hex(shouldInterpretXrpAsIou
|
||||
? Currency.HEX_CURRENCY_BAD
|
||||
: Currency.HEX_ZERO);
|
||||
break;
|
||||
}
|
||||
|
||||
if (j === '1') {
|
||||
// 'no currency'
|
||||
this.parse_hex(Currency.HEX_ONE);
|
||||
break;
|
||||
}
|
||||
|
||||
if (/^[A-F0-9]{40}$/.test(j)) {
|
||||
// Hex format
|
||||
this.parse_hex(j);
|
||||
break;
|
||||
}
|
||||
|
||||
// match the given string to see if it's in an allowed format
|
||||
var matches = String(j).match(this.human_RE);
|
||||
var matches = j.match(this.human_RE);
|
||||
|
||||
if (matches) {
|
||||
|
||||
var currencyCode = matches[1];
|
||||
|
||||
// for the currency 'XRP' case
|
||||
// we drop everything else that could have been provided
|
||||
// e.g. 'XRP - Ripple'
|
||||
if (!currencyCode || /^(0|XRP)$/.test(currencyCode)) {
|
||||
this.parse_hex(shouldInterpretXrpAsIou ? Currency.HEX_CURRENCY_BAD : Currency.HEX_ZERO);
|
||||
this.parse_hex(shouldInterpretXrpAsIou
|
||||
? Currency.HEX_CURRENCY_BAD
|
||||
: Currency.HEX_ZERO);
|
||||
|
||||
// early break, we can't have interest on XRP
|
||||
break;
|
||||
}
|
||||
|
||||
// the full currency is matched as it is part of the valid currency format, but not stored
|
||||
// the full currency is matched as it is part of the valid currency
|
||||
// format, but not stored
|
||||
// var full_currency = matches[2] || '';
|
||||
var interest = matches[3] || '';
|
||||
|
||||
@@ -117,25 +156,28 @@ Currency.prototype.parse_json = function(j, shouldInterpretXrpAsIou) {
|
||||
currencyData[2] = currencyCode.charCodeAt(1) & 0xff;
|
||||
currencyData[3] = currencyCode.charCodeAt(2) & 0xff;
|
||||
|
||||
// byte 5-8 are for reference date, but should always be 0 so we won't fill it
|
||||
// byte 5-8 are for reference date, but should always be 0 so we
|
||||
// won't fill it
|
||||
|
||||
// byte 9-16 are for the interest
|
||||
percentage = parseFloat(percentage[0]);
|
||||
|
||||
// the interest or demurrage is expressed as a yearly (per annum) value
|
||||
// the interest or demurrage is expressed as a yearly (per annum)
|
||||
// value
|
||||
var secondsPerYear = 31536000; // 60 * 60 * 24 * 365
|
||||
|
||||
// Calculating the interest e-fold
|
||||
// 0.5% demurrage is expressed 0.995, 0.005 less than 1
|
||||
// 0.5% interest is expressed as 1.005, 0.005 more than 1
|
||||
var interestEfold = secondsPerYear / Math.log(1 + percentage/100);
|
||||
var interestEfold = secondsPerYear / Math.log(1 + percentage / 100);
|
||||
var bytes = Float.toIEEE754Double(interestEfold);
|
||||
|
||||
for (var i=0; i<=bytes.length; i++) {
|
||||
for (var i = 0; i <= bytes.length; i++) {
|
||||
currencyData[8 + i] = bytes[i] & 0xff;
|
||||
}
|
||||
|
||||
// the last 4 bytes are reserved for future use, so we won't fill those
|
||||
// the last 4 bytes are reserved for future use, so we won't fill
|
||||
// those
|
||||
|
||||
} else {
|
||||
currencyData[12] = currencyCode.charCodeAt(0) & 0xff;
|
||||
@@ -144,21 +186,6 @@ Currency.prototype.parse_json = function(j, shouldInterpretXrpAsIou) {
|
||||
}
|
||||
|
||||
this.parse_bytes(currencyData);
|
||||
} else {
|
||||
this.parse_hex(j);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'number':
|
||||
if (!isNaN(j)) {
|
||||
this.parse_number(j);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'object':
|
||||
if (j instanceof Currency) {
|
||||
this._value = j.copyTo({})._value;
|
||||
this._update();
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -166,7 +193,6 @@ Currency.prototype.parse_json = function(j, shouldInterpretXrpAsIou) {
|
||||
return this;
|
||||
};
|
||||
|
||||
|
||||
Currency.prototype.parse_human = function(j) {
|
||||
return this.parse_json(j);
|
||||
};
|
||||
@@ -176,6 +202,7 @@ Currency.prototype.parse_human = function(j) {
|
||||
*
|
||||
* You should never need to call this.
|
||||
*/
|
||||
|
||||
Currency.prototype._update = function() {
|
||||
var bytes = this.to_bytes();
|
||||
|
||||
@@ -183,7 +210,7 @@ Currency.prototype._update = function() {
|
||||
var isZeroExceptInStandardPositions = true;
|
||||
|
||||
if (!bytes) {
|
||||
return 'XRP';
|
||||
return;
|
||||
}
|
||||
|
||||
this._native = false;
|
||||
@@ -192,8 +219,9 @@ Currency.prototype._update = function() {
|
||||
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);
|
||||
for (var i = 0; i < 20; i++) {
|
||||
isZeroExceptInStandardPositions = isZeroExceptInStandardPositions
|
||||
&& (i === 12 || i === 13 || i === 14 || bytes[i] === 0);
|
||||
}
|
||||
|
||||
if (isZeroExceptInStandardPositions) {
|
||||
@@ -201,7 +229,7 @@ Currency.prototype._update = function() {
|
||||
+ String.fromCharCode(bytes[13])
|
||||
+ String.fromCharCode(bytes[14]);
|
||||
|
||||
if (this._iso_code === '\0\0\0') {
|
||||
if (this._iso_code === '\u0000\u0000\u0000') {
|
||||
this._native = true;
|
||||
this._iso_code = 'XRP';
|
||||
}
|
||||
@@ -215,8 +243,8 @@ Currency.prototype._update = function() {
|
||||
this._type = 1;
|
||||
this._interest_start = (bytes[4] << 24) +
|
||||
(bytes[5] << 16) +
|
||||
(bytes[6] << 8) +
|
||||
(bytes[7] );
|
||||
(bytes[6] << 8) +
|
||||
(bytes[7]);
|
||||
this._interest_period = Float.fromIEEE754Double(bytes.slice(8, 16));
|
||||
}
|
||||
};
|
||||
@@ -230,7 +258,8 @@ Currency.prototype.parse_bytes = function(byte_array) {
|
||||
var isZeroExceptInStandardPositions = true;
|
||||
|
||||
for (var i=0; i<20; i++) {
|
||||
isZeroExceptInStandardPositions = isZeroExceptInStandardPositions && (i===12 || i===13 || i===14 || byte_array[0]===0)
|
||||
isZeroExceptInStandardPositions = isZeroExceptInStandardPositions
|
||||
&& (i===12 || i===13 || i===14 || byte_array[0]===0)
|
||||
}
|
||||
|
||||
if (isZeroExceptInStandardPositions) {
|
||||
@@ -260,20 +289,25 @@ Currency.prototype.is_native = function() {
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether this currency is an interest-bearing/demurring currency.
|
||||
* @return {Boolean} whether this currency is an interest-bearing currency
|
||||
*/
|
||||
|
||||
Currency.prototype.has_interest = function() {
|
||||
return this._type === 1 && !isNaN(this._interest_start) && !isNaN(this._interest_period);
|
||||
return this._type === 1
|
||||
&& !isNaN(this._interest_start)
|
||||
&& !isNaN(this._interest_period);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param referenceDate - number of seconds since the Ripple Epoch (0:00 on January 1, 2000 UTC)
|
||||
* used to calculate the interest over provided interval
|
||||
* pass in one years worth of seconds to ge the yearly interest
|
||||
* @returns {number} - interest for provided interval, can be negative for demurred currencies
|
||||
* @param {number} referenceDate number of seconds since the Ripple Epoch
|
||||
* (0:00 on January 1, 2000 UTC) used to calculate the
|
||||
* interest over provided interval pass in one years
|
||||
* worth of seconds to ge the yearly interest
|
||||
* @returns {number} interest for provided interval, can be negative for
|
||||
* demurred currencies
|
||||
*/
|
||||
Currency.prototype.get_interest_at = function(referenceDate, decimals) {
|
||||
Currency.prototype.get_interest_at = function(referenceDate) {
|
||||
if (!this.has_interest()) {
|
||||
return 0;
|
||||
}
|
||||
@@ -288,18 +322,20 @@ Currency.prototype.get_interest_at = function(referenceDate, decimals) {
|
||||
}
|
||||
|
||||
// calculate interest by e-fold number
|
||||
return Math.exp((referenceDate - this._interest_start) / this._interest_period);
|
||||
return Math.exp((referenceDate - this._interest_start)
|
||||
/ this._interest_period);
|
||||
};
|
||||
|
||||
Currency.prototype.get_interest_percentage_at = function(referenceDate, decimals) {
|
||||
Currency.prototype.get_interest_percentage_at
|
||||
= function(referenceDate, decimals) {
|
||||
var interest = this.get_interest_at(referenceDate, decimals);
|
||||
|
||||
// convert to percentage
|
||||
var interest = (interest*100)-100;
|
||||
var decimalMultiplier = decimals ? Math.pow(10,decimals) : 100;
|
||||
interest = (interest * 100) - 100;
|
||||
var decimalMultiplier = decimals ? Math.pow(10, decimals) : 100;
|
||||
|
||||
// round to two decimals behind the dot
|
||||
return Math.round(interest*decimalMultiplier) / decimalMultiplier;
|
||||
return Math.round(interest * decimalMultiplier) / decimalMultiplier;
|
||||
};
|
||||
|
||||
// XXX Currently we inherit UInt.prototype.is_valid, which is mostly fine.
|
||||
@@ -307,9 +343,9 @@ Currency.prototype.get_interest_percentage_at = function(referenceDate, decimals
|
||||
// We could be doing further checks into the internal format of the
|
||||
// currency data, since there are some values that are invalid.
|
||||
//
|
||||
//Currency.prototype.is_valid = function() {
|
||||
// Currency.prototype.is_valid = function() {
|
||||
// return UInt.prototype.is_valid() && ...;
|
||||
//};
|
||||
// };
|
||||
|
||||
Currency.prototype.to_json = function(opts) {
|
||||
if (!this.is_valid()) {
|
||||
@@ -317,28 +353,35 @@ Currency.prototype.to_json = function(opts) {
|
||||
return 'XRP';
|
||||
}
|
||||
|
||||
var opts = opts || {};
|
||||
if (!opts) {
|
||||
opts = {};
|
||||
}
|
||||
|
||||
var currency;
|
||||
var fullName = opts && opts.full_name ? ' - ' + opts.full_name : '';
|
||||
opts.show_interest = opts.show_interest !== void(0) ? opts.show_interest : this.has_interest();
|
||||
opts.show_interest = opts.show_interest !== undefined
|
||||
? opts.show_interest
|
||||
: this.has_interest();
|
||||
|
||||
if (!opts.force_hex && /^[A-Z0-9]{3}$/.test(this._iso_code)) {
|
||||
currency = this._iso_code + fullName;
|
||||
if (opts.show_interest) {
|
||||
var decimals = !isNaN(opts.decimals) ? opts.decimals : void(0);
|
||||
var interestPercentage = this.has_interest() ? this.get_interest_percentage_at(this._interest_start + 3600 * 24 * 365, decimals) : 0;
|
||||
var decimals = !isNaN(opts.decimals) ? opts.decimals : undefined;
|
||||
var interestPercentage = this.has_interest()
|
||||
? this.get_interest_percentage_at(
|
||||
this._interest_start + 3600 * 24 * 365, decimals
|
||||
)
|
||||
: 0;
|
||||
currency += ' (' + interestPercentage + '%pa)';
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// Fallback to returning the raw currency hex
|
||||
currency = 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.
|
||||
// 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 (currency === Currency.HEX_ONE) {
|
||||
currency = 1;
|
||||
}
|
||||
@@ -357,5 +400,3 @@ Currency.prototype.get_iso = function() {
|
||||
};
|
||||
|
||||
exports.Currency = Currency;
|
||||
|
||||
// vim:sw=2:sts=2:ts=8:et
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/*eslint-disable */
|
||||
|
||||
var assert = require('assert');
|
||||
var currency = require('ripple-lib').Currency;
|
||||
var timeUtil = require('ripple-lib').utils.time;
|
||||
@@ -54,6 +56,16 @@ describe('Currency', function() {
|
||||
assert(r.is_valid());
|
||||
assert.strictEqual('1D2', r.to_json());
|
||||
});
|
||||
it('from_json("1").to_human()', function() {
|
||||
var r = currency.from_json('1');
|
||||
assert(r.is_valid());
|
||||
assert.strictEqual(1, r.to_json());
|
||||
});
|
||||
it('from_json("#$%").to_human()', function() {
|
||||
var r = currency.from_json('#$%');
|
||||
assert(r.is_valid());
|
||||
assert.strictEqual('0000000000000000000000002324250000000000', r.to_json());
|
||||
});
|
||||
it('from_json("XAU").to_json() hex', function() {
|
||||
var r = currency.from_json("XAU");
|
||||
assert.strictEqual('0000000000000000000000005841550000000000', r.to_json({force_hex: true}));
|
||||
|
||||
Reference in New Issue
Block a user