// Remote access to a server. // - We never send binary data. // - We use the W3C interface for node and browser compatibility: // http://www.w3.org/TR/websockets/#the-websocket-interface // // This class is intended for both browser and node.js use. // // This class is designed to work via peer protocol via either the public or // private websocket interfaces. The JavaScript class for the peer protocol // has not yet been implemented. However, this class has been designed for it // to be a very simple drop option. // // YYY Will later provide js/network.js which will transparently use multiple // instances of this class for network access. // // npm var EventEmitter = require('events').EventEmitter; var util = require('util'); var Request = require('./request').Request; var Server = require('./server').Server; var Amount = require('./amount').Amount; var Currency = require('./currency').Currency; var UInt160 = require('./uint160').UInt160; var Transaction = require('./transaction').Transaction; var Account = require('./account').Account; var Meta = require('./meta').Meta; var OrderBook = require('./orderbook').OrderBook; var PathFind = require('./pathfind').PathFind; var RippleError = require('./rippleerror').RippleError; var utils = require('./utils'); var config = require('./config'); var sjcl = require('./utils').sjcl; /** Interface to manage the connection to a Ripple server. This implementation uses WebSockets. Keys for opts: trace max_listeners : Set maxListeners for remote; prevents EventEmitter warnings connection_offset : Connect to remote servers on supplied interval (in seconds) trusted : truthy, if remote is trusted max_fee : Maximum acceptable transaction fee fee_cushion : Extra fee multiplier to account for async fee changes. servers : Array of server objects with the following 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. 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 */ function Remote(opts, trace) { EventEmitter.call(this); var self = this; this.trusted = Boolean(opts.trusted); this.local_sequence = Boolean(opts.local_sequence); // Locally track sequence numbers this.local_fee = (typeof opts.local_fee === 'undefined') ? true : Boolean(opts.local_fee); // Locally set fees this.local_signing = (typeof opts.local_signing === 'undefined') ? true : Boolean(opts.local_signing); this.fee_cushion = (typeof opts.fee_cushion === 'undefined') ? 1.5 : Number(opts.fee_cushion); this.max_fee = (typeof opts.max_fee === 'undefined') ? Infinity : Number(opts.max_fee); this.id = 0; this.trace = Boolean(opts.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._load_base = 256; this._load_factor = 256; this._fee_ref = 10; this._fee_base = 10; this._reserve_base = void(0); this._reserve_inc = void(0); this._connection_count = 0; this._connected = false; this._connection_offset = 1000 * (Number(opts.connection_offset) || 5); this._submission_timeout = 1000 * (Number(opts.submission_timeout) || 10); this._received_tx = { }; this._cur_path_find = null; // Local signing implies local fees and sequences if (this.local_signing) { this.local_sequence = true; this.local_fee = true; } this._servers = [ ]; this._primary_server = void(0); // Cache information for accounts. // DEPRECATED, will be removed this.accounts = { // Consider sequence numbers stable if you know you're not generating bad transactions. // Otherwise, clear it to have it automatically refreshed from the network. // account : { seq : __ } }; // Account objects by AccountId. this._accounts = { }; // OrderBook objects this._books = { }; // Secrets that we know about. this.secrets = { // Secrets can be set by calling set_secret(account, secret). // account : secret }; // Cache for various ledgers. // XXX Clear when ledger advances. this.ledgers = { current : { account_root : {} } }; // 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 } ]; } opts.servers.forEach(function(server) { var pool = Number(server.pool) || 1; while (pool--) { self.add_server(server); }; }); // This is used to remove Node EventEmitter warnings var maxListeners = opts.maxListeners || opts.max_listeners || 0; this._servers.concat(this).forEach(function(emitter) { emitter.setMaxListeners(maxListeners); }); function listener_added(type, listener) { if (type === 'transaction_all') { if (!self._transaction_subs && self._connected) { self.request_subscribe('transactions').request(); } self._transaction_subs += 1; } } function listener_removed(type, listener) { if (type === 'transaction_all') { self._transaction_subs -= 1; if (!self._transaction_subs && self._connected) { self.request_unsubscribe('transactions').request(); } } } this.on('newListener', listener_added); this.on('removeListener', listener_removed); } 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 } }; function isTemMalformed(engine_result_code) { return (engine_result_code >= -299 && engine_result_code < 199); }; function isTefFailure(engine_result_code) { return (engine_result_code >= -299 && engine_result_code < 199); }; Remote.from_config = function (obj, trace) { var serverConfig = typeof obj === 'string' ? config.servers[obj] : obj; var remote = new Remote(serverConfig, trace); function initialize_account(account) { var accountInfo = config.accounts[account]; if (typeof accountInfo === 'object') { if (accountInfo.secret) { // Index by nickname ... remote.set_secret(account, accountInfo.secret); // ... and by account ID remote.set_secret(accountInfo.account, accountInfo.secret); } } } if (typeof config.accounts === 'object') { for (var account in config.accounts) { initialize_account(account); } } return remote; }; Remote.create_remote = function(options, callback) { var remote = Remote.from_config(options); remote.connect(callback); return remote; }; Remote.prototype.add_server = function (opts) { var self = this; var server = new Server(this, { host : opts.host || opts.websocket_ip, port : opts.port || opts.websocket_port, secure : opts.secure || opts.websocket_ssl }); function server_message(data) { self._handle_message(data, server); } function server_connect() { self._connection_count++; self._set_state('online'); if (opts.primary || !self._primary_server) { self._set_primary_server(server); } if (self._connection_count === self._servers.length) { self.emit('ready'); } } function server_disconnect() { self._connection_count--; if (!self._connection_count) { self._set_state('offline'); } } server.on('message', server_message); server.on('connect', server_connect); server.on('disconnect', server_disconnect); this._servers.push(server); return this; }; // Inform remote that the remote server is not comming back. Remote.prototype.server_fatal = function () { this._server_fatal = true; }; // Set the emitted state: 'online' or 'offline' Remote.prototype._set_state = function (state) { this._trace('remote: set_state: %s', state); if (this.state !== state) { this.state = state; this.emit('state', state); switch (state) { case 'online': this._online_state = 'open'; this._connected = true; this.emit('connect'); this.emit('connected'); break; case 'offline': this._online_state = 'closed'; this._connected = false; this.emit('disconnect'); this.emit('disconnected'); break; } } }; Remote.prototype.set_trace = function (trace) { this.trace = trace === void(0) || trace; return this; }; Remote.prototype._trace = function() { if (this.trace) { utils.logObject.apply(utils, arguments); } }; /** * Connect to the Ripple network. */ Remote.prototype.connect = function (online) { if (!this._servers.length) { throw new Error('No servers available.'); } switch (typeof online) { case 'undefined': break; case 'function': this.once('connect', online); break; default: // Downwards compatibility if (!Boolean(online)) { return this.disconnect(); } } var self = this; ;(function next_server(i) { var server = self._servers[i]; server.connect(); server._sid = ++i; if (i < self._servers.length) { setTimeout(function() { next_server(i); }, self._connection_offset); } })(0); return this; }; /** * Disconnect from the Ripple network. */ Remote.prototype.disconnect = function (online) { if (!this._servers.length) { throw new Error('No servers available, not disconnecting'); } this._servers.forEach(function(server) { server.disconnect(); }); this._set_state('offline'); return this; }; // It is possible for messages to be dispatched after the connection is closed. Remote.prototype._handle_message = function (message, server) { var self = this; try { message = JSON.parse(message); } catch(e) { } var unexpected = typeof message !== 'object' || typeof message.type !== 'string'; if (unexpected) { // Unexpected response from remote. this.emit('error', new RippleError('remoteUnexpected', 'Unexpected response from remote')); return; } switch (message.type) { case 'response': // Handled by the server that sent the request break; case 'ledgerClosed': // XXX If not trusted, need to verify we consider ledger closed. // XXX Also need to consider a slow server or out of order response. // XXX Be more defensive fields could be missing or of wrong type. // YYY Might want to do some cache management. this._ledger_time = message.ledger_time; this._ledger_hash = message.ledger_hash; this._ledger_current_index = message.ledger_index + 1; this.emit('ledger_closed', message, server); break; case 'transaction': // To get these events, just subscribe to them. A subscribes and // unsubscribes will be added as needed. // XXX If not trusted, need proof. // De-duplicate transactions that are immediately following each other var hash = message.transaction.hash; if (this._received_tx.hasOwnProperty(hash)) { break; } this._received_tx[hash] = true; this._trace('remote: tx: %s', message); // Process metadata message.mmeta = new Meta(message.meta); // Pass the event on to any related Account objects message.mmeta.getAffectedAccounts().forEach(function(account) { account = self._accounts[account]; if (account) account.notify(message); }); // Pass the event on to any related OrderBooks message.mmeta.getAffectedBooks().forEach(function(book) { book = self._books[book]; if (book) book.notify(message); }); this.emit('transaction', message); this.emit('transaction_all', message); break; case 'path_find': // Pass the event to the currently open PathFind object if (this._cur_path_find) { this._cur_path_find.notify_update(message); } this.emit('path_find_all', message); break; case 'serverStatus': self.emit('server_status', message); var load_changed = message.hasOwnProperty('load_base') && message.hasOwnProperty('load_factor') && (message.load_base !== self._load_base || message.load_factor !== self._load_factor) ; if (load_changed) { self._load_base = message.load_base; self._load_factor = message.load_factor; var obj = { load_base: self._load_base, load_factor: self._load_factor, fee_units: self.fee_tx_unit() } self.emit('load', obj); self.emit('load_changed', obj); } break; // All other messages default: this._trace('remote: ' + message.type + ': %s', message); this.emit('net_' + message.type, message); break; } }; Remote.prototype.ledger_hash = function () { return this._ledger_hash; }; Remote.prototype._set_primary_server = function (server) { if (this._primary_server) { this._primary_server._primary = false; } this._primary_server = server; this._primary_server._primary = true; }; Remote.prototype._server_is_available = function (server) { return server && server._connected; }; Remote.prototype._next_server = function () { var result = null; for (var i=0; i request: what to send, consumed. 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 if (request.server === null) { this.emit('error', new Error('Server does not exist')); } else { var server = request.server || this._get_server(); if (server) { server.request(request); } else { request.emit('error', new Error('No servers available')); } } }; 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) { //utils.assert(this.trusted); var request = new Request(this, 'ledger'); if (ledger) { // DEPRECATED: use .ledger_hash() or .ledger_index() //console.log('request_ledger: ledger parameter is deprecated'); request.message.ledger = ledger; } var props = [ 'full' , 'expand' , 'transactions' , 'accounts' ]; switch (typeof opts) { case 'object': for (var key in opts) { if (~props.indexOf(key)) { request.message[key] = true; } } break; case 'function': callback = opts; opts = void(0); break; default: //DEPRECATED this._trace('request_ledger: full parameter is deprecated'); request.message.full = true; break; } request.callback(callback); return request; }; // Only for unit testing. 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); }; // .ledger() // .ledger_index() 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) { 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, callback) { //utils.assert(this.trusted); // If not trusted, need to check proof, maybe talk packet protocol. var self = this; var request = new Request(this, 'ledger_entry'); // Transparent caching. When .request() is invoked, look in the Remote object for the result. // 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. var bDefault = true; // .self = Remote // this = Request // console.log('request_ledger_entry: caught'); //if (self._ledger_hash) { // A specific ledger is requested. // XXX Add caching. // else if (req.ledger_index) // else if ('ripple_state' === request.type) // YYY Could be cached per ledger. //} if (!self._ledger_hash && type === 'account_root') { var cache = self.ledgers.current.account_root; if (!cache) { cache = self.ledgers.current.account_root = {}; } var node = self.ledgers.current.account_root[request.message.account_root]; if (node) { // Emulate fetch of ledger entry. // console.log('request_ledger_entry: emulating'); // YYY Missing lots of fields. request.emit('success', { 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.once('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'); } } } if (bDefault) { // console.log('request_ledger_entry: invoking'); request.request_default(); } }; } request.callback(callback); return request; }; // .accounts(accounts, realtime) Remote.prototype.request_subscribe = function (streams, callback) { var request = new Request(this, 'subscribe'); if (streams) { request.message.streams = Array.isArray(streams) ? streams : [ streams ]; } request.callback(callback); return request; }; // .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 ]; } request.callback(callback); return request; }; // .ledger_choose() // .ledger_hash() // .ledger_index() Remote.prototype.request_transaction = Remote.prototype.request_transaction_entry = function (hash, ledger_hash, callback) { //utils.assert(this.trusted); // If not trusted, need to check proof, maybe talk packet protocol. var request = new Request(this, 'transaction_entry'); request.tx_hash(hash); switch (typeof ledger_hash) { case 'string': request.ledger_hash(ledger_hash); break; default: request.ledger_index('validated'); callback = ledger_hash; } request.callback(callback); return request; }; // DEPRECATED: use request_transaction_entry Remote.prototype.request_tx = function (hash, callback) { var request = new Request(this, 'tx'); request.message.transaction = hash; request.callback(callback); return request; }; Remote.prototype.request_account_info = function (accountID, callback) { var request = new Request(this, 'account_info'); var account = UInt160.json_rewrite(accountID); request.message.ident = account; //DEPRECATED; request.message.account = account; request.callback(callback); return request; }; Remote.account_request = function(type, accountID, account_index, ledger, callback) { if (typeof accountID === 'object') { var options = accountID; callback = account_index; ledger = options.ledger; account_index = options.account_index; accoutID = options.accountID; } var request = new Request(this, type); request.message.account = UInt160.json_rewrite(accountID); if (account_index) { request.message.index = account_index; } request.ledger_choose(ledger); 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, ledger, callback) { // XXX Does this require the server to be trusted? //utils.assert(this.trusted); var args = Array.prototype.slice.call(arguments); args.unshift('account_lines'); return Remote.account_request.apply(this, args); }; // --> account_index: sub_account index (optional) // --> current: true, for the current ledger. Remote.prototype.request_account_offers = function (accountID, account_index, ledger, callback) { var args = Array.prototype.slice.call(arguments); args.unshift('account_offers'); return Remote.account_request.apply(this, args); }; /* account: account, ledger_index_min: ledger_index, // optional, defaults to -1 if ledger_index_max is specified. ledger_index_max: ledger_index, // optional, defaults to -1 if ledger_index_min is specified. binary: boolean, // optional, defaults to false count: boolean, // optional, defaults to false descending: boolean, // optional, defaults to false offset: integer, // optional, defaults to 0 limit: integer // optional */ Remote.prototype.request_account_tx = function (options, callback) { // XXX Does this require the server to be trusted? //utils.assert(this.trusted); var request = new Request(this, 'account_tx'); var request_fields = [ 'account' , 'ledger_index_min' //earliest , 'ledger_index_max' //latest , 'binary' //false , 'count' //false , 'descending' //false , 'offset' //0 , 'limit' //extended account_tx , 'forward' //false , 'marker' ]; for (var key in options) { if (~request_fields.indexOf(key)) { request.message[key] = options[key]; } } request.callback(callback); return request; }; /** * Request the overall transaction history. * * Returns a list of transactions that happened recently on the network. The * default number of transactions to be returned is 20. */ Remote.prototype.request_tx_history = function (start, callback) { // XXX Does this require the server to be trusted? //utils.assert(this.trusted); var request = new Request(this, 'tx_history'); request.message.start = start; request.callback(callback); return request; }; Remote.prototype.request_book_offers = function (gets, pays, taker, callback) { if (typeof gets === 'object') { var options = gets; taker = options.taker; pays = options.pays; gets = options.gets; } var request = new Request(this, 'book_offers'); request.message.taker_gets = { currency: Currency.json_rewrite(gets.currency) }; if (request.message.taker_gets.currency !== 'XRP') { request.message.taker_gets.issuer = UInt160.json_rewrite(gets.issuer); } request.message.taker_pays = { currency: Currency.json_rewrite(pays.currency) }; if (request.message.taker_pays.currency !== 'XRP') { request.message.taker_pays.issuer = UInt160.json_rewrite(pays.issuer); } request.message.taker = taker ? taker : UInt160.ACCOUNT_ONE; request.callback(callback); return request; }; 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.callback(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; request.callback(callback); return request; }; // Submit a transaction. Remote.prototype.request_submit = function (callback) { return new Request(this, 'submit').callback(callback); }; // // Higher level functions. // /** * Create a subscribe request with current subscriptions. * * Other classes can add their own subscriptions to this request by listening to * the server_subscribe event. * * This function will create and return the request, but not submit it. */ Remote.prototype._server_prepare_subscribe = function (callback) { var self = this; var feeds = [ 'ledger', 'server' ]; if (this._transaction_subs) { feeds.push('transactions'); } var request = this.request_subscribe(feeds); request.once('success', function (message) { self._stand_alone = !!message.stand_alone; self._testnet = !!message.testnet; 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)); } self.emit('random', utils.hexToArray(message.random)); } if (message.ledger_hash && message.ledger_index) { 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); } // FIXME Use this to estimate fee. // XXX When we have multiple server support, most of this should be tracked // by the Server objects and then aggregated/interpreted by Remote. self._load_base = message.load_base || 256; self._load_factor = message.load_factor || 256; self._fee_ref = message.fee_ref; self._fee_base = message.fee_base; self._reserve_base = message.reserve_base; self._reserve_inc = message.reserve_inc; self.emit('subscribed'); }); request.on('error', function (err) { // XXX We need a better global error handling //console.log(err); }); self.emit('prepare_subscribe', request); request.callback(callback); // XXX Could give error events, maybe even time out. 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) { var request = new Request(this, 'ledger_accept'); request.request(); request.callback(callback); } else { this.emit('error', new RippleError('notStandAlone')); } return this; }; // Return a request to refresh the account balance. Remote.prototype.request_account_balance = function (account, ledger, callback) { if (typeof account === 'object') { callback = ledger; ledger = account.ledger; account = account.account; } var request = this.request_ledger_entry('account_root'); request.account_root(account); request.ledger_choose(ledger); request.once('success', function (message) { request.emit('account_balance', Amount.from_json(message.node.Balance)); }); request.callback(callback, 'account_balance'); return request; }; // Return a request to return the account flags. Remote.prototype.request_account_flags = function (account, ledger, callback) { if (typeof account === 'object') { callback = ledger; ledger = account.ledger; account = account.account; } var request = this.request_ledger_entry('account_root'); request.account_root(account); request.ledger_choose(ledger); request.once('success', function (message) { request.emit('account_flags', message.node.Flags); }); request.callback(callback, 'account_flags'); return request; }; // Return a request to emit the owner count. Remote.prototype.request_owner_count = function (account, ledger, callback) { if (typeof account === 'object') { callback = ledger; ledger = account.ledger; account = account.account; } var request = this.request_ledger_entry('account_root'); request.account_root(account); request.ledger_choose(ledger); request.once('success', function (message) { request.emit('owner_count', message.node.OwnerCount); }); request.callback(callback, 'owner_count'); return request; }; Remote.prototype.get_account = function(accountID) { return this._accounts[UInt160.json_rewrite(accountID)]; }; Remote.prototype.add_account = function(accountID) { var account = new Account(this, accountID); if (account.is_valid()) { this._accounts[accountID] = account; } return account; }; Remote.prototype.account = function (accountID) { var account = this.get_account(accountID); return account ? account : this.add_account(accountID); }; Remote.prototype.path_find = function (src_account, dst_account, dst_amount, src_currencies) { if (typeof src_account === 'object') { var options = src_account; src_currencies = options.src_currencies; dst_amount = options.dst_amount; dst_account = options.dst_account; src_account = options.src_account; } var path_find = new PathFind(this, src_account, dst_account, dst_amount, src_currencies); if (this._cur_path_find) { this._cur_path_find.notify_superceded(); } path_find.create(); this._cur_path_find = path_find; return path_find; }; Remote.prepare_trade = function(currency, issuer) { return currency + (currency === 'XRP' ? '' : ('/' + issuer)); }; Remote.prototype.book = function (currency_gets, issuer_gets, currency_pays, issuer_pays) { if (typeof currency_gets === 'object') { var options = currency_gets; issuer_pays = options.issuer_pays; currency_pays = options.currency_pays; issuer_gets = options.issuer_gets; currency_gets = options.currency_gets; } var gets = Remote.prepare_trade(currency_gets, issuer_gets); var pays = Remote.prepare_trade(currency_pays, issuer_pays); var key = gets + ':' + pays; var book; if (!this._books.hasOwnProperty(key)) { book = new OrderBook(this, currency_gets, issuer_gets, currency_pays, issuer_pays); if (book.is_valid()) { this._books[key] = book; } } return this._books[key]; }; // Return the next account sequence if possible. // <-- undefined or Sequence 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) { seq = account_info.seq; var change = { ADVANCE: 1, REWIND: -1 }[advance.toUpperCase()] || 0; account_info.seq += change; } return seq; }; Remote.prototype.set_account_seq = function (account, seq) { var account = UInt160.json_rewrite(account); if (!this.accounts.hasOwnProperty(account)) { this.accounts[account] = { }; } this.accounts[account].seq = seq; } // Return a request to refresh accounts[account].seq. Remote.prototype.account_seq_cache = function (account, ledger, callback) { if (typeof account === 'object') { var options = account; callback = ledger; ledger = options.ledger; account = options.account; } var self = this; if (!this.accounts.hasOwnProperty(account)) { this.accounts[account] = { }; } var account_info = this.accounts[account]; var request = account_info.caching_seq_request; if (!request) { // console.log('starting: %s', account); request = this.request_ledger_entry('account_root'); request.account_root(account); request.ledger_choose(ledger); function account_root_success(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); } function account_root_error(message) { // console.log('error: %s', account); delete account_info.caching_seq_request; request.emit('error_account_seq_cache', message); } request.once('success', account_root_success); request.once('error', account_root_error); account_info.caching_seq_request = request; } 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) { 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) { this.secrets[account] = secret; }; // Return a request to get a ripple balance. // // --> account: String // --> issuer: String // --> currency: String // --> 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, ledger, callback) { if (typeof account === 'object') { var options = account; callback = issuer; ledger = options.ledger; currency = options.currency; issuer = options.issuer; account = options.account; } var request = this.request_ledger_entry('ripple_state'); // YYY Could be cached per ledger. request.ripple_state(account, issuer, currency); request.ledger_choose(ledger); request.once('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()); 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_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), }); }); request.callback(callback, 'ripple_state'); return request; }; Remote.prepare_currencies = function(ci) { var ci_new = { }; if (ci.hasOwnProperty('issuer')) { ci_new.issuer = UInt160.json_rewrite(ci.issuer); } if (ci.hasOwnProperty('currency')) { ci_new.currency = Currency.json_rewrite(ci.currency); } return ci_new; }; Remote.prototype.request_ripple_path_find = function (src_account, dst_account, dst_amount, src_currencies, callback) { if (typeof src_account === 'object') { var options = src_account; callback = dst_account; src_currencies = options.src_currencies; dst_amount = options.dst_amount; dst_account = options.dst_account; src_account = options.src_account; } 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 (src_currencies) { request.message.source_currencies = src_currencies.map(Remote.prepare_currencies); } request.callback(callback); return request; }; Remote.prototype.request_path_find_create = function (src_account, dst_account, dst_amount, src_currencies, callback) { if (typeof src_account === 'object') { var options = src_account; callback = dst_account; src_currencies = options.src_currencies; dst_amount = options.dst_amount; dst_account = options.dst_account; src_account = options.src_account; } var request = new Request(this, 'path_find'); request.message.subcommand = 'create'; 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 (src_currencies) { request.message.source_currencies = src_currencies.map(Remote.prepare_currencies); } request.callback(callback); return request; }; Remote.prototype.request_path_find_close = function () { var request = new Request(this, 'path_find'); request.message.subcommand = 'close'; return request; }; 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; if (comment) { request.message.comment = note; } request.callback(callback); return request; }; // --> node: | Remote.prototype.request_unl_delete = function (node, callback) { var request = new Request(this, 'unl_delete'); request.message.node = node; request.callback(callback); return request; }; Remote.prototype.request_peers = function (callback) { return new Request(this, 'peers').callback(callback); }; Remote.prototype.request_connect = function (ip, port, callback) { var request = new Request(this, 'connect'); request.message.ip = ip; if (port) { request.message.port = port; } request.callback(callback); return request; }; Remote.prototype.transaction = function (source, destination, amount, callback) { var tx = new Transaction(this); if (arguments.length >= 3) { tx = tx.payment(source, destination, amount); if (typeof callback === 'function') { tx.submit(callback); } } return tx; }; /** * Calculate a transaction fee for a number of tx fee units. * * This takes into account the last known network and local load fees. * * @return {Amount} Final fee in XRP for specified number of fee units. */ Remote.prototype.fee_tx = function (units) { var fee_unit = this.fee_tx_unit(); return Amount.from_json(String(Math.ceil(units * fee_unit))); }; /** * 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 as float. */ Remote.prototype.fee_tx_unit = 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.reserve = function (owner_count) { var reserve_base = Amount.from_json(String(this._reserve_base)); var reserve_inc = Amount.from_json(String(this._reserve_inc)); var owner_count = owner_count || 0; if (owner_count < 0) { throw new Error('Owner count must not be negative.'); } return reserve_base.add(reserve_inc.product_human(owner_count)); }; Remote.prototype.ping = function(host, callback) { var request = new Request(this, 'ping'); switch (typeof host) { case 'function': callback = host; break; case 'string': request.set_server(host); break; } var then = Date.now(); request.once('success', function() { request.emit('pong', Date.now() - then); }); request.callback(callback, 'pong'); return request; }; exports.Remote = Remote; // vim:sw=2:sts=2:ts=8:et