Files
xahau.js/src/js/ripple/remote.js
2013-06-26 03:28:50 +09:00

1418 lines
39 KiB
JavaScript

// 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 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 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, callback) {
EventEmitter.call(this);
var self = this;
this.remote = remote;
this.requested = false;
this.message = {
'command': command,
'id': void(0)
};
this.callback(callback);
};
util.inherits(Request, EventEmitter);
// Send the request to a remote.
Request.prototype.request = function(remote) {
if (!this.requested) {
this.requested = true;
this.remote.request(this);
this.emit('request', remote);
}
};
Request.prototype.callback = function(callback, successEvent) {
if (typeof callback === 'function') {
this.once('error', callback);
this.once(successEvent || 'success', callback.bind(this, null));
this.request();
}
return this;
};
Request.prototype.build_path = function(build) {
if (build) {
this.message.build_path = true;
}
return this;
};
Request.prototype.ledger_choose = function(current) {
if (current) {
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) {
switch (ledger_spec) {
case 'current':
case 'closed':
case 'verified':
this.message.ledger_index = ledger_spec;
break;
default:
// XXX Better test needed
if (String(ledger_spec).length > 12) {
this.message.ledger_hash = ledger_spec;
} else {
this.message.ledger_index = ledger_spec;
}
}
return this;
};
Request.prototype.account_root = function(account) {
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 (!Array.isArray(accounts)) {
accounts = [ accounts ];
}
// Process accounts parameters
var procAccounts = accounts.map(function(account) {
return UInt160.json_rewrite(account);
});
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, snapshot) {
var procBooks = [];
for (var i = 0, l = books.length; i < l; i++) {
var book = books[i];
var json = {};
function processSide(side) {
if (!book[side]) throw new Error('Missing '+side);
var obj = {};
obj['currency'] = Currency.json_rewrite(book[side]['currency']);
if (obj['currency'] !== 'XRP') {
obj.issuer = UInt160.json_rewrite(book[side]['issuer']);
}
json[side] = obj;
}
processSide('taker_gets');
processSide('taker_pays');
if (snapshot || book['snapshot']) json['snapshot'] = true;
if (book['both']) json['both'] = true;
procBooks.push(json);
}
this.message.books = procBooks;
return this;
};
//------------------------------------------------------------------------------
/**
Interface to manage the connection to a Ripple server.
This implementation uses WebSockets.
Keys for opts:
trusted : truthy, if remote is trusted
websocket_ip
websocket_port
websocket_ssl
trace
maxListeners
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
*/
var Remote = function(opts, trace) {
EventEmitter.call(this);
var self = this;
this.trusted = opts.trusted;
this.local_sequence = opts.local_sequence; // Locally track sequence numbers
this.local_fee = opts.local_fee; // Locally set fees
this.local_signing = ('undefined' === typeof opts.local_signing)
? true : opts.local_signing;
this.id = 0;
this.trace = opts.trace || trace;
this._server_fatal = false; // True, if we know server exited.
this._ledger_current_index = void(0);
this._ledger_hash = void(0);
this._ledger_time = void(0);
this._stand_alone = void(0);
this._testnet = void(0);
this._transaction_subs = 0;
this.online_target = false;
this._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 = 1.0;
this._fee_ref = void(0);
this._fee_base = void(0);
this._reserve_base = void(0);
this._reserve_inc = void(0);
this._connection_count = 0;
this._last_tx = 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 : __ }
};
// Hash map of Account objects by AccountId.
this._accounts = {};
// Hash map of OrderBook objects
this._books = {};
// 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' : {}
}
};
// Support old API
if (!('servers' in opts)) {
opts.servers = [ {
host: opts.websocket_ip,
port: opts.websocket_port,
secure: opts.websocket_ssl,
trusted: opts.trusted
} ]
}
// Initialize servers
opts.servers.forEach(function(server) {
var i = typeof server.pool === 'number' ? server.pool : 1;
while (i--) {
self.add_server(server);
}
});
// This is used to remove EventEmitter warnings
if ('maxListeners' in opts) {
this._servers.concat(this).forEach(function(i) {
i.setMaxListeners(opts.maxListeners);
});
}
this.on('newListener', function(type, listener) {
if ('transaction_all' === type) {
if (!self._transaction_subs && self._online_state === 'open') {
self.request_subscribe('transactions').request();
}
self._transaction_subs += 1;
}
});
this.on('removeListener', function(type, listener) {
if ('transaction_all' === type) {
self._transaction_subs -= 1;
if (!self._transaction_subs && self._online_state === 'open') {
self.request_unsubscribe('transactions').request();
}
}
});
};
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
}
};
Remote.from_config = function(obj, trace) {
var serverConfig = typeof obj === 'string' ? config.servers[obj] : obj;
var remote = new Remote(serverConfig, trace);
for (var account in config.accounts) {
var accountInfo = config.accounts[account];
if (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);
}
}
}
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);
};
Remote.prototype.add_server = function(opts) {
var self = this;
var url = (opts.secure || opts.websocket_ssl ? 'wss://' : 'ws://')
+ (opts.host || opts.websocket_ip) + ':'
+ (opts.port || opts.websocket_port);
var server = new Server(this, { url: url })
if ('maxListeners' in opts) {
server.setMaxListeners(opts.maxListeners);
}
server.on('message', function(data) {
self._handle_message(data);
});
server.on('connect', function() {
if (opts.primary || !self._primary_server) {
self._set_primary_server(server);
}
self._connection_count++;
self._set_state('online');
});
server.on('disconnect', function() {
self._connection_count--;
if (!self._connection_count) {
self._set_state('offline');
}
});
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) {
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;
};
/**
* Connect to the Ripple network.
*/
Remote.prototype.connect = function(online) {
// Downwards compatibility
if (typeof online !== 'undefined' && !online) {
this.disconnect();
} else {
if (!this._servers.length) {
throw new Error('No servers available.');
} else {
for (var i=0, l=this._servers.length; i<l; i++) {
this._servers[i].connect();
}
}
}
return this;
};
/**
* Disconnect from the Ripple network.
*/
Remote.prototype.disconnect = function(online) {
for (var i=0, l=this._servers.length; i<l; i++) {
this._servers[i].disconnect();
}
this._set_state('offline');
return this;
};
Remote.prototype.ledger_hash = function() {
return this._ledger_hash;
};
// It is possible for messages to be dispatched after the connection is closed.
Remote.prototype._handle_message = function(json) {
var self = this;
var unexpected = false;
var request, message;
try {
message = JSON.parse(json);
} catch(exception) {
unexpected = true;
}
if (typeof message !== 'object' || typeof message.type !== 'string') {
unexpected = true;
}
if (!unexpected) {
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);
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
// XXX Should have a cache of n txs so we can dedup out of order txs
if (this._last_tx === message.transaction.hash) break;
this._last_tx = message.transaction.hash;
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]];
if (account) account.notifyTx(message);
}
// Pass the event on to any related OrderBooks
affected = message.mmeta.getAffectedBooks();
for (i=0, l=affected.length; i<l; i++) {
var book = self._books[affected[i]];
if (book) book.notifyTx(message);
}
this.emit('transaction', message);
this.emit('transaction_all', message);
break;
// XXX Should be tracked by the Server object
case 'serverStatus':
if ('load_base' in message && 'load_factor' in message &&
(message.load_base !== self._load_base) ||
(message.load_factor != self._load_factor))
{
self._load_base = message.load_base;
self._load_factor = message.load_factor;
self.emit('load', {
'load_base' : self._load_base,
'load_factor' : self.load_factor
});
}
break;
// All other messages
default:
if (this.trace) {
utils.logObject('remote: ' + message.type+': %s', message);
}
this.emit('net_' + message.type, message);
break;
}
}
// Unexpected response from remote.
if (unexpected) {
console.log('unexpected message from trusted remote: %s', json);
(request || this).emit('error', {
'error': 'remoteUnexpected',
'error_message': 'Unexpected response from remote.'
});
}
};
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<this._servers.length; i++) {
var server = this._servers[i];
if (this._server_is_available(server)) {
result = server;
break;
}
}
return result;
};
Remote.prototype._get_server = function() {
if (this._server_is_available(this._primary_server)) {
return this._primary_server;
} else {
var server = this._next_server();
if (server) this._set_primary_server(server);
return server;
}
};
// Send a request.
// <-> request: what to send, consumed.
Remote.prototype.request = function(request) {
if (!this._servers.length) {
request.emit('error', new Error('No servers available'));
} else {
var server = this._get_server();
if (server) {
server.request(request);
} else {
request.emit('error', new Error('No servers availale'));
}
}
};
Remote.prototype.server_info =
Remote.prototype.request_server_info = function(callback) {
return new Request(this, 'server_info').callback(callback);
};
// XXX This is a bad command. Some varients don't scale.
// XXX Require the server to be trusted.
Remote.prototype.request_ledger = function(ledger, opts, callback) {
//utils.assert(this.trusted);
if (typeof opts === 'function') {
callback = opts;
opts = { };
}
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 (typeof opts === 'object') {
if (opts.full)
request.message.full = true;
if (opts.expand)
request.message.expand = true;
if (opts.transactions)
request.message.transactions = true;
if (opts.accounts)
request.message.accounts = true;
}
// DEPRECATED:
else if (opts) {
console.log('request_ledger: full parameter is deprecated');
request.message.full = true;
}
return request.callback(callback);;
};
// 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 ('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.callback(callback);
};
// .accounts(accounts, realtime)
Remote.prototype.request_subscribe = function(streams, callback) {
var request = new Request(this, 'subscribe');
if (streams) {
if ('object' !== typeof streams) {
streams = [streams];
}
request.message.streams = streams;
}
return request.callback(callback);
};
Remote.prototype.request_unsubscribe = function(streams, callback) {
var request = new Request(this, 'unsubscribe');
if (streams) {
if ('object' !== typeof streams) {
streams = [streams];
}
request.message.streams = streams;
}
return request.callback(callback);
};
// .ledger_choose()
// .ledger_hash()
// .ledger_index()
Remote.prototype.request_transaction_entry = function(hash, callback) {
//utils.assert(this.trusted); // If not trusted, need to check proof, maybe talk packet protocol.
return (new Request(this, 'transaction_entry', callback)).tx_hash(hash);
};
// DEPRECATED: use request_transaction_entry
Remote.prototype.request_tx = function(hash, callback) {
var request = new Request(this, 'tx');
request.message.transaction = hash;
return request.callback(callback);
};
Remote.prototype.request_account_info = function(accountID, callback) {
var request = new Request(this, 'account_info');
request.message.ident = UInt160.json_rewrite(accountID); // DEPRECATED
request.message.account = UInt160.json_rewrite(accountID);
return request.callback(callback);
};
// --> account_index: sub_account index (optional)
// --> current: true, for the current ledger.
Remote.prototype.request_account_lines = function(accountID, account_index, current, callback) {
// 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).callback(callback);
};
// --> account_index: sub_account index (optional)
// --> current: true, for the current ledger.
Remote.prototype.request_account_offers = function(accountID, account_index, current, callback) {
var request = new Request(this, 'account_offers');
request.message.account = UInt160.json_rewrite(accountID);
if (account_index) {
request.message.index = account_index;
}
return request.ledger_choose(current).callback(callback);
};
/*
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(obj, callback) {
// XXX Does this require the server to be trusted?
//utils.assert(this.trusted);
var request = new Request(this, 'account_tx');
request.message.account = obj.account;
if (false && ledger_min === ledger_max) {
//request.message.ledger = ledger_min;
}
else {
if ('undefined' !== typeof obj.ledger_index_min) {request.message.ledger_index_min = obj.ledger_index_min;}
if ('undefined' !== typeof obj.ledger_index_max) {request.message.ledger_index_max = obj.ledger_index_max;}
if ('undefined' !== typeof obj.binary) {request.message.binary = obj.binary;}
if ('undefined' !== typeof obj.count) {request.message.count = obj.count;}
if ('undefined' !== typeof obj.descending) {request.message.descending = obj.descending;}
if ('undefined' !== typeof obj.offset) {request.message.offset = obj.offset;}
if ('undefined' !== typeof obj.limit) {request.message.limit = obj.limit;}
}
return request.callback(callback);
};
Remote.prototype.request_book_offers = function(gets, pays, taker, callback) {
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.callback(callback);
};
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;
return request.callback(callback);
};
// 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.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.
// 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 || 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.emit('subscribed');
});
self.emit('prepare_subscribe', request);
// XXX Could give error events, maybe even time out.
return request.callback(callback);
};
// For unit testing: ask the remote to accept the current ledger.
// - To be notified when the ledger is accepted, server_subscribe() then listen to 'ledger_hash' events.
// A good way to be notified of the result of this is:
// remote.once('ledger_closed', function(ledger_closed, ledger_index) { ... } );
Remote.prototype.ledger_accept = function(callback) {
if (this._stand_alone || undefined === this._stand_alone) {
var request = new Request(this, 'ledger_accept');
request.callback(callback).request();
} else {
var err = { 'error' : 'notStandAlone' }
if (typeof callback === 'function') {
callback(err);
}
this.emit('error', err);
}
return this;
};
// Return a request to refresh the account balance.
Remote.prototype.request_account_balance = function(account, current, callback) {
var request = this.request_ledger_entry('account_root');
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));
});
if (typeof callback === 'function') {
request.callback(callback, 'account_balance');
}
return request;
};
// Return a request to return the account flags.
Remote.prototype.request_account_flags = function(account, current, callback) {
var request = this.request_ledger_entry('account_root');
request.account_root(account)
.ledger_choose(current)
.on('success', function(message) {
// If the caller also waits for 'success', they might run before this.
request.emit('account_flags', message.node.Flags);
});
if (typeof callback === 'function') {
request.callback(callback, 'account_flags');
}
return request;
};
// Return a request to emit the owner count.
Remote.prototype.request_owner_count = function(account, current, callback) {
var request = this.request_ledger_entry('account_root');
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);
});
if (typeof callback === 'function') {
request.callback(callback, 'owner_count');
}
return request;
};
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_gets, issuer_gets, currency_pays, issuer_pays) {
var gets = currency_gets;
if (gets !== 'XRP') gets += '/' + issuer_gets;
var pays = currency_pays;
if (pays !== 'XRP') pays += '/' + issuer_pays;
var key = gets + ':' + pays;
if (!this._books[key]) {
var book = new OrderBook(this, currency_gets, issuer_gets, currency_pays, issuer_pays);
if (!book.is_valid()) return book;
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;
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, callback) {
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;
}
if (typeof callback === 'function') {
request.callback(callback, 'success_account_seq_cache');
}
return request;
};
// Mark an account's root node as dirty.
Remote.prototype.dirty_account_root = function(account) {
var account = UInt160.json_rewrite(account);
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, callback) {
var request = this.request_ledger_entry('ripple_state'); // YYY Could be cached per ledger.
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());
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),
});
});
if (typeof callback === 'function') {
request.callback(callback, 'ripple_state');
}
return request;
};
Remote.prototype.request_ripple_path_find = function(src_account, dst_account, dst_amount, src_currencies, callback) {
var self = this;
var opts = { };
if (typeof src_account === 'object') {
opts = src_account;
} else {
opts.src_account = src_account;
opts.dst_account = dst_account;
opts.dst_amount = dst_amount;
opts.src_currencies = src_currencies;
}
var request = new Request(this, 'ripple_path_find');
request.message.source_account = UInt160.json_rewrite(opts.src_account);
request.message.destination_account = UInt160.json_rewrite(opts.dst_account);
request.message.destination_amount = Amount.json_rewrite(opts.dst_amount);
if (opts.src_currencies) {
request.message.source_currencies = opts.src_currencies.map(function(ci) {
var ci_new = {};
if ('issuer' in ci) {
ci_new.issuer = UInt160.json_rewrite(ci.issuer);
}
if ('currency' in ci) {
ci_new.currency = Currency.json_rewrite(ci.currency);
}
return ci_new;
});
}
return request.callback(callback);
};
Remote.prototype.request_unl_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;
switch (typeof comment) {
case 'string':
request.message.comment = comment;
break;
case 'function':
callback = comment;
break;
}
return request.callback(callback);
};
// --> node: <domain> | <public_key>
Remote.prototype.request_unl_delete = function(node) {
var request = new Request(this, 'unl_delete');
request.message.node = node;
return request.callback(callback);
};
Remote.prototype.request_peers = function(callback) {
return new Request(this, 'peers', callback);
};
Remote.prototype.request_connect = function(ip, port, callback) {
var request = new Request(this, 'connect');
request.message.ip = ip;
if (typeof port !== 'undefined') {
request.message.port = port;
}
return request.callback(callback);
};
Remote.prototype.transaction = function() {
return new Transaction(this);
};
exports.Remote = Remote;
// vim:sw=2:sts=2:ts=8:et