mirror of
https://github.com/Xahau/xahau.js.git
synced 2025-11-25 14:45:48 +00:00
JS: Realtime orderbooks.
This commit is contained in:
@@ -128,6 +128,22 @@ Account.prototype.entry = function (callback)
|
|||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify object of a relevant transaction.
|
||||||
|
*
|
||||||
|
* This is only meant to be called by the Remote class. You should never have to
|
||||||
|
* call this yourself.
|
||||||
|
*/
|
||||||
|
Account.prototype.notifyTx = function (message)
|
||||||
|
{
|
||||||
|
// Only trigger the event if the account object is actually
|
||||||
|
// subscribed - this prevents some weird phantom events from
|
||||||
|
// occurring.
|
||||||
|
if (this._subs) {
|
||||||
|
this.emit('transaction', message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
exports.Account = Account;
|
exports.Account = Account;
|
||||||
|
|
||||||
// vim:sw=2:sts=2:ts=8:et
|
// vim:sw=2:sts=2:ts=8:et
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ Currency.json_rewrite = function (j) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Currency.from_json = function (j) {
|
Currency.from_json = function (j) {
|
||||||
return 'string' === typeof j
|
if (j instanceof Currency) return j.clone();
|
||||||
? (new Currency()).parse_json(j)
|
else if ('string' === typeof j) return (new Currency()).parse_json(j);
|
||||||
: j.clone();
|
else return new Currency(); // NaN
|
||||||
};
|
};
|
||||||
|
|
||||||
Currency.is_valid = function (j) {
|
Currency.is_valid = function (j) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
exports.Remote = require('./remote').Remote;
|
exports.Remote = require('./remote').Remote;
|
||||||
exports.Amount = require('./amount').Amount;
|
exports.Amount = require('./amount').Amount;
|
||||||
|
exports.Currency = require('./currency').Currency;
|
||||||
exports.UInt160 = require('./amount').UInt160;
|
exports.UInt160 = require('./amount').UInt160;
|
||||||
exports.Seed = require('./amount').Seed;
|
exports.Seed = require('./amount').Seed;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
var extend = require('extend');
|
var extend = require('extend');
|
||||||
|
var utils = require('./utils');
|
||||||
var UInt160 = require('./uint160').UInt160;
|
var UInt160 = require('./uint160').UInt160;
|
||||||
var Amount = require('./amount').Amount;
|
var Amount = require('./amount').Amount;
|
||||||
|
|
||||||
@@ -102,7 +103,35 @@ Meta.prototype.getAffectedAccounts = function ()
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
accounts = utils.arrayUnique(accounts);
|
||||||
|
|
||||||
return accounts;
|
return accounts;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Meta.prototype.getAffectedBooks = function ()
|
||||||
|
{
|
||||||
|
var books = [];
|
||||||
|
|
||||||
|
this.each(function (an) {
|
||||||
|
if (an.entryType !== 'Offer') return;
|
||||||
|
|
||||||
|
var gets = Amount.from_json(an.fields.TakerGets);
|
||||||
|
var pays = Amount.from_json(an.fields.TakerPays);
|
||||||
|
|
||||||
|
var getsKey = gets.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;
|
||||||
|
|
||||||
|
books.push(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
books = utils.arrayUnique(books);
|
||||||
|
|
||||||
|
return books;
|
||||||
|
};
|
||||||
|
|
||||||
exports.Meta = Meta;
|
exports.Meta = Meta;
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
// Routines for working with an orderbook.
|
// Routines for working with an orderbook.
|
||||||
//
|
//
|
||||||
|
// One OrderBook object represents one half of an order book. (i.e. bids OR
|
||||||
|
// asks) Which one depends on the ordering of the parameters.
|
||||||
|
//
|
||||||
// Events:
|
// Events:
|
||||||
|
// - transaction A transaction that affects the order book.
|
||||||
|
|
||||||
// var network = require("./network.js");
|
// var network = require("./network.js");
|
||||||
|
|
||||||
@@ -12,28 +16,29 @@ var Currency = require('./currency').Currency;
|
|||||||
var extend = require('extend');
|
var extend = require('extend');
|
||||||
|
|
||||||
var OrderBook = function (remote,
|
var OrderBook = function (remote,
|
||||||
currency_out, issuer_out,
|
currency_gets, issuer_gets,
|
||||||
currency_in, issuer_in) {
|
currency_pays, issuer_pays) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
this._remote = remote;
|
this._remote = remote;
|
||||||
this._currency_out = currency_out;
|
this._currency_gets = currency_gets;
|
||||||
this._issuer_out = issuer_out;
|
this._issuer_gets = issuer_gets;
|
||||||
this._currency_in = currency_in;
|
this._currency_pays = currency_pays;
|
||||||
this._issuer_in = issuer_in;
|
this._issuer_pays = issuer_pays;
|
||||||
|
|
||||||
this._subs = 0;
|
this._subs = 0;
|
||||||
|
|
||||||
// Ledger entry object
|
// We consider ourselves synchronized if we have a current copy of the offers,
|
||||||
// Important: This must never be overwritten, only extend()-ed
|
// we are online and subscribed to updates.
|
||||||
this._entry = {};
|
this._sync = false;
|
||||||
|
|
||||||
|
// Offers
|
||||||
|
this._offers = [];
|
||||||
|
|
||||||
this.on('newListener', function (type, listener) {
|
this.on('newListener', function (type, listener) {
|
||||||
if (OrderBook.subscribe_events.indexOf(type) !== -1) {
|
if (OrderBook.subscribe_events.indexOf(type) !== -1) {
|
||||||
if (!self._subs && 'open' === self._remote._online_state) {
|
if (!self._subs && 'open' === self._remote._online_state) {
|
||||||
self._remote.request_subscribe()
|
self._subscribe();
|
||||||
.books([self.to_json()], true)
|
|
||||||
.request();
|
|
||||||
}
|
}
|
||||||
self._subs += 1;
|
self._subs += 1;
|
||||||
}
|
}
|
||||||
@@ -44,6 +49,7 @@ var OrderBook = function (remote,
|
|||||||
self._subs -= 1;
|
self._subs -= 1;
|
||||||
|
|
||||||
if (!self._subs && 'open' === self._remote._online_state) {
|
if (!self._subs && 'open' === self._remote._online_state) {
|
||||||
|
self._sync = false;
|
||||||
self._remote.request_unsubscribe()
|
self._remote.request_unsubscribe()
|
||||||
.books([self.to_json()])
|
.books([self.to_json()])
|
||||||
.request();
|
.request();
|
||||||
@@ -59,6 +65,10 @@ var OrderBook = function (remote,
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this._remote.on('disconnect', function () {
|
||||||
|
self._sync = false;
|
||||||
|
});
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -67,17 +77,42 @@ OrderBook.prototype = new EventEmitter;
|
|||||||
/**
|
/**
|
||||||
* List of events that require a remote subscription to the orderbook.
|
* List of events that require a remote subscription to the orderbook.
|
||||||
*/
|
*/
|
||||||
OrderBook.subscribe_events = ['transaction'];
|
OrderBook.subscribe_events = ['transaction', 'model'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes to orderbook.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
OrderBook.prototype._subscribe = function ()
|
||||||
|
{
|
||||||
|
var self = this;
|
||||||
|
self._remote.request_subscribe()
|
||||||
|
.books([self.to_json()], true)
|
||||||
|
.on('error', function () {
|
||||||
|
// XXX What now?
|
||||||
|
})
|
||||||
|
.on('success', function (res) {
|
||||||
|
self._sync = true;
|
||||||
|
self._offers = res.offers;
|
||||||
|
self.emit('model', self._offers);
|
||||||
|
})
|
||||||
|
.request();
|
||||||
|
};
|
||||||
|
|
||||||
OrderBook.prototype.to_json = function ()
|
OrderBook.prototype.to_json = function ()
|
||||||
{
|
{
|
||||||
var json = {
|
var json = {
|
||||||
"CurrencyOut": this._currency_out,
|
"taker_gets": {
|
||||||
"CurrencyIn": this._currency_in
|
"currency": this._currency_gets
|
||||||
|
},
|
||||||
|
"taker_pays": {
|
||||||
|
"currency": this._currency_pays
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (json["CurrencyOut"] !== "XRP") json["IssuerOut"] = this._issuer_out;
|
if (this._currency_gets !== "XRP") json["taker_gets"]["issuer"] = this._issuer_gets;
|
||||||
if (json["CurrencyIn"] !== "XRP") json["IssuerIn"] = this._issuer_in;
|
if (this._currency_pays !== "XRP") json["taker_pays"]["issuer"] = this._issuer_pays;
|
||||||
|
|
||||||
return json;
|
return json;
|
||||||
};
|
};
|
||||||
@@ -90,15 +125,108 @@ OrderBook.prototype.to_json = function ()
|
|||||||
*/
|
*/
|
||||||
OrderBook.prototype.is_valid = function ()
|
OrderBook.prototype.is_valid = function ()
|
||||||
{
|
{
|
||||||
|
// XXX Should check for same currency (non-native) && same issuer
|
||||||
return (
|
return (
|
||||||
Currency.is_valid(this._currency_in) &&
|
Currency.is_valid(this._currency_pays) &&
|
||||||
(this._currency_in !== "XRP" && UInt160.is_valid(this._issuer_in)) &&
|
(this._currency_pays === "XRP" || UInt160.is_valid(this._issuer_pays)) &&
|
||||||
Currency.is_valid(this._currency_out) &&
|
Currency.is_valid(this._currency_gets) &&
|
||||||
(this._currency_out !== "XRP" && UInt160.is_valid(this._issuer_out)) &&
|
(this._currency_gets === "XRP" || UInt160.is_valid(this._issuer_gets)) &&
|
||||||
!(this._currency_in === "XRP" && this._currency_out === "XRP")
|
!(this._currency_pays === "XRP" && this._currency_gets === "XRP")
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify object of a relevant transaction.
|
||||||
|
*
|
||||||
|
* This is only meant to be called by the Remote class. You should never have to
|
||||||
|
* call this yourself.
|
||||||
|
*/
|
||||||
|
OrderBook.prototype.notifyTx = function (message)
|
||||||
|
{
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var changed = false;
|
||||||
|
|
||||||
|
message.mmeta.each(function (an) {
|
||||||
|
if (an.entryType !== 'Offer') return;
|
||||||
|
|
||||||
|
var i, l, offer;
|
||||||
|
if (an.diffType === 'DeletedNode' ||
|
||||||
|
an.diffType === 'ModifiedNode') {
|
||||||
|
for (i = 0, l = self._offers.length; i < l; i++) {
|
||||||
|
offer = self._offers[i];
|
||||||
|
console.log(offer.index, an.ledgerIndex);
|
||||||
|
if (offer.index === an.ledgerIndex) {
|
||||||
|
if (an.diffType === 'DeletedNode') {
|
||||||
|
self._offers.splice(i, 1);
|
||||||
|
console.log('node removed');
|
||||||
|
}
|
||||||
|
else extend(offer, an.fieldsFinal);
|
||||||
|
changed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (an.diffType === 'CreatedNode') {
|
||||||
|
var price = Amount.from_json(an.fields.TakerPays).ratio_human(an.fields.TakerGets);
|
||||||
|
for (i = 0, l = self._offers.length; i < l; i++) {
|
||||||
|
offer = self._offers[i];
|
||||||
|
var priceItem = Amount.from_json(offer.TakerPays).ratio_human(offer.TakerGets);
|
||||||
|
console.log(price.to_text_full(), priceItem.to_text_full());
|
||||||
|
if (price.compareTo(priceItem) <= 0) {
|
||||||
|
var obj = an.fields;
|
||||||
|
obj.index = an.ledgerIndex;
|
||||||
|
self._offers.splice(i, 0, an.fields);
|
||||||
|
changed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only trigger the event if the account object is actually
|
||||||
|
// subscribed - this prevents some weird phantom events from
|
||||||
|
// occurring.
|
||||||
|
if (this._subs) {
|
||||||
|
this.emit('transaction', message);
|
||||||
|
if (changed) this.emit('model', this._offers);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get offers model asynchronously.
|
||||||
|
*
|
||||||
|
* This function takes a callback and calls it with an array containing the
|
||||||
|
* current set of offers in this order book.
|
||||||
|
*
|
||||||
|
* If the data is available immediately, the callback may be called synchronously.
|
||||||
|
*/
|
||||||
|
OrderBook.prototype.offers = function (callback)
|
||||||
|
{
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if ("function" === typeof callback) {
|
||||||
|
if (this._sync) {
|
||||||
|
callback(this._offers);
|
||||||
|
} else {
|
||||||
|
this.once('model', function (offers) {
|
||||||
|
callback(offers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return latest known offers.
|
||||||
|
*
|
||||||
|
* Usually, this will just be an empty array if the order book hasn't been
|
||||||
|
* loaded yet. But this accessor may be convenient in some circumstances.
|
||||||
|
*/
|
||||||
|
OrderBook.prototype.offersSync = function ()
|
||||||
|
{
|
||||||
|
return this._offers;
|
||||||
|
};
|
||||||
|
|
||||||
exports.OrderBook = OrderBook;
|
exports.OrderBook = OrderBook;
|
||||||
|
|
||||||
// vim:sw=2:sts=2:ts=8:et
|
// vim:sw=2:sts=2:ts=8:et
|
||||||
|
|||||||
@@ -208,20 +208,25 @@ Request.prototype.books = function (books, state) {
|
|||||||
|
|
||||||
for (var i = 0, l = books.length; i < l; i++) {
|
for (var i = 0, l = books.length; i < l; i++) {
|
||||||
var book = books[i];
|
var book = books[i];
|
||||||
|
var json = {};
|
||||||
|
|
||||||
var json = {
|
function process(side) {
|
||||||
"CurrencyOut": Currency.json_rewrite(book["CurrencyOut"]),
|
if (!book[side]) throw new Error("Missing "+side);
|
||||||
"CurrencyIn": Currency.json_rewrite(book["CurrencyIn"])
|
|
||||||
};
|
|
||||||
|
|
||||||
if (json["CurrencyOut"] !== "XRP") {
|
var obj = {};
|
||||||
json["IssuerOut"] = UInt160.json_rewrite(book["IssuerOut"]);
|
obj["currency"] = Currency.json_rewrite(book[side]["currency"]);
|
||||||
}
|
if (obj["currency"] !== "XRP") {
|
||||||
if (json["CurrencyIn"] !== "XRP") {
|
obj.issuer = UInt160.json_rewrite(book[side]["issuer"]);
|
||||||
json["IssuerIn"] = UInt160.json_rewrite(book["IssuerIn"]);
|
}
|
||||||
|
|
||||||
|
json[side] = obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state || book["StateNow"]) json["StateNow"] = true;
|
process("taker_gets");
|
||||||
|
process("taker_pays");
|
||||||
|
|
||||||
|
if (state || book["state_now"]) json["state_now"] = true;
|
||||||
|
if (book["both_sides"]) json["both_sides"] = true;
|
||||||
|
|
||||||
procBooks.push(json);
|
procBooks.push(json);
|
||||||
}
|
}
|
||||||
@@ -280,6 +285,7 @@ var Remote = function (opts, trace) {
|
|||||||
this._reserve_base = undefined;
|
this._reserve_base = undefined;
|
||||||
this._reserve_inc = undefined;
|
this._reserve_inc = undefined;
|
||||||
this._server_status = undefined;
|
this._server_status = undefined;
|
||||||
|
this._last_tx = null;
|
||||||
|
|
||||||
// Local signing implies local fees and sequences
|
// Local signing implies local fees and sequences
|
||||||
if (this.local_signing) {
|
if (this.local_signing) {
|
||||||
@@ -300,6 +306,9 @@ var Remote = function (opts, trace) {
|
|||||||
// Hash map of Account objects by AccountId.
|
// Hash map of Account objects by AccountId.
|
||||||
this._accounts = {};
|
this._accounts = {};
|
||||||
|
|
||||||
|
// Hash map of OrderBook objects
|
||||||
|
this._books = {};
|
||||||
|
|
||||||
// List of secrets that we know about.
|
// List of secrets that we know about.
|
||||||
this.secrets = {
|
this.secrets = {
|
||||||
// Secrets can be set by calling set_secret(account, secret).
|
// Secrets can be set by calling set_secret(account, secret).
|
||||||
@@ -629,7 +638,10 @@ Remote.prototype._connect_message = function (ws, json) {
|
|||||||
// unsubscribes will be added as needed.
|
// unsubscribes will be added as needed.
|
||||||
// XXX If not trusted, need proof.
|
// XXX If not trusted, need proof.
|
||||||
|
|
||||||
// XXX Should de-duplicate transaction events
|
// De-duplicate transactions that are immediately following each other
|
||||||
|
// XXX Should have a cache of n txs so we can dedup out of order txs
|
||||||
|
if (this._last_tx === message.transaction.hash) break;
|
||||||
|
this._last_tx = message.transaction.hash;
|
||||||
|
|
||||||
if (this.trace) utils.logObject("remote: tx: %s", message);
|
if (this.trace) utils.logObject("remote: tx: %s", message);
|
||||||
|
|
||||||
@@ -641,12 +653,15 @@ Remote.prototype._connect_message = function (ws, json) {
|
|||||||
for (var i = 0, l = affected.length; i < l; i++) {
|
for (var i = 0, l = affected.length; i < l; i++) {
|
||||||
var account = self._accounts[affected[i]];
|
var account = self._accounts[affected[i]];
|
||||||
|
|
||||||
// Only trigger the event if the account object is actually
|
if (account) account.notifyTx(message);
|
||||||
// subscribed - this prevents some weird phantom events from
|
}
|
||||||
// occurring.
|
|
||||||
if (account && account._subs) {
|
// Pass the event on to any related OrderBooks
|
||||||
account.emit('transaction', message);
|
affected = message.mmeta.getAffectedBooks();
|
||||||
}
|
for (i = 0, l = affected.length; i < l; i++) {
|
||||||
|
var book = self._books[affected[i]];
|
||||||
|
|
||||||
|
if (book) book.notifyTx(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.emit('transaction', message);
|
this.emit('transaction', message);
|
||||||
@@ -1143,13 +1158,26 @@ Remote.prototype.account = function (accountId) {
|
|||||||
return this._accounts[accountId];
|
return this._accounts[accountId];
|
||||||
};
|
};
|
||||||
|
|
||||||
Remote.prototype.book = function (currency_out, issuer_out,
|
Remote.prototype.book = function (currency_gets, issuer_gets,
|
||||||
currency_in, issuer_in) {
|
currency_pays, issuer_pays) {
|
||||||
var book = new OrderBook(this,
|
var gets = currency_gets;
|
||||||
currency_out, issuer_out,
|
if (gets !== 'XRP') gets += '/' + issuer_gets;
|
||||||
currency_in, issuer_in);
|
var pays = currency_pays;
|
||||||
|
if (pays !== 'XRP') pays += '/' + issuer_pays;
|
||||||
|
|
||||||
return book;
|
var key = gets + ":" + pays;
|
||||||
|
|
||||||
|
if (!this._books[key]) {
|
||||||
|
var book = new OrderBook(this,
|
||||||
|
currency_gets, issuer_gets,
|
||||||
|
currency_pays, issuer_pays);
|
||||||
|
|
||||||
|
if (!book.is_valid()) return book;
|
||||||
|
|
||||||
|
this._books[key] = book;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._books[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the next account sequence if possible.
|
// Return the next account sequence if possible.
|
||||||
|
|||||||
@@ -97,6 +97,21 @@ var assert = function (assertion, msg) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return unique values in array.
|
||||||
|
*/
|
||||||
|
var arrayUnique = function (arr) {
|
||||||
|
var u = {}, a = [];
|
||||||
|
for (var i = 0, l = arr.length; i < l; ++i){
|
||||||
|
if (u.hasOwnProperty(arr[i])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
a.push(arr[i]);
|
||||||
|
u[arr[i]] = 1;
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a ripple epoch to a JavaScript timestamp.
|
* Convert a ripple epoch to a JavaScript timestamp.
|
||||||
*
|
*
|
||||||
@@ -115,6 +130,7 @@ exports.stringToHex = stringToHex;
|
|||||||
exports.chunkString = chunkString;
|
exports.chunkString = chunkString;
|
||||||
exports.logObject = logObject;
|
exports.logObject = logObject;
|
||||||
exports.assert = assert;
|
exports.assert = assert;
|
||||||
|
exports.arrayUnique = arrayUnique;
|
||||||
exports.toTimestamp = toTimestamp;
|
exports.toTimestamp = toTimestamp;
|
||||||
|
|
||||||
// vim:sw=2:sts=2:ts=8:et
|
// vim:sw=2:sts=2:ts=8:et
|
||||||
|
|||||||
Reference in New Issue
Block a user