<commit_before>// 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.
//

// Node
var util	  = require('util');
var EventEmitter  = require('events').EventEmitter;

// npm
var WebSocket = require('ws');

var Amount    = require('./amount.js').Amount;
var UInt160   = require('./amount.js').UInt160;

// Don't include in browser context.
var config    = require('../test/config.js');

// Request events emmitted:
// '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.on('request', function () {
      self.request_default();
    });
};

Request.prototype  = new EventEmitter;

// Return this.  node EventEmitter's on doesn't return this.
Request.prototype.on = function (e, c) {
  EventEmitter.prototype.on.call(this, e, c);

  return this;
};

Request.prototype.once = function (e, c) {
  EventEmitter.prototype.once.call(this, e, c);

  return this;
};

// Send the request to a remote.
Request.prototype.request = function (remote) {
  this.emit('request', remote);
};

Request.prototype.request_default = function () {
  this.remote.request(this);
};

Request.prototype.ledger_choose = function (current) {
  if (current)
  {
    this.message.ledger_index = this.remote.ledger_current_index;
  }
  else {
    this.message.ledger	      = this.remote.ledger_closed;
  }

  return this;
};

// Set the ledger for a request.
// - ledger_entry
// - transaction_entry
Request.prototype.ledger_closed = function (ledger) {
  this.message.ledger_closed  = ledger;

  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.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;
};

Request.prototype.secret = function (s) {
  if (s)
    this.message.secret  = s;

  return this;
};

Request.prototype.transaction = function (t) {
  this.message.transaction  = t;

  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;
};

//
// Remote - access to a remote Ripple server via websocket.
//
// Events:
// 'connectted'
// 'disconnected'
// 'state':
// - 'online' : connectted and subscribed
// - 'offline' : not subscribed or not connectted.
// 'ledger_closed': A good indicate of ready to serve.
// 'subscribed' : This indicates stand-alone is available.
//

// --> trusted: truthy, if remote is trusted
var Remote = function (trusted, websocket_ip, websocket_port, trace) {
  this.trusted              = trusted;
  this.websocket_ip         = websocket_ip;
  this.websocket_port       = websocket_port;
  this.id                   = 0;
  this.trace                = trace;
  this.ledger_closed        = undefined;
  this.ledger_current_index = undefined;
  this.stand_alone          = undefined;
  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.config		    = config || { 'accounts' : {}};
  
  // Cache information for accounts.
  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 : __ }

    };
  
  // Cache for various ledgers.
  // XXX Clear when ledger advances.
  this.ledgers = {
    'current' : {
      'account_root' : {}
    }
  };
};

Remote.prototype      = new EventEmitter;

Remote.from_config = function (name, trace) {
  var serverConfig = config.servers[name];

  return new Remote(serverConfig.trusted, serverConfig.websocket_ip, serverConfig.websocket_port, trace);
};

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);
};

Remote.flags = {
  'OfferCreate' : {
    'Passive'		      : 0x00010000,
  },

  'Payment' : {
    'CreateAccount'	      : 0x00010000,
    'PartialPayment'	      : 0x00020000,
    'LimitQuality'	      : 0x00040000,
    'NoRippleDirect'	      : 0x00080000,
  },
};

// XXX This needs to be determined from the network.
Remote.fees = {
  'default'	    : Amount.from_json("10"),
  'account_create'  : Amount.from_json("1000000000"),
  'nickname_create' : Amount.from_json("1000"),
  'offer'	    : Amount.from_json("10"),
};

// Set the emited 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('connected');
	break;

      case 'offline':
	this.online_state	= 'closed';
	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;
};

// Stop from open state.
Remote.prototype._connect_stop = function () {
  delete this.ws.onerror;
  delete this.ws.onclose;

  this.ws.terminate();
  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._connect_start();
  }
  else
  {
    // Delay and retry.
    this.retry	      += 1;
    this.retry_timer  =  setTimeout(function () {
	if (self.trace) console.log("remote: retry");

	if (self.online_target) {
	  self._connect_start();
	}
	else {
	  self._connect_retry();
	}
      }, this.retry < 40 ? 1000/20 : 1000); // 20 times per second for 2 seconds then once per second.
  }
};

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  = util.format("ws://%s:%s", this.websocket_ip, this.websocket_port);
  
  if (this.trace) console.log("remote: connect: %s", url);
  
  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) {
      self._set_state('online');

      // Note, we could get disconnected before tis go 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();
  };
  
  // Node's ws module doesn't pass arguments to onmessage.
  ws.on('message', function (json, flags) {
      self._connect_message(ws, json, flags);
    });
};

