// 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 Amount = require('./amount').Amount; var Currency = require('./amount').Currency; var UInt160 = require('./amount').UInt160; var Transaction = require('./transaction').Transaction; var Account = require('./account').Account; var Meta = require('./meta').Meta; var OrderBook = require('./orderbook').OrderBook; var utils = require('./utils'); var config = require('./config'); var sjcl = require('../../build/sjcl'); // Request events emitted: // 'success' : Request successful. // 'error' : Request failed. // 'remoteError' // 'remoteUnexpected' // 'remoteDisconnected' var Request = function (remote, command) { var self = this; this.message = { 'command' : command, 'id' : undefined, }; this.remote = remote; this.requested = false; }; Request.prototype = new EventEmitter; // Send the request to a remote. Request.prototype.request = function (remote) { if (!this.requested) { this.requested = true; this.remote.request(this); this.emit('request', remote); } }; Request.prototype.build_path = function (build) { if (build) this.message.build_path = true; return this; }; Request.prototype.ledger_choose = function (current) { if (current) { this.message.ledger_index = this.remote._ledger_current_index; } else { this.message.ledger_hash = this.remote._ledger_hash; } return this; }; // Set the ledger for a request. // - ledger_entry // - transaction_entry Request.prototype.ledger_hash = function (h) { this.message.ledger_hash = h; return this; }; // Set the ledger_index for a request. // - ledger_entry Request.prototype.ledger_index = function (ledger_index) { this.message.ledger_index = ledger_index; return this; }; Request.prototype.ledger_select = function (ledger_spec) { if (ledger_spec === 'current') { this.message.ledger_index = ledger_spec; } else if (ledger_spec === 'closed') { this.message.ledger_index = ledger_spec; } else if (ledger_spec === 'verified') { this.message.ledger_index = ledger_spec; } else if (String(ledger_spec).length > 12) { // XXX Better test needed this.message.ledger_hash = ledger_spec; } else { this.message.ledger_index = ledger_spec; } return this; }; Request.prototype.account_root = function (account) { this.message.account_root = UInt160.json_rewrite(account); return this; }; Request.prototype.index = function (hash) { this.message.index = hash; return this; }; // Provide the information id an offer. // --> account // --> seq : sequence number of transaction creating offer (integer) Request.prototype.offer_id = function (account, seq) { this.message.offer = { 'account' : UInt160.json_rewrite(account), 'seq' : seq }; return this; }; // --> index : ledger entry index. Request.prototype.offer_index = function (index) { this.message.offer = index; return this; }; Request.prototype.secret = function (s) { if (s) this.message.secret = s; return this; }; Request.prototype.tx_hash = function (h) { this.message.tx_hash = h; return this; }; Request.prototype.tx_json = function (j) { this.message.tx_json = j; return this; }; Request.prototype.tx_blob = function (j) { this.message.tx_blob = j; return this; }; Request.prototype.ripple_state = function (account, issuer, currency) { this.message.ripple_state = { 'accounts' : [ UInt160.json_rewrite(account), UInt160.json_rewrite(issuer) ], 'currency' : currency }; return this; }; Request.prototype.accounts = function (accounts, realtime) { if ("object" !== typeof accounts) { accounts = [accounts]; } // Process accounts parameters var procAccounts = []; for (var i = 0, l = accounts.length; i < l; i++) { procAccounts.push(UInt160.json_rewrite(accounts[i])); } if (realtime) { this.message.rt_accounts = procAccounts; } else { this.message.accounts = procAccounts; } return this; }; Request.prototype.rt_accounts = function (accounts) { return this.accounts(accounts, true); }; Request.prototype.books = function (books) { var procBooks = []; for (var i = 0, l = books.length; i < l; i++) { var book = books[i]; var json = { "CurrencyOut": Currency.json_rewrite(book["CurrencyOut"]), "CurrencyIn": Currency.json_rewrite(book["CurrencyIn"]) }; if (json["CurrencyOut"] !== "XRP") { json["IssuerOut"] = UInt160.json_rewrite(book["IssuerOut"]); } if (json["CurrencyIn"] !== "XRP") { json["IssuerIn"] = UInt160.json_rewrite(book["IssuerIn"]); } procBooks.push(json); } this.message.books = procBooks; return this; }; // // Remote - access to a remote Ripple server via websocket. // // Events: // 'connected' // 'disconnected' // '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. // --> trusted: truthy, if remote is trusted var Remote = function (opts, trace) { var self = this; this.trusted = opts.trusted; this.websocket_ip = opts.websocket_ip; this.websocket_port = opts.websocket_port; this.websocket_ssl = opts.websocket_ssl; this.local_sequence = opts.local_sequence; // Locally track sequence numbers this.local_fee = opts.local_fee; // Locally set fees this.local_signing = opts.local_signing; this.id = 0; this.trace = opts.trace || trace; this._server_fatal = false; // True, if we know server exited. this._ledger_current_index = undefined; this._ledger_hash = undefined; this._ledger_time = undefined; this._stand_alone = undefined; this._testnet = undefined; this._transaction_subs = 0; this.online_target = false; this._online_state = 'closed'; // 'open', 'closed', 'connecting', 'closing' this.state = 'offline'; // 'online', 'offline' this.retry_timer = undefined; this.retry = undefined; this._load_base = 256; this._load_factor = 1.0; this._fee_ref = undefined; this._fee_base = undefined; this._reserve_base = undefined; this._reserve_inc = undefined; this._server_status = undefined; // Local signing implies local fees and sequences if (this.local_signing) { this.local_sequence = true; this.local_fee = true; } // 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 : __ } }; // Hash map of Account objects by AccountId. this._accounts = {}; // List of 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' : {} } }; this.on('newListener', function (type, listener) { if ('transaction_all' === type) { if (!self._transaction_subs && 'open' === self._online_state) { self.request_subscribe([ 'transactions' ]) .request(); } self._transaction_subs += 1; } }); this.on('removeListener', function (type, listener) { if ('transaction_all' === type) { self._transaction_subs -= 1; if (!self._transaction_subs && 'open' === self._online_state) { self.request_unsubscribe([ 'transactions' ]) .request(); } } }); }; Remote.prototype = new EventEmitter; Remote.from_config = function (obj, trace) { var serverConfig = 'string' === typeof obj ? config.servers[obj] : obj; var remote = new Remote(serverConfig, trace); for (var account in config.accounts) { var accountInfo = config.accounts[account]; if ("object" === typeof accountInfo) { if (accountInfo.secret) { // Index by nickname ... remote.set_secret(account, accountInfo.secret); // ... and by account ID remote.set_secret(accountInfo.account, accountInfo.secret); } } } return remote; }; var isTemMalformed = function (engine_result_code) { return (engine_result_code >= -299 && engine_result_code < 199); }; var isTefFailure = function (engine_result_code) { return (engine_result_code >= -299 && engine_result_code < 199); }; /** * Server states that we will treat as the server being online. * * Our requirements are that the server can process transactions and notify * us of changes. */ Remote.online_states = [ 'proposing', 'validating', 'full' ]; // 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) { if (this.trace) console.log("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.emit('connect'); this.emit('connected'); break; case 'offline': this._online_state = 'closed'; this.emit('disconnect'); this.emit('disconnected'); break; } } }; Remote.prototype.set_trace = function (trace) { this.trace = undefined === trace || trace; return this; }; // Set the target online state. Defaults to false. Remote.prototype.connect = function (online) { var target = undefined === online || online; if (this.online_target != target) { this.online_target = target; // If we were in a stable state, go dynamic. switch (this._online_state) { case 'open': if (!target) this._connect_stop(); break; case 'closed': if (target) this._connect_retry(); break; } } return this; }; Remote.prototype.ledger_hash = function () { return this._ledger_hash; }; // Stop from open state. Remote.prototype._connect_stop = function () { if (this.ws) { delete this.ws.onerror; delete this.ws.onclose; this.ws.close(); delete this.ws; } this._set_state('offline'); }; // Implictly we are not connected. Remote.prototype._connect_retry = function () { var self = this; if (!self.online_target) { // Do not continue trying to connect. this._set_state('offline'); } else if ('connecting' !== this._online_state) { // New to connecting state. this._online_state = 'connecting'; this.retry = 0; this._set_state('offline'); // Report newly offline. this._connect_start(); } else { // Delay and retry. this.retry += 1; this.retry_timer = setTimeout(function () { if (self.trace) console.log("remote: retry"); if (self._server_fatal) { // Stop trying to connect. // nothing(); console.log("FATAL"); } else if (self.online_target) { self._connect_start(); } else { self._connect_retry(); } }, this.retry < 40 ? 1000/20 // First, for 2 seconds: 20 times per second : this.retry < 40+60 ? 1000 // Then, for 1 minute: once per second : this.retry < 40+60+60 ? 10*1000 // Then, for 10 minutes: once every 10 seconds : 30*1000); // Then: once every 30 seconds } }; Remote.prototype._connect_start = function () { // Note: as a browser client can't make encrypted connections to random ips // with self-signed certs as the user must have pre-approved the self-signed certs. var self = this; var url = (this.websocket_ssl ? "wss://" : "ws://") + this.websocket_ip + ":" + this.websocket_port; if (this.trace) console.log("remote: connect: %s", url); // There should not be an active connection at this point, but if there is // we will shut it down so we don't end up with a duplicate. if (this.ws) { this._connect_stop(); } var WebSocket = require('ws'); var ws = this.ws = new WebSocket(url); ws.response = {}; ws.onopen = function () { if (self.trace) console.log("remote: onopen: %s: online_target=%s", ws.readyState, self.online_target); ws.onerror = function () { if (self.trace) console.log("remote: onerror: %s", ws.readyState); delete ws.onclose; self._connect_retry(); }; ws.onclose = function () { if (self.trace) console.log("remote: onclose: %s", ws.readyState); delete ws.onerror; self._connect_retry(); }; if (self.online_target) { // Note, we could get disconnected before this goes through. self._server_subscribe(); // Automatically subscribe. } else { self._connect_stop(); } }; ws.onerror = function () { if (self.trace) console.log("remote: onerror: %s", ws.readyState); delete ws.onclose; self._connect_retry(); }; // Failure to open. ws.onclose = function () { if (self.trace) console.log("remote: onclose: %s", ws.readyState); delete ws.onerror; self._connect_retry(); }; ws.onmessage = function (json) { self._connect_message(ws, json.data); }; }; // It is possible for messages to be dispatched after the connection is closed. Remote.prototype._connect_message = function (ws, json) { var self = this; var message = JSON.parse(json); var unexpected = false; var request; if ('object' !== typeof message) { unexpected = true; } else { switch (message.type) { case 'response': // A response to a request. { request = ws.response[message.id]; if (!request) { unexpected = true; } else if ('success' === message.status) { if (this.trace) utils.logObject("remote: response: %s", message); request.emit('success', message.result); } else if (message.error) { if (this.trace) utils.logObject("remote: error: %s", message); request.emit('error', { 'error' : 'remoteError', 'error_message' : 'Remote reported an error.', 'remote' : message, }); } } 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); 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. // XXX Should de-duplicate transaction events if (this.trace) utils.logObject("remote: tx: %s", message); // Process metadata message.mmeta = new Meta(message.meta); // Pass the event on to any related Account objects var affected = message.mmeta.getAffectedAccounts(); for (var i = 0, l = affected.length; i < l; i++) { var account = self._accounts[affected[i]]; // Only trigger the event if the account object is actually // subscribed - this prevents some weird phantom events from // occurring. if (account && account._subs) { account.emit('transaction', message); } } this.emit('transaction', message); this.emit('transaction_all', message); break; case 'serverStatus': // This message is only received when online. As we are connected, it is the definative final state. this._set_state( Remote.online_states.indexOf(message.server_status) !== -1 ? 'online' : 'offline'); if ('load_base' in message && 'load_factor' in message && (message.load_base !== this._load_base || message.load_factor != this._load_factor)) { this._load_base = message.load_base; this._load_factor = message.load_factor; this.emit('load', { 'load_base' : this._load_base, 'load_factor' : this.load_factor }); } break; // All other messages default: if (this.trace) utils.logObject("remote: "+message.type+": %s", message); this.emit('net_'+message.type, message); break; } } if (!unexpected) { } // Unexpected response from remote. // XXX This isn't so robust. Hard fails should probably only happen in a debugging scenairo. else if (this.trusted) { // Remote is trusted, report an error. console.log("unexpected message from trusted remote: %s", json); (request || this).emit('error', { 'error' : 'remoteUnexpected', 'error_message' : 'Unexpected response from remote.' }); } else { // Treat as a disconnect. if (this.trace) console.log("unexpected message from untrusted remote: %s", json); // XXX All pending request need this treatment and need to actionally disconnect. (request || this).emit('error', { 'error' : 'remoteDisconnected', 'error_message' : 'Remote disconnected.' }); } }; // Send a request. // <-> request: what to send, consumed. Remote.prototype.request = function (request) { if (this.ws) { // Only bother if we are still connected. this.ws.response[request.message.id = this.id] = request; this.id += 1; // Advance id. if (this.trace) utils.logObject("remote: request: %s", request.message); this.ws.send(JSON.stringify(request.message)); } else { if (this.trace) utils.logObject("remote: request: DROPPING: %s", request.message); } }; Remote.prototype.request_server_info = function () { return new Request(this, 'server_info'); }; // 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) { //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; } if ('object' == typeof opts) { 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; } return request; }; // Only for unit testing. Remote.prototype.request_ledger_hash = function () { //utils.assert(this.trusted); // If not trusted, need to check proof. return new Request(this, 'ledger_closed'); }; // .ledger() // .ledger_index() Remote.prototype.request_ledger_header = function () { return new Request(this, 'ledger_header'); }; // Get the current proposed ledger entry. May be closed (and revised) at any time (even before returning). // Only for unit testing. Remote.prototype.request_ledger_current = function () { return new Request(this, 'ledger_current'); }; // --> type : the type of ledger entry. // .ledger() // .ledger_index() // .offer_id() Remote.prototype.request_ledger_entry = function (type) { //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 ('account_root' === type) { 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. else if ('account_root' === type) { 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'); request.emit('success', { // YYY Missing lots of fields. 'node' : node, }); bDefault = false; } else { // Was not cached. // XXX Only allow with trusted mode. Must sync response with advance. switch (type) { case 'account_root': request.on('success', function (message) { // Cache node. // console.log('request_ledger_entry: caching'); self.ledgers.current.account_root[message.node.Account] = message.node; }); 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(); } } }; return request; }; // .accounts(accounts, realtime) Remote.prototype.request_subscribe = function (streams) { var request = new Request(this, 'subscribe'); if (streams) { if ("object" !== typeof streams) { streams = [streams]; } request.message.streams = streams; } return request; }; // .accounts(accounts, realtime) Remote.prototype.request_unsubscribe = function (streams) { var request = new Request(this, 'unsubscribe'); if (streams) { if ("object" !== typeof streams) { streams = [streams]; } request.message.streams = streams; } return request; }; // .ledger_choose() // .ledger_hash() // .ledger_index() Remote.prototype.request_transaction_entry = function (hash) { //utils.assert(this.trusted); // If not trusted, need to check proof, maybe talk packet protocol. return (new Request(this, 'transaction_entry')) .tx_hash(hash); }; // DEPRECATED: use request_transaction_entry Remote.prototype.request_tx = function (hash) { var request = new Request(this, 'tx'); request.message.transaction = hash; return request; }; Remote.prototype.request_account_info = function (accountID) { var request = new Request(this, 'account_info'); request.message.ident = UInt160.json_rewrite(accountID); // DEPRECATED request.message.account = UInt160.json_rewrite(accountID); return request; }; // --> account_index: sub_account index (optional) // --> current: true, for the current ledger. Remote.prototype.request_account_lines = function (accountID, account_index, current) { // XXX Does this require the server to be trusted? //utils.assert(this.trusted); var request = new Request(this, 'account_lines'); request.message.account = UInt160.json_rewrite(accountID); if (account_index) request.message.index = account_index; return request .ledger_choose(current); }; // --> account_index: sub_account index (optional) // --> current: true, for the current ledger. Remote.prototype.request_account_offers = function (accountID, account_index, current) { var request = new Request(this, 'account_offers'); request.message.account = UInt160.json_rewrite(accountID); if (account_index) request.message.index = account_index; return request .ledger_choose(current); }; Remote.prototype.request_account_tx = function (accountID, ledger_min, ledger_max) { // XXX Does this require the server to be trusted? //utils.assert(this.trusted); var request = new Request(this, 'account_tx'); request.message.account = accountID; if (ledger_min === ledger_max) { request.message.ledger = ledger_min; } else { request.message.ledger_min = ledger_min; request.message.ledger_max = ledger_max; } return request; }; Remote.prototype.request_book_offers = function (gets, pays, taker) { 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; return request; }; Remote.prototype.request_wallet_accounts = function (seed) { utils.assert(this.trusted); // Don't send secrets. var request = new Request(this, 'wallet_accounts'); request.message.seed = seed; return request; }; Remote.prototype.request_sign = function (secret, tx_json) { 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; }; // Submit a transaction. Remote.prototype.request_submit = function () { var self = this; var request = new Request(this, 'submit'); return request; }; // // Higher level functions. // // Subscribe to a server to get 'ledger_closed' events. // 'subscribed' : This command was successful. // 'ledger_closed : ledger_closed and ledger_current_index are updated. Remote.prototype._server_subscribe = function () { var self = this; var feeds = [ 'ledger', 'server' ]; if (this._transaction_subs) feeds.push('transactions'); this.request_subscribe(feeds) .on('success', function (message) { self._stand_alone = !!message.stand_alone; self._testnet = !!message.testnet; if ("string" === typeof message.random) { 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. self._load_base = message.load_base || 256; self._load_factor = message.load_factor || 1.0; 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._server_status = message.server_status; if (Remote.online_states.indexOf(message.server_status) !== -1) { self._set_state('online'); } self.emit('subscribed'); }) .request(); // XXX Could give error events, maybe even time out. return this; }; // For unit testing: ask the remote to accept the current ledger. // - To be notified when the ledger is accepted, server_subscribe() then listen to 'ledger_hash' events. // A good way to be notified of the result of this is: // remote.once('ledger_closed', function (ledger_closed, ledger_index) { ... } ); Remote.prototype.ledger_accept = function () { if (this._stand_alone || undefined === this._stand_alone) { var request = new Request(this, 'ledger_accept'); request .request(); } else { this.emit('error', { 'error' : 'notStandAlone' }); } return this; }; // Return a request to refresh the account balance. Remote.prototype.request_account_balance = function (account, current) { var request = this.request_ledger_entry('account_root'); return request .account_root(account) .ledger_choose(current) .on('success', function (message) { // If the caller also waits for 'success', they might run before this. request.emit('account_balance', Amount.from_json(message.node.Balance)); }); }; // Return a request to emit the owner count. Remote.prototype.request_owner_count = function (account, current) { var request = this.request_ledger_entry('account_root'); return request .account_root(account) .ledger_choose(current) .on('success', function (message) { // If the caller also waits for 'success', they might run before this. request.emit('owner_count', message.node.OwnerCount); }); }; Remote.prototype.account = function (accountId) { accountId = UInt160.json_rewrite(accountId); if (!this._accounts[accountId]) { var account = new Account(this, accountId); if (!account.is_valid()) return account; this._accounts[accountId] = account; } return this._accounts[accountId]; }; Remote.prototype.book = function (currency_out, issuer_out, currency_in, issuer_in) { var book = new OrderBook(this, currency_out, issuer_out, currency_in, issuer_in); return book; } // Return the next account sequence if possible. // <-- undefined or Sequence Remote.prototype.account_seq = function (account, advance) { account = UInt160.json_rewrite(account); var account_info = this.accounts[account]; var seq; if (account_info && account_info.seq) { seq = account_info.seq; if (advance === "ADVANCE") account_info.seq += 1; if (advance === "REWIND") account_info.seq -= 1; // console.log("cached: %s current=%d next=%d", account, seq, account_info.seq); } else { // console.log("uncached: %s", account); } return seq; } Remote.prototype.set_account_seq = function (account, seq) { var account = UInt160.json_rewrite(account); 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) { var self = this; var request; if (!self.accounts[account]) self.accounts[account] = {}; var account_info = self.accounts[account]; 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) { 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) { // console.log("error: %s", account); delete account_info.caching_seq_request; request.emit('error_account_seq_cache', message); }); account_info.caching_seq_request = request; } 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, current) { var request = this.request_ledger_entry('ripple_state'); // YYY Could be cached per ledger. return request .ripple_state(account, issuer, currency) .ledger_choose(current) .on('success', function (message) { var node = message.node; var lowLimit = Amount.from_json(node.LowLimit); var highLimit = Amount.from_json(node.HighLimit); // The amount the low account holds of issuer. var balance = Amount.from_json(node.Balance); // accountHigh implies: for account: balance is negated, highLimit is the limit set by account. var accountHigh = UInt160.from_json(account).equals(highLimit.issuer()); // The limit set by account. var accountLimit = (accountHigh ? highLimit : lowLimit).parse_issuer(account); // The limit set by issuer. var issuerLimit = (accountHigh ? lowLimit : highLimit).parse_issuer(issuer); var accountBalance = (accountHigh ? balance.negate() : balance).parse_issuer(account); var issuerBalance = (accountHigh ? balance : balance.negate()).parse_issuer(issuer); request.emit('ripple_state', { 'issuer_balance' : issuerBalance, // Balance with dst as issuer. 'account_balance' : accountBalance, // Balance with account as issuer. 'issuer_limit' : issuerLimit, // Limit set by issuer with src as issuer. 'account_limit' : accountLimit // Limit set by account with dst as issuer. }); }); }; Remote.prototype.request_ripple_path_find = function (src_account, dst_account, dst_amount, source_currencies) { var self = this; var request = new Request(this, 'ripple_path_find'); request.message.source_account = UInt160.json_rewrite(src_account); request.message.destination_account = UInt160.json_rewrite(dst_account); request.message.destination_amount = Amount.json_rewrite(dst_amount); if (source_currencies) { request.message.source_currencies = source_currencies.map(function (ci) { var ci_new = {}; if ('issuer' in ci) ci_new.issuer = UInt160.json_rewrite(ci.issuer); if ('currency' in ci) ci_new.currency = Currency.json_rewrite(ci.currency); return ci_new; }); } return request; }; Remote.prototype.request_unl_list = function () { return new Request(this, 'unl_list'); }; Remote.prototype.request_unl_add = function (addr, comment) { var request = new Request(this, 'unl_add'); request.message.node = addr; if (comment !== undefined) request.message.comment = note; return request; }; // --> node: | Remote.prototype.request_unl_delete = function (node) { var request = new Request(this, 'unl_delete'); request.message.node = node; return request; }; Remote.prototype.request_peers = function () { return new Request(this, 'peers'); }; Remote.prototype.request_connect = function (ip, port) { var request = new Request(this, 'connect'); request.message.ip = ip; if (port) request.message.port = port; return request; }; Remote.prototype.transaction = function () { return new Transaction(this); }; exports.Remote = Remote; // vim:sw=2:sts=2:ts=8:et