diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 0fa05585..19d71acb 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -4,27 +4,27 @@ "dependencies": { "async": { "version": "0.9.0", - "from": "async@>=0.9.0 <0.10.0", + "from": "async@~0.9.0", "resolved": "https://registry.npmjs.org/async/-/async-0.9.0.tgz" }, "bignumber.js": { "version": "2.0.3", - "from": "bignumber.js@>=2.0.3 <3.0.0", + "from": "bignumber.js@^2.0.3", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-2.0.3.tgz" }, "extend": { "version": "1.2.1", - "from": "extend@>=1.2.1 <1.3.0", + "from": "extend@~1.2.1", "resolved": "https://registry.npmjs.org/extend/-/extend-1.2.1.tgz" }, "lodash": { - "version": "3.1.0", - "from": "lodash@>=3.1.0 <4.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.1.0.tgz" + "version": "3.3.1", + "from": "lodash@^3.1.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.3.1.tgz" }, "lru-cache": { "version": "2.5.0", - "from": "lru-cache@>=2.5.0 <2.6.0", + "from": "lru-cache@~2.5.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.5.0.tgz" }, "ripple-wallet-generator": { @@ -34,7 +34,7 @@ }, "superagent": { "version": "0.18.2", - "from": "superagent@>=0.18.0 <0.19.0", + "from": "superagent@^0.18.0", "resolved": "https://registry.npmjs.org/superagent/-/superagent-0.18.2.tgz", "dependencies": { "qs": { @@ -69,7 +69,7 @@ }, "debug": { "version": "1.0.4", - "from": "debug@>=1.0.1 <1.1.0", + "from": "debug@~1.0.1", "resolved": "https://registry.npmjs.org/debug/-/debug-1.0.4.tgz", "dependencies": { "ms": { @@ -91,7 +91,7 @@ "dependencies": { "combined-stream": { "version": "0.0.7", - "from": "combined-stream@>=0.0.4 <0.1.0", + "from": "combined-stream@~0.0.4", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-0.0.7.tgz", "dependencies": { "delayed-stream": { @@ -110,7 +110,7 @@ "dependencies": { "core-util-is": { "version": "1.0.1", - "from": "core-util-is@>=1.0.0 <1.1.0", + "from": "core-util-is@~1.0.0", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.1.tgz" }, "isarray": { @@ -120,12 +120,12 @@ }, "string_decoder": { "version": "0.10.31", - "from": "string_decoder@>=0.10.0 <0.11.0", + "from": "string_decoder@~0.10.x", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" }, "inherits": { "version": "2.0.1", - "from": "inherits@>=2.0.1 <2.1.0", + "from": "inherits@~2.0.1", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" } } @@ -134,7 +134,7 @@ }, "ws": { "version": "0.7.1", - "from": "ws@>=0.7.1 <0.8.0", + "from": "ws@~0.7.1", "resolved": "https://registry.npmjs.org/ws/-/ws-0.7.1.tgz", "dependencies": { "options": { @@ -144,38 +144,40 @@ }, "ultron": { "version": "1.0.1", - "from": "ultron@>=1.0.0 <1.1.0", + "from": "ultron@1.0.x", "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.1.tgz" }, "bufferutil": { "version": "1.0.1", - "from": "bufferutil@>=1.0.0 <1.1.0", + "from": "bufferutil@1.0.x", "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-1.0.1.tgz", "dependencies": { "bindings": { "version": "1.2.1", - "from": "bindings@>=1.2.0 <1.3.0" + "from": "bindings@1.2.x", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz" }, "nan": { - "version": "1.6.1", - "from": "nan@>=1.6.0 <1.7.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-1.6.1.tgz" + "version": "1.6.2", + "from": "nan@1.6.x", + "resolved": "https://registry.npmjs.org/nan/-/nan-1.6.2.tgz" } } }, "utf-8-validate": { "version": "1.0.1", - "from": "utf-8-validate@>=1.0.0 <1.1.0", + "from": "utf-8-validate@1.0.x", "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-1.0.1.tgz", "dependencies": { "bindings": { "version": "1.2.1", - "from": "bindings@>=1.2.0 <1.3.0" + "from": "bindings@1.2.x", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz" }, "nan": { - "version": "1.6.1", - "from": "nan@>=1.6.0 <1.7.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-1.6.1.tgz" + "version": "1.6.2", + "from": "nan@1.6.x", + "resolved": "https://registry.npmjs.org/nan/-/nan-1.6.2.tgz" } } } diff --git a/package.json b/package.json index 4587c535..d46b8256 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "superagent": "^0.18.0" }, "devDependencies": { - "assert-diff": "0.0.4", + "assert-diff": "^1.0.1", "coveralls": "~2.10.0", "eslint": "^0.13.0", "gulp": "~3.8.10", diff --git a/src/js/ripple/transactionmanager.js b/src/js/ripple/transactionmanager.js index bacfc521..ca6332f5 100644 --- a/src/js/ripple/transactionmanager.js +++ b/src/js/ripple/transactionmanager.js @@ -1,11 +1,13 @@ -var util = require('util'); -var assert = require('assert'); -var async = require('async'); +'use strict'; + +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; +var Transaction = require('./transaction').Transaction; +var RippleError = require('./rippleerror').RippleError; var PendingQueue = require('./transactionqueue').TransactionQueue; -var log = require('./log').internal.sub('transactionmanager'); +var log = require('./log').internal.sub('transactionmanager'); /** * @constructor TransactionManager @@ -20,7 +22,7 @@ function TransactionManager(account) { this._account = account; this._accountID = account._account_id; this._remote = account._remote; - this._nextSequence = void(0); + this._nextSequence = undefined; this._maxFee = this._remote.max_fee; this._maxAttempts = this._remote.max_attempts; this._submissionTimeout = this._remote.submission_timeout; @@ -37,7 +39,7 @@ function TransactionManager(account) { function updatePendingStatus(ledger) { self._updatePendingStatus(ledger); - }; + } this._remote.on('ledger_closed', updatePendingStatus); @@ -47,7 +49,7 @@ function TransactionManager(account) { // hooking back into ledger_closed self._remote.on('ledger_closed', updatePendingStatus); }); - }; + } this._remote.on('disconnect', function() { self._remote.removeListener('ledger_closed', updatePendingStatus); @@ -56,7 +58,7 @@ function TransactionManager(account) { // Query server for next account transaction sequence this._loadSequence(); -}; +} util.inherits(TransactionManager, EventEmitter); @@ -96,7 +98,7 @@ TransactionManager.normalizeTransaction = function(tx) { var transaction = { }; var keys = Object.keys(tx); - for (var i=0; i self._maxFee) { // Max transaction fee exceeded, abort submission - return maxFeeExceeded(transaction); + maxFeeExceeded(transaction); + return; } transaction.tx_json.Fee = newFee; @@ -244,6 +248,10 @@ TransactionManager.prototype._updatePendingStatus = function(ledger) { assert.strictEqual(typeof ledger.ledger_index, 'number'); this._pending.forEach(function(transaction) { + if (transaction.finalized) { + return; + } + switch (ledger.ledger_index - transaction.submitIndex) { case 4: transaction.emit('missing', ledger); @@ -253,10 +261,6 @@ TransactionManager.prototype._updatePendingStatus = function(ledger) { break; } - if (transaction.finalized) { - return; - } - if (ledger.ledger_index > transaction.tx_json.LastLedgerSequence) { // Transaction must fail transaction.emit('error', new RippleError( @@ -265,46 +269,54 @@ TransactionManager.prototype._updatePendingStatus = function(ledger) { }); }; -//Fill an account transaction sequence +// Fill an account transaction sequence TransactionManager.prototype._fillSequence = function(tx, callback) { var self = this; - function submitFill(sequence, callback) { - var fill = self._remote.transaction(); - fill.account_set(self._accountID); - fill.tx_json.Sequence = sequence; - fill.once('submitted', callback); + function submitFill(sequence, fCallback) { + var fillTransaction = self._remote.createTransaction('AccountSet', { + account: self._accountID + }); + fillTransaction.tx_json.Sequence = sequence; // Secrets may be set on a per-transaction basis if (tx._secret) { - fill.secret(tx._secret); + fillTransaction.secret(tx._secret); } - fill.submit(); - }; + fillTransaction.once('submitted', fCallback); + fillTransaction.submit(); + } function sequenceLoaded(err, sequence) { if (typeof sequence !== 'number') { - return callback(new Error('Failed to fetch account transaction sequence')); + log.info('fill sequence: failed to fetch account transaction sequence'); + return callback(); } - var sequenceDif = tx.tx_json.Sequence - sequence; + var sequenceDiff = tx.tx_json.Sequence - sequence; var submitted = 0; - ;(function nextFill(sequence) { - if (sequence >= tx.tx_json.Sequence) { - return; - } - - submitFill(sequence, function() { - if (++submitted === sequenceDif) { + async.whilst( + function() { + return submitted < sequenceDiff; + }, + function(asyncCallback) { + submitFill(sequence, function(res) { + ++submitted; + if (res.engine_result === 'tesSUCCESS') { + self.emit('sequence_filled', err); + } + asyncCallback(); + }); + }, + function() { + if (callback) { callback(); - } else { - nextFill(sequence + 1); } - }); - })(sequence); - }; + } + ); + } this._loadSequence(sequenceLoaded); }; @@ -318,7 +330,7 @@ TransactionManager.prototype._fillSequence = function(tx, callback) { TransactionManager.prototype._loadSequence = function(callback) { var self = this; - var callback = (typeof callback === 'function') ? callback : function(){}; + callback = (typeof callback === 'function') ? callback : function() {}; function sequenceLoaded(err, sequence) { if (err || typeof sequence !== 'number') { @@ -331,7 +343,7 @@ TransactionManager.prototype._loadSequence = function(callback) { self._nextSequence = sequence; self.emit('sequence_loaded', sequence); callback(err, sequence); - }; + } this._account.getNextSequence(sequenceLoaded); }; @@ -346,10 +358,11 @@ TransactionManager.prototype._loadSequence = function(callback) { TransactionManager.prototype._handleReconnect = function(callback) { var self = this; - var callback = (typeof callback === 'function') ? callback : function(){}; + callback = (typeof callback === 'function') ? callback : function() {}; if (!this._pending.length()) { - return callback(); + callback(); + return; } function handleTransactions(err, transactions) { @@ -372,7 +385,7 @@ TransactionManager.prototype._handleReconnect = function(callback) { // Resubmit pending transactions after sequence is loaded self._resubmit(); }); - }; + } var options = { account: this._accountID, @@ -410,7 +423,7 @@ TransactionManager.prototype._waitLedgers = function(ledgers, callback) { self._remote.removeListener('ledger_closed', ledgerClosed); callback(); } - }; + } this._remote.on('ledger_closed', ledgerClosed); }; @@ -426,8 +439,16 @@ TransactionManager.prototype._waitLedgers = function(ledgers, callback) { TransactionManager.prototype._resubmit = function(ledgers, pending) { var self = this; - var ledgers = ledgers || 0; - var pending = pending ? [ pending ] : this._pending; + + if (ledgers && typeof ledgers !== 'number') { + pending = ledgers; + ledgers = 0; + } + + ledgers = ledgers || 0; + pending = pending instanceof Transaction + ? [pending] + : this.getPending().getQueue(); function resubmitTransaction(transaction, next) { if (!transaction || transaction.finalized) { @@ -449,7 +470,7 @@ TransactionManager.prototype._resubmit = function(ledgers, pending) { } while (self._pending.hasSequence(transaction.tx_json.Sequence)) { - //Sequence number has been consumed by another transaction + // Sequence number has been consumed by another transaction transaction.tx_json.Sequence += 1; if (self._remote.trace) { @@ -467,7 +488,7 @@ TransactionManager.prototype._resubmit = function(ledgers, pending) { }); self._request(transaction); - }; + } this._waitLedgers(ledgers, function() { async.eachSeries(pending, resubmitTransaction); @@ -528,8 +549,8 @@ TransactionManager.prototype._request = function(tx) { } if (tx.attempts > 0 && !remote.local_signing) { - var message = 'Automatic resubmission requires local signing'; - tx.emit('error', new RippleError('tejLocalSigningRequired', message)); + var errMessage = 'Automatic resubmission requires local signing'; + tx.emit('error', new RippleError('tejLocalSigningRequired', errMessage)); return; } @@ -542,22 +563,22 @@ TransactionManager.prototype._request = function(tx) { // Transaction may succeed after Sequence is updated self._resubmit(1, tx); } - }; + } - function transactionRetry(message) { + function transactionRetry() { // XXX This may no longer be necessary. Instead, update sequence numbers // after a transaction fails definitively self._fillSequence(tx, function() { self._resubmit(1, tx); }); - }; + } function transactionFailedLocal(message) { if (message.engine_result === 'telINSUF_FEE_P') { // Transaction may succeed after Fee is updated self._resubmit(1, tx); } - }; + } function submissionError(error) { // Either a tem-class error or generic server error such as tooBusy. This @@ -568,7 +589,7 @@ TransactionManager.prototype._request = function(tx) { self._nextSequence--; tx.emit('error', error); } - }; + } function submitted(message) { if (tx.finalized) { @@ -611,7 +632,7 @@ TransactionManager.prototype._request = function(tx) { // tem submissionError(message); } - }; + } function requestTimeout() { // ND: What if the response is just slow and we get a response that @@ -630,7 +651,7 @@ TransactionManager.prototype._request = function(tx) { } self._resubmit(1, tx); } - }; + } tx.submitIndex = this._remote._ledger_current_index; @@ -661,10 +682,7 @@ TransactionManager.prototype._request = function(tx) { tx.emit('postsubmit'); - //XXX submitRequest.timeout(self._submissionTimeout, requestTimeout); - - return submitRequest; }; /** @@ -675,7 +693,6 @@ TransactionManager.prototype._request = function(tx) { TransactionManager.prototype.submit = function(tx) { var self = this; - var remote = this._remote; if (typeof this._nextSequence !== 'number') { // If sequence number is not yet known, defer until it is. diff --git a/src/js/ripple/transactionqueue.js b/src/js/ripple/transactionqueue.js index 91613147..ac328ce0 100644 --- a/src/js/ripple/transactionqueue.js +++ b/src/js/ripple/transactionqueue.js @@ -1,3 +1,6 @@ +'use strict'; + +var lodash = require('lodash'); var LRU = require('lru-cache'); var Transaction = require('./transaction').Transaction; @@ -7,9 +10,9 @@ var Transaction = require('./transaction').Transaction; function TransactionQueue() { this._queue = [ ]; - this._idCache = LRU({ max: 200 }); - this._sequenceCache = LRU({ max: 200 }); -}; + this._idCache = new LRU({max: 200}); + this._sequenceCache = new LRU({max: 200}); +} /** * Store received (validated) sequence @@ -64,16 +67,9 @@ TransactionQueue.prototype.getReceived = function(id) { */ TransactionQueue.prototype.getSubmission = function(id) { - var result = void(0); - - for (var i=0, tx; (tx=this._queue[i]); i++) { - if (~tx.submittedIDs.indexOf(id)) { - result = tx; - break; - } - } - - return result; + return lodash.find(this._queue, function(tx) { + return lodash.contains(tx.submittedIDs, id); + }); }; /** @@ -83,11 +79,15 @@ TransactionQueue.prototype.getSubmission = function(id) { */ TransactionQueue.prototype.getMinLedger = function() { + if (this.length() < 1) { + return -1; + } + var result = Infinity; - for (var i=0, tx; (tx=this._queue[i]); i++) { - if (tx.initialSubmitIndex < result) { - result = tx.initialSubmitIndex; + for (var i = 0; i < this.length(); i++) { + if (this._queue[i].initialSubmitIndex < result) { + result = this._queue[i].initialSubmitIndex; } } @@ -153,4 +153,12 @@ TransactionQueue.prototype.getLength = function() { return this._queue.length; }; +/** + * @return {Array} pending queue + */ + +TransactionQueue.prototype.getQueue = function() { + return this._queue; +}; + exports.TransactionQueue = TransactionQueue; diff --git a/test/fixtures/transactionmanager.json b/test/fixtures/transactionmanager.json new file mode 100644 index 00000000..7e620eb2 --- /dev/null +++ b/test/fixtures/transactionmanager.json @@ -0,0 +1,294 @@ +{ + "ACCOUNT": { + "address": "rNP2Y5EZrVZdFKsow11NoKTE5FjXuBQd3d", + "secret": "snoBcf899BdZP6nyaRHnSvE7qAJFP" + }, + "ACCOUNT2": { + "address": "rn7GZeJpUafLAyKQ7wrU5SUCu2hpQq7S2W", + "secret": "ssThngbFXQWyJpK1mTeaNBrhHxHhp" + }, + "SUBSCRIBE_RESPONSE": { + "id": 1, + "type": "response", + "status": "success", + "result": { + "fee_base": 10, + "fee_ref": 10, + "hostid": "NBZ", + "ledger_hash": "F1AF7D977B01D99D013EEE75136263A0937575882CC9A741662C3C111B08B112", + "ledger_index": 1, + "ledger_time": 463782770, + "load_base": 256, + "load_factor": 256, + "pubkey_node": "n3Lp7DfQmxjHF5mYJsV2U9anALHmPem8PWQHWGpw4XMz79HA5aJH", + "random": "EECFEE93BBB608914F190EC177B11DE52FC1D75D2C97DACBD26D2DFC6050E874", + "reserve_base": 20000000, + "reserve_inc": 5000000, + "server_status": "full", + "validated_ledgers": "32570-11692908" + } + }, + "ACCOUNT_INFO_RESPONSE": { + "id": 1, + "type": "response", + "status": "success", + "result": { + "account_data": { + "Account": "rNP2Y5EZrVZdFKsow11NoKTE5FjXuBQd3d", + "Balance": "10000", + "Flags": 4849664, + "LedgerEntryType": "AccountRoot", + "OwnerCount": 1, + "Sequence": 3, + "index": "01F3A2D58A5B986BF31CD92B513EB539CE48F705BB0E18FA39EF042DBE07A5DE" + }, + "ledger_current_index": 11699032, + "validated": false + } + + }, + "TX_STREAM_TRANSACTION": { + "engine_result": "tesSUCCESS", + "engine_result_code": 0, + "engine_result_message": "The transaction was applied. Only final in a validated ledger.", + "ledger_hash": "8093A78DEFD1F02037ABD349BD452081554A1DB1FE5E20DCB82D8DF16DD23B6D", + "ledger_index": 1, + "meta": { + "AffectedNodes": [ + { + "ModifiedNode": { + "FinalFields": { + "Account": "rNP2Y5EZrVZdFKsow11NoKTE5FjXuBQd3d", + "Balance": "1000", + "Flags": 4849664, + "OwnerCount": 1, + "Sequence": 1 + }, + "LedgerEntryType": "AccountRoot", + "LedgerIndex": "A4B28FB972EF890DC39A8557DF8960D41DADA00D39B0F1EFCD4BBB85FCA13A30", + "PreviousFields": { + "Balance": "1000", + "Sequence": 3864 + }, + "PreviousTxnID": "F4910E55A39C42AB82071212D84119631DDE0B0F4F8F9040F252B0066898DBDF", + "PreviousTxnLgrSeq": 11693103 + } + } + ], + "TransactionIndex": 9, + "TransactionResult": "tesSUCCESS" + }, + "status": "closed", + "transaction": { + "Account": "rNP2Y5EZrVZdFKsow11NoKTE5FjXuBQd3d", + "Fee": "10", + "Flags": 2147483648, + "LastLedgerSequence": 11693114, + "Sequence": 1, + "SigningPubKey": "2D7F6A1F5F1AE2E0105A88F47D33A0B68031DE7FC43FF42388DA7D4B4C93865F", + "TransactionType": "AccountSet", + "TxnSignature": "39A0EC2307723546901BC8017CED16DF3DD79F50CA68A32377E0CF1BE097DCA004C21D99120220321921230C53A87629D79E6A5E74C08ECB59F8CBB9D695256DE5D0F8CB22FF0A", + "date": 4771619, + "hash": "01D66ACBD00B2A8F5D66FC8F67AC879CAECF49BC94FB97CF24F66B8406F4C040" + }, + "type": "transaction", + "validated": true + + }, + "ACCOUNT_TX_TRANSACTION": { + "validated": true, + "meta": { + "TransactionIndex": 3, + "AffectedNodes": [ + { + "ModifiedNode": { + "LedgerEntryType": "AccountRoot", + "PreviousTxnLgrSeq": 11693103, + "PreviousTxnID": "F4910E55A39C42AB82071212D84119631DDE0B0F4F8F9040F252B0066898DBDF", + "LedgerIndex": "A4B28FB972EF890DC39A8557DF8960D41DADA00D39B0F1EFCD4BBB85FCA13A30", + "PreviousFields": { + "Sequence": 3864, + "Balance": "1000" + }, + "FinalFields": { + "Flags": 4849664, + "Sequence": 3865, + "OwnerCount": 1, + "Balance": "1000", + "Account": "rNP2Y5EZrVZdFKsow11NoKTE5FjXuBQd3d" + } + } + } + ], + "TransactionResult": "tesSUCCESS" + }, + "tx": { + "TransactionType": "AccountSet", + "Flags": 2147483648, + "Sequence": 3864, + "LastLedgerSequence": 2, + "Fee": "10", + "SigningPubKey": "2D7F6A1F5F1AE2E0105A88F47D33A0B68031DE7FC43FF42388DA7D4B4C93865F", + "TxnSignature": "39A0EC2307723546901BC8017CED16DF3DD79F50CA68A32377E0CF1BE097DCA004C21D99120220321921230C53A87629D79E6A5E74C08ECB59F8CBB9D695256DE5D0F8CB22FF", + "Account": "rNP2Y5EZrVZdFKsow11NoKTE5FjXuBQd3d", + "hash": "01D66ACBD00B2A8F5D66FC8F67AC879CAECF49BC94FB97CF24F66B8406F4C040", + "ledger_index": 1, + "inLedger": 1 + } + }, + "ACCOUNT_TX_RESPONSE": { + "id": 1, + "status": "success", + "type": "response", + "result": { + "account": "rNP2Y5EZrVZdFKsow11NoKTE5FjXuBQd3d", + "ledger_index_max": 100, + "ledger_index_min": 1, + "limit": 10, + "transactions": [ ] + } + }, + "LEDGER": { + "type": "ledgerClosed", + "fee_base": 10, + "fee_ref": 10, + "ledger_hash": "FF757ECCF710C27DBCEC569840C38C5583594B56C49693079D95D8A99C30A928", + "ledger_index": 1, + "ledger_time": 478088, + "reserve_base": 20000000, + "reserve_inc": 5000000, + "txn_count": 1, + "validated_ledgers": "1-2" + }, + "ACCOUNT_TX_ERROR": { + "id": 1, + "status": "error", + "type": "response", + "error": "actMalformed", + "error_code": 33, + "error_message": "Account malformed.", + "request": { + "account": "rNP2Y5EZrVZdFKsow11NoKTE5FjXuBQd3dZ", + "binary": true, + "command": "account_tx", + "id": 1, + "ledger_index_max": -1, + "ledger_index_min": -1, + "limit": 10 + } + }, + "SUBMIT_RESPONSE": { + "id": 1, + "result": { + "engine_result": "tesSUCCESS", + "engine_result_code": 0, + "engine_result_message": "The transaction was applied. Only final in a validated ledger.", + "tx_blob": "12000322800000002400000001201B0000000568400000000000000C732102999FB4BC17144F83CDC2F17EA642519FF115EE7B0CC8C78DE9061F1A473F7BAC744730450221008DFE8B50D66F2094652AA6BD1B354CC97E885BC1AEDF586C7ED61C7D786AA66202200FEE38460CF25C726A7F31626BD7A45542AFB2E468EBE9A175F9A9EC3FD19DDB811492DECA2DC92352BE97C1F6347F7E6CCB9A8241C8", + "tx_json": { + "Account": "rNP2Y5EZrVZdFKsow11NoKTE5FjXuBQd3d", + "Fee": "12000", + "Flags": 2147483648, + "LastLedgerSequence": 1, + "Sequence": 1, + "SigningPubKey": "02999FB4BC17144F83CDC2F17EA642519FF115EE7B0CC8C78DE9061F1A473F7BAC", + "TransactionType": "AccountSet", + "TxnSignature": "30440220332316AC7B96B1385F4BD0159706439889A9E05AD62D168E61AC5CBD7BD417C00220361D9FBC31A5919F68FA70E9FB78C6E6C25CC7F89D0A2F9CC42660594EE0D0A2", + "hash": "0D15A847D605DB5F1B76A4EE88EDCD5D279D47F009CFCE27F1D3DFE763225975" + } + }, + "status": "success", + "type": "response" + + }, + "SUBMIT_TEC_RESPONSE": { + "id": 1, + "result": { + "engine_result": "tecNO_REGULAR_KEY", + "engine_result_code": 131, + "engine_result_message": "Regular key is not set.", + "tx_blob": "12000322800000002400000001201B0000000520210000000468400000000000000C732102999FB4BC17144F83CDC2F17EA642519FF115EE7B0CC8C78DE9061F1A473F7BAC74473045022100EEB2D1EFDC4B63A3FA9BD934307D3377197E86133A156022A01FFAD7CE3A78FA022024EB39D525FE42853F8E6B62BB74124FD0A1F763C56621AA8EC6A0FC0D0A6FC0811492DECA2DC92352BE97C1F6347F7E6CCB9A8241C8", + "tx_json": { + "Account": "rNP2Y5EZrVZdFKsow11NoKTE5FjXuBQd3d", + "Fee": "12000", + "Flags": 2147483648, + "LastLedgerSequence": 2, + "Sequence": 1, + "SetFlag": 4, + "SigningPubKey": "02999FB4BC17144F83CDC2F17EA642519FF115EE7B0CC8C78DE9061F1A473F7BAC", + "TransactionType": "AccountSet", + "TxnSignature": "304402202DF7ED29DCA16F7A6191A74127437ACA04ADFCE6DB570101737C6426DD664FF5022069E9C6965DED37207708420B3B77A8CE9D0D009EE1D2434580ED4871398574D0", + "hash": "56DCAE121213AA2E3A74921F934E78635DC0576E4DA1184AEE7D0F8E49046790" + } + }, + "status": "success", + "type": "response" + }, + "SUBMIT_TER_RESPONSE": { + "id": 1, + "result": { + "engine_result": "terNO_ACCOUNT", + "engine_result_code": -96, + "engine_result_message": "The source account does not exist.", + "tx_blob": "12000022800000002400000004201B0000000561400000000000000168400000000000000C732102999FB4BC17144F83CDC2F17EA642519FF115EE7B0CC8C78DE9061F1A473F7BAC74473045022100BDEF89CF25B541FFDBBE0B652BF29D8C30715C8226346C72DBA69B275BC471C50220582C5F3DE6DE489E10594BD8911B2917EE9A452E6ED3A1BEED25E402CD2E3EFF811492DECA2DC92352BE97C1F6347F7E6CCB9A8241C883143108B9AC27BF036EFE5CBE787921F54D622B7A5B", + "tx_json": { + "Account": "rNP2Y5EZrVZdFKsow11NoKTE5FjXuBQd3d", + "Amount": "1", + "Destination": "rn7GZeJpUafLAyKQ7wrU5SUCu2hpQq7S2W", + "Fee": "12000", + "Flags": 2147483648, + "LastLedgerSequence": 2, + "Sequence": 1, + "SigningPubKey": "02999FB4BC17144F83CDC2F17EA642519FF115EE7B0CC8C78DE9061F1A473F7BAC", + "TransactionType": "Payment", + "TxnSignature": "3044022041F099FD8F35F67E73B5F41FFD3CBCF5781748BFEBA6FD1AB56586EE71B7EE9902205F1C4DF58B40859A404939B44A169DA995DA03C6ED713C5DF76E57A843CCE7D1", + "hash": "31090EA7996DFA6C2E0B43F704C30EC9AE635C18D7BEF90F42D4E0E2943A2680" + } + }, + "status": "success", + "type": "response" + }, + "SUBMIT_TEF_RESPONSE": { + "id": 1, + "result": { + "engine_result": "tefPAST_SEQ", + "engine_result_code": -189, + "engine_result_message": "This sequence number has already past.", + "tx_blob": "12000322800000002400000002201B0000000568400000000000000C732102999FB4BC17144F83CDC2F17EA642519FF115EE7B0CC8C78DE9061F1A473F7BAC744730450221008B213EA1B4AACA9545E3DBFE43C69711DF295170B6F510CCD24B383684852C0702200E2A8FA3B205F1AFECF6D6DE298889D71E06336942ADF538847C8EFB88D3AA74811492DECA2DC92352BE97C1F6347F7E6CCB9A8241C8", + "tx_json": { + "Account": "rNP2Y5EZrVZdFKsow11NoKTE5FjXuBQd3d", + "Fee": "12000", + "Flags": 2147483648, + "LastLedgerSequence": 2, + "Sequence": 1, + "SigningPubKey": "02999FB4BC17144F83CDC2F17EA642519FF115EE7B0CC8C78DE9061F1A473F7BAC", + "TransactionType": "AccountSet", + "TxnSignature": "304402203F6B9445B3B2176957C1008EBC0F18668464080A2CB10E78D83FB67DE2ABC3F50220769673DE79E6C1AF0E076A716ABE53018158385615E7EC2866DCFD6268806BF8", + "hash": "731E94632E219ECAF2043C09C0075F1DEA2EC08E66A381CD536F0B1F3B30844D" + } + }, + "status": "success", + "type": "response" + }, + "SUBMIT_TEL_RESPONSE": { + "id": 1, + "result": { + "engine_result": "telINSUF_FEE_P", + "engine_result_code": -394, + "engine_result_message": "Fee insufficient.", + "tx_blob": "12000322800000002400000003201B00000005684000000000000001732102999FB4BC17144F83CDC2F17EA642519FF115EE7B0CC8C78DE9061F1A473F7BAC7446304402206480AC2410BE4370E74E27B4427EF05F8A156D313C9D768E57B6EEC3BB89CDD402201503759955F367F88E5442F08D713B5369C5528475F046500721E676C24D0DFD811492DECA2DC92352BE97C1F6347F7E6CCB9A8241C8", + "tx_json": { + "Account": "rNP2Y5EZrVZdFKsow11NoKTE5FjXuBQd3d", + "Fee": "12000", + "Flags": 2147483648, + "LastLedgerSequence": 2, + "Sequence": 1, + "SigningPubKey": "02999FB4BC17144F83CDC2F17EA642519FF115EE7B0CC8C78DE9061F1A473F7BAC", + "TransactionType": "AccountSet", + "TxnSignature": "3044022002C0C13B21F9B690C6A3FF4B5A3F966AC47ACEE80453DAD553F5051FB3CED0E902202851BCC9EF8E2160BD1F3F6CF91B7C679149BD5FAD502C2BA544F7E61755AD1B", + "hash": "ABF970193B8C9BE9BBD2777750924B28E7542DE1491EB55F4795539045AEA8B9" + } + }, + "status": "success", + "type": "response" + } +} diff --git a/test/transaction-manager-test.js b/test/transaction-manager-test.js new file mode 100644 index 00000000..17770c77 --- /dev/null +++ b/test/transaction-manager-test.js @@ -0,0 +1,630 @@ +'use strict'; + +var ws = require('ws'); +var lodash = require('lodash'); +var assert = require('assert-diff'); +var Remote = require('ripple-lib').Remote; +var SerializedObject = require('ripple-lib').SerializedObject; +var Transaction = require('ripple-lib').Transaction; +var TransactionManager = require('../src/js/ripple/transactionmanager') +.TransactionManager; + +var LEDGER = require('./fixtures/transactionmanager').LEDGER; +var ACCOUNT = require('./fixtures/transactionmanager').ACCOUNT; +var ACCOUNT2 = require('./fixtures/transactionmanager').ACCOUNT2; +var SUBSCRIBE_RESPONSE = require('./fixtures/transactionmanager') +.SUBSCRIBE_RESPONSE; +var ACCOUNT_INFO_RESPONSE = require('./fixtures/transactionmanager') +.ACCOUNT_INFO_RESPONSE; +var TX_STREAM_TRANSACTION = require('./fixtures/transactionmanager') +.TX_STREAM_TRANSACTION; +var ACCOUNT_TX_TRANSACTION = require('./fixtures/transactionmanager') +.ACCOUNT_TX_TRANSACTION; +var ACCOUNT_TX_RESPONSE = require('./fixtures/transactionmanager') +.ACCOUNT_TX_RESPONSE; +var ACCOUNT_TX_ERROR = require('./fixtures/transactionmanager') +.ACCOUNT_TX_ERROR; +var SUBMIT_RESPONSE = require('./fixtures/transactionmanager') +.SUBMIT_RESPONSE; +var SUBMIT_TEC_RESPONSE = require('./fixtures/transactionmanager') +.SUBMIT_TEC_RESPONSE; +var SUBMIT_TER_RESPONSE = require('./fixtures/transactionmanager') +.SUBMIT_TER_RESPONSE; +var SUBMIT_TEF_RESPONSE = require('./fixtures/transactionmanager') +.SUBMIT_TEF_RESPONSE; +var SUBMIT_TEL_RESPONSE = require('./fixtures/transactionmanager') +.SUBMIT_TEL_RESPONSE; + +describe('TransactionManager', function() { + var rippled; + var rippledConnection; + var remote; + var account; + var transactionManager; + + beforeEach(function(done) { + rippled = new ws.Server({port: 5763}); + + rippled.on('connection', function(c) { + var ledger = lodash.extend({}, LEDGER); + c.sendJSON = function(v) { + try { + c.send(JSON.stringify(v)); + } catch (e) { + } + }; + c.sendResponse = function(baseResponse, ext) { + assert.strictEqual(typeof baseResponse, 'object'); + assert.strictEqual(baseResponse.type, 'response'); + c.sendJSON(lodash.extend(baseResponse, ext)); + }; + c.closeLedger = function() { + c.sendJSON(lodash.extend(ledger, { + ledger_index: ++ledger.ledger_index + })); + }; + c.on('message', function(m) { + m = JSON.parse(m); + rippled.emit('request_' + m.command, m, c); + }); + rippledConnection = c; + }); + + rippled.on('request_subscribe', function(message, c) { + if (lodash.isEqual(message.streams, ['ledger', 'server'])) { + c.sendResponse(SUBSCRIBE_RESPONSE, {id: message.id}); + } + }); + rippled.on('request_account_info', function(message, c) { + if (message.account === ACCOUNT.address) { + c.sendResponse(ACCOUNT_INFO_RESPONSE, {id: message.id}); + } + }); + + remote = new Remote({servers: ['ws://localhost:5763']}); + remote.setSecret(ACCOUNT.address, ACCOUNT.secret); + account = remote.account(ACCOUNT.address); + transactionManager = account._transactionManager; + + remote.connect(function() { + setTimeout(done, 10); + }); + }); + + afterEach(function(done) { + remote.disconnect(function() { + rippled.close(); + setImmediate(done); + }); + }); + + it('Normalize transaction', function() { + var t1 = TransactionManager.normalizeTransaction(TX_STREAM_TRANSACTION); + var t2 = TransactionManager.normalizeTransaction(ACCOUNT_TX_TRANSACTION); + + [t1, t2].forEach(function(t) { + assert(t.hasOwnProperty('metadata')); + assert(t.hasOwnProperty('tx_json')); + assert.strictEqual(t.validated, true); + assert.strictEqual(t.ledger_index, 1); + assert.strictEqual(t.engine_result, 'tesSUCCESS'); + assert.strictEqual(t.type, 'transaction'); + assert.strictEqual(t.tx_json.hash, + '01D66ACBD00B2A8F5D66FC8F67AC879CAECF49BC94FB97CF24F66B8406F4C040'); + }); + }); + + it('Handle received transaction', function(done) { + var transaction = Transaction.from_json(TX_STREAM_TRANSACTION.transaction); + + transaction.once('success', function() { + done(); + }); + + transaction.addId(TX_STREAM_TRANSACTION.transaction.hash); + transactionManager.getPending().push(transaction); + rippledConnection.sendJSON(TX_STREAM_TRANSACTION); + }); + it('Handle received transaction -- failed', function(done) { + var transaction = Transaction.from_json(TX_STREAM_TRANSACTION.transaction); + + transaction.once('error', function(err) { + assert.strictEqual(err.engine_result, 'tecINSUFF_FEE_P'); + done(); + }); + + transaction.addId(TX_STREAM_TRANSACTION.transaction.hash); + transactionManager.getPending().push(transaction); + rippledConnection.sendJSON(lodash.extend({ }, TX_STREAM_TRANSACTION, { + engine_result: 'tecINSUFF_FEE_P' + })); + }); + it('Handle received transaction -- not submitted', function(done) { + rippledConnection.sendJSON(TX_STREAM_TRANSACTION); + + remote.once('transaction', function() { + assert(transactionManager.getPending().getReceived( + TX_STREAM_TRANSACTION.transaction.hash)); + done(); + }); + }); + it('Handle received transaction -- Account mismatch', function(done) { + var tx = lodash.extend({ }, TX_STREAM_TRANSACTION); + lodash.extend(tx.transaction, { + Account: 'rMP2Y5EZrVZdFKsow11NoKTE5FjXuBQd3d' + }); + rippledConnection.sendJSON(tx); + + setImmediate(function() { + assert(!transactionManager.getPending().getReceived( + TX_STREAM_TRANSACTION.transaction.hash)); + done(); + }); + }); + it('Handle received transaction -- not validated', function(done) { + var tx = lodash.extend({ }, TX_STREAM_TRANSACTION, { + validated: false + }); + rippledConnection.sendJSON(tx); + + setImmediate(function() { + assert(!transactionManager.getPending().getReceived( + TX_STREAM_TRANSACTION.transaction.hash)); + done(); + }); + }); + it('Handle received transaction -- from account_tx', function(done) { + var transaction = Transaction.from_json(ACCOUNT_TX_TRANSACTION.tx); + transaction.once('success', function() { + done(); + }); + + transaction.addId(ACCOUNT_TX_TRANSACTION.tx.hash); + transactionManager.getPending().push(transaction); + transactionManager._transactionReceived(ACCOUNT_TX_TRANSACTION); + }); + + it('Adjust pending transaction fee', function(done) { + var transaction = new Transaction(remote); + transaction.tx_json = ACCOUNT_TX_TRANSACTION.tx; + + transaction.once('fee_adjusted', function(a, b) { + assert.strictEqual(a, '10'); + assert.strictEqual(b, '24'); + assert.strictEqual(transaction.tx_json.Fee, '24'); + done(); + }); + + transactionManager.getPending().push(transaction); + + rippledConnection.sendJSON({ + type: 'serverStatus', + load_base: 256, + load_factor: 256 * 2, + server_status: 'full' + }); + }); + + it('Adjust pending transaction fee -- max fee exceeded', function(done) { + transactionManager._maxFee = 10; + + var transaction = new Transaction(remote); + transaction.tx_json = ACCOUNT_TX_TRANSACTION.tx; + + transaction.once('fee_adjusted', function() { + assert(false, 'Fee should not be adjusted'); + }); + + transactionManager.getPending().push(transaction); + + rippledConnection.sendJSON({ + type: 'serverStatus', + load_base: 256, + load_factor: 256 * 2, + server_status: 'full' + }); + + setImmediate(done); + }); + + it('Adjust pending transaction fee -- no local fee', function(done) { + remote.local_fee = false; + + var transaction = new Transaction(remote); + transaction.tx_json = ACCOUNT_TX_TRANSACTION.tx; + + transaction.once('fee_adjusted', function() { + assert(false, 'Fee should not be adjusted'); + }); + + transactionManager.getPending().push(transaction); + + rippledConnection.sendJSON({ + type: 'serverStatus', + load_base: 256, + load_factor: 256 * 2, + server_status: 'full' + }); + + setImmediate(done); + }); + + it('Wait ledgers', function(done) { + transactionManager._waitLedgers(3, done); + + for (var i = 1; i <= 3; i++) { + rippledConnection.closeLedger(); + } + }); + + it('Wait ledgers -- no ledgers', function(done) { + transactionManager._waitLedgers(0, done); + }); + + it('Update pending status', function(done) { + var transaction = Transaction.from_json(TX_STREAM_TRANSACTION.transaction); + transaction.submitIndex = 1; + transaction.tx_json.LastLedgerSequence = 10; + + var receivedMissing = false; + var receivedLost = false; + + transaction.once('missing', function() { + receivedMissing = true; + }); + transaction.once('lost', function() { + receivedLost = true; + }); + transaction.once('error', function(err) { + assert.strictEqual(err.engine_result, 'tejMaxLedger'); + assert(receivedMissing); + assert(receivedLost); + done(); + }); + + transaction.addId(TX_STREAM_TRANSACTION.transaction.hash); + transactionManager.getPending().push(transaction); + + for (var i = 1; i <= 10; i++) { + rippledConnection.closeLedger(); + } + }); + + it('Update pending status -- finalized before max ledger exceeded', + function(done) { + var transaction = Transaction.from_json(TX_STREAM_TRANSACTION.transaction); + transaction.submitIndex = 1; + transaction.tx_json.LastLedgerSequence = 10; + transaction.finalized = true; + + var receivedMissing = false; + var receivedLost = false; + + transaction.once('missing', function() { + receivedMissing = true; + }); + transaction.once('lost', function() { + receivedLost = true; + }); + transaction.once('error', function() { + assert(false, 'Should not err'); + }); + + transaction.addId(TX_STREAM_TRANSACTION.transaction.hash); + transactionManager.getPending().push(transaction); + + for (var i = 1; i <= 10; i++) { + rippledConnection.closeLedger(); + } + + setImmediate(function() { + assert(!receivedMissing); + assert(!receivedLost); + done(); + }); + }); + + it('Handle reconnect', function(done) { + var transaction = Transaction.from_json(TX_STREAM_TRANSACTION.transaction); + + var binaryTx = lodash.extend({}, ACCOUNT_TX_TRANSACTION, { + ledger_index: ACCOUNT_TX_TRANSACTION.tx.ledger_index, + tx_blob: SerializedObject.from_json(ACCOUNT_TX_TRANSACTION.tx).to_hex(), + meta: SerializedObject.from_json(ACCOUNT_TX_TRANSACTION.meta).to_hex() + }); + + var hash = new SerializedObject(binaryTx.tx_blob).hash(0x54584E00).to_hex(); + + transaction.addId(hash); + + transaction.once('success', function(res) { + assert.strictEqual(res.engine_result, 'tesSUCCESS'); + done(); + }); + + transactionManager.getPending().push(transaction); + + rippled.once('request_account_tx', function(m, req) { + var response = lodash.extend({}, ACCOUNT_TX_RESPONSE); + response.result.transactions = [binaryTx]; + req.sendResponse(response, {id: m.id}); + }); + + remote.disconnect(remote.connect); + }); + + it('Handle reconnect -- no matching transaction found', function(done) { + var transaction = Transaction.from_json(TX_STREAM_TRANSACTION.transaction); + + var binaryTx = lodash.extend({}, ACCOUNT_TX_TRANSACTION, { + ledger_index: ACCOUNT_TX_TRANSACTION.tx.ledger_index, + tx_blob: SerializedObject.from_json(ACCOUNT_TX_TRANSACTION.tx).to_hex(), + meta: SerializedObject.from_json(ACCOUNT_TX_TRANSACTION.meta).to_hex() + }); + + transactionManager._request = function() { + // Resubmitting + done(); + }; + + transactionManager.getPending().push(transaction); + + rippled.once('request_account_tx', function(m, req) { + var response = lodash.extend({}, ACCOUNT_TX_RESPONSE); + response.result.transactions = [binaryTx]; + req.sendResponse(response, {id: m.id}); + }); + + remote.disconnect(remote.connect); + }); + + it('Handle reconnect -- account_tx error', function(done) { + var transaction = Transaction.from_json(TX_STREAM_TRANSACTION.transaction); + transactionManager.getPending().push(transaction); + + transactionManager._resubmit = function() { + assert(false, 'Should not resubmit'); + }; + + rippled.once('request_account_tx', function(m, req) { + req.sendResponse(ACCOUNT_TX_ERROR, {id: m.id}); + setImmediate(done); + }); + + remote.disconnect(remote.connect); + }); + + it('Submit transaction', function(done) { + var transaction = remote.createTransaction('AccountSet', { + account: ACCOUNT.address + }); + + var receivedInitialSuccess = false; + var receivedProposed = false; + transaction.once('proposed', function(m) { + assert.strictEqual(m.engine_result, 'tesSUCCESS'); + receivedProposed = true; + }); + transaction.once('submitted', function(m) { + assert.strictEqual(m.engine_result, 'tesSUCCESS'); + receivedInitialSuccess = true; + }); + + rippled.once('request_submit', function(m, req) { + assert.strictEqual(m.tx_blob, SerializedObject.from_json( + transaction.tx_json).to_hex()); + assert.strictEqual(transactionManager.getPending().length(), 1); + req.sendResponse(SUBMIT_RESPONSE, {id: m.id}); + setImmediate(function() { + var txEvent = lodash.extend({}, TX_STREAM_TRANSACTION); + txEvent.transaction = transaction.tx_json; + txEvent.transaction.hash = transaction.hash(); + rippledConnection.sendJSON(txEvent); + }); + }); + + transaction.submit(function(err, res) { + assert(!err, 'Transaction submission should succeed'); + assert(receivedInitialSuccess); + assert(receivedProposed); + assert.strictEqual(res.engine_result, 'tesSUCCESS'); + assert.strictEqual(transactionManager.getPending().length(), 0); + done(); + }); + }); + + it('Submit transaction -- tec error', function(done) { + var transaction = remote.createTransaction('AccountSet', { + account: ACCOUNT.address, + set_flag: 'asfDisableMaster' + }); + + var receivedSubmitted = false; + transaction.once('proposed', function() { + assert(false, 'Should not receive proposed event'); + }); + transaction.once('submitted', function(m) { + assert.strictEqual(m.engine_result, 'tecNO_REGULAR_KEY'); + receivedSubmitted = true; + }); + + rippled.once('request_submit', function(m, req) { + assert.strictEqual(m.tx_blob, SerializedObject.from_json( + transaction.tx_json).to_hex()); + assert.strictEqual(transactionManager.getPending().length(), 1); + req.sendResponse(SUBMIT_TEC_RESPONSE, {id: m.id}); + setImmediate(function() { + var txEvent = lodash.extend({}, TX_STREAM_TRANSACTION, + SUBMIT_TEC_RESPONSE.result); + txEvent.transaction = transaction.tx_json; + txEvent.transaction.hash = transaction.hash(); + rippledConnection.sendJSON(txEvent); + }); + }); + + transaction.submit(function(err) { + assert(err, 'Transaction submission should not succeed'); + assert(receivedSubmitted); + assert.strictEqual(err.engine_result, 'tecNO_REGULAR_KEY'); + assert.strictEqual(transactionManager.getPending().length(), 0); + done(); + }); + }); + + it('Submit transaction -- ter error', function(done) { + var transaction = remote.createTransaction('Payment', { + account: ACCOUNT.address, + destination: ACCOUNT2.address, + amount: '1' + }); + transaction.tx_json.Sequence = ACCOUNT_INFO_RESPONSE.result + .account_data.Sequence + 1; + + var receivedSubmitted = false; + transaction.once('proposed', function() { + assert(false, 'Should not receive proposed event'); + }); + transaction.once('submitted', function(m) { + assert.strictEqual(m.engine_result, 'terNO_ACCOUNT'); + receivedSubmitted = true; + }); + + rippled.on('request_submit', function(m, req) { + var deserialized = new SerializedObject(m.tx_blob).to_json(); + + switch (deserialized.TransactionType) { + case 'Payment': + assert.strictEqual(transactionManager.getPending().length(), 1); + assert.deepEqual(deserialized, transaction.tx_json); + req.sendResponse(SUBMIT_TER_RESPONSE, {id: m.id}); + break; + case 'AccountSet': + assert.strictEqual(deserialized.Account, ACCOUNT.address); + assert.strictEqual(deserialized.Flags, 2147483648); + req.sendResponse(SUBMIT_RESPONSE, {id: m.id}); + req.closeLedger(); + break; + } + }); + rippled.once('request_submit', function(m, req) { + req.sendJSON(lodash.extend({}, LEDGER, { + ledger_index: transaction.tx_json.LastLedgerSequence + 1 + })); + }); + + transaction.submit(function(err) { + assert(err, 'Transaction submission should not succeed'); + assert.strictEqual(err.engine_result, 'tejMaxLedger'); + assert(receivedSubmitted); + assert.strictEqual(transactionManager.getPending().length(), 0); + transactionManager.once('sequence_filled', done); + }); + }); + + it('Submit transaction -- tef error', function(done) { + var transaction = remote.createTransaction('AccountSet', { + account: ACCOUNT.address + }); + + transaction.tx_json.Sequence = ACCOUNT_INFO_RESPONSE.result + .account_data.Sequence - 1; + + var receivedSubmitted = false; + var receivedResubmitted = false; + transaction.once('proposed', function() { + assert(false, 'Should not receive proposed event'); + }); + transaction.once('submitted', function(m) { + assert.strictEqual(m.engine_result, 'tefPAST_SEQ'); + receivedSubmitted = true; + }); + + rippled.on('request_submit', function(m, req) { + assert.strictEqual(transactionManager.getPending().length(), 1); + assert.strictEqual(m.tx_blob, SerializedObject.from_json( + transaction.tx_json).to_hex()); + req.sendResponse(SUBMIT_TEF_RESPONSE, {id: m.id}); + }); + + rippled.once('request_submit', function(m, req) { + transaction.once('resubmitted', function() { + receivedResubmitted = true; + req.sendJSON(lodash.extend({}, LEDGER, { + ledger_index: transaction.tx_json.LastLedgerSequence + 1 + })); + }); + + req.closeLedger(); + }); + + transaction.submit(function(err) { + assert(err, 'Transaction submission should not succeed'); + assert(receivedSubmitted); + assert(receivedResubmitted); + assert.strictEqual(err.engine_result, 'tejMaxLedger'); + assert.strictEqual(transactionManager.getPending().length(), 0); + done(); + }); + }); + + it('Submit transaction -- tel error', function(done) { + var transaction = remote.createTransaction('AccountSet', { + account: ACCOUNT.address + }); + + var receivedSubmitted = false; + var receivedResubmitted = false; + transaction.once('proposed', function() { + assert(false, 'Should not receive proposed event'); + }); + transaction.once('submitted', function(m) { + assert.strictEqual(m.engine_result, 'telINSUF_FEE_P'); + receivedSubmitted = true; + }); + + rippled.on('request_submit', function(m, req) { + assert.strictEqual(transactionManager.getPending().length(), 1); + assert.strictEqual(m.tx_blob, SerializedObject.from_json( + transaction.tx_json).to_hex()); + req.sendResponse(SUBMIT_TEL_RESPONSE, {id: m.id}); + }); + + rippled.once('request_submit', function(m, req) { + transaction.once('resubmitted', function() { + receivedResubmitted = true; + req.sendJSON(lodash.extend({}, LEDGER, { + ledger_index: transaction.tx_json.LastLedgerSequence + 1 + })); + }); + + req.closeLedger(); + }); + + transaction.submit(function(err) { + assert(err, 'Transaction submission should not succeed'); + assert(receivedSubmitted); + assert(receivedResubmitted); + assert.strictEqual(err.engine_result, 'tejMaxLedger'); + assert.strictEqual(transactionManager.getPending().length(), 0); + done(); + }); + }); + + it('Submit transaction -- invalid secret', function(done) { + remote.setSecret(ACCOUNT.address, ACCOUNT.secret + 'z'); + + var transaction = remote.createTransaction('AccountSet', { + account: ACCOUNT.address + }); + + rippled.once('request_submit', function() { + assert(false, 'Should not request submit'); + }); + + transaction.submit(function(err) { + assert.strictEqual(err.engine_result, 'tejSecretInvalid'); + assert.strictEqual(transactionManager.getPending().length(), 0); + done(); + }); + }); +});