Merge branch 'develop'

This commit is contained in:
Stefan Thomas
2013-07-23 21:36:43 -07:00
9 changed files with 978 additions and 871 deletions

2
.npmignore Normal file
View File

@@ -0,0 +1,2 @@
build
deploy

118
README.md
View File

@@ -3,9 +3,121 @@ Ripple JavaScript Library - ripple-lib
This library can connect to the Ripple network via the WebSocket protocol and runs in Node.js as well as in the browser.
Build instructions:
* https://ripple.com/wiki/Ripple_JavaScript_library
For more information:
* https://ripple.com
* https://ripple.com/wiki
##Initializing a remote connection
[ripple-lib.remote](https://github.com/ripple/ripple-lib/blob/develop/src/js/ripple/remote.js) is responsible for managing connections to rippled servers.
```js
var Remote = require('ripple-lib').Remote;
var remote = new Remote({
trusted: false,
servers: [
{
host: ''
, port: 1111,
, secure: true
}
]
});
remote.connect();
```
Once a connection is formed to any of the supplied servers, a `connect` event is emitted, indicating that the remote is ready to begin fulfilling requests. When there are no more connected servers to fulfill requests, a `disconnect` event is emitted. If you send requests before ripple-lib is connected to any servers, requests are deferred until the `connect` event is received.
```js
var remote = new Remote({ /* options */ }).connect();
remote.request_server_info(function(err, info) { }); // will defer until connected
```
##Remote functions
Each remote function returns a `Request` object. is object is an `EventEmitter`. You may listen for success or failure events from each request, or provide a callback. Example:
```js
var request = remote.request_server_info();
request.on('success', function(res) {
//handle success conditions
});
request.on('error', function(err) {
//handle error conditions
});
request.request();
```
Or:
```js
remote.request_server_info(function(err, res) {
});
```
**request_server_info([callback])**
**request_ledger(ledger, [opts], [callback])**
**request_ledger_hash([callback])**
**request_ledger_header([callback])**
**request_ledger_current([callback])**
**request_ledger_entry(type, [callback])**
**request_subscribe(streams, [callback])**
**request_unsubscribe(streams, [callback])**
**request_transaction_entry(hash, [callback])**
**request_tx(hash, [callback])**
**request_account_info(accountID, [callback])**
**request_account_lines(accountID, account_index, current, [callback])**
**request_account_offers(accountID, account_index, current, [callback])**
**request_account_tx(opts, [callback])**
**request_book_offers(gets, pays, taker, [callback])**
**request_wallet_accounts(seed, [callback])**
+ requires trusted **remote
**request_sign(secret, tx_json, [callback])**
+ requires trusted **remote
**request_submit([callback])**
**request_account_balance(account, current, [callback])**
**request_account_flags(account, current, [callback])**
**request_owner_count(account, current, [callback])**
**request_ripple_balance(account, issuer, currency, current, [callback])**
**request_ripple_path_find(src_account, dst_account, dst_amount, src_currencies, [callback])**
**request_unl_list([callback])**
**request_unl_add(addr, comment, [callback])**
**request_unl_delete(node, [callback])**
**request_peers([callback])**
**request_connect(ip, port, [callback])**
**transaction()**
+ 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.15",
"version": "0.7.17",
"description": "Ripple JavaScript client library",
"files": [
"src/js/ripple/*.js",
@@ -13,17 +13,18 @@
},
"dependencies": {
"async": "~0.2.9",
"ws": "~0.4.25",
"ws": "~0.4.27",
"extend": "~1.1.3",
"simple-jsonrpc": "~0.0.2"
"simple-jsonrpc": "~0.0.2",
"jshint-loader": "~0.5.0"
},
"devDependencies": {
"grunt": "~0.4.1",
"grunt-contrib-concat": "~0.3.0",
"grunt-contrib-watch": "~0.4.0",
"grunt-webpack": "~0.10.2",
"grunt-dox": "~0.4.1",
"buster": "~0.6.2"
"grunt-contrib-watch": "~0.4.4",
"grunt-webpack": "~0.10.5",
"grunt-dox": "~0.5.0",
"buster": "~0.6.12"
},
"scripts": {
"test": "node_modules/buster/bin/buster test"

View File

@@ -882,6 +882,8 @@ Amount.prototype.to_text = function (allow_nan) {
* @param opts.min_precision {Number} Min. number of digits after dec. point.
* @param opts.skip_empty_fraction {Boolean} Don't show fraction if it is zero,
* even if min_precision is set.
* @param opts.max_sig_digits {Number} Maximum number of significant digits.
* Will cut fractional part, but never integer part.
* @param opts.group_sep {Boolean|String} Whether to show a separator every n
* digits, if a string, that value will be used as the separator. Default: ","
* @param opts.group_width {Number} How many numbers will be grouped together,
@@ -914,10 +916,38 @@ Amount.prototype.to_human = function (opts)
fraction_part = fraction_part.replace(/0*$/, '');
if (fraction_part.length || !opts.skip_empty_fraction) {
// Enforce the maximum number of decimal digits (precision)
if ("number" === typeof opts.precision) {
fraction_part = fraction_part.slice(0, opts.precision);
}
// Limit the number of significant digits (max_sig_digits)
if ("number" === typeof opts.max_sig_digits) {
// First, we count the significant digits we have.
// A zero in the integer part does not count.
var int_is_zero = +int_part === 0;
var digits = int_is_zero ? 0 : int_part.length;
// Don't count leading zeros in the fractional part if the integer part is
// zero.
var sig_frac = int_is_zero ? fraction_part.replace(/^0*/, '') : fraction_part;
digits += sig_frac.length;
// Now we calculate where we are compared to the maximum
var rounding = digits - opts.max_sig_digits;
// If we're under the maximum we want to cut no (=0) digits
rounding = Math.max(rounding, 0);
// If we're over the maximum we still only want to cut digits from the
// fractional part, not from the integer part.
rounding = Math.min(rounding, fraction_part.length);
// Now we cut `rounding` digits off the right.
if (rounding > 0) fraction_part = fraction_part.slice(0, -rounding);
}
// Enforce the minimum number of decimal digits (min_precision)
if ("number" === typeof opts.min_precision) {
while (fraction_part.length < opts.min_precision) {
fraction_part += "0";

View File

@@ -9,35 +9,32 @@
// var network = require("./network.js");
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var util = require('util');
var Amount = require('./amount').Amount;
var UInt160 = require('./uint160').UInt160;
var Currency = require('./currency').Currency;
var Amount = require('./amount').Amount;
var UInt160 = require('./uint160').UInt160;
var Currency = require('./currency').Currency;
var extend = require('extend');
var extend = require('extend');
var OrderBook = function (remote,
currency_gets, issuer_gets,
currency_pays, issuer_pays) {
var OrderBook = function (remote, currency_gets, issuer_gets, currency_pays, issuer_pays) {
EventEmitter.call(this);
var self = this;
var self = this;
this._remote = remote;
this._remote = remote;
this._currency_gets = currency_gets;
this._issuer_gets = issuer_gets;
this._issuer_gets = issuer_gets;
this._currency_pays = currency_pays;
this._issuer_pays = issuer_pays;
this._subs = 0;
this._issuer_pays = issuer_pays;
this._subs = 0;
// We consider ourselves synchronized if we have a current copy of the offers,
// we are online and subscribed to updates.
this._sync = false;
this._sync = false;
// Offers
this._offers = [];
this._offers = [];
this.on('newListener', function (type, listener) {
if (OrderBook.subscribe_events.indexOf(type) !== -1) {
@@ -49,10 +46,9 @@ var OrderBook = function (remote,
});
this.on('removeListener', function (type, listener) {
if (OrderBook.subscribe_events.indexOf(type) !== -1) {
if (~OrderBook.subscribe_events.indexOf(type)) {
self._subs -= 1;
if (!self._subs && 'open' === self._remote._online_state) {
if (!self._subs && self._remote._connected) {
self._sync = false;
self._remote.request_unsubscribe()
.books([self.to_json()])
@@ -86,8 +82,7 @@ OrderBook.subscribe_events = ['transaction', 'model', 'trade'];
*
* @private
*/
OrderBook.prototype._subscribe = function ()
{
OrderBook.prototype._subscribe = function () {
var self = this;
self._remote.request_subscribe()
.books([self.to_json()], true)
@@ -95,26 +90,28 @@ OrderBook.prototype._subscribe = function ()
// XXX What now?
})
.on('success', function (res) {
self._sync = true;
self._sync = true;
self._offers = res.offers;
self.emit('model', self._offers);
})
.request();
};
OrderBook.prototype.to_json = function ()
{
OrderBook.prototype.to_json = function () {
var json = {
"taker_gets": {
"currency": this._currency_gets
'taker_gets': {
'currency': this._currency_gets
},
"taker_pays": {
"currency": this._currency_pays
'taker_pays': {
'currency': this._currency_pays
}
};
if (this._currency_gets !== "XRP") json["taker_gets"]["issuer"] = this._issuer_gets;
if (this._currency_pays !== "XRP") json["taker_pays"]["issuer"] = this._issuer_pays;
if (this._currency_gets !== 'XRP')
json['taker_gets']['issuer'] = this._issuer_gets;
if (this._currency_pays !== 'XRP')
json['taker_pays']['issuer'] = this._issuer_pays;
return json;
};
@@ -125,78 +122,88 @@ OrderBook.prototype.to_json = function ()
* Note: This only checks whether the parameters (currencies and issuer) are
* syntactically valid. It does not check anything against the ledger.
*/
OrderBook.prototype.is_valid = function ()
{
OrderBook.prototype.is_valid = function () {
// XXX Should check for same currency (non-native) && same issuer
return (
Currency.is_valid(this._currency_pays) &&
(this._currency_pays === "XRP" || UInt160.is_valid(this._issuer_pays)) &&
(this._currency_pays === 'XRP' || UInt160.is_valid(this._issuer_pays)) &&
Currency.is_valid(this._currency_gets) &&
(this._currency_gets === "XRP" || UInt160.is_valid(this._issuer_gets)) &&
!(this._currency_pays === "XRP" && this._currency_gets === "XRP")
(this._currency_gets === 'XRP' || UInt160.is_valid(this._issuer_gets)) &&
!(this._currency_pays === 'XRP' && this._currency_gets === 'XRP')
);
};
OrderBook.prototype.trade = function(type) {
var tradeStr = '0'
+ (this['_currency_' + type] === 'XRP') ? '' : '/'
+ this['_currency_' + type ] + '/'
+ this['_issuer_' + type];
return Amount.from_json(tradeStr);
};
/**
* Notify object of a relevant transaction.
*
* This is only meant to be called by the Remote class. You should never have to
* call this yourself.
*/
OrderBook.prototype.notifyTx = function (message)
{
var self = this;
var changed = false;
var trade_gets = Amount.from_json("0" + ((this._currency_gets === 'XRP') ? "" :
"/" + this._currency_gets +
"/" + this._issuer_gets));
var trade_pays = Amount.from_json("0" + ((this._currency_pays === 'XRP') ? "" :
"/" + this._currency_pays +
"/" + this._issuer_pays));
OrderBook.prototype.notifyTx = function (message) {
var self = this;
var changed = false;
var trade_gets = this.trade('gets');
var trade_pays = this.trade('pays');
message.mmeta.each(function (an) {
if (an.entryType !== 'Offer') return;
var i, l, offer;
if (an.diffType === 'DeletedNode' ||
an.diffType === 'ModifiedNode') {
for (i = 0, l = self._offers.length; i < l; i++) {
offer = self._offers[i];
if (offer.index === an.ledgerIndex) {
if (an.diffType === 'DeletedNode') {
self._offers.splice(i, 1);
switch(an.diffType) {
case 'DeletedNode':
case 'ModifiedNode':
var deletedNode = an.diffType === 'DeletedNode';
for (i = 0, l = self._offers.length; i < l; i++) {
offer = self._offers[i];
if (offer.index === an.ledgerIndex) {
if (deletedNode) {
self._offers.splice(i, 1);
} else {
extend(offer, an.fieldsFinal);
}
changed = true;
break;
}
else extend(offer, an.fieldsFinal);
changed = true;
break;
}
}
// We don't want to count a OfferCancel as a trade
if (message.transaction.TransactionType === "OfferCancel") return;
// We don't want to count a OfferCancel as a trade
if (message.transaction.TransactionType === 'OfferCancel') return;
trade_gets = trade_gets.add(an.fieldsPrev.TakerGets);
trade_pays = trade_pays.add(an.fieldsPrev.TakerPays);
if (an.diffType === 'ModifiedNode') {
trade_gets = trade_gets.subtract(an.fieldsFinal.TakerGets);
trade_pays = trade_pays.subtract(an.fieldsFinal.TakerPays);
}
} else if (an.diffType === 'CreatedNode') {
var price = Amount.from_json(an.fields.TakerPays).ratio_human(an.fields.TakerGets);
for (i = 0, l = self._offers.length; i < l; i++) {
offer = self._offers[i];
var priceItem = Amount.from_json(offer.TakerPays).ratio_human(offer.TakerGets);
trade_gets = trade_gets.add(an.fieldsPrev.TakerGets);
trade_pays = trade_pays.add(an.fieldsPrev.TakerPays);
if (price.compareTo(priceItem) <= 0) {
var obj = an.fields;
obj.index = an.ledgerIndex;
self._offers.splice(i, 0, an.fields);
changed = true;
break;
if (!deletedNode) {
trade_gets = trade_gets.subtract(an.fieldsFinal.TakerGets);
trade_pays = trade_pays.subtract(an.fieldsFinal.TakerPays);
}
}
break;
case 'CreatedNode':
var price = Amount.from_json(an.fields.TakerPays).ratio_human(an.fields.TakerGets);
for (i = 0, l = self._offers.length; i < l; i++) {
offer = self._offers[i];
var priceItem = Amount.from_json(offer.TakerPays).ratio_human(offer.TakerGets);
if (price.compareTo(priceItem) <= 0) {
var obj = an.fields;
obj.index = an.ledgerIndex;
self._offers.splice(i, 0, an.fields);
changed = true;
break;
}
}
break;
}
});
@@ -218,17 +225,13 @@ OrderBook.prototype.notifyTx = function (message)
*
* If the data is available immediately, the callback may be called synchronously.
*/
OrderBook.prototype.offers = function (callback)
{
OrderBook.prototype.offers = function (callback) {
var self = this;
if ("function" === typeof callback) {
if (typeof callback === 'function') {
if (this._sync) {
callback(this._offers);
} else {
this.once('model', function (offers) {
callback(offers);
});
this.once('model', callback);
}
}
return this;
@@ -240,11 +243,10 @@ OrderBook.prototype.offers = function (callback)
* Usually, this will just be an empty array if the order book hasn't been
* loaded yet. But this accessor may be convenient in some circumstances.
*/
OrderBook.prototype.offersSync = function ()
{
OrderBook.prototype.offersSync = function () {
return this._offers;
};
exports.OrderBook = OrderBook;
exports.OrderBook = OrderBook;
// vim:sw=2:sts=2:ts=8:et

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,7 @@ var extend = require('extend'),
var amount = require('./amount'),
UInt160 = amount.UInt160,
UInt256 = require('./uint256').UInt256,
Amount = amount.Amount,
Currency= amount.Currency;
@@ -109,8 +110,8 @@ var STHash128 = exports.Hash128 = new SerializedType({
var STHash256 = exports.Hash256 = new SerializedType({
serialize: function (so, val) {
// XXX
throw new Error("Serializing Hash256 not implemented");
var hash = UInt256.from_json(val);
this.serialize_hex(so, hash.to_hex());
},
parse: function (so) {
// XXX

View File

@@ -1,20 +1,17 @@
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var WebSocket = require('ws');
var utils = require('./utils');
/**
* Server
* @constructor Server
* @param remote The Remote object
* @param cfg Configuration parameters.
*
* Options must contain `url` to
* WebSocket server
*
* @constructor
* @param {Object} remote
* @param {Object} opts
*/
* Keys for cfg:
* url
*/
function Server(remote, opts) {
var Server = function (remote, opts) {
EventEmitter.call(this);
if (typeof opts !== 'object' || typeof opts.url !== 'string') {
@@ -53,8 +50,7 @@ util.inherits(Server, EventEmitter);
* Our requirements are that the server can process transactions and notify
* us of changes.
*/
Server.online_states = [
Server.online_states = [
'syncing'
, 'tracking'
, 'proposing'
@@ -62,24 +58,27 @@ Server.online_states = [
, 'full'
];
/**
* Determine if a server status qualifies
* as 'online'
*
* @param {String} status
* @return {Boolean}
* @api private
*/
Server.prototype.is_online = function(status) {
Server.prototype._is_online = function (status) {
return Server.online_states.indexOf(status) !== -1;
};
/**
* Connect to WebSocket server
*/
Server.prototype._set_state = function (state) {
if (state !== this._state) {
this._state = state;
Server.prototype.connect = function() {
this.emit('state', state);
if (state === 'online') {
this._connected = true;
this.emit('connect');
} else if (state === 'offline') {
this._connected = false;
this.emit('disconnect');
}
}
};
Server.prototype.connect = function () {
var self = this;
// We don't connect if we believe we're already connected. This means we have
@@ -88,22 +87,21 @@ Server.prototype.connect = function() {
// we will automatically reconnect.
if (this._connected === true) return;
if (this._remote.trace) {
console.log('server: connect: %s', this._opts.url);
}
if (this._remote.trace) console.log('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 ws = this._ws = new WebSocket(this._opts.url);
this._should_connect = true;
self.emit('connecting');
ws.onopen = function() {
ws.onopen = function () {
// If we are no longer the active socket, simply ignore any event
if (ws !== self._ws) return;
@@ -114,13 +112,11 @@ Server.prototype.connect = function() {
self.request(request);
};
ws.onerror = function(e) {
ws.onerror = function (e) {
// If we are no longer the active socket, simply ignore any event
if (ws !== self._ws) return;
if (self._remote.trace) {
console.log('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
@@ -140,13 +136,11 @@ Server.prototype.connect = function() {
};
// Failure to open.
ws.onclose = function() {
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);
}
if (self._remote.trace) console.log('server: onclose: %s', ws.readyState);
handleConnectionClose();
};
@@ -156,17 +150,16 @@ Server.prototype.connect = function() {
self._set_state('offline');
// Prevent additional events from this socket
ws.removeAllListeners();
ws.on('error', function() {});
ws.onopen = ws.onerror = ws.onclose = ws.onmessage = function () {};
// Should we be connected?
if (!self._should_connect) return;
// Delay and retry.
self._retry += 1;
self._retry_timer = setTimeout(function retryTimeout() {
self._retry += 1;
self._retry_timer = setTimeout(function () {
if (self._remote.trace) console.log('server: retry');
if (!self._should_connect) return;
self.connect();
}, self._retry < 40
@@ -176,20 +169,14 @@ Server.prototype.connect = function() {
: self._retry < 40+60+60
? 10*1000 // Then, for 10 minutes: once every 10 seconds
: 30*1000); // Then: once every 30 seconds
};
}
ws.onmessage = function(msg) {
ws.onmessage = function (msg) {
self.emit('message', msg.data);
};
};
/**
* Disconnect from WebSocket server
*
* @api public
*/
Server.prototype.disconnect = function() {
Server.prototype.disconnect = function () {
this._should_connect = false;
this._set_state('offline');
if (this._ws) {
@@ -197,27 +184,14 @@ Server.prototype.disconnect = function() {
}
};
/**
* Send stringified message to WebSocket server
*
* @param {Object} message
* @api private
*/
Server.prototype.send = function(message) {
if (this._ws) {
this._ws.send(JSON.stringify(message));
}
Server.prototype.send_message = function (message) {
this._ws.send(JSON.stringify(message));
};
/**
* Submit a Request object to this server.
*
* @param {Object} request
* @api public
*/
Server.prototype.request = function(request) {
Server.prototype.request = function (request) {
var self = this;
// Only bother if we are still connected.
@@ -229,19 +203,19 @@ Server.prototype.request = function(request) {
// Advance message ID
self._id++;
if (self._connected || (request.message.command === 'subscribe'
&& self._ws.readyState === 1)) {
if (self._connected || (request.message.command === 'subscribe' && self._ws.readyState === 1)) {
if (self._remote.trace) {
utils.logObject('server: request: %s', request.message);
}
self.send(request.message);
self.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);
}
self.send(request.message);
self.send_message(request.message);
});
}
} else {
@@ -251,111 +225,56 @@ Server.prototype.request = function(request) {
}
};
/**
* Set server state
*
* Examples:
*
* set_state('online')
* set_state('offline')
*
* @param {String} state
* @api private
*/
Server.prototype._handle_message = function (json) {
var self = this;
Server.prototype._set_state = function(state) {
if (state !== this._state) {
this._state = state;
this.emit('state', state);
if (state === 'online') {
this._connected = true;
this.emit('connect');
} else if (state === 'offline') {
this._connected = false;
this.emit('disconnect');
}
}
};
/**
* Handle WebSocket message
*
* @param {String} json
* @api private
*/
Server.prototype._handle_message = function(json) {
var self = this;
var unexpected = false;
var message;
try { message = JSON.parse(json); } catch(exception) { }
try {
message = JSON.parse(json);
} catch(exception) { return; }
var unexpected = typeof message !== 'object' || typeof message.type !== 'string';
switch(message.type) {
case 'response':
// A response to a request.
var request = self._requests[message.id];
if (unexpected) {
// We received a malformed response from the server
}
delete self._requests[message.id];
if (!unexpected) {
switch (message.type) {
case 'response':
// A response to a request.
var request = self._requests[message.id];
if (!request) {
if (self._remote.trace) utils.logObject('server: UNEXPECTED: %s', message);
} else if ('success' === message.status) {
if (self._remote.trace) utils.logObject('server: response: %s', message);
delete self._requests[message.id];
request.emit('success', message.result);
if (!request) {
if (self._remote.trace) {
utils.logObject('server: UNEXPECTED: %s', message);
}
} else if (message.status === 'success') {
if (self._remote.trace) {
utils.logObject('server: response: %s', message);
}
[ self, self._remote ].forEach(function(emitter) {
emitter.emit('response_' + request.message.command, message.result, request, message);
});
} else if (message.error) {
if (self._remote.trace) utils.logObject('server: error: %s', message);
request.emit('success', message.result);
request.emit('error', {
'error' : 'remoteError',
'error_message' : 'Remote reported an error.',
'remote' : message
});
}
break;
[ self, self._remote ].forEach(function(emitter) {
emitter.emit('response_' + request.message.command, message.result, request, message);
});
} else if (message.error) {
if (self._remote.trace) {
utils.logObject('server: error: %s', message);
}
request.emit('error', {
'error' : 'remoteError',
'error_message' : 'Remote reported an error.',
'remote' : message
});
}
break;
case 'serverStatus':
// This message is only received when online.
// As we are connected, it is the definitive final state.
self._set_state(self.is_online(message.server_status) ? 'online' : 'offline');
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');
break;
}
};
/**
* Handle subscribe response
*
* @param {Object} message
* @api private
*/
Server.prototype._handle_response_subscribe = function(message) {
Server.prototype._handle_response_subscribe = function (message) {
var self = this;
self._server_status = message.server_status;
if (self.is_online(message.server_status)) {
if (self._is_online(message.server_status)) {
self._set_state('online');
}
};

View File

@@ -72,43 +72,44 @@ var Transaction = function (remote) {
this.remote = remote;
this._secret = undefined;
this._build_path = false;
this.tx_json = { // Transaction data.
'Flags' : 0, // XXX Would be nice if server did not require this.
// Transaction data.
this.tx_json = {
'Flags' : 0, // XXX Would be nice if server did not require this.
};
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.on('success', function (message) {
if (message.engine_result) {
self.hash = message.tx_json.hash;
if (message.engine_result) {
self.hash = message.tx_json.hash;
self.set_state('client_proposed');
self.set_state('client_proposed');
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.
});
}
});
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');
});
// Might want to give more detailed information.
self.set_state('remoteError');
});
};
util.inherits(Transaction, EventEmitter);
// XXX This needs to be determined from the network.
Transaction.fees = {
'default' : Amount.from_json("10"),
'nickname_create' : Amount.from_json("1000"),
'offer' : Amount.from_json("10"),
'default' : 10,
};
Transaction.flags = {
@@ -194,15 +195,17 @@ Transaction.prototype.set_state = function (state) {
Transaction.prototype.complete = function () {
var tx_json = this.tx_json;
if (undefined === tx_json.Fee && this.remote.local_fee) {
tx_json.Fee = Transaction.fees['default'].to_json();
if ("undefined" === typeof tx_json.Fee && this.remote.local_fee) {
this.tx_json.Fee = "" + Math.ceil(this.remote.fee_tx() * this.fee_units());
}
if (undefined === tx_json.SigningPubKey && (!this.remote || this.remote.local_signing)) {
if ("undefined" === typeof tx_json.SigningPubKey && (!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();
}
return this.tx_json;
};
Transaction.prototype.serialize = function () {
@@ -211,23 +214,28 @@ Transaction.prototype.serialize = function () {
Transaction.prototype.signing_hash = function () {
var prefix = config.testnet
? Transaction.HASH_SIGN_TESTNET
: Transaction.HASH_SIGN;
? Transaction.HASH_SIGN_TESTNET
: Transaction.HASH_SIGN;
return SerializedObject.from_json(this.tx_json).signing_hash(prefix);
};
Transaction.prototype.sign = function () {
var seed = Seed.from_json(this._secret),
hash = this.signing_hash();
var key = seed.get_key(this.tx_json.Account),
sig = key.sign(hash, 0),
hex = sjcl.codec.hex.fromBits(sig).toUpperCase();
var seed = Seed.from_json(this._secret);
var hash = this.signing_hash();
var key = seed.get_key(this.tx_json.Account);
var sig = key.sign(hash, 0);
var hex = sjcl.codec.hex.fromBits(sig).toUpperCase();
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.
@@ -242,18 +250,25 @@ Transaction.prototype.sign = function () {
// 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 = callback;
this.callback = typeof callback === 'function'
? callback
: function(){};
if ('string' !== typeof tx_json.Account)
{
(this.callback || this.emit)('error', {
'error' : 'tejInvalidAccount',
'error_message' : 'Bad account.'
});
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;
}
@@ -261,143 +276,142 @@ Transaction.prototype.submit = function (callback) {
this.complete();
if (this.callback || this.listeners('final').length || this.listeners('lost').length || this.listeners('pending').length) {
// There are listeners for callback, 'final', 'lost', or 'pending' arrange to emit them.
//console.log('Callback or has listeners');
this.submit_index = this.remote._ledger_current_index;
// There are listeners for callback, 'final', 'lost', or 'pending' arrange to emit them.
// When a ledger closes, look for the result.
var on_ledger_closed = function (message) {
var ledger_hash = message.ledger_hash;
var ledger_index = message.ledger_index;
var stop = false;
this.submit_index = this.remote._ledger_current_index;
// XXX make sure self.hash is available.
self.remote.request_transaction_entry(self.hash)
.ledger_hash(ledger_hash)
.on('success', function (message) {
if (self.finalized) return;
// When a ledger closes, look for the result.
function on_ledger_closed(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;
var ledger_hash = message.ledger_hash;
var ledger_index = message.ledger_index;
var stop = false;
if (self.callback)
self.callback(message.metadata.TransactionResult, message);
})
.on('error', function (message) {
if (self.finalized) return;
// XXX make sure self.hash is available.
var transaction_entry = self.remote.request_transaction_entry(self.hash)
if ('remoteError' === message.error
&& 'transactionNotFound' === message.remote.error) {
if (self.submit_index + SUBMIT_LOST < ledger_index) {
self.set_state('client_lost'); // Gave up.
self.emit('lost');
transaction_entry.ledger_hash(ledger_hash)
if (self.callback)
self.callback('tejLost', message);
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);
});
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.
})
.request();
};
transaction_entry.on('error', function (message) {
if (self.finalized) return;
this.remote.on('ledger_closed', on_ledger_closed);
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.
});
if (this.callback) {
this.on('error', function (message) {
self.callback(message.error, message);
});
}
}
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');
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.
this.remote.account_seq_cache(self.tx_json.Account, false)
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) {
// 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();
// 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.on('success', function (res) {
if (!res || "string" !== typeof res.engine_result) return;
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;
switch (res.engine_result.slice(0, 3)) {
// Synchronous local error
case 'tej':
self.remote.account_seq(self.tx_json.Account, 'REWIND');
break;
// XXX: What do we do in case of ter?
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();
case 'ter':
// XXX: What do we do in case of ter?
break;
// XXX Hotfix. This only works if no other transactions are pending.
self.remote.account_seq(self.tx_json.Account, 'REWIND');
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 successes and errors.
request.on('success', function (message) {
self.emit('success', message);
});
request.on('error', function (message) {
self.emit('error', message);
});
// Forward events
request.emit = this.emit.bind(this);
if (!this._secret && !this.tx_json.Signature) {
this.emit('error', {
finish({
'result' : 'tejSecretUnknown',
'result_message' : "Could not sign transactions because we."
});
@@ -407,11 +421,10 @@ Transaction.prototype.submit = function (callback) {
request.tx_blob(this.serialize().to_hex());
} else {
if (!this.remote.trusted) {
this.emit('error', {
finish({
'result' : 'tejServerUntrusted',
'result_message' : "Attempt to give a secret to an untrusted server."
});
return this;
}
request.secret(this._secret);
@@ -440,8 +453,9 @@ Transaction.prototype.build_path = function (build) {
// tag should be undefined or a 32 bit integer.
// YYY Add range checking for tag.
Transaction.prototype.destination_tag = function (tag) {
if (undefined !== tag)
this.tx_json.DestinationTag = tag;
if (tag !== undefined) {
this.tx_json.DestinationTag = tag;
}
return this;
}
@@ -491,8 +505,9 @@ Transaction.prototype.secret = function (secret) {
}
Transaction.prototype.send_max = function (send_max) {
if (send_max)
this.tx_json.SendMax = Amount.json_rewrite(send_max);
if (send_max) {
this.tx_json.SendMax = Amount.json_rewrite(send_max);
}
return this;
}
@@ -500,8 +515,9 @@ Transaction.prototype.send_max = function (send_max) {
// tag should be undefined or a 32 bit integer.
// YYY Add range checking for tag.
Transaction.prototype.source_tag = function (tag) {
if (undefined !== tag)
this.tx_json.SourceTag = tag;
if (tag) {
this.tx_json.SourceTag = tag;
}
return this;
}
@@ -510,8 +526,9 @@ Transaction.prototype.source_tag = function (tag) {
Transaction.prototype.transfer_rate = function (rate) {
this.tx_json.TransferRate = Number(rate);
if (this.tx_json.TransferRate < 1e9)
throw 'invalidTransferRate';
if (this.tx_json.TransferRate < 1e9) {
throw new Error('invalidTransferRate');
}
return this;
}
@@ -520,24 +537,26 @@ Transaction.prototype.transfer_rate = function (rate) {
// --> flags: undefined, _flag_, or [ _flags_ ]
Transaction.prototype.set_flags = function (flags) {
if (flags) {
var transaction_flags = Transaction.flags[this.tx_json.TransactionType];
var transaction_flags = Transaction.flags[this.tx_json.TransactionType];
if (undefined == this.tx_json.Flags) // We plan to not define this field on new Transaction.
this.tx_json.Flags = 0;
// We plan to not define this field on new Transaction.
if (this.tx_json.Flags === undefined) {
this.tx_json.Flags = 0;
}
var flag_set = 'object' === typeof flags ? flags : [ flags ];
var flag_set = Array.isArray(flags) ? flags : [ flags ];
for (var index in flag_set) {
if (!flag_set.hasOwnProperty(index)) continue;
for (var index in flag_set) {
if (!flag_set.hasOwnProperty(index)) continue;
var flag = flag_set[index];
var flag = flag_set[index];
if (flag in transaction_flags) {
this.tx_json.Flags += transaction_flags[flag];
} else {
// XXX Immediately report an error or mark it.
}
if (flag in transaction_flags) {
this.tx_json.Flags += transaction_flags[flag];
} else {
// XXX Immediately report an error or mark it.
}
}
}
return this;
@@ -597,17 +616,15 @@ Transaction.prototype.offer_create = function (src, taker_pays, taker_gets, expi
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
? expiration.getTime()
: Number(expiration);
}
if (expiration)
this.tx_json.Expiration = Date === expiration.constructor
? expiration.getTime()
: Number(expiration);
if (cancel_sequence)
this.tx_json.OfferSequence = Number(cancel_sequence);
if (cancel_sequence) {
this.tx_json.OfferSequence = Number(cancel_sequence);
}
return this;
};
@@ -664,7 +681,7 @@ Transaction.prototype.ripple_line_set = function (src, limit, quality_in, qualit
this.tx_json.Account = UInt160.json_rewrite(src);
// Allow limit of 0 through.
if (undefined !== limit)
if (limit !== undefined)
this.tx_json.LimitAmount = Amount.json_rewrite(limit);
if (quality_in)
@@ -689,6 +706,20 @@ Transaction.prototype.wallet_add = function (src, amount, authorized_key, public
return this;
};
/**
* Returns the number of fee units this transaction will cost.
*
* Each Ripple transaction based on its type and makeup costs a certain number
* of fee units. The fee units are calculated on a per-server basis based on the
* current load on both the network and the server.
*
* @see https://ripple.com/wiki/Transaction_Fee
*/
Transaction.prototype.fee_units = function ()
{
return Transaction.fees["default"];
};
exports.Transaction = Transaction;
// vim:sw=2:sts=2:ts=8:et