Merge pull request #133 from ripple/develop

Track unfunded orders in the orderbook
This commit is contained in:
Geert Weening
2014-08-13 21:50:27 +02:00
5 changed files with 2312 additions and 459 deletions

View File

@@ -160,157 +160,64 @@ Amount.prototype.add = function(v) {
return result; return result;
}; };
/** // Result in terms of this currency and issuer.
* Turn this amount into its inverse. Amount.prototype.subtract = function(v) {
* // Correctness over speed, less code has less bugs, reuse add code.
* @private return this.add(Amount.from_json(v).negate());
*/
Amount.prototype._invert = function() {
this._value = consts.bi_1e32.divide(this._value);
this._offset = -32 - this._offset;
this.canonicalize();
return this;
}; };
/** // Result in terms of this' currency and issuer.
* Return the inverse of this amount. // XXX Diverges from cpp.
* Amount.prototype.multiply = function(v) {
* @return {Amount} New Amount object with same currency and issuer, but the
* inverse of the value.
*/
Amount.prototype.invert = function() {
return this.copy()._invert();
};
Amount.prototype.canonicalize = function() {
if (!(this._value instanceof BigInteger)) {
// NaN.
// nothing
} else if (this._is_native) {
// Native.
if (this._value.equals(BigInteger.ZERO)) {
this._offset = 0;
this._is_negative = false;
} else {
// Normalize _offset to 0.
while (this._offset < 0) {
this._value = this._value.divide(consts.bi_10);
this._offset += 1;
}
while (this._offset > 0) {
this._value = this._value.multiply(consts.bi_10);
this._offset -= 1;
}
}
// XXX Make sure not bigger than supported. Throw if so.
} else if (this.is_zero()) {
this._offset = -100;
this._is_negative = false;
} else {
// Normalize mantissa to valid range.
while (this._value.compareTo(consts.bi_man_min_value) < 0) {
this._value = this._value.multiply(consts.bi_10);
this._offset -= 1;
}
while (this._value.compareTo(consts.bi_man_max_value) > 0) {
this._value = this._value.divide(consts.bi_10);
this._offset += 1;
}
}
return this;
};
Amount.prototype.clone = function(negate) {
return this.copyTo(new Amount(), negate);
};
Amount.prototype.compareTo = function(v) {
var result; var result;
if (!this.is_comparable(v)) { v = Amount.from_json(v);
result = Amount.NaN();
} else if (this._is_negative !== v._is_negative) { if (this.is_zero()) {
// Different sign. result = this;
result = this._is_negative ? -1 : 1; } else if (v.is_zero()) {
} else if (this._value.equals(BigInteger.ZERO)) { result = this.clone();
// Same sign: positive. result._value = BigInteger.ZERO;
result = v._value.equals(BigInteger.ZERO) ? 0 : -1;
} else if (v._value.equals(BigInteger.ZERO)) {
// Same sign: positive.
result = 1;
} else if (!this._is_native && this._offset > v._offset) {
result = this._is_negative ? -1 : 1;
} else if (!this._is_native && this._offset < v._offset) {
result = this._is_negative ? 1 : -1;
} else { } else {
result = this._value.compareTo(v._value); var v1 = this._value;
if (result > 0) { var o1 = this._offset;
result = this._is_negative ? -1 : 1; var v2 = v._value;
} else if (result < 0) { var o2 = v._offset;
result = this._is_negative ? 1 : -1;
if (this.is_native()) {
while (v1.compareTo(consts.bi_man_min_value) < 0) {
v1 = v1.multiply(consts.bi_10);
o1 -= 1;
}
} }
if (v.is_native()) {
while (v2.compareTo(consts.bi_man_min_value) < 0) {
v2 = v2.multiply(consts.bi_10);
o2 -= 1;
}
}
result = new Amount();
result._offset = o1 + o2 + 14;
result._value = v1.multiply(v2).divide(consts.bi_1e14).add(consts.bi_7);
result._is_native = this._is_native;
result._is_negative = this._is_negative !== v._is_negative;
result._currency = this._currency;
result._issuer = this._issuer;
result.canonicalize();
} }
return result; return result;
}; };
// Make d a copy of this. Returns d.
// Modification of objects internally refered to is not allowed.
Amount.prototype.copyTo = function(d, negate) {
if (typeof this._value === 'object') {
this._value.copyTo(d._value);
} else {
d._value = this._value;
}
d._offset = this._offset;
d._is_native = this._is_native;
d._is_negative = negate
? !this._is_negative // Negating.
: this._is_negative; // Just copying.
d._currency = this._currency;
d._issuer = this._issuer;
// Prevent negative zero
if (d.is_zero()) {
d._is_negative = false;
}
return d;
};
Amount.prototype.currency = function() {
return this._currency;
};
Amount.prototype.equals = function(d, ignore_issuer) {
if (typeof d === 'string') {
return this.equals(Amount.from_json(d));
}
var result = true;
result = !((!this.is_valid() || !d.is_valid())
|| (this._is_native !== d._is_native)
|| (!this._value.equals(d._value) || this._offset !== d._offset)
|| (this._is_negative !== d._is_negative)
|| (!this._is_native && (!this._currency.equals(d._currency) || !ignore_issuer && !this._issuer.equals(d._issuer))));
return result;
};
// Result in terms of this' currency and issuer. // Result in terms of this' currency and issuer.
Amount.prototype.divide = function(d) { Amount.prototype.divide = function(d) {
var result; var result;
d = Amount.from_json(d);
if (d.is_zero()) { if (d.is_zero()) {
throw new Error('divide by zero'); throw new Error('divide by zero');
} }
@@ -359,8 +266,6 @@ Amount.prototype.divide = function(d) {
}; };
/** /**
* Calculate a ratio between two amounts.
*
* This function calculates a ratio - such as a price - between two Amount * This function calculates a ratio - such as a price - between two Amount
* objects. * objects.
* *
@@ -382,14 +287,15 @@ Amount.prototype.divide = function(d) {
Amount.prototype.ratio_human = function(denominator, opts) { Amount.prototype.ratio_human = function(denominator, opts) {
opts = extend({ }, opts); opts = extend({ }, opts);
var numerator = this;
if (typeof denominator === 'number' && parseInt(denominator, 10) === denominator) { if (typeof denominator === 'number' && parseInt(denominator, 10) === denominator) {
// Special handling of integer arguments // Special handling of integer arguments
denominator = Amount.from_json('' + denominator + '.0'); denominator = Amount.from_json(String(denominator) + '.0');
} else { } else {
denominator = Amount.from_json(denominator); denominator = Amount.from_json(denominator);
} }
var numerator = this;
denominator = Amount.from_json(denominator); denominator = Amount.from_json(denominator);
// If either operand is NaN, the result is NaN. // If either operand is NaN, the result is NaN.
@@ -397,6 +303,10 @@ Amount.prototype.ratio_human = function(denominator, opts) {
return Amount.NaN(); return Amount.NaN();
} }
if (denominator.is_zero()) {
return Amount.NaN();
}
// Apply interest/demurrage // Apply interest/demurrage
// //
// We only need to apply it to the second factor, because the currency unit of // We only need to apply it to the second factor, because the currency unit of
@@ -482,6 +392,155 @@ Amount.prototype.product_human = function(factor, opts) {
return product; return product;
}; };
/**
* Turn this amount into its inverse.
*
* @private
*/
Amount.prototype._invert = function() {
this._value = consts.bi_1e32.divide(this._value);
this._offset = -32 - this._offset;
this.canonicalize();
return this;
};
/**
* Return the inverse of this amount.
*
* @return {Amount} New Amount object with same currency and issuer, but the
* inverse of the value.
*/
Amount.prototype.invert = function() {
return this.copy()._invert();
};
Amount.prototype.canonicalize = function() {
if (!(this._value instanceof BigInteger)) {
// NaN.
// nothing
} else if (this._is_native) {
// Native.
if (this._value.equals(BigInteger.ZERO)) {
this._offset = 0;
this._is_negative = false;
} else {
// Normalize _offset to 0.
while (this._offset < 0) {
this._value = this._value.divide(consts.bi_10);
this._offset += 1;
}
while (this._offset > 0) {
this._value = this._value.multiply(consts.bi_10);
this._offset -= 1;
}
}
// XXX Make sure not bigger than supported. Throw if so.
} else if (this.is_zero()) {
this._offset = -100;
this._is_negative = false;
} else {
// Normalize mantissa to valid range.
while (this._value.compareTo(consts.bi_man_min_value) < 0) {
this._value = this._value.multiply(consts.bi_10);
this._offset -= 1;
}
while (this._value.compareTo(consts.bi_man_max_value) > 0) {
this._value = this._value.divide(consts.bi_10);
this._offset += 1;
}
}
return this;
};
Amount.prototype.clone = function(negate) {
return this.copyTo(new Amount(), negate);
};
Amount.prototype.compareTo = function(v) {
var result;
v = Amount.from_json(v);
if (!this.is_comparable(v)) {
result = Amount.NaN();
} else if (this._is_negative !== v._is_negative) {
// Different sign.
result = this._is_negative ? -1 : 1;
} else if (this._value.equals(BigInteger.ZERO)) {
// Same sign: positive.
result = v._value.equals(BigInteger.ZERO) ? 0 : -1;
} else if (v._value.equals(BigInteger.ZERO)) {
// Same sign: positive.
result = 1;
} else if (!this._is_native && this._offset > v._offset) {
result = this._is_negative ? -1 : 1;
} else if (!this._is_native && this._offset < v._offset) {
result = this._is_negative ? 1 : -1;
} else {
result = this._value.compareTo(v._value);
if (result > 0) {
result = this._is_negative ? -1 : 1;
} else if (result < 0) {
result = this._is_negative ? 1 : -1;
}
}
return result;
};
// Make d a copy of this. Returns d.
// Modification of objects internally refered to is not allowed.
Amount.prototype.copyTo = function(d, negate) {
if (typeof this._value === 'object') {
this._value.copyTo(d._value);
} else {
d._value = this._value;
}
d._offset = this._offset;
d._is_native = this._is_native;
d._is_negative = negate
? !this._is_negative // Negating.
: this._is_negative; // Just copying.
d._currency = this._currency;
d._issuer = this._issuer;
// Prevent negative zero
if (d.is_zero()) {
d._is_negative = false;
}
return d;
};
Amount.prototype.currency = function() {
return this._currency;
};
Amount.prototype.equals = function(d, ignore_issuer) {
if (typeof d === 'string') {
return this.equals(Amount.from_json(d));
}
var result = true;
result = !((!this.is_valid() || !d.is_valid())
|| (this._is_native !== d._is_native)
|| (!this._value.equals(d._value) || this._offset !== d._offset)
|| (this._is_negative !== d._is_negative)
|| (!this._is_native && (!this._currency.equals(d._currency) || !ignore_issuer && !this._issuer.equals(d._issuer))));
return result;
};
// True if Amounts are valid and both native or non-native. // True if Amounts are valid and both native or non-native.
Amount.prototype.is_comparable = function(v) { Amount.prototype.is_comparable = function(v) {
return this._value instanceof BigInteger return this._value instanceof BigInteger
@@ -520,50 +579,6 @@ Amount.prototype.issuer = function() {
return this._issuer; return this._issuer;
}; };
// Result in terms of this' currency and issuer.
// XXX Diverges from cpp.
Amount.prototype.multiply = function(v) {
var result;
if (this.is_zero()) {
result = this;
} else if (v.is_zero()) {
result = this.clone();
result._value = BigInteger.ZERO;
} else {
var v1 = this._value;
var o1 = this._offset;
var v2 = v._value;
var o2 = v._offset;
if (this.is_native()) {
while (v1.compareTo(consts.bi_man_min_value) < 0) {
v1 = v1.multiply(consts.bi_10);
o1 -= 1;
}
}
if (v.is_native()) {
while (v2.compareTo(consts.bi_man_min_value) < 0) {
v2 = v2.multiply(consts.bi_10);
o2 -= 1;
}
}
result = new Amount();
result._offset = o1 + o2 + 14;
result._value = v1.multiply(v2).divide(consts.bi_1e14).add(consts.bi_7);
result._is_native = this._is_native;
result._is_negative = this._is_negative !== v._is_negative;
result._currency = this._currency;
result._issuer = this._issuer;
result.canonicalize();
}
return result;
};
// Return a new value. // Return a new value.
Amount.prototype.negate = function() { Amount.prototype.negate = function() {
return this.clone('NEGATE'); return this.clone('NEGATE');
@@ -953,12 +968,6 @@ Amount.prototype.set_issuer = function(issuer) {
return this; return this;
}; };
// Result in terms of this' currency and issuer.
Amount.prototype.subtract = function(v) {
// Correctness over speed, less code has less bugs, reuse add code.
return this.add(Amount.from_json(v).negate());
};
Amount.prototype.to_number = function(allow_nan) { Amount.prototype.to_number = function(allow_nan) {
var s = this.to_text(allow_nan); var s = this.to_text(allow_nan);
return typeof s === 'string' ? Number(s) : s; return typeof s === 'string' ? Number(s) : s;

View File

@@ -5,47 +5,51 @@ var Amount = require('./amount').Amount;
/** /**
* Meta data processing facility * Meta data processing facility
*
* @constructor
* @param {Object} transaction metadata
*/ */
function Meta(raw_data) { function Meta(data) {
var self = this; var self = this;
this.nodes = [ ]; this.nodes = [ ];
raw_data.AffectedNodes.forEach(function(an) { if (typeof data !== 'object') {
var result = { }; throw new TypeError('Missing metadata');
}
if ((result.diffType = self.diffType(an))) { if (!Array.isArray(data.AffectedNodes)) {
an = an[result.diffType]; throw new TypeError('Metadata missing AffectedNodes');
}
result.entryType = an.LedgerEntryType; data.AffectedNodes.forEach(this.addNode, this);
result.ledgerIndex = an.LedgerIndex;
result.fields = extend({}, an.PreviousFields, an.NewFields, an.FinalFields);
result.fieldsPrev = an.PreviousFields || {};
result.fieldsNew = an.NewFields || {};
result.fieldsFinal = an.FinalFields || {};
// getAffectedBooks will set this
// result.bookKey = undefined;
self.nodes.push(result);
}
});
}; };
Meta.node_types = [ Meta.nodeTypes = [
'CreatedNode', 'CreatedNode',
'ModifiedNode', 'ModifiedNode',
'DeletedNode' 'DeletedNode'
]; ];
Meta.prototype.diffType = function(an) { Meta.amountFieldsAffectingIssuer = [
var result = false; 'LowLimit',
'HighLimit',
'TakerPays',
'TakerGets'
];
for (var i=0; i<Meta.node_types.length; i++) { /**
var x = Meta.node_types[i]; * @api private
if (an.hasOwnProperty(x)) { */
result = x;
Meta.prototype.getNodeType = function(node) {
var result = null;
for (var i=0; i<Meta.nodeTypes.length; i++) {
var type = Meta.nodeTypes[i];
if (node.hasOwnProperty(type)) {
result = type;
break; break;
} }
} }
@@ -53,6 +57,63 @@ Meta.prototype.diffType = function(an) {
return result; return result;
}; };
/**
* Add node to metadata
*
* @param {Object} node
* @api private
*/
Meta.prototype.addNode = function(node) {
this._affectedAccounts = void(0);
this._affectedBooks = void(0);
var result = { };
if ((result.nodeType = this.getNodeType(node))) {
node = node[result.nodeType];
result.diffType = result.nodeType;
result.entryType = node.LedgerEntryType;
result.ledgerIndex = node.LedgerIndex;
result.fields = extend({ }, node.PreviousFields, node.NewFields, node.FinalFields);
result.fieldsPrev = node.PreviousFields || { };
result.fieldsNew = node.NewFields || { };
result.fieldsFinal = node.FinalFields || { };
// getAffectedBooks will set this
// result.bookKey = undefined;
this.nodes.push(result);
}
};
/**
* Get affected nodes array
*
* @param {Object} filter options
* @return {Array} nodes
*/
Meta.prototype.getNodes = function(options) {
if (typeof options === 'object') {
return this.nodes.filter(function(node) {
if (options.nodeType && options.nodeType !== node.nodeType) {
return false;
}
if (options.entryType && options.entryType !== node.entryType) {
return false;
}
if (options.bookKey && options.bookKey !== node.bookKey) {
return false;
}
return true;
});
} else {
return this.nodes;
}
};
/** /**
* Execute a function on each affected node. * Execute a function on each affected node.
* *
@@ -61,7 +122,7 @@ Meta.prototype.diffType = function(an) {
* *
* { * {
* // Type of diff, e.g. CreatedNode, ModifiedNode * // Type of diff, e.g. CreatedNode, ModifiedNode
* diffType: 'CreatedNode' * nodeType: 'CreatedNode'
* *
* // Type of node affected, e.g. RippleState, AccountRoot * // Type of node affected, e.g. RippleState, AccountRoot
* entryType: 'RippleState', * entryType: 'RippleState',
@@ -72,7 +133,7 @@ Meta.prototype.diffType = function(an) {
* // Contains all fields with later versions taking precedence * // Contains all fields with later versions taking precedence
* // * //
* // This is a shorthand for doing things like checking which account * // This is a shorthand for doing things like checking which account
* // this affected without having to check the diffType. * // this affected without having to check the nodeType.
* fields: {...}, * fields: {...},
* *
* // Old fields (before the change) * // Old fields (before the change)
@@ -88,43 +149,39 @@ Meta.prototype.diffType = function(an) {
* The second parameter to the callback is the index of the node in the metadata * The second parameter to the callback is the index of the node in the metadata
* (first entry is index 0). * (first entry is index 0).
*/ */
Meta.prototype.each = function(fn) {
for (var i = 0, l = this.nodes.length; i < l; i++) {
fn(this.nodes[i], i);
}
};
([ [
'forEach', 'forEach',
'map', 'map',
'filter', 'filter',
'every', 'every',
'some',
'reduce' 'reduce'
]).forEach(function(fn) { ].forEach(function(fn) {
Meta.prototype[fn] = function() { Meta.prototype[fn] = function() {
return Array.prototype[fn].apply(this.nodes, arguments); return Array.prototype[fn].apply(this.nodes, arguments);
}; };
}); });
var amountFieldsAffectingIssuer = [ Meta.prototype.each = Meta.prototype.forEach;
'LowLimit',
'HighLimit', Meta.prototype.getAffectedAccounts = function(from) {
'TakerPays', if (this._affectedAccounts) {
'TakerGets' return this._affectedAccounts;
]; }
Meta.prototype.getAffectedAccounts = function() {
var accounts = [ ]; var accounts = [ ];
// This code should match the behavior of the C++ method: // This code should match the behavior of the C++ method:
// TransactionMetaSet::getAffectedAccounts // TransactionMetaSet::getAffectedAccounts
this.nodes.forEach(function(an) { for (var i=0; i<this.nodes.length; i++) {
var fields = (an.diffType === 'CreatedNode') ? an.fieldsNew : an.fieldsFinal; var node = this.nodes[i];
for (var i in fields) { var fields = (node.nodeType === 'CreatedNode') ? node.fieldsNew : node.fieldsFinal;
var field = fields[i]; for (var fieldName in fields) {
var field = fields[fieldName];
if (typeof field === 'string' && UInt160.is_valid(field)) { if (typeof field === 'string' && UInt160.is_valid(field)) {
accounts.push(field); accounts.push(field);
} else if (amountFieldsAffectingIssuer.indexOf(i) !== -1) { } else if (~Meta.amountFieldsAffectingIssuer.indexOf(fieldName)) {
var amount = Amount.from_json(field); var amount = Amount.from_json(field);
var issuer = amount.issuer(); var issuer = amount.issuer();
if (issuer.is_valid() && !issuer.is_zero()) { if (issuer.is_valid() && !issuer.is_zero()) {
@@ -132,43 +189,53 @@ Meta.prototype.getAffectedAccounts = function() {
} }
} }
} }
}); }
return utils.arrayUnique(accounts); this._affectedAccounts = utils.arrayUnique(accounts);
return this._affectedAccounts;
}; };
Meta.prototype.getAffectedBooks = function() { Meta.prototype.getAffectedBooks = function() {
if (this._affectedBooks) {
return this._affectedBooks;
}
var books = [ ]; var books = [ ];
this.nodes.forEach(function(an) { for (var i=0; i<this.nodes.length; i++) {
if (an.entryType !== 'Offer') { var node = this.nodes[i];
return;
if (node.entryType !== 'Offer') {
continue;
} }
var gets = Amount.from_json(an.fields.TakerGets); var gets = Amount.from_json(node.fields.TakerGets);
var pays = Amount.from_json(an.fields.TakerPays); var pays = Amount.from_json(node.fields.TakerPays);
var getsKey = gets.currency().to_json(); var getsKey = gets.currency().to_json();
var paysKey = pays.currency().to_json();
if (getsKey !== 'XRP') { if (getsKey !== 'XRP') {
getsKey += '/' + gets.issuer().to_json(); getsKey += '/' + gets.issuer().to_json();
} }
var paysKey = pays.currency().to_json();
if (paysKey !== 'XRP') { if (paysKey !== 'XRP') {
paysKey += '/' + pays.issuer().to_json(); paysKey += '/' + pays.issuer().to_json();
} }
var key = [ getsKey, paysKey ].join(':'); var key = getsKey + ':' + paysKey;
// Hell of a lot of work, so we are going to cache this. We can use this // Hell of a lot of work, so we are going to cache this. We can use this
// later to good effect in OrderBook.notify to make sure we only process // later to good effect in OrderBook.notify to make sure we only process
// pertinent offers. // pertinent offers.
an.bookKey = key; node.bookKey = key;
books.push(key); books.push(key);
}); }
return utils.arrayUnique(books); this._affectedBooks = utils.arrayUnique(books);
return this._affectedBooks;
}; };
exports.Meta = Meta; exports.Meta = Meta;

File diff suppressed because it is too large Load Diff

View File

@@ -1480,19 +1480,19 @@ Remote.prototype.requestTxHistory = function(start, callback) {
*/ */
Remote.prototype.requestBookOffers = function(gets, pays, taker, callback) { Remote.prototype.requestBookOffers = function(gets, pays, taker, callback) {
var lastArg = arguments[arguments.length - 1];
if (gets.hasOwnProperty('gets') || gets.hasOwnProperty('taker_gets')) { if (gets.hasOwnProperty('gets') || gets.hasOwnProperty('taker_gets')) {
var options = gets; var options = gets;
// This would mutate the `lastArg` in `arguments` to be `null` and is // This would mutate the `lastArg` in `arguments` to be `null` and is
// redundant. Once upon a time, some awkward code was written f(g, null, // redundant. Once upon a time, some awkward code was written f(g, null,
// null, cb) ... // null, cb) ...
// callback = pays; callback = pays;
taker = options.taker; taker = options.taker;
pays = options.pays || options.taker_pays; pays = options.pays || options.taker_pays;
gets = options.gets || options.taker_gets; gets = options.gets || options.taker_gets;
} }
var lastArg = arguments[arguments.length - 1];
if (typeof lastArg === 'function') { if (typeof lastArg === 'function') {
callback = lastArg; callback = lastArg;
} }

1057
test/orderbook-test.js Normal file

File diff suppressed because it is too large Load Diff