// It is possible for messages to be dispatched after the connection is closed.
Remote.prototype._connect_message = function (ws, json, flags) {
  var message	  = JSON.parse(json);
  var unexpected  = false;
  var request;

  if ('object' !== typeof message) {
    unexpected  = true;
  }
  else {
    switch (message.type) {
      case 'response':
	{
	  request	  = ws.response[message.id];

	  if (!request) {
	    unexpected  = true;
	  }
	  else if ('success' === message.result) {
	    if (this.trace) console.log("message: %s", json);

	    request.emit('success', message);
	  }
	  else if (message.error) {
	    if (this.trace) console.log("message: %s", json);

	    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_closed	  = message.ledger_closed;
	this.ledger_current_index = message.ledger_closed_index + 1;

	this.emit('ledger_closed', message.ledger_closed, message.ledger_closed_index);
	break;
      
      default:
	unexpected  = true;
	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) console.log("remote: request: %s", JSON.stringify(request.message));
    
    this.ws.send(JSON.stringify(request.message));
  }
  else {
    if (this.trace) console.log("remote: request: DROPPING: %s", JSON.stringify(request.message));
  }
};

Remote.prototype.request_ledger_closed = function () {
  assert(this.trusted);   // If not trusted, need to check proof.

  return new Request(this, 'ledger_closed');
};

// Get the current proposed ledger entry.  May be closed (and revised) at any time (even before returning).
// Only for use by unit tests.
Remote.prototype.request_ledger_current = function () {
  return new Request(this, 'ledger_current');
};

// --> ledger : optional
// --> ledger_index : optional
Remote.prototype.request_ledger_entry = function (type) {
  assert(this.trusted);   // If not trusted, need to check proof, maybe talk packet protocol.
  
  var self    = this;
  var request = new Request(this, 'ledger_entry');

  if (type)
    this.type = type;

  // Transparent caching:
  request.on('request', function (remote) {	      // Intercept default request.
    if (this.ledger_closed) {
      // XXX Add caching.
    }
    // else if (req.ledger_index)
    // else if ('ripple_state' === this.type)	      // YYY Could be cached per ledger.
    else if ('account_root' === this.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.
	this.request.emit('success', {
	    // YYY Missing lots of fields.
	    'node' :  node,
	  });
      }
      else {
	// Was not cached.

	// XXX Only allow with trusted mode.  Must sync response with advance.
	switch (response.type) {
	  case 'account_root':
	    request.on('success', function (message) {
		// Cache node.
		self.ledgers.current.account_root[message.node.Account] = message.node;
	      });
	    break;
	    
	  default:
	    // This type not cached.
	}

	this.request_default();
      }
    }
  });

  return request;
};

Remote.prototype.request_transaction_entry = function (hash) {
  assert(this.trusted);   // If not trusted, need to check proof, maybe talk packet protocol.
  
  return (new Request(this, 'transaction_entry'))
    .transaction(hash);
};

// Submit a transaction.
Remote.prototype.submit = function (transaction) {
  var self  = this;

  if (this.trace) console.log("remote: submit: %s", JSON.stringify(transaction.transaction));

  if (transaction.secret && !this.trusted)
  {
    transaction.emit('error', {
	'result'	  : 'serverUntrusted',
	'result_message'  : "Attempt to give a secret to an untrusted server."
      });
  }
  else {
    if (!transaction.transaction.Sequence) {
      transaction.transaction.Sequence	= this.account_seq(transaction.transaction.Account, 'ADVANCE');
    }

    if (!transaction.transaction.Sequence) {
      // Look in the last closed ledger.
      this.account_seq_cache(transaction.transaction.Account, false)
	.on('success_account_seq_cache', function () {
	    // Try again.
	    self.submit(transaction);
	  })
	.on('error', 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.account_seq_cache(transaction.transaction.Account, 'CURRENT')
	      .on('success_account_seq_cache', function () {
		  // Try again.
		  self.submit(transaction);
		})
	      .on('error', function (message) {
		  // Forward errors.
		  transaction.emit('error', message);
		})
	      .request();
	  })
	.request();
    }
    else {
      var submit_request = new Request(this, 'submit');

      submit_request.transaction(transaction.transaction);
      submit_request.secret(transaction.secret);

      // Forward successes and errors.
      submit_request.on('success', function (message) { transaction.emit('success', message); });
      submit_request.on('error', function (message) { transaction.emit('error', message); });
    
      submit_request.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;

  (new Request(this, 'server_subscribe'))
    .on('success', function (message) {
	self.stand_alone          = !!message.stand_alone;

	if (message.ledger_closed && message.ledger_current_index) {
	  self.ledger_closed        = message.ledger_closed;
	  self.ledger_current_index = message.ledger_current_index;

	  self.emit('ledger_closed', self.ledger_closed, self.ledger_current_index-1);
	}

	self.emit('subscribed');
      })
    .request();

  // XXX Could give error events, maybe even time out.

  return this;
};

// Ask the remote to accept the current ledger.
// - To be notified when the ledger is accepted, server_subscribe() then listen to 'ledger_closed' events.
// A good way to be notified of the result of this is:
//    remote.once('ledger_closed', function (ledger_closed, ledger_closed_index) { ... } );
Remote.prototype.ledger_accept = function () {
  if (this.stand_alone || undefined === this.stand_alone)
  {
    (new Request(this, 'ledger_accept'))
      .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) {
  return (this.request_ledger_entry('account_root'))
    .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', message.node.Balance);
    });
};

// 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;

    if (advance) account_info.seq += 1;
  }

  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 = this.request_ledger_entry('account_root');

  return request
    .account_root(account)
    .ledger_choose(current)
    .on('success', function (message) {
	var seq = message.node.Sequence;
    
	if (!self.accounts[account]) self.accounts[account] = {};

	self.accounts[account].seq  = seq;

	// If the caller also waits for 'success', they might run before this.
	request.emit('success_account_seq_cache');
      });
};

