From 9f76907f515b6a1d1d4e6ae05161409d416d0c0b Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Fri, 24 Jan 2014 05:24:46 -0800 Subject: [PATCH] Currency: Add support for complex currencies. (UInt160) This patch might regress the performance of the Currency class and by extension the Amount class. Since Amount is on a lot of hot paths in the client we should make sure this isn't a major problem. As for compatibility, this patch is a major change, but it should maintain the public interface very well, which the exception of some strange edge cases (e.g. Currency.from_json(1337)), which weren't well-defined before anyway. Any code that accesses _value directly (shame on you!) will need to be fixed. There aren't any such references in ripple-client or the rippled test suite, so I think we're looking pretty good. --- src/js/ripple/amount.js | 4 +- src/js/ripple/currency.js | 128 ++++++++++++++++++++----------- src/js/ripple/serializedtypes.js | 22 ++---- src/js/ripple/uint.js | 22 ++++++ test/currency-test.js | 2 +- test/serializedtypes-test.js | 10 +++ 6 files changed, 124 insertions(+), 64 deletions(-) diff --git a/src/js/ripple/amount.js b/src/js/ripple/amount.js index ce740ed4..145a5c2c 100644 --- a/src/js/ripple/amount.js +++ b/src/js/ripple/amount.js @@ -628,7 +628,7 @@ Amount.prototype.parse_json = function (j) { switch (typeof j) { case 'string': // .../.../... notation is not a wire format. But allowed for easier testing. - var m = j.match(/^([^/]+)\/(...)(?:\/(.+))?$/); + var m = j.match(/^([^/]+)\/([^/]+)(?:\/(.+))?$/); if (m) { this._currency = Currency.from_json(m[2]); @@ -655,7 +655,7 @@ Amount.prototype.parse_json = function (j) { j.copyTo(this); } else if (j.hasOwnProperty('value')) { // Parse the passed value to sanitize and copy it. - this._currency.parse_json(j.currency); // Never XRP. + this._currency.parse_json(j.currency, true); // Never XRP. if (typeof j.issuer === 'string') { this._issuer.parse_json(j.issuer); diff --git a/src/js/ripple/currency.js b/src/js/ripple/currency.js index eef1f042..4dcf51b0 100644 --- a/src/js/ripple/currency.js +++ b/src/js/ripple/currency.js @@ -1,10 +1,14 @@ +var extend = require('extend'); + +var UInt160 = require('./uint160').UInt160; +var utils = require('./utils'); + // // Currency support // -// XXX Internal form should be UInt160. -function Currency() { +var Currency = extend(function () { // Internal form: 0 = XRP. 3 letter-code. // XXX Internal should be 0 or hex with three letter annotation when valid. @@ -14,72 +18,63 @@ function Currency() { // XXX Should support hex, C++ doesn't currently allow it. this._value = NaN; -}; +}, UInt160); -// Given "USD" return the json. -Currency.json_rewrite = function (j) { - return Currency.from_json(j).to_json(); -}; +Currency.prototype = extend({}, UInt160.prototype); +Currency.prototype.constructor = Currency; -Currency.from_json = function (j) { - return j instanceof Currency ? j.clone() : new Currency().parse_json(j); -}; +Currency.HEX_CURRENCY_BAD = "0000000000000000000000005852500000000000"; -Currency.from_bytes = function (j) { - return j instanceof Currency ? j.clone() : new Currency().parse_bytes(j); -}; - -Currency.is_valid = function (j) { - return Currency.from_json(j).is_valid(); -}; - -Currency.prototype.clone = function() { - return this.copyTo(new Currency()); -}; - -// Returns copy. -Currency.prototype.copyTo = function (d) { - d._value = this._value; - return d; -}; - -Currency.prototype.equals = function (d) { - var equals = (typeof this._value !== 'string' && isNaN(this._value)) - || (typeof d._value !== 'string' && isNaN(d._value)); - return equals ? false: this._value === d._value; +Currency.from_json = function (j, shouldInterpretXrpAsIou) { + if (j instanceof this) { + return j.clone(); + } else { + return (new this()).parse_json(j, shouldInterpretXrpAsIou); + } }; // this._value = NaN on error. -Currency.prototype.parse_json = function (j) { - var result = NaN; +Currency.prototype.parse_json = function (j, shouldInterpretXrpAsIou) { + this._value = NaN; switch (typeof j) { case 'string': if (!j || /^(0|XRP)$/.test(j)) { - result = 0; + if (shouldInterpretXrpAsIou) { + this.parse_hex(Currency.HEX_CURRENCY_BAD); + } else { + this.parse_hex(Currency.HEX_ZERO); + } } else if (/^[a-zA-Z0-9]{3}$/.test(j)) { - result = j; + var currencyCode = j.toUpperCase(); + var currencyData = utils.arraySet(20, 0); + currencyData[12] = currencyCode.charCodeAt(0) & 0xff; + currencyData[13] = currencyCode.charCodeAt(1) & 0xff; + currencyData[14] = currencyCode.charCodeAt(2) & 0xff; + this.parse_bytes(currencyData); + } else { + this.parse_hex(j); } break; case 'number': if (!isNaN(j)) { - result = j; + this.parse_number(j); } break; case 'object': if (j instanceof Currency) { - result = j.copyTo({})._value; + this._value = j.copyTo({})._value; } break; } - this._value = result; - return this; }; +// XXX Probably not needed anymore? +/* Currency.prototype.parse_bytes = function (byte_array) { if (Array.isArray(byte_array) && byte_array.length === 20) { var result; @@ -110,21 +105,62 @@ Currency.prototype.parse_bytes = function (byte_array) { } return this; }; +*/ Currency.prototype.is_native = function () { - return !isNaN(this._value) && !this._value; + return !isNaN(this._value) && this.is_zero(); }; -Currency.prototype.is_valid = function () { - return typeof this._value === 'string' || !isNaN(this._value); -}; +// 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 this._value instanceof BigInteger && ...; +//}; Currency.prototype.to_json = function () { - return this._value ? this._value : "XRP"; + var bytes = this.to_bytes(); + + // is it 0 everywhere except 12, 13, 14? + var isZeroExceptInStandardPositions = true; + + if (!bytes) { + return "XRP"; + } + + for (var i=0; i<20; i++) { + isZeroExceptInStandardPositions = isZeroExceptInStandardPositions && (i===12 || i===13 || i===14 || bytes[i]===0); + } + + if (isZeroExceptInStandardPositions) { + var currencyCode = String.fromCharCode(bytes[12]) + + 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 + // behavior, so we should deprecate it and get rid of it as soon as + // possible. + if (currencyHex === Currency.HEX_ONE) { + return 1; + } + + return currencyHex; + } }; Currency.prototype.to_human = function () { - return this._value ? this._value : "XRP"; + return this.to_json(); }; exports.Currency = Currency; diff --git a/src/js/ripple/serializedtypes.js b/src/js/ripple/serializedtypes.js index 88da4027..c240b61d 100644 --- a/src/js/ripple/serializedtypes.js +++ b/src/js/ripple/serializedtypes.js @@ -280,22 +280,13 @@ STHash160.id = 17; // Internal var STCurrency = new SerializedType({ serialize: function (so, val, xrp_as_ascii) { - var currency = val.to_json().toUpperCase(); + var currencyData = val.to_bytes(); - if (!isCurrencyString(currency)) { + if (!currencyData) { throw new Error('Tried to serialize invalid/unimplemented currency type.'); } - if (currency === 'XRP' && !xrp_as_ascii) { - serialize_hex(so, UInt160.HEX_ZERO, true); - } else { - var currencyCode = currency.toUpperCase(); - var currencyData = utils.arraySet(20, 0); - currencyData[12] = currencyCode.charCodeAt(0) & 0xff; - currencyData[13] = currencyCode.charCodeAt(1) & 0xff; - currencyData[14] = currencyCode.charCodeAt(2) & 0xff; - so.append(currencyData); - } + so.append(currencyData); }, parse: function (so) { var bytes = so.read(20); @@ -484,8 +475,8 @@ var STPathSet = exports.PathSet = new SerializedType({ } if (entry.currency) { - var currency = Currency.from_json(entry.currency); - STCurrency.serialize(so, currency, entry.non_native); + var currency = Currency.from_json(entry.currency, entry.non_native); + STCurrency.serialize(so, currency); } if (entry.issuer) { @@ -544,7 +535,8 @@ var STPathSet = exports.PathSet = new SerializedType({ } if (tag_byte & this.typeIssuer) { //console.log('entry.issuer'); - entry.issuer = STHash160.parse(so); //should know to use Base58? + entry.issuer = STHash160.parse(so); + // Enable and set correct type of base-58 encoding entry.issuer.set_version(Base.VER_ACCOUNT_ID); //console.log('DONE WITH ISSUER!'); } diff --git a/src/js/ripple/uint.js b/src/js/ripple/uint.js index 486b0fc9..545d517b 100644 --- a/src/js/ripple/uint.js +++ b/src/js/ripple/uint.js @@ -75,6 +75,15 @@ UInt.from_bn = function (j) { } }; +// Return a new UInt from j. +UInt.from_number = function (j) { + if (j instanceof this) { + return j.clone(); + } else { + return (new this()).parse_number(j); + } +}; + UInt.is_valid = function (j) { return this.from_json(j).is_valid(); }; @@ -192,6 +201,19 @@ UInt.prototype.parse_bn = function (j) { return this; }; +UInt.prototype.parse_number = function (j) { + this._value = NaN; + + if ("number" === typeof j && + j === +j && + j > 0) { + // XXX Better, faster way to get BigInteger from JS int? + this._value = new BigInteger(""+j); + } + + return this; +}; + // Convert from internal form. UInt.prototype.to_bytes = function () { if (!(this._value instanceof BigInteger)) diff --git a/test/currency-test.js b/test/currency-test.js index 07cbf879..c8ccd550 100644 --- a/test/currency-test.js +++ b/test/currency-test.js @@ -19,8 +19,8 @@ describe('Currency', function() { }); it('from_json("XRP").to_json() == "XRP"', function() { var r = currency.from_json('XRP'); - assert.strictEqual(0, r._value); assert(r.is_valid()); + assert(r.is_native()); assert.strictEqual('XRP', r.to_json()); }); }); diff --git a/test/serializedtypes-test.js b/test/serializedtypes-test.js index 4f0551b9..fc17aa94 100644 --- a/test/serializedtypes-test.js +++ b/test/serializedtypes-test.js @@ -531,6 +531,16 @@ describe('Serialized types', function() { }); assert.strictEqual(so.to_hex(), 'D5438D7EA4C680000000000000000000000000005852500000000000E4FE687C90257D3D2D694C8531CDEECBE84F3367'); }); + // Test support for 20-byte hex raw currency codes + it('Serialize 15/015841551A748AD23FEFFFFFFFEA028000000000/1', function () { + var so = new SerializedObject(); + types.Amount.serialize(so, { + "value":"1000", + "currency":"015841551A748AD23FEFFFFFFFEA028000000000", + "issuer":"rM1oqKtfh1zgjdAgbFmaRm3btfGBX25xVo" + }); + assert.strictEqual(so.to_hex(), 'D5438D7EA4C68000015841551A748AD23FEFFFFFFFEA028000000000E4FE687C90257D3D2D694C8531CDEECBE84F3367'); + }); it('Parse 1 XRP', function () { var so = new SerializedObject('4000000000000001'); assert.strictEqual(types.Amount.parse(so).to_json(), '1');