Separate Server fee calculation, select fee-optimal servers while submitting a transaction

This commit is contained in:
wltsmrz
2014-01-03 20:08:24 -08:00
parent 5c3f3ff0f1
commit fd614ce0df
4 changed files with 194 additions and 144 deletions

View File

@@ -28,10 +28,9 @@ var Meta = require('./meta').Meta;
var OrderBook = require('./orderbook').OrderBook;
var PathFind = require('./pathfind').PathFind;
var RippleError = require('./rippleerror').RippleError;
var utils = require('./utils');
var config = require('./config');
var sjcl = require('./utils').sjcl;
var config = require('./config');
/**
Interface to manage the connection to a Ripple server.
@@ -99,13 +98,6 @@ function Remote(opts, trace) {
this.state = 'offline'; // 'online', 'offline'
this.retry_timer = void(0);
this.retry = void(0);
this._load_base = 256;
this._load_factor = 256;
this._fee_ref = 10;
this._fee_base = 10;
this._reserve_base = void(0);
this._reserve_inc = void(0);
this._connection_count = 0;
this._connected = false;
this._connection_offset = 1000 * (typeof opts.connection_offset === 'number' ? opts.connection_offset : 5)
@@ -172,6 +164,7 @@ function Remote(opts, trace) {
// This is used to remove Node EventEmitter warnings
var maxListeners = opts.maxListeners || opts.max_listeners || 0;
this._servers.concat(this).forEach(function(emitter) {
emitter.setMaxListeners(maxListeners);
});
@@ -440,6 +433,10 @@ Remote.prototype._handleMessage = function(message, server) {
}
break;
case 'serverStatus':
self.emit('server_status', message);
break;
case 'transaction':
// To get these events, just subscribe to them. A subscribes and
// unsubscribes will be added as needed.
@@ -493,29 +490,6 @@ Remote.prototype._handleMessage = function(message, server) {
this.emit('path_find_all', message);
break;
case 'serverStatus':
self.emit('server_status', message);
var loadChanged = message.hasOwnProperty('load_base')
&& message.hasOwnProperty('load_factor')
&& (message.load_base !== self._load_base || message.load_factor !== self._load_factor)
;
if (loadChanged) {
self._load_base = message.load_base;
self._load_factor = message.load_factor;
var obj = {
load_base: self._load_base,
load_factor: self._load_factor,
fee_units: self.feeTxUnit()
}
self.emit('load', obj);
self.emit('load_changed', obj);
}
break;
// All other messages
default:
this._trace('remote: ' + message.type + ': ', message);
@@ -542,6 +516,11 @@ Remote.isValidLedgerData = function(ledger) {
&& (typeof ledger.txn_count === 'number')
};
Remote.isLoadStatus = function(message) {
return (typeof message.load_base === 'number')
&& (typeof message.load_factor === 'number');
};
Remote.prototype.ledgerHash = function() {
return this._ledger_hash;
};
@@ -1010,10 +989,6 @@ Remote.prototype.requestSubmit = function(callback) {
return new Request(this, 'submit').callback(callback);
};
//
// Higher level functions.
//
/**
* Create a subscribe request with current subscriptions.
*
@@ -1034,15 +1009,17 @@ Remote.prototype._serverPrepareSubscribe = function(callback) {
var request = this.requestSubscribe(feeds);
request.once('success', function(message) {
function serverSubscribed(message) {
self._stand_alone = !!message.stand_alone;
self._testnet = !!message.testnet;
if (typeof message.random === 'string') {
var rand = message.random.match(/[0-9A-F]{8}/ig);
while (rand && rand.length) {
sjcl.random.addEntropy(parseInt(rand.pop(), 16));
}
self.emit('random', utils.hexToArray(message.random));
}
@@ -1053,22 +1030,14 @@ Remote.prototype._serverPrepareSubscribe = function(callback) {
self.emit('ledger_closed', message);
}
// FIXME Use this to estimate fee.
// XXX When we have multiple server support, most of this should be tracked
// by the Server objects and then aggregated/interpreted by Remote.
self._load_base = message.load_base || 256;
self._load_factor = message.load_factor || 256;
self._fee_ref = message.fee_ref;
self._fee_base = message.fee_base;
self._reserve_base = message.reserve_base;
self._reserve_inc = message.reserve_inc;
self.emit('subscribed');
});
};
request.once('success', serverSubscribed);
self.emit('prepare_subscribe', request);
request.callback(callback);
request.callback(callback, 'subscribed');
// XXX Could give error events, maybe even time out.
@@ -1123,7 +1092,7 @@ Remote.prototype.requestAccountBalance = function(account, ledger, callback) {
return Amount.from_json(message.node.Balance);
};
var args = Array.prototype.concat.apply(['account_balance', responseFilter], arguments);
var args = Array.prototype.concat.apply(['account_balance', responseFilter], arguments);
var request = Remote.accountRootRequest.apply(this, args);
return request;
@@ -1135,7 +1104,7 @@ Remote.prototype.requestAccountFlags = function(account, ledger, callback) {
return message.node.Flags;
};
var args = Array.prototype.concat.apply(['account_flags', responseFilter], arguments);
var args = Array.prototype.concat.apply(['account_flags', responseFilter], arguments);
var request = Remote.accountRootRequest.apply(this, args);
return request;
@@ -1147,7 +1116,7 @@ Remote.prototype.requestOwnerCount = function(account, ledger, callback) {
return message.node.OwnerCount;
};
var args = Array.prototype.concat.apply(['owner_count', responseFilter], arguments);
var args = Array.prototype.concat.apply(['owner_count', responseFilter], arguments);
var request = Remote.accountRootRequest.apply(this, args);
return request;
@@ -1330,7 +1299,8 @@ Remote.prototype.requestRippleBalance = function(account, issuer, currency, ledg
request.rippleState(account, issuer, currency);
request.ledgerChoose(ledger);
request.once('success', function(message) {
function rippleState(message) {
var node = message.node;
var lowLimit = Amount.from_json(node.LowLimit);
var highLimit = Amount.from_json(node.HighLimit);
@@ -1352,8 +1322,9 @@ Remote.prototype.requestRippleBalance = function(account, issuer, currency, ledg
account_quality_out : ( accountHigh ? node.HighQualityOut : node.LowQualityOut),
peer_quality_out : (!accountHigh ? node.HighQualityOut : node.LowQualityOut),
});
});
};
request.once('success', rippleState);
request.callback(callback, 'ripple_state');
return request;
@@ -1499,9 +1470,9 @@ Remote.prototype.transaction = function(source, destination, amount, callback) {
*
* @return {Amount} Final fee in XRP for specified number of fee units.
*/
Remote.prototype.feeTx = function(units) {
var fee_unit = this.feeTxUnit();
return Amount.from_json(String(Math.ceil(units * fee_unit)));
return this._getServer().feeTx(units);
};
/**
@@ -1512,16 +1483,9 @@ Remote.prototype.feeTx = function(units) {
*
* @return {Number} Recommended amount for one fee unit as float.
*/
Remote.prototype.feeTxUnit = function() {
var fee_unit = this._fee_base / this._fee_ref;
// Apply load fees
fee_unit *= this._load_factor / this._load_base;
// Apply fee cushion (a safety margin in case fees rise since we were last updated
fee_unit *= this.fee_cushion;
return fee_unit;
return this._getServer().feeTxUnit();
};
/**
@@ -1529,16 +1493,9 @@ Remote.prototype.feeTxUnit = function() {
*
* Returns the base reserve with load fees and safety margin applied.
*/
Remote.prototype.reserve = function(owner_count) {
var reserve_base = Amount.from_json(String(this._reserve_base));
var reserve_inc = Amount.from_json(String(this._reserve_inc));
var owner_count = owner_count || 0;
if (owner_count < 0) {
throw new Error('Owner count must not be negative.');
}
return reserve_base.add(reserve_inc.product_human(owner_count));
return this._getServer().reserve(owner_count);
};
Remote.prototype.ping = function(host, callback) {

View File

@@ -1,7 +1,8 @@
var Amount = require('./amount').Amount;
var utils = require('./utils');
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var Transaction = require('./transaction').Transaction;
var Amount = require('./amount').Amount;
var utils = require('./utils');
/**
* @constructor Server
@@ -295,7 +296,7 @@ Server.prototype._handleClose = function() {
this._setState('offline');
// Prevent additional events from this socket
ws.onopen = ws.onerror = ws.onclose = ws.onmessage = function() {};
ws.onopen = ws.onerror = ws.onclose = ws.onmessage = function noOp() {};
if (self._shouldConnect) {
this._retryConnect();
@@ -314,22 +315,34 @@ Server.prototype._handleMessage = function(message) {
try { message = JSON.parse(message); } catch(e) { }
if (!this.isValidMessage(message)) return;
if (!Server.isValidMessage(message)) return;
switch (message.type) {
case 'serverStatus':
// This message is only received when online.
// As we are connected, it is the definitive final state.
this._setState(~(Server._onlineStates.indexOf(message.server_status)) ? 'online' : 'offline');
break;
case 'ledgerClosed':
this._lastLedgerClose = Date.now();
this.emit('ledger_closed', message);
break;
case 'path_find':
this._remote._trace('server: path_find:', self._opts.url, message);
case 'serverStatus':
// This message is only received when online.
// As we are connected, it is the definitive final state.
this._setState(~(Server.onlineStates.indexOf(message.server_status)) ? 'online' : 'offline');
if (Server.isLoadStatus(message)) {
self.emit('load', message, self);
self._remote.emit('load', message, self);
var loadChanged = ((message.load_base !== self._load_base) ||
(message.load_factor !== self._load_factor));
if (loadChanged) {
self._load_base = message.load_base;
self._load_factor = message.load_factor;
self.emit('load_changed', message, self);
self._remote.emit('load_changed', message, self);
}
}
break;
case 'response':
@@ -357,6 +370,11 @@ Server.prototype._handleMessage = function(message) {
});
}
break;
case 'path_find':
this._remote._trace('server: path_find:', self._opts.url, message);
break;
}
};
@@ -366,11 +384,23 @@ Server.prototype._handleMessage = function(message) {
* @api private
*/
Server.prototype.isValidMessage = function(message) {
Server.isValidMessage = function(message) {
return (typeof message === 'object')
&& (typeof message.type === 'string');
};
/**
* Check that received serverStatus message contains
* load status information
*
* @api private
*/
Server.isLoadStatus = function(message) {
return (typeof message.load_base === 'number')
&& (typeof message.load_factor === 'number');
};
/**
* Handle subscription response messages. Subscription response
* messages indicate that a connection to the server is ready
@@ -382,6 +412,14 @@ Server.prototype._handleResponseSubscribe = function(message) {
if (~(Server.onlineStates.indexOf(message.server_status))) {
this._setState('online');
}
if (Server.isLoadStatus(message)) {
this._load_base = message.load_base || 256;
this._load_factor = message.load_factor || 256;
this._fee_ref = message.fee_ref;
this._fee_base = message.fee_base;
this._reserve_base = message.reserve_base;
this._reserve_inc = message.reserve_inc;
}
};
/**
@@ -437,7 +475,33 @@ Server.prototype.request = function(request) {
};
Server.prototype._isConnected = function(request) {
return this._connected || (request.message.command === 'subscribe' && this._ws.readyState === 1);
var isSubscribeRequest = request
&& request.message.command === 'subscribe'
&& this._ws.readyState === 1;
return this._connected || (this._ws && isSubscribeRequest);
};
/**
* Calculate transaction fee
*
* @param {Transaction|Number} Fee units for a provided transaction
* @return {Number} Final fee in XRP for specified number of fee units
* @api private
*/
Server.prototype.computeFee = function(transaction) {
var units;
if (transaction instanceof Transaction) {
units = transaction.feeUnits();
} else if (typeof transaction === 'number') {
units = transaction;
} else {
throw new Error('Invalid argument');
}
return this.feeTx(units).to_json();
};
/**
@@ -445,8 +509,10 @@ Server.prototype._isConnected = function(request) {
*
* This takes into account the last known network and local load fees.
*
* @param {Number} Fee units for a provided transaction
* @return {Amount} Final fee in XRP for specified number of fee units.
*/
Server.prototype.feeTx = function(units) {
var fee_unit = this.feeTxUnit();
return Amount.from_json(String(Math.ceil(units * fee_unit)));
@@ -460,13 +526,14 @@ Server.prototype.feeTx = function(units) {
*
* @return {Number} Recommended amount for one fee unit as float.
*/
Server.prototype.feeTxUnit = function() {
var fee_unit = this._fee_base / this._fee_ref;
// Apply load fees
fee_unit *= this._load_factor / this._load_base;
// Apply fee cushion (a safety margin in case fees rise since we were last updated
// Apply fee cushion (a safety margin in case fees rise since we were last updated)
fee_unit *= this._fee_cushion;
return fee_unit;
@@ -477,6 +544,7 @@ Server.prototype.feeTxUnit = function() {
*
* Returns the base reserve with load fees and safety margin applied.
*/
Server.prototype.reserve = function(owner_count) {
var reserve_base = Amount.from_json(String(this._reserve_base));
var reserve_inc = Amount.from_json(String(this._reserve_inc));

View File

@@ -44,18 +44,14 @@
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var sjcl = require('./utils').sjcl;
var Amount = require('./amount').Amount;
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 hashprefixes = require('./hashprefixes');
var config = require('./config');
// A class to implement transactions.
@@ -86,7 +82,7 @@ function Transaction(remote) {
// of all submitted transactionIDs (which can change due to load_factor
// effecting the Fee amount). This should be populated with a transactionID
// any time it goes on the network
this.submittedTxnIDs = []
this.submittedTxnIDs = [ ]
};
util.inherits(Transaction, EventEmitter);
@@ -177,12 +173,43 @@ Transaction.prototype.setState = function(state) {
};
/**
* TODO
* Actually do this right
* 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
*
* @return {Number} Number of fee units for this transaction.
*/
Transaction.prototype.getFee = function() {
return Transaction.fees['default'].to_json();
Transaction.prototype.getFee =
Transaction.prototype.feeUnits = function() {
return Transaction.fee_units['default'];
};
/**
* Get the server whose fee is currently the lowest
*/
Transaction.prototype._getServer = function() {
var self = this;
var servers = this.remote._servers;
var fee = Infinity;
var result;
for (var i=0; i<servers.length; i++) {
var server = servers[i];
if (!server._connected) continue;
var n = server.computeFee(this);
if (n < fee) {
result = server;
fee = n;
}
}
return result;
};
/**
@@ -192,10 +219,12 @@ Transaction.prototype.getFee = function() {
* SigningPubKey, which can be determined by the library based on network
* information and other fields.
*/
Transaction.prototype.complete = function() {
if (this.remote && typeof this.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();
this._server = this._getServer();
this.tx_json.Fee = this._server.computeFee(this);
}
}
@@ -218,22 +247,19 @@ Transaction.prototype.signingHash = function() {
Transaction.prototype.addSubmittedTxnID = function(hash) {
if (this.submittedTxnIDs.indexOf(hash) === -1) {
this.submittedTxnIDs.push(hash);
this.submittedTxnIDs.unshift(hash);
}
};
Transaction.prototype.findResultInCache = function(cache) {
var cached;
var result;
for (var i = this.submittedTxnIDs.length - 1; i >= 0; i--) {
for (var i=0; i<this.submittedTxnIDs.length; i++) {
var hash = this.submittedTxnIDs[i];
cached = cache[hash];
if (cached != null) {
break;
};
};
if (result = cache[hash]) break;
}
return cached;
return result;
};
Transaction.prototype.hash = function(prefix, as_uint256) {
@@ -262,7 +288,7 @@ Transaction.prototype.sign = function() {
// If the hash is the same, we can re-use the previous signature
if (prev_sig && hash === this._previous_signing_hash) {
this.tx_json.TxnSignature = prev_sig;
return;
return this;
}
var key = seed.get_key(this.tx_json.Account);
@@ -271,6 +297,8 @@ Transaction.prototype.sign = function() {
this.tx_json.TxnSignature = hex;
this._previous_signing_hash = hash;
return this;
};
//
@@ -345,6 +373,7 @@ Transaction.prototype.paths = function(paths) {
// If the secret is in the config object, it does not need to be provided.
Transaction.prototype.secret = function(secret) {
this._secret = secret;
return this;
};
Transaction.prototype.sendMax = function(send_max) {
@@ -568,7 +597,9 @@ Transaction.prototype.payment = function(src, dst, amount) {
amount = options.amount;
dst = options.destination || options.to;
src = options.source || options.from;
if (options.invoiceID) this.invoiceID(options.invoiceID);
if (options.invoiceID) {
this.invoiceID(options.invoiceID);
}
}
if (!UInt160.is_valid(src)) {
@@ -609,7 +640,6 @@ Transaction.prototype.rippleLineSet = function(src, limit, quality_in, quality_o
this.tx_json.TransactionType = 'TrustSet';
this.tx_json.Account = UInt160.json_rewrite(src);
// Allow limit of 0 through.
if (limit !== void(0)) {
this.tx_json.LimitAmount = Amount.json_rewrite(limit);
}
@@ -650,21 +680,6 @@ Transaction.prototype.walletAdd = function(src, amount, authorized_key, public_k
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
*
* @return {Number} Number of fee units for this transaction.
*/
Transaction.prototype.feeUnits = function() {
return Transaction.fee_units['default'];
};
// Submit a transaction to the network.
Transaction.prototype.submit = function(callback) {
var self = this;

View File

@@ -53,14 +53,19 @@ function TransactionManager(account) {
this._account.on('transaction-outbound', transactionReceived);
function adjustFees() {
function adjustFees(loadData, server) {
// ND: note, that `Fee` is a component of a transactionID
self._pending.forEach(function(pending) {
if (self._remote.local_fee && pending.tx_json.Fee) {
var shouldAdjust = pending._server === server
&& self._remote.local_fee && pending.tx_json.Fee;
if (shouldAdjust) {
var oldFee = pending.tx_json.Fee;
var newFee = self._remote.feeTx(pending.fee_units()).to_json();
var newFee = server.feeTx(pending.fee_units()).to_json();
pending.tx_json.Fee = newFee;
pending.emit('fee_adjusted', oldFee, newFee);
self._remote._trace('transactionmanager: adjusting_fees:', pending.tx_json, oldFee, newFee);
}
});
@@ -256,6 +261,9 @@ TransactionManager.prototype._resubmit = function(ledgers, pending) {
this._waitLedgers(ledgers, resubmitTransactions);
};
TransactionManager.prototype._selectServer = function() {
};
TransactionManager.prototype._waitLedgers = function(ledgers, callback) {
if (ledgers < 1) {
return callback();
@@ -297,9 +305,7 @@ TransactionManager.prototype._request = function(tx) {
// ND: We could consider sharing the work with tx_blob when doing
// local_signing
tx.addSubmittedTxnID(tx.hash());
// tx._hash = tx.hash();
remote._trace('transactionmanager: submit:', tx.tx_json);
@@ -321,7 +327,7 @@ TransactionManager.prototype._request = function(tx) {
};
function transactionRetry(message) {
if (self._is_no_op(tx)) {
if (TransactionManager._isNoOp(tx)) {
self._resubmit(1, tx);
} else {
self._fillSequence(tx, function() {
@@ -338,7 +344,7 @@ TransactionManager.prototype._request = function(tx) {
// Finalized (e.g. aborted) transactions must stop all activity
if (tx.finalized) return;
if (self._is_too_busy(error)) {
if (TransactionManager._isTooBusy(error)) {
self._resubmit(1, tx);
} else {
self._nextSequence--;
@@ -417,23 +423,27 @@ TransactionManager.prototype._request = function(tx) {
return submitRequest;
};
TransactionManager.prototype._is_no_op = function(transaction) {
return transaction.tx_json.TransactionType === 'AccountSet'
&& transaction.tx_json.Flags === 0;
TransactionManager._isNoOp = function(transaction) {
return (typeof transaction === 'object')
&& (typeof transaction.tx_json === 'object')
&& (transaction.tx_json.TransactionType === 'AccountSet')
&& (transaction.tx_json.Flags === 0);
};
TransactionManager.prototype._is_remote_error = function(error) {
return error && typeof error === 'object'
&& error.error === 'remoteError'
&& typeof error.remote === 'object'
TransactionManager._isRemoteError = function(error) {
return (typeof error === 'object')
&& (error.error === 'remoteError')
&& (typeof error.remote === 'object')
};
TransactionManager.prototype._is_not_found = function(error) {
return this._is_remote_error(error) && /^(txnNotFound|transactionNotFound)$/.test(error.remote.error);
TransactionManager._isNotFound = function(error) {
return TransactionManager._isRemoteError(error)
&& /^(txnNotFound|transactionNotFound)$/.test(error.remote.error);
};
TransactionManager.prototype._is_too_busy = function(error) {
return this._is_remote_error(error) && error.remote.error === 'tooBusy';
TransactionManager._isTooBusy = function(error) {
return TransactionManager._isRemoteError(error)
&& error.remote.error === 'tooBusy';
};
/**