diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..da0f9eb2 --- /dev/null +++ b/.npmignore @@ -0,0 +1,2 @@ +build +deploy diff --git a/README.md b/README.md index 90a430ad..9efbca86 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,121 @@ Ripple JavaScript Library - ripple-lib This library can connect to the Ripple network via the WebSocket protocol and runs in Node.js as well as in the browser. -Build instructions: * https://ripple.com/wiki/Ripple_JavaScript_library - -For more information: * https://ripple.com * https://ripple.com/wiki + +##Initializing a remote connection + +[ripple-lib.remote](https://github.com/ripple/ripple-lib/blob/develop/src/js/ripple/remote.js) is responsible for managing connections to rippled servers. + +```js +var Remote = require('ripple-lib').Remote; + +var remote = new Remote({ + trusted: false, + servers: [ + { + host: '' + , port: 1111, + , secure: true + } + ] +}); + +remote.connect(); +``` + +Once a connection is formed to any of the supplied servers, a `connect` event is emitted, indicating that the remote is ready to begin fulfilling requests. When there are no more connected servers to fulfill requests, a `disconnect` event is emitted. If you send requests before ripple-lib is connected to any servers, requests are deferred until the `connect` event is received. + +```js +var remote = new Remote({ /* options */ }).connect(); +remote.request_server_info(function(err, info) { }); // will defer until connected +``` + +##Remote functions + +Each remote function returns a `Request` object. is object is an `EventEmitter`. You may listen for success or failure events from each request, or provide a callback. Example: + +```js +var request = remote.request_server_info(); +request.on('success', function(res) { + //handle success conditions +}); +request.on('error', function(err) { + //handle error conditions +}); +request.request(); +``` + +Or: + +```js +remote.request_server_info(function(err, res) { + +}); +``` + +**request_server_info([callback])** + +**request_ledger(ledger, [opts], [callback])** + +**request_ledger_hash([callback])** + +**request_ledger_header([callback])** + +**request_ledger_current([callback])** + +**request_ledger_entry(type, [callback])** + +**request_subscribe(streams, [callback])** + +**request_unsubscribe(streams, [callback])** + +**request_transaction_entry(hash, [callback])** + +**request_tx(hash, [callback])** + +**request_account_info(accountID, [callback])** + +**request_account_lines(accountID, account_index, current, [callback])** + +**request_account_offers(accountID, account_index, current, [callback])** + +**request_account_tx(opts, [callback])** + +**request_book_offers(gets, pays, taker, [callback])** + +**request_wallet_accounts(seed, [callback])** + ++ requires trusted **remote + +**request_sign(secret, tx_json, [callback])** + ++ requires trusted **remote + +**request_submit([callback])** + +**request_account_balance(account, current, [callback])** + +**request_account_flags(account, current, [callback])** + +**request_owner_count(account, current, [callback])** + +**request_ripple_balance(account, issuer, currency, current, [callback])** + +**request_ripple_path_find(src_account, dst_account, dst_amount, src_currencies, [callback])** + +**request_unl_list([callback])** + +**request_unl_add(addr, comment, [callback])** + +**request_unl_delete(node, [callback])** + +**request_peers([callback])** + +**request_connect(ip, port, [callback])** + +**transaction()** + ++ returns a [Transaction](https://github.com/ripple/ripple-lib/blob/develop/src/js/ripple/transaction.js) object diff --git a/package.json b/package.json index fab74725..35b59005 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ripple-lib", - "version": "0.7.15", + "version": "0.7.17", "description": "Ripple JavaScript client library", "files": [ "src/js/ripple/*.js", @@ -13,17 +13,18 @@ }, "dependencies": { "async": "~0.2.9", - "ws": "~0.4.25", + "ws": "~0.4.27", "extend": "~1.1.3", - "simple-jsonrpc": "~0.0.2" + "simple-jsonrpc": "~0.0.2", + "jshint-loader": "~0.5.0" }, "devDependencies": { "grunt": "~0.4.1", "grunt-contrib-concat": "~0.3.0", - "grunt-contrib-watch": "~0.4.0", - "grunt-webpack": "~0.10.2", - "grunt-dox": "~0.4.1", - "buster": "~0.6.2" + "grunt-contrib-watch": "~0.4.4", + "grunt-webpack": "~0.10.5", + "grunt-dox": "~0.5.0", + "buster": "~0.6.12" }, "scripts": { "test": "node_modules/buster/bin/buster test" diff --git a/src/js/ripple/amount.js b/src/js/ripple/amount.js index b86b5c5d..5e09e3eb 100644 --- a/src/js/ripple/amount.js +++ b/src/js/ripple/amount.js @@ -882,6 +882,8 @@ Amount.prototype.to_text = function (allow_nan) { * @param opts.min_precision {Number} Min. number of digits after dec. point. * @param opts.skip_empty_fraction {Boolean} Don't show fraction if it is zero, * even if min_precision is set. + * @param opts.max_sig_digits {Number} Maximum number of significant digits. + * Will cut fractional part, but never integer part. * @param opts.group_sep {Boolean|String} Whether to show a separator every n * digits, if a string, that value will be used as the separator. Default: "," * @param opts.group_width {Number} How many numbers will be grouped together, @@ -914,10 +916,38 @@ Amount.prototype.to_human = function (opts) fraction_part = fraction_part.replace(/0*$/, ''); if (fraction_part.length || !opts.skip_empty_fraction) { + // Enforce the maximum number of decimal digits (precision) if ("number" === typeof opts.precision) { fraction_part = fraction_part.slice(0, opts.precision); } + // Limit the number of significant digits (max_sig_digits) + if ("number" === typeof opts.max_sig_digits) { + // First, we count the significant digits we have. + // A zero in the integer part does not count. + var int_is_zero = +int_part === 0; + var digits = int_is_zero ? 0 : int_part.length; + + // Don't count leading zeros in the fractional part if the integer part is + // zero. + var sig_frac = int_is_zero ? fraction_part.replace(/^0*/, '') : fraction_part; + digits += sig_frac.length; + + // Now we calculate where we are compared to the maximum + var rounding = digits - opts.max_sig_digits; + + // If we're under the maximum we want to cut no (=0) digits + rounding = Math.max(rounding, 0); + + // If we're over the maximum we still only want to cut digits from the + // fractional part, not from the integer part. + rounding = Math.min(rounding, fraction_part.length); + + // Now we cut `rounding` digits off the right. + if (rounding > 0) fraction_part = fraction_part.slice(0, -rounding); + } + + // Enforce the minimum number of decimal digits (min_precision) if ("number" === typeof opts.min_precision) { while (fraction_part.length < opts.min_precision) { fraction_part += "0"; diff --git a/src/js/ripple/orderbook.js b/src/js/ripple/orderbook.js index 70baa4d5..a67c60c7 100644 --- a/src/js/ripple/orderbook.js +++ b/src/js/ripple/orderbook.js @@ -9,35 +9,32 @@ // var network = require("./network.js"); var EventEmitter = require('events').EventEmitter; -var util = require('util'); +var util = require('util'); -var Amount = require('./amount').Amount; -var UInt160 = require('./uint160').UInt160; -var Currency = require('./currency').Currency; +var Amount = require('./amount').Amount; +var UInt160 = require('./uint160').UInt160; +var Currency = require('./currency').Currency; -var extend = require('extend'); +var extend = require('extend'); -var OrderBook = function (remote, - currency_gets, issuer_gets, - currency_pays, issuer_pays) { +var OrderBook = function (remote, currency_gets, issuer_gets, currency_pays, issuer_pays) { EventEmitter.call(this); - var self = this; + var self = this; - this._remote = remote; + this._remote = remote; this._currency_gets = currency_gets; - this._issuer_gets = issuer_gets; + this._issuer_gets = issuer_gets; this._currency_pays = currency_pays; - this._issuer_pays = issuer_pays; - - this._subs = 0; + this._issuer_pays = issuer_pays; + this._subs = 0; // We consider ourselves synchronized if we have a current copy of the offers, // we are online and subscribed to updates. - this._sync = false; + this._sync = false; // Offers - this._offers = []; + this._offers = []; this.on('newListener', function (type, listener) { if (OrderBook.subscribe_events.indexOf(type) !== -1) { @@ -49,10 +46,9 @@ var OrderBook = function (remote, }); this.on('removeListener', function (type, listener) { - if (OrderBook.subscribe_events.indexOf(type) !== -1) { + if (~OrderBook.subscribe_events.indexOf(type)) { self._subs -= 1; - - if (!self._subs && 'open' === self._remote._online_state) { + if (!self._subs && self._remote._connected) { self._sync = false; self._remote.request_unsubscribe() .books([self.to_json()]) @@ -86,8 +82,7 @@ OrderBook.subscribe_events = ['transaction', 'model', 'trade']; * * @private */ -OrderBook.prototype._subscribe = function () -{ +OrderBook.prototype._subscribe = function () { var self = this; self._remote.request_subscribe() .books([self.to_json()], true) @@ -95,26 +90,28 @@ OrderBook.prototype._subscribe = function () // XXX What now? }) .on('success', function (res) { - self._sync = true; + 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 = { - "taker_gets": { - "currency": this._currency_gets + 'taker_gets': { + 'currency': this._currency_gets }, - "taker_pays": { - "currency": this._currency_pays + 'taker_pays': { + 'currency': this._currency_pays } }; - if (this._currency_gets !== "XRP") json["taker_gets"]["issuer"] = this._issuer_gets; - if (this._currency_pays !== "XRP") json["taker_pays"]["issuer"] = this._issuer_pays; + if (this._currency_gets !== 'XRP') + json['taker_gets']['issuer'] = this._issuer_gets; + + if (this._currency_pays !== 'XRP') + json['taker_pays']['issuer'] = this._issuer_pays; return json; }; @@ -125,78 +122,88 @@ OrderBook.prototype.to_json = function () * Note: This only checks whether the parameters (currencies and issuer) are * syntactically valid. It does not check anything against the ledger. */ -OrderBook.prototype.is_valid = function () -{ +OrderBook.prototype.is_valid = function () { // XXX Should check for same currency (non-native) && same issuer return ( Currency.is_valid(this._currency_pays) && - (this._currency_pays === "XRP" || UInt160.is_valid(this._issuer_pays)) && + (this._currency_pays === 'XRP' || UInt160.is_valid(this._issuer_pays)) && Currency.is_valid(this._currency_gets) && - (this._currency_gets === "XRP" || UInt160.is_valid(this._issuer_gets)) && - !(this._currency_pays === "XRP" && this._currency_gets === "XRP") + (this._currency_gets === 'XRP' || UInt160.is_valid(this._issuer_gets)) && + !(this._currency_pays === 'XRP' && this._currency_gets === 'XRP') ); }; +OrderBook.prototype.trade = function(type) { + var tradeStr = '0' + + (this['_currency_' + type] === 'XRP') ? '' : '/' + + this['_currency_' + type ] + '/' + + this['_issuer_' + type]; + return Amount.from_json(tradeStr); +}; + /** * 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; - - var trade_gets = Amount.from_json("0" + ((this._currency_gets === 'XRP') ? "" : - "/" + this._currency_gets + - "/" + this._issuer_gets)); - var trade_pays = Amount.from_json("0" + ((this._currency_pays === 'XRP') ? "" : - "/" + this._currency_pays + - "/" + this._issuer_pays)); +OrderBook.prototype.notifyTx = function (message) { + var self = this; + var changed = false; + var trade_gets = this.trade('gets'); + var trade_pays = this.trade('pays'); 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]; - if (offer.index === an.ledgerIndex) { - if (an.diffType === 'DeletedNode') { - self._offers.splice(i, 1); + + switch(an.diffType) { + case 'DeletedNode': + case 'ModifiedNode': + var deletedNode = an.diffType === 'DeletedNode'; + + for (i = 0, l = self._offers.length; i < l; i++) { + offer = self._offers[i]; + if (offer.index === an.ledgerIndex) { + if (deletedNode) { + self._offers.splice(i, 1); + } else { + extend(offer, an.fieldsFinal); + } + changed = true; + break; } - else extend(offer, an.fieldsFinal); - changed = true; - break; } - } - // We don't want to count a OfferCancel as a trade - if (message.transaction.TransactionType === "OfferCancel") return; + // We don't want to count a OfferCancel as a trade + if (message.transaction.TransactionType === 'OfferCancel') return; - trade_gets = trade_gets.add(an.fieldsPrev.TakerGets); - trade_pays = trade_pays.add(an.fieldsPrev.TakerPays); - if (an.diffType === 'ModifiedNode') { - trade_gets = trade_gets.subtract(an.fieldsFinal.TakerGets); - trade_pays = trade_pays.subtract(an.fieldsFinal.TakerPays); - } - } 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); + trade_gets = trade_gets.add(an.fieldsPrev.TakerGets); + trade_pays = trade_pays.add(an.fieldsPrev.TakerPays); - if (price.compareTo(priceItem) <= 0) { - var obj = an.fields; - obj.index = an.ledgerIndex; - self._offers.splice(i, 0, an.fields); - changed = true; - break; + if (!deletedNode) { + trade_gets = trade_gets.subtract(an.fieldsFinal.TakerGets); + trade_pays = trade_pays.subtract(an.fieldsFinal.TakerPays); } - } + break; + + case '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); + + if (price.compareTo(priceItem) <= 0) { + var obj = an.fields; + obj.index = an.ledgerIndex; + self._offers.splice(i, 0, an.fields); + changed = true; + break; + } + } + break; } }); @@ -218,17 +225,13 @@ OrderBook.prototype.notifyTx = function (message) * * If the data is available immediately, the callback may be called synchronously. */ -OrderBook.prototype.offers = function (callback) -{ +OrderBook.prototype.offers = function (callback) { var self = this; - - if ("function" === typeof callback) { + if (typeof callback === 'function') { if (this._sync) { callback(this._offers); } else { - this.once('model', function (offers) { - callback(offers); - }); + this.once('model', callback); } } return this; @@ -240,11 +243,10 @@ OrderBook.prototype.offers = function (callback) * 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 () -{ +OrderBook.prototype.offersSync = function () { return this._offers; }; -exports.OrderBook = OrderBook; +exports.OrderBook = OrderBook; // vim:sw=2:sts=2:ts=8:et diff --git a/src/js/ripple/remote.js b/src/js/ripple/remote.js index 7e35a98d..1ecb16dd 100644 --- a/src/js/ripple/remote.js +++ b/src/js/ripple/remote.js @@ -37,46 +37,63 @@ var sjcl = require('../../../build/sjcl'); // 'remoteError' // 'remoteUnexpected' // 'remoteDisconnected' -var Request = function(remote, command, callback) { +function Request(remote, command) { EventEmitter.call(this); - - var self = this; - this.remote = remote; - this.requested = false; - this.message = { - 'command': command, - 'id': void(0) + 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.remote._connected) { - this.remote._offline_queue.push(this); - } else if (!this.requested) { +Request.prototype.request = function (remote) { + if (!this.requested) { this.requested = true; this.remote.request(this); this.emit('request', remote); } }; -Request.prototype.callback = function(callback, successEvent) { - if (typeof callback === 'function') { - this.once('error', callback); +Request.prototype.callback = function(callback, successEvent, errorEvent) { + if (callback && typeof callback === 'function') { this.once(successEvent || 'success', callback.bind(this, null)); + this.once(errorEvent || 'error' , callback.bind(this)); this.request(); } + return this; }; -Request.prototype.build_path = function(build) { +Request.prototype.timeout = function(duration, callback) { + if (!this.requested) { + this.once('request', this.timeout.bind(this, duration, callback)); + return; + }; + + var self = this; + var emit = this.emit; + var timed_out = false; + + var timeout = setTimeout(function() { + timed_out = true; + if (typeof callback === 'function') callback(); + emit.call(self, 'timeout'); + }, duration); + + this.emit = function() { + if (timed_out) return; + else clearTimeout(timeout); + emit.apply(self, arguments); + }; + + return this; +}; + +Request.prototype.build_path = function (build) { if (build) { this.message.build_path = true; } @@ -84,7 +101,7 @@ Request.prototype.build_path = function(build) { return this; }; -Request.prototype.ledger_choose = function(current) { +Request.prototype.ledger_choose = function (current) { if (current) { this.message.ledger_index = this.remote._ledger_current_index; } else { @@ -97,7 +114,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; @@ -105,38 +122,40 @@ 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) { +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; + this.message.ledger_index = ledger_spec; } + break; } 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; @@ -145,23 +164,23 @@ 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) { +Request.prototype.secret = function (s) { if (s) { this.message.secret = s; } @@ -169,25 +188,25 @@ Request.prototype.secret = function(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), @@ -199,7 +218,7 @@ Request.prototype.ripple_state = function(account, issuer, currency) { return this; }; -Request.prototype.accounts = function(accounts, realtime) { +Request.prototype.accounts = function (accounts, realtime) { if (!Array.isArray(accounts)) { accounts = [ accounts ]; } @@ -208,21 +227,21 @@ Request.prototype.accounts = function(accounts, realtime) { 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++) { @@ -232,20 +251,20 @@ Request.prototype.books = function(books, snapshot) { 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']); + var obj = json[side] = { + currency: Currency.json_rewrite(book[side].currency) + }; + + if (obj.currency !== 'XRP') { + obj.issuer = UInt160.json_rewrite(book[side].issuer); } - - json[side] = obj; } processSide('taker_gets'); processSide('taker_pays'); - if (snapshot || book['snapshot']) json['snapshot'] = true; - if (book['both']) json['both'] = true; + if (snapshot) json.snapshot = true; + if (book.both) json.both = true; procBooks.push(json); } @@ -255,119 +274,89 @@ Request.prototype.books = function(books, snapshot) { return this; }; - +//------------------------------------------------------------------------------ /** Interface to manage the connection to a Ripple server. + This implementation uses WebSockets. - Configuration options: + Keys for opts: - + `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: - } + trusted : truthy, if remote is trusted + websocket_ip + websocket_port + websocket_ssl + trace + maxListeners + fee_cushion : Extra fee multiplier to account for async fee changes. Events: - - + '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 + 'connect' + 'connected' (DEPRECATED) + 'disconnect' + 'disconnected' (DEPRECATED) + 'state': + - '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. - + '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 + @param opts Connection options. + @param trace */ -var Remote = function(opts, trace) { +function Remote(opts, trace) { EventEmitter.call(this); var self = this; - this.trusted = opts.trusted; - 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.trusted = opts.trusted; + this.local_sequence = opts.local_sequence; // Locally track sequence numbers + this.local_fee = opts.local_fee; // Locally set fees + this.local_signing = (typeof opts.local_signing === 'undefined') ? 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 = 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 = void(0); - this.retry = void(0); - this._offline_queue = [ ]; + this.fee_cushion = (typeof opts.fee_cushion === 'undefined') + ? 1.5 : opts.fee_cushion; - this._load_base = 256; - this._load_factor = 1.0; - 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.id = 0; + this.trace = opts.trace || trace; + this._server_fatal = false; // True, if we know server exited. + 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._online_state = 'closed'; // 'open', 'closed', 'connecting', 'closing' + this.state = 'offline'; // 'online', 'offline' + this.retry_timer = void(0); + this.retry = void(0); - this._last_tx = null; + this._load_base = 256; + this._load_factor = 1.0; + 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._connected = false; + + this._last_tx = null; // 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._primary_server = void(0); + this._servers = [ ]; + this._primary_server = void(0); // Cache information for accounts. // DEPRECATED, will be removed @@ -399,56 +388,46 @@ var Remote = function(opts, trace) { } }; - // Support old API - if (!('servers' in opts)) { - opts.servers = [ { - host: opts.websocket_ip, - port: opts.websocket_port, - secure: opts.websocket_ssl, - trusted: opts.trusted - } ] + // Fallback for previous API + if (!opts.hasOwnProperty('servers')) { + opts.servers = [ + { + host: opts.websocket_ip, + port: opts.websocket_port, + secure: opts.websocket_ssl, + trusted: opts.trusted + } + ] } - // Initialize servers opts.servers.forEach(function(server) { - var i = typeof server.pool === 'number' ? server.pool : 1; - while (i--) { - self.add_server(server); - } + var i = Number(server.pool) || 1; + while (i--) { self.add_server(server); } }); - // This is used to remove EventEmitter warnings - if ('maxListeners' in opts) { - this._servers.concat(this).forEach(function(i) { - i.setMaxListeners(opts.maxListeners); - }); - } + // This is used to remove Node EventEmitter warnings + var maxListeners = opts.maxListeners || 0; + this._servers.concat(this).forEach(function(emitter) { + emitter.setMaxListeners(maxListeners); + }); - this.on('newListener', function(type, listener) { - if ('transaction_all' === type) { - if (!self._transaction_subs && self._online_state === 'open') { + this.on('newListener', function (type, listener) { + if (type === 'transaction_all') { + if (!self._transaction_subs && self._connected) { self.request_subscribe('transactions').request(); } self._transaction_subs += 1; } }); - this.on('removeListener', function(type, listener) { - if ('transaction_all' === type) { + this.on('removeListener', function (type, listener) { + if (type === 'transaction_all') { self._transaction_subs -= 1; - if (!self._transaction_subs && self._online_state === 'open') { + if (!self._transaction_subs && self._connected) { 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); @@ -456,14 +435,14 @@ 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) { +Remote.from_config = function (obj, trace) { var serverConfig = typeof obj === 'string' ? config.servers[obj] : obj; var remote = new Remote(serverConfig, trace); @@ -483,32 +462,35 @@ Remote.from_config = function(obj, trace) { return remote; }; -var isTemMalformed = function(engine_result_code) { +Remote.create_remote = function(options, callback) { + var remote = Remote.from_config(options); + remote.connect(callback); + return remote; +}; + +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(opts) { +Remote.prototype.add_server = function (opts) { var self = this; - var url = (opts.secure || opts.websocket_ssl ? 'wss://' : 'ws://') - + (opts.host || opts.websocket_ip) + ':' - + (opts.port || opts.websocket_port); + 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 }) + var server = new Server(this, {url: url}); - if ('maxListeners' in opts) { - server.setMaxListeners(opts.maxListeners); - } - - server.on('message', function(data) { + 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); } @@ -516,7 +498,7 @@ Remote.prototype.add_server = function(opts) { self._set_state('online'); }); - server.on('disconnect', function() { + server.on('disconnect', function () { self._connection_count--; if (!self._connection_count) { self._set_state('offline'); @@ -529,15 +511,13 @@ Remote.prototype.add_server = function(opts) { }; // 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; @@ -546,15 +526,15 @@ Remote.prototype._set_state = function(state) { switch (state) { case 'online': - this._online_state = 'open'; - this._connected = true; + this._online_state = 'open'; + this._connected = true; this.emit('connect'); this.emit('connected'); break; case 'offline': - this._online_state = 'closed'; - this._connected = false; + this._online_state = 'closed'; + this._connected = false; this.emit('disconnect'); this.emit('disconnected'); break; @@ -562,32 +542,33 @@ Remote.prototype._set_state = function(state) { } }; -Remote.prototype.set_trace = function(trace) { - this.trace = undefined === trace || trace; - +Remote.prototype.set_trace = function (trace) { + this.trace = trace === void(0) || trace; return this; }; /** * Connect to the Ripple network. */ -Remote.prototype.connect = function(online, callback) { - if (typeof online === 'function') { - callback = online; - online = void(0); - this.once('connect', callback); +Remote.prototype.connect = function (online) { + // Downwards compatibility + switch(typeof online) { + case 'undefined': + break; + case 'function': + this.once('connect', online); + break; + default: + if (!Boolean(online)) + return this.disconnect() + break; } - // Downwards compatibility - if (typeof online !== 'undefined' && !online) { - this.disconnect(); + if (!this._servers.length) { + throw new Error('No servers available.'); } else { - 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) { - var server = this._get_server(); - if (server) { - server.request(request); +Remote.prototype.request = function (request) { + if (!this._servers.length) { + request.emit('error', new Error('No servers available')); + } else if (!this._connected) { + this.once('connect', this.request.bind(this, request)); } else { - request.emit('error', new Error('No servers availale')); + var server = this._get_server(); + if (server) { + server.request(request); + } else { + request.emit('error', new Error('No servers available')); + } } }; -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, callback) { +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) { @@ -784,30 +758,31 @@ Remote.prototype.request_ledger = function(ledger, opts, callback) { request.message.ledger = ledger; } - if (typeof opts === 'object') { - if (opts.full) - request.message.full = true; - - if (opts.expand) - request.message.expand = true; - - if (opts.transactions) - request.message.transactions = true; - - if (opts.accounts) - request.message.accounts = true; - } - // DEPRECATED: - else if (opts) { - console.log('request_ledger: full parameter is deprecated'); - request.message.full = true; + switch(typeof opts) { + case 'object': + if (opts.full) request.message.full = true; + if (opts.expand) request.message.expand = true; + if (opts.transactions) request.message.transactions = true; + if (opts.accounts) request.message.accounts = true; + break; + case 'function': + callback = opts; + opts = void(0); + break; + default: + //DEPRECATED + console.log('request_ledger: full parameter is deprecated'); + request.message.full = true; + break; } - return request.callback(callback); + request.callback(callback); + + return request; }; // Only for unit testing. -Remote.prototype.request_ledger_hash = function(callback) { +Remote.prototype.request_ledger_hash = function (callback) { //utils.assert(this.trusted); // If not trusted, need to check proof. return new Request(this, 'ledger_closed').callback(callback); @@ -815,13 +790,13 @@ Remote.prototype.request_ledger_hash = function(callback) { // .ledger() // .ledger_index() -Remote.prototype.request_ledger_header = function(callback) { +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(callback) { +Remote.prototype.request_ledger_current = function (callback) { return new Request(this, 'ledger_current').callback(callback); }; @@ -829,7 +804,7 @@ Remote.prototype.request_ledger_current = function(callback) { // .ledger() // .ledger_index() // .offer_id() -Remote.prototype.request_ledger_entry = function(type, callback) { +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; @@ -839,11 +814,10 @@ Remote.prototype.request_ledger_entry = function(type, callback) { // If not found, listen, cache result, and emit it. // // Transparent caching: - 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 @@ -875,19 +849,18 @@ Remote.prototype.request_ledger_entry = function(type, callback) { }); bDefault = false; - } - else { - // Was not cached. + } 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) { + 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'); @@ -902,60 +875,71 @@ Remote.prototype.request_ledger_entry = function(type, callback) { } }; - return request.callback(callback); + request.callback(callback); + + return request; }; // .accounts(accounts, realtime) -Remote.prototype.request_subscribe = function(streams, callback) { +Remote.prototype.request_subscribe = function (streams, callback) { var request = new Request(this, 'subscribe'); if (streams) { request.message.streams = Array.isArray(streams) ? streams : [ streams ]; } - return request.callback(callback); + request.callback(callback); + + return request; }; -Remote.prototype.request_unsubscribe = function(streams, callback) { +// .accounts(accounts, realtime) +Remote.prototype.request_unsubscribe = function (streams, callback) { var request = new Request(this, 'unsubscribe'); if (streams) { request.message.streams = Array.isArray(streams) ? streams : [ streams ]; } - return request.callback(callback); + request.callback(callback); + + return request; }; // .ledger_choose() // .ledger_hash() // .ledger_index() -Remote.prototype.request_transaction_entry = function(hash, callback) { +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).callback(callback); + return (new Request(this, 'transaction_entry')) + .tx_hash(hash) + .callback(callback); }; // DEPRECATED: use request_transaction_entry -Remote.prototype.request_tx = function(hash, callback) { +Remote.prototype.request_tx = function (hash, callback) { var request = new Request(this, 'tx'); request.message.transaction = hash; + request.callback(callback); - return request.callback(callback); + return request; }; -Remote.prototype.request_account_info = function(accountID, callback) { +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); + request.callback(callback); - return request.callback(callback); + return request; }; // --> account_index: sub_account index (optional) // --> current: true, for the current ledger. -Remote.prototype.request_account_lines = function(accountID, account_index, current, callback) { +Remote.prototype.request_account_lines = function (accountID, account_index, current, callback) { // XXX Does this require the server to be trusted? //utils.assert(this.trusted); @@ -967,12 +951,15 @@ Remote.prototype.request_account_lines = function(accountID, account_index, curr request.message.index = account_index; } - return request.ledger_choose(current).callback(callback); + request.ledger_choose(current); + request.callback(callback); + + return request; }; // --> account_index: sub_account index (optional) // --> current: true, for the current ledger. -Remote.prototype.request_account_offers = function(accountID, account_index, current, callback) { +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); @@ -981,7 +968,10 @@ Remote.prototype.request_account_offers = function(accountID, account_index, cur request.message.index = account_index; } - return request.ledger_choose(current).callback(callback); + request.ledger_choose(current); + request.callback(callback); + + return request; }; @@ -996,7 +986,7 @@ Remote.prototype.request_account_offers = function(accountID, account_index, cur limit: integer // optional */ -Remote.prototype.request_account_tx = function(obj, callback) { +Remote.prototype.request_account_tx = function (obj, callback) { // XXX Does this require the server to be trusted? //utils.assert(this.trusted); @@ -1008,19 +998,21 @@ Remote.prototype.request_account_tx = function(obj, callback) { //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 (typeof obj.ledger_index_min !== 'undefined') {request.message.ledger_index_min = obj.ledger_index_min;} + if (typeof obj.ledger_index_max !== 'undefined') {request.message.ledger_index_max = obj.ledger_index_max;} + if (typeof obj.binary !== 'undefined') {request.message.binary = obj.binary;} + if (typeof obj.count !== 'undefined') {request.message.count = obj.count;} + if (typeof obj.descending !== 'undefined') {request.message.descending = obj.descending;} + if (typeof obj.offset !== 'undefined') {request.message.offset = obj.offset;} + if (typeof obj.limit !== 'undefined') {request.message.limit = obj.limit;} } - return request.callback(callback); + request.callback(callback); + + return request; }; -Remote.prototype.request_book_offers = function(gets, pays, taker, callback) { +Remote.prototype.request_book_offers = function (gets, pays, taker, callback) { var request = new Request(this, 'book_offers'); request.message.taker_gets = { @@ -1041,10 +1033,12 @@ Remote.prototype.request_book_offers = function(gets, pays, taker, callback) { request.message.taker = taker ? taker : UInt160.ACCOUNT_ONE; - return request.callback(callback); + request.callback(callback); + + return request; }; -Remote.prototype.request_wallet_accounts = function(seed, callback) { +Remote.prototype.request_wallet_accounts = function (seed, callback) { utils.assert(this.trusted); // Don't send secrets. var request = new Request(this, 'wallet_accounts'); @@ -1054,20 +1048,23 @@ Remote.prototype.request_wallet_accounts = function(seed, callback) { return request.callback(callback); }; -Remote.prototype.request_sign = function(secret, tx_json, callback) { +Remote.prototype.request_sign = function (secret, tx_json, callback) { utils.assert(this.trusted); // Don't send secrets. var request = new Request(this, 'sign'); request.message.secret = secret; request.message.tx_json = tx_json; - - return request.callback(callback); + request.callback(callback); + + return request; }; // Submit a transaction. -Remote.prototype.request_submit = function(callback) { - return new Request(this, 'submit').callback(callback); +Remote.prototype.request_submit = function (callback) { + var request = new Request(this, 'submit'); + request.callback(callback); + return request; }; // @@ -1082,20 +1079,22 @@ Remote.prototype.request_submit = function(callback) { * * This function will create and return the request, but not submit it. */ -Remote.prototype._server_prepare_subscribe = function(callback) { +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 request = this.request_subscribe(feeds); - request.on('success', function(message) { - self._stand_alone = !!message.stand_alone; - self._testnet = !!message.testnet; + request.on('success', function (message) { + self._stand_alone = !!message.stand_alone; + self._testnet = !!message.testnet; - if ('string' === typeof message.random) { + if (typeof message.random === 'string') { var rand = message.random.match(/[0-9A-F]{8}/ig); while (rand && rand.length) { sjcl.random.addEntropy(parseInt(rand.pop(), 16)); @@ -1107,7 +1106,6 @@ Remote.prototype._server_prepare_subscribe = function(callback) { self._ledger_time = message.ledger_time; self._ledger_hash = message.ledger_hash; self._ledger_current_index = message.ledger_index+1; - self.emit('ledger_closed', message); } @@ -1126,86 +1124,82 @@ Remote.prototype._server_prepare_subscribe = function(callback) { self.emit('prepare_subscribe', request); + request.callback(callback); + + // XXX Could give error events, maybe even time out. - return request.callback(callback); + return request; }; // 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(callback) { - 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) { var request = new Request(this, 'ledger_accept'); - request.callback(callback).request(); + request.request(); + request.callback(callback); } else { - var err = { 'error' : 'notStandAlone' } - if (typeof callback === 'function') { - callback(err); - } - this.emit('error', err); + this.emit('error', { + 'error' : 'notStandAlone' + }); } return this; }; // Return a request to refresh the account balance. -Remote.prototype.request_account_balance = function(account, current, callback) { +Remote.prototype.request_account_balance = function (account, current, callback) { var request = this.request_ledger_entry('account_root'); request.account_root(account) .ledger_choose(current) - .on('success', function(message) { + .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'); - } + request.callback(callback, 'account_balance'); return request; }; // Return a request to return the account flags. -Remote.prototype.request_account_flags = function(account, current, callback) { +Remote.prototype.request_account_flags = function (account, current, callback) { var request = this.request_ledger_entry('account_root'); request.account_root(account) .ledger_choose(current) - .on('success', function(message) { + .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'); - } + request.callback(callback, 'account_flags'); return request; }; // Return a request to emit the owner count. -Remote.prototype.request_owner_count = function(account, current, callback) { +Remote.prototype.request_owner_count = function (account, current, callback) { var request = this.request_ledger_entry('account_root'); request.account_root(account) .ledger_choose(current) - .on('success', function(message) { + .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'); - } + request.callback(callback, 'owner_count'); return request; }; -Remote.prototype.account = function(accountId) { - accountId = UInt160.json_rewrite(accountId); +Remote.prototype.account = function (accountId, callback) { + var accountId = UInt160.json_rewrite(accountId); if (!this._accounts[accountId]) { var account = new Account(this, accountId); @@ -1215,10 +1209,13 @@ Remote.prototype.account = function(accountId) { this._accounts[accountId] = account; } - return this._accounts[accountId]; + var account = this._accounts[accountId]; + + return account; }; -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; @@ -1227,7 +1224,10 @@ Remote.prototype.book = function(currency_gets, issuer_gets, currency_pays, issu 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; @@ -1235,11 +1235,11 @@ Remote.prototype.book = function(currency_gets, issuer_gets, currency_pays, issu } return this._books[key]; -}; +} // Return the next account sequence if possible. // <-- undefined or Sequence -Remote.prototype.account_seq = function(account, advance) { +Remote.prototype.account_seq = function (account, advance) { var account = UInt160.json_rewrite(account); var account_info = this.accounts[account]; var seq; @@ -1256,45 +1256,41 @@ Remote.prototype.account_seq = function(account, advance) { } return seq; -}; +} -Remote.prototype.set_account_seq = function(account, seq) { +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, callback) { - var self = this; - var request; +Remote.prototype.account_seq_cache = function (account, current, callback) { + var self = this; if (!self.accounts[account]) self.accounts[account] = {}; var account_info = self.accounts[account]; + var request = account_info.caching_seq_request; - request = account_info.caching_seq_request; if (!request) { // console.log('starting: %s', account); request = self.request_ledger_entry('account_root') .account_root(account) .ledger_choose(current) - .on('success', function(message) { + .on('success', function (message) { delete account_info.caching_seq_request; var seq = message.node.Sequence; - 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) { + .on('error', function (message) { // console.log('error: %s', account); delete account_info.caching_seq_request; @@ -1304,22 +1300,20 @@ Remote.prototype.account_seq_cache = function(account, current, callback) { account_info.caching_seq_request = request; } - if (typeof callback === 'function') { - request.callback(callback, 'success_account_seq_cache'); - } + request.callback(callback, 'success_account_seq_cache', 'error_account_seq_cache'); return request; }; // Mark an account's root node as dirty. -Remote.prototype.dirty_account_root = function(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; }; @@ -1332,13 +1326,12 @@ 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, callback) { - 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. - request - .ripple_state(account, issuer, currency) + return request.ripple_state(account, issuer, currency) .ledger_choose(current) - .on('success', function(message) { + .on('success', function (message) { var node = message.node; var lowLimit = Amount.from_json(node.LowLimit); @@ -1361,104 +1354,120 @@ Remote.prototype.request_ripple_balance = function(account, issuer, currency, cu 'account_quality_out' : ( accountHigh ? node.HighQualityOut : node.LowQualityOut), 'peer_quality_out' : (!accountHigh ? node.HighQualityOut : node.LowQualityOut), }); - }); - - if (typeof callback === 'function') { - request.callback(callback, 'ripple_state'); - } - - return request; + }) + .callback(callback, 'ripple_state'); }; -Remote.prototype.request_ripple_path_find = function(src_account, dst_account, dst_amount, src_currencies, callback) { +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); + 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 = opts.src_currencies.map(function(ci) { + if (src_currencies) { + request.message.source_currencies = src_currencies.map(function (ci) { var ci_new = {}; - if ('issuer' in ci) { + if ('issuer' in ci) ci_new.issuer = UInt160.json_rewrite(ci.issuer); - } - if ('currency' in ci) { + + if ('currency' in ci) ci_new.currency = Currency.json_rewrite(ci.currency); - } return ci_new; }); } - return request.callback(callback); + request.callback(callback); + + return request; }; -Remote.prototype.request_unl_list = function(callback) { - return new Request(this, 'unl_list').callback(callback); +Remote.prototype.request_unl_list = function (callback) { + var request = new Request(this, 'unl_list'); + request.callback(callback); + return request; }; -Remote.prototype.request_unl_add = function(addr, comment, 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; - switch (typeof comment) { - case 'string': - request.message.comment = comment; - break; - - case 'function': - callback = comment; - break; + if (comment) { + request.message.comment = note; } - return request.callback(callback); + request.callback(callback); + + return request; }; // --> node: | -Remote.prototype.request_unl_delete = function(node) { +Remote.prototype.request_unl_delete = function (node, callback) { var request = new Request(this, 'unl_delete'); - request.message.node = node; - - return request.callback(callback); + request.callback(callback); + return request; }; -Remote.prototype.request_peers = function(callback) { - return new Request(this, 'peers', callback); +Remote.prototype.request_peers = function (callback) { + var request = new Request(this, 'peers'); + request.callback(callback); + return request; }; -Remote.prototype.request_connect = function(ip, port, callback) { +Remote.prototype.request_connect = function (ip, port, callback) { var request = new Request(this, 'connect'); request.message.ip = ip; - if (typeof port !== 'undefined') { + if (port) { request.message.port = port; } - return request.callback(callback); + request.callback(callback); + + return request; }; -Remote.prototype.transaction = function() { +Remote.prototype.transaction = function () { return new Transaction(this); }; +/** + * Get the current recommended transaction fee unit. + * + * Multiply this value with the number of fee units in order to calculate the + * recommended fee for the transaction you are trying to submit. + * + * @return {Number} Recommended amount for one fee unit. + */ +Remote.prototype.fee_tx = function () +{ + var fee_unit = this._fee_base / this._fee_ref; + + // Apply load fees + fee_unit *= this._load_factor / this._load_base; + + // Apply fee cushion (a safety margin in case fees rise since we were last updated + fee_unit *= this.fee_cushion; + + return fee_unit; +}; + +/** + * Get the current recommended reserve base. + * + * Returns the base reserve with load fees and safety margin applied. + */ +Remote.prototype.fee_reserve_base = function () +{ + // XXX +}; + exports.Remote = Remote; // vim:sw=2:sts=2:ts=8:et diff --git a/src/js/ripple/serializedtypes.js b/src/js/ripple/serializedtypes.js index e402788c..5fe7383c 100644 --- a/src/js/ripple/serializedtypes.js +++ b/src/js/ripple/serializedtypes.js @@ -12,6 +12,7 @@ var extend = require('extend'), var amount = require('./amount'), UInt160 = amount.UInt160, + UInt256 = require('./uint256').UInt256, Amount = amount.Amount, Currency= amount.Currency; @@ -109,8 +110,8 @@ var STHash128 = exports.Hash128 = new SerializedType({ var STHash256 = exports.Hash256 = new SerializedType({ serialize: function (so, val) { - // XXX - throw new Error("Serializing Hash256 not implemented"); + var hash = UInt256.from_json(val); + this.serialize_hex(so, hash.to_hex()); }, parse: function (so) { // XXX diff --git a/src/js/ripple/server.js b/src/js/ripple/server.js index 26f146a2..a9275f5d 100644 --- a/src/js/ripple/server.js +++ b/src/js/ripple/server.js @@ -1,20 +1,17 @@ var EventEmitter = require('events').EventEmitter; var util = require('util'); -var WebSocket = require('ws'); var utils = require('./utils'); /** - * Server + * @constructor Server + * @param remote The Remote object + * @param cfg Configuration parameters. * - * Options must contain `url` to - * WebSocket server - * - * @constructor - * @param {Object} remote - * @param {Object} opts - */ + * Keys for cfg: + * url + */ -function Server(remote, opts) { +var Server = function (remote, opts) { EventEmitter.call(this); if (typeof opts !== 'object' || typeof opts.url !== 'string') { @@ -53,8 +50,7 @@ util.inherits(Server, EventEmitter); * Our requirements are that the server can process transactions and notify * us of changes. */ - -Server.online_states = [ +Server.online_states = [ 'syncing' , 'tracking' , 'proposing' @@ -62,24 +58,27 @@ Server.online_states = [ , 'full' ]; -/** - * Determine if a server status qualifies - * as 'online' - * - * @param {String} status - * @return {Boolean} - * @api private - */ - -Server.prototype.is_online = function(status) { +Server.prototype._is_online = function (status) { return Server.online_states.indexOf(status) !== -1; }; -/** - * Connect to WebSocket server - */ +Server.prototype._set_state = function (state) { + if (state !== this._state) { + this._state = state; -Server.prototype.connect = function() { + 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.connect = function () { var self = this; // We don't connect if we believe we're already connected. This means we have @@ -88,22 +87,21 @@ Server.prototype.connect = function() { // we will automatically reconnect. if (this._connected === true) return; - if (this._remote.trace) { - console.log('server: connect: %s', this._opts.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(); + // We require this late, because websocket shims may be loaded after + // ripple-lib. + var WebSocket = require('ws'); 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; @@ -114,13 +112,11 @@ 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 // code 1006. This is done for security purposes and therefore unlikely to @@ -140,13 +136,11 @@ 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(); }; @@ -156,17 +150,16 @@ Server.prototype.connect = function() { self._set_state('offline'); // Prevent additional events from this socket - ws.removeAllListeners(); - ws.on('error', function() {}); + ws.onopen = ws.onerror = ws.onclose = ws.onmessage = function () {}; // Should we be connected? if (!self._should_connect) return; // Delay and retry. - self._retry += 1; - - self._retry_timer = setTimeout(function retryTimeout() { + self._retry += 1; + self._retry_timer = setTimeout(function () { if (self._remote.trace) console.log('server: retry'); + if (!self._should_connect) return; self.connect(); }, self._retry < 40 @@ -176,20 +169,14 @@ 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); }; }; -/** - * Disconnect from WebSocket server - * - * @api public - */ - -Server.prototype.disconnect = function() { +Server.prototype.disconnect = function () { this._should_connect = false; this._set_state('offline'); if (this._ws) { @@ -197,27 +184,14 @@ Server.prototype.disconnect = function() { } }; -/** - * 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)); - } +Server.prototype.send_message = function (message) { + 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. @@ -229,19 +203,19 @@ Server.prototype.request = function(request) { // Advance message ID self._id++; - if (self._connected || (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); } - self.send(request.message); + + self.send_message(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); } - self.send(request.message); + self.send_message(request.message); }); } } else { @@ -251,111 +225,56 @@ Server.prototype.request = function(request) { } }; -/** - * Set server state - * - * Examples: - * - * set_state('online') - * set_state('offline') - * - * @param {String} state - * @api private - */ +Server.prototype._handle_message = function (json) { + var self = this; -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'); - } - } -}; - -/** - * Handle WebSocket message - * - * @param {String} json - * @api private - */ - -Server.prototype._handle_message = function(json) { - var self = this; - var unexpected = false; var message; - try { message = JSON.parse(json); } catch(exception) { } + try { + message = JSON.parse(json); + } catch(exception) { return; } - var unexpected = typeof message !== 'object' || typeof message.type !== 'string'; + switch(message.type) { + case 'response': + // A response to a request. + var request = self._requests[message.id]; - if (unexpected) { - // We received a malformed response from the server - } + delete self._requests[message.id]; - if (!unexpected) { - switch (message.type) { - case 'response': - // A response to a request. - var request = self._requests[message.id]; + 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); - delete self._requests[message.id]; + request.emit('success', message.result); - 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); - } + [ 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('success', message.result); + request.emit('error', { + 'error' : 'remoteError', + 'error_message' : 'Remote reported an error.', + 'remote' : message + }); + } + break; - [ 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; - } + case 'serverStatus': + // This message is only received when online. As we are connected, it is the definative final state. + self._set_state(self._is_online(message.server_status) ? 'online' : 'offline'); + break; } }; -/** - * Handle subscribe response - * - * @param {Object} message - * @api private - */ - -Server.prototype._handle_response_subscribe = function(message) { +Server.prototype._handle_response_subscribe = function (message) { var self = this; self._server_status = message.server_status; - if (self.is_online(message.server_status)) { + 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 6b2531ff..8cabe68e 100644 --- a/src/js/ripple/transaction.js +++ b/src/js/ripple/transaction.js @@ -72,43 +72,44 @@ var Transaction = function (remote) { this.remote = remote; this._secret = undefined; this._build_path = false; - this.tx_json = { // Transaction data. - 'Flags' : 0, // XXX Would be nice if server did not require this. + + // Transaction data. + this.tx_json = { + 'Flags' : 0, // XXX Would be nice if server did not require this. }; + this.hash = undefined; this.submit_index = undefined; // ledger_current_index was this when transaction was submited. this.state = undefined; // Under construction. this.finalized = false; this.on('success', function (message) { - if (message.engine_result) { - self.hash = message.tx_json.hash; + if (message.engine_result) { + self.hash = message.tx_json.hash; - self.set_state('client_proposed'); + self.set_state('client_proposed'); - self.emit('proposed', { - 'tx_json' : message.tx_json, - 'result' : message.engine_result, - 'result_code' : message.engine_result_code, - 'result_message' : message.engine_result_message, - 'rejected' : self.isRejected(message.engine_result_code), // If server is honest, don't expect a final if rejected. - }); - } - }); + self.emit('proposed', { + 'tx_json' : message.tx_json, + 'result' : message.engine_result, + 'result_code' : message.engine_result_code, + 'result_message' : message.engine_result_message, + 'rejected' : self.isRejected(message.engine_result_code), // If server is honest, don't expect a final if rejected. + }); + } + }); this.on('error', function (message) { - // Might want to give more detailed information. - self.set_state('remoteError'); - }); + // Might want to give more detailed information. + self.set_state('remoteError'); + }); }; util.inherits(Transaction, EventEmitter); // XXX This needs to be determined from the network. Transaction.fees = { - 'default' : Amount.from_json("10"), - 'nickname_create' : Amount.from_json("1000"), - 'offer' : Amount.from_json("10"), + 'default' : 10, }; Transaction.flags = { @@ -194,15 +195,17 @@ Transaction.prototype.set_state = function (state) { Transaction.prototype.complete = function () { var tx_json = this.tx_json; - if (undefined === tx_json.Fee && this.remote.local_fee) { - tx_json.Fee = Transaction.fees['default'].to_json(); + if ("undefined" === typeof tx_json.Fee && this.remote.local_fee) { + this.tx_json.Fee = "" + Math.ceil(this.remote.fee_tx() * this.fee_units()); } - if (undefined === tx_json.SigningPubKey && (!this.remote || this.remote.local_signing)) { + if ("undefined" === typeof tx_json.SigningPubKey && (!this.remote || this.remote.local_signing)) { var seed = Seed.from_json(this._secret); var key = seed.get_key(this.tx_json.Account); tx_json.SigningPubKey = key.to_hex_pub(); } + + return this.tx_json; }; Transaction.prototype.serialize = function () { @@ -211,23 +214,28 @@ Transaction.prototype.serialize = function () { Transaction.prototype.signing_hash = function () { var prefix = config.testnet - ? Transaction.HASH_SIGN_TESTNET - : Transaction.HASH_SIGN; + ? Transaction.HASH_SIGN_TESTNET + : Transaction.HASH_SIGN; return SerializedObject.from_json(this.tx_json).signing_hash(prefix); }; Transaction.prototype.sign = function () { - var seed = Seed.from_json(this._secret), - hash = this.signing_hash(); - - var key = seed.get_key(this.tx_json.Account), - sig = key.sign(hash, 0), - hex = sjcl.codec.hex.fromBits(sig).toUpperCase(); + var seed = Seed.from_json(this._secret); + var hash = this.signing_hash(); + var key = seed.get_key(this.tx_json.Account); + var sig = key.sign(hash, 0); + var hex = sjcl.codec.hex.fromBits(sig).toUpperCase(); this.tx_json.TxnSignature = hex; }; +Transaction.prototype._hasTransactionListeners = function() { + return this.listeners('final').length + || this.listeners('lost').length + || this.listeners('pending').length +}; + // Submit a transaction to the network. // XXX Don't allow a submit without knowing ledger_index. // XXX Have a network canSubmit(), post events for following. @@ -242,18 +250,25 @@ Transaction.prototype.sign = function () { // case 'tejLost': locally gave up looking // default: some other TER // } + Transaction.prototype.submit = function (callback) { var self = this; var tx_json = this.tx_json; - this.callback = callback; + this.callback = typeof callback === 'function' + ? callback + : function(){}; - if ('string' !== typeof tx_json.Account) - { - (this.callback || this.emit)('error', { - 'error' : 'tejInvalidAccount', - 'error_message' : 'Bad account.' - }); + function finish(err) { + self.emit('error', err); + self.callback('error', err); + } + + if (typeof tx_json.Account !== 'string') { + finish({ + 'error' : 'tejInvalidAccount', + 'error_message' : 'Bad account.' + }); return this; } @@ -261,143 +276,142 @@ Transaction.prototype.submit = function (callback) { this.complete(); - if (this.callback || this.listeners('final').length || this.listeners('lost').length || this.listeners('pending').length) { - // There are listeners for callback, 'final', 'lost', or 'pending' arrange to emit them. + //console.log('Callback or has listeners'); - this.submit_index = this.remote._ledger_current_index; + // There are listeners for callback, 'final', 'lost', or 'pending' arrange to emit them. - // When a ledger closes, look for the result. - var on_ledger_closed = function (message) { - var ledger_hash = message.ledger_hash; - var ledger_index = message.ledger_index; - var stop = false; + this.submit_index = this.remote._ledger_current_index; -// XXX make sure self.hash is available. - self.remote.request_transaction_entry(self.hash) - .ledger_hash(ledger_hash) - .on('success', function (message) { - if (self.finalized) return; + // When a ledger closes, look for the result. + function on_ledger_closed(message) { + if (self.finalized) return; - self.set_state(message.metadata.TransactionResult); - self.remote.removeListener('ledger_closed', on_ledger_closed); - self.emit('final', message); - self.finalized = true; + var ledger_hash = message.ledger_hash; + var ledger_index = message.ledger_index; + var stop = false; - if (self.callback) - self.callback(message.metadata.TransactionResult, message); - }) - .on('error', function (message) { - if (self.finalized) return; + // XXX make sure self.hash is available. + var transaction_entry = self.remote.request_transaction_entry(self.hash) - if ('remoteError' === message.error - && 'transactionNotFound' === message.remote.error) { - if (self.submit_index + SUBMIT_LOST < ledger_index) { - self.set_state('client_lost'); // Gave up. - self.emit('lost'); + transaction_entry.ledger_hash(ledger_hash) - if (self.callback) - self.callback('tejLost', message); + transaction_entry.on('success', function (message) { + if (self.finalized) return; + self.set_state(message.metadata.TransactionResult); + self.remote.removeListener('ledger_closed', on_ledger_closed); + self.emit('final', message); + self.finalized = true; + self.callback(message.metadata.TransactionResult, message); + }); - self.remote.removeListener('ledger_closed', on_ledger_closed); - self.emit('final', message); - self.finalized = true; - } - else if (self.submit_index + SUBMIT_MISSING < ledger_index) { - self.set_state('client_missing'); // We don't know what happened to transaction, still might find. - self.emit('pending'); - } - else { - self.emit('pending'); - } - } - // XXX Could log other unexpectedness. - }) - .request(); - }; + transaction_entry.on('error', function (message) { + if (self.finalized) return; - this.remote.on('ledger_closed', on_ledger_closed); + if (message.error === 'remoteError' && message.remote.error === 'transactionNotFound') { + if (self.submit_index + SUBMIT_LOST < ledger_index) { + self.set_state('client_lost'); // Gave up. + self.emit('lost'); + self.callback('tejLost', message); + self.remote.removeListener('ledger_closed', on_ledger_closed); + self.emit('final', message); + self.finalized = true; + } else if (self.submit_index + SUBMIT_MISSING < ledger_index) { + self.set_state('client_missing'); // We don't know what happened to transaction, still might find. + self.emit('pending'); + } else { + self.emit('pending'); + } + } + // XXX Could log other unexpectedness. + }); - if (this.callback) { - this.on('error', function (message) { - self.callback(message.error, message); - }); - } - } + transaction_entry.request(); + }; + + this.remote.on('ledger_closed', on_ledger_closed); + + this.once('error', function (message) { + self.callback(message.error, message); + }); this.set_state('client_submitted'); if (self.remote.local_sequence && !self.tx_json.Sequence) { - self.tx_json.Sequence = this.remote.account_seq(self.tx_json.Account, 'ADVANCE'); + + self.tx_json.Sequence = this.remote.account_seq(self.tx_json.Account, 'ADVANCE'); // console.log("Sequence: %s", self.tx_json.Sequence); if (!self.tx_json.Sequence) { + //console.log('NO SEQUENCE'); + // Look in the last closed ledger. - this.remote.account_seq_cache(self.tx_json.Account, false) + var account_seq = this.remote.account_seq_cache(self.tx_json.Account, false) + + account_seq.on('success_account_seq_cache', function () { + // Try again. + self.submit(); + }) + + account_seq.on('error_account_seq_cache', function (message) { + // XXX Maybe be smarter about this. Don't want to trust an untrusted server for this seq number. + // Look in the current ledger. + self.remote.account_seq_cache(self.tx_json.Account, 'CURRENT') .on('success_account_seq_cache', function () { // Try again. self.submit(); }) .on('error_account_seq_cache', function (message) { - // XXX Maybe be smarter about this. Don't want to trust an untrusted server for this seq number. - - // Look in the current ledger. - self.remote.account_seq_cache(self.tx_json.Account, 'CURRENT') - .on('success_account_seq_cache', function () { - // Try again. - self.submit(); - }) - .on('error_account_seq_cache', function (message) { - // Forward errors. - self.emit('error', message); - }) - .request(); + // Forward errors. + self.emit('error', message); }) .request(); + }) + + account_seq.request(); + return this; } // If the transaction fails we want to either undo incrementing the sequence // or submit a noop transaction to consume the sequence remotely. - this.on('success', function (res) { - if (!res || "string" !== typeof res.engine_result) return; + this.once('success', function (res) { + if (typeof res.engine_result === 'string') { + switch (res.engine_result.slice(0, 3)) { + // Synchronous local error + case 'tej': + self.remote.account_seq(self.tx_json.Account, 'REWIND'); + break; - switch (res.engine_result.slice(0, 3)) { - // Synchronous local error - case 'tej': - self.remote.account_seq(self.tx_json.Account, 'REWIND'); - break; - // XXX: What do we do in case of ter? - case 'tel': - case 'tem': - case 'tef': - // XXX Once we have a transaction submission manager class, we can - // check if there are any other transactions pending. If there are, - // we should submit a dummy transaction to ensure those - // transactions are still valid. - //var noop = self.remote.transaction().account_set(self.tx_json.Account); - //noop.submit(); + case 'ter': + // XXX: What do we do in case of ter? + break; - // XXX Hotfix. This only works if no other transactions are pending. - self.remote.account_seq(self.tx_json.Account, 'REWIND'); - break; + case 'tel': + case 'tem': + case 'tef': + // XXX Once we have a transaction submission manager class, we can + // check if there are any other transactions pending. If there are, + // we should submit a dummy transaction to ensure those + // transactions are still valid. + //var noop = self.remote.transaction().account_set(self.tx_json.Account); + //noop.submit(); + + // XXX Hotfix. This only works if no other transactions are pending. + self.remote.account_seq(self.tx_json.Account, 'REWIND'); + break; + } } }); } // Prepare request - var request = this.remote.request_submit(); - // Forward successes and errors. - request.on('success', function (message) { - self.emit('success', message); - }); - request.on('error', function (message) { - self.emit('error', message); - }); + // Forward events + request.emit = this.emit.bind(this); if (!this._secret && !this.tx_json.Signature) { - this.emit('error', { + finish({ 'result' : 'tejSecretUnknown', 'result_message' : "Could not sign transactions because we." }); @@ -407,11 +421,10 @@ Transaction.prototype.submit = function (callback) { request.tx_blob(this.serialize().to_hex()); } else { if (!this.remote.trusted) { - this.emit('error', { + finish({ 'result' : 'tejServerUntrusted', 'result_message' : "Attempt to give a secret to an untrusted server." }); - return this; } request.secret(this._secret); @@ -440,8 +453,9 @@ Transaction.prototype.build_path = function (build) { // tag should be undefined or a 32 bit integer. // YYY Add range checking for tag. Transaction.prototype.destination_tag = function (tag) { - if (undefined !== tag) - this.tx_json.DestinationTag = tag; + if (tag !== undefined) { + this.tx_json.DestinationTag = tag; + } return this; } @@ -491,8 +505,9 @@ Transaction.prototype.secret = function (secret) { } Transaction.prototype.send_max = function (send_max) { - if (send_max) - this.tx_json.SendMax = Amount.json_rewrite(send_max); + if (send_max) { + this.tx_json.SendMax = Amount.json_rewrite(send_max); + } return this; } @@ -500,8 +515,9 @@ Transaction.prototype.send_max = function (send_max) { // tag should be undefined or a 32 bit integer. // YYY Add range checking for tag. Transaction.prototype.source_tag = function (tag) { - if (undefined !== tag) - this.tx_json.SourceTag = tag; + if (tag) { + this.tx_json.SourceTag = tag; + } return this; } @@ -510,8 +526,9 @@ Transaction.prototype.source_tag = function (tag) { Transaction.prototype.transfer_rate = function (rate) { this.tx_json.TransferRate = Number(rate); - if (this.tx_json.TransferRate < 1e9) - throw 'invalidTransferRate'; + if (this.tx_json.TransferRate < 1e9) { + throw new Error('invalidTransferRate'); + } return this; } @@ -520,24 +537,26 @@ Transaction.prototype.transfer_rate = function (rate) { // --> flags: undefined, _flag_, or [ _flags_ ] Transaction.prototype.set_flags = function (flags) { if (flags) { - var transaction_flags = Transaction.flags[this.tx_json.TransactionType]; + var transaction_flags = Transaction.flags[this.tx_json.TransactionType]; - if (undefined == this.tx_json.Flags) // We plan to not define this field on new Transaction. - this.tx_json.Flags = 0; + // We plan to not define this field on new Transaction. + if (this.tx_json.Flags === undefined) { + this.tx_json.Flags = 0; + } - var flag_set = 'object' === typeof flags ? flags : [ flags ]; + var flag_set = Array.isArray(flags) ? flags : [ flags ]; - for (var index in flag_set) { - if (!flag_set.hasOwnProperty(index)) continue; + for (var index in flag_set) { + if (!flag_set.hasOwnProperty(index)) continue; - var flag = flag_set[index]; + var flag = flag_set[index]; - if (flag in transaction_flags) { - this.tx_json.Flags += transaction_flags[flag]; - } else { - // XXX Immediately report an error or mark it. - } + if (flag in transaction_flags) { + this.tx_json.Flags += transaction_flags[flag]; + } else { + // XXX Immediately report an error or mark it. } + } } return this; @@ -597,17 +616,15 @@ Transaction.prototype.offer_create = function (src, taker_pays, taker_gets, expi this.tx_json.TakerPays = Amount.json_rewrite(taker_pays); this.tx_json.TakerGets = Amount.json_rewrite(taker_gets); - if (this.remote.local_fee) { - this.tx_json.Fee = Transaction.fees.offer.to_json(); + if (expiration) { + this.tx_json.Expiration = expiration instanceof Date + ? expiration.getTime() + : Number(expiration); } - if (expiration) - this.tx_json.Expiration = Date === expiration.constructor - ? expiration.getTime() - : Number(expiration); - - if (cancel_sequence) - this.tx_json.OfferSequence = Number(cancel_sequence); + if (cancel_sequence) { + this.tx_json.OfferSequence = Number(cancel_sequence); + } return this; }; @@ -664,7 +681,7 @@ Transaction.prototype.ripple_line_set = function (src, limit, quality_in, qualit this.tx_json.Account = UInt160.json_rewrite(src); // Allow limit of 0 through. - if (undefined !== limit) + if (limit !== undefined) this.tx_json.LimitAmount = Amount.json_rewrite(limit); if (quality_in) @@ -689,6 +706,20 @@ Transaction.prototype.wallet_add = function (src, amount, authorized_key, public return this; }; +/** + * Returns the number of fee units this transaction will cost. + * + * Each Ripple transaction based on its type and makeup costs a certain number + * of fee units. The fee units are calculated on a per-server basis based on the + * current load on both the network and the server. + * + * @see https://ripple.com/wiki/Transaction_Fee + */ +Transaction.prototype.fee_units = function () +{ + return Transaction.fees["default"]; +}; + exports.Transaction = Transaction; // vim:sw=2:sts=2:ts=8:et