Cleanup amount.js

This commit is contained in:
Chris Clark
2015-01-27 17:34:50 -08:00
parent 793523cbe9
commit d0fb291c4e
5 changed files with 208 additions and 233 deletions

View File

@@ -9,6 +9,19 @@ var Seed = require('./seed').Seed;
var Currency = require('./currency').Currency;
var BigNumber = require('./bignumber');
function isInteger(number) {
return parseInt(number) === number;
}
function ensureDecimalPoint(value) {
return isInteger(value) ? String(value) + '.0' : value;
}
function inverse(number) {
return (new BigNumber(number)).toPower(-1);
}
function Amount() {
// Json format:
// integer : XRP
@@ -26,20 +39,19 @@ var consts = {
xns_precision: 6,
// bi_ prefix refers to "big integer"
bi_5: new BigNumber('5'),
bi_7: new BigNumber('7'),
bi_10: new BigNumber('10'),
bi_1e14: new BigNumber(String(1e14)),
bi_1e16: new BigNumber(String(1e16)),
bi_1e17: new BigNumber(String(1e17)),
bi_1e32: new BigNumber('100000000000000000000000000000000'),
// TODO: we shouldn't expose our BigNumber library publicly
bi_5: new BigNumber(5),
bi_7: new BigNumber(7),
bi_10: new BigNumber(10),
bi_1e14: new BigNumber(1e14),
bi_1e16: new BigNumber(1e16),
bi_1e17: new BigNumber(1e17),
bi_1e32: new BigNumber(1e32),
bi_man_max_value: new BigNumber('9999999999999999'),
bi_man_min_value: new BigNumber('1000000000000000'),
bi_xns_max: new BigNumber('9000000000000000000'), // Json wire limit.
bi_xns_min: new BigNumber('-9000000000000000000'),// Json wire limit.
bi_xrp_max: new BigNumber('9000000000000'),
bi_xrp_min: new BigNumber('-9000000000000'),
bi_xns_unit: new BigNumber('1000000'),
bi_man_min_value: new BigNumber(1e15),
bi_xns_max: new BigNumber(1e17),
bi_xns_min: new BigNumber(-1e17),
bi_xns_unit: new BigNumber(1e6),
cMinOffset: -96,
cMaxOffset: 80,
@@ -51,18 +63,22 @@ var consts = {
min_value: '-1000000000000000e-96'
};
var MAX_XRP_VALUE = new BigNumber(1e11);
var MAX_IOU_VALUE = new BigNumber(consts.max_value);
var MIN_IOU_VALUE = (new BigNumber(consts.min_value)).abs();
// Add constants to Amount class
extend(Amount, consts);
// DEPRECATED: Use Amount instead, e.g. Amount.currency_xns
exports.consts = consts;
// Given '100/USD/mtgox' return the a string with mtgox remapped.
// Given '100/USD/ISSUER' return the a string with ISSUER remapped.
Amount.text_full_rewrite = function(j) {
return Amount.from_json(j).to_text_full();
};
// Given '100/USD/mtgox' return the json.
// Given '100/USD/ISSUER' return the json.
Amount.json_rewrite = function(j) {
return Amount.from_json(j).to_json();
};
@@ -176,13 +192,7 @@ Amount.prototype.ratio_human = function(denominator, opts) {
opts = extend({ }, opts);
var numerator = this;
if (typeof denominator === 'number' && parseInt(denominator, 10) === denominator) {
// Special handling of integer arguments
denominator = Amount.from_json(String(denominator) + '.0');
} else {
denominator = Amount.from_json(denominator);
}
denominator = Amount.from_json(ensureDecimalPoint(denominator));
// If either operand is NaN, the result is NaN.
if (!numerator.is_valid() || !denominator.is_valid()) {
@@ -242,12 +252,7 @@ Amount.prototype.ratio_human = function(denominator, opts) {
Amount.prototype.product_human = function(factor, opts) {
opts = opts || {};
if (typeof factor === 'number' && parseInt(factor, 10) === factor) {
// Special handling of integer arguments
factor = Amount.from_json(String(factor) + '.0');
} else {
factor = Amount.from_json(factor);
}
factor = Amount.from_json(ensureDecimalPoint(factor));
// If either operand is NaN, the result is NaN.
if (!this.is_valid() || !factor.is_valid()) {
@@ -281,7 +286,7 @@ Amount.prototype.product_human = function(factor, opts) {
* @private
*/
Amount.prototype._invert = function() {
this._set_value((new BigNumber(1)).dividedBy(this._value));
this._set_value(inverse(this._value));
return this;
};
@@ -342,13 +347,17 @@ Amount.prototype._check_limits = function() {
if (this._value.isNaN() || this._value.isZero()) {
return this;
}
if (!this._is_native) {
var absval = this._value.absoluteValue();
if (absval.lessThan((new BigNumber(Amount.min_value)).absoluteValue())) {
throw new Error('Exceeding min value of ' + Amount.min_value);
var absval = this._value.absoluteValue();
if (this._is_native) {
if (absval.greaterThan(MAX_XRP_VALUE)) {
throw new Error('Exceeding max value of ' + MAX_XRP_VALUE.toString());
}
if (absval.greaterThan(new BigNumber(Amount.max_value))) {
throw new Error('Exceeding max value of ' + Amount.max_value);
} else {
if (absval.lessThan(MIN_IOU_VALUE)) {
throw new Error('Exceeding min value of ' + MIN_IOU_VALUE.toString());
}
if (absval.greaterThan(MAX_IOU_VALUE)) {
throw new Error('Exceeding max value of ' + MAX_IOU_VALUE.toString());
}
}
return this;
@@ -387,7 +396,7 @@ Amount.prototype.currency = function() {
};
Amount.prototype.equals = function(d, ignore_issuer) {
if (typeof d === 'string') {
if (!(d instanceof Amount)) {
return this.equals(Amount.from_json(d));
}
@@ -506,19 +515,7 @@ Amount.prototype.parse_human = function(j, opts) {
// Apply interest/demurrage
if (opts.reference_date && this._currency.has_interest()) {
var interest = this._currency.get_interest_at(opts.reference_date);
// XXX Because the Amount parsing routines don't support some of the things
// that JavaScript can output when casting a float to a string, the
// following call sometimes does not produce a valid Amount.
//
// The correct way to solve this is probably to switch to a proper
// BigDecimal for our internal representation and then use that across
// the board instead of instantiating these dummy Amount objects.
var interestTempAmount = Amount.from_json(''+interest+'/1/1');
if (interestTempAmount.is_valid()) {
this._set_value(this.divide(interestTempAmount)._value);
}
this._set_value(this._value.dividedBy(interest.toString()));
}
return this;
@@ -578,40 +575,48 @@ Amount.prototype.parse_quality = function(quality, counterCurrency, counterIssue
this._issuer = UInt160.from_json(counterIssuer);
this._is_native = this._currency.is_native();
var power = 0;
if (this._is_native) {
if (opts.inverse) {
power += 1;
} else {
power -= 1;
if (this._is_native && baseCurrency.is_native()) {
throw new Error('XRP/XRP quality is not allowed');
}
/*
The quality, as stored in the last 64 bits of a directory index, is stored as
the quotient of TakerPays/TakerGets.
When `opts.inverse` is true we are looking at a quality used for determining a
`bid` price and it must first be inverted, before our declared base/counter
currencies are in line with the price.
For example:
quality as stored : 5 USD / 3000000 drops
inverted : 3000000 drops / 5 USD
*/
var adjusted = opts.inverse ? inverse(value) : value;
var nativeAdjusted = adjusted;
if (!opts.xrp_as_drops) {
// `In a currency exchange, the exchange rate is quoted as the units of the
// counter currency in terms of a single unit of a base currency`. A
// quality is how much taker must `pay` to get ONE `gets` unit thus:
// pays ~= counterCurrency
// gets ~= baseCurrency.
if (this._is_native) {
// pay:$price drops get:1 X
// pay:($price / 1,000,000) XRP get:1 X
nativeAdjusted = adjusted.div(Amount.bi_xns_unit);
} else if (baseCurrency.is_valid() && baseCurrency.is_native()) {
// pay:$price X get:1 drop
// pay:($price * 1,000,000) X get:1 XRP
nativeAdjusted = adjusted.times(Amount.bi_xns_unit);
}
}
// Correct offset if xrp_as_drops option is not set and base currency is XRP
if (!opts.xrp_as_drops &&
baseCurrency.is_valid() &&
baseCurrency.is_native()) {
if (opts.inverse) {
power -= 1;
} else {
power += 1;
}
}
var one = new BigNumber(1);
var adjusted = value.times(Amount.bi_xns_unit.toPower(power));
var newValue = opts.inverse ? one.dividedBy(adjusted) : adjusted;
this._set_value(newValue);
this._set_value(nativeAdjusted);
if (opts.reference_date && baseCurrency.is_valid() && baseCurrency.has_interest()) {
var interest = baseCurrency.get_interest_at(opts.reference_date);
// XXX If we had better math utilities, we wouldn't need this hack.
var interestTempAmount = Amount.from_json(''+interest+'/1/1');
if (interestTempAmount.is_valid()) {
this._set_value(this.divide(interestTempAmount)._value);
}
this._set_value(this._value.dividedBy(interest.toString()));
}
return this;
@@ -690,10 +695,6 @@ Amount.prototype.parse_native = function(j) {
} else {
this._set_value(value.dividedBy(Amount.bi_xns_unit));
}
// TODO: move this overflow check to canonicalize
if (this._value.abs().greaterThan(Amount.bi_xrp_max)) {
this._set_value(new BigNumber(NaN));
}
} else {
this._set_value(new BigNumber(NaN));
}
@@ -717,36 +718,26 @@ Amount.prototype.set_currency = function(c) {
Amount.prototype.set_issuer = function(issuer) {
if (issuer instanceof UInt160) {
this._issuer = issuer;
this._issuer = issuer;
} else {
this._issuer = UInt160.from_json(issuer);
this._issuer = UInt160.from_json(issuer);
}
return this;
};
Amount.prototype.to_number = function(allow_nan) {
var s = this.to_text(allow_nan);
return typeof s === 'string' ? Number(s) : s;
Amount.prototype.to_number = function() {
return Number(this.to_text());
};
// Convert only value to JSON wire format.
Amount.prototype.to_text = function(allow_nan) {
if (this._is_native && this._value.abs().greaterThan(Amount.bi_xrp_max)) {
return '0';
}
if (this._value.isNaN() && !allow_nan) {
return '0';
} else if (this._value.isNaN()) {
return NaN; // TODO: why does to_text return NaN? return 'NaN'?
Amount.prototype.to_text = function() {
if (!this.is_valid()) {
return 'NaN';
}
if (this._is_native) {
if (this.is_valid() && this._value.lessThanOrEqualTo(Amount.bi_xns_max)){
return this._value.times(Amount.bi_xns_unit).toString();
} else {
return NaN; // TODO: why does to_text return NaN? return 'NaN'?
}
return this._value.times(Amount.bi_xns_unit).toString();
}
// not native
@@ -784,24 +775,11 @@ Amount.prototype.to_text = function(allow_nan) {
* @return {Amount} The amount with interest applied.
*/
Amount.prototype.applyInterest = function(referenceDate) {
if (this._currency.has_interest()) {
var interest = this._currency.get_interest_at(referenceDate);
// XXX Because the Amount parsing routines don't support some of the things
// that JavaScript can output when casting a float to a string, the
// following call sometimes does not produce a valid Amount.
//
// The correct way to solve this is probably to switch to a proper
// BigDecimal for our internal representation and then use that across
// the board instead of instantiating these dummy Amount objects.
var interestTempAmount = Amount.from_json(String(interest) + '/1/1');
if (interestTempAmount.is_valid()) {
return this.multiply(interestTempAmount);
}
} else {
if (!this._currency.has_interest()) {
return this;
}
var interest = this._currency.get_interest_at(referenceDate);
return this._copy(this._value.times(interest.toString()));
};
/**
@@ -833,16 +811,6 @@ Amount.prototype.to_human = function(opts) {
return 'NaN';
}
// Default options
if (typeof opts.signed === 'undefined') {
opts.signed = true;
}
if (typeof opts.group_sep === 'undefined') {
opts.group_sep = true;
}
opts.group_width = opts.group_width || 3;
// Apply demurrage/interest
var ref = this;
if (opts.reference_date) {
@@ -914,19 +882,15 @@ Amount.prototype.to_human = function(opts) {
}
}
if (opts.group_sep) {
if (typeof opts.group_sep !== 'string') {
opts.group_sep = ',';
}
int_part = utils.chunkString(int_part, opts.group_width, true).join(opts.group_sep);
if (opts.group_sep !== false) {
var sep = (typeof opts.group_sep === 'string') ? opts.group_sep : ',';
var groups = utils.chunkString(int_part, opts.group_width || 3, true);
int_part = groups.join(sep);
}
var formatted = '';
if (opts.signed && isNegative) {
if (typeof opts.signed !== 'string') {
opts.signed = '-';
}
formatted += opts.signed;
if(isNegative && opts.signed !== false) {
formatted += '-';
}
formatted += int_part.length ? int_part : '0';
@@ -937,30 +901,27 @@ Amount.prototype.to_human = function(opts) {
Amount.prototype.to_human_full = function(opts) {
opts = opts || {};
var a = this.to_human(opts);
var c = this._currency.to_human();
var i = this._issuer.to_json(opts);
var o = this.is_native() ? (o = a + '/' + c) : (o = a + '/' + c + '/' + i);
return o;
var value = this.to_human(opts);
var currency = this._currency.to_human();
var issuer = this._issuer.to_json(opts);
var base = value + '/' + currency;
return this.is_native() ? base : (base + '/' + issuer);
};
Amount.prototype.to_json = function() {
var result;
if (this._is_native) {
result = this.to_text();
return this.to_text();
} else {
var amount_json = {
value : this.to_text(),
currency : this._currency.has_interest() ? this._currency.to_hex() : this._currency.to_json()
currency : this._currency.has_interest() ?
this._currency.to_hex() : this._currency.to_json()
};
if (this._issuer.is_valid()) {
amount_json.issuer = this._issuer.to_json();
}
result = amount_json;
return amount_json;
}
return result;
};
Amount.prototype.to_text_full = function(opts) {
@@ -969,7 +930,8 @@ Amount.prototype.to_text_full = function(opts) {
}
return this._is_native
? this.to_human() + '/XRP'
: this.to_text() + '/' + this._currency.to_json() + '/' + this._issuer.to_json(opts);
: this.to_text() + '/' + this._currency.to_json()
+ '/' + this._issuer.to_json(opts);
};
// For debugging.
@@ -977,34 +939,31 @@ Amount.prototype.not_equals_why = function(d, ignore_issuer) {
if (typeof d === 'string') {
return this.not_equals_why(Amount.from_json(d));
}
if (!(d instanceof Amount)) {
return 'Not an Amount';
}
var result = false;
if (!this.is_valid() || !d.is_valid()) {
result = 'Invalid amount.';
} else if (this._is_native !== d._is_native) {
result = 'Native mismatch.';
} else {
var type = this._is_native ? 'XRP' : 'Non-XRP';
if (!this._value.isZero() && this._value.negated().equals(d._value)) {
result = type + ' sign differs.';
} else if (!this._value.equals(d._value)) {
result = type + ' value differs.';
} else if (!this._is_native) {
if (!this._currency.equals(d._currency)) {
result = 'Non-XRP currency differs.';
} else if (!ignore_issuer && !this._issuer.equals(d._issuer)) {
result = 'Non-XRP issuer differs: ' + d._issuer.to_json() + '/' + this._issuer.to_json();
}
}
return 'Invalid amount.';
}
if (this._is_native !== d._is_native) {
return 'Native mismatch.';
}
return result;
var type = this._is_native ? 'XRP' : 'Non-XRP';
if (!this._value.isZero() && this._value.negated().equals(d._value)) {
return type + ' sign differs.';
}
if (!this._value.equals(d._value)) {
return type + ' value differs.';
}
if (!this._is_native) {
if (!this._currency.equals(d._currency)) {
return 'Non-XRP currency differs.';
}
if (!ignore_issuer && !this._issuer.equals(d._issuer)) {
return 'Non-XRP issuer differs: ' + d._issuer.to_json() + '/' + this._issuer.to_json();
}
}
};
exports.Amount = Amount;

View File

@@ -298,7 +298,11 @@ var STAmount = exports.Amount = new SerializedType({
var valueBytes = utils.arraySet(8, 0);
if (amount.is_native()) {
var valueHex = value.absoluteValue().toString(16);
var valueHex = value.abs().toString(16);
if (value.abs().greaterThan(Amount.bi_xns_max)) {
throw new Error('Value out of bounds');
}
// Enforce correct length (64 bits)
if (valueHex.length > 16) {

View File

@@ -336,9 +336,9 @@ describe('Amount', function() {
});
describe('Amount parsing', function() {
it('Parse invalid string', function() {
assert.strictEqual(Amount.from_json('x').to_text(), '0');
assert.strictEqual(typeof Amount.from_json('x').to_text(true), 'number');
assert(isNaN(Amount.from_json('x').to_text(true)));
assert.strictEqual(Amount.from_json('x').to_text(), 'NaN');
assert.strictEqual(typeof Amount.from_json('x').to_text(), 'string');
assert(isNaN(Amount.from_json('x').to_text()));
});
it('parse dem', function() {
assert.strictEqual(Amount.from_json('10/015841551A748AD2C1F76FF6ECB0CCCD00000000/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').to_text_full(), '10/XAU (-0.5%pa)/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh');
@@ -1085,6 +1085,11 @@ describe('Amount', function() {
});
describe('from_quality', function() {
it('XRP/XRP', function () {
assert.throws(function() {
Amount.from_quality('7B73A610A009249B0CC0D4311E8BA7927B5A34D86634581C5F0FF9FF678E1000', 'XRP', NaN, {base_currency: 'XRP'}).to_text_full()
});
});
it('BTC/XRP', function () {
assert.strictEqual(Amount.from_quality('7B73A610A009249B0CC0D4311E8BA7927B5A34D86634581C5F0FF9FF678E1000', 'XRP', NaN, {base_currency: 'BTC'}).to_text_full(), '44,970/XRP');
});
@@ -1188,11 +1193,11 @@ describe('Amount', function() {
describe('amount limits', function() {
it ('max JSON wire limite', function() {
assert.strictEqual(Amount.bi_xns_max.toString(), '9000000000000000000');
assert.strictEqual(Amount.bi_xns_max.toString(), '100000000000000000');
});
it ('max JSON wire limite', function() {
assert.strictEqual(Amount.bi_xns_min.toString(), '-9000000000000000000');
assert.strictEqual(Amount.bi_xns_min.toString(), '-100000000000000000');
});
it('max mantissa value', function() {
@@ -1204,23 +1209,25 @@ describe('Amount', function() {
});
it ('from_json minimum XRP', function() {
var amt = Amount.from_json('-9000000000000000000');
assert.strictEqual(amt.to_json(), '-9000000000000000000');
var amt = Amount.from_json('-100000000000000000');
assert.strictEqual(amt.to_json(), '-100000000000000000');
});
it ('from_json maximum XRP', function() {
var amt = Amount.from_json('-9000000000000000000');
assert.strictEqual(amt.to_json(), '-9000000000000000000');
var amt = Amount.from_json('100000000000000000');
assert.strictEqual(amt.to_json(), '100000000000000000');
});
it ('from_json less than minimum XRP', function() {
var amt = Amount.from_json('-9000000000000000001');
assert.strictEqual(amt.to_json(), '0');
assert.throws(function() {
Amount.from_json('-100000000000000001');
});
});
it ('from_json more than maximum XRP', function() {
var amt = Amount.from_json('9000000000000000001');
assert.strictEqual(amt.to_json(), '0');
assert.throws(function() {
Amount.from_json('100000000000000001');
});
});
it ('from_json minimum IOU', function() {

View File

@@ -858,7 +858,7 @@ describe('Remote', function() {
it('Get reserve', function() {
remote._connected = true;
remote._servers[0]._connected = true;
assert.strictEqual(remote.reserve(1).to_json(), '0');
assert.strictEqual(remote.reserve(1).to_json(), 'NaN');
remote._servers = [ ];
assert.throws(function() {
remote.reserve(10).to_json();

View File

@@ -509,8 +509,10 @@ describe('Serialized types', function() {
});
it('Serialize 1161981756646125568 XRP', function () {
var so = new SerializedObject();
types.Amount.serialize(so, '1161981756646125696');
assert.strictEqual(so.to_hex(), '5020304050607080');
assert.throws(function() {
var amt = Amount.from_json('1161981756646125696');
types.Amount.serialize(so, amt);
});
});
it('Serialize 1/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', function () {
var so = new SerializedObject();
@@ -570,8 +572,11 @@ describe('Serialized types', function() {
assert.strictEqual(types.Amount.parse(so).to_json(), '270544960');
});
it('Parse 1161981756646125568 XRP', function () {
var so = new SerializedObject('5020304050607080');
assert.strictEqual(types.Amount.parse(so).to_json(), '1161981756646125696');
assert.throws(function() {
// hex(1161981756646125568) = 1020304050607000
var so = new SerializedObject('1020304050607000');
types.Amount.parse(so).to_json();
})
});
it('Parse 1/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', function () {
var so = new SerializedObject('D4838D7EA4C680000000000000000000000000005553440000000000B5F762798A53D543A014CAF8B297CFF8F2F937E8');
@@ -687,25 +692,25 @@ describe('Serialized types', function() {
}]
];
var result_json = [
[{
var result_json = [
[{
account: 'rrrrrrrrrrrrrrrrrrrrNxV3Xza',
currency: 'USD',
issuer: 'rrrrrrrrrrrrrrrrrrrpYnYCNYf',
type: 49,
type_hex: '0000000000000031'
type_hex: '0000000000000031'
}],
[{
currency: 'XRP',
type: 16,
type_hex: '0000000000000010'
}, {
[{
currency: 'XRP',
type: 16,
type_hex: '0000000000000010'
}, {
account: 'rrrrrrrrrrrrrrrrrrrpvQsW3V3',
currency: 'EUR',
issuer: 'rrrrrrrrrrrrrrrrrrrdHRtqg2',
type: 49,
type_hex: '0000000000000031'
}]
type_hex: '0000000000000031'
}]
];
var so = new SerializedObject();
@@ -734,26 +739,26 @@ describe('Serialized types', function() {
}]
];
var result_json = [
[{
var result_json = [
[{
account: 'rrrrrrrrrrrrrrrrrrrrNxV3Xza',
currency: 'USD',
issuer: 'rrrrrrrrrrrrrrrrrrrpYnYCNYf',
type: 49,
type_hex: '0000000000000031'
type_hex: '0000000000000031'
}],
[{
[{
currency: 'XRP',
non_native: true,
type: 16,
type_hex: '0000000000000010'
}, {
type_hex: '0000000000000010'
}, {
account: 'rrrrrrrrrrrrrrrrrrrpvQsW3V3',
currency: 'EUR',
issuer: 'rrrrrrrrrrrrrrrrrrrdHRtqg2',
type: 49,
type_hex: '0000000000000031'
}]
type_hex: '0000000000000031'
}]
];
var so = new SerializedObject();
@@ -821,78 +826,78 @@ describe('Serialized types', function() {
}]
];
var result_json = [
[{
var result_json = [
[{
account: 'r9hEDb4xBGRfBCcX3E4FirDWQBAYtpxC8K',
currency: 'BTC',
issuer: 'r9hEDb4xBGRfBCcX3E4FirDWQBAYtpxC8K',
type: 49,
type_hex: '0000000000000031'
}, {
type_hex: '0000000000000031'
}, {
account: 'rM1oqKtfh1zgjdAgbFmaRm3btfGBX25xVo',
currency: 'BTC',
issuer: 'rM1oqKtfh1zgjdAgbFmaRm3btfGBX25xVo',
type: 49,
type_hex: '0000000000000031'
}, {
type_hex: '0000000000000031'
}, {
account: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B',
currency: 'BTC',
issuer: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B',
type: 49,
type_hex: '0000000000000031'
}, {
type_hex: '0000000000000031'
}, {
currency: 'USD',
issuer: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B',
type: 48,
type_hex: '0000000000000030'
type_hex: '0000000000000030'
}],
[{
[{
account: 'r9hEDb4xBGRfBCcX3E4FirDWQBAYtpxC8K',
currency: 'BTC',
issuer: 'r9hEDb4xBGRfBCcX3E4FirDWQBAYtpxC8K',
type: 49,
type_hex: '0000000000000031'
}, {
type_hex: '0000000000000031'
}, {
account: 'rM1oqKtfh1zgjdAgbFmaRm3btfGBX25xVo',
currency: 'BTC',
issuer: 'rM1oqKtfh1zgjdAgbFmaRm3btfGBX25xVo',
type: 49,
type_hex: '0000000000000031'
}, {
type_hex: '0000000000000031'
}, {
account: 'rpvfJ4mR6QQAeogpXEKnuyGBx8mYCSnYZi',
currency: 'BTC',
issuer: 'rpvfJ4mR6QQAeogpXEKnuyGBx8mYCSnYZi',
type: 49,
type_hex: '0000000000000031'
}, {
type_hex: '0000000000000031'
}, {
currency: 'USD',
issuer: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B',
type: 48,
type_hex: '0000000000000030'
type_hex: '0000000000000030'
}],
[{
[{
account: 'r9hEDb4xBGRfBCcX3E4FirDWQBAYtpxC8K',
currency: 'BTC',
issuer: 'r9hEDb4xBGRfBCcX3E4FirDWQBAYtpxC8K',
type: 49,
type_hex: '0000000000000031'
}, {
type_hex: '0000000000000031'
}, {
account: 'r3AWbdp2jQLXLywJypdoNwVSvr81xs3uhn',
currency: 'BTC',
issuer: 'r3AWbdp2jQLXLywJypdoNwVSvr81xs3uhn',
type: 49,
type_hex: '0000000000000031'
}, {
type_hex: '0000000000000031'
}, {
currency: 'XRP',
non_native: true,
type: 16,
type_hex: '0000000000000010'
}, {
type_hex: '0000000000000010'
}, {
currency: 'USD',
issuer: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B',
type: 48,
type_hex: '0000000000000030'
}]
type_hex: '0000000000000030'
}]
];
var so = new SerializedObject();