mirror of
https://github.com/Xahau/xahau.js.git
synced 2026-06-03 16:56:41 +00:00
Merge branch 'develop' of https://github.com/ripple/ripple-lib into develop
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
88
src/js/ripple/pathfind.js
Normal 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
269
src/js/ripple/request.js
Normal 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;
|
||||
22
src/js/ripple/rippleerror.js
Normal file
22
src/js/ripple/rippleerror.js
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
363
src/js/ripple/transactionmanager.js
Normal file
363
src/js/ripple/transactionmanager.js
Normal 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;
|
||||
59
src/js/ripple/transactionqueue.js
Normal file
59
src/js/ripple/transactionqueue.js
Normal 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;
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user