diff --git a/package.json b/package.json index 2cb25123..fab74725 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ripple-lib", - "version": "0.7.14", + "version": "0.7.15", "description": "Ripple JavaScript client library", "files": [ "src/js/ripple/*.js", @@ -12,10 +12,10 @@ "test": "test" }, "dependencies": { - "async": "~0.1.22", - "ws": "~0.4.22", - "extend": "~1.1.1", - "simple-jsonrpc": "~0.0.1" + "async": "~0.2.9", + "ws": "~0.4.25", + "extend": "~1.1.3", + "simple-jsonrpc": "~0.0.2" }, "devDependencies": { "grunt": "~0.4.1", @@ -32,5 +32,8 @@ "type": "git", "url": "git://github.com/rippleFoundation/ripple-lib.git" }, - "readmeFilename": "README.md" + "readmeFilename": "README.md", + "engines": { + "node": ">=0.10.0" + } } diff --git a/src/js/ripple/base.js b/src/js/ripple/base.js index 903c2495..82833a38 100644 --- a/src/js/ripple/base.js +++ b/src/js/ripple/base.js @@ -64,6 +64,8 @@ Base.encode = function (input, alpha) { // --> input: String // <-- array of bytes or undefined. Base.decode = function (input, alpha) { + if ("string" !== typeof input) return undefined; + var alphabet = alphabets[alpha || 'ripple']; var bi_base = new BigInteger(String(alphabet.length)); var bi_value = nbi(); @@ -105,6 +107,17 @@ Base.decode = function (input, alpha) { return [].concat(utils.arraySet(zeros, 0), bytes); }; +Base.verify_checksum = function (bytes) { + var computed = sha256hash(bytes.slice(0, -4)).slice(0, 4); + var checksum = bytes.slice(-4); + + for (var i = 0; i < 4; i++) + if (computed[i] !== checksum[i]) + return false; + + return true; +}; + // --> input: Array // <-- String Base.encode_check = function (version, input, alphabet) { @@ -119,16 +132,24 @@ Base.encode_check = function (version, input, alphabet) { Base.decode_check = function (version, input, alphabet) { var buffer = Base.decode(input, alphabet); - if (!buffer || buffer[0] !== version || buffer.length < 5) + if (!buffer || buffer.length < 5) return NaN; - var computed = sha256hash(buffer.slice(0, -4)).slice(0, 4); - var checksum = buffer.slice(-4); - var i; + // Single valid version + if ("number" === typeof version && buffer[0] !== version) + return NaN; - for (i = 0; i != 4; i += 1) - if (computed[i] !== checksum[i]) - return NaN; + // Multiple allowed versions + if ("object" === typeof version && Array.isArray(version)) { + var match = false; + for (var i = 0, l = version.length; i < l; i++) { + match |= version[i] === buffer[0]; + } + if (!match) return NaN; + } + + if (!Base.verify_checksum(buffer)) + return NaN; // We'll use the version byte to add a leading zero, this ensures JSBN doesn't // intrepret the value as a negative number diff --git a/src/js/ripple/index.js b/src/js/ripple/index.js index bcfa829d..47b3b759 100644 --- a/src/js/ripple/index.js +++ b/src/js/ripple/index.js @@ -1,6 +1,7 @@ exports.Remote = require('./remote').Remote; exports.Amount = require('./amount').Amount; exports.Currency = require('./currency').Currency; +exports.Base = require('./base').Base; exports.UInt160 = require('./amount').UInt160; exports.Seed = require('./amount').Seed; exports.Transaction = require('./transaction').Transaction; diff --git a/src/js/ripple/remote.js b/src/js/ripple/remote.js index 61fbc43c..7e35a98d 100644 --- a/src/js/ripple/remote.js +++ b/src/js/ripple/remote.js @@ -32,48 +32,62 @@ var config = require('./config'); var sjcl = require('../../../build/sjcl'); // Request events emitted: -// 'success' : Request successful. -// 'error' : Request failed. -// 'remoteError' -// 'remoteUnexpected' -// 'remoteDisconnected' -var Request = function (remote, command) { +// 'success' : Request successful. +// 'error' : Request failed. +// 'remoteError' +// 'remoteUnexpected' +// 'remoteDisconnected' +var Request = function(remote, command, callback) { EventEmitter.call(this); var self = this; - this.message = { - 'command' : command, - 'id' : undefined, - }; this.remote = remote; + this.requested = false; + + this.message = { + 'command': command, + 'id': void(0) + }; + + this.callback(callback); }; util.inherits(Request, EventEmitter); // Send the request to a remote. -Request.prototype.request = function (remote) { - if (!this.requested) { +Request.prototype.request = function(remote) { + if (!this.remote._connected) { + this.remote._offline_queue.push(this); + } else if (!this.requested) { this.requested = true; this.remote.request(this); this.emit('request', remote); } }; -Request.prototype.build_path = function (build) { - if (build) +Request.prototype.callback = function(callback, successEvent) { + if (typeof callback === 'function') { + this.once('error', callback); + this.once(successEvent || 'success', callback.bind(this, null)); + this.request(); + } + return this; +}; + +Request.prototype.build_path = function(build) { + if (build) { this.message.build_path = true; + } return this; }; -Request.prototype.ledger_choose = function (current) { - if (current) - { +Request.prototype.ledger_choose = function(current) { + if (current) { this.message.ledger_index = this.remote._ledger_current_index; - } - else { + } else { this.message.ledger_hash = this.remote._ledger_hash; } @@ -83,7 +97,7 @@ Request.prototype.ledger_choose = function (current) { // Set the ledger for a request. // - ledger_entry // - transaction_entry -Request.prototype.ledger_hash = function (h) { +Request.prototype.ledger_hash = function(h) { this.message.ledger_hash = h; return this; @@ -91,39 +105,38 @@ Request.prototype.ledger_hash = function (h) { // Set the ledger_index for a request. // - ledger_entry -Request.prototype.ledger_index = function (ledger_index) { +Request.prototype.ledger_index = function(ledger_index) { this.message.ledger_index = ledger_index; return this; }; -Request.prototype.ledger_select = function (ledger_spec) { - if (ledger_spec === 'current') { - this.message.ledger_index = ledger_spec; - - } else if (ledger_spec === 'closed') { - this.message.ledger_index = ledger_spec; - - } else if (ledger_spec === 'verified') { - this.message.ledger_index = ledger_spec; - - } else if (String(ledger_spec).length > 12) { // XXX Better test needed - this.message.ledger_hash = ledger_spec; - - } else { - this.message.ledger_index = ledger_spec; +Request.prototype.ledger_select = function(ledger_spec) { + switch (ledger_spec) { + case 'current': + case 'closed': + case 'verified': + this.message.ledger_index = ledger_spec; + break; + default: + // XXX Better test needed + if (String(ledger_spec).length > 12) { + this.message.ledger_hash = ledger_spec; + } else { + this.message.ledger_index = ledger_spec; + } } return this; }; -Request.prototype.account_root = function (account) { +Request.prototype.account_root = function(account) { this.message.account_root = UInt160.json_rewrite(account); return this; }; -Request.prototype.index = function (hash) { +Request.prototype.index = function(hash) { this.message.index = hash; return this; @@ -132,183 +145,217 @@ Request.prototype.index = function (hash) { // Provide the information id an offer. // --> account // --> seq : sequence number of transaction creating offer (integer) -Request.prototype.offer_id = function (account, seq) { +Request.prototype.offer_id = function(account, seq) { this.message.offer = { - 'account' : UInt160.json_rewrite(account), - 'seq' : seq + 'account': UInt160.json_rewrite(account), + 'seq': seq }; return this; }; // --> index : ledger entry index. -Request.prototype.offer_index = function (index) { +Request.prototype.offer_index = function(index) { this.message.offer = index; return this; }; -Request.prototype.secret = function (s) { - if (s) +Request.prototype.secret = function(s) { + if (s) { this.message.secret = s; + } return this; }; -Request.prototype.tx_hash = function (h) { +Request.prototype.tx_hash = function(h) { this.message.tx_hash = h; return this; }; -Request.prototype.tx_json = function (j) { +Request.prototype.tx_json = function(j) { this.message.tx_json = j; return this; }; -Request.prototype.tx_blob = function (j) { +Request.prototype.tx_blob = function(j) { this.message.tx_blob = j; return this; }; -Request.prototype.ripple_state = function (account, issuer, currency) { +Request.prototype.ripple_state = function(account, issuer, currency) { this.message.ripple_state = { - 'accounts' : [ - UInt160.json_rewrite(account), - UInt160.json_rewrite(issuer) - ], - 'currency' : currency - }; + 'accounts' : [ + UInt160.json_rewrite(account), + UInt160.json_rewrite(issuer) + ], + 'currency' : currency + }; return this; }; -Request.prototype.accounts = function (accounts, realtime) { - if ("object" !== typeof accounts) { - accounts = [accounts]; +Request.prototype.accounts = function(accounts, realtime) { + if (!Array.isArray(accounts)) { + accounts = [ accounts ]; } // Process accounts parameters - var procAccounts = []; - for (var i = 0, l = accounts.length; i < l; i++) { - procAccounts.push(UInt160.json_rewrite(accounts[i])); - } + var procAccounts = accounts.map(function(account) { + return UInt160.json_rewrite(account); + }); + if (realtime) { this.message.rt_accounts = procAccounts; } else { - this.message.accounts = procAccounts; + this.message.accounts = procAccounts; } return this; }; -Request.prototype.rt_accounts = function (accounts) { +Request.prototype.rt_accounts = function(accounts) { return this.accounts(accounts, true); }; -Request.prototype.books = function (books, snapshot) { +Request.prototype.books = function(books, snapshot) { var procBooks = []; for (var i = 0, l = books.length; i < l; i++) { var book = books[i]; var json = {}; - function process(side) { - if (!book[side]) throw new Error("Missing "+side); + function processSide(side) { + if (!book[side]) throw new Error('Missing '+side); var obj = {}; - obj["currency"] = Currency.json_rewrite(book[side]["currency"]); - if (obj["currency"] !== "XRP") { - obj.issuer = UInt160.json_rewrite(book[side]["issuer"]); + obj['currency'] = Currency.json_rewrite(book[side]['currency']); + if (obj['currency'] !== 'XRP') { + obj.issuer = UInt160.json_rewrite(book[side]['issuer']); } json[side] = obj; } - process("taker_gets"); - process("taker_pays"); + processSide('taker_gets'); + processSide('taker_pays'); - if (snapshot || book["snapshot"]) json["snapshot"] = true; - if (book["both"]) json["both"] = true; + if (snapshot || book['snapshot']) json['snapshot'] = true; + if (book['both']) json['both'] = true; procBooks.push(json); } + this.message.books = procBooks; return this; }; -//------------------------------------------------------------------------------ + /** Interface to manage the connection to a Ripple server. - This implementation uses WebSockets. - Keys for opts: + Configuration options: - trusted : truthy, if remote is trusted - websocket_ip - websocket_port - websocket_ssl - trace - maxListeners + + `trusted` {Boolean} + if remote is trusted + + + `trace` {Boolean} + + + `maxListeners` {Number} + set maxListeners for EventEmitters to prevent + leak warnings. set to 0 for infinite + + + `servers` {Array} + list of remote servers to use. each entry + has the form: + + { + host: + port: + secure: + } Events: - 'connect' - 'connected' (DEPRECATED) - 'disconnect' - 'disconnected' (DEPRECATED) - 'state': - - 'online' : Connected and subscribed. - - 'offline' : Not subscribed or not connected. - 'subscribed' : This indicates stand-alone is available. + + + 'connect' + at least one server has connected. the remote + is ready to begin processing requests + + + 'connected' (DEPRECATED) + + + 'disconnect' + there are no more available servers. the + remote is unprepared to process requests + + + 'disconnected' (DEPRECATED) + + + 'state' + either 'online' or 'offline' + + + 'online' + connected and subscribed + + + 'offline' + not subscribed or not connected + + + 'subscribed' + this indicates stand-alone is available Server events: - 'ledger_closed' : A good indicate of ready to serve. - 'transaction' : Transactions we receive based on current subscriptions. - 'transaction_all' : Listening triggers a subscribe to all transactions - globally in the network. - @param opts Connection options. - @param trace + + 'ledger_closed' + a good indicate of ready to serve + + + 'transaction' + transactions we receive based on current subscriptions + + + 'transaction_all' + listening triggers a subscribe to all transactions + globally in the network + + @param {Object} opts Connection options. + @param {Boolean} trace */ -var Remote = function (opts, trace) { +var Remote = function(opts, trace) { EventEmitter.call(this); var self = this; this.trusted = opts.trusted; - this.websocket_ip = opts.websocket_ip; - this.websocket_port = opts.websocket_port; - this.websocket_ssl = opts.websocket_ssl; this.local_sequence = opts.local_sequence; // Locally track sequence numbers this.local_fee = opts.local_fee; // Locally set fees - this.local_signing = ("undefined" === typeof opts.local_signing) + this.local_signing = ('undefined' === typeof opts.local_signing) ? true : opts.local_signing; this.id = 0; this.trace = opts.trace || trace; this._server_fatal = false; // True, if we know server exited. - this._ledger_current_index = undefined; - this._ledger_hash = undefined; - this._ledger_time = undefined; - this._stand_alone = undefined; - this._testnet = undefined; + this._ledger_current_index = void(0); + this._ledger_hash = void(0); + this._ledger_time = void(0); + this._stand_alone = void(0); + this._testnet = void(0); this._transaction_subs = 0; this.online_target = false; + this._connected = false; this._online_state = 'closed'; // 'open', 'closed', 'connecting', 'closing' this.state = 'offline'; // 'online', 'offline' - this.retry_timer = undefined; - this.retry = undefined; + this.retry_timer = void(0); + this.retry = void(0); + this._offline_queue = [ ]; this._load_base = 256; this._load_factor = 1.0; - this._fee_ref = undefined; - this._fee_base = undefined; - this._reserve_base = undefined; - this._reserve_inc = undefined; + this._fee_ref = void(0); + this._fee_base = void(0); + this._reserve_base = void(0); + this._reserve_inc = void(0); this._connection_count = 0; this._last_tx = null; @@ -316,10 +363,11 @@ var Remote = function (opts, trace) { // Local signing implies local fees and sequences if (this.local_signing) { this.local_sequence = true; - this.local_fee = true; + this.local_fee = true; } - this._servers = []; + this._servers = []; + this._primary_server = void(0); // Cache information for accounts. // DEPRECATED, will be removed @@ -328,8 +376,7 @@ var Remote = function (opts, trace) { // Otherwise, clear it to have it automatically refreshed from the network. // account : { seq : __ } - - }; + }; // Hash map of Account objects by AccountId. this._accounts = {}; @@ -352,46 +399,56 @@ var Remote = function (opts, trace) { } }; - // XXX Add support for multiple servers - var url = (this.websocket_ssl ? "wss://" : "ws://") + - this.websocket_ip + ":" + this.websocket_port; - - var server = new Server (this, {url: url}) - - if ('maxListeners' in opts) - { - // This is used to remove Emitter warnings - // - server.setMaxListeners (opts.maxListeners) - this.setMaxListeners (opts.maxListeners) + // Support old API + if (!('servers' in opts)) { + opts.servers = [ { + host: opts.websocket_ip, + port: opts.websocket_port, + secure: opts.websocket_ssl, + trusted: opts.trusted + } ] } - this.add_server(server) + // Initialize servers + opts.servers.forEach(function(server) { + var i = typeof server.pool === 'number' ? server.pool : 1; + while (i--) { + self.add_server(server); + } + }); - this.on('newListener', function (type, listener) { - if ('transaction_all' === type) - { - if (!self._transaction_subs && 'open' === self._online_state) - { - self.request_subscribe([ 'transactions' ]) - .request(); - } - self._transaction_subs += 1; - } + // This is used to remove EventEmitter warnings + if ('maxListeners' in opts) { + this._servers.concat(this).forEach(function(i) { + i.setMaxListeners(opts.maxListeners); }); + } - this.on('removeListener', function (type, listener) { - if ('transaction_all' === type) - { - self._transaction_subs -= 1; - - if (!self._transaction_subs && 'open' === self._online_state) - { - self.request_unsubscribe([ 'transactions' ]) - .request(); - } + this.on('newListener', function(type, listener) { + if ('transaction_all' === type) { + if (!self._transaction_subs && self._online_state === 'open') { + self.request_subscribe('transactions').request(); } - }); + self._transaction_subs += 1; + } + }); + + this.on('removeListener', function(type, listener) { + if ('transaction_all' === type) { + self._transaction_subs -= 1; + if (!self._transaction_subs && self._online_state === 'open') { + self.request_unsubscribe('transactions').request(); + } + } + }); + + this.once('connect', function offlineQueueListener() { + var offline_queue = self._offline_queue; + var request; + while (request = offline_queue.shift()) { + request.request(); + } + }); }; util.inherits(Remote, EventEmitter); @@ -399,21 +456,21 @@ util.inherits(Remote, EventEmitter); // Flags for ledger entries. In support of account_root(). Remote.flags = { 'account_root' : { - 'PasswordSpent' : 0x00010000, - 'RequireDestTag' : 0x00020000, - 'RequireAuth' : 0x00040000, - 'DisallowXRP' : 0x00080000, + 'PasswordSpent': 0x00010000, + 'RequireDestTag': 0x00020000, + 'RequireAuth': 0x00040000, + 'DisallowXRP': 0x00080000 } }; -Remote.from_config = function (obj, trace) { - var serverConfig = 'string' === typeof obj ? config.servers[obj] : obj; +Remote.from_config = function(obj, trace) { + var serverConfig = typeof obj === 'string' ? config.servers[obj] : obj; var remote = new Remote(serverConfig, trace); for (var account in config.accounts) { var accountInfo = config.accounts[account]; - if ("object" === typeof accountInfo) { + if (typeof accountInfo === 'object') { if (accountInfo.secret) { // Index by nickname ... remote.set_secret(account, accountInfo.secret); @@ -426,29 +483,44 @@ Remote.from_config = function (obj, trace) { return remote; }; -var isTemMalformed = function (engine_result_code) { +var isTemMalformed = function(engine_result_code) { return (engine_result_code >= -299 && engine_result_code < 199); }; -var isTefFailure = function (engine_result_code) { +var isTefFailure = function(engine_result_code) { return (engine_result_code >= -299 && engine_result_code < 199); }; -Remote.prototype.add_server = function (server) { +Remote.prototype.add_server = function(opts) { var self = this; - server.on('message', function (data) { + var url = (opts.secure || opts.websocket_ssl ? 'wss://' : 'ws://') + + (opts.host || opts.websocket_ip) + ':' + + (opts.port || opts.websocket_port); + + var server = new Server(this, { url: url }) + + if ('maxListeners' in opts) { + server.setMaxListeners(opts.maxListeners); + } + + server.on('message', function(data) { self._handle_message(data); }); - server.on('connect', function () { + server.on('connect', function() { + if (opts.primary || !self._primary_server) { + self._set_primary_server(server); + } self._connection_count++; self._set_state('online'); }); - server.on('disconnect', function () { + server.on('disconnect', function() { self._connection_count--; - if (!self._connection_count) self._set_state('offline'); + if (!self._connection_count) { + self._set_state('offline'); + } }); this._servers.push(server); @@ -457,13 +529,15 @@ Remote.prototype.add_server = function (server) { }; // Inform remote that the remote server is not comming back. -Remote.prototype.server_fatal = function () { +Remote.prototype.server_fatal = function() { this._server_fatal = true; }; // Set the emitted state: 'online' or 'offline' -Remote.prototype._set_state = function (state) { - if (this.trace) console.log("remote: set_state: %s", state); +Remote.prototype._set_state = function(state) { + if (this.trace) { + console.log('remote: set_state: %s', state); + } if (this.state !== state) { this.state = state; @@ -472,13 +546,15 @@ Remote.prototype._set_state = function (state) { switch (state) { case 'online': - this._online_state = 'open'; + this._online_state = 'open'; + this._connected = true; this.emit('connect'); this.emit('connected'); break; case 'offline': - this._online_state = 'closed'; + this._online_state = 'closed'; + this._connected = false; this.emit('disconnect'); this.emit('disconnected'); break; @@ -486,7 +562,7 @@ Remote.prototype._set_state = function (state) { } }; -Remote.prototype.set_trace = function (trace) { +Remote.prototype.set_trace = function(trace) { this.trace = undefined === trace || trace; return this; @@ -495,17 +571,24 @@ Remote.prototype.set_trace = function (trace) { /** * Connect to the Ripple network. */ -Remote.prototype.connect = function (online) { - // Downwards compatibility - if ("undefined" !== typeof online && !online) { - return this.disconnect(); +Remote.prototype.connect = function(online, callback) { + if (typeof online === 'function') { + callback = online; + online = void(0); + this.once('connect', callback); } - if (!this._servers.length) { - throw new Error("No servers available."); + // Downwards compatibility + if (typeof online !== 'undefined' && !online) { + this.disconnect(); } else { - // XXX Add support for multiple servers - this._servers[0].connect(); + if (!this._servers.length) { + throw new Error('No servers available.'); + } else { + for (var i=0, l=this._servers.length; i request: what to send, consumed. -Remote.prototype.request = function (request) { - if (!this._servers.length) { - throw new Error("No servers available."); +Remote.prototype.request = function(request) { + var server = this._get_server(); + if (server) { + server.request(request); } else { - // XXX Add support for multiple servers - this._servers[0].request(request); + request.emit('error', new Error('No servers availale')); } }; -Remote.prototype.request_server_info = function () { - return new Request(this, 'server_info'); +Remote.prototype.server_info = +Remote.prototype.request_server_info = function(callback) { + return new Request(this, 'server_info').callback(callback); }; // XXX This is a bad command. Some varients don't scale. // XXX Require the server to be trusted. -Remote.prototype.request_ledger = function (ledger, opts) { +Remote.prototype.request_ledger = function(ledger, opts, callback) { //utils.assert(this.trusted); + if (typeof opts === 'function') { + callback = opts; + opts = { }; + } + var request = new Request(this, 'ledger'); - if (ledger) - { + if (ledger) { // DEPRECATED: use .ledger_hash() or .ledger_index() - console.log("request_ledger: ledger parameter is deprecated"); + console.log('request_ledger: ledger parameter is deprecated'); request.message.ledger = ledger; } - if ('object' == typeof opts) { + if (typeof opts === 'object') { if (opts.full) request.message.full = true; @@ -667,39 +798,38 @@ Remote.prototype.request_ledger = function (ledger, opts) { request.message.accounts = true; } // DEPRECATED: - else if (opts) - { - console.log("request_ledger: full parameter is deprecated"); + else if (opts) { + console.log('request_ledger: full parameter is deprecated'); request.message.full = true; } - return request; + return request.callback(callback); }; // Only for unit testing. -Remote.prototype.request_ledger_hash = function () { +Remote.prototype.request_ledger_hash = function(callback) { //utils.assert(this.trusted); // If not trusted, need to check proof. - return new Request(this, 'ledger_closed'); + return new Request(this, 'ledger_closed').callback(callback); }; // .ledger() // .ledger_index() -Remote.prototype.request_ledger_header = function () { - return new Request(this, 'ledger_header'); +Remote.prototype.request_ledger_header = function(callback) { + return new Request(this, 'ledger_header').callback(callback); }; // Get the current proposed ledger entry. May be closed (and revised) at any time (even before returning). // Only for unit testing. -Remote.prototype.request_ledger_current = function () { - return new Request(this, 'ledger_current'); +Remote.prototype.request_ledger_current = function(callback) { + return new Request(this, 'ledger_current').callback(callback); }; // --> type : the type of ledger entry. // .ledger() // .ledger_index() // .offer_id() -Remote.prototype.request_ledger_entry = function (type) { +Remote.prototype.request_ledger_entry = function(type, callback) { //utils.assert(this.trusted); // If not trusted, need to check proof, maybe talk packet protocol. var self = this; @@ -709,10 +839,11 @@ Remote.prototype.request_ledger_entry = function (type) { // If not found, listen, cache result, and emit it. // // Transparent caching: - if ('account_root' === type) { + + if (type === 'account_root') { request.request_default = request.request; - request.request = function () { // Intercept default request. + request.request = function() { // Intercept default request. var bDefault = true; // .self = Remote // this = Request @@ -726,11 +857,10 @@ Remote.prototype.request_ledger_entry = function (type) { } // else if (req.ledger_index) // else if ('ripple_state' === request.type) // YYY Could be cached per ledger. - else if ('account_root' === type) { + else if (type === 'account_root') { var cache = self.ledgers.current.account_root; - if (!cache) - { + if (!cache) { cache = self.ledgers.current.account_root = {}; } @@ -740,25 +870,24 @@ Remote.prototype.request_ledger_entry = function (type) { // Emulate fetch of ledger entry. // console.log('request_ledger_entry: emulating'); request.emit('success', { - // YYY Missing lots of fields. - 'node' : node, - }); + // YYY Missing lots of fields. + 'node' : node, + }); bDefault = false; - } + } else { // Was not cached. // XXX Only allow with trusted mode. Must sync response with advance. switch (type) { case 'account_root': - request.on('success', function (message) { - // Cache node. - // console.log('request_ledger_entry: caching'); - self.ledgers.current.account_root[message.node.Account] = message.node; - }); + request.on('success', function(message) { + // Cache node. + // console.log('request_ledger_entry: caching'); + self.ledgers.current.account_root[message.node.Account] = message.node; + }); break; - default: // This type not cached. // console.log('request_ledger_entry: non-cached type'); @@ -773,68 +902,60 @@ Remote.prototype.request_ledger_entry = function (type) { } }; - return request; + return request.callback(callback); }; // .accounts(accounts, realtime) -Remote.prototype.request_subscribe = function (streams) { +Remote.prototype.request_subscribe = function(streams, callback) { var request = new Request(this, 'subscribe'); if (streams) { - if ("object" !== typeof streams) { - streams = [streams]; - } - request.message.streams = streams; + request.message.streams = Array.isArray(streams) ? streams : [ streams ]; } - return request; + return request.callback(callback); }; -// .accounts(accounts, realtime) -Remote.prototype.request_unsubscribe = function (streams) { +Remote.prototype.request_unsubscribe = function(streams, callback) { var request = new Request(this, 'unsubscribe'); if (streams) { - if ("object" !== typeof streams) { - streams = [streams]; - } - request.message.streams = streams; + request.message.streams = Array.isArray(streams) ? streams : [ streams ]; } - return request; + return request.callback(callback); }; // .ledger_choose() // .ledger_hash() // .ledger_index() -Remote.prototype.request_transaction_entry = function (hash) { +Remote.prototype.request_transaction_entry = function(hash, callback) { //utils.assert(this.trusted); // If not trusted, need to check proof, maybe talk packet protocol. - return (new Request(this, 'transaction_entry')) - .tx_hash(hash); + return (new Request(this, 'transaction_entry')).tx_hash(hash).callback(callback); }; // DEPRECATED: use request_transaction_entry -Remote.prototype.request_tx = function (hash) { +Remote.prototype.request_tx = function(hash, callback) { var request = new Request(this, 'tx'); request.message.transaction = hash; - return request; + return request.callback(callback); }; -Remote.prototype.request_account_info = function (accountID) { +Remote.prototype.request_account_info = function(accountID, callback) { var request = new Request(this, 'account_info'); request.message.ident = UInt160.json_rewrite(accountID); // DEPRECATED request.message.account = UInt160.json_rewrite(accountID); - return request; + return request.callback(callback); }; // --> account_index: sub_account index (optional) // --> current: true, for the current ledger. -Remote.prototype.request_account_lines = function (accountID, account_index, current) { +Remote.prototype.request_account_lines = function(accountID, account_index, current, callback) { // XXX Does this require the server to be trusted? //utils.assert(this.trusted); @@ -842,25 +963,25 @@ Remote.prototype.request_account_lines = function (accountID, account_index, cur request.message.account = UInt160.json_rewrite(accountID); - if (account_index) + if (account_index) { request.message.index = account_index; + } - return request - .ledger_choose(current); + return request.ledger_choose(current).callback(callback); }; // --> account_index: sub_account index (optional) // --> current: true, for the current ledger. -Remote.prototype.request_account_offers = function (accountID, account_index, current) { +Remote.prototype.request_account_offers = function(accountID, account_index, current, callback) { var request = new Request(this, 'account_offers'); request.message.account = UInt160.json_rewrite(accountID); - if (account_index) + if (account_index) { request.message.index = account_index; + } - return request - .ledger_choose(current); + return request.ledger_choose(current).callback(callback); }; @@ -875,7 +996,7 @@ Remote.prototype.request_account_offers = function (accountID, account_index, cu limit: integer // optional */ -Remote.prototype.request_account_tx = function (obj) { +Remote.prototype.request_account_tx = function(obj, callback) { // XXX Does this require the server to be trusted? //utils.assert(this.trusted); @@ -887,18 +1008,19 @@ Remote.prototype.request_account_tx = function (obj) { //request.message.ledger = ledger_min; } else { - if ("undefined" != typeof obj.ledger_index_min) {request.message.ledger_index_min = obj.ledger_index_min;} - if ("undefined" != typeof obj.ledger_index_max) {request.message.ledger_index_max = obj.ledger_index_max;} - if ("undefined" != typeof obj.binary) {request.message.binary = obj.binary;} - if ("undefined" != typeof obj.count) {request.message.count = obj.count;} - if ("undefined" != typeof obj.descending) {request.message.descending = obj.descending;} - if ("undefined" != typeof obj.offset) {request.message.offset = obj.offset;} - if ("undefined" != typeof obj.limit) {request.message.limit = obj.limit;} + if ('undefined' !== typeof obj.ledger_index_min) {request.message.ledger_index_min = obj.ledger_index_min;} + if ('undefined' !== typeof obj.ledger_index_max) {request.message.ledger_index_max = obj.ledger_index_max;} + if ('undefined' !== typeof obj.binary) {request.message.binary = obj.binary;} + if ('undefined' !== typeof obj.count) {request.message.count = obj.count;} + if ('undefined' !== typeof obj.descending) {request.message.descending = obj.descending;} + if ('undefined' !== typeof obj.offset) {request.message.offset = obj.offset;} + if ('undefined' !== typeof obj.limit) {request.message.limit = obj.limit;} } - return request; + + return request.callback(callback); }; -Remote.prototype.request_book_offers = function (gets, pays, taker) { +Remote.prototype.request_book_offers = function(gets, pays, taker, callback) { var request = new Request(this, 'book_offers'); request.message.taker_gets = { @@ -919,20 +1041,20 @@ Remote.prototype.request_book_offers = function (gets, pays, taker) { request.message.taker = taker ? taker : UInt160.ACCOUNT_ONE; - return request; + return request.callback(callback); }; -Remote.prototype.request_wallet_accounts = function (seed) { +Remote.prototype.request_wallet_accounts = function(seed, callback) { utils.assert(this.trusted); // Don't send secrets. var request = new Request(this, 'wallet_accounts'); request.message.seed = seed; - return request; + return request.callback(callback); }; -Remote.prototype.request_sign = function (secret, tx_json) { +Remote.prototype.request_sign = function(secret, tx_json, callback) { utils.assert(this.trusted); // Don't send secrets. var request = new Request(this, 'sign'); @@ -940,16 +1062,12 @@ Remote.prototype.request_sign = function (secret, tx_json) { request.message.secret = secret; request.message.tx_json = tx_json; - return request; + return request.callback(callback); }; // Submit a transaction. -Remote.prototype.request_submit = function () { - var self = this; - - var request = new Request(this, 'submit'); - - return request; +Remote.prototype.request_submit = function(callback) { + return new Request(this, 'submit').callback(callback); }; // @@ -964,26 +1082,24 @@ Remote.prototype.request_submit = function () { * * This function will create and return the request, but not submit it. */ -Remote.prototype._server_prepare_subscribe = function () -{ +Remote.prototype._server_prepare_subscribe = function(callback) { var self = this; var feeds = [ 'ledger', 'server' ]; - if (this._transaction_subs) - feeds.push('transactions'); + if (this._transaction_subs) feeds.push('transactions'); - var req = this.request_subscribe(feeds); + var request = this.request_subscribe(feeds); - req.on('success', function (message) { + request.on('success', function(message) { self._stand_alone = !!message.stand_alone; self._testnet = !!message.testnet; - if ("string" === typeof message.random) { + if ('string' === typeof message.random) { var rand = message.random.match(/[0-9A-F]{8}/ig); - while (rand && rand.length) + while (rand && rand.length) { sjcl.random.addEntropy(parseInt(rand.pop(), 16)); - + } self.emit('random', utils.hexToArray(message.random)); } @@ -1008,74 +1124,87 @@ Remote.prototype._server_prepare_subscribe = function () self.emit('subscribed'); }); - self.emit('prepare_subscribe', req); + self.emit('prepare_subscribe', request); // XXX Could give error events, maybe even time out. - return req; + return request.callback(callback); }; // For unit testing: ask the remote to accept the current ledger. // - To be notified when the ledger is accepted, server_subscribe() then listen to 'ledger_hash' events. // A good way to be notified of the result of this is: -// remote.once('ledger_closed', function (ledger_closed, ledger_index) { ... } ); -Remote.prototype.ledger_accept = function () { - if (this._stand_alone || undefined === this._stand_alone) - { +// remote.once('ledger_closed', function(ledger_closed, ledger_index) { ... } ); +Remote.prototype.ledger_accept = function(callback) { + if (this._stand_alone || undefined === this._stand_alone) { var request = new Request(this, 'ledger_accept'); - - request - .request(); - } - else { - this.emit('error', { - 'error' : 'notStandAlone' - }); + request.callback(callback).request(); + } else { + var err = { 'error' : 'notStandAlone' } + if (typeof callback === 'function') { + callback(err); + } + this.emit('error', err); } return this; }; // Return a request to refresh the account balance. -Remote.prototype.request_account_balance = function (account, current) { +Remote.prototype.request_account_balance = function(account, current, callback) { var request = this.request_ledger_entry('account_root'); - return request - .account_root(account) + request.account_root(account) .ledger_choose(current) - .on('success', function (message) { - // If the caller also waits for 'success', they might run before this. - request.emit('account_balance', Amount.from_json(message.node.Balance)); - }); + .on('success', function(message) { + // If the caller also waits for 'success', they might run before this. + request.emit('account_balance', Amount.from_json(message.node.Balance)); + }); + + if (typeof callback === 'function') { + request.callback(callback, 'account_balance'); + } + + return request; }; // Return a request to return the account flags. -Remote.prototype.request_account_flags = function (account, current) { +Remote.prototype.request_account_flags = function(account, current, callback) { var request = this.request_ledger_entry('account_root'); - return request - .account_root(account) + request.account_root(account) .ledger_choose(current) - .on('success', function (message) { - // If the caller also waits for 'success', they might run before this. - request.emit('account_flags', message.node.Flags); - }); + .on('success', function(message) { + // If the caller also waits for 'success', they might run before this. + request.emit('account_flags', message.node.Flags); + }); + + if (typeof callback === 'function') { + request.callback(callback, 'account_flags'); + } + + return request; }; // Return a request to emit the owner count. -Remote.prototype.request_owner_count = function (account, current) { +Remote.prototype.request_owner_count = function(account, current, callback) { var request = this.request_ledger_entry('account_root'); - return request - .account_root(account) + request.account_root(account) .ledger_choose(current) - .on('success', function (message) { - // If the caller also waits for 'success', they might run before this. - request.emit('owner_count', message.node.OwnerCount); - }); + .on('success', function(message) { + // If the caller also waits for 'success', they might run before this. + request.emit('owner_count', message.node.OwnerCount); + }); + + if (typeof callback === 'function') { + request.callback(callback, 'owner_count'); + } + + return request; }; -Remote.prototype.account = function (accountId) { +Remote.prototype.account = function(accountId) { accountId = UInt160.json_rewrite(accountId); if (!this._accounts[accountId]) { @@ -1089,19 +1218,16 @@ Remote.prototype.account = function (accountId) { return this._accounts[accountId]; }; -Remote.prototype.book = function (currency_gets, issuer_gets, - currency_pays, issuer_pays) { +Remote.prototype.book = function(currency_gets, issuer_gets, currency_pays, issuer_pays) { var gets = currency_gets; if (gets !== 'XRP') gets += '/' + issuer_gets; var pays = currency_pays; if (pays !== 'XRP') pays += '/' + issuer_pays; - var key = gets + ":" + pays; + var key = gets + ':' + pays; if (!this._books[key]) { - var book = new OrderBook(this, - currency_gets, issuer_gets, - currency_pays, issuer_pays); + var book = new OrderBook(this, currency_gets, issuer_gets, currency_pays, issuer_pays); if (!book.is_valid()) return book; @@ -1109,41 +1235,41 @@ Remote.prototype.book = function (currency_gets, issuer_gets, } return this._books[key]; -} +}; // Return the next account sequence if possible. // <-- undefined or Sequence -Remote.prototype.account_seq = function (account, advance) { - account = UInt160.json_rewrite(account); - var account_info = this.accounts[account]; +Remote.prototype.account_seq = function(account, advance) { + var account = UInt160.json_rewrite(account); + var account_info = this.accounts[account]; var seq; - if (account_info && account_info.seq) - { + if (account_info && account_info.seq) { seq = account_info.seq; - if (advance === "ADVANCE") account_info.seq += 1; - if (advance === "REWIND") account_info.seq -= 1; + if (advance === 'ADVANCE') account_info.seq += 1; + if (advance === 'REWIND') account_info.seq -= 1; - // console.log("cached: %s current=%d next=%d", account, seq, account_info.seq); - } - else { - // console.log("uncached: %s", account); + // console.log('cached: %s current=%d next=%d', account, seq, account_info.seq); + } else { + // console.log('uncached: %s', account); } return seq; -} +}; -Remote.prototype.set_account_seq = function (account, seq) { - var account = UInt160.json_rewrite(account); +Remote.prototype.set_account_seq = function(account, seq) { + var account = UInt160.json_rewrite(account); - if (!this.accounts[account]) this.accounts[account] = {}; + if (!this.accounts[account]) { + this.accounts[account] = { }; + } this.accounts[account].seq = seq; -} +}; // Return a request to refresh accounts[account].seq. -Remote.prototype.account_seq_cache = function (account, current) { +Remote.prototype.account_seq_cache = function(account, current, callback) { var self = this; var request; @@ -1153,43 +1279,47 @@ Remote.prototype.account_seq_cache = function (account, current) { request = account_info.caching_seq_request; if (!request) { - // console.log("starting: %s", account); + // console.log('starting: %s', account); request = self.request_ledger_entry('account_root') .account_root(account) .ledger_choose(current) - .on('success', function (message) { - delete account_info.caching_seq_request; + .on('success', function(message) { + delete account_info.caching_seq_request; - var seq = message.node.Sequence; + var seq = message.node.Sequence; - account_info.seq = seq; + account_info.seq = seq; - // console.log("caching: %s %d", account, seq); - // If the caller also waits for 'success', they might run before this. - request.emit('success_account_seq_cache', message); - }) - .on('error', function (message) { - // console.log("error: %s", account); - delete account_info.caching_seq_request; + // console.log('caching: %s %d', account, seq); + // If the caller also waits for 'success', they might run before this. + request.emit('success_account_seq_cache', message); + }) + .on('error', function(message) { + // console.log('error: %s', account); + delete account_info.caching_seq_request; - request.emit('error_account_seq_cache', message); - }); + request.emit('error_account_seq_cache', message); + }); account_info.caching_seq_request = request; } + if (typeof callback === 'function') { + request.callback(callback, 'success_account_seq_cache'); + } + return request; }; // Mark an account's root node as dirty. -Remote.prototype.dirty_account_root = function (account) { - var account = UInt160.json_rewrite(account); +Remote.prototype.dirty_account_root = function(account) { + var account = UInt160.json_rewrite(account); delete this.ledgers.current.account_root[account]; }; // Store a secret - allows the Remote to automatically fill out auth information. -Remote.prototype.set_secret = function (account, secret) { +Remote.prototype.set_secret = function(account, secret) { this.secrets[account] = secret; }; @@ -1202,103 +1332,130 @@ Remote.prototype.set_secret = function (account, secret) { // --> current: bool : true = current ledger // // If does not exist: emit('error', 'error' : 'remoteError', 'remote' : { 'error' : 'entryNotFound' }) -Remote.prototype.request_ripple_balance = function (account, issuer, currency, current) { - var request = this.request_ledger_entry('ripple_state'); // YYY Could be cached per ledger. +Remote.prototype.request_ripple_balance = function(account, issuer, currency, current, callback) { + var request = this.request_ledger_entry('ripple_state'); // YYY Could be cached per ledger. - return request + request .ripple_state(account, issuer, currency) .ledger_choose(current) - .on('success', function (message) { - var node = message.node; + .on('success', function(message) { + var node = message.node; - var lowLimit = Amount.from_json(node.LowLimit); - var highLimit = Amount.from_json(node.HighLimit); - // The amount the low account holds of issuer. - var balance = Amount.from_json(node.Balance); - // accountHigh implies: for account: balance is negated, highLimit is the limit set by account. - var accountHigh = UInt160.from_json(account).equals(highLimit.issuer()); + var lowLimit = Amount.from_json(node.LowLimit); + var highLimit = Amount.from_json(node.HighLimit); + // The amount the low account holds of issuer. + var balance = Amount.from_json(node.Balance); + // accountHigh implies: for account: balance is negated, highLimit is the limit set by account. + var accountHigh = UInt160.from_json(account).equals(highLimit.issuer()); - request.emit('ripple_state', { - 'account_balance' : ( accountHigh ? balance.negate() : balance.clone()).parse_issuer(account), - 'peer_balance' : (!accountHigh ? balance.negate() : balance.clone()).parse_issuer(issuer), + request.emit('ripple_state', { + 'account_balance' : ( accountHigh ? balance.negate() : balance.clone()).parse_issuer(account), + 'peer_balance' : (!accountHigh ? balance.negate() : balance.clone()).parse_issuer(issuer), - 'account_limit' : ( accountHigh ? highLimit : lowLimit).clone().parse_issuer(issuer), - 'peer_limit' : (!accountHigh ? highLimit : lowLimit).clone().parse_issuer(account), + 'account_limit' : ( accountHigh ? highLimit : lowLimit).clone().parse_issuer(issuer), + 'peer_limit' : (!accountHigh ? highLimit : lowLimit).clone().parse_issuer(account), - 'account_quality_in' : ( accountHigh ? node.HighQualityIn : node.LowQualityIn), - 'peer_quality_in' : (!accountHigh ? node.HighQualityIn : node.LowQualityIn), + 'account_quality_in' : ( accountHigh ? node.HighQualityIn : node.LowQualityIn), + 'peer_quality_in' : (!accountHigh ? node.HighQualityIn : node.LowQualityIn), - 'account_quality_out' : ( accountHigh ? node.HighQualityOut : node.LowQualityOut), - 'peer_quality_out' : (!accountHigh ? node.HighQualityOut : node.LowQualityOut), - }); + 'account_quality_out' : ( accountHigh ? node.HighQualityOut : node.LowQualityOut), + 'peer_quality_out' : (!accountHigh ? node.HighQualityOut : node.LowQualityOut), }); -}; - -Remote.prototype.request_ripple_path_find = function (src_account, dst_account, dst_amount, source_currencies) { - var self = this; - var request = new Request(this, 'ripple_path_find'); - - request.message.source_account = UInt160.json_rewrite(src_account); - request.message.destination_account = UInt160.json_rewrite(dst_account); - request.message.destination_amount = Amount.json_rewrite(dst_amount); - - if (source_currencies) { - request.message.source_currencies = source_currencies.map(function (ci) { - var ci_new = {}; - - if ('issuer' in ci) - ci_new.issuer = UInt160.json_rewrite(ci.issuer); - - if ('currency' in ci) - ci_new.currency = Currency.json_rewrite(ci.currency); - - return ci_new; }); + + if (typeof callback === 'function') { + request.callback(callback, 'ripple_state'); } return request; }; -Remote.prototype.request_unl_list = function () { - return new Request(this, 'unl_list'); +Remote.prototype.request_ripple_path_find = function(src_account, dst_account, dst_amount, src_currencies, callback) { + var self = this; + + var opts = { }; + + if (typeof src_account === 'object') { + opts = src_account; + } else { + opts.src_account = src_account; + opts.dst_account = dst_account; + opts.dst_ammount = dst_amount; + opts.src_currencies = src_currencies; + } + + var request = new Request(this, 'ripple_path_find'); + + request.message.source_account = UInt160.json_rewrite(opts.src_account); + request.message.destination_account = UInt160.json_rewrite(opts.dst_account); + request.message.destination_amount = Amount.json_rewrite(opts.dst_amount); + + if (source_currencies) { + request.message.source_currencies = opts.src_currencies.map(function(ci) { + var ci_new = {}; + + if ('issuer' in ci) { + ci_new.issuer = UInt160.json_rewrite(ci.issuer); + } + if ('currency' in ci) { + ci_new.currency = Currency.json_rewrite(ci.currency); + } + + return ci_new; + }); + } + + return request.callback(callback); }; -Remote.prototype.request_unl_add = function (addr, comment) { +Remote.prototype.request_unl_list = function(callback) { + return new Request(this, 'unl_list').callback(callback); +}; + +Remote.prototype.request_unl_add = function(addr, comment, callback) { var request = new Request(this, 'unl_add'); - request.message.node = addr; + request.message.node = addr; - if (comment !== undefined) - request.message.comment = note; + switch (typeof comment) { + case 'string': + request.message.comment = comment; + break; - return request; + case 'function': + callback = comment; + break; + } + + return request.callback(callback); }; // --> node: | -Remote.prototype.request_unl_delete = function (node) { +Remote.prototype.request_unl_delete = function(node) { var request = new Request(this, 'unl_delete'); request.message.node = node; - return request; + return request.callback(callback); }; -Remote.prototype.request_peers = function () { - return new Request(this, 'peers'); +Remote.prototype.request_peers = function(callback) { + return new Request(this, 'peers', callback); }; -Remote.prototype.request_connect = function (ip, port) { +Remote.prototype.request_connect = function(ip, port, callback) { var request = new Request(this, 'connect'); request.message.ip = ip; - if (port) + if (typeof port !== 'undefined') { request.message.port = port; + } - return request; + return request.callback(callback); }; -Remote.prototype.transaction = function () { +Remote.prototype.transaction = function() { return new Transaction(this); }; diff --git a/src/js/ripple/server.js b/src/js/ripple/server.js index b7a342d0..26f146a2 100644 --- a/src/js/ripple/server.js +++ b/src/js/ripple/server.js @@ -1,47 +1,50 @@ -var EventEmitter = require('events').EventEmitter; -var util = require('util'); +var EventEmitter = require('events').EventEmitter; +var util = require('util'); +var WebSocket = require('ws'); +var utils = require('./utils'); -var utils = require('./utils'); - -//------------------------------------------------------------------------------ /** - Constructor + * Server + * + * Options must contain `url` to + * WebSocket server + * + * @constructor + * @param {Object} remote + * @param {Object} opts + */ - Keys for cfg: - - url - - @param remote The Remote object - @param cfg Configuration parameters. -*/ - -var Server = function (remote, cfg) -{ +function Server(remote, opts) { EventEmitter.call(this); - if ("object" !== typeof cfg || "string" !== typeof cfg.url) { - throw new Error("Invalid server configuration."); + if (typeof opts !== 'object' || typeof opts.url !== 'string') { + throw new Error('Invalid server configuration.'); } - this._remote = remote; - this._cfg = cfg; + var self = this; - this._ws = null; - this._connected = false; + this._remote = remote; + this._opts = opts; + + this._ws = void(0); + this._connected = false; this._should_connect = false; - this._state = null; + this._state = void(0); - this._id = 0; - this._retry = 0; + this._id = 0; + this._retry = 0; - this._requests = {}; + this._requests = { }; - this.on('message', this._handle_message.bind(this)); - this.on('response_subscribe', this._handle_response_subscribe.bind(this)); + this.on('message', function(message) { + self._handle_message(message); + }); + + this.on('response_subscribe', function(message) { + self._handle_response_subscribe(message); + }); }; -//------------------------------------------------------------------------------ - util.inherits(Server, EventEmitter); /** @@ -50,16 +53,33 @@ util.inherits(Server, EventEmitter); * Our requirements are that the server can process transactions and notify * us of changes. */ -Server.online_states = [ - 'syncing', - 'tracking', - 'proposing', - 'validating', - 'full' + +Server.online_states = [ + 'syncing' + , 'tracking' + , 'proposing' + , 'validating' + , 'full' ]; -Server.prototype.connect = function () -{ +/** + * Determine if a server status qualifies + * as 'online' + * + * @param {String} status + * @return {Boolean} + * @api private + */ + +Server.prototype.is_online = function(status) { + return Server.online_states.indexOf(status) !== -1; +}; + +/** + * Connect to WebSocket server + */ + +Server.prototype.connect = function() { var self = this; // We don't connect if we believe we're already connected. This means we have @@ -68,19 +88,22 @@ Server.prototype.connect = function () // we will automatically reconnect. if (this._connected === true) return; - if (this._remote.trace) console.log("server: connect: %s", this._cfg.url); + if (this._remote.trace) { + console.log('server: connect: %s', this._opts.url); + } // Ensure any existing socket is given the command to close first. - if (this._ws) this._ws.close(); + if (this._ws) { + this._ws.close(); + } - var WebSocket = require('ws'); - var ws = this._ws = new WebSocket(this._cfg.url); + var ws = this._ws = new WebSocket(this._opts.url); this._should_connect = true; self.emit('connecting'); - ws.onopen = function () { + ws.onopen = function() { // If we are no longer the active socket, simply ignore any event if (ws !== self._ws) return; @@ -91,13 +114,15 @@ Server.prototype.connect = function () self.request(request); }; - ws.onerror = function (e) { + ws.onerror = function(e) { // If we are no longer the active socket, simply ignore any event if (ws !== self._ws) return; - if (self._remote.trace) console.log("server: onerror: %s", e.data || e); + if (self._remote.trace) { + console.log('server: onerror: %s', e.data || e); + } - // Most connection errors for WebSockets are conveyed as "close" events with + // Most connection errors for WebSockets are conveyed as 'close' events with // code 1006. This is done for security purposes and therefore unlikely to // ever change. @@ -115,31 +140,33 @@ Server.prototype.connect = function () }; // Failure to open. - ws.onclose = function () { + ws.onclose = function() { // If we are no longer the active socket, simply ignore any event if (ws !== self._ws) return; - if (self._remote.trace) console.log("server: onclose: %s", ws.readyState); + if (self._remote.trace) { + console.log('server: onclose: %s', ws.readyState); + } handleConnectionClose(); }; - function handleConnectionClose() - { + function handleConnectionClose() { self.emit('socket_close'); self._set_state('offline'); // Prevent additional events from this socket - ws.onopen = ws.onerror = ws.onclose = ws.onmessage = function () {}; + ws.removeAllListeners(); + ws.on('error', function() {}); // Should we be connected? if (!self._should_connect) return; // Delay and retry. - self._retry += 1; - self._retry_timer = setTimeout(function () { - if (self._remote.trace) console.log("server: retry"); + self._retry += 1; + self._retry_timer = setTimeout(function retryTimeout() { + if (self._remote.trace) console.log('server: retry'); if (!self._should_connect) return; self.connect(); }, self._retry < 40 @@ -149,28 +176,48 @@ Server.prototype.connect = function () : self._retry < 40+60+60 ? 10*1000 // Then, for 10 minutes: once every 10 seconds : 30*1000); // Then: once every 30 seconds - } + }; - ws.onmessage = function (msg) { + ws.onmessage = function(msg) { self.emit('message', msg.data); }; }; -Server.prototype.disconnect = function () -{ +/** + * Disconnect from WebSocket server + * + * @api public + */ + +Server.prototype.disconnect = function() { this._should_connect = false; this._set_state('offline'); + if (this._ws) { + this._ws.close(); + } +}; - if (this.ws) { - this.ws.close(); +/** + * Send stringified message to WebSocket server + * + * @param {Object} message + * @api private + */ + +Server.prototype.send = function(message) { + if (this._ws) { + this._ws.send(JSON.stringify(message)); } }; /** * Submit a Request object to this server. + * + * @param {Object} request + * @api public */ -Server.prototype.request = function (request) -{ + +Server.prototype.request = function(request) { var self = this; // Only bother if we are still connected. @@ -182,86 +229,133 @@ Server.prototype.request = function (request) // Advance message ID self._id++; - if (self._state === "online" || - (request.message.command === "subscribe" && self._ws.readyState === 1)) { + if (self._connected || (request.message.command === 'subscribe' + && self._ws.readyState === 1)) { if (self._remote.trace) { - utils.logObject("server: request: %s", request.message); + utils.logObject('server: request: %s', request.message); } - - self._ws.send(JSON.stringify(request.message)); + self.send(request.message); } else { // XXX There are many ways to make self smarter. self.once('connect', function () { if (self._remote.trace) { - utils.logObject("server: request: %s", request.message); + utils.logObject('server: request: %s', request.message); } - self._ws.send(JSON.stringify(request.message)); + self.send(request.message); }); } } else { if (self._remote.trace) { - utils.logObject("server: request: DROPPING: %s", request.message); + utils.logObject('server: request: DROPPING: %s', request.message); } } }; -Server.prototype._set_state = function (state) { +/** + * Set server state + * + * Examples: + * + * set_state('online') + * set_state('offline') + * + * @param {String} state + * @api private + */ + +Server.prototype._set_state = function(state) { if (state !== this._state) { this._state = state; this.emit('state', state); + if (state === 'online') { + this._connected = true; this.emit('connect'); } else if (state === 'offline') { + this._connected = false; this.emit('disconnect'); } } }; -Server.prototype._handle_message = function (json) { - var self = this; +/** + * Handle WebSocket message + * + * @param {String} json + * @api private + */ - var message = JSON.parse(json); +Server.prototype._handle_message = function(json) { + var self = this; + var unexpected = false; + var message; + + try { message = JSON.parse(json); } catch(exception) { } - if (message.type === 'response') { - // A response to a request. - var request = self._requests[message.id]; + var unexpected = typeof message !== 'object' || typeof message.type !== 'string'; - delete self._requests[message.id]; + if (unexpected) { + // We received a malformed response from the server + } - if (!request) { - if (self._remote.trace) utils.logObject("server: UNEXPECTED: %s", message); - } else if ('success' === message.status) { - if (self._remote.trace) utils.logObject("server: response: %s", message); + if (!unexpected) { + switch (message.type) { + case 'response': + // A response to a request. + var request = self._requests[message.id]; - request.emit('success', message.result); - self.emit('response_'+request.message.command, message.result, request, message); - self._remote.emit('response_'+request.message.command, message.result, request, message); - } else if (message.error) { - if (self._remote.trace) utils.logObject("server: error: %s", message); + delete self._requests[message.id]; - request.emit('error', { - 'error' : 'remoteError', - 'error_message' : 'Remote reported an error.', - 'remote' : message - }); + if (!request) { + if (self._remote.trace) { + utils.logObject('server: UNEXPECTED: %s', message); + } + } else if (message.status === 'success') { + if (self._remote.trace) { + utils.logObject('server: response: %s', message); + } + + request.emit('success', message.result); + + [ self, self._remote ].forEach(function(emitter) { + emitter.emit('response_' + request.message.command, message.result, request, message); + }); + } else if (message.error) { + if (self._remote.trace) { + utils.logObject('server: error: %s', message); + } + + request.emit('error', { + 'error' : 'remoteError', + 'error_message' : 'Remote reported an error.', + 'remote' : message + }); + } + break; + + case 'serverStatus': + // This message is only received when online. + // As we are connected, it is the definitive final state. + self._set_state(self.is_online(message.server_status) ? 'online' : 'offline'); + break; } - } else if (message.type === 'serverStatus') { - // This message is only received when online. As we are connected, it is the definative final state. - self._set_state( - Server.online_states.indexOf(message.server_status) !== -1 - ? 'online' - : 'offline'); } }; -Server.prototype._handle_response_subscribe = function (message) -{ +/** + * Handle subscribe response + * + * @param {Object} message + * @api private + */ + +Server.prototype._handle_response_subscribe = function(message) { var self = this; self._server_status = message.server_status; - if (Server.online_states.indexOf(message.server_status) !== -1) { + if (self.is_online(message.server_status)) { self._set_state('online'); } }; diff --git a/src/js/ripple/transaction.js b/src/js/ripple/transaction.js index 3d8a08a1..6b2531ff 100644 --- a/src/js/ripple/transaction.js +++ b/src/js/ripple/transaction.js @@ -588,8 +588,9 @@ Transaction.prototype.offer_cancel = function (src, sequence) { // Options: // .set_flags() -// --> expiration : Date or Number -Transaction.prototype.offer_create = function (src, taker_pays, taker_gets, expiration) { +// --> expiration : if not undefined, Date or Number +// --> cancel_sequence : if not undefined, Sequence +Transaction.prototype.offer_create = function (src, taker_pays, taker_gets, expiration, cancel_sequence) { this._secret = this._account_secret(src); this.tx_json.TransactionType = 'OfferCreate'; this.tx_json.Account = UInt160.json_rewrite(src); @@ -605,6 +606,9 @@ Transaction.prototype.offer_create = function (src, taker_pays, taker_gets, expi ? expiration.getTime() : Number(expiration); + if (cancel_sequence) + this.tx_json.OfferSequence = Number(cancel_sequence); + return this; }; diff --git a/test/amount-test.js b/test/amount-test.js index de85aedb..62228dc2 100644 --- a/test/amount-test.js +++ b/test/amount-test.js @@ -7,7 +7,13 @@ var nbi = jsbn.nbi; var Amount = require("../src/js/ripple/amount").Amount; var UInt160 = require("../src/js/ripple/uint160").UInt160; -var config = require('../src/js/ripple/config').load(require('./config')); +try { + var conf = require('./config'); +} catch(exception) { + var conf = require('./config-example'); +} + +var config = require('../src/js/ripple/config').load(conf); // XXX Add test cases that push XRP vs non-XRP behavior. diff --git a/test/base58-test.js b/test/base58-test.js index 901f1e28..10ffd2ac 100644 --- a/test/base58-test.js +++ b/test/base58-test.js @@ -2,7 +2,13 @@ var buster = require("buster"); var Seed = require("../src/js/ripple/seed").Seed; -var config = require('../src/js/ripple/config').load(require('./config')); +try { + var conf = require('./config'); +} catch(exception) { + var conf = require('./config-example'); +} + +var config = require('../src/js/ripple/config').load(conf); buster.testCase("Base58", { "Seed" : { diff --git a/test/sign-test.js b/test/sign-test.js index 47d053f7..9dfa9c7d 100644 --- a/test/sign-test.js +++ b/test/sign-test.js @@ -2,7 +2,13 @@ var buster = require("buster"); var Seed = require("../src/js/ripple/seed").Seed; -var config = require('../src/js/ripple/config').load(require('./config')); +try { + var conf = require('./config'); +} catch(exception) { + var conf = require('./config-example'); +} + +var config = require('../src/js/ripple/config').load(conf); buster.testCase("Signing", { "Keys" : {