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.
This commit is contained in:
Stefan Thomas
2014-01-24 05:24:46 -08:00
parent 4e67167394
commit 9f76907f51
6 changed files with 124 additions and 64 deletions

View File

@@ -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);

View File

@@ -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;

View File

@@ -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!');
}

View File

@@ -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))

View File

@@ -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());
});
});

View File

@@ -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');