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;
};
/**
* 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;
// 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());
};
/**
* 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) {
// Result in terms of this' currency and issuer.
// XXX Diverges from cpp.
Amount.prototype.multiply = function(v) {
var result;
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;
v = Amount.from_json(v);
if (this.is_zero()) {
result = this;
} else if (v.is_zero()) {
result = this.clone();
result._value = BigInteger.ZERO;
} 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;
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;
};
// 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.
Amount.prototype.divide = function(d) {
var result;
d = Amount.from_json(d);
if (d.is_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
* objects.
*
@@ -382,14 +287,15 @@ Amount.prototype.divide = function(d) {
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('' + denominator + '.0');
denominator = Amount.from_json(String(denominator) + '.0');
} else {
denominator = Amount.from_json(denominator);
}
var numerator = this;
denominator = Amount.from_json(denominator);
// If either operand is NaN, the result is NaN.
@@ -397,6 +303,10 @@ Amount.prototype.ratio_human = function(denominator, opts) {
return Amount.NaN();
}
if (denominator.is_zero()) {
return Amount.NaN();
}
// Apply interest/demurrage
//
// 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;
};
/**
* 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.
Amount.prototype.is_comparable = function(v) {
return this._value instanceof BigInteger
@@ -520,50 +579,6 @@ Amount.prototype.issuer = function() {
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.
Amount.prototype.negate = function() {
return this.clone('NEGATE');
@@ -953,12 +968,6 @@ Amount.prototype.set_issuer = function(issuer) {
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) {
var s = this.to_text(allow_nan);
return typeof s === 'string' ? Number(s) : s;

View File

@@ -5,47 +5,51 @@ var Amount = require('./amount').Amount;
/**
* Meta data processing facility
*
* @constructor
* @param {Object} transaction metadata
*/
function Meta(raw_data) {
function Meta(data) {
var self = this;
this.nodes = [ ];
raw_data.AffectedNodes.forEach(function(an) {
var result = { };
if (typeof data !== 'object') {
throw new TypeError('Missing metadata');
}
if ((result.diffType = self.diffType(an))) {
an = an[result.diffType];
if (!Array.isArray(data.AffectedNodes)) {
throw new TypeError('Metadata missing AffectedNodes');
}
result.entryType = an.LedgerEntryType;
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);
}
});
data.AffectedNodes.forEach(this.addNode, this);
};
Meta.node_types = [
Meta.nodeTypes = [
'CreatedNode',
'ModifiedNode',
'DeletedNode'
];
Meta.prototype.diffType = function(an) {
var result = false;
Meta.amountFieldsAffectingIssuer = [
'LowLimit',
'HighLimit',
'TakerPays',
'TakerGets'
];
for (var i=0; i<Meta.node_types.length; i++) {
var x = Meta.node_types[i];
if (an.hasOwnProperty(x)) {
result = x;
/**
* @api private
*/
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;
}
}
@@ -53,6 +57,63 @@ Meta.prototype.diffType = function(an) {
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.
*
@@ -61,7 +122,7 @@ Meta.prototype.diffType = function(an) {
*
* {
* // Type of diff, e.g. CreatedNode, ModifiedNode
* diffType: 'CreatedNode'
* nodeType: 'CreatedNode'
*
* // Type of node affected, e.g. RippleState, AccountRoot
* entryType: 'RippleState',
@@ -72,7 +133,7 @@ Meta.prototype.diffType = function(an) {
* // Contains all fields with later versions taking precedence
* //
* // 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: {...},
*
* // 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
* (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',
'map',
'filter',
'every',
'some',
'reduce'
]).forEach(function(fn) {
].forEach(function(fn) {
Meta.prototype[fn] = function() {
return Array.prototype[fn].apply(this.nodes, arguments);
};
});
var amountFieldsAffectingIssuer = [
'LowLimit',
'HighLimit',
'TakerPays',
'TakerGets'
];
Meta.prototype.each = Meta.prototype.forEach;
Meta.prototype.getAffectedAccounts = function(from) {
if (this._affectedAccounts) {
return this._affectedAccounts;
}
Meta.prototype.getAffectedAccounts = function() {
var accounts = [ ];
// This code should match the behavior of the C++ method:
// TransactionMetaSet::getAffectedAccounts
this.nodes.forEach(function(an) {
var fields = (an.diffType === 'CreatedNode') ? an.fieldsNew : an.fieldsFinal;
for (var i in fields) {
var field = fields[i];
for (var i=0; i<this.nodes.length; i++) {
var node = this.nodes[i];
var fields = (node.nodeType === 'CreatedNode') ? node.fieldsNew : node.fieldsFinal;
for (var fieldName in fields) {
var field = fields[fieldName];
if (typeof field === 'string' && UInt160.is_valid(field)) {
accounts.push(field);
} else if (amountFieldsAffectingIssuer.indexOf(i) !== -1) {
} else if (~Meta.amountFieldsAffectingIssuer.indexOf(fieldName)) {
var amount = Amount.from_json(field);
var issuer = amount.issuer();
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() {
if (this._affectedBooks) {
return this._affectedBooks;
}
var books = [ ];
this.nodes.forEach(function(an) {
if (an.entryType !== 'Offer') {
return;
for (var i=0; i<this.nodes.length; i++) {
var node = this.nodes[i];
if (node.entryType !== 'Offer') {
continue;
}
var gets = Amount.from_json(an.fields.TakerGets);
var pays = Amount.from_json(an.fields.TakerPays);
var gets = Amount.from_json(node.fields.TakerGets);
var pays = Amount.from_json(node.fields.TakerPays);
var getsKey = gets.currency().to_json();
var paysKey = pays.currency().to_json();
if (getsKey !== 'XRP') {
getsKey += '/' + gets.issuer().to_json();
}
var paysKey = pays.currency().to_json();
if (paysKey !== 'XRP') {
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
// later to good effect in OrderBook.notify to make sure we only process
// pertinent offers.
an.bookKey = key;
node.bookKey = key;
books.push(key);
});
}
return utils.arrayUnique(books);
this._affectedBooks = utils.arrayUnique(books);
return this._affectedBooks;
};
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) {
var lastArg = arguments[arguments.length - 1];
if (gets.hasOwnProperty('gets') || gets.hasOwnProperty('taker_gets')) {
var options = gets;
// 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,
// null, cb) ...
// callback = pays;
callback = pays;
taker = options.taker;
pays = options.pays || options.taker_pays;
gets = options.gets || options.taker_gets;
}
var lastArg = arguments[arguments.length - 1];
if (typeof lastArg === 'function') {
callback = lastArg;
}

1057
test/orderbook-test.js Normal file

File diff suppressed because it is too large Load Diff