// 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];
};

// 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) {<commit_msg>JS: Fix account sequence caching.<commit_after>// 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.
//

// Node
var util	  = require('util');
var EventEmitter  = require('events').EventEmitter;

// npm
var WebSocket = require('ws');

var Amount    = require('./amount.js').Amount;
var UInt160   = require('./amount.js').UInt160;

// Don't include in browser context.
var config    = require('../test/config.js');

// Request events emmitted:
// '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;

  this.on('request', function () {
      self.request_default();
    });
};

Request.prototype  = new EventEmitter;

// Return this.  node EventEmitter's on doesn't return this.
Request.prototype.on = function (e, c) {
  EventEmitter.prototype.on.call(this, e, c);

  return this;
};

Request.prototype.once = function (e, c) {
  EventEmitter.prototype.once.call(this, e, c);

  return this;
};

// Send the request to a remote.
Request.prototype.request = function (remote) {
  this.emit('request', remote);
};

Request.prototype.request_default = function () {
  if (!this.requested) {
    this.requested  = true;
    this.remote.request(this);
  }
};

Request.prototype.ledger_choose = function (current) {
  if (current)
  {
    this.message.ledger_index = this.remote.ledger_current_index;
  }
  else {
    this.message.ledger	      = this.remote.ledger_closed;
  }

  return this;
};

// Set the ledger for a request.
// - ledger_entry
// - transaction_entry
Request.prototype.ledger_closed = function (ledger) {
  this.message.ledger_closed  = ledger;

  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.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;
};

Request.prototype.secret = function (s) {
  if (s)
    this.message.secret  = s;

  return this;
};

Request.prototype.transaction = function (t) {
  this.message.transaction  = t;

  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;
};

//
// Remote - access to a remote Ripple server via websocket.
//
// Events:
// 'connectted'
// 'disconnected'
// 'state':
// - 'online' : connectted and subscribed
// - 'offline' : not subscribed or not connectted.
// 'ledger_closed': A good indicate of ready to serve.
// 'subscribed' : This indicates stand-alone is available.
//

