Skip to content

Commit

Permalink
Fix currency parsing of non-alphanumeric and no-currency currencies
Browse files Browse the repository at this point in the history
  • Loading branch information
wltsmrz committed Mar 5, 2015
1 parent a9b7d7d commit 2166bb2
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 74 deletions.
189 changes: 115 additions & 74 deletions src/js/ripple/currency.js
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);

Expand All @@ -32,65 +33,103 @@ 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);
};

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;

if (j instanceof Currency) {
this._value = j.copyTo({})._value;
this._update();
return this;
}

switch (typeof j) {
case 'number':
if (!isNaN(j)) {
this.parse_number(j);
}
break;
case 'string':

// if an empty string is given, fall back to XRP
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] || '';

Expand All @@ -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;
Expand All @@ -144,29 +186,13 @@ 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;
}

return this;
};


Currency.prototype.parse_human = function(j) {
return this.parse_json(j);
};
Expand All @@ -176,14 +202,15 @@ Currency.prototype.parse_human = function(j) {
*
* 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';
return;
}

this._native = false;
Expand All @@ -192,16 +219,17 @@ 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) {
this._iso_code = String.fromCharCode(bytes[12])
+ 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';
}
Expand All @@ -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));
}
};
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
}
Expand All @@ -288,57 +322,66 @@ 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.
//
// 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()) {
// XXX This is backwards compatible behavior, but probably not very good.
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;
}
Expand All @@ -357,5 +400,3 @@ Currency.prototype.get_iso = function() {
};

exports.Currency = Currency;

// vim:sw=2:sts=2:ts=8:et
Loading

0 comments on commit 2166bb2

Please sign in to comment.