diff --git a/src/core/remote.js b/src/core/remote.js index c66d330f..f0dc9562 100644 --- a/src/core/remote.js +++ b/src/core/remote.js @@ -135,6 +135,9 @@ function Remote(options = {}) { if (typeof this.submission_timeout !== 'number') { throw new TypeError('submission_timeout must be a number'); } + if (typeof this.automatic_resubmission !== 'boolean') { + throw new TypeError('automatic_resubmission must be a boolean'); + } if (typeof this.last_ledger_offset !== 'number') { throw new TypeError('last_ledger_offset must be a number'); } @@ -191,6 +194,7 @@ Remote.DEFAULTS = { max_fee: 1000000, // 1 XRP max_attempts: 10, submission_timeout: 1000 * 20, + automatic_resubmission: true, last_ledger_offset: 3, servers: [ ], max_listeners: 0 // remove Node EventEmitter warnings diff --git a/src/core/transaction.js b/src/core/transaction.js index f9285cac..e5399594 100644 --- a/src/core/transaction.js +++ b/src/core/transaction.js @@ -39,6 +39,9 @@ function Transaction(remote) { this.tx_json = {Flags: 0}; this._secret = undefined; this._build_path = false; + this._should_resubmit = remoteExists + ? this.remote.automatic_resubmission + : true; this._maxFee = remoteExists ? this.remote.max_fee : undefined; this.state = 'unsubmitted'; this.finalized = false; @@ -191,9 +194,10 @@ Transaction.prototype.isRejected = function(ter) { }; Transaction.from_json = function(j) { - return (new Transaction()).parseJson(j); + return (new Transaction()).setJson(j); }; +Transaction.prototype.setJson = Transaction.prototype.parseJson = function(v) { this.tx_json = v; return this; @@ -212,6 +216,15 @@ Transaction.prototype.setState = function(state) { } }; +Transaction.prototype.setResubmittable = function(v) { + if (typeof v === 'boolean') { + this._should_resubmit = v; + } +}; +Transaction.prototype.isResubmittable = function() { + return this._should_resubmit; +}; + /** * Finalize transaction. This will prevent future activity * @@ -269,13 +282,12 @@ Transaction.prototype.getTransactionType = function() { * @return {TransactionManager] */ -Transaction.prototype.getManager = function(account_) { +Transaction.prototype.getManager = function(account) { if (!this.remote) { return undefined; } - const account = account_ || this.tx_json.Account; - return this.remote.account(account)._transactionManager; + return this.remote.account(account || this.getAccount())._transactionManager; }; /** @@ -285,13 +297,12 @@ Transaction.prototype.getManager = function(account_) { */ Transaction.prototype.getSecret = -Transaction.prototype._accountSecret = function(account_) { +Transaction.prototype._accountSecret = function(account) { if (!this.remote) { return undefined; } - const account = account_ || this.tx_json.Account; - return this.remote.secrets[account]; + return this.remote.secrets[account || this.getAccount()]; }; /** diff --git a/src/core/transactionmanager.js b/src/core/transactionmanager.js index 1785e0a4..e7383287 100644 --- a/src/core/transactionmanager.js +++ b/src/core/transactionmanager.js @@ -95,7 +95,7 @@ TransactionManager._isTooBusy = function(error) { */ TransactionManager.normalizeTransaction = function(tx) { - let transaction = { }; + const transaction = { }; const keys = Object.keys(tx); for (let i = 0; i < keys.length; i++) { @@ -274,7 +274,7 @@ TransactionManager.prototype._fillSequence = function(tx, callback) { const self = this; function submitFill(sequence, fCallback) { - let fillTransaction = self._remote.createTransaction('AccountSet', { + const fillTransaction = self._remote.createTransaction('AccountSet', { account: self._accountID }); fillTransaction.tx_json.Sequence = sequence; @@ -476,6 +476,13 @@ TransactionManager.prototype._resubmit = function(ledgers_, pending_) { } } + if (!transaction.isResubmittable()) { + // Rather than resubmit, wait for the transaction to fail due to + // LastLedgerSequence's being exceeded. The ultimate error emitted on + // transaction is 'tejMaxLedger'; should be definitive + return; + } + while (self._pending.hasSequence(transaction.tx_json.Sequence)) { // Sequence number has been consumed by another transaction transaction.tx_json.Sequence += 1; @@ -591,7 +598,12 @@ TransactionManager.prototype._request = function(tx) { // Either a tem-class error or generic server error such as tooBusy. This // should be a definitive failure if (TransactionManager._isTooBusy(error)) { - self._resubmit(1, tx); + self._waitLedgers(1, function() { + tx.once('submitted', function(m) { + tx.emit('resubmitted', m); + }); + self._request(tx); + }); } else { self._nextSequence--; tx.emit('error', error); @@ -716,7 +728,8 @@ TransactionManager.prototype.submit = function(tx) { if (typeof tx.tx_json.Sequence !== 'number') { // Honor manually-set sequences - tx.tx_json.Sequence = this._nextSequence++; + this._nextSequence += 1; + tx.tx_json.Sequence = this._nextSequence; } tx.once('cleanup', function() { @@ -724,7 +737,7 @@ TransactionManager.prototype.submit = function(tx) { }); if (!tx.complete()) { - this._nextSequence--; + this._nextSequence -= 1; return; } diff --git a/test/fixtures/transactionmanager.json b/test/fixtures/transactionmanager.json index 1808b334..a41f2164 100644 --- a/test/fixtures/transactionmanager.json +++ b/test/fixtures/transactionmanager.json @@ -302,5 +302,17 @@ }, "status": "error", "type": "response" + }, + "SUBMIT_TOO_BUSY_ERROR": { + "error": "tooBusy", + "error_exception": "Too busy", + "id": 1, + "request": { + "command": "submit", + "id": 1, + "tx_blob": "12000022800000002400000001201B00CCEAD0614000000000000001684000000000002EE0732102999FB4BC17144F83CDC2F17EA642519FF115EE7B0CC8C78DE9061F1A473F7BAC7447304502210098DC7E9ED1CE860FB6B0904E6E8140D5463D288BA633F36E69A68ACB3D6FCA06022014E76E22F5173B37239F9F56F904839B462F5019169C56A324D3F074FBA39A2A811492DECA2DC92352BE97C1F6347F7E6CCB9A8241C883143108B9AC27BF036EFE5CBE787921F54D622B7A5BF9EA7C0B6D79206D656D6F747970657E0C6D79206D656D6F5F64617461E1F1" + }, + "status": "error", + "type": "response" } } diff --git a/test/transaction-manager-test.js b/test/transaction-manager-test.js index 82c772af..0bfe4144 100644 --- a/test/transaction-manager-test.js +++ b/test/transaction-manager-test.js @@ -38,6 +38,8 @@ const SUBMIT_TEL_RESPONSE = require('./fixtures/transactionmanager') .SUBMIT_TEL_RESPONSE; const SUBMIT_REMOTE_ERROR = require('./fixtures/transactionmanager') .SUBMIT_REMOTE_ERROR; +const SUBMIT_TOO_BUSY_ERROR = require('./fixtures/transactionmanager') +.SUBMIT_TOO_BUSY_ERROR; describe('TransactionManager', function() { let rippled; @@ -59,9 +61,9 @@ describe('TransactionManager', function() { c.sendJSON = function(v) { try { c.send(JSON.stringify(v)); - } catch (e) { + } catch (e) /* eslint-disable no-empty */{ // empty - } + } /* eslint-enable no-empty */ }; c.sendResponse = function(baseResponse, ext) { assert.strictEqual(typeof baseResponse, 'object'); @@ -426,7 +428,7 @@ describe('TransactionManager', function() { assert.strictEqual(transactionManager.getPending().length(), 1); req.sendResponse(SUBMIT_RESPONSE, {id: m.id}); setImmediate(function() { - let txEvent = lodash.extend({}, TX_STREAM_TRANSACTION); + const txEvent = lodash.extend({}, TX_STREAM_TRANSACTION); txEvent.transaction = transaction.tx_json; txEvent.transaction.hash = transaction.hash(); rippledConnection.sendJSON(txEvent); @@ -766,4 +768,130 @@ describe('TransactionManager', function() { done(); }); }); + + it('Submit transaction -- disabled resubmission', function(done) { + const transaction = remote.createTransaction('AccountSet', { + account: ACCOUNT.address + }); + + transaction.setResubmittable(false); + + let receivedSubmitted = false; + let 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.closeLedger(); + + setImmediate(function() { + req.sendJSON(lodash.extend({}, LEDGER, { + ledger_index: transaction.tx_json.LastLedgerSequence + 1 + })); + }); + }); + + 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); + + const summary = transaction.summary(); + assert.strictEqual(summary.submissionAttempts, 1); + assert.strictEqual(summary.submitIndex, 2); + assert.strictEqual(summary.initialSubmitIndex, 2); + assert.strictEqual(summary.lastLedgerSequence, 5); + assert.strictEqual(summary.state, 'failed'); + assert.strictEqual(summary.finalized, true); + assert.deepEqual(summary.result, { + engine_result: SUBMIT_TEL_RESPONSE.result.engine_result, + engine_result_message: SUBMIT_TEL_RESPONSE.result.engine_result_message, + ledger_hash: undefined, + ledger_index: undefined, + transaction_hash: SUBMIT_TEL_RESPONSE.result.tx_json.hash + }); + done(); + }); + }); + + it('Submit transaction -- disabled resubmission -- too busy error', function(done) { + // Transactions should always be resubmitted in the event of a 'tooBusy' + // rippled response, even with transaction resubmission disabled + + const transaction = remote.createTransaction('AccountSet', { + account: ACCOUNT.address + }); + + transaction.setResubmittable(false); + + let receivedSubmitted = false; + let receivedResubmitted = false; + transaction.once('proposed', function() { + assert(false, 'Should not receive proposed event'); + }); + transaction.once('submitted', function() { + 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_TOO_BUSY_ERROR, {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); + + const summary = transaction.summary(); + assert.strictEqual(summary.submissionAttempts, 2); + assert.strictEqual(summary.submitIndex, 3); + assert.strictEqual(summary.initialSubmitIndex, 2); + assert.strictEqual(summary.lastLedgerSequence, 5); + assert.strictEqual(summary.state, 'failed'); + assert.strictEqual(summary.finalized, true); + assert.deepEqual(summary.result, { + engine_result: undefined, + engine_result_message: undefined, + ledger_hash: undefined, + ledger_index: undefined, + transaction_hash: undefined + }); + done(); + }); + }); }); diff --git a/test/transaction-test.js b/test/transaction-test.js index 35ea73ea..c05e58df 100644 --- a/test/transaction-test.js +++ b/test/transaction-test.js @@ -1008,6 +1008,18 @@ describe('Transaction', function() { assert.strictEqual(transaction.tx_json.Fee, '1000'); }); + it('Set resubmittable', function() { + const tx = new Transaction(); + + assert.strictEqual(tx.isResubmittable(), true); + + tx.setResubmittable(false); + assert.strictEqual(tx.isResubmittable(), false); + + tx.setResubmittable(true); + assert.strictEqual(tx.isResubmittable(), true); + }); + it('Rewrite transaction path', function() { const path = [ {