mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-27 14:35:52 +00:00
2333 lines
57 KiB
JavaScript
2333 lines
57 KiB
JavaScript
'use strict';
|
|
|
|
// Interface to manage connections to rippled servers
|
|
//
|
|
// - 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.
|
|
|
|
var _toConsumableArray = require('babel-runtime/helpers/to-consumable-array')['default'];
|
|
|
|
var _Object$keys = require('babel-runtime/core-js/object/keys')['default'];
|
|
|
|
var util = require('util');
|
|
var assert = require('assert');
|
|
var _ = require('lodash');
|
|
var LRU = require('lru-cache');
|
|
var async = require('async');
|
|
var EventEmitter = require('events').EventEmitter;
|
|
var Server = require('./server').Server;
|
|
var Request = require('./request').Request;
|
|
var Amount = require('./amount').Amount;
|
|
var Currency = require('./currency').Currency;
|
|
var UInt160 = require('./uint160').UInt160;
|
|
var UInt256 = require('./uint256').UInt256;
|
|
var Transaction = require('./transaction').Transaction;
|
|
var Account = require('./account').Account;
|
|
var Meta = require('./meta').Meta;
|
|
var OrderBook = require('./orderbook').OrderBook;
|
|
var PathFind = require('./pathfind').PathFind;
|
|
var SerializedObject = require('./serializedobject').SerializedObject;
|
|
var RippleError = require('./rippleerror').RippleError;
|
|
var utils = require('./utils');
|
|
var hashprefixes = require('./hashprefixes');
|
|
var log = require('./log').internal.sub('remote');
|
|
|
|
/**
|
|
* Interface to manage connections to rippled servers
|
|
*
|
|
* @param {Object} Options
|
|
*/
|
|
|
|
function Remote() {
|
|
var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
|
|
|
|
EventEmitter.call(this);
|
|
|
|
var self = this;
|
|
|
|
_.merge(this, _.defaults(options, Remote.DEFAULTS));
|
|
|
|
this.state = 'offline'; // 'online', 'offline'
|
|
this._server_fatal = false; // server exited
|
|
this._stand_alone = undefined;
|
|
this._testnet = undefined;
|
|
|
|
this._ledger_current_index = undefined;
|
|
this._ledger_hash = undefined;
|
|
this._ledger_time = undefined;
|
|
|
|
this._connection_count = 0;
|
|
this._connected = false;
|
|
this._should_connect = true;
|
|
|
|
this._transaction_listeners = 0;
|
|
this._received_tx = new LRU({ max: 100 });
|
|
this._cur_path_find = null;
|
|
this._queued_path_finds = [];
|
|
|
|
if (this.local_signing) {
|
|
// Local signing implies local fees and sequences
|
|
this.local_sequence = true;
|
|
this.local_fee = true;
|
|
}
|
|
|
|
this._servers = [];
|
|
this._primary_server = undefined;
|
|
|
|
// Cache information for accounts.
|
|
// DEPRECATED, will be removed
|
|
// 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 : __ }
|
|
this.accounts = {};
|
|
|
|
// Account objects by AccountId.
|
|
this._accounts = {};
|
|
|
|
// OrderBook objects
|
|
this._books = {};
|
|
|
|
// Secrets that we know about.
|
|
// Secrets can be set by calling setSecret(account, secret).
|
|
// account : secret
|
|
this.secrets = {};
|
|
|
|
// Cache for various ledgers.
|
|
// XXX Clear when ledger advances.
|
|
this.ledgers = {
|
|
current: {
|
|
account_root: {}
|
|
}
|
|
};
|
|
|
|
if (typeof this.trusted !== 'boolean') {
|
|
throw new TypeError('trusted must be a boolean');
|
|
}
|
|
if (typeof this.trace !== 'boolean') {
|
|
throw new TypeError('trace must be a boolean');
|
|
}
|
|
if (typeof this.allow_partial_history !== 'boolean') {
|
|
throw new TypeError('allow_partial_history must be a boolean');
|
|
}
|
|
if (typeof this.max_fee !== 'number') {
|
|
throw new TypeError('max_fee must be a number');
|
|
}
|
|
if (typeof this.max_attempts !== 'number') {
|
|
throw new TypeError('max_attempts must be a number');
|
|
}
|
|
if (typeof this.fee_cushion !== 'number') {
|
|
throw new TypeError('fee_cushion must be a number');
|
|
}
|
|
if (typeof this.local_signing !== 'boolean') {
|
|
throw new TypeError('local_signing must be a boolean');
|
|
}
|
|
if (typeof this.local_fee !== 'boolean') {
|
|
throw new TypeError('local_fee must be a boolean');
|
|
}
|
|
if (typeof this.local_sequence !== 'boolean') {
|
|
throw new TypeError('local_sequence must be a boolean');
|
|
}
|
|
if (typeof this.canonical_signing !== 'boolean') {
|
|
throw new TypeError('canonical_signing must be a boolean');
|
|
}
|
|
if (typeof this.submission_timeout !== 'number') {
|
|
throw new TypeError('submission_timeout must be a number');
|
|
}
|
|
if (typeof this.automatic_resubmission !== 'boolean') {
|
|
throw new TypeError('automatic_resubmission must be a boolean');
|
|
}
|
|
if (typeof this.last_ledger_offset !== 'number') {
|
|
throw new TypeError('last_ledger_offset must be a number');
|
|
}
|
|
if (!Array.isArray(this.servers)) {
|
|
throw new TypeError('servers must be an array');
|
|
}
|
|
|
|
this.setMaxListeners(this.max_listeners);
|
|
|
|
this.servers.forEach(function (serverOptions) {
|
|
var server = self.addServer(serverOptions);
|
|
server.setMaxListeners(self.max_listeners);
|
|
});
|
|
|
|
function listenersModified(action, event) {
|
|
// Automatically subscribe and unsubscribe to orderbook
|
|
// on the basis of existing event listeners
|
|
if (_.contains(Remote.TRANSACTION_EVENTS, event)) {
|
|
switch (action) {
|
|
case 'add':
|
|
if (++self._transaction_listeners === 1) {
|
|
self.requestSubscribe('transactions').request();
|
|
}
|
|
break;
|
|
case 'remove':
|
|
if (--self._transaction_listeners === 0) {
|
|
self.requestUnsubscribe('transactions').request();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.on('newListener', function (event) {
|
|
listenersModified('add', event);
|
|
});
|
|
|
|
this.on('removeListener', function (event) {
|
|
listenersModified('remove', event);
|
|
});
|
|
}
|
|
|
|
util.inherits(Remote, EventEmitter);
|
|
|
|
Remote.DEFAULTS = {
|
|
trusted: false,
|
|
trace: false,
|
|
allow_partial_history: true,
|
|
local_sequence: true,
|
|
local_fee: true,
|
|
local_signing: true,
|
|
canonical_signing: true,
|
|
fee_cushion: 1.2,
|
|
max_fee: 1000000, // 1 XRP
|
|
max_attempts: 10,
|
|
submission_timeout: 1000 * 20,
|
|
automatic_resubmission: true,
|
|
last_ledger_offset: 3,
|
|
servers: [],
|
|
max_listeners: 0 // remove Node EventEmitter warnings
|
|
};
|
|
|
|
Remote.TRANSACTION_EVENTS = ['transaction', 'transaction_all'];
|
|
|
|
// Flags for ledger entries. In support of accountRoot().
|
|
Remote.flags = {
|
|
// AccountRoot
|
|
account_root: {
|
|
PasswordSpent: 0x00010000, // password set fee is spent
|
|
RequireDestTag: 0x00020000, // require a DestinationTag for payments
|
|
RequireAuth: 0x00040000, // require a authorization to hold IOUs
|
|
DisallowXRP: 0x00080000, // disallow sending XRP
|
|
DisableMaster: 0x00100000, // force regular key
|
|
NoFreeze: 0x00200000, // permanently disallowed freezing trustlines
|
|
GlobalFreeze: 0x00400000, // trustlines globally frozen
|
|
DefaultRipple: 0x00800000
|
|
},
|
|
// Offer
|
|
offer: {
|
|
Passive: 0x00010000,
|
|
Sell: 0x00020000 // offer was placed as a sell
|
|
},
|
|
// Ripple tate
|
|
state: {
|
|
LowReserve: 0x00010000, // entry counts toward reserve
|
|
HighReserve: 0x00020000,
|
|
LowAuth: 0x00040000,
|
|
HighAuth: 0x00080000,
|
|
LowNoRipple: 0x00100000,
|
|
HighNoRipple: 0x00200000
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Check that server message is valid
|
|
*
|
|
* @param {Object} message
|
|
* @return Boolean
|
|
*/
|
|
|
|
Remote.isValidMessage = function (message) {
|
|
return typeof message === 'object' && typeof message.type === 'string';
|
|
};
|
|
|
|
/**
|
|
* Check that server message contains valid
|
|
* ledger data
|
|
*
|
|
* @param {Object} message
|
|
* @return {Boolean}
|
|
*/
|
|
|
|
Remote.isValidLedgerData = function (message) {
|
|
return typeof message === 'object' && typeof message.fee_base === 'number' && typeof message.fee_ref === 'number' && typeof message.ledger_hash === 'string' && typeof message.ledger_index === 'number' && typeof message.ledger_time === 'number' && typeof message.reserve_base === 'number' && typeof message.reserve_inc === 'number';
|
|
};
|
|
|
|
/**
|
|
* Check that server message contains valid
|
|
* load status data
|
|
*
|
|
* @param {Object} message
|
|
* @return {Boolean}
|
|
*/
|
|
|
|
Remote.isValidLoadStatus = function (message) {
|
|
return typeof message.load_base === 'number' && typeof message.load_factor === 'number';
|
|
};
|
|
|
|
/**
|
|
* Check that provided ledger is validated
|
|
*
|
|
* @param {Object} ledger
|
|
* @return {Boolean}
|
|
*/
|
|
|
|
Remote.isValidated = function (message) {
|
|
return message && typeof message === 'object' && message.validated === true;
|
|
};
|
|
|
|
/**
|
|
* Set the emitted state: 'online' or 'offline'
|
|
*
|
|
* @param {String} state
|
|
*/
|
|
|
|
Remote.prototype._setState = function (state) {
|
|
if (this.state !== state) {
|
|
if (this.trace) {
|
|
log.info('set_state:', state);
|
|
}
|
|
|
|
this.state = state;
|
|
this.emit('state', state);
|
|
|
|
switch (state) {
|
|
case 'online':
|
|
this._online_state = 'open';
|
|
this._connected = true;
|
|
this.emit('connect');
|
|
this.emit('connected');
|
|
break;
|
|
case 'offline':
|
|
this._online_state = 'closed';
|
|
this._connected = false;
|
|
this.emit('disconnect');
|
|
this.emit('disconnected');
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Inform remote that the remote server is not comming back.
|
|
*/
|
|
|
|
Remote.prototype.setServerFatal = function () {
|
|
this._server_fatal = true;
|
|
};
|
|
|
|
/**
|
|
* Enable debug output
|
|
*
|
|
* @param {Boolean} trace
|
|
*/
|
|
|
|
Remote.prototype.setTrace = function (trace) {
|
|
this.trace = trace === undefined || trace;
|
|
return this;
|
|
};
|
|
|
|
Remote.prototype._trace = function () {
|
|
if (this.trace) {
|
|
log.info.apply(log, arguments);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Store a secret - allows the Remote to automatically fill
|
|
* out auth information.
|
|
*
|
|
* @param {String} account
|
|
* @param {String} secret
|
|
*/
|
|
|
|
Remote.prototype.setSecret = function (account, secret) {
|
|
this.secrets[account] = secret;
|
|
};
|
|
|
|
Remote.prototype.addServer = function (options) {
|
|
var self = this;
|
|
var server = new Server(this, options);
|
|
|
|
function serverMessage(data) {
|
|
self._handleMessage(data, server);
|
|
}
|
|
|
|
server.on('message', serverMessage);
|
|
|
|
function serverConnect() {
|
|
self._connection_count += 1;
|
|
|
|
if (options.primary) {
|
|
self._setPrimaryServer(server);
|
|
}
|
|
if (self._connection_count === 1) {
|
|
self._setState('online');
|
|
}
|
|
if (self._connection_count === self._servers.length) {
|
|
self.emit('ready');
|
|
}
|
|
}
|
|
|
|
server.on('connect', serverConnect);
|
|
|
|
function serverDisconnect() {
|
|
self._connection_count--;
|
|
if (self._connection_count === 0) {
|
|
self._setState('offline');
|
|
}
|
|
}
|
|
|
|
server.on('disconnect', serverDisconnect);
|
|
|
|
this._servers.push(server);
|
|
|
|
return server;
|
|
};
|
|
|
|
/**
|
|
* Reconnect to Ripple network
|
|
*/
|
|
|
|
Remote.prototype.reconnect = function () {
|
|
if (!this._should_connect) {
|
|
return;
|
|
}
|
|
|
|
log.info('reconnecting');
|
|
|
|
this._servers.forEach(function (server) {
|
|
server.reconnect();
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Connect to the Ripple network
|
|
*
|
|
* @param {Function} callback
|
|
* @api public
|
|
*/
|
|
|
|
Remote.prototype.connect = function (callback) {
|
|
if (!this._servers.length) {
|
|
throw new Error('No servers available.');
|
|
}
|
|
|
|
if (typeof callback === 'function') {
|
|
this.once('connect', callback);
|
|
}
|
|
|
|
this._should_connect = true;
|
|
|
|
this._servers.forEach(function (server) {
|
|
server.connect();
|
|
});
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Disconnect from the Ripple network.
|
|
*
|
|
* @param {Function} callback
|
|
* @api public
|
|
*/
|
|
|
|
Remote.prototype.disconnect = function (callback_) {
|
|
if (!this._servers.length) {
|
|
throw new Error('No servers available, not disconnecting');
|
|
}
|
|
|
|
var callback = _.isFunction(callback_) ? callback_ : function () {};
|
|
|
|
this._should_connect = false;
|
|
|
|
if (!this.isConnected()) {
|
|
callback();
|
|
return this;
|
|
}
|
|
|
|
this.once('disconnect', callback);
|
|
|
|
this._servers.forEach(function (server) {
|
|
server.disconnect();
|
|
});
|
|
|
|
this._setState('offline');
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Handle server message. Server messages are proxied to
|
|
* the Remote, such that global events can be handled
|
|
*
|
|
* It is possible for messages to be dispatched after the
|
|
* connection is closed.
|
|
*
|
|
* @param {JSON} message
|
|
* @param {Server} server
|
|
*/
|
|
|
|
Remote.prototype._handleMessage = function (message, server) {
|
|
if (!Remote.isValidMessage(message)) {
|
|
// Unexpected response from remote.
|
|
var error = new RippleError('remoteUnexpected', 'Unexpected response from remote: ' + JSON.stringify(message));
|
|
|
|
this.emit('error', error);
|
|
log.error(error);
|
|
return;
|
|
}
|
|
|
|
switch (message.type) {
|
|
case 'ledgerClosed':
|
|
this._handleLedgerClosed(message, server);
|
|
break;
|
|
case 'serverStatus':
|
|
this._handleServerStatus(message, server);
|
|
break;
|
|
case 'transaction':
|
|
this._handleTransaction(message, server);
|
|
break;
|
|
case 'path_find':
|
|
this._handlePathFind(message, server);
|
|
break;
|
|
case 'validationReceived':
|
|
this._handleValidationReceived(message, server);
|
|
break;
|
|
default:
|
|
if (this.trace) {
|
|
log.info(message.type + ': ', message);
|
|
}
|
|
break;
|
|
}
|
|
};
|
|
|
|
Remote.prototype.getLedgerSequence = function () {
|
|
if (!this._ledger_current_index) {
|
|
throw new Error('Ledger sequence has not yet been initialized');
|
|
}
|
|
// the "current" ledger is the one after the most recently closed ledger
|
|
return this._ledger_current_index - 1;
|
|
};
|
|
|
|
/**
|
|
* Handle server ledger_closed event
|
|
*
|
|
* @param {Object} message
|
|
*/
|
|
|
|
Remote.prototype._handleLedgerClosed = function (message, server) {
|
|
var self = this;
|
|
|
|
// 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.
|
|
if (!Remote.isValidLedgerData(message)) {
|
|
return;
|
|
}
|
|
|
|
var ledgerAdvanced = message.ledger_index >= this._ledger_current_index;
|
|
|
|
if (isNaN(this._ledger_current_index) || ledgerAdvanced) {
|
|
this._ledger_time = message.ledger_time;
|
|
this._ledger_hash = message.ledger_hash;
|
|
this._ledger_current_index = message.ledger_index + 1;
|
|
|
|
if (this.isConnected()) {
|
|
this.emit('ledger_closed', message, server);
|
|
} else {
|
|
this.once('connect', function () {
|
|
// Delay until server is 'online'
|
|
self.emit('ledger_closed', message, server);
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Handle server validation_received event
|
|
*
|
|
* @param {Object} message
|
|
*/
|
|
|
|
Remote.prototype._handleValidationReceived = function (message, server) {
|
|
this.emit('validation_received', message, server);
|
|
};
|
|
|
|
/**
|
|
* Handle server server_status event
|
|
*
|
|
* @param {Object} message
|
|
*/
|
|
|
|
Remote.prototype._handleServerStatus = function (message, server) {
|
|
this.emit('server_status', message, server);
|
|
};
|
|
|
|
/**
|
|
* Handle server transaction event
|
|
*
|
|
* @param {Object} message
|
|
*/
|
|
|
|
Remote.prototype._handleTransaction = function (message, server) {
|
|
// XXX If not trusted, need proof.
|
|
var transactionHash = message.transaction.hash;
|
|
|
|
if (this._received_tx.get(transactionHash)) {
|
|
// De-duplicate transactions
|
|
return;
|
|
}
|
|
|
|
if (message.validated) {
|
|
this._received_tx.set(transactionHash, true);
|
|
}
|
|
|
|
if (this.trace) {
|
|
log.info('tx:', message);
|
|
}
|
|
|
|
var metadata = message.meta || message.metadata;
|
|
|
|
if (metadata) {
|
|
// Process metadata
|
|
message.mmeta = new Meta(metadata);
|
|
|
|
// Pass the event on to any related Account objects
|
|
message.mmeta.getAffectedAccounts().forEach(function (account) {
|
|
if (this._accounts[account]) {
|
|
this._accounts[account].notify(message);
|
|
}
|
|
}, this);
|
|
|
|
// Pass the event on to any related OrderBooks
|
|
message.mmeta.getAffectedBooks().forEach(function (book) {
|
|
if (this._books[book]) {
|
|
this._books[book].notify(message);
|
|
}
|
|
}, this);
|
|
} else {
|
|
// Transaction could be from proposed transaction stream
|
|
// XX
|
|
['Account', 'Destination'].forEach(function (prop) {
|
|
if (this._accounts[message.transaction[prop]]) {
|
|
this._accounts[message.transaction[prop]].notify(message);
|
|
}
|
|
}, this);
|
|
}
|
|
|
|
this.emit('transaction', message, server);
|
|
this.emit('transaction_all', message, server);
|
|
};
|
|
|
|
/**
|
|
* Handle server path_find event
|
|
*
|
|
* @param {Object} message
|
|
*/
|
|
|
|
Remote.prototype._handlePathFind = function (message, server) {
|
|
// Pass the event to the currently open PathFind object
|
|
if (this._cur_path_find) {
|
|
this._cur_path_find.notify_update(message);
|
|
}
|
|
|
|
this.emit('path_find_all', message, server);
|
|
};
|
|
|
|
/**
|
|
* Returns the current ledger hash
|
|
*
|
|
* @return {String} ledger hash
|
|
*/
|
|
|
|
Remote.prototype.getLedgerHash = function () {
|
|
return this._ledger_hash;
|
|
};
|
|
|
|
/**
|
|
* Set primary server. Primary server will be selected
|
|
* to handle requested regardless of its internally-tracked
|
|
* priority score
|
|
*
|
|
* @param {Server} server
|
|
*/
|
|
|
|
Remote.prototype._setPrimaryServer = Remote.prototype.setPrimaryServer = function (server) {
|
|
if (this._primary_server) {
|
|
this._primary_server._primary = false;
|
|
}
|
|
this._primary_server = server;
|
|
this._primary_server._primary = true;
|
|
};
|
|
|
|
/**
|
|
* Get connected state
|
|
*
|
|
* @return {Boolean} connected
|
|
*/
|
|
|
|
Remote.prototype.isConnected = function () {
|
|
return this._connected;
|
|
};
|
|
|
|
/**
|
|
* Get array of connected servers
|
|
*/
|
|
|
|
Remote.prototype.getConnectedServers = function () {
|
|
return this._servers.filter(function (server) {
|
|
return server.isConnected();
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Select a server to handle a request. Servers are
|
|
* automatically prioritized
|
|
*/
|
|
|
|
Remote.prototype._getServer = Remote.prototype.getServer = function () {
|
|
if (this._primary_server && this._primary_server.isConnected()) {
|
|
return this._primary_server;
|
|
}
|
|
|
|
if (!this._servers.length) {
|
|
return null;
|
|
}
|
|
|
|
var connectedServers = this.getConnectedServers();
|
|
if (connectedServers.length === 0 || !connectedServers[0]) {
|
|
return null;
|
|
}
|
|
|
|
var server = connectedServers[0];
|
|
var cScore = server._score + server._fee;
|
|
|
|
for (var i = 1; i < connectedServers.length; i++) {
|
|
var _server = connectedServers[i];
|
|
var bScore = _server._score + _server._fee;
|
|
if (bScore < cScore) {
|
|
server = _server;
|
|
cScore = bScore;
|
|
}
|
|
}
|
|
|
|
return server;
|
|
};
|
|
|
|
/**
|
|
* Send a request. This method is called internally by Request
|
|
* objects. Each Request contains a reference to Remote, and
|
|
* Request.request calls Request.remote.request
|
|
*
|
|
* @param {Request} request
|
|
*/
|
|
|
|
Remote.prototype.request = function (request) {
|
|
if (typeof request === 'string') {
|
|
var prefix = /^request_/.test(request) ? '' : 'request_';
|
|
var requestName = prefix + request;
|
|
var methodName = requestName.replace(/(\_\w)/g, function (m) {
|
|
return m[1].toUpperCase();
|
|
});
|
|
|
|
if (typeof this[methodName] === 'function') {
|
|
var args = _.slice(arguments, 1);
|
|
return this[methodName].apply(this, args);
|
|
}
|
|
|
|
throw new Error('Command does not exist: ' + requestName);
|
|
}
|
|
|
|
if (!(request instanceof Request)) {
|
|
throw new Error('Argument is not a Request');
|
|
}
|
|
|
|
if (!this._servers.length) {
|
|
return request.emit('error', new Error('No servers available'));
|
|
}
|
|
if (!this.isConnected()) {
|
|
return this.once('connect', this.request.bind(this, request));
|
|
}
|
|
if (request.server === null) {
|
|
return request.emit('error', new Error('Server does not exist'));
|
|
}
|
|
|
|
var server = request.server || this.getServer();
|
|
if (server) {
|
|
server._request(request);
|
|
} else {
|
|
request.emit('error', new Error('No servers available'));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Request ping
|
|
*
|
|
* @param [String] server host
|
|
* @param [Function] callback
|
|
* @return {Request} request
|
|
*/
|
|
|
|
Remote.prototype.ping = Remote.prototype.requestPing = function (host, callback_) {
|
|
var request = new Request(this, 'ping');
|
|
var callback = callback_;
|
|
|
|
switch (typeof host) {
|
|
case 'function':
|
|
callback = host;
|
|
break;
|
|
case 'string':
|
|
request.setServer(host);
|
|
break;
|
|
}
|
|
|
|
var then = Date.now();
|
|
|
|
request.once('success', function () {
|
|
request.emit('pong', Date.now() - then);
|
|
});
|
|
|
|
request.callback(callback, 'pong');
|
|
|
|
return request;
|
|
};
|
|
|
|
/**
|
|
* Request server_info
|
|
*
|
|
* @param [Function] callback
|
|
* @return {Request} request
|
|
*/
|
|
|
|
Remote.prototype.requestServerInfo = function (callback) {
|
|
return new Request(this, 'server_info').callback(callback);
|
|
};
|
|
|
|
/**
|
|
* Request ledger
|
|
*
|
|
* @return {Request} request
|
|
*/
|
|
|
|
Remote.prototype.requestLedger = function (options, callback_) {
|
|
// XXX This is a bad command. Some variants don't scale.
|
|
// XXX Require the server to be trusted.
|
|
// utils.assert(this.trusted);
|
|
|
|
var request = new Request(this, 'ledger');
|
|
var callback = callback_;
|
|
|
|
switch (typeof options) {
|
|
case 'undefined':
|
|
break;
|
|
case 'function':
|
|
callback = options;
|
|
break;
|
|
|
|
case 'object':
|
|
if (!options) {
|
|
break;
|
|
}
|
|
|
|
_Object$keys(options).forEach(function (o) {
|
|
switch (o) {
|
|
case 'full':
|
|
case 'expand':
|
|
case 'transactions':
|
|
case 'accounts':
|
|
request.message[o] = options[o] ? true : false;
|
|
break;
|
|
case 'ledger':
|
|
request.selectLedger(options.ledger);
|
|
break;
|
|
case 'ledger_index':
|
|
case 'ledger_hash':
|
|
request.message[o] = options[o];
|
|
break;
|
|
case 'closed':
|
|
case 'current':
|
|
case 'validated':
|
|
request.message.ledger_index = o;
|
|
break;
|
|
}
|
|
}, options);
|
|
break;
|
|
|
|
default:
|
|
request.selectLedger(options);
|
|
break;
|
|
}
|
|
|
|
request.callback(callback);
|
|
|
|
return request;
|
|
};
|
|
|
|
/**
|
|
* Request ledger_closed
|
|
*
|
|
* @param [Function] callback
|
|
* @return {Request} request
|
|
*/
|
|
|
|
Remote.prototype.requestLedgerClosed = Remote.prototype.requestLedgerHash = function (callback) {
|
|
// utils.assert(this.trusted); // If not trusted, need to check proof.
|
|
return new Request(this, 'ledger_closed').callback(callback);
|
|
};
|
|
|
|
/**
|
|
* Request ledger_header
|
|
*
|
|
* @param [Function] callback
|
|
* @return {Request} request
|
|
*/
|
|
|
|
Remote.prototype.requestLedgerHeader = function (callback) {
|
|
return new Request(this, 'ledger_header').callback(callback);
|
|
};
|
|
|
|
/**
|
|
* Request ledger_current
|
|
*
|
|
* Get the current proposed ledger entry. May be closed (and revised)
|
|
* at any time (even before returning).
|
|
*
|
|
* Only for unit testing.
|
|
*
|
|
* @param [Function] callback
|
|
* @return {Request} request
|
|
*/
|
|
|
|
Remote.prototype.requestLedgerCurrent = function (callback) {
|
|
return new Request(this, 'ledger_current').callback(callback);
|
|
};
|
|
|
|
/**
|
|
* Request ledger_data
|
|
*
|
|
* Get the contents of a specified ledger
|
|
*
|
|
* @param {Object} options
|
|
* @property {Boolean} [options.binary]- Flag which determines if rippled
|
|
* returns binary or parsed JSON
|
|
* @property {String|Number} [options.ledger] - Hash or sequence of a ledger
|
|
* to get contents for
|
|
* @property {Number} [options.limit] - Number of contents to retrieve
|
|
* from the ledger
|
|
* @property {Function} callback
|
|
*
|
|
* @callback
|
|
* @param {Error} error
|
|
* @param {LedgerData} ledgerData
|
|
*
|
|
* @return {Request} request
|
|
*/
|
|
|
|
Remote.prototype.requestLedgerData = function (options, callback) {
|
|
var request = new Request(this, 'ledger_data');
|
|
|
|
request.message.binary = options.binary !== false;
|
|
request.selectLedger(options.ledger);
|
|
request.message.limit = options.limit;
|
|
|
|
request.once('success', function (res) {
|
|
if (options.binary === false) {
|
|
request.emit('state', res);
|
|
return;
|
|
}
|
|
|
|
function iterator(ledgerData, next) {
|
|
async.setImmediate(function () {
|
|
next(null, Remote.parseBinaryLedgerData(ledgerData));
|
|
});
|
|
}
|
|
|
|
function complete(err, state) {
|
|
if (err) {
|
|
request.emit('error', err);
|
|
} else {
|
|
res.state = state;
|
|
request.emit('state', res);
|
|
}
|
|
}
|
|
|
|
async.mapSeries(res.state, iterator, complete);
|
|
});
|
|
|
|
request.callback(callback, 'state');
|
|
|
|
return request;
|
|
};
|
|
|
|
/**
|
|
* Request ledger_entry
|
|
*
|
|
* @param [String] type
|
|
* @param [Function] callback
|
|
* @return {Request} request
|
|
*/
|
|
|
|
Remote.prototype.requestLedgerEntry = 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');
|
|
var callback = _.isFunction(type) ? type : callback_;
|
|
|
|
// Transparent caching. When .request() is invoked, look in the Remote object
|
|
// for the result. If not found, listen, cache result, and emit it.
|
|
//
|
|
// Transparent caching:
|
|
if (type === 'account_root') {
|
|
request.request_default = request.request;
|
|
|
|
request.request = function () {
|
|
// Intercept default request.
|
|
var bDefault = true;
|
|
|
|
if (!self._ledger_hash && type === 'account_root') {
|
|
var cache = self.ledgers.current.account_root;
|
|
|
|
if (!cache) {
|
|
cache = self.ledgers.current.account_root = {};
|
|
}
|
|
|
|
var node = self.ledgers.current.account_root[request.message.account_root];
|
|
|
|
if (node) {
|
|
// Emulate fetch of ledger entry.
|
|
// YYY Missing lots of fields.
|
|
request.emit('success', { node: node });
|
|
bDefault = false;
|
|
} else {
|
|
// Was not cached.
|
|
// XXX Only allow with trusted mode. Must sync response with advance
|
|
switch (type) {
|
|
case 'account_root':
|
|
request.once('success', function (message) {
|
|
// Cache node.
|
|
self.ledgers.current.account_root[message.node.Account] = message.node;
|
|
});
|
|
break;
|
|
|
|
default:
|
|
// This type not cached.
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bDefault) {
|
|
request.request_default();
|
|
}
|
|
};
|
|
}
|
|
|
|
request.callback(callback);
|
|
|
|
return request;
|
|
};
|
|
|
|
/**
|
|
* Request subscribe
|
|
*
|
|
* @param {Array} streams
|
|
* @param [Function] callback
|
|
* @return {Request} request
|
|
*/
|
|
|
|
Remote.prototype.requestSubscribe = function (streams, callback) {
|
|
var request = new Request(this, 'subscribe');
|
|
|
|
if (streams) {
|
|
request.message.streams = Array.isArray(streams) ? streams : [streams];
|
|
}
|
|
|
|
request.callback(callback);
|
|
|
|
return request;
|
|
};
|
|
|
|
/**
|
|
* Request usubscribe
|
|
*
|
|
* @param {Array} streams
|
|
* @param [Function] callback
|
|
* @return {Request} request
|
|
*/
|
|
|
|
Remote.prototype.requestUnsubscribe = function (streams, callback) {
|
|
var request = new Request(this, 'unsubscribe');
|
|
|
|
if (streams) {
|
|
request.message.streams = Array.isArray(streams) ? streams : [streams];
|
|
}
|
|
|
|
request.callback(callback);
|
|
|
|
return request;
|
|
};
|
|
|
|
/**
|
|
* Request transaction_entry
|
|
*
|
|
* @param {Object} options -
|
|
* @param {String} [options.transaction] - hash
|
|
* @param {String|Number} [options.ledger='validated'] - hash or sequence
|
|
* @param [Function] callback
|
|
* @return {Request} request
|
|
*/
|
|
|
|
Remote.prototype.requestTransactionEntry = function (options, callback) {
|
|
var request = new Request(this, 'transaction_entry');
|
|
request.txHash(options.hash);
|
|
request.selectLedger(options.ledger, 'validated');
|
|
request.callback(callback);
|
|
return request;
|
|
};
|
|
|
|
/**
|
|
* Request tx
|
|
*
|
|
* @param {Object|String} hash
|
|
* @property {String} hash.hash - Transaction hash
|
|
* @property {Boolean} [hash.binary=true] - Flag which determines if rippled
|
|
* returns binary or parsed JSON
|
|
* @param [Function] callback
|
|
* @return {Request} request
|
|
*/
|
|
|
|
Remote.prototype.requestTransaction = function (options, callback) {
|
|
var request = new Request(this, 'tx');
|
|
request.message.binary = options.binary !== false;
|
|
request.message.transaction = options.hash;
|
|
|
|
request.once('success', function (res) {
|
|
if (options.binary === false) {
|
|
request.emit('transaction', res);
|
|
} else {
|
|
request.emit('transaction', Remote.parseBinaryTransaction(res));
|
|
}
|
|
});
|
|
|
|
request.callback(callback, 'transaction');
|
|
|
|
return request;
|
|
};
|
|
|
|
/**
|
|
* Account Request
|
|
*
|
|
* Optional paging with limit and marker options
|
|
* supported in rippled for 'account_lines' and 'account_offers'
|
|
*
|
|
* The paged responses aren't guaranteed to be reliable between
|
|
* ledger closes. You have to supply a ledger_index or ledger_hash
|
|
* when paging to ensure a complete response
|
|
*
|
|
* @param {String} command - request command, e.g. 'account_lines'
|
|
* @param {Object} options - all optional
|
|
* @param {String} account - ripple address
|
|
* @param {String} peer - ripple address
|
|
* @param [String|Number] ledger identifier
|
|
* @param [Number] limit - max results per response
|
|
* @param {String} marker - start position in response paging
|
|
* @param [Function] callback
|
|
* @return {Request}
|
|
* @throws {Error} if a marker is provided, but no ledger_index or ledger_hash
|
|
*/
|
|
|
|
Remote.accountRequest = function (command, options, callback) {
|
|
if (options.marker) {
|
|
if (!(Number(options.ledger) > 0) && !UInt256.is_valid(options.ledger)) {
|
|
throw new Error('A ledger_index or ledger_hash must be provided when using a marker');
|
|
}
|
|
}
|
|
|
|
var request = new Request(this, command);
|
|
|
|
request.message.account = UInt160.json_rewrite(options.account);
|
|
request.selectLedger(options.ledger);
|
|
|
|
if (UInt160.is_valid(options.peer)) {
|
|
request.message.peer = UInt160.json_rewrite(options.peer);
|
|
}
|
|
|
|
if (!isNaN(options.limit)) {
|
|
var _limit = Number(options.limit);
|
|
|
|
// max for 32-bit unsigned int is 4294967295
|
|
// we'll clamp to 1e9
|
|
if (_limit > 1e9) {
|
|
_limit = 1e9;
|
|
}
|
|
// min for 32-bit unsigned int is 0
|
|
// we'll clamp to 0
|
|
if (_limit < 0) {
|
|
_limit = 0;
|
|
}
|
|
|
|
request.message.limit = _limit;
|
|
}
|
|
|
|
if (options.marker) {
|
|
request.message.marker = options.marker;
|
|
}
|
|
|
|
request.callback(callback);
|
|
|
|
return request;
|
|
};
|
|
|
|
/**
|
|
* Request account_info
|
|
*
|
|
* @param {Object} options
|
|
* @param {String} account - ripple address
|
|
* @param {String} peer - ripple address
|
|
* @param [String|Number] ledger identifier
|
|
* @param [Function] callback
|
|
* @return {Request}
|
|
*/
|
|
|
|
Remote.prototype.requestAccountInfo = function () {
|
|
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
|
|
args[_key] = arguments[_key];
|
|
}
|
|
|
|
var options = ['account_info'].concat(args);
|
|
return Remote.accountRequest.apply(this, options);
|
|
};
|
|
|
|
/**
|
|
* Request account_currencies
|
|
*
|
|
* @param {Object} options
|
|
* @param {String} account - ripple address
|
|
* @param {String} peer - ripple address
|
|
* @param [String|Number] ledger identifier
|
|
* @param [Function] callback
|
|
* @return {Request}
|
|
*/
|
|
|
|
Remote.prototype.requestAccountCurrencies = function () {
|
|
for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
|
|
args[_key2] = arguments[_key2];
|
|
}
|
|
|
|
var options = ['account_currencies'].concat(args);
|
|
return Remote.accountRequest.apply(this, options);
|
|
};
|
|
|
|
/**
|
|
* Request account_lines
|
|
*
|
|
* Requests for account_lines support paging, provide a limit and marker
|
|
* to page through responses.
|
|
*
|
|
* The paged responses aren't guaranteed to be reliable between
|
|
* ledger closes. You have to supply a ledger_index or ledger_hash
|
|
* when paging to ensure a complete response
|
|
*
|
|
* @param {Object} options
|
|
* @param {String} account - ripple address
|
|
* @param {String} peer - ripple address
|
|
* @param [String|Number] ledger identifier
|
|
* @param [Number] limit - max results per response
|
|
* @param {String} marker - start position in response paging
|
|
* @param [Function] callback
|
|
* @return {Request}
|
|
*/
|
|
|
|
Remote.prototype.requestAccountLines = function () {
|
|
// XXX Does this require the server to be trusted?
|
|
// utils.assert(this.trusted);
|
|
var options = ['account_lines'];
|
|
|
|
for (var _len3 = arguments.length, args = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
|
|
args[_key3] = arguments[_key3];
|
|
}
|
|
|
|
if (_.isPlainObject(args[0])) {
|
|
options = options.concat(args);
|
|
} else {
|
|
var account = args[0];
|
|
var peer = args[1];
|
|
var ledger = args[2];
|
|
|
|
options = options.concat([account, ledger, peer].concat(_toConsumableArray(args.slice(3))));
|
|
}
|
|
|
|
return Remote.accountRequest.apply(this, options);
|
|
};
|
|
|
|
/**
|
|
* Request account_offers
|
|
*
|
|
* Requests for account_offers support paging, provide a limit and marker
|
|
* to page through responses.
|
|
*
|
|
* The paged responses aren't guaranteed to be reliable between
|
|
* ledger closes. You have to supply a ledger_index or ledger_hash
|
|
* when paging to ensure a complete response
|
|
*
|
|
* @param {Object} options
|
|
* @param {String} account - ripple address
|
|
* @param [String|Number] ledger identifier
|
|
* @param [Number] limit - max results per response
|
|
* @param {String} marker - start position in response paging
|
|
* @param [Function] callback
|
|
* @return {Request}
|
|
*/
|
|
|
|
Remote.prototype.requestAccountOffers = function () {
|
|
for (var _len4 = arguments.length, args = Array(_len4), _key4 = 0; _key4 < _len4; _key4++) {
|
|
args[_key4] = arguments[_key4];
|
|
}
|
|
|
|
var options = ['account_offers'].concat(args);
|
|
return Remote.accountRequest.apply(this, options);
|
|
};
|
|
|
|
/**
|
|
* Request account_tx
|
|
*
|
|
* @param {Object} options
|
|
*
|
|
* @param {String} account
|
|
* @param [Number] ledger_index_min defaults to -1
|
|
* @param [Number] ledger_index_max defaults to -1
|
|
* @param [Boolean] binary, defaults to true
|
|
* @param [Boolean] parseBinary, defaults to true
|
|
* @param [Boolean] count, defaults to false
|
|
* @param [Boolean] descending, defaults to false
|
|
* @param [Number] offset, defaults to 0
|
|
* @param [Number] limit
|
|
*
|
|
* @param [Function] callback
|
|
* @return {Request}
|
|
*/
|
|
|
|
Remote.prototype.requestAccountTransactions = Remote.prototype.requestAccountTx = function (options, callback) {
|
|
// XXX Does this require the server to be trusted?
|
|
// utils.assert(this.trusted);
|
|
|
|
var request = new Request(this, 'account_tx');
|
|
|
|
options.binary = options.binary !== false;
|
|
|
|
if (options.min_ledger !== undefined) {
|
|
options.ledger_index_min = options.min_ledger;
|
|
}
|
|
|
|
if (options.max_ledger !== undefined) {
|
|
options.ledger_index_max = options.max_ledger;
|
|
}
|
|
|
|
if (options.binary && options.parseBinary === undefined) {
|
|
options.parseBinary = true;
|
|
}
|
|
|
|
_Object$keys(options).forEach(function (o) {
|
|
switch (o) {
|
|
case 'account':
|
|
case 'ledger_index_min': // earliest
|
|
case 'ledger_index_max': // latest
|
|
case 'binary': // false
|
|
case 'count': // false
|
|
case 'descending': // false
|
|
case 'offset': // 0
|
|
case 'limit':
|
|
|
|
// extended account_tx
|
|
case 'forward': // false
|
|
case 'marker':
|
|
request.message[o] = this[o];
|
|
break;
|
|
}
|
|
}, options);
|
|
|
|
request.once('success', function (res) {
|
|
if (!options.parseBinary) {
|
|
request.emit('transactions', res);
|
|
return;
|
|
}
|
|
|
|
function iterator(transaction, next) {
|
|
async.setImmediate(function () {
|
|
next(null, Remote.parseBinaryAccountTransaction(transaction));
|
|
});
|
|
}
|
|
|
|
function complete(err, transactions) {
|
|
if (err) {
|
|
request.emit('error', err);
|
|
} else {
|
|
res.transactions = transactions;
|
|
request.emit('transactions', res);
|
|
}
|
|
}
|
|
|
|
async.mapSeries(res.transactions, iterator, complete);
|
|
});
|
|
|
|
request.callback(callback, 'transactions');
|
|
|
|
return request;
|
|
};
|
|
|
|
/**
|
|
* @param {Object} transaction
|
|
* @return {Transaction}
|
|
*/
|
|
|
|
Remote.parseBinaryAccountTransaction = function (transaction) {
|
|
var tx_obj = new SerializedObject(transaction.tx_blob);
|
|
var tx_obj_json = tx_obj.to_json();
|
|
var meta = new SerializedObject(transaction.meta).to_json();
|
|
|
|
var tx_result = {
|
|
validated: transaction.validated
|
|
};
|
|
|
|
tx_result.meta = meta;
|
|
tx_result.tx = tx_obj_json;
|
|
tx_result.tx.hash = tx_obj.hash(hashprefixes.HASH_TX_ID).to_hex();
|
|
tx_result.tx.ledger_index = transaction.ledger_index;
|
|
tx_result.tx.inLedger = transaction.ledger_index;
|
|
|
|
if (typeof meta.DeliveredAmount === 'object') {
|
|
tx_result.meta.delivered_amount = meta.DeliveredAmount;
|
|
} else {
|
|
switch (typeof tx_obj_json.Amount) {
|
|
case 'string':
|
|
case 'object':
|
|
tx_result.meta.delivered_amount = tx_obj_json.Amount;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return tx_result;
|
|
};
|
|
|
|
Remote.parseBinaryTransaction = function (transaction) {
|
|
var tx_obj = new SerializedObject(transaction.tx).to_json();
|
|
var meta = new SerializedObject(transaction.meta).to_json();
|
|
|
|
var tx_result = tx_obj;
|
|
|
|
tx_result.date = transaction.date;
|
|
tx_result.hash = transaction.hash;
|
|
tx_result.inLedger = transaction.inLedger;
|
|
tx_result.ledger_index = transaction.ledger_index;
|
|
tx_result.meta = meta;
|
|
tx_result.validated = transaction.validated;
|
|
|
|
switch (typeof meta.DeliveredAmount) {
|
|
case 'string':
|
|
case 'object':
|
|
tx_result.meta.delivered_amount = meta.DeliveredAmount;
|
|
break;
|
|
default:
|
|
switch (typeof tx_obj.Amount) {
|
|
case 'string':
|
|
case 'object':
|
|
tx_result.meta.delivered_amount = tx_obj.Amount;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return tx_result;
|
|
};
|
|
|
|
/**
|
|
* Parse binary ledger state data
|
|
*
|
|
* @param {Object} ledgerData
|
|
* @property {String} ledgerData.data
|
|
* @property {String} ledgerData.index
|
|
*
|
|
* @return {State}
|
|
*/
|
|
|
|
Remote.parseBinaryLedgerData = function (ledgerData) {
|
|
var data = new SerializedObject(ledgerData.data).to_json();
|
|
data.index = ledgerData.index;
|
|
return data;
|
|
};
|
|
|
|
/**
|
|
* Request the overall transaction history.
|
|
*
|
|
* Returns a list of transactions that happened recently on the network. The
|
|
* default number of transactions to be returned is 20.
|
|
*
|
|
* @param [Number] start
|
|
* @param [Function] callback
|
|
* @return {Request}
|
|
*/
|
|
|
|
Remote.prototype.requestTransactionHistory = function (options, callback) {
|
|
// XXX Does this require the server to be trusted?
|
|
// utils.assert(this.trusted);
|
|
var request = new Request(this, 'tx_history');
|
|
request.message.start = options.start;
|
|
request.callback(callback);
|
|
|
|
return request;
|
|
};
|
|
|
|
/**
|
|
* Request book_offers
|
|
*
|
|
* @param {Object} options
|
|
* @param {Object} options.gets - taker_gets with issuer and currency
|
|
* @param {Object} options.pays - taker_pays with issuer and currency
|
|
* @param {String} [options.taker]
|
|
* @param {String} [options.ledger]
|
|
* @param {String|Number} [options.limit]
|
|
* @param [Function] callback
|
|
* @return {Request}
|
|
*/
|
|
|
|
Remote.prototype.requestBookOffers = function (options, callback) {
|
|
var gets = options.gets;
|
|
var pays = options.pays;
|
|
var taker = options.taker;
|
|
var ledger = options.ledger;
|
|
var limit = options.limit;
|
|
|
|
var request = new Request(this, 'book_offers');
|
|
|
|
request.message.taker_gets = {
|
|
currency: Currency.json_rewrite(gets.currency, { force_hex: true })
|
|
};
|
|
|
|
if (!Currency.from_json(request.message.taker_gets.currency).is_native()) {
|
|
request.message.taker_gets.issuer = UInt160.json_rewrite(gets.issuer);
|
|
}
|
|
|
|
request.message.taker_pays = {
|
|
currency: Currency.json_rewrite(pays.currency, { force_hex: true })
|
|
};
|
|
|
|
if (!Currency.from_json(request.message.taker_pays.currency).is_native()) {
|
|
request.message.taker_pays.issuer = UInt160.json_rewrite(pays.issuer);
|
|
}
|
|
|
|
request.message.taker = taker ? taker : UInt160.ACCOUNT_ONE;
|
|
request.selectLedger(ledger);
|
|
|
|
if (!isNaN(limit)) {
|
|
var _limit = Number(limit);
|
|
|
|
// max for 32-bit unsigned int is 4294967295
|
|
// we'll clamp to 1e9
|
|
if (_limit > 1e9) {
|
|
_limit = 1e9;
|
|
}
|
|
// min for 32-bit unsigned int is 0
|
|
// we'll clamp to 0
|
|
if (_limit < 0) {
|
|
_limit = 0;
|
|
}
|
|
|
|
request.message.limit = _limit;
|
|
}
|
|
|
|
request.callback(callback);
|
|
return request;
|
|
};
|
|
|
|
/**
|
|
* Request wallet_accounts
|
|
*
|
|
* @param {String} seed
|
|
* @param [Function] callback
|
|
* @return {Request}
|
|
*/
|
|
|
|
Remote.prototype.requestWalletAccounts = function (options, callback) {
|
|
utils.assert(this.trusted); // Don't send secrets.
|
|
var request = new Request(this, 'wallet_accounts');
|
|
request.message.seed = options.seed;
|
|
request.callback(callback);
|
|
|
|
return request;
|
|
};
|
|
|
|
/**
|
|
* Request sign
|
|
*
|
|
* @param {String} secret
|
|
* @param {Object} tx_json
|
|
* @param [Function] callback
|
|
* @return {Request}
|
|
*/
|
|
|
|
Remote.prototype.requestSign = function (options, callback) {
|
|
utils.assert(this.trusted); // Don't send secrets.
|
|
|
|
var request = new Request(this, 'sign');
|
|
request.message.secret = options.secret;
|
|
request.message.tx_json = options.tx_json;
|
|
request.callback(callback);
|
|
|
|
return request;
|
|
};
|
|
|
|
/**
|
|
* Request submit
|
|
*
|
|
* @param [Function] callback
|
|
* @return {Request}
|
|
*/
|
|
|
|
Remote.prototype.requestSubmit = function (callback) {
|
|
return new Request(this, 'submit').callback(callback);
|
|
};
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @param [Function] callback
|
|
* @api private
|
|
*/
|
|
|
|
Remote.prototype._serverPrepareSubscribe = function (server, callback_) {
|
|
var self = this;
|
|
var feeds = ['ledger', 'server'];
|
|
var callback = _.isFunction(server) ? server : callback_;
|
|
|
|
if (this._transaction_listeners) {
|
|
feeds.push('transactions');
|
|
}
|
|
|
|
var request = this.requestSubscribe(feeds);
|
|
|
|
function serverSubscribed(message) {
|
|
self._stand_alone = Boolean(message.stand_alone);
|
|
self._testnet = Boolean(message.testnet);
|
|
self._handleLedgerClosed(message, server);
|
|
self.emit('subscribed');
|
|
}
|
|
|
|
request.on('error', function (err) {
|
|
if (self.trace) {
|
|
log.info('Initial server subscribe failed', err);
|
|
}
|
|
});
|
|
|
|
request.once('success', serverSubscribed);
|
|
|
|
self.emit('prepare_subscribe', request);
|
|
|
|
request.callback(callback, 'subscribed');
|
|
|
|
return request;
|
|
};
|
|
|
|
/**
|
|
* For unit testing: ask the remote to accept the current ledger.
|
|
* To be notified when the ledger is accepted, server_subscribe() then listen
|
|
* to 'ledger_hash' events. A good way to be notified of the result of this is:
|
|
* remote.on('ledger_closed', function(ledger_closed, ledger_index) { ... } );
|
|
*
|
|
* @param [Function] callback
|
|
*/
|
|
|
|
Remote.prototype.ledgerAccept = Remote.prototype.requestLedgerAccept = function (callback) {
|
|
/* eslint-disable consistent-return */
|
|
var request = new Request(this, 'ledger_accept');
|
|
|
|
if (!this._stand_alone) {
|
|
// XXX This should emit error on the request
|
|
this.emit('error', new RippleError('notStandAlone'));
|
|
return;
|
|
}
|
|
|
|
this.once('ledger_closed', function (ledger) {
|
|
request.emit('ledger_closed', ledger);
|
|
});
|
|
|
|
request.callback(callback, 'ledger_closed');
|
|
request.request();
|
|
|
|
return request;
|
|
/* eslint-enable consistent-return */
|
|
};
|
|
|
|
/**
|
|
* Account root request abstraction
|
|
*
|
|
* @this Remote
|
|
* @api private
|
|
*/
|
|
|
|
Remote.accountRootRequest = function (command, filter, options, callback) {
|
|
var request = this.requestLedgerEntry('account_root');
|
|
request.accountRoot(options.account);
|
|
request.selectLedger(options.ledger);
|
|
|
|
request.once('success', function (message) {
|
|
request.emit(command, filter(message));
|
|
});
|
|
|
|
request.callback(callback, command);
|
|
|
|
return request;
|
|
};
|
|
|
|
/**
|
|
* Request account balance
|
|
*
|
|
* @param {String} account
|
|
* @param [String|Number] ledger
|
|
* @param [Function] callback
|
|
* @return {Request}
|
|
*/
|
|
|
|
Remote.prototype.requestAccountBalance = function () {
|
|
function responseFilter(message) {
|
|
return Amount.from_json(message.node.Balance);
|
|
}
|
|
|
|
for (var _len5 = arguments.length, args = Array(_len5), _key5 = 0; _key5 < _len5; _key5++) {
|
|
args[_key5] = arguments[_key5];
|
|
}
|
|
|
|
var options = ['account_balance', responseFilter].concat(args);
|
|
return Remote.accountRootRequest.apply(this, options);
|
|
};
|
|
|
|
/**
|
|
* Request account flags
|
|
*
|
|
* @param {String} account
|
|
* @param [String|Number] ledger
|
|
* @param [Function] callback
|
|
* @return {Request}
|
|
*/
|
|
|
|
Remote.prototype.requestAccountFlags = function () {
|
|
function responseFilter(message) {
|
|
return message.node.Flags;
|
|
}
|
|
|
|
for (var _len6 = arguments.length, args = Array(_len6), _key6 = 0; _key6 < _len6; _key6++) {
|
|
args[_key6] = arguments[_key6];
|
|
}
|
|
|
|
var options = ['account_flags', responseFilter].concat(args);
|
|
return Remote.accountRootRequest.apply(this, options);
|
|
};
|
|
|
|
/**
|
|
* Request owner count
|
|
*
|
|
* @param {String} account
|
|
* @param [String|Number] ledger
|
|
* @param [Function] callback
|
|
* @return {Request}
|
|
*/
|
|
|
|
Remote.prototype.requestOwnerCount = function () {
|
|
function responseFilter(message) {
|
|
return message.node.OwnerCount;
|
|
}
|
|
|
|
for (var _len7 = arguments.length, args = Array(_len7), _key7 = 0; _key7 < _len7; _key7++) {
|
|
args[_key7] = arguments[_key7];
|
|
}
|
|
|
|
var options = ['owner_count', responseFilter].concat(args);
|
|
return Remote.accountRootRequest.apply(this, options);
|
|
};
|
|
|
|
/**
|
|
* Get an account by accountID (address)
|
|
*
|
|
*
|
|
* @param {String} account
|
|
* @return {Account}
|
|
*/
|
|
|
|
Remote.prototype.getAccount = function (accountID) {
|
|
return this._accounts[UInt160.json_rewrite(accountID)];
|
|
};
|
|
|
|
/**
|
|
* Add an account by accountID (address)
|
|
*
|
|
* @param {String} account
|
|
* @return {Account}
|
|
*/
|
|
|
|
Remote.prototype.addAccount = function (accountID) {
|
|
var account = new Account(this, accountID);
|
|
|
|
if (account.isValid()) {
|
|
this._accounts[accountID] = account;
|
|
}
|
|
|
|
return account;
|
|
};
|
|
|
|
/**
|
|
* Add an account if it does not exist, return the
|
|
* account by accountID (address)
|
|
*
|
|
* @param {String} account
|
|
* @return {Account}
|
|
*/
|
|
|
|
Remote.prototype.account = Remote.prototype.findAccount = function (accountID) {
|
|
var account = this.getAccount(accountID);
|
|
return account ? account : this.addAccount(accountID);
|
|
};
|
|
|
|
/**
|
|
* Create a pathfind
|
|
*
|
|
* @param {Object} options -
|
|
* @param {Function} callback -
|
|
* @return {PathFind} -
|
|
*/
|
|
Remote.prototype.createPathFind = function (options, callback) {
|
|
if (this._cur_path_find !== null) {
|
|
this._queued_path_finds.push({ options: options, callback: callback });
|
|
return null;
|
|
}
|
|
|
|
var pathFind = new PathFind(this, options.src_account, options.dst_account, options.dst_amount, options.src_currencies);
|
|
|
|
if (this._cur_path_find) {
|
|
this._cur_path_find.notify_superceded();
|
|
}
|
|
|
|
if (callback) {
|
|
pathFind.on('update', function (data) {
|
|
if (data.full_reply) {
|
|
pathFind.close();
|
|
callback(null, data);
|
|
}
|
|
});
|
|
pathFind.on('error', callback);
|
|
}
|
|
|
|
this._cur_path_find = pathFind;
|
|
pathFind.create();
|
|
return pathFind;
|
|
};
|
|
|
|
Remote.prepareTrade = function (currency, issuer) {
|
|
var suffix = Currency.from_json(currency).is_native() ? '' : '/' + issuer;
|
|
return currency + suffix;
|
|
};
|
|
|
|
/**
|
|
* Create an OrderBook if it does not exist, return
|
|
* the order book
|
|
*
|
|
* @param {Object} options
|
|
* @return {OrderBook}
|
|
*/
|
|
|
|
Remote.prototype.book = Remote.prototype.createOrderBook = function (options) {
|
|
var gets = Remote.prepareTrade(options.currency_gets, options.issuer_gets);
|
|
var pays = Remote.prepareTrade(options.currency_pays, options.issuer_pays);
|
|
var key = gets + ':' + pays;
|
|
|
|
if (this._books.hasOwnProperty(key)) {
|
|
return this._books[key];
|
|
}
|
|
|
|
var book = new OrderBook(this, options.currency_gets, options.issuer_gets, options.currency_pays, options.issuer_pays, key);
|
|
|
|
if (book.is_valid()) {
|
|
this._books[key] = book;
|
|
}
|
|
|
|
return book;
|
|
};
|
|
|
|
/**
|
|
* Return the next account sequence
|
|
*
|
|
* @param {String} account
|
|
* @param {String} sequence modifier (ADVANCE or REWIND)
|
|
* @return {Number} sequence
|
|
*/
|
|
|
|
Remote.prototype.accountSeq = Remote.prototype.getAccountSequence = function (account_, advance) {
|
|
var account = UInt160.json_rewrite(account_);
|
|
var accountInfo = this.accounts[account];
|
|
|
|
if (!accountInfo) {
|
|
return NaN;
|
|
}
|
|
|
|
var seq = accountInfo.seq;
|
|
var change = ({ ADVANCE: 1, REWIND: -1 })[advance.toUpperCase()] || 0;
|
|
|
|
accountInfo.seq += change;
|
|
|
|
return seq;
|
|
};
|
|
|
|
/**
|
|
* Set account sequence
|
|
*
|
|
* @param {String} account
|
|
* @param {Number} sequence
|
|
*/
|
|
|
|
Remote.prototype.setAccountSequence = Remote.prototype.setAccountSeq = function (account_, sequence) {
|
|
var account = UInt160.json_rewrite(account_);
|
|
|
|
if (!this.accounts.hasOwnProperty(account)) {
|
|
this.accounts[account] = {};
|
|
}
|
|
|
|
this.accounts[account].seq = sequence;
|
|
};
|
|
|
|
/**
|
|
* Refresh an account's sequence from server
|
|
*
|
|
* @param {String} account
|
|
* @param [String|Number] ledger
|
|
* @param [Function] callback
|
|
* @return {Request}
|
|
*/
|
|
|
|
Remote.prototype.accountSeqCache = function (options, callback) {
|
|
if (!this.accounts.hasOwnProperty(options.account)) {
|
|
this.accounts[options.account] = {};
|
|
}
|
|
|
|
var account_info = this.accounts[options.account];
|
|
var request = account_info.caching_seq_request;
|
|
|
|
function accountRootSuccess(message) {
|
|
delete account_info.caching_seq_request;
|
|
|
|
var seq = message.node.Sequence;
|
|
account_info.seq = seq;
|
|
|
|
request.emit('success_cache', message);
|
|
}
|
|
|
|
function accountRootError(message) {
|
|
delete account_info.caching_seq_request;
|
|
|
|
request.emit('error_cache', message);
|
|
}
|
|
|
|
if (!request) {
|
|
request = this.requestLedgerEntry('account_root');
|
|
request.accountRoot(options.account);
|
|
|
|
if (!_.isUndefined(options.ledger)) {
|
|
request.selectLedger(options.ledger);
|
|
}
|
|
|
|
request.once('success', accountRootSuccess);
|
|
request.once('error', accountRootError);
|
|
|
|
account_info.caching_seq_request = request;
|
|
}
|
|
|
|
request.callback(callback, 'success_cache', 'error_cache');
|
|
|
|
return request;
|
|
};
|
|
|
|
/**
|
|
* Mark an account's root node as dirty.
|
|
*
|
|
* @param {String} account
|
|
*/
|
|
|
|
Remote.prototype.dirtyAccountRoot = function (account_) {
|
|
var account = UInt160.json_rewrite(account_);
|
|
delete this.ledgers.current.account_root[account];
|
|
};
|
|
|
|
/**
|
|
* Get an Offer from the ledger
|
|
*
|
|
* @param {Object} options
|
|
* @param {String|Number} options.ledger
|
|
* @param {String} [options.account] - Required unless using options.index
|
|
* @param {Number} [options.sequence] - Required unless using options.index
|
|
* @param {String} [options.index] - Required only if options.account and
|
|
* options.sequence not provided
|
|
*
|
|
* @callback
|
|
* @param {Error} error
|
|
* @param {Object} message
|
|
*
|
|
* @return {Request}
|
|
*/
|
|
|
|
Remote.prototype.requestOffer = function (options, callback) {
|
|
var request = this.requestLedgerEntry('offer');
|
|
|
|
if (options.account && options.sequence) {
|
|
request.offerId(options.account, options.sequence);
|
|
} else if (options.index) {
|
|
request.offerIndex(options.index);
|
|
}
|
|
|
|
request.ledgerSelect(options.ledger);
|
|
|
|
request.once('success', function (res) {
|
|
request.emit('offer', res);
|
|
});
|
|
|
|
request.callback(callback, 'offer');
|
|
|
|
return request;
|
|
};
|
|
|
|
/**
|
|
* Get an account's balance
|
|
*
|
|
* @param {String} account
|
|
* @param [String] issuer
|
|
* @param [String] currency
|
|
* @param [String|Number] ledger
|
|
* @param [Function] callback
|
|
* @return {Request}
|
|
*/
|
|
|
|
Remote.prototype.requestRippleBalance = function (options, callback) {
|
|
// YYY Could be cached per ledger.
|
|
var request = this.requestLedgerEntry('ripple_state');
|
|
request.rippleState(options.account, options.issuer, options.currency);
|
|
|
|
if (!_.isUndefined(options.ledger)) {
|
|
request.selectLedger(options.ledger);
|
|
}
|
|
|
|
function rippleState(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(options.account).equals(highLimit.issuer());
|
|
|
|
request.emit('ripple_state', {
|
|
account_balance: (accountHigh ? balance.negate() : balance.clone()).parse_issuer(options.account),
|
|
peer_balance: (!accountHigh ? balance.negate() : balance.clone()).parse_issuer(options.issuer),
|
|
account_limit: (accountHigh ? highLimit : lowLimit).clone().parse_issuer(options.issuer),
|
|
peer_limit: (!accountHigh ? highLimit : lowLimit).clone().parse_issuer(options.account),
|
|
account_quality_in: accountHigh ? node.HighQualityIn : node.LowQualityIn,
|
|
peer_quality_in: !accountHigh ? node.HighQualityIn : node.LowQualityIn,
|
|
account_quality_out: accountHigh ? node.HighQualityOut : node.LowQualityOut,
|
|
peer_quality_out: !accountHigh ? node.HighQualityOut : node.LowQualityOut
|
|
});
|
|
}
|
|
|
|
request.once('success', rippleState);
|
|
request.callback(callback, 'ripple_state');
|
|
|
|
return request;
|
|
};
|
|
|
|
Remote.prepareCurrency = Remote.prepareCurrencies = function (currency) {
|
|
var newCurrency = {};
|
|
|
|
if (currency.hasOwnProperty('issuer')) {
|
|
newCurrency.issuer = UInt160.json_rewrite(currency.issuer);
|
|
}
|
|
|
|
if (currency.hasOwnProperty('currency')) {
|
|
newCurrency.currency = Currency.json_rewrite(currency.currency, { force_hex: true });
|
|
}
|
|
|
|
return newCurrency;
|
|
};
|
|
|
|
/**
|
|
* Request ripple_path_find
|
|
*
|
|
* @param {Object} options
|
|
* @param [Function] callback
|
|
* @return {Request}
|
|
*/
|
|
|
|
Remote.prototype.requestRipplePathFind = function (options, callback) {
|
|
var request = new Request(this, 'ripple_path_find');
|
|
|
|
request.message.source_account = UInt160.json_rewrite(options.source_account);
|
|
|
|
request.message.destination_account = UInt160.json_rewrite(options.destination_account);
|
|
|
|
request.message.destination_amount = Amount.json_rewrite(options.destination_amount);
|
|
|
|
if (Array.isArray(options.source_currencies)) {
|
|
request.message.source_currencies = options.source_currencies.map(Remote.prepareCurrency);
|
|
}
|
|
|
|
request.callback(callback);
|
|
|
|
return request;
|
|
};
|
|
|
|
/**
|
|
* Request path_find/create
|
|
*
|
|
* @param {Object} options
|
|
* @param [Function] callback
|
|
* @return {Request}
|
|
*/
|
|
|
|
Remote.prototype.requestPathFindCreate = function (options, callback) {
|
|
var request = new Request(this, 'path_find');
|
|
request.message.subcommand = 'create';
|
|
|
|
request.message.source_account = UInt160.json_rewrite(options.source_account);
|
|
|
|
request.message.destination_account = UInt160.json_rewrite(options.destination_account);
|
|
|
|
request.message.destination_amount = Amount.json_rewrite(options.destination_amount);
|
|
|
|
if (Array.isArray(options.source_currencies)) {
|
|
request.message.source_currencies = options.source_currencies.map(Remote.prepareCurrency);
|
|
}
|
|
|
|
request.callback(callback);
|
|
return request;
|
|
};
|
|
|
|
/**
|
|
* Request path_find/close
|
|
*
|
|
* @param [Function] callback
|
|
* @return {Request}
|
|
*/
|
|
|
|
Remote.prototype.requestPathFindClose = function (callback) {
|
|
var request = new Request(this, 'path_find');
|
|
|
|
request.message.subcommand = 'close';
|
|
request.callback(callback);
|
|
this._cur_path_find = null;
|
|
if (this._queued_path_finds.length > 0) {
|
|
var pathfind = this._queued_path_finds.shift();
|
|
this.createPathFind(pathfind.options, pathfind.callback);
|
|
}
|
|
|
|
return request;
|
|
};
|
|
|
|
/**
|
|
* Request unl_list
|
|
*
|
|
* @param [Function] callback
|
|
* @return {Request}
|
|
*/
|
|
|
|
Remote.prototype.requestUnlList = function (callback) {
|
|
return new Request(this, 'unl_list').callback(callback);
|
|
};
|
|
|
|
/**
|
|
* Request unl_add
|
|
*
|
|
* @param {String} address
|
|
* @param {String} comment
|
|
* @param [Function] callback
|
|
* @return {Request}
|
|
*/
|
|
|
|
Remote.prototype.requestUnlAdd = function (address, comment, callback) {
|
|
var request = new Request(this, 'unl_add');
|
|
|
|
request.message.node = address;
|
|
|
|
if (comment) {
|
|
// note is not specified anywhere, should remove?
|
|
request.message.comment = undefined;
|
|
}
|
|
|
|
request.callback(callback);
|
|
|
|
return request;
|
|
};
|
|
|
|
/**
|
|
* Request unl_delete
|
|
*
|
|
* @param {String} node
|
|
* @param [Function] callback
|
|
* @return {Request}
|
|
*/
|
|
|
|
Remote.prototype.requestUnlDelete = function (node, callback) {
|
|
var request = new Request(this, 'unl_delete');
|
|
|
|
request.message.node = node;
|
|
request.callback(callback);
|
|
|
|
return request;
|
|
};
|
|
|
|
/**
|
|
* Request peers
|
|
*
|
|
* @param [Function] callback
|
|
* @return {Request}
|
|
*/
|
|
|
|
Remote.prototype.requestPeers = function (callback) {
|
|
return new Request(this, 'peers').callback(callback);
|
|
};
|
|
|
|
/**
|
|
* Request connect
|
|
*
|
|
* @param {String} ip
|
|
* @param {Number} port
|
|
* @param [Function] callback
|
|
* @return {Request}
|
|
*/
|
|
|
|
Remote.prototype.requestConnect = function (ip, port, callback) {
|
|
var request = new Request(this, 'connect');
|
|
|
|
request.message.ip = ip;
|
|
|
|
if (port) {
|
|
request.message.port = port;
|
|
}
|
|
|
|
request.callback(callback);
|
|
|
|
return request;
|
|
};
|
|
|
|
/**
|
|
* Create a Transaction
|
|
*
|
|
* @param {String} TransactionType
|
|
* @param {Object} options
|
|
* @return {Transaction}
|
|
*/
|
|
|
|
Remote.prototype.transaction = Remote.prototype.createTransaction = function (type) {
|
|
var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
|
|
|
|
var transaction = new Transaction(this);
|
|
|
|
if (arguments.length === 0) {
|
|
// Fallback
|
|
return transaction;
|
|
}
|
|
|
|
assert.strictEqual(typeof type, 'string', 'TransactionType must be a string');
|
|
|
|
var constructorMap = {
|
|
Payment: transaction.payment,
|
|
AccountSet: transaction.accountSet,
|
|
TrustSet: transaction.trustSet,
|
|
OfferCreate: transaction.offerCreate,
|
|
OfferCancel: transaction.offerCancel,
|
|
SetRegularKey: transaction.setRegularKey,
|
|
SignerListSet: transaction.setSignerList
|
|
};
|
|
|
|
var transactionConstructor = constructorMap[type];
|
|
|
|
if (!transactionConstructor) {
|
|
throw new Error('TransactionType must be a valid transaction type');
|
|
}
|
|
|
|
return transactionConstructor.call(transaction, options);
|
|
};
|
|
|
|
/**
|
|
* Calculate a transaction fee for a number of tx fee units.
|
|
*
|
|
* This takes into account the last known network and local load fees.
|
|
*
|
|
* @param {Number} fee units
|
|
* @return {Amount} Final fee in XRP for specified number of fee units.
|
|
*/
|
|
|
|
Remote.prototype.feeTx = function (units) {
|
|
var server = this.getServer();
|
|
|
|
if (!server) {
|
|
throw new Error('No connected servers');
|
|
}
|
|
|
|
return server._feeTx(units);
|
|
};
|
|
|
|
/**
|
|
* Get the current recommended transaction fee unit.
|
|
*
|
|
* Multiply this value with the number of fee units in order to calculate the
|
|
* recommended fee for the transaction you are trying to submit.
|
|
*
|
|
* @return {Number} Recommended amount for one fee unit as float.
|
|
*/
|
|
|
|
Remote.prototype.feeTxUnit = function () {
|
|
var server = this.getServer();
|
|
|
|
if (!server) {
|
|
throw new Error('No connected servers');
|
|
}
|
|
|
|
return server._feeTxUnit();
|
|
};
|
|
|
|
/**
|
|
* Get the current recommended reserve base.
|
|
*
|
|
* Returns the base reserve with load fees and safety margin applied.
|
|
*
|
|
* @param {Number} owner count
|
|
* @return {Amount}
|
|
*/
|
|
|
|
Remote.prototype.reserve = function (owner_count) {
|
|
var server = this.getServer();
|
|
|
|
if (!server) {
|
|
throw new Error('No connected servers');
|
|
}
|
|
|
|
return server._reserve(owner_count);
|
|
};
|
|
|
|
exports.Remote = Remote;
|
|
|
|
// vim:sw=2:sts=2:ts=8:et
|