UNPKG

ripplelib

Version:

A JavaScript API for interacting with Ripple in Node.js and the browser

379 lines (310 loc) 12.4 kB
'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;