Merge branch 'develop' of https://github.com/ripple/ripple-lib into develop

This commit is contained in:
jatchili
2013-08-14 18:33:28 -07:00
14 changed files with 1738 additions and 1132 deletions

View File

@@ -74,7 +74,7 @@ remote.request_server_info(function(err, res) {
**request_unsubscribe(streams, [callback])**
**request_transaction_entry(hash, [callback])**
**request_transaction_entry(tx_hash, [ledger_hash], [callback])**
**request_tx(hash, [callback])**
@@ -90,11 +90,11 @@ remote.request_server_info(function(err, res) {
**request_wallet_accounts(seed, [callback])**
+ requires trusted **remote
+ requires trusted remote
**request_sign(secret, tx_json, [callback])**
+ requires trusted **remote
+ requires trusted remote
**request_submit([callback])**
@@ -118,6 +118,6 @@ remote.request_server_info(function(err, res) {
**request_connect(ip, port, [callback])**
**transaction()**
**transaction([destination], [source], [amount], [callback])**
+ returns a [Transaction](https://github.com/ripple/ripple-lib/blob/develop/src/js/ripple/transaction.js) object

View File

@@ -1,6 +1,6 @@
{
"name": "ripple-lib",
"version": "0.7.18",
"version": "0.7.19",
"description": "Ripple JavaScript client library",
"files": [
"src/js/ripple/*.js",
@@ -33,7 +33,7 @@
},
"repository": {
"type": "git",
"url": "git://github.com/rippleFoundation/ripple-lib.git"
"url": "git://github.com/ripple/ripple-lib.git"
},
"readmeFilename": "README.md",
"engines": {

View File

@@ -11,67 +11,81 @@
// var network = require("./network.js");
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var extend = require('extend');
var Amount = require('./amount').Amount;
var UInt160 = require('./uint160').UInt160;
var Amount = require('./amount').Amount;
var UInt160 = require('./uint160').UInt160;
var TransactionManager = require('./transactionmanager').TransactionManager;
var extend = require('extend');
var Account = function (remote, account) {
function Account(remote, account) {
EventEmitter.call(this);
var self = this;
this._remote = remote;
this._account = UInt160.from_json(account);
this._tx_manager = null;
this._remote = remote;
this._account = UInt160.from_json(account);
this._account_id = this._account.to_json();
this._subs = 0;
this._subs = 0;
// Ledger entry object
// Important: This must never be overwritten, only extend()-ed
this._entry = {};
this._entry = { };
this.on('newListener', function (type, listener) {
if (Account.subscribe_events.indexOf(type) !== -1) {
if (!self._subs && 'open' === self._remote._online_state) {
function listener_added(type, listener) {
if (~Account.subscribe_events.indexOf(type)) {
if (!self._subs && self._remote._connected) {
self._remote.request_subscribe()
.accounts(self._account_id)
.request();
.accounts(self._account_id)
.request();
}
self._subs += 1;
self._subs += 1;
}
});
}
this.on('removeListener', function (type, listener) {
if (Account.subscribe_events.indexOf(type) !== -1) {
self._subs -= 1;
if (!self._subs && 'open' === self._remote._online_state) {
function listener_removed(type, listener) {
if (~Account.subscribe_events.indexOf(type)) {
self._subs -= 1;
if (!self._subs && self._remote._connected) {
self._remote.request_unsubscribe()
.accounts(self._account_id)
.request();
.accounts(self._account_id)
.request();
}
}
});
}
this._remote.on('prepare_subscribe', function (request) {
if (self._subs) request.accounts(self._account_id);
});
this.on('newListener', listener_added);
this.on('removeListener', listener_removed);
this.on('transaction', function (msg) {
function prepare_subscribe(request) {
if (self._subs) {
request.accounts(self._account_id);
}
}
this._remote.on('prepare_subscribe', prepare_subscribe);
function handle_transaction(transaction) {
var changed = false;
msg.mmeta.each(function (an) {
if (an.entryType === 'AccountRoot' &&
an.fields.Account === self._account_id) {
transaction.mmeta.each(function(an) {
var isAccountRoot = an.entryType === 'AccountRoot'
&& an.fields.Account === self._account_id;
if (isAccountRoot) {
extend(self._entry, an.fieldsNew, an.fieldsFinal);
changed = true;
}
});
if (changed) {
self.emit('entry', self._entry);
}
});
}
this.on('transaction', handle_transaction);
return this;
};
@@ -81,10 +95,9 @@ util.inherits(Account, EventEmitter);
/**
* List of events that require a remote subscription to the account.
*/
Account.subscribe_events = ['transaction', 'entry'];
Account.subscribe_events = [ 'transaction', 'entry' ];
Account.prototype.to_json = function ()
{
Account.prototype.to_json = function () {
return this._account.to_json();
};
@@ -93,11 +106,15 @@ Account.prototype.to_json = function ()
*
* Note: This does not tell you whether the account exists in the ledger.
*/
Account.prototype.is_valid = function ()
{
Account.prototype.is_valid = function () {
return this._account.is_valid();
};
Account.prototype.get_info = function(callback) {
var callback = typeof callback === 'function' ? callback : function(){};
this._remote.request_account_info(this._account_id, callback);
};
/**
* Retrieve the current AccountRoot entry.
*
@@ -106,14 +123,53 @@ Account.prototype.is_valid = function ()
*
* @param {function (err, entry)} callback Called with the result
*/
Account.prototype.entry = function (callback)
Account.prototype.entry = function (callback) {
var self = this;
var callback = typeof callback === 'function' ? callback : function(){};
this.get_info(function account_info(err, info) {
if (err) {
callback(err);
} else {
extend(self._entry, info.account_data);
self.emit('entry', self._entry);
callback(null, info);
}
});
return this;
};
Account.prototype.get_next_sequence = function(callback) {
var callback = typeof callback === 'function' ? callback : function(){};
this.get_info(function account_info(err, info) {
if (err) {
callback(err);
} else {
callback(null, info.account_data.Sequence);
}
});
return this;
};
/**
* Retrieve this account's Ripple trust lines.
*
* To keep up-to-date with changes to the AccountRoot entry, subscribe to the
* "lines" event. (Not yet implemented.)
*
* @param {function (err, lines)} callback Called with the result
*/
Account.prototype.lines = function (callback)
{
var self = this;
self._remote.request_account_info(this._account_id)
self._remote.request_account_lines(this._account_id)
.on('success', function (e) {
extend(self._entry, e.account_data);
self.emit('entry', self._entry);
self._lines = e.lines;
self.emit('lines', self._lines);
if ("function" === typeof callback) {
callback(null, e);
@@ -133,8 +189,7 @@ Account.prototype.entry = function (callback)
* This is only meant to be called by the Remote class. You should never have to
* call this yourself.
*/
Account.prototype.notifyTx = function (message)
{
Account.prototype.notifyTx = function (message) {
// Only trigger the event if the account object is actually
// subscribed - this prevents some weird phantom events from
// occurring.
@@ -143,6 +198,13 @@ Account.prototype.notifyTx = function (message)
}
};
exports.Account = Account;
Account.prototype.submit = function(tx) {
if (!this._tx_manager) {
this._tx_manager = new TransactionManager(this);
}
this._tx_manager.submit(tx);
};
exports.Account = Account;
// vim:sw=2:sts=2:ts=8:et

View File

@@ -556,6 +556,21 @@ Amount.prototype.negate = function () {
return this.clone('NEGATE');
};
/**
* Invert this amount and return the new value.
*
* Creates a new Amount object as a copy of the current one (including the same
* unit (currency & issuer), inverts it (1/x) and returns the result.
*/
Amount.prototype.invert = function () {
var one = this.clone();
one._value = BigInteger.ONE;
one._offset = 0;
one._is_negative = false;
one.canonicalize();
return one.ratio_human(this);
};
/**
* Tries to correctly interpret an amount as entered by a user.
*

View File

@@ -10,6 +10,7 @@ exports.SerializedObject = require('./serializedobject').SerializedObject;
exports.binformat = require('./binformat');
exports.utils = require('./utils');
exports.Server = require('./server').Server;
// Important: We do not guarantee any specific version of SJCL or for any
// specific features to be included. The version and configuration may change at

88
src/js/ripple/pathfind.js Normal file
View File

@@ -0,0 +1,88 @@
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var Amount = require('./amount').Amount;
var extend = require('extend');
/**
* Represents a persistent path finding request.
*
* Only one path find request is allowed per connection, so when another path
* find request is triggered it will supercede the existing one, making it emit
* the 'end' and 'superceded' events.
*/
var PathFind = function (remote, src_account, dst_account,
dst_amount, src_currencies)
{
EventEmitter.call(this);
this.remote = remote;
this.src_account = src_account;
this.dst_account = dst_account;
this.dst_amount = dst_amount;
this.src_currencies = src_currencies;
};
util.inherits(PathFind, EventEmitter);
/**
* Submits a path_find_create request to the network.
*
* This starts a path find request, superceding all previous path finds.
*
* This will be called automatically by Remote when this object is instantiated,
* so you should only have to call it if the path find was closed or superceded
* and you wish to restart it.
*/
PathFind.prototype.create = function ()
{
var self = this;
var req = this.remote.request_path_find_create(this.src_account,
this.dst_account,
this.dst_amount,
this.src_currencies,
handleInitialPath);
function handleInitialPath(err, msg) {
if (err) {
// XXX Handle error
return;
}
self.notify_update(msg);
}
req.request();
};
PathFind.prototype.close = function ()
{
this.remote.request_path_find_close().request();
this.emit('end');
this.emit('close');
};
PathFind.prototype.notify_update = function (message)
{
var src_account = message.source_account;
var dst_account = message.destination_account;
var dst_amount = Amount.from_json(message.destination_amount);
// Only pass the event along if this path find response matches what we were
// looking for.
if (this.src_account === src_account &&
this.dst_account === dst_account &&
this.dst_amount.equals(dst_amount)) {
this.emit('update', message);
}
};
PathFind.prototype.notify_superceded = function ()
{
this.emit('end');
this.emit('superceded');
};
exports.PathFind = PathFind;

File diff suppressed because it is too large Load Diff

269
src/js/ripple/request.js Normal file
View File

@@ -0,0 +1,269 @@
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var UInt160 = require('./uint160').UInt160;
var Currency = require('./currency').Currency;
var Transaction = require('./transaction').Transaction;
var Account = require('./account').Account;
var Meta = require('./meta').Meta;
var OrderBook = require('./orderbook').OrderBook;
var RippleError = require('./rippleerror').RippleError;
// Request events emitted:
// 'success' : Request successful.
// 'error' : Request failed.
// 'remoteError'
// 'remoteUnexpected'
// 'remoteDisconnected'
function Request(remote, command) {
EventEmitter.call(this);
this.remote = remote;
this.requested = false;
this.message = {
command : command,
id : void(0)
};
};
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, errorEvent) {
if (callback && typeof callback === 'function') {
function request_success(message) {
callback.call(this, null, message);
}
function request_error(error) {
if (!(error instanceof RippleError)) {
error = new RippleError(error);
}
callback.call(this, error);
}
this.once(successEvent || 'success', request_success);
this.once(errorEvent || 'error' , request_error);
this.request();
}
return this;
};
Request.prototype.timeout = function(duration, callback) {
var self = this;
if (!this.requested) {
function requested() {
self.timeout(duration, callback);
}
this.once('request', requested);
return;
}
var emit = this.emit;
var timed_out = false;
var timeout = setTimeout(function() {
timed_out = true;
if (typeof callback === 'function') callback();
emit.call(self, 'timeout');
}, duration);
this.emit = function() {
if (!timed_out) {
clearTimeout(timeout);
emit.apply(self, arguments);
}
};
return this;
};
Request.prototype.set_server = function(server) {
this.server = server;
};
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 (hash) {
this.message.ledger_hash = hash;
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 (Number(ledger_spec)) {
this.message.ledger_index = ledger_spec;
} else {
this.message.ledger_hash = ledger_spec;
}
break;
}
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 (secret) {
if (secret) {
this.message.secret = secret;
}
return this;
};
Request.prototype.tx_hash = function (hash) {
this.message.tx_hash = hash;
return this;
};
Request.prototype.tx_json = function (json) {
this.message.tx_json = json;
return this;
};
Request.prototype.tx_blob = function (json) {
this.message.tx_blob = json;
return this;
};
Request.prototype.ripple_state = function (account, issuer, currency) {
this.message.ripple_state = {
currency : currency,
accounts : [
UInt160.json_rewrite(account),
UInt160.json_rewrite(issuer)
]
};
return this;
};
Request.prototype.accounts = function (accounts, realtime) {
if (!Array.isArray(accounts)) {
accounts = [ accounts ];
}
// Process accounts parameters
var processedAccounts = accounts.map(function(account) {
return UInt160.json_rewrite(account);
});
if (realtime) {
this.message.rt_accounts = processedAccounts;
} else {
this.message.accounts = processedAccounts;
}
return this;
};
Request.prototype.rt_accounts = function (accounts) {
return this.accounts(accounts, true);
};
Request.prototype.books = function (books, snapshot) {
var processedBooks = [ ];
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 = json[side] = {
currency: Currency.json_rewrite(book[side].currency)
};
if (obj.currency !== 'XRP') {
obj.issuer = UInt160.json_rewrite(book[side].issuer);
}
}
processSide('taker_gets');
processSide('taker_pays');
if (snapshot) {
json.snapshot = true;
}
if (book.both) {
json.both = true;
}
processedBooks.push(json);
}
this.message.books = processedBooks;
return this;
};
exports.Request = Request;

View File

@@ -0,0 +1,22 @@
var util = require('util');
var extend = require('extend');
function RippleError(code, message) {
if (typeof code === 'object') {
extend(this, code);
} else {
this.result = code;
this.message = message;
this.result_message = message;
}
this.message = this.result_message || 'Error';
Error.captureStackTrace(this, code || this);
}
util.inherits(RippleError, Error);
RippleError.prototype.name = 'RippleError';
exports.RippleError = RippleError;

View File

@@ -5,16 +5,16 @@ var utils = require('./utils');
/**
* @constructor Server
* @param remote The Remote object
* @param cfg Configuration parameters.
* @param opts Configuration parameters.
*
* Keys for cfg:
* url
*/
var Server = function (remote, opts) {
function Server(remote, opts) {
EventEmitter.call(this);
if (typeof opts !== 'object' || typeof opts.url !== 'string') {
if (typeof opts !== 'object') {
throw new Error('Invalid server configuration.');
}
@@ -22,17 +22,20 @@ var Server = function (remote, opts) {
this._remote = remote;
this._opts = opts;
this._host = opts.host;
this._port = opts.port;
this._secure = typeof opts.secure === Boolean ? opts.secure : true;
this._ws = void(0);
this._connected = false;
this._should_connect = false;
this._state = void(0);
this._id = 0;
this._retry = 0;
this._requests = { };
this._opts.url = (opts.secure ? 'wss://' : 'ws://')
+ [ opts.host, opts.port ].join(':');
this.on('message', function(message) {
self._handle_message(message);
});
@@ -40,7 +43,7 @@ var Server = function (remote, opts) {
this.on('response_subscribe', function(message) {
self._handle_response_subscribe(message);
});
};
}
util.inherits(Server, EventEmitter);
@@ -68,16 +71,40 @@ Server.prototype._set_state = function (state) {
this.emit('state', state);
if (state === 'online') {
this._connected = true;
this.emit('connect');
} else if (state === 'offline') {
this._connected = false;
this.emit('disconnect');
switch (state) {
case 'online':
this._connected = true;
this.emit('connect');
break;
case 'offline':
this._connected = false;
this.emit('disconnect');
break;
}
}
};
Server.prototype._trace = function() {
if (this._remote.trace) {
utils.logObject.apply(utils, arguments);
}
};
Server.prototype._remote_address = function() {
try { var address = this._ws._socket.remoteAddress; } catch (e) { }
return address;
};
// This is the final interface between client code and a socket connection to a
// `rippled` server. As such, this is a decent hook point to allow a WebSocket
// interface conforming object to be used as a basis to mock rippled. This
// avoids the need to bind a websocket server to a port and allows a more
// synchronous style of code to represent a client <-> server message sequence.
// We can also use this to log a message sequence to a buffer.
Server.prototype.websocket_constructor = function () {
return require('ws');
};
Server.prototype.connect = function () {
var self = this;
@@ -85,16 +112,20 @@ Server.prototype.connect = function () {
// recently received a message from the server and the WebSocket has not
// reported any issues either. If we do fail to ping or the connection drops,
// we will automatically reconnect.
if (this._connected === true) return;
if (this._connected) {
return;
}
if (this._remote.trace) console.log('server: connect: %s', this._opts.url);
this._trace('server: connect: %s', this._opts.url);
// Ensure any existing socket is given the command to close first.
if (this._ws) this._ws.close();
if (this._ws) {
this._ws.close();
}
// We require this late, because websocket shims may be loaded after
// ripple-lib.
var WebSocket = require('ws');
var WebSocket = this.websocket_constructor();
var ws = this._ws = new WebSocket(this._opts.url);
this._should_connect = true;
@@ -103,46 +134,44 @@ Server.prototype.connect = function () {
ws.onopen = function () {
// If we are no longer the active socket, simply ignore any event
if (ws !== self._ws) return;
self.emit('socket_open');
// Subscribe to events
var request = self._remote._server_prepare_subscribe();
self.request(request);
if (ws === self._ws) {
self.emit('socket_open');
// Subscribe to events
var request = self._remote._server_prepare_subscribe();
self.request(request);
}
};
ws.onerror = function (e) {
// If we are no longer the active socket, simply ignore any event
if (ws !== self._ws) return;
if (ws === self._ws) {
self._trace('server: onerror: %s', e.data || e);
if (self._remote.trace) console.log('server: onerror: %s', e.data || e);
// Most connection errors for WebSockets are conveyed as 'close' events with
// code 1006. This is done for security purposes and therefore unlikely to
// ever change.
// Most connection errors for WebSockets are conveyed as 'close' events with
// code 1006. This is done for security purposes and therefore unlikely to
// ever change.
// This means that this handler is hardly ever called in practice. If it is,
// it probably means the server's WebSocket implementation is corrupt, or
// the connection is somehow producing corrupt data.
// This means that this handler is hardly ever called in practice. If it is,
// it probably means the server's WebSocket implementation is corrupt, or
// the connection is somehow producing corrupt data.
// Most WebSocket applications simply log and ignore this error. Once we
// support for multiple servers, we may consider doing something like
// lowering this server's quality score.
// Most WebSocket applications simply log and ignore this error. Once we
// support for multiple servers, we may consider doing something like
// lowering this server's quality score.
// However, in Node.js this event may be triggered instead of the close
// event, so we need to handle it.
handleConnectionClose();
// However, in Node.js this event may be triggered instead of the close
// event, so we need to handle it.
handleConnectionClose();
}
};
// Failure to open.
ws.onclose = function () {
// If we are no longer the active socket, simply ignore any event
if (ws !== self._ws) return;
if (self._remote.trace) console.log('server: onclose: %s', ws.readyState);
handleConnectionClose();
if (ws === self._ws) {
self._trace('server: onclose: %s', ws.readyState);
handleConnectionClose();
}
};
function handleConnectionClose() {
@@ -153,14 +182,17 @@ Server.prototype.connect = function () {
ws.onopen = ws.onerror = ws.onclose = ws.onmessage = function () {};
// Should we be connected?
if (!self._should_connect) return;
if (!self._should_connect) {
return;
}
// Delay and retry.
self._retry += 1;
self._retry_timer = setTimeout(function () {
if (self._remote.trace) console.log('server: retry');
if (!self._should_connect) return;
self._trace('server: retry');
if (!self._should_connect) {
return;
}
self.connect();
}, self._retry < 40
? 1000/20 // First, for 2 seconds: 20 times per second
@@ -185,7 +217,9 @@ Server.prototype.disconnect = function () {
};
Server.prototype.send_message = function (message) {
this._ws.send(JSON.stringify(message));
if (this._ws) {
this._ws.send(JSON.stringify(message));
}
};
/**
@@ -195,56 +229,54 @@ Server.prototype.request = function (request) {
var self = this;
// Only bother if we are still connected.
if (self._ws) {
request.message.id = self._id;
if (this._ws) {
request.server = this;
request.message.id = this._id;
self._requests[request.message.id] = request;
this._requests[request.message.id] = request;
// Advance message ID
self._id++;
this._id++;
if (self._connected || (request.message.command === 'subscribe' && self._ws.readyState === 1)) {
if (self._remote.trace) {
utils.logObject('server: request: %s', request.message);
}
self.send_message(request.message);
var is_connected = this._connected || (request.message.command === 'subscribe' && this._ws.readyState === 1);
if (is_connected) {
this._trace('server: request: %s', request.message);
this.send_message(request.message);
} else {
// XXX There are many ways to make self smarter.
self.once('connect', function () {
if (self._remote.trace) {
utils.logObject('server: request: %s', request.message);
}
// XXX There are many ways to make this smarter.
function server_reconnected() {
self._trace('server: request: %s', request.message);
self.send_message(request.message);
});
}
this.once('connect', server_reconnected);
}
} else {
if (self._remote.trace) {
utils.logObject('server: request: DROPPING: %s', request.message);
}
this._trace('server: request: DROPPING: %s', request.message);
}
};
Server.prototype._handle_message = function (json) {
Server.prototype._handle_message = function (message) {
var self = this;
var message;
try {
message = JSON.parse(json);
} catch(exception) { return; }
try { message = JSON.parse(message); } catch(e) { }
switch(message.type) {
var unexpected = typeof message !== 'object' || typeof message.type !== 'string';
if (unexpected) {
return;
}
switch (message.type) {
case 'response':
// A response to a request.
var request = self._requests[message.id];
delete self._requests[message.id];
if (!request) {
if (self._remote.trace) utils.logObject('server: UNEXPECTED: %s', message);
this._trace('server: UNEXPECTED: %s', message);
} else if ('success' === message.status) {
if (self._remote.trace) utils.logObject('server: response: %s', message);
this._trace('server: response: %s', message);
request.emit('success', message.result);
@@ -252,32 +284,33 @@ Server.prototype._handle_message = function (json) {
emitter.emit('response_' + request.message.command, message.result, request, message);
});
} else if (message.error) {
if (self._remote.trace) utils.logObject('server: error: %s', message);
this._trace('server: error: %s', message);
request.emit('error', {
'error' : 'remoteError',
'error_message' : 'Remote reported an error.',
'remote' : message
error : 'remoteError',
error_message : 'Remote reported an error.',
remote : message
});
}
break;
case 'path_find':
if (self._remote.trace) utils.logObject('server: path_find: %s', message);
break;
case 'serverStatus':
// This message is only received when online. As we are connected, it is the definative final state.
self._set_state(self._is_online(message.server_status) ? 'online' : 'offline');
this._set_state(this._is_online(message.server_status) ? 'online' : 'offline');
break;
}
};
}
Server.prototype._handle_response_subscribe = function (message) {
var self = this;
self._server_status = message.server_status;
if (self._is_online(message.server_status)) {
self._set_state('online');
this._server_status = message.server_status;
if (this._is_online(message.server_status)) {
this._set_state('online');
}
};
}
exports.Server = Server;

View File

@@ -1,4 +1,3 @@
//
// Transactions
//
// Construction:
@@ -53,86 +52,65 @@ var Currency = require('./amount').Currency;
var UInt160 = require('./amount').UInt160;
var Seed = require('./seed').Seed;
var SerializedObject = require('./serializedobject').SerializedObject;
var RippleError = require('./rippleerror').RippleError;
var config = require('./config');
var SUBMIT_MISSING = 4; // Report missing.
var SUBMIT_LOST = 8; // Give up tracking.
// A class to implement transactions.
// - Collects parameters
// - Allow event listeners to be attached to determine the outcome.
var Transaction = function (remote) {
function Transaction(remote) {
EventEmitter.call(this);
// YYY Make private as many variables as possible.
var self = this;
this.callback = undefined;
this.remote = remote;
this._secret = undefined;
this._build_path = false;
this.remote = remote;
this._secret = void(0);
this._build_path = false;
// Transaction data.
this.tx_json = {
'Flags' : 0, // XXX Would be nice if server did not require this.
};
this.tx_json = { Flags: 0 };
this.hash = undefined;
this.submit_index = undefined; // ledger_current_index was this when transaction was submited.
this.state = undefined; // Under construction.
this.finalized = false;
this.hash = void(0);
this.on('success', function (message) {
if (message.engine_result) {
self.hash = message.tx_json.hash;
// ledger_current_index was this when transaction was submited.
this.submit_index = void(0);
self.set_state('client_proposed');
// Under construction.
this.state = void(0);
self.emit('proposed', {
'tx_json' : message.tx_json,
'result' : message.engine_result,
'result_code' : message.engine_result_code,
'result_message' : message.engine_result_message,
'rejected' : self.isRejected(message.engine_result_code), // If server is honest, don't expect a final if rejected.
});
}
});
this.on('error', function (message) {
// Might want to give more detailed information.
self.set_state('remoteError');
});
this.finalized = false;
this._previous_signing_hash = void(0);
};
util.inherits(Transaction, EventEmitter);
// XXX This needs to be determined from the network.
Transaction.fee_units = {
'default' : 10,
default: 10,
};
Transaction.flags = {
'AccountSet' : {
'RequireDestTag' : 0x00010000,
'OptionalDestTag' : 0x00020000,
'RequireAuth' : 0x00040000,
'OptionalAuth' : 0x00080000,
'DisallowXRP' : 0x00100000,
'AllowXRP' : 0x00200000,
AccountSet: {
RequireDestTag: 0x00010000,
OptionalDestTag: 0x00020000,
RequireAuth: 0x00040000,
OptionalAuth: 0x00080000,
DisallowXRP: 0x00100000,
AllowXRP: 0x00200000
},
'OfferCreate' : {
'Passive' : 0x00010000,
'ImmediateOrCancel' : 0x00020000,
'FillOrKill' : 0x00040000,
'Sell' : 0x00080000,
OfferCreate: {
Passive: 0x00010000,
ImmediateOrCancel: 0x00020000,
FillOrKill: 0x00040000,
Sell: 0x00080000
},
'Payment' : {
'NoRippleDirect' : 0x00010000,
'PartialPayment' : 0x00020000,
'LimitQuality' : 0x00040000,
Payment: {
NoRippleDirect: 0x00010000,
PartialPayment: 0x00020000,
LimitQuality: 0x00040000
},
};
@@ -142,12 +120,12 @@ Transaction.HASH_SIGN = 0x53545800;
Transaction.HASH_SIGN_TESTNET = 0x73747800;
Transaction.prototype.consts = {
'telLOCAL_ERROR' : -399,
'temMALFORMED' : -299,
'tefFAILURE' : -199,
'terRETRY' : -99,
'tesSUCCESS' : 0,
'tecCLAIMED' : 100,
telLOCAL_ERROR : -399,
temMALFORMED : -299,
tefFAILURE : -199,
terRETRY : -99,
tesSUCCESS : 0,
tecCLAIMED : 100,
};
Transaction.prototype.isTelLocal = function (ter) {
@@ -185,6 +163,15 @@ Transaction.prototype.set_state = function (state) {
}
};
/**
* TODO
* Actually do this right
*/
Transaction.prototype.get_fee = function() {
return Transaction.fees['default'].to_json();
};
/**
* Attempts to complete the transaction for submission.
*
@@ -195,11 +182,13 @@ Transaction.prototype.set_state = function (state) {
Transaction.prototype.complete = function () {
var tx_json = this.tx_json;
if ("undefined" === typeof tx_json.Fee && this.remote.local_fee) {
this.tx_json.Fee = this.remote.fee_tx(this.fee_units()).to_json();
if (typeof tx_json.Fee === 'undefined') {
if (this.remote.local_fee || !this.remote.trusted) {
this.tx_json.Fee = this.remote.fee_tx(this.fee_units()).to_json();
}
}
if ("undefined" === typeof tx_json.SigningPubKey && (!this.remote || this.remote.local_signing)) {
if (typeof tx_json.SigningPubKey === 'undefined' && (!this.remote || this.remote.local_signing)) {
var seed = Seed.from_json(this._secret);
var key = seed.get_key(this.tx_json.Account);
tx_json.SigningPubKey = key.to_hex_pub();
@@ -213,16 +202,19 @@ Transaction.prototype.serialize = function () {
};
Transaction.prototype.signing_hash = function () {
var prefix = config.testnet
? Transaction.HASH_SIGN_TESTNET
: Transaction.HASH_SIGN;
var prefix = Transaction[config.testnet ? 'HASH_SIGN_TESTNET' : 'HASH_SIGN'];
return SerializedObject.from_json(this.tx_json).signing_hash(prefix);
};
Transaction.prototype.sign = function () {
var seed = Seed.from_json(this._secret);
var hash = this.signing_hash();
var previously_signed = this.tx_json.TxnSignature
&& hash === this._previous_signing_hash;
if (previously_signed) return;
var key = seed.get_key(this.tx_json.Account);
var sig = key.sign(hash, 0);
var hex = sjcl.codec.hex.fromBits(sig).toUpperCase();
@@ -230,213 +222,6 @@ Transaction.prototype.sign = function () {
this.tx_json.TxnSignature = hex;
};
Transaction.prototype._hasTransactionListeners = function() {
return this.listeners('final').length
|| this.listeners('lost').length
|| this.listeners('pending').length
};
// Submit a transaction to the network.
// XXX Don't allow a submit without knowing ledger_index.
// XXX Have a network canSubmit(), post events for following.
// XXX Also give broader status for tracking through network disconnects.
// callback = function (status, info) {
// // status is final status. Only works under a ledger_accepting conditions.
// switch status:
// case 'tesSUCCESS': all is well.
// case 'tejSecretUnknown': unable to sign transaction - secret unknown
// case 'tejServerUntrusted': sending secret to untrusted server.
// case 'tejInvalidAccount': locally detected error.
// case 'tejLost': locally gave up looking
// default: some other TER
// }
Transaction.prototype.submit = function (callback) {
var self = this;
var tx_json = this.tx_json;
this.callback = typeof callback === 'function'
? callback
: function(){};
function finish(err) {
self.emit('error', err);
self.callback('error', err);
}
if (typeof tx_json.Account !== 'string') {
finish({
'error' : 'tejInvalidAccount',
'error_message' : 'Bad account.'
});
return this;
}
// YYY Might check paths for invalid accounts.
this.complete();
//console.log('Callback or has listeners');
// There are listeners for callback, 'final', 'lost', or 'pending' arrange to emit them.
this.submit_index = this.remote._ledger_current_index;
// When a ledger closes, look for the result.
function on_ledger_closed(message) {
if (self.finalized) return;
var ledger_hash = message.ledger_hash;
var ledger_index = message.ledger_index;
var stop = false;
// XXX make sure self.hash is available.
var transaction_entry = self.remote.request_transaction_entry(self.hash)
transaction_entry.ledger_hash(ledger_hash)
transaction_entry.on('success', function (message) {
if (self.finalized) return;
self.set_state(message.metadata.TransactionResult);
self.remote.removeListener('ledger_closed', on_ledger_closed);
self.emit('final', message);
self.finalized = true;
self.callback(message.metadata.TransactionResult, message);
});
transaction_entry.on('error', function (message) {
if (self.finalized) return;
if (message.error === 'remoteError' && message.remote.error === 'transactionNotFound') {
if (self.submit_index + SUBMIT_LOST < ledger_index) {
self.set_state('client_lost'); // Gave up.
self.emit('lost');
self.callback('tejLost', message);
self.remote.removeListener('ledger_closed', on_ledger_closed);
self.emit('final', message);
self.finalized = true;
} else if (self.submit_index + SUBMIT_MISSING < ledger_index) {
self.set_state('client_missing'); // We don't know what happened to transaction, still might find.
self.emit('pending');
} else {
self.emit('pending');
}
}
// XXX Could log other unexpectedness.
});
transaction_entry.request();
};
this.remote.on('ledger_closed', on_ledger_closed);
this.once('error', function (message) {
self.callback(message.error, message);
});
this.set_state('client_submitted');
if (self.remote.local_sequence && !self.tx_json.Sequence) {
self.tx_json.Sequence = this.remote.account_seq(self.tx_json.Account, 'ADVANCE');
// console.log("Sequence: %s", self.tx_json.Sequence);
if (!self.tx_json.Sequence) {
//console.log('NO SEQUENCE');
// Look in the last closed ledger.
var account_seq = this.remote.account_seq_cache(self.tx_json.Account, false)
account_seq.on('success_account_seq_cache', function () {
// Try again.
self.submit();
})
account_seq.on('error_account_seq_cache', function (message) {
// XXX Maybe be smarter about this. Don't want to trust an untrusted server for this seq number.
// Look in the current ledger.
self.remote.account_seq_cache(self.tx_json.Account, 'CURRENT')
.on('success_account_seq_cache', function () {
// Try again.
self.submit();
})
.on('error_account_seq_cache', function (message) {
// Forward errors.
self.emit('error', message);
})
.request();
})
account_seq.request();
return this;
}
// If the transaction fails we want to either undo incrementing the sequence
// or submit a noop transaction to consume the sequence remotely.
this.once('success', function (res) {
if (typeof res.engine_result === 'string') {
switch (res.engine_result.slice(0, 3)) {
// Synchronous local error
case 'tej':
self.remote.account_seq(self.tx_json.Account, 'REWIND');
break;
case 'ter':
// XXX: What do we do in case of ter?
break;
case 'tel':
case 'tem':
case 'tef':
// XXX Once we have a transaction submission manager class, we can
// check if there are any other transactions pending. If there are,
// we should submit a dummy transaction to ensure those
// transactions are still valid.
//var noop = self.remote.transaction().account_set(self.tx_json.Account);
//noop.submit();
// XXX Hotfix. This only works if no other transactions are pending.
self.remote.account_seq(self.tx_json.Account, 'REWIND');
break;
}
}
});
}
// Prepare request
var request = this.remote.request_submit();
// Forward events
request.emit = this.emit.bind(this);
if (!this._secret && !this.tx_json.Signature) {
finish({
'result' : 'tejSecretUnknown',
'result_message' : "Could not sign transactions because we."
});
return this;
} else if (this.remote.local_signing) {
this.sign();
request.tx_blob(this.serialize().to_hex());
} else {
if (!this.remote.trusted) {
finish({
'result' : 'tejServerUntrusted',
'result_message' : "Attempt to give a secret to an untrusted server."
});
}
request.secret(this._secret);
request.build_path(this._build_path);
request.tx_json(this.tx_json);
}
request.request();
return this;
}
//
// Set options for Transactions
//
@@ -446,38 +231,36 @@ Transaction.prototype.submit = function (callback) {
// "blindly" because the sender has no idea of the actual cost except that is must be less than send max.
Transaction.prototype.build_path = function (build) {
this._build_path = build;
return this;
}
// tag should be undefined or a 32 bit integer.
// YYY Add range checking for tag.
Transaction.prototype.destination_tag = function (tag) {
if (tag !== undefined) {
if (tag !== void(0)) {
this.tx_json.DestinationTag = tag;
}
return this;
}
Transaction._path_rewrite = function (path) {
var path_new = [];
var props = [
'account'
, 'issuer'
, 'currency'
]
for (var i = 0, l = path.length; i < l; i++) {
var node = path[i];
var node_new = {};
var path_new = path.map(function(node) {
var node_new = { };
if ('account' in node)
node_new.account = UInt160.json_rewrite(node.account);
for (var prop in node) {
if (~props.indexOf(prop)) {
node_new[prop] = UInt160.json_rewrite(node[prop]);
}
}
if ('issuer' in node)
node_new.issuer = UInt160.json_rewrite(node.issuer);
if ('currency' in node)
node_new.currency = Currency.json_rewrite(node.currency);
path_new.push(node_new);
}
return node_new;
});
return path_new;
}
@@ -485,32 +268,29 @@ Transaction._path_rewrite = function (path) {
Transaction.prototype.path_add = function (path) {
this.tx_json.Paths = this.tx_json.Paths || [];
this.tx_json.Paths.push(Transaction._path_rewrite(path));
return this;
}
// --> paths: undefined or array of path
// A path is an array of objects containing some combination of: account, currency, issuer
Transaction.prototype.paths = function (paths) {
for (var i = 0, l = paths.length; i < l; i++) {
for (var i=0, l=paths.length; i<l; i++) {
this.path_add(paths[i]);
}
return this;
}
};
// If the secret is in the config object, it does not need to be provided.
Transaction.prototype.secret = function (secret) {
this._secret = secret;
}
};
Transaction.prototype.send_max = function (send_max) {
if (send_max) {
this.tx_json.SendMax = Amount.json_rewrite(send_max);
}
return this;
}
};
// tag should be undefined or a 32 bit integer.
// YYY Add range checking for tag.
@@ -518,9 +298,8 @@ Transaction.prototype.source_tag = function (tag) {
if (tag) {
this.tx_json.SourceTag = tag;
}
return this;
}
};
// --> rate: In billionths.
Transaction.prototype.transfer_rate = function (rate) {
@@ -531,7 +310,7 @@ Transaction.prototype.transfer_rate = function (rate) {
}
return this;
}
};
// Add flags to a transaction.
// --> flags: undefined, _flag_, or [ _flags_ ]
@@ -540,18 +319,15 @@ Transaction.prototype.set_flags = function (flags) {
var transaction_flags = Transaction.flags[this.tx_json.TransactionType];
// We plan to not define this field on new Transaction.
if (this.tx_json.Flags === undefined) {
if (this.tx_json.Flags === void(0)) {
this.tx_json.Flags = 0;
}
var flag_set = Array.isArray(flags) ? flags : [ flags ];
for (var index in flag_set) {
if (!flag_set.hasOwnProperty(index)) continue;
var flag = flag_set[index];
if (flag in transaction_flags) {
for (var i=0, l=flag_set.length; i<l; i++) {
var flag = flag_set[i];
if (transaction_flags.hasOwnProperty(flag)) {
this.tx_json.Flags += transaction_flags[flag];
} else {
// XXX Immediately report an error or mark it.
@@ -560,7 +336,7 @@ Transaction.prototype.set_flags = function (flags) {
}
return this;
}
};
//
// Transactions
@@ -582,26 +358,23 @@ Transaction.prototype.account_set = function (src) {
this._secret = this._account_secret(src);
this.tx_json.TransactionType = 'AccountSet';
this.tx_json.Account = UInt160.json_rewrite(src);
return this;
};
Transaction.prototype.claim = function (src, generator, public_key, signature) {
this._secret = this._account_secret(src);
this.tx_json.TransactionType = 'Claim';
this.tx_json.Generator = generator;
this.tx_json.PublicKey = public_key;
this.tx_json.Signature = signature;
this._secret = this._account_secret(src);
this.tx_json.TransactionType = 'Claim';
this.tx_json.Generator = generator;
this.tx_json.PublicKey = public_key;
this.tx_json.Signature = signature;
return this;
};
Transaction.prototype.offer_cancel = function (src, sequence) {
this._secret = this._account_secret(src);
this.tx_json.TransactionType = 'OfferCancel';
this.tx_json.Account = UInt160.json_rewrite(src);
this.tx_json.OfferSequence = Number(sequence);
this._secret = this._account_secret(src);
this.tx_json.TransactionType = 'OfferCancel';
this.tx_json.Account = UInt160.json_rewrite(src);
this.tx_json.OfferSequence = Number(sequence);
return this;
};
@@ -610,11 +383,15 @@ Transaction.prototype.offer_cancel = function (src, sequence) {
// --> expiration : if not undefined, Date or Number
// --> cancel_sequence : if not undefined, Sequence
Transaction.prototype.offer_create = function (src, taker_pays, taker_gets, expiration, cancel_sequence) {
this._secret = this._account_secret(src);
this.tx_json.TransactionType = 'OfferCreate';
this.tx_json.Account = UInt160.json_rewrite(src);
this.tx_json.TakerPays = Amount.json_rewrite(taker_pays);
this.tx_json.TakerGets = Amount.json_rewrite(taker_gets);
this._secret = this._account_secret(src);
this.tx_json.TransactionType = 'OfferCreate';
this.tx_json.Account = UInt160.json_rewrite(src);
this.tx_json.TakerPays = Amount.json_rewrite(taker_pays);
this.tx_json.TakerGets = Amount.json_rewrite(taker_gets);
if (this.remote.local_fee) {
//this.tx_json.Fee = Transaction.fees.offer.to_json();
}
if (expiration) {
this.tx_json.Expiration = expiration instanceof Date
@@ -630,21 +407,19 @@ Transaction.prototype.offer_create = function (src, taker_pays, taker_gets, expi
};
Transaction.prototype.password_fund = function (src, dst) {
this._secret = this._account_secret(src);
this.tx_json.TransactionType = 'PasswordFund';
this.tx_json.Destination = UInt160.json_rewrite(dst);
this._secret = this._account_secret(src);
this.tx_json.TransactionType = 'PasswordFund';
this.tx_json.Destination = UInt160.json_rewrite(dst);
return this;
}
Transaction.prototype.password_set = function (src, authorized_key, generator, public_key, signature) {
this._secret = this._account_secret(src);
this.tx_json.TransactionType = 'PasswordSet';
this.tx_json.RegularKey = authorized_key;
this.tx_json.Generator = generator;
this.tx_json.PublicKey = public_key;
this.tx_json.Signature = signature;
this._secret = this._account_secret(src);
this.tx_json.TransactionType = 'PasswordSet';
this.tx_json.RegularKey = authorized_key;
this.tx_json.Generator = generator;
this.tx_json.PublicKey = public_key;
this.tx_json.Signature = signature;
return this;
}
@@ -666,29 +441,40 @@ Transaction.prototype.password_set = function (src, authorized_key, generator, p
// .set_flags()
// .source_tag()
Transaction.prototype.payment = function (src, dst, deliver_amount) {
this._secret = this._account_secret(src);
this.tx_json.TransactionType = 'Payment';
this.tx_json.Account = UInt160.json_rewrite(src);
this.tx_json.Amount = Amount.json_rewrite(deliver_amount);
this.tx_json.Destination = UInt160.json_rewrite(dst);
if (!UInt160.is_valid(src)) {
throw new Error('Payment source address invalid');
}
if (!UInt160.is_valid(dst)) {
throw new Error('Payment destination address invalid');
}
this._secret = this._account_secret(src);
this.tx_json.TransactionType = 'Payment';
this.tx_json.Account = UInt160.json_rewrite(src);
this.tx_json.Amount = Amount.json_rewrite(deliver_amount);
this.tx_json.Destination = UInt160.json_rewrite(dst);
return this;
}
Transaction.prototype.ripple_line_set = function (src, limit, quality_in, quality_out) {
this._secret = this._account_secret(src);
this.tx_json.TransactionType = 'TrustSet';
this.tx_json.Account = UInt160.json_rewrite(src);
this._secret = this._account_secret(src);
this.tx_json.TransactionType = 'TrustSet';
this.tx_json.Account = UInt160.json_rewrite(src);
// Allow limit of 0 through.
if (limit !== undefined)
this.tx_json.LimitAmount = Amount.json_rewrite(limit);
if (limit !== void(0)) {
this.tx_json.LimitAmount = Amount.json_rewrite(limit);
}
if (quality_in) {
this.tx_json.QualityIn = quality_in;
}
if (quality_in)
this.tx_json.QualityIn = quality_in;
if (quality_out)
this.tx_json.QualityOut = quality_out;
if (quality_out) {
this.tx_json.QualityOut = quality_out;
}
// XXX Throw an error if nothing is set.
@@ -702,7 +488,6 @@ Transaction.prototype.wallet_add = function (src, amount, authorized_key, public
this.tx_json.RegularKey = authorized_key;
this.tx_json.PublicKey = public_key;
this.tx_json.Signature = signature;
return this;
};
@@ -717,11 +502,42 @@ Transaction.prototype.wallet_add = function (src, amount, authorized_key, public
*
* @return {Number} Number of fee units for this transaction.
*/
Transaction.prototype.fee_units = function ()
{
return Transaction.fee_units["default"];
Transaction.prototype.fee_units = function () {
return Transaction.fee_units['default'];
};
exports.Transaction = Transaction;
// Submit a transaction to the network.
Transaction.prototype.submit = function (callback) {
var self = this;
this.callback = typeof callback === 'function' ? callback : function(){};
function submission_error(error, message) {
if (!(error instanceof RippleError)) {
error = new RippleError(error, message);
}
self.callback(error);
}
function submission_success(message) {
self.callback(null, message);
}
this.once('error', submission_error);
this.once('success', submission_success);
var account = this.tx_json.Account;
if (typeof account !== 'string') {
this.emit('error', new RippleError('tejInvalidAccount', 'Account is unspecified'));
} else {
// YYY Might check paths for invalid accounts.
this.remote.get_account(account).submit(this);
}
return this;
}
exports.Transaction = Transaction;
// vim:sw=2:sts=2:ts=8:et

View File

@@ -0,0 +1,363 @@
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var RippleError = require('./rippleerror').RippleError;
var Queue = require('./transactionqueue').TransactionQueue;
/**
* @constructor TransactionManager
* @param {Object} account
*/
function TransactionManager(account) {
EventEmitter.call(this);
var self = this;
this.account = account;
this.remote = account._remote;
this._timeout = void(0);
this._resubmitting = false;
this._pending = new Queue;
this._next_sequence = void(0);
this._cache = { };
//XX Fee units
this._max_fee = Number(this.remote.max_fee) || Infinity;
function remote_disconnected() {
function remote_reconnected() {
self._resubmit();
};
self.remote.once('connect', remote_reconnected);
};
this.remote.on('disconnect', remote_disconnected);
function sequence_loaded(err, sequence) {
self._next_sequence = sequence;
self.emit('sequence_loaded', sequence);
};
this.account.get_next_sequence(sequence_loaded);
function cache_transaction(message) {
var transaction = {
ledger_hash: message.ledger_hash,
ledger_index: message.ledger_index,
metadata: message.meta,
tx_json: message.transaction
}
transaction.tx_json.ledger_index = transaction.ledger_index;
transaction.tx_json.inLedger = transaction.ledger_index;
self._cache[message.transaction.Sequence] = transaction;
}
this.account.on('transaction', cache_transaction);
};
util.inherits(TransactionManager, EventEmitter);
// request_tx presents transactions in
// a format slightly different from
// request_transaction_entry
function rewrite_transaction(tx) {
try {
var result = {
ledger_index: tx.ledger_index,
metadata: tx.meta,
tx_json: {
Account: tx.Account,
Amount: tx.Amount,
Destination: tx.Destination,
Fee: tx.Fee,
Flags: tx.Flags,
Sequence: tx.Sequence,
SigningPubKey: tx.SigningPubKey,
TransactionType: tx.TransactionType,
hash: tx.hash
}
}
} catch(exception) {
}
return result || { };
};
TransactionManager.prototype._resubmit = function() {
var self = this;
function resubmit(pending, index) {
if (pending.finalized) {
// Transaction has been finalized, nothing to do
return;
}
var sequence = pending.tx_json.Sequence;
var cached = self._cache[sequence];
pending.emit('resubmit');
if (cached) {
// Transaction was received while waiting for
// resubmission
pending.emit('success', cached);
delete self._cache[sequence];
} else if (pending.hash) {
// Transaction was successfully submitted, and
// its hash discovered, but not validated
function pending_check(err, res) {
if (self._is_not_found(err)) {
self._request(pending);
} else {
pending.emit('success', rewrite_transaction(res));
}
}
self.remote.request_tx(pending.hash, pending_check);
} else {
self._request(pending);
}
}
this._wait_ledgers(3, function() {
self._pending.forEach(resubmit);
});
}
TransactionManager.prototype._wait_ledgers = function(ledgers, callback) {
var self = this;
var closes = 0;
function ledger_closed() {
if (++closes === ledgers) {
callback();
self.remote.removeListener('ledger_closed', ledger_closed);
}
}
this.remote.on('ledger_closed', ledger_closed);
}
TransactionManager.prototype._request = function(tx) {
var self = this;
var remote = this.remote;
if (!tx._secret && !tx.tx_json.TxnSignature) {
tx.emit('error', new RippleError('tejSecretUnknown', 'Missing secret'));
return;
}
if (!remote.trusted && !remote.local_signing) {
tx.emit('error', new RippleError('tejServerUntrusted', 'Attempt to give secret to untrusted server'));
return;
}
function finalize(message) {
if (!tx.finalized) {
tx.finalized = true;
tx.emit('final', message);
self._pending.removeSequence(tx.tx_json.Sequence);
}
}
tx.once('error', finalize);
tx.once('success', finalize);
// Listen for 'ledger closed' events to verify
// that the transaction is discovered in the
// ledger before considering the transaction
// successful
this._detect_ledger_entry(tx);
var submit_request = remote.request_submit();
if (remote.local_signing) {
tx.sign();
submit_request.tx_blob(tx.serialize().to_hex());
} else {
submit_request.secret(tx._secret);
submit_request.build_path(tx._build_path);
submit_request.tx_json(tx.tx_json);
}
tx.submit_index = remote._ledger_current_index;
function transaction_proposed(message) {
tx.hash = message.tx_json.hash;
tx.set_state('client_proposed');
tx.emit('proposed', {
tx_json: message.tx_json,
result: message.engine_result,
engine_result: message.engine_result,
result_code: message.engine_result_code,
engine_result_code: message.engine_result_code,
result_message: message.engine_result_message,
engine_result_message: message.engine_result_message,
// If server is honest, don't expect a final if rejected.
rejected: tx.isRejected(message.engine_result_code),
});
}
function transaction_failed(message) {
if (!tx.hash) tx.hash = message.tx_json.hash;
function transaction_requested(err, res) {
if (self._is_not_found(err)) {
self._resubmit();
} else {
tx.emit('error', new RippleError(message));
self._pending.removeSequence(tx.tx_json.Sequence);
}
}
self.remote.request_tx(tx.hash, transaction_requested);
}
function submission_error(err) {
tx.set_state('remoteError');
tx.emit('error', new RippleError(err));
}
function submission_success(message) {
var engine_result = message.engine_result || '';
tx.hash = message.tx_json.hash;
switch (engine_result.slice(0, 3)) {
case 'tef':
//tefPAST_SEQ
transaction_failed(message);
break;
case 'tes':
transaction_proposed(message);
break;
default:
submission_error(message);
}
}
submit_request.once('success', submission_success);
submit_request.once('error', submission_error);
submit_request.request();
submit_request.timeout(1000 * 10, function() {
if (self.remote._connected) {
self._resubmit();
}
});
tx.set_state('client_submitted');
tx.emit('submitted');
return submit_request;
};
TransactionManager.prototype._is_not_found = function(error) {
var not_found_re = /^(txnNotFound|transactionNotFound)$/;
return error && typeof error === 'object'
&& error.error === 'remoteError'
&& typeof error.remote === 'object'
&& not_found_re.test(error.remote.error);
};
TransactionManager.prototype._detect_ledger_entry = function(tx) {
var self = this;
var remote = this.remote;
var checked_ledgers = { };
function entry_callback(err, message) {
if (typeof message !== 'object') return;
var ledger_hash = message.ledger_hash;
var ledger_index = message.ledger_index;
if (tx.finalized || checked_ledgers[ledger_hash]) {
// Transaction submission has already erred or
// this ledger has already been checked for
// transaction
return;
}
checked_ledgers[ledger_hash] = true;
if (self._is_not_found(err)) {
var dif = ledger_index - tx.submit_index;
if (dif >= 8) {
// Lost
tx.emit('error', message);
tx.emit('lost', message);
} else if (dif >= 4) {
// Missing
tx.set_state('client_missing');
tx.emit('missing', message);
} else {
// Pending
tx.emit('pending', message);
}
} else {
// Transaction was found in the ledger,
// consider this transaction successful
if (message.metadata) {
tx.set_state(message.metadata.TransactionResult);
}
tx.emit('success', message);
}
}
function ledger_closed(message) {
if (!tx.finalized && !checked_ledgers[message.ledger_hash]) {
remote.request_transaction_entry(tx.hash, message.ledger_hash, entry_callback);
}
}
function transaction_proposed() {
// Check the ledger for transaction entry
remote.addListener('ledger_closed', ledger_closed);
}
function transaction_finalized() {
// Stop checking the ledger
remote.removeListener('ledger_closed', ledger_closed);
tx.removeListener('proposed', transaction_proposed);
}
tx.once('proposed', transaction_proposed);
tx.once('final', transaction_finalized);
tx.once('resubmit', transaction_finalized);
};
/**
* @param {Object} tx
*/
TransactionManager.prototype.submit = function(tx) {
// If sequence number is not yet known, defer until it is.
var self = this;
if (!this._next_sequence) {
function resubmit_transaction() {
self.submit(tx);
}
this.once('sequence_loaded', resubmit_transaction);
return;
}
tx.tx_json.Sequence = this._next_sequence++;
tx.complete();
this._pending.push(tx);
var fee = tx.tx_json.Fee;
if (fee === void(0) || fee <= this._max_fee) {
this._request(tx);
}
};
exports.TransactionManager = TransactionManager;

View File

@@ -0,0 +1,59 @@
function TransactionQueue() {
this._queue = [ ];
}
TransactionQueue.prototype.length = function() {
return this._queue.length;
};
TransactionQueue.prototype.push = function(o) {
return this._queue.push(o);
};
TransactionQueue.prototype.hasHash = function(hash) {
return this.indexOf('hash', hash) !== -1;
};
TransactionQueue.prototype.hasSequence = function(sequence) {
return this.indexOf('sequence', sequence) !== -1;
};
TransactionQueue.prototype.indexOf = function(prop, val) {
var index = -1;
for (var i=0, tx; tx=this._queue[i]; i++) {
if (tx[prop] === val) {
index = i;
break;
}
}
return index;
};
TransactionQueue.prototype.removeSequence = function(sequence) {
var result = [ ];
for (var i=0, tx; tx=this._queue[i]; i++) {
if (!tx.tx_json) continue;
if (tx.tx_json.Sequence !== sequence)
result.push(tx);
}
this._queue = result;
};
TransactionQueue.prototype.removeHash = function(hash) {
var result = [ ];
for (var i=0, tx; tx=this._queue[i]; i++) {
if (!tx.tx_json) continue;
if (tx.hash !== hash)
result.push(tx);
}
this._queue = result;
};
TransactionQueue.prototype.forEach = function(fn) {
for (var i=0, tx; tx=this._queue[i]; i++) {
fn(tx, i);
}
};
exports.TransactionQueue = TransactionQueue;

View File

@@ -1 +1,6 @@
module.exports = WebSocket;
// If there is no WebSocket, try MozWebSocket (support for some old browsers)
try {
module.exports = WebSocket
} catch(err) {
module.exports = MozWebSocket
}