ripplelib
Version:
A JavaScript API for interacting with Ripple in Node.js and the browser
379 lines (310 loc) • 12.4 kB
JavaScript
'use strict';
var extend = require('extend');
var UInt160 = require('./uint160').UInt160;
var utils = require('./utils');
var Float = require('./ieee754').Float;
//
// Currency support
//
var Currency = extend(function () {
// Internal form: 0 = XRP. 3 letter-code.
// XXX Internal should be 0 or hex with three letter annotation when valid.
// Json form:
// '', 'XRP', '0': 0
// 3-letter code: ...
// XXX Should support hex, C++ doesn't currently allow it.
this._value = NaN;
this._update();
}, UInt160);
Currency.prototype = Object.create(extend({}, UInt160.prototype));
Currency.prototype.constructor = Currency;
Currency.HEX_CURRENCY_BAD = '0000000000000000000000005852500000000000';
/**
* Tries to correctly interpret a Currency as entered by a user.
*
* Examples:
*
* USD => currency
* 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
* 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
*
* 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)
* \s*$ // end with any amount of whitespace
*
*/
/*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 (!j || j === '0') {
// 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 = 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);
// 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
// var full_currency = matches[2] || '';
var interest = matches[3] || '';
// interest is defined as interest per year, per annum (pa)
var percentage = interest.match(/(-?\d+\.?\d+)/);
currencyCode = currencyCode.toUpperCase();
var currencyData = utils.arraySet(20, 0);
if (percentage) {
/*
* 20 byte layout of a interest bearing currency
*
* 01 __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __
* CURCODE- DATE------- RATE------------------- RESERVED---
*/
// byte 1 for type, use '1' to denote demurrage currency
currencyData[0] = 1;
// byte 2-4 for currency code
currencyData[1] = currencyCode.charCodeAt(0) & 0xff;
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 9-16 are for the interest
percentage = parseFloat(percentage[0]);
// 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 bytes = Float.toIEEE754Double(interestEfold);
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
} else {
currencyData[12] = currencyCode.charCodeAt(0) & 0xff;
currencyData[13] = currencyCode.charCodeAt(1) & 0xff;
currencyData[14] = currencyCode.charCodeAt(2) & 0xff;
}
this.parse_bytes(currencyData);
}
break;
}
return this;
};
Currency.prototype.parse_human = function (j) {
return this.parse_json(j);
};
/**
* 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;
}
this._native = false;
this._type = -1;
this._interest_start = NaN;
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 === '\u0000\u0000\u0000') {
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.fromIEEE754Double(bytes.slice(8, 16));
}
};
// XXX Probably not needed anymore?
/*
Currency.prototype.parse_bytes = function(byte_array) {
if (Array.isArray(byte_array) && byte_array.length === 20) {
var result;
// is it 0 everywhere except 12, 13, 14?
var isZeroExceptInStandardPositions = true;
for (var i=0; i<20; i++) {
isZeroExceptInStandardPositions = isZeroExceptInStandardPositions
&& (i===12 || i===13 || i===14 || byte_array[0]===0)
}
if (isZeroExceptInStandardPositions) {
var currencyCode = String.fromCharCode(byte_array[12])
+ String.fromCharCode(byte_array[13])
+ String.fromCharCode(byte_array[14]);
if (/^[A-Z0-9]{3}$/.test(currencyCode) && currencyCode !== 'XRP' ) {
this._value = currencyCode;
} else if (currencyCode === '\0\0\0') {
this._value = 0;
} else {
this._value = NaN;
}
} else {
// XXX Should support non-standard currency codes
this._value = NaN;
}
} else {
this._value = NaN;
}
return this;
};
*/
Currency.prototype.is_native = function () {
return this._native;
};
/**
* @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);
};
/**
*
* @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) {
if (!this.has_interest()) {
return 0;
}
// use one year as a default period
if (!referenceDate) {
referenceDate = this._interest_start + 3600 * 24 * 365;
}
if (referenceDate instanceof Date) {
referenceDate = utils.fromTimestamp(referenceDate.getTime());
}
// calculate interest by e-fold number
return Math.exp((referenceDate - this._interest_start) / this._interest_period);
};
Currency.prototype.get_interest_percentage_at = function (referenceDate, decimals) {
var interest = this.get_interest_at(referenceDate, decimals);
// convert to percentage
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;
};
// 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() {
// 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';
}
if (!opts) {
opts = {};
}
var currency;
var fullName = opts && opts.full_name ? ' - ' + opts.full_name : '';
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 : 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.
if (currency === Currency.HEX_ONE) {
currency = 1;
}
}
return currency;
};
Currency.prototype.to_human = function (opts) {
// to_human() will always print the human-readable currency code if available.
return this.to_json(opts);
};
Currency.prototype.get_iso = function () {
return this._iso_code;
};
exports.Currency = Currency;