From c5bd4239a46b13ce833a143e917484a5e7863944 Mon Sep 17 00:00:00 2001 From: wltsmrz Date: Sun, 7 Dec 2014 23:04:31 -0800 Subject: [PATCH] Cleanup - Deprecate 'save' event - Add TransactionQueue.getMinLedger(), use this as ledger_index_min in account_tx request on reconnect - tx.sign() no longer accepts a callback - Add various setters and jsdoc to transaction.js - Normalize setters, e.g. sourceTag() and destinationTag() - Minor optimization in call to tx.hash() in TransactionManager prior to submit; allow `serialized` argument to tx.hash() such that the transaction is not serialized twice --- src/js/ripple/transaction.js | 729 +++++++++++++++++----------- src/js/ripple/transactionmanager.js | 721 +++++++++++++-------------- src/js/ripple/transactionqueue.js | 63 ++- test/transaction-test.js | 193 ++++---- 4 files changed, 979 insertions(+), 727 deletions(-) diff --git a/src/js/ripple/transaction.js b/src/js/ripple/transaction.js index 5b2381ba..da834e84 100644 --- a/src/js/ripple/transaction.js +++ b/src/js/ripple/transaction.js @@ -1,49 +1,6 @@ -// Transactions -// -// Construction: -// remote.transaction() // Build a transaction object. -// .offer_create(...) // Set major parameters. -// .set_flags() // Set optional parameters. -// .on() // Register for events. -// .submit(); // Send to network. -// -// Events: -// 'success' : Transaction submitted without error. -// 'error' : Error submitting transaction. -// 'proposed' : Advisory proposed status transaction. -// - A client should expect 0 to multiple results. -// - Might not get back. The remote might just forward the transaction. -// - A success could be reverted in final. -// - local error: other remotes might like it. -// - malformed error: local server thought it was malformed. -// - The client should only trust this when talking to a trusted server. -// 'final' : Final status of transaction. -// - Only expect a final from dishonest servers after a tesSUCCESS or ter*. -// 'lost' : Gave up looking for on ledger_closed. -// 'pending' : Transaction was not found on ledger_closed. -// 'state' : Follow the state of a transaction. -// 'client_submitted' - Sent to remote -// |- 'remoteError' - Remote rejected transaction. -// \- 'client_proposed' - Remote provisionally accepted transaction. -// |- 'client_missing' - Transaction has not appeared in ledger as expected. -// | |\- 'client_lost' - No longer monitoring missing transaction. -// |/ -// |- 'tesSUCCESS' - Transaction in ledger as expected. -// |- 'ter...' - Transaction failed. -// \- 'tec...' - Transaction claimed fee only. -// -// Notes: -// - All transactions including those with local and malformed errors may be -// forwarded anyway. -// - A malicous server can: -// - give any proposed result. -// - it may declare something correct as incorrect or something incorrect as correct. -// - it may not communicate with the rest of the network. -// - may or may not forward. -// - -var EventEmitter = require('events').EventEmitter; var util = require('util'); +var assert = require('assert'); +var EventEmitter = require('events').EventEmitter; var utils = require('./utils'); var sjcl = require('./utils').sjcl; var Amount = require('./amount').Amount; @@ -55,55 +12,67 @@ var RippleError = require('./rippleerror').RippleError; var hashprefixes = require('./hashprefixes'); var config = require('./config'); +/** + * @constructor Transaction + * + * Notes: + * All transactions including those with local and malformed errors may be + * forwarded anyway. + * + * A malicous server can: + * - may or may not forward + * - give any result + * + it may declare something correct as incorrect or something incorrect + * as correct + * + it may not communicate with the rest of the network + */ + function Transaction(remote) { EventEmitter.call(this); var self = this; - - var remote = remote || void(0); + var remoteExists = (typeof remote === 'object'); this.remote = remote; - - // Transaction data this.tx_json = { Flags: 0 }; - this._secret = void(0); this._build_path = false; - this._maxFee = (typeof remote === 'object') ? this.remote.max_fee : void(0); - + this._maxFee = remoteExists ? this.remote.max_fee : void(0); this.state = 'unsubmitted'; this.finalized = false; this.previousSigningHash = void(0); - - // Index at which transaction was submitted this.submitIndex = void(0); - - // Canonical signing setting defaults to the Remote's configuration - this.canonical = (typeof remote === 'object') ? Boolean(remote.canonical_signing) : true; - - // We aren't clever enough to eschew preventative measures so we keep an array - // 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.canonical = remoteExists ? this.remote.canonical_signing : true; this.submittedIDs = [ ]; + this.attempts = 0; + this.submissions = 0; + this.responses = 0; this.once('success', function(message) { - self.finalize(message); + // Transaction definitively succeeded self.setState('validated'); - self.emit('cleanup', message); + self.finalize(message); + if (self._successHandler) { + self._successHandler(message); + } }); this.once('error', function(message) { - self.finalize(message); + // Transaction definitively failed self.setState('failed'); - self.emit('cleanup', message); + self.finalize(message); + if (self._errorHandler) { + self._errorHandler(message); + } }); this.once('submitted', function() { + // Transaction was submitted to the network self.setState('submitted'); }); this.once('proposed', function() { + // Transaction was submitted successfully to the network self.setState('pending'); }); }; @@ -170,6 +139,8 @@ Transaction.set_clear_flags = { Transaction.MEMO_TYPES = { }; +Transaction.ASCII_REGEX = /^[\x00-\x7F]*$/; + Transaction.formats = require('./binformat').tx; Transaction.prototype.consts = { @@ -181,15 +152,6 @@ Transaction.prototype.consts = { tecCLAIMED: 100 }; -Transaction.from_json = function(j) { - return (new Transaction()).parseJson(j); -}; - -Transaction.prototype.parseJson = function(v) { - this.tx_json = v; - return this; -}; - Transaction.prototype.isTelLocal = function(ter) { return ter >= this.consts.telLOCAL_ERROR && ter < this.consts.temMALFORMED; }; @@ -218,14 +180,35 @@ Transaction.prototype.isRejected = function(ter) { return this.isTelLocal(ter) || this.isTemMalformed(ter) || this.isTefFailure(ter); }; +Transaction.from_json = function(j) { + return (new Transaction()).parseJson(j); +}; + +Transaction.prototype.parseJson = function(v) { + this.tx_json = v; + return this; +}; + +/** + * Set state on the condition that the state is different + * + * @param {String} state + */ + Transaction.prototype.setState = function(state) { if (this.state !== state) { this.state = state; this.emit('state', state); - this.emit('save'); } }; +/** + * Finalize transaction. This will prevent future activity + * + * @param {Object} message + * @api private + */ + Transaction.prototype.finalize = function(message) { this.finalized = true; @@ -237,11 +220,73 @@ Transaction.prototype.finalize = function(message) { this.result.tx_json = this.tx_json; } + this.emit('cleanup'); + this.emit('final', message); + + if (this.remote && this.remote.trace) { + log.info('transaction finalized:', + this.tx_json, this.getManager()._pending.getLength()); + } + return this; }; +/** + * Get transaction Account + * + * @return {Account} + */ + +Transaction.prototype.getAccount = function() { + return this.tx_json.Account; +}; + +/** + * Get TransactionType + * + * @return {String} + */ + +Transaction.prototype.getTransactionType = function() { + return this.tx_json.TransactionType; +}; + +/** + * Get transaction TransactionManager + * + * @param [String] account + * @return {TransactionManager] + */ + +Transaction.prototype.getManager = function(account) { + if (!this.remote) { + return void(0); + } + + if (!account) { + account = this.tx_json.Account; + } + + return this.remote.account(account)._transactionManager; +}; + +/** + * Get transaction secret + * + * @param [String] account + */ + +Transaction.prototype.getSecret = Transaction.prototype._accountSecret = function(account) { - return this.remote ? this.remote.secrets[account] : void(0); + if (!this.remote) { + return void(0); + } + + if (!account) { + account = this.tx_json.Account; + } + + return this.remote.secrets[account]; }; /** @@ -263,6 +308,8 @@ Transaction.prototype.feeUnits = function() { /** * Compute median server fee + * + * @return {String} median fee */ Transaction.prototype._computeFee = function() { @@ -310,18 +357,22 @@ Transaction.prototype._computeFee = function() { * This function seeks to fill out certain fields, such as Fee and * SigningPubKey, which can be determined by the library based on network * information and other fields. + * + * @return {Boolean|Transaction} If succeeded, return transaction. Otherwise + * return `false` */ Transaction.prototype.complete = function() { if (this.remote) { if (!this.remote.trusted && !this.remote.local_signing) { - this.emit('error', new RippleError('tejServerUntrusted', 'Attempt to give secret to untrusted server')); + this.emit('error', new RippleError( + 'tejServerUntrusted', 'Attempt to give secret to untrusted server')); return false; } } // Try to auto-fill the secret - if (!this._secret && !(this._secret = this._accountSecret(this.tx_json.Account))) { + if (!this._secret && !(this._secret = this.getSecret())) { this.emit('error', new RippleError('tejSecretUnknown', 'Missing secret')); return false; } @@ -332,7 +383,8 @@ Transaction.prototype.complete = function() { var key = seed.get_key(this.tx_json.Account); this.tx_json.SigningPubKey = key.to_hex_pub(); } catch(e) { - this.emit('error', new RippleError('tejSecretInvalid', 'Invalid secret')); + this.emit('error', new RippleError( + 'tejSecretInvalid', 'Invalid secret')); return false; } } @@ -343,13 +395,14 @@ Transaction.prototype.complete = function() { if (this.remote.local_fee || !this.remote.trusted) { if (!(this.tx_json.Fee = this._computeFee())) { this.emit('error', new RippleError('tejUnconnected')); - return; + return false; } } } if (Number(this.tx_json.Fee) > this._maxFee) { - this.emit('error', new RippleError('tejMaxFeeExceeded', 'Max fee exceeded')); + this.emit('error', new RippleError( + 'tejMaxFeeExceeded', 'Max fee exceeded')); return false; } @@ -373,7 +426,7 @@ Transaction.prototype.signingHash = function() { return this.hash(config.testnet ? 'HASH_TX_SIGN_TESTNET' : 'HASH_TX_SIGN'); }; -Transaction.prototype.hash = function(prefix, as_uint256) { +Transaction.prototype.hash = function(prefix, asUINT256, serialized) { if (typeof prefix === 'string') { if (typeof hashprefixes[prefix] === 'undefined') { throw new Error('Unknown hashing prefix requested.'); @@ -383,13 +436,12 @@ Transaction.prototype.hash = function(prefix, as_uint256) { prefix = hashprefixes.HASH_TX_ID; } - var hash = SerializedObject.from_json(this.tx_json).hash(prefix); + var hash = (serialized || this.serialize()).hash(prefix); - return as_uint256 ? hash : hash.to_hex(); + return asUINT256 ? hash : hash.to_hex(); }; -Transaction.prototype.sign = function(callback) { - var callback = typeof callback === 'function' ? callback : function(){}; +Transaction.prototype.sign = function() { var seed = Seed.from_json(this._secret); var prev_sig = this.tx_json.TxnSignature; @@ -400,7 +452,6 @@ Transaction.prototype.sign = function(callback) { // If the hash is the same, we can re-use the previous signature if (prev_sig && hash === this.previousSigningHash) { this.tx_json.TxnSignature = prev_sig; - callback(); return this; } @@ -411,25 +462,30 @@ Transaction.prototype.sign = function(callback) { this.tx_json.TxnSignature = hex; this.previousSigningHash = hash; - callback(); - return this; }; /** - * Add a ID to list of submitted IDs for this transaction + * Add an ID to cached list of submitted IDs + * + * @param {String} transaction id + * @api private */ -Transaction.prototype.addId = function(hash) { - if (this.submittedIDs.indexOf(hash) === -1) { - this.submittedIDs.unshift(hash); - this.emit('signed', hash); - this.emit('save'); +Transaction.prototype.addId = function(id) { + if (this.submittedIDs.indexOf(id) === -1) { + this.submittedIDs.unshift(id); } }; /** - * Find ID within list of submitted IDs for this transaction + * Find ID within cached received (validated) IDs. If this transaction has + * an ID that is within the cache, it has been seen validated, so return the + * received message + * + * @param {Object} cache + * @return {Object} message + * @api private */ Transaction.prototype.findId = function(cache) { @@ -445,27 +501,67 @@ Transaction.prototype.findId = function(cache) { return result; }; -// -// Set options for Transactions -// +/** + * Set build_path to have server blindly construct a path for Payment + * + * "blindly" because the sender has no idea of the actual cost must be less + * than send max. + * + * XXX Abort if not a Payment + * + * @param {Boolean} build path + */ -// --> build: true, to have server blindly construct a path. -// -// "blindly" because the sender has no idea of the actual cost except that is must be less than send max. +Transaction.prototype.setBuildPath = Transaction.prototype.buildPath = function(build) { this._build_path = build; return this; }; -// tag should be undefined or a 32 bit integer. -// YYY Add range checking for tag. +/** + * Set SourceTag + * + * tag should be undefined or a 32 bit integer. + * YYY Add range checking for tag + * + * @param {Number} source tag + */ + +Transaction.prototype.sourceTag = function(tag) { + if (typeof tag === 'number' && isFinite(tag)) { + this.tx_json.SourceTag = tag; + } + return this; +}; + +/** + * Set DestinationTag for Payment transaction + * + * tag should be undefined or a 32 bit integer. + * YYY Add range checking for tag. + * + * XXX Abort if not a Payment + * + * @param {Number} destination tag + */ + +Transaction.prototype.setDestinationTag = Transaction.prototype.destinationTag = function(tag) { - if (tag !== void(0)) { + if (typeof tag === 'number' && isFinite(tag)) { this.tx_json.DestinationTag = tag; } return this; }; +/** + * Set InvoiceID for Payment transaction + * + * XXX Abort if not a Payment + * + * @param {String} id + */ + +Transaction.prototype.setInvoiceID = Transaction.prototype.invoiceID = function(id) { if (typeof id === 'string') { while (id.length < 64) { @@ -476,6 +572,19 @@ Transaction.prototype.invoiceID = function(id) { return this; }; +/** + * Set client ID. This is an identifier specified by the user of the API to + * identify a transaction in the event of a disconnect. It is not currently + * persisted in the transaction itself, but used offline for identification. + * In applications that require high reliability, client-specified ID should + * be persisted such that one could map it to submitted transactions. Use + * .summary() for a consistent transaction summary output for persisitng. In + * the future, this ID may be stored in the transaction itself (in the ledger) + * + * @param {String} id + */ + +Transaction.prototype.setClientID = Transaction.prototype.clientID = function(id) { if (typeof id === 'string') { this._clientID = id; @@ -483,34 +592,55 @@ Transaction.prototype.clientID = function(id) { return this; }; +/** + * Set LastLedgerSequence as the absolute last ledger sequence the transaction + * is valid for. LastLedgerSequence is set automatically if not set using this + * method + * + * @param {Number} ledger index + */ + +Transaction.prototype.setLastLedger = Transaction.prototype.lastLedger = function(sequence) { if (typeof sequence === 'number' && isFinite(sequence)) { this._setLastLedger = true; this.tx_json.LastLedgerSequence = sequence; } + return this; }; -/* - * Set the transaction's proposed fee. No op when fee parameter - * is not 0 or a positive number +/** + * Set max fee. Submission will abort if this is exceeded. Specified fee must + * be >= 0 * * @param {Number} fee The proposed fee - * - * @returns {Transaction} calling instance for chaining */ + Transaction.prototype.maxFee = function(fee) { if (typeof fee === 'number' && fee >= 0) { this._setMaxFee = true; this._maxFee = fee; } - return this; }; -Transaction._pathRewrite = function(path) { +/** + * Filter invalid properties from path objects in a path array + * + * Valid properties are: + * - account + * - currency + * - issuer + * - type_hex + * + * @param {Array} path + * @return {Array} filtered path + */ + +Transaction._rewritePath = function(path) { if (!Array.isArray(path)) { - return; + throw new Error('Path must be an array'); } var newPath = path.map(function(node) { @@ -538,32 +668,63 @@ Transaction._pathRewrite = function(path) { return newPath; }; +/** + * Add a path for Payment transaction + * + * XXX Abort if not a Payment + * + * @param {Array} path + */ + +Transaction.prototype.addPath = Transaction.prototype.pathAdd = function(path) { if (Array.isArray(path)) { - this.tx_json.Paths = this.tx_json.Paths || []; - this.tx_json.Paths.push(Transaction._pathRewrite(path)); + this.tx_json.Paths = this.tx_json.Paths || [ ]; + this.tx_json.Paths.push(Transaction._rewritePath(path)); } return this; }; -// --> paths: undefined or array of path -// // A path is an array of objects containing some combination of: account, currency, issuer +/** + * Set paths for Payment transaction + * + * XXX Abort if not a Payment + * + * @param {Array} paths + */ +Transaction.prototype.setPaths = Transaction.prototype.paths = function(paths) { if (Array.isArray(paths)) { for (var i=0, l=paths.length; i rate: In billionths. Transaction.prototype.transferRate = function(rate) { - this.tx_json.TransferRate = Number(rate); - - if (this.tx_json.TransferRate < 1e9) { - throw new Error('invalidTransferRate'); + if (typeof rate === 'number' && rate >= 1e9) { + this.tx_json.TransferRate = rate; } - return this; }; -// Add flags to a transaction. -// --> flags: undefined, _flag_, or [ _flags_ ] +/** + * Set Flags. You may specify flags as a number, as the string name of the + * flag, or as an array of strings. + * + * setFlags(Transaction.flags.AccountSet.RequireDestTag) + * setFlags('RequireDestTag') + * setFlags('RequireDestTag', 'RequireAuth') + * setFlags([ 'RequireDestTag', 'RequireAuth' ]) + * + * @param {Number|String|Array} flags + */ + Transaction.prototype.setFlags = function(flags) { if (flags === void(0)) { return this; @@ -603,8 +767,8 @@ Transaction.prototype.setFlags = function(flags) { return this; } - var flag_set = Array.isArray(flags) ? flags : Array.prototype.slice.call(arguments); - var transaction_flags = Transaction.flags[this.tx_json.TransactionType] || { }; + var transaction_flags = Transaction.flags[this.getTransactionType()] || { }; + var flag_set = Array.isArray(flags) ? flags : [].slice.call(arguments); for (var i=0, l=flag_set.length; i expiration : if not undefined, Date or Number -// --> cancel_sequence : if not undefined, Sequence +/** + * Construct an 'OfferCreate transaction + * + * @param {String} account + * @param {Amount} taker pays amount + * @param {Amount} taker gets amount + * @param [Number|Date] + * @param [Number] sequence of an existing offer to replace + */ + Transaction.prototype.offerCreate = function(src, taker_pays, taker_gets, expiration, cancel_sequence) { if (typeof src === 'object') { var options = src; - cancel_sequence = options.cancel_sequence; - expiration = options.expiration; - taker_gets = options.taker_gets || options.sell; - taker_pays = options.taker_pays || options.buy; - src = options.source || options.from || options.account; + cancel_sequence = options.cancel_sequence || options.sequence; + expiration = options.expiration; + taker_gets = options.taker_gets || options.sell; + taker_pays = options.taker_pays || options.buy; + src = options.source || options.from || options.account; } if (!UInt160.is_valid(src)) { @@ -773,9 +956,9 @@ Transaction.prototype.offerCreate = function(src, taker_pays, taker_gets, expira } 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.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 (expiration) { this.tx_json.Expiration = utils.time.toRipple(expiration); @@ -789,12 +972,16 @@ Transaction.prototype.offerCreate = function(src, taker_pays, taker_gets, expira }; /** - * Construct a 'SetRegularKey' transaction. - * If the RegularKey is set, the private key that corresponds to - * it can be used to sign transactions instead of the master key + * Construct a 'SetRegularKey' transaction * - * The RegularKey must be a valid Ripple Address, or a Hash160 of - * the public key corresponding to the new private signing key. + * If the RegularKey is set, the private key that corresponds to it can be + * used to sign transactions instead of the master key + * + * The RegularKey must be a valid Ripple Address, or a Hash160 of the public + * key corresponding to the new private signing key. + * + * @param {String} account + * @param {String} regular key */ Transaction.prototype.setRegularKey = function(src, regular_key) { @@ -809,7 +996,7 @@ Transaction.prototype.setRegularKey = function(src, regular_key) { } if (!UInt160.is_valid(regular_key)) { - throw new Error('RegularKey must be a valid Ripple Address (a Hash160 of the public key)'); + throw new Error('RegularKey must be a valid Ripple Address'); } this.tx_json.TransactionType = 'SetRegularKey'; @@ -819,29 +1006,29 @@ Transaction.prototype.setRegularKey = function(src, regular_key) { return this; }; -// Construct a 'payment' transaction. -// -// When a transaction is submitted: -// - If the connection is reliable and the server is not merely forwarding and is not malicious, -// --> src : UInt160 or String -// --> dst : UInt160 or String -// --> deliver_amount : Amount or String. -// -// Options: -// .paths() -// .build_path() -// .destination_tag() -// .path_add() -// .secret() -// .send_max() -// .set_flags() -// .source_tag() +/** + * Construct a 'Payment' transaction + * + * Relevant setters: + * - setPaths() + * - setBuildPath() + * - addPath() + * - setSourceTag() + * - setDestinationTag() + * - setSendMax() + * - setFlags() + * + * @param {String} source account + * @param {StrinG} destination account + * @param {Amount} payment amount + */ + Transaction.prototype.payment = function(src, dst, amount) { if (typeof src === 'object') { var options = src; amount = options.amount; - dst = options.destination || options.to; - src = options.source || options.from || options.account; + dst = options.destination || options.to; + src = options.source || options.from || options.account; } if (!UInt160.is_valid(src)) { @@ -853,21 +1040,30 @@ Transaction.prototype.payment = function(src, dst, amount) { } this.tx_json.TransactionType = 'Payment'; - this.tx_json.Account = UInt160.json_rewrite(src); - this.tx_json.Amount = Amount.json_rewrite(amount); - this.tx_json.Destination = UInt160.json_rewrite(dst); + this.tx_json.Account = UInt160.json_rewrite(src); + this.tx_json.Amount = Amount.json_rewrite(amount); + this.tx_json.Destination = UInt160.json_rewrite(dst); return this; }; +/** + * Construct a 'TrustSet' transaction + * + * @param {String} account + * @param {Amount} limit + * @param {Number} quality in + * @param {Number} quality out + */ + Transaction.prototype.trustSet = Transaction.prototype.rippleLineSet = function(src, limit, quality_in, quality_out) { if (typeof src === 'object') { var options = src; quality_out = options.quality_out; - quality_in = options.quality_in; - limit = options.limit; - src = options.source || options.from || options.account; + quality_in = options.quality_in; + limit = options.limit; + src = options.source || options.from || options.account; } if (!UInt160.is_valid(src)) { @@ -875,7 +1071,7 @@ Transaction.prototype.rippleLineSet = function(src, limit, quality_in, quality_o } this.tx_json.TransactionType = 'TrustSet'; - this.tx_json.Account = UInt160.json_rewrite(src); + this.tx_json.Account = UInt160.json_rewrite(src); if (limit !== void(0)) { this.tx_json.LimitAmount = Amount.json_rewrite(limit); @@ -894,78 +1090,71 @@ Transaction.prototype.rippleLineSet = function(src, limit, quality_in, quality_o return this; }; -// Submit a transaction to the network. +/** + * Submit transaction to the network + * + * @param [Function] callback + */ + Transaction.prototype.submit = function(callback) { var self = this; this.callback = (typeof callback === 'function') ? callback : function(){}; - function transactionError(error, message) { + this._errorHandler = function transactionError(error, message) { if (!(error instanceof RippleError)) { error = new RippleError(error, message); } self.callback(error); }; - this._errorHandler = transactionError; - - function transactionSuccess(message) { + this._successHandler = function transactionSuccess(message) { self.callback(null, message); }; - this._successHandler = transactionSuccess; - - this.on('error', function(){}); - - var account = this.tx_json.Account; - if (!this.remote) { - return this.emit('error', new Error('No remote found')); + this.emit('error', new Error('No remote found')); + return; } - if (!UInt160.is_valid(account)) { - return this.emit('error', new RippleError('tejInvalidAccount', 'Account is missing or invalid')); - } - - // YYY Might check paths for invalid accounts. - this.remote.account(account).submit(this); + this.getManager().submit(this); return this; }; -Transaction.prototype.abort = function(callback) { - var callback = (typeof callback === 'function') ? callback : function(){}; +Transaction.prototype.abort = function() { if (!this.finalized) { - this.once('final', callback); - this.emit('abort'); + this.emit('error', new RippleError('tejAbort', 'Transaction aborted')); } }; -Transaction.prototype.summary = function() { - return Transaction.summary.call(this); -}; +/** + * Return summary object containing important information for persistence + * + * @return {Object} transaction summary + */ Transaction.summary = function() { var result = { - tx_json: this.tx_json, - clientID: this._clientID, - submittedIDs: this.submittedIDs, - submissionAttempts: this.attempts, - submitIndex: this.submitIndex, - initialSubmitIndex: this.initialSubmitIndex, - lastLedgerSequence: this.lastLedgerSequence, - state: this.state, - server: this._server ? this._server._opts.url : void(0), - finalized: this.finalized + tx_json: this.tx_json, + clientID: this._clientID, + submittedIDs: this.submittedIDs, + submissionAttempts: this.attempts, + submitIndex: this.submitIndex, + initialSubmitIndex: this.initialSubmitIndex, + lastLedgerSequence: this.lastLedgerSequence, + state: this.state, + server: this._server ? this._server._opts.url : void(0), + finalized: this.finalized }; if (this.result) { result.result = { - engine_result : this.result.engine_result, + engine_result: this.result.engine_result, engine_result_message: this.result.engine_result_message, - ledger_hash : this.result.ledger_hash, - ledger_index : this.result.ledger_index, - transaction_hash : this.result.tx_json.hash + ledger_hash: this.result.ledger_hash, + ledger_index: this.result.ledger_index, + transaction_hash: this.result.tx_json.hash }; } diff --git a/src/js/ripple/transactionmanager.js b/src/js/ripple/transactionmanager.js index cc3e6c17..68073719 100644 --- a/src/js/ripple/transactionmanager.js +++ b/src/js/ripple/transactionmanager.js @@ -1,4 +1,6 @@ var util = require('util'); +var assert = require('assert'); +var async = require('async'); var EventEmitter = require('events').EventEmitter; var Transaction = require('./transaction').Transaction; var RippleError = require('./rippleerror').RippleError; @@ -15,193 +17,207 @@ function TransactionManager(account) { var self = this; - this._account = account; - this._accountID = account._account_id; - this._remote = account._remote; - this._nextSequence = void(0); - this._maxFee = this._remote.max_fee; - this._maxAttempts = this._remote.max_attempts; + this._account = account; + this._accountID = account._account_id; + this._remote = account._remote; + this._nextSequence = void(0); + this._maxFee = this._remote.max_fee; + this._maxAttempts = this._remote.max_attempts; this._submissionTimeout = this._remote._submission_timeout; - this._pending = new PendingQueue(); + this._pending = new PendingQueue(); - // Query remote server for next account sequence number - this._loadSequence(); + this._account.on('transaction-outbound', function(res) { + self._transactionReceived(res); + }); - function transactionReceived(res) { - var transaction = TransactionManager.normalizeTransaction(res); - var sequence = transaction.tx_json.Sequence; - var hash = transaction.tx_json.hash; - - if (!transaction.validated) { - return; - } - - self._pending.addReceivedSequence(sequence); - - // ND: we need to check against all submissions IDs - var submission = self._pending.getSubmission(hash); - - if (self._remote.trace) { - log.info('transaction received:', transaction.tx_json); - } - - if (submission instanceof Transaction) { - - // ND: A `success` handler will `finalize` this later - switch (transaction.engine_result) { - case 'tesSUCCESS': - submission.emit('success', transaction); - break; - default: - submission.emit('error', transaction); - } - - } else { - self._pending.addReceivedId(hash, transaction); - } - }; - - this._account.on('transaction-outbound', transactionReceived); - - this._remote.on('load_changed', this._adjustFees.bind(this)); + this._remote.on('load_changed', function(load) { + self._adjustFees(load); + }); function updatePendingStatus(ledger) { - self._pending.forEach(function(pending) { - switch (ledger.ledger_index - pending.submitIndex) { - case 8: - pending.emit('lost', ledger); - break; - case 4: - pending.emit('missing', ledger); - break; - } - }); + self._updatePendingStatus(ledger); }; this._remote.on('ledger_closed', updatePendingStatus); - function remoteReconnected(callback) { - var callback = (typeof callback === 'function') ? callback : function(){}; - - if (!self._pending.length) { - return callback(); - } - - //Load account transaction history - var options = { - account: self._accountID, - ledger_index_min: -1, - ledger_index_max: -1, - binary: true, - parseBinary: true, - limit: 100, - filter: 'outbound' - }; - - function accountTx(err, transactions) { - if (!err && Array.isArray(transactions.transactions)) { - transactions.transactions.forEach(transactionReceived); - } - - self._remote.on('ledger_closed', updatePendingStatus); - - //Load next transaction sequence - self._loadSequence(self._resubmit.bind(self)); - - callback(); - }; - - self._remote.requestAccountTx(options, accountTx); - - self.emit('reconnect'); - }; - - function remoteDisconnected() { - self._remote.once('connect', remoteReconnected); + this._remote.on('disconnect', function() { self._remote.removeListener('ledger_closed', updatePendingStatus); - }; + self._remote.once('connect', function() { + self._remote.on('ledger_closed', updatePendingStatus); + self._handleReconnect(); + }); + }); - this._remote.on('disconnect', remoteDisconnected); - - function saveTransaction(transaction) { - self._remote.storage.saveTransaction(transaction.summary()); - }; - - if (this._remote.storage) { - this.on('save', saveTransaction); - } + // Query server for next account transaction sequence + this._loadSequence(); }; util.inherits(TransactionManager, EventEmitter); -//Normalize transactions received from account -//transaction stream and account_tx +/** + * Normalize transactions received from account transaction stream and + * account_tx + * + * @param {Transaction} + * @return {Transaction} normalized + * @api private + */ + TransactionManager.normalizeTransaction = function(tx) { var transaction = { }; + var keys = Object.keys(tx); - Object.keys(tx).forEach(function(key) { - transaction[key] = tx[key]; - }); - - if (!tx.engine_result) { - // account_tx - transaction = { - engine_result: tx.meta.TransactionResult, - tx_json: tx.tx, - hash: tx.tx.hash, - ledger_index: tx.tx.ledger_index, - meta: tx.meta, - type: 'transaction', - validated: true - }; - - transaction.result = transaction.engine_result; - transaction.result_message = transaction.engine_result_message; + for (var i=0; i self._maxFee) { - return maxFeeExceeded(); + // Max transaction fee exceeded, abort submission + return maxFeeExceeded(transaction); } - pending.tx_json.Fee = newFee; - pending.emit('fee_adjusted', oldFee, newFee); + transaction.tx_json.Fee = newFee; + transaction.emit('fee_adjusted', oldFee, newFee); if (self._remote.trace) { - log.info('fee adjusted:', pending.tx_json, oldFee, newFee); + log.info('fee adjusted:', transaction.tx_json, oldFee, newFee); + } + }); +}; + +/** + * Get pending transactions + * + * @return {Array} pending transactions + */ + +TransactionManager.prototype.getPending = function() { + return this._pending; +}; + +/** + * Legacy code. Update transaction status after excessive ledgers pass. One of + * either "missing" or "lost" + * + * @param {Object} ledger data + * @api private + */ + +TransactionManager.prototype._updatePendingStatus = function(ledger) { + assert.strictEqual(typeof ledger, 'object'); + assert.strictEqual(typeof ledger.ledger_index, 'number'); + + this._pending.forEach(function(transaction) { + switch (ledger.ledger_index - transaction.submitIndex) { + case 4: + transaction.emit('missing', ledger); + break; + case 8: + transaction.emit('lost', ledger); + break; } }); }; @@ -250,85 +266,88 @@ TransactionManager.prototype._fillSequence = function(tx, callback) { this._loadSequence(sequenceLoaded); }; +/** + * Load account transaction sequence + * + * @param [Function] callback + */ + TransactionManager.prototype._loadSequence = function(callback) { var self = this; + var callback = (typeof callback === 'function') ? callback : function(){}; function sequenceLoaded(err, sequence) { - if (typeof sequence === 'number') { - self._nextSequence = sequence; - self.emit('sequence_loaded', sequence); - if (typeof callback === 'function') { - callback(err, sequence); + if (err || typeof sequence !== 'number') { + if (self._remote.trace) { + log.info('error requesting account transaction sequence', err); + return; } - } else { - setTimeout(function() { - self._loadSequence(callback); - }, 1000 * 3); } + + self._nextSequence = sequence; + self.emit('sequence_loaded', sequence); + callback(err, sequence); }; this._account.getNextSequence(sequenceLoaded); }; -TransactionManager.prototype._resubmit = function(ledgers, pending) { - var self = this; - var pending = pending ? [ pending ] : this._pending; - var ledgers = Number(ledgers) || 0; +/** + * On reconnect, load account_tx in case a pending transaction succeeded while + * disconnected + * + * @api private + */ - function resubmitTransaction(pending) { - if (!pending || pending.finalized) { - // Transaction has been finalized, nothing to do +TransactionManager.prototype._handleReconnect = function() { + var self = this; + + if (!this._pending.length) { + return callback(); + } + + function handleTransactions(err, transactions) { + if (err || typeof transactions !== 'object') { + if (self._remote.trace) { + log.info('error requesting account_tx', err); + } return; } - var hashCached = pending.findId(self._pending._idCache); - - if (self._remote.trace) { - log.info('resubmit:', pending.tx_json); + if (Array.isArray(transactions.transactions)) { + // Treat each transaction in account transaction history as received + transactions.transactions.forEach(self._transactionReceived, self); } - if (hashCached) { - return pending.emit('success', hashCached); - } - - while (self._pending.hasSequence(pending.tx_json.Sequence)) { - //Sequence number has been consumed by another transaction - pending.tx_json.Sequence += 1; - - if (self._remote.trace) { - log.info('incrementing sequence:', pending.tx_json); - } - } - - self._request(pending); + self._loadSequence(function() { + // Resubmit pending transactions after sequence is loaded + self._resubmit(); + }); }; - function resubmitTransactions() { - ;(function nextTransaction(i) { - var transaction = pending[i]; - - if (!(transaction instanceof Transaction)) { - return; - } - - transaction.once('submitted', function(m) { - transaction.emit('resubmitted', m); - - self._loadSequence(); - - if (++i < pending.length) { - nextTransaction(i); - } - }); - - resubmitTransaction(transaction); - })(0); + var options = { + account: this._accountID, + ledger_index_min: this._pending.getMinLedger(), + ledger_index_max: -1, + binary: true, + parseBinary: true, + limit: 20 }; - this._waitLedgers(ledgers, resubmitTransactions); + this._remote.requestAccountTx(options, handleTransactions); }; +/** + * Wait for specified number of ledgers to pass + * + * @param {Number} ledgers + * @param {Function} callback + */ + TransactionManager.prototype._waitLedgers = function(ledgers, callback) { + assert.strictEqual(typeof ledgers, 'number'); + assert.strictEqual(typeof callback, 'function'); + if (ledgers < 1) { return callback(); } @@ -337,35 +356,121 @@ TransactionManager.prototype._waitLedgers = function(ledgers, callback) { var closes = 0; function ledgerClosed() { - if (++closes < ledgers) { - return; + if (++closes === ledgers) { + self._remote.removeListener('ledger_closed', ledgerClosed); + callback(); } - - self._remote.removeListener('ledger_closed', ledgerClosed); - - callback(); }; this._remote.on('ledger_closed', ledgerClosed); }; +/** + * Resubmit pending transactions. If a transaction is specified, it will be + * resubmitted. Otherwise, all pending transactions will be resubmitted + * + * @param [Number] ledgers to wait before resubmitting + * @param [Transaction] pending transactions to resubmit + */ + +TransactionManager.prototype._resubmit = function(ledgers, pending) { + var self = this; + var ledgers = ledgers || 0; + var pending = pending ? [ pending ] : this._pending; + + function resubmitTransaction(transaction, next) { + if (!transaction || transaction.finalized) { + // Transaction has been finalized, nothing to do + return; + } + + // Find ID within cache of received (validated) transaction IDs + var received = transaction.findId(self._pending._idCache); + + if (received) { + return transaction.emit('success', received); + } + + while (self._pending.hasSequence(transaction.tx_json.Sequence)) { + //Sequence number has been consumed by another transaction + transaction.tx_json.Sequence += 1; + + if (self._remote.trace) { + log.info('incrementing sequence:', transaction.tx_json); + } + } + + if (self._remote.trace) { + log.info('resubmit:', transaction.tx_json); + } + + transaction.once('submitted', function(m) { + transaction.emit('resubmitted', m); + next(); + }); + + self._request(transaction); + }; + + this._waitLedgers(ledgers, function() { + async.eachSeries(pending, resubmitTransaction); + }); +}; + +/** + * Prepare submit request + * + * @param {Transaction} transaction to submit + * @return {Request} submit request + */ + +TransactionManager.prototype._prepareRequest = function(tx) { + var submitRequest = this._remote.requestSubmit(); + + if (this._remote.local_signing) { + tx.sign(); + + var serialized = tx.serialize(); + submitRequest.tx_blob(serialized.to_hex()); + + var hash = tx.hash(null, null, serialized); + tx.addId(hash); + } else { + // ND: `build_path` is completely ignored when doing local signing as + // `Paths` is a component of the signed blob, the `tx_blob` is signed, + // sealed and delivered, and the txn unmodified. + // TODO: perhaps an exception should be raised if build_path is attempted + // while local signing + submitRequest.build_path(tx._build_path); + submitRequest.secret(tx._secret); + submitRequest.tx_json(tx.tx_json); + } + + return submitRequest; +}; + +/** + * Send `submit` request, handle response + * + * @param {Transaction} transaction to submit + */ + TransactionManager.prototype._request = function(tx) { - var self = this; + var self = this; var remote = this._remote; + if (tx.finalized) { + return; + } + if (tx.attempts > this._maxAttempts) { - return tx.emit('error', new RippleError('tejAttemptsExceeded')); + tx.emit('error', new RippleError('tejAttemptsExceeded')); + return; } if (tx.attempts > 0 && !remote.local_signing) { - var message = '' - + 'It is not possible to resubmit transactions automatically safely without ' - + 'synthesizing the transactionID locally. See `local_signing` config option'; - - return tx.emit('error', new RippleError('tejLocalSigningRequired', message)); - } - - if (tx.finalized) { + var message = 'Automatic resubmission requires local signing'; + tx.emit('error', new RippleError('tejLocalSigningRequired', message)); return; } @@ -373,22 +478,7 @@ TransactionManager.prototype._request = function(tx) { log.info('submit transaction:', tx.tx_json); } - function transactionProposed(message) { - if (tx.finalized) { - return; - } - - // If server is honest, don't expect a final if rejected. - message.rejected = tx.isRejected(message.engine_result_code); - - tx.emit('proposed', message); - }; - function transactionFailed(message) { - if (tx.finalized) { - return; - } - switch (message.engine_result) { case 'tefPAST_SEQ': self._resubmit(1, tx); @@ -406,39 +496,20 @@ TransactionManager.prototype._request = function(tx) { }; function transactionRetry(message) { - if (tx.finalized) { - return; - } - self._fillSequence(tx, function() { self._resubmit(1, tx); }); }; - function transactionFeeClaimed(message) { - if (tx.finalized) { - return; - } - }; - function transactionFailedLocal(message) { - if (tx.finalized) { - return; - } - - if (self._remote.local_fee && (message.engine_result === 'telINSUF_FEE_P')) { - self._resubmit(2, tx); - } else { + if (!self._remote.local_fee) { submissionError(message); + } else if (message.engine_result === 'telINSUF_FEE_P') { + self._resubmit(2, tx); } }; function submissionError(error) { - // Finalized (e.g. aborted) transactions must stop all activity - if (tx.finalized) { - return; - } - if (TransactionManager._isTooBusy(error)) { self._resubmit(1, tx); } else { @@ -448,13 +519,12 @@ TransactionManager.prototype._request = function(tx) { }; function submitted(message) { - // Finalized (e.g. aborted) transactions must stop all activity if (tx.finalized) { return; } - // ND: If for some unknown reason our hash wasn't computed correctly this is - // an extra measure. + // ND: If for some unknown reason our hash wasn't computed correctly this + // is an extra measure. if (message.tx_json && message.tx_json.hash) { tx.addId(message.tx_json.hash); } @@ -472,10 +542,9 @@ TransactionManager.prototype._request = function(tx) { switch (message.result.slice(0, 3)) { case 'tes': - transactionProposed(message); + tx.emit('proposed', message); break; case 'tec': - transactionFeeClaimed(message); break; case 'ter': transactionRetry(message); @@ -492,57 +561,18 @@ TransactionManager.prototype._request = function(tx) { } }; - var submitRequest = remote.requestSubmit(); - - submitRequest.once('error', submitted); - submitRequest.once('success', submitted); - - function prepareSubmit() { - if (remote.local_signing) { - // TODO: We are serializing twice, when we could/should be feeding the - // tx_blob to `tx.hash()` which rebuilds it to sign it. - submitRequest.tx_blob(tx.serialize().to_hex()); - - // ND: ecdsa produces a random `TxnSignature` field value, a component of - // the hash. Attempting to identify a transaction via a hash synthesized - // locally while using remote signing is inherently flawed. - tx.addId(tx.hash()); - } else { - // ND: `build_path` is completely ignored when doing local signing as - // `Paths` is a component of the signed blob, the `tx_blob` is signed, - // sealed and delivered, and the txn unmodified. - // TODO: perhaps an exception should be raised if build_path is attempted - // while local signing - submitRequest.build_path(tx._build_path); - submitRequest.secret(tx._secret); - submitRequest.tx_json(tx.tx_json); - } - - if (tx._server) { - submitRequest.server = tx._server; - } - - submitTransaction(); - }; - function requestTimeout() { // ND: What if the response is just slow and we get a response that // `submitted` above will cause to have concurrent resubmit logic streams? // It's simpler to just mute handlers and look out for finalized // `transaction` messages. - - // ND: We should audit the code for other potential multiple resubmit - // streams. Connection/reconnection could be one? That's why it's imperative - // that ALL transactionIDs sent over network are tracked. - - // Finalized (e.g. aborted) transactions must stop all activity if (tx.finalized) { return; } tx.emit('timeout'); - if (remote._connected) { + if (remote.isConnected()) { if (remote.trace) { log.info('timeout:', tx.tx_json); } @@ -550,20 +580,6 @@ TransactionManager.prototype._request = function(tx) { } }; - function submitTransaction() { - if (tx.finalized) { - return; - } - - tx.emit('presubmit'); - - submitRequest.timeout(self._submissionTimeout, requestTimeout); - - tx.submissions = submitRequest.broadcast(); - tx.attempts++; - tx.emit('postsubmit'); - }; - tx.submitIndex = this._remote._ledger_current_index; if (tx.attempts === 0) { @@ -571,19 +587,31 @@ TransactionManager.prototype._request = function(tx) { } if (!tx._setLastLedger) { - // Honor LastLedgerSequence set by user of API. If - // left unset by API, bump LastLedgerSequence + // Honor LastLedgerSequence set by user of API. If left unset by API, bump + // LastLedgerSequence tx.tx_json.LastLedgerSequence = tx.submitIndex + 8; } tx.lastLedgerSequence = tx.tx_json.LastLedgerSequence; if (remote.local_signing) { - tx.sign(prepareSubmit); - } else { - prepareSubmit(); + tx.sign(); } + var submitRequest = this._prepareRequest(tx); + submitRequest.once('error', submitted); + submitRequest.once('success', submitted); + + tx.emit('presubmit'); + + tx.submissions = submitRequest.broadcast(); + tx.attempts++; + + tx.emit('postsubmit'); + + //XXX + submitRequest.timeout(self._submissionTimeout, requestTimeout); + return submitRequest; }; @@ -620,60 +648,33 @@ TransactionManager.prototype.submit = function(tx) { var self = this; var remote = this._remote; - // If sequence number is not yet known, defer until it is. if (typeof this._nextSequence !== 'number') { - this.once('sequence_loaded', this.submit.bind(this, tx)); + // If sequence number is not yet known, defer until it is. + this.once('sequence_loaded', function() { + self.submit(tx); + }); return; } - // Finalized (e.g. aborted) transactions must stop all activity if (tx.finalized) { + // Finalized transactions must stop all activity return; } - function cleanup(message) { - // ND: We can just remove this `tx` by identity - self._pending.remove(tx); - tx.emit('final', message); - if (remote.trace) { - log.info('transaction finalized:', tx.tx_json, self._pending.getLength()); - } - }; - - tx.once('cleanup', cleanup); - - tx.on('save', function() { - self.emit('save', tx); - }); - - tx.once('error', function(message) { - tx._errorHandler(message); - }); - - tx.once('success', function(message) { - tx._successHandler(message); - }); - - tx.once('abort', function() { - tx.emit('error', new RippleError('tejAbort', 'Transaction aborted')); - }); - if (typeof tx.tx_json.Sequence !== 'number') { + // Honor manually-set sequences tx.tx_json.Sequence = this._nextSequence++; } - // Attach secret, associate transaction with a server, attach fee. - // If the transaction can't complete, decrement sequence so that - // subsequent transactions + tx.once('cleanup', function() { + self.getPending().remove(tx); + }); + if (!tx.complete()) { this._nextSequence--; return; } - tx.attempts = 0; - tx.submissions = 0; - tx.responses = 0; - // ND: this is the ONLY place we put the tx into the queue. The // TransactionQueue queue is merely a list, so any mutations to tx._hash // will cause subsequent look ups (eg. inside 'transaction-outbound' diff --git a/src/js/ripple/transactionqueue.js b/src/js/ripple/transactionqueue.js index d71e7418..91613147 100644 --- a/src/js/ripple/transactionqueue.js +++ b/src/js/ripple/transactionqueue.js @@ -1,19 +1,20 @@ +var LRU = require('lru-cache'); +var Transaction = require('./transaction').Transaction; /** * Manager for pending transactions */ -var LRU = require('lru-cache'); -var Transaction = require('./transaction').Transaction; - function TransactionQueue() { this._queue = [ ]; - this._idCache = LRU(); - this._sequenceCache = LRU(); + this._idCache = LRU({ max: 200 }); + this._sequenceCache = LRU({ max: 200 }); }; /** * Store received (validated) sequence + * + * @param {Number} sequence */ TransactionQueue.prototype.addReceivedSequence = function(sequence) { @@ -23,6 +24,9 @@ TransactionQueue.prototype.addReceivedSequence = function(sequence) { /** * Check that sequence number has been consumed by a validated * transaction + * + * @param {Number} sequence + * @return {Boolean} */ TransactionQueue.prototype.hasSequence = function(sequence) { @@ -31,6 +35,9 @@ TransactionQueue.prototype.hasSequence = function(sequence) { /** * Store received (validated) ID transaction + * + * @param {String} transaction id + * @param {Transaction} transaction */ TransactionQueue.prototype.addReceivedId = function(id, transaction) { @@ -39,6 +46,9 @@ TransactionQueue.prototype.addReceivedId = function(id, transaction) { /** * Get received (validated) transaction by ID + * + * @param {String} transaction id + * @return {Object} */ TransactionQueue.prototype.getReceived = function(id) { @@ -48,6 +58,9 @@ TransactionQueue.prototype.getReceived = function(id) { /** * Get a submitted transaction by ID. Transactions * may have multiple associated IDs. + * + * @param {String} transaction id + * @return {Transaction} */ TransactionQueue.prototype.getSubmission = function(id) { @@ -63,8 +76,32 @@ TransactionQueue.prototype.getSubmission = function(id) { return result; }; +/** + * Get earliest ledger in the pending queue + * + * @return {Number} ledger + */ + +TransactionQueue.prototype.getMinLedger = function() { + var result = Infinity; + + for (var i=0, tx; (tx=this._queue[i]); i++) { + if (tx.initialSubmitIndex < result) { + result = tx.initialSubmitIndex; + } + } + + if (!isFinite(result)) { + result = -1; + } + + return result; +}; + /** * Remove a transaction from the queue + * + * @param {String|Transaction} transaction or id */ TransactionQueue.prototype.remove = function(tx) { @@ -87,14 +124,30 @@ TransactionQueue.prototype.remove = function(tx) { } }; +/** + * Add a transaction to pending queue + * + * @param {Transaction} transaction + */ + TransactionQueue.prototype.push = function(tx) { this._queue.push(tx); }; +/** + * Iterate over pending transactions + * + * @param {Function} iterator + */ + TransactionQueue.prototype.forEach = function(fn) { this._queue.forEach(fn); }; +/** + * @return {Number} length of pending queue + */ + TransactionQueue.prototype.length = TransactionQueue.prototype.getLength = function() { return this._queue.length; diff --git a/test/transaction-test.js b/test/transaction-test.js index 79eb82e7..8c418eb7 100644 --- a/test/transaction-test.js +++ b/test/transaction-test.js @@ -1,9 +1,10 @@ -var utils = require('./testutils'); -var assert = require('assert'); -var Amount = utils.load_module('amount').Amount; -var Transaction = utils.load_module('transaction').Transaction; -var Remote = utils.load_module('remote').Remote; -var Server = utils.load_module('server').Server; +var utils = require('./testutils'); +var assert = require('assert'); +var Amount = utils.load_module('amount').Amount; +var Transaction = utils.load_module('transaction').Transaction; +var TransactionQueue = utils.load_module('transactionqueue').TransactionQueue; +var Remote = utils.load_module('remote').Remote; +var Server = utils.load_module('server').Server; var transactionResult = { engine_result: 'tesSUCCESS', @@ -38,7 +39,7 @@ describe('Transaction', function() { it('Success listener', function(done) { var transaction = new Transaction(); - transaction.once('cleanup', function(message) { + transaction.once('final', function(message) { assert.deepEqual(message, transactionResult); assert(transaction.finalized); assert.strictEqual(transaction.state, 'validated'); @@ -51,7 +52,7 @@ describe('Transaction', function() { it('Error listener', function(done) { var transaction = new Transaction(); - transaction.once('cleanup', function(message) { + transaction.once('final', function(message) { assert.deepEqual(message, transactionResult); assert(transaction.finalized); assert.strictEqual(transaction.state, 'failed'); @@ -130,25 +131,29 @@ describe('Transaction', function() { it('Set state', function(done) { var transaction = new Transaction(); - transaction.state = 'pending'; + + assert.strictEqual(transaction.state, 'unsubmitted'); var receivedEvents = 0; - var events = 2; + var events = [ + 'submitted', + 'pending', + 'validated' + ]; - transaction.once('state', function(state) { - assert.strictEqual(state, 'validated'); - if (++receivedEvents === events) { - done(); - } + transaction.on('state', function(state) { + receivedEvents++; + assert(events.indexOf(state) > -1, 'Invalid state: ' + state); }); - transaction.once('save', function() { - if (++receivedEvents === events) { - done(); - } - }); + transaction.setState(events[0]); + transaction.setState(events[1]); + transaction.setState(events[1]); + transaction.setState(events[2]); - transaction.setState('validated'); + assert.strictEqual(receivedEvents, 3); + assert.strictEqual(transaction.state, events[2]); + done(); }); it('Finalize submission', function() { @@ -720,47 +725,20 @@ describe('Transaction', function() { done(); }); - it('Sign transaction - with callback', function(done) { - var transaction = new Transaction(); - transaction._secret = 'sh2pTicynUEG46jjR4EoexHcQEoij'; - transaction.tx_json.SigningPubKey = '021FED5FD081CE5C4356431267D04C6E2167E4112C897D5E10335D4E22B4DA49ED'; - transaction.tx_json.Account = 'rMWwx3Ma16HnqSd4H6saPisihX9aKpXxHJ'; - transaction.tx_json.Flags = 0; - transaction.tx_json.Fee = 10; - transaction.tx_json.Sequence = 1; - transaction.tx_json.TransactionType = 'AccountSet'; - - transaction.sign(function() { - var signature = transaction.tx_json.TxnSignature; - assert.strictEqual(transaction.previousSigningHash, 'D1C15200CF532175F1890B6440AD223D3676140522BC11D2784E56760AE3B4FE'); - assert(/^[A-Z0-9]+$/.test(signature)); - done(); - }); - }); - it('Add transaction ID', function(done) { var transaction = new Transaction(); - var saved = 0; - - transaction.on('save', function() { - ++saved; - }); - - transaction.once('save', function() { - setImmediate(function() { - assert.strictEqual(saved, 2); - assert.deepEqual( - transaction.submittedIDs, - [ 'F1C15200CF532175F1890B6440AD223D3676140522BC11D2784E56760AE3B4FE', - 'D1C15200CF532175F1890B6440AD223D3676140522BC11D2784E56760AE3B4FE' ] - ); - done(); - }); - }); transaction.addId('D1C15200CF532175F1890B6440AD223D3676140522BC11D2784E56760AE3B4FE'); transaction.addId('F1C15200CF532175F1890B6440AD223D3676140522BC11D2784E56760AE3B4FE'); transaction.addId('F1C15200CF532175F1890B6440AD223D3676140522BC11D2784E56760AE3B4FE'); + + assert.deepEqual( + transaction.submittedIDs, + [ 'F1C15200CF532175F1890B6440AD223D3676140522BC11D2784E56760AE3B4FE', + 'D1C15200CF532175F1890B6440AD223D3676140522BC11D2784E56760AE3B4FE' ] + ); + + done(); }); it('Find transaction IDs in cache', function(done) { @@ -791,9 +769,10 @@ describe('Transaction', function() { it('Set DestinationTag', function() { var transaction = new Transaction(); - transaction.destinationTag(); transaction.destinationTag('tag'); - assert.strictEqual(transaction.tx_json.DestinationTag, 'tag'); + assert.strictEqual(transaction.tx_json.DestinationTag, void(0)); + transaction.destinationTag(1); + assert.strictEqual(transaction.tx_json.DestinationTag, 1); }); it('Set InvoiceID', function() { @@ -872,7 +851,7 @@ describe('Transaction', function() { } ]; - assert.deepEqual(Transaction._pathRewrite(path), [ + assert.deepEqual(Transaction._rewritePath(path), [ { account: 'rP51ycDJw5ZhgvdKiRjBYZKYjsyoCcHmnY', issuer: 'rsLEU1TPdCJPPysqhWYw9jD97xtG5WqSJm', @@ -892,7 +871,9 @@ describe('Transaction', function() { }); it('Rewrite transaction path - invalid path', function() { - assert.strictEqual(Transaction._pathRewrite(1), void(0)); + assert.throws(function() { + assert.strictEqual(Transaction._rewritePath(1), void(0)); + }); }); it('Add transaction path', function() { @@ -1001,51 +982,52 @@ describe('Transaction', function() { it('Set SourceTag', function() { var transaction = new Transaction(); transaction.sourceTag('tag'); - assert.strictEqual(transaction.tx_json.SourceTag, 'tag'); + assert.strictEqual(transaction.tx_json.SourceTag, void(0)); + transaction.sourceTag(1); + assert.strictEqual(transaction.tx_json.SourceTag, 1); }); it('Set TransferRate', function() { var transaction = new Transaction(); - - assert.throws(function() { - transaction.transferRate(1); - }); - - assert.throws(function() { - transaction.transferRate('1'); - }); - + transaction.transferRate(1); + assert.strictEqual(transaction.tx_json.TransferRate, void(0)); transaction.transferRate(1.5 * 1e9); - assert.strictEqual(transaction.tx_json.TransferRate, 1.5 * 1e9); }); it('Set Flags', function(done) { var transaction = new Transaction(); transaction.tx_json.TransactionType = 'Payment'; - transaction.setFlags(); - assert.strictEqual(transaction.tx_json.Flags, 0); - transaction.setFlags(1); + var transaction = new Transaction(); + transaction.tx_json.TransactionType = 'Payment'; + transaction.setFlags(Transaction.flags.Payment.PartialPayment); + assert.strictEqual(transaction.tx_json.Flags, 131072); - assert.strictEqual(transaction.tx_json.Flags, 1); + var transaction = new Transaction(); + transaction.tx_json.TransactionType = 'Payment'; + transaction.setFlags('NoRippleDirect'); + assert.strictEqual(transaction.tx_json.Flags, 65536); - transaction.setFlags('PartialPayment'); - - assert.strictEqual(transaction.tx_json.Flags, 131073); + var transaction = new Transaction(); + transaction.tx_json.TransactionType = 'Payment'; + transaction.setFlags('PartialPayment', 'NoRippleDirect'); + assert.strictEqual(transaction.tx_json.Flags, 196608); + var transaction = new Transaction(); + transaction.tx_json.TransactionType = 'Payment'; transaction.setFlags([ 'LimitQuality', 'PartialPayment' ]); + assert.strictEqual(transaction.tx_json.Flags, 393216); - assert.strictEqual(transaction.tx_json.Flags, 524289); - + var transaction = new Transaction(); + transaction.tx_json.TransactionType = 'Payment'; transaction.once('error', function(err) { assert.strictEqual(err.result, 'tejInvalidFlag'); done(); }); - - transaction.setFlags('test'); + transaction.setFlags('asdf'); }); it('Add Memo', function() { @@ -1548,16 +1530,10 @@ describe('Transaction', function() { it('Submit transaction - invalid account', function(done) { var remote = new Remote(); - var transaction = new Transaction(remote).accountSet('r36xtKNKR43SeXnGn7kN4r4JdQzcrkqpWe'); - - transaction.tx_json.Account += 'z'; - - transaction.once('error', function(err) { - assert.strictEqual(err.result, 'tejInvalidAccount'); - done(); + assert.throws(function() { + var transaction = new Transaction(remote).accountSet('r36xtKNKR43SeXnGn7kN4r4JdQzcrkqpWeZ'); }); - - transaction.submit(); + done(); }); it('Abort submission on presubmit', function(done) { @@ -1589,6 +1565,39 @@ describe('Transaction', function() { }); }); }); + + it('Get min ledger', function() { + var queue = new TransactionQueue(); + + // Randomized submit indexes + [ + 28093, + 456944, + 347213, + 165662, + 729760, + 808990, + 927393, + 925550, + 872298, + 543305 + ] + .forEach(function(index){ + var tx = new Transaction(); + tx.initialSubmitIndex = index; + queue.push(tx); + }); + + // Pending queue sorted by submit index + var sorted = queue._queue.slice().sort(function(a, b) { + return a.initialSubmitIndex - b.initialSubmitIndex; + }); + + sorted.forEach(function(tx){ + assert.strictEqual(queue.getMinLedger(), tx.initialSubmitIndex); + queue.remove(tx); + }); + }); }); -// vim:sw=2:sts=2:ts=8:et +//vim:sw=2:sts=2:ts=8:et