// --> trusted: truthy, if remote is trusted
var Remote = function (trusted, websocket_ip, websocket_port, trace) {
  this.trusted              = trusted;
  this.websocket_ip         = websocket_ip;
  this.websocket_port       = websocket_port;
  this.id                   = 0;
  this.trace                = trace;
  this.ledger_closed        = undefined;
  this.ledger_current_index = undefined;
  this.stand_alone          = undefined;
  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.config		    = config || { 'accounts' : {}};
  
  // Cache information for accounts.
  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 : __ }

    };
  
  // Cache for various ledgers.
  // XXX Clear when ledger advances.
  this.ledgers = {
    'current' : {
      'account_root' : {}
    }
  };
};

Remote.prototype      = new EventEmitter;

Remote.from_config = function (name, trace) {
  var serverConfig = config.servers[name];

  return new Remote(serverConfig.trusted, serverConfig.websocket_ip, serverConfig.websocket_port, trace);
};

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);
};

Remote.flags = {
  'OfferCreate' : {
    'Passive'		      : 0x00010000,
  },

  'Payment' : {
    'CreateAccount'	      : 0x00010000,
    'PartialPayment'	      : 0x00020000,
    'LimitQuality'	      : 0x00040000,
    'NoRippleDirect'	      : 0x00080000,
  },
};

// XXX This needs to be determined from the network.
Remote.fees = {
  'default'	    : Amount.from_json("10"),
  'account_create'  : Amount.from_json("1000000000"),
  'nickname_create' : Amount.from_json("1000"),
  'offer'	    : Amount.from_json("10"),
};

// Set the emited 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('connected');
	break;

      case 'offline':
	this.online_state	= 'closed';
	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;
};

// Stop from open state.
Remote.prototype._connect_stop = function () {
  delete this.ws.onerror;
  delete this.ws.onclose;

  this.ws.terminate();
  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._connect_start();
  }
  else
  {
    // Delay and retry.
    this.retry	      += 1;
    this.retry_timer  =  setTimeout(function () {
	if (self.trace) console.log("remote: retry");

	if (self.online_target) {
	  self._connect_start();
	}
	else {
	  self._connect_retry();
	}
      }, this.retry < 40 ? 1000/20 : 1000); // 20 times per second for 2 seconds then once per second.
  }
};

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  = util.format("ws://%s:%s", this.websocket_ip, this.websocket_port);
  
  if (this.trace) console.log("remote: connect: %s", url);
  
  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) {
      self._set_state('online');

      // Note, we could get disconnected before tis go 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();
  };
  
  // Node's ws module doesn't pass arguments to onmessage.
  ws.on('message', function (json, flags) {
      self._connect_message(ws, json, flags);
    });
};

// It is possible for messages to be dispatched after the connection is closed.
Remote.prototype._connect_message = function (ws, json, flags) {
  var message	  = JSON.parse(json);
  var unexpected  = false;
  var request;

  if ('object' !== typeof message) {
    unexpected  = true;
  }
  else {
    switch (message.type) {
      case 'response':
	{
	  request	  = ws.response[message.id];

	  if (!request) {
	    unexpected  = true;
	  }
	  else if ('success' === message.result) {
	    if (this.trace) console.log("message: %s", json);

	    request.emit('success', message);
	  }
	  else if (message.error) {
	    if (this.trace) console.log("message: %s", json);

	    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_closed	  = message.ledger_closed;
	this.ledger_current_index = message.ledger_closed_index + 1;

	this.emit('ledger_closed', message.ledger_closed, message.ledger_closed_index);
	break;
      
      default:
	unexpected  = true;
	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) console.log("remote: request: %s", JSON.stringify(request.message));
    
    this.ws.send(JSON.stringify(request.message));
  }
  else {
    if (this.trace) console.log("remote: request: DROPPING: %s", JSON.stringify(request.message));
  }
};

Remote.prototype.request_ledger_closed = function () {
  assert(this.trusted);   // If not trusted, need to check proof.

  return new Request(this, 'ledger_closed');
};

// Get the current proposed ledger entry.  May be closed (and revised) at any time (even before returning).
// Only for use by unit tests.
Remote.prototype.request_ledger_current = function () {
  return new Request(this, 'ledger_current');
};

// --> ledger : optional
// --> ledger_index : optional
Remote.prototype.request_ledger_entry = function (type) {
  assert(this.trusted);   // If not trusted, need to check proof, maybe talk packet protocol.
  
  var self    = this;
  var request = new Request(this, 'ledger_entry');

  if (type)
    this.type = type;

  // Transparent caching:
  request.on('request', function (remote) {	      // Intercept default request.
    if (this.ledger_closed) {
      // XXX Add caching.
    }
    // else if (req.ledger_index)
    // else if ('ripple_state' === this.type)	      // YYY Could be cached per ledger.
    else if ('account_root' === this.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.
	this.request.emit('success', {
	    // YYY Missing lots of fields.
	    'node' :  node,
	  });
      }
      else {
	// Was not cached.

	// XXX Only allow with trusted mode.  Must sync response with advance.
	switch (response.type) {
	  case 'account_root':
	    request.on('success', function (message) {
		// Cache node.
		self.ledgers.current.account_root[message.node.Account] = message.node;
	      });
	    break;
	    
	  default:
	    // This type not cached.
	}

	this.request_default();
      }
    }
  });

  return request;
};

Remote.prototype.request_transaction_entry = function (hash) {
  assert(this.trusted);   // If not trusted, need to check proof, maybe talk packet protocol.
  
  return (new Request(this, 'transaction_entry'))
    .transaction(hash);
};

// Submit a transaction.
Remote.prototype.submit = function (transaction) {
  var self  = this;

  if (this.trace) console.log("remote: submit: %s", JSON.stringify(transaction.transaction));

  if (transaction.secret && !this.trusted)
  {
    transaction.emit('error', {
	'result'	  : 'serverUntrusted',
	'result_message'  : "Attempt to give a secret to an untrusted server."
      });
  }
  else {
    if (!transaction.transaction.Sequence) {
      transaction.transaction.Sequence	= this.account_seq(transaction.transaction.Account, 'ADVANCE');
      // console.log("Sequence: %s", transaction.transaction.Sequence);
    }

    if (!transaction.transaction.Sequence) {
      // Look in the last closed ledger.
      this.account_seq_cache(transaction.transaction.Account, false)
	.on('success_account_seq_cache', function () {
	    // Try again.
	    self.submit(transaction);
	  })
	.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.account_seq_cache(transaction.transaction.Account, 'CURRENT')
	      .on('success_account_seq_cache', function () {
		  // Try again.
		  self.submit(transaction);
		})
	      .on('error_account_seq_cache', function (message) {
		  // Forward errors.
		  transaction.emit('error', message);
		})
	      .request();
	  })
	.request();
    }
    else {
      var submit_request = new Request(this, 'submit');

      submit_request.transaction(transaction.transaction);
      submit_request.secret(transaction.secret);

      // Forward successes and errors.
      submit_request.on('success', function (message) { transaction.emit('success', message); });
      submit_request.on('error', function (message) { transaction.emit('error', message); });
    
      submit_request.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;

  (new Request(this, 'server_subscribe'))
    .on('success', function (message) {
	self.stand_alone          = !!message.stand_alone;

	if (message.ledger_closed && message.ledger_current_index) {
	  self.ledger_closed        = message.ledger_closed;
	  self.ledger_current_index = message.ledger_current_index;

	  self.emit('ledger_closed', self.ledger_closed, self.ledger_current_index-1);
	}

	self.emit('subscribed');
      })
    .request();

  // XXX Could give error events, maybe even time out.

  return this;
};

// Ask the remote to accept the current ledger.
// - To be notified when the ledger is accepted, server_subscribe() then listen to 'ledger_closed' events.
// A good way to be notified of the result of this is:
//    remote.once('ledger_closed', function (ledger_closed, ledger_closed_index) { ... } );
Remote.prototype.ledger_accept = function () {
  if (this.stand_alone || undefined === this.stand_alone)
  {
    (new Request(this, 'ledger_accept'))
      .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) {
  return (this.request_ledger_entry('account_root'))
    .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', message.node.Balance);
    });
};

// 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;

    if (advance) 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];
};

// 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) {
{'fraction_non_alphanumeric': 0.1349868470499812, 'fraction_numerical': 0.006163096580232995, 'mean_word_length': 3.027037933817595, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 23, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 2, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4770696', 'n_tokens_mistral': 13096, 'n_tokens_neox': 12512, 'n_words': 3273}