diff --git a/Builds/VisualStudio2015/RippleD.vcxproj b/Builds/VisualStudio2015/RippleD.vcxproj
index a739952c9..5efa6a773 100644
--- a/Builds/VisualStudio2015/RippleD.vcxproj
+++ b/Builds/VisualStudio2015/RippleD.vcxproj
@@ -3283,6 +3283,10 @@
True
True
+
+ True
+ True
+
True
True
diff --git a/Builds/VisualStudio2015/RippleD.vcxproj.filters b/Builds/VisualStudio2015/RippleD.vcxproj.filters
index 35c0ed33a..a7b487a98 100644
--- a/Builds/VisualStudio2015/RippleD.vcxproj.filters
+++ b/Builds/VisualStudio2015/RippleD.vcxproj.filters
@@ -3765,6 +3765,9 @@
ripple\rpc\tests
+
+ ripple\rpc\tests
+
ripple\rpc\tests
diff --git a/src/ripple/rpc/tests/RobustTransaction.test.cpp b/src/ripple/rpc/tests/RobustTransaction.test.cpp
new file mode 100644
index 000000000..86e7e3d80
--- /dev/null
+++ b/src/ripple/rpc/tests/RobustTransaction.test.cpp
@@ -0,0 +1,321 @@
+//------------------------------------------------------------------------------
+/*
+ This file is part of rippled: https://github.com/ripple/rippled
+ Copyright (c) 2012, 2013 Ripple Labs Inc.
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+//==============================================================================
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace ripple {
+namespace test {
+
+class RobustTransaction_test : public beast::unit_test::suite
+{
+public:
+ void
+ testSequenceRealignment()
+ {
+ using namespace std::chrono_literals;
+ using namespace jtx;
+ Env env(*this);
+ env.fund(XRP(10000), "alice", "bob");
+ env.close();
+ auto wsc = makeWSClient(env.app().config());
+
+ {
+ // RPC subscribe to transactions stream
+ Json::Value jv;
+ jv[jss::streams] = Json::arrayValue;
+ jv[jss::streams].append("transactions");
+ jv = wsc->invoke("subscribe", jv);
+ expect(jv[jss::status] == "success");
+ }
+
+ {
+ // Submit past ledger sequence transaction
+ Json::Value payment;
+ payment[jss::secret] = toBase58(generateSeed("alice"));
+ payment[jss::tx_json] = pay("alice", "bob", XRP(1));
+ payment[jss::tx_json][sfLastLedgerSequence.fieldName] = 1;
+ auto jv = wsc->invoke("submit", payment);
+ expect(jv[jss::result][jss::engine_result] ==
+ "tefMAX_LEDGER");
+
+ // Submit past sequence transaction
+ payment[jss::tx_json] = pay("alice", "bob", XRP(1));
+ payment[jss::tx_json][sfSequence.fieldName] =
+ env.seq("alice") - 1;
+ jv = wsc->invoke("submit", payment);
+ expect(jv[jss::result][jss::engine_result] ==
+ "tefPAST_SEQ");
+
+ // Submit future sequence transaction
+ payment[jss::tx_json][sfSequence.fieldName] =
+ env.seq("alice") + 1;
+ jv = wsc->invoke("submit", payment);
+ expect(jv[jss::result][jss::engine_result] ==
+ "terPRE_SEQ");
+
+ // Submit transaction to bridge the sequence gap
+ payment[jss::tx_json][sfSequence.fieldName] =
+ env.seq("alice");
+ jv = wsc->invoke("submit", payment);
+ expect(jv[jss::result][jss::engine_result] ==
+ "tesSUCCESS");
+
+ // Wait for the jobqueue to process everything
+ env.app().getJobQueue().rendezvous();
+
+ // Finalize transactions
+ jv = wsc->invoke("ledger_accept");
+ expect(jv[jss::result].isMember(
+ jss::ledger_current_index));
+ }
+
+ {
+ // Check balances
+ expect(wsc->findMsg(5s,
+ [&](auto const& jv)
+ {
+ auto const& ff = jv[jss::meta]["AffectedNodes"]
+ [1u]["ModifiedNode"]["FinalFields"];
+ return ff[jss::Account] == Account("bob").human() &&
+ ff["Balance"] == "10001000000";
+ }));
+
+ expect(wsc->findMsg(5s,
+ [&](auto const& jv)
+ {
+ auto const& ff = jv[jss::meta]["AffectedNodes"]
+ [1u]["ModifiedNode"]["FinalFields"];
+ return ff[jss::Account] == Account("bob").human() &&
+ ff["Balance"] == "10002000000";
+ }));
+ }
+ }
+
+ /*
+ Submit a normal payment. Client disconnects after the proposed
+ transaction result is received.
+
+ Client reconnects in the future. During this time it is presumed that the
+ transaction should have succeeded.
+
+ Upon reconnection, recent account transaction history is loaded.
+ The submitted transaction should be detected, and the transaction should
+ ultimately succeed.
+ */
+ void
+ testReconnect()
+ {
+ using namespace jtx;
+ Env env(*this);
+ env.fund(XRP(10000), "alice", "bob");
+ env.close();
+ auto wsc = makeWSClient(env.app().config());
+
+ {
+ // Submit normal payment
+ Json::Value jv;
+ jv[jss::secret] = toBase58(generateSeed("alice"));
+ jv[jss::tx_json] = pay("alice", "bob", XRP(1));
+ jv = wsc->invoke("submit", jv);
+ expect(jv[jss::result][jss::engine_result] ==
+ "tesSUCCESS");
+
+ // Disconnect
+ wsc.reset();
+
+ // Server finalizes transaction
+ env.close();
+ }
+
+ {
+ // RPC account_tx
+ Json::Value jv;
+ jv[jss::account] = Account("bob").human();
+ jv[jss::ledger_index_min] = -1;
+ jv[jss::ledger_index_max] = -1;
+ wsc = makeWSClient(env.app().config());
+ jv = wsc->invoke("account_tx", jv);
+
+ // Check balance
+ auto ff = jv[jss::result][jss::transactions][0u][jss::meta]
+ ["AffectedNodes"][1u]["ModifiedNode"]["FinalFields"];
+ expect(ff[jss::Account] ==
+ Account("bob").human());
+ expect(ff["Balance"] == "10001000000");
+ }
+ }
+
+ void
+ testReconnectAfterWait()
+ {
+ using namespace std::chrono_literals;
+ using namespace jtx;
+ Env env(*this);
+ env.fund(XRP(10000), "alice", "bob");
+ env.close();
+ auto wsc = makeWSClient(env.app().config());
+
+ {
+ // Submit normal payment
+ Json::Value jv;
+ jv[jss::secret] = toBase58(generateSeed("alice"));
+ jv[jss::tx_json] = pay("alice", "bob", XRP(1));
+ jv = wsc->invoke("submit", jv);
+ expect(jv[jss::result][jss::engine_result] ==
+ "tesSUCCESS");
+
+ // Finalize transaction
+ jv = wsc->invoke("ledger_accept");
+ expect(jv[jss::result].isMember(
+ jss::ledger_current_index));
+
+ // Wait for the jobqueue to process everything
+ env.app().getJobQueue().rendezvous();
+ }
+
+ {
+ // RPC subscribe to ledger stream
+ Json::Value jv;
+ jv[jss::streams] = Json::arrayValue;
+ jv[jss::streams].append("ledger");
+ jv = wsc->invoke("subscribe", jv);
+ expect(jv[jss::status] == "success");
+
+ // Close ledgers
+ for(auto i = 0; i < 8; ++i)
+ {
+ expect(wsc->invoke("ledger_accept")[jss::result].
+ isMember(jss::ledger_current_index));
+
+ // Wait for the jobqueue to process everything
+ env.app().getJobQueue().rendezvous();
+
+ expect(wsc->findMsg(5s,
+ [&](auto const& jv)
+ {
+ return jv[jss::type] == "ledgerClosed";
+ }));
+ }
+ }
+
+ {
+ // Disconnect, reconnect
+ wsc = makeWSClient(env.app().config());
+
+ // RPC subscribe to ledger stream
+ Json::Value jv;
+ jv[jss::streams] = Json::arrayValue;
+ jv[jss::streams].append("ledger");
+ jv = wsc->invoke("subscribe", jv);
+ expect(jv[jss::status] == "success");
+
+ // Close ledgers
+ for (auto i = 0; i < 2; ++i)
+ {
+ expect(wsc->invoke("ledger_accept")[jss::result].
+ isMember(jss::ledger_current_index));
+
+ // Wait for the jobqueue to process everything
+ env.app().getJobQueue().rendezvous();
+
+ expect(wsc->findMsg(5s,
+ [&](auto const& jv)
+ {
+ return jv[jss::type] == "ledgerClosed";
+ }));
+ }
+ }
+
+ {
+ // RPC account_tx
+ Json::Value jv;
+ jv[jss::account] = Account("bob").human();
+ jv[jss::ledger_index_min] = -1;
+ jv[jss::ledger_index_max] = -1;
+ wsc = makeWSClient(env.app().config());
+ jv = wsc->invoke("account_tx", jv);
+
+ // Check balance
+ auto ff = jv[jss::result][jss::transactions][0u][jss::meta]
+ ["AffectedNodes"][1u]["ModifiedNode"]["FinalFields"];
+ expect(ff[jss::Account] ==
+ Account("bob").human());
+ expect(ff["Balance"] == "10001000000");
+ }
+ }
+
+ void
+ testAccountsProposed()
+ {
+ using namespace std::chrono_literals;
+ using namespace jtx;
+ Env env(*this);
+ env.fund(XRP(10000), "alice");
+ env.close();
+ auto wsc = makeWSClient(env.app().config());
+
+ {
+ // RPC subscribe to accounts_proposed stream
+ Json::Value jv;
+ jv[jss::accounts_proposed] = Json::arrayValue;
+ jv[jss::accounts_proposed].append(
+ Account("alice").human());
+ jv = wsc->invoke("subscribe", jv);
+ expect(jv[jss::status] == "success");
+ }
+
+ {
+ // Submit account_set transaction
+ Json::Value jv;
+ jv[jss::secret] = toBase58(generateSeed("alice"));
+ jv[jss::tx_json] = fset("alice", 0);
+ jv[jss::tx_json][jss::Fee] = 10;
+ jv = wsc->invoke("submit", jv);
+ expect(jv[jss::result][jss::engine_result] ==
+ "tesSUCCESS");
+ }
+
+ {
+ // Check stream update
+ expect(wsc->findMsg(5s,
+ [&](auto const& jv)
+ {
+ return jv[jss::transaction][jss::TransactionType] ==
+ "AccountSet";
+ }));
+ }
+ }
+
+ void
+ run() override
+ {
+ testSequenceRealignment();
+ testReconnect();
+ testReconnectAfterWait();
+ testAccountsProposed();
+ }
+};
+
+BEAST_DEFINE_TESTSUITE(RobustTransaction,app,ripple);
+
+} // test
+} // ripple
diff --git a/src/ripple/unity/rpcx.cpp b/src/ripple/unity/rpcx.cpp
index e226ce3d6..8d6ef5e90 100644
--- a/src/ripple/unity/rpcx.cpp
+++ b/src/ripple/unity/rpcx.cpp
@@ -105,5 +105,6 @@
#include
#include
#include
+#include
#include
#include
diff --git a/test/robust-transaction-test.js b/test/robust-transaction-test.js
deleted file mode 100644
index a5824dbee..000000000
--- a/test/robust-transaction-test.js
+++ /dev/null
@@ -1,463 +0,0 @@
-var async = require('async');
-var assert = require('assert');
-var ripple = require('ripple-lib');
-var Amount = require('ripple-lib').Amount;
-var Remote = require('ripple-lib').Remote;
-var Transaction = require('ripple-lib').Transaction;
-var Server = require('./server').Server;
-var testutils = require('./testutils');
-var config = testutils.init_config();
-
-var make_suite = process.env.CI ? suite.skip : suite;
-make_suite('Robust transaction submission', function() {
- var $ = { };
-
- setup(function(done) {
- testutils.build_setup().call($, function() {
- $.remote.local_signing = true;
-
- $.remote.request_subscribe()
- .accounts($.remote.account('root')._account_id)
- .callback(done);
- });
- });
-
- teardown(function(done) {
- testutils.build_teardown().call($, done);
- });
-
- // Payment is submitted (without a destination tag)
- // to a destination which requires one.
- //
- // The sequence is now in the future.
- //
- // Immediately subsequent transactions should err
- // with terPRE_SEQ.
- //
- // Gaps in the sequence should be filled with an
- // empty transaction.
- //
- // Transaction should ultimately succeed.
- //
- // Subsequent transactions should be submitted with
- // an up-to-date transction sequence. i.e. the
- // internal sequence should always catch up.
-
- test('sequence realignment', function(done) {
- var self = this;
-
- var steps = [
-
- function createAccounts(callback) {
- self.what = 'Create accounts';
- testutils.create_accounts($.remote, 'root', '20000.0', [ 'alice', 'bob' ], callback);
- },
-
- function sendInvalidTransaction(callback) {
- self.what = 'Send transaction without a destination tag';
-
- var tx = $.remote.transaction().payment({
- from: 'root',
- to: 'alice',
- amount: Amount.from_human('1 XRP')
- });
-
- tx.once('submitted', function(m) {
- assert.strictEqual('tefMAX_LEDGER', m.engine_result);
- });
- tx.once('error', function(m) {
- assert.strictEqual('tejMaxLedger', m.engine_result);
- });
-
- // Standalone mode starts with the open ledger as 3, so there's no way
- // for this to be anything other than tefMAX_LEDGER.
- tx.lastLedger(1);
- tx.submit();
-
- //Invoke callback immediately
- callback(null, tx);
- },
-
- function sendValidTransaction(previousTx, callback) {
- self.what = 'Send normal transaction which should succeed';
-
- var tx = $.remote.transaction().payment({
- from: 'root',
- to: 'bob',
- amount: Amount.from_human('1 XRP')
- });
-
- tx.on('submitted', function(m) {
- //console.log('Submitted', m);
- });
-
- tx.once('resubmitted', function() {
- self.resubmitted = true;
- });
-
- //First attempt at submission should result in
- //terPRE_SEQ as the sequence is still in the future
- tx.once('submitted', function(m) {
- assert.strictEqual('terPRE_SEQ', m.engine_result);
- });
- tx.once('final', function() {
- assert(previousTx.finalized,
- "Expected lastLedger 1 transaction to be finalized");
- callback();
- });
-
- tx.submit();
-
- testutils.ledger_wait($.remote, tx);
- },
-
- function checkPending(callback) {
- self.what = 'Check pending';
- var pending = $.remote.getAccount('root')._transactionManager._pending;
- assert.strictEqual(pending._queue.length, 0, 'Pending transactions persisting');
- callback();
- },
-
- function verifyBalance(callback) {
- self.what = 'Verify balance';
- testutils.verify_balance($.remote, 'bob', '20000999988', callback);
- }
-
- ]
-
- async.waterfall(steps, function(err) {
- assert(!err, self.what + ': ' + err);
- assert(self.resubmitted, 'Transaction failed to resubmit');
- done();
- });
- });
-
- // Submit a normal payment which should succeed.
- //
- // Remote disconnects immediately after submission
- // and before the validated transaction result is
- // received.
- //
- // Remote reconnects in the future. During this
- // time it is presumed that the transaction should
- // have succeeded, but an immediate response was
- // not possible, as the server was disconnected.
- //
- // Upon reconnection, recent account transaction
- // history is loaded.
- //
- // The submitted transaction should be detected,
- // and the transaction should ultimately succeed.
-
- test('temporary server disconnection', function(done) {
- var self = this;
-
- var steps = [
-
- function createAccounts(callback) {
- self.what = 'Create accounts';
- testutils.create_accounts($.remote, 'root', '20000.0', [ 'alice' ], callback);
- },
-
- function submitTransaction(callback) {
- self.what = 'Submit a transaction';
-
- var tx = $.remote.transaction().payment({
- from: 'root',
- to: 'alice',
- amount: Amount.from_human('1 XRP')
- });
-
- tx.submit();
-
- setImmediate(function() {
- $.remote.once('disconnect', function remoteDisconnected() {
- assert(!$.remote._connected);
-
- tx.once('final', function(m) {
- assert.strictEqual(m.engine_result, 'tesSUCCESS');
- callback();
- });
-
- $.remote.connect(function() {
- testutils.ledger_wait($.remote, tx);
- });
- });
-
- $.remote.disconnect();
- });
- },
-
- function waitLedger(callback) {
- self.what = 'Wait ledger';
- $.remote.once('ledger_closed', function() {
- callback();
- });
- $.remote.ledger_accept();
- },
-
- function checkPending(callback) {
- self.what = 'Check pending';
- var pending = $.remote.getAccount('root')._transactionManager._pending;
- assert.strictEqual(pending._queue.length, 0, 'Pending transactions persisting');
- callback();
- },
-
- function verifyBalance(callback) {
- self.what = 'Verify balance';
- testutils.verify_balance($.remote, 'alice', '20000999988', callback);
- }
-
- ]
-
- async.series(steps, function(err) {
- assert(!err, self.what + ': ' + err);
- done();
- });
- });
-
- test('temporary server disconnection -- reconnect after max ledger wait', function(done) {
- var self = this;
-
- var steps = [
-
- function createAccounts(callback) {
- self.what = 'Create accounts';
- testutils.create_accounts($.remote, 'root', '20000.0', [ 'alice' ], callback);
- },
-
- function waitLedgers(callback) {
- self.what = 'Wait ledger';
- $.remote.once('ledger_closed', function() {
- callback();
- });
- $.remote.ledger_accept();
- },
-
- function verifyBalance(callback) {
- self.what = 'Verify balance';
- testutils.verify_balance($.remote, 'alice', '19999999988', callback);
- },
-
- function submitTransaction(callback) {
- self.what = 'Submit a transaction';
-
- var tx = $.remote.transaction().payment({
- from: 'root',
- to: 'alice',
- amount: Amount.from_human('1 XRP')
- });
-
- tx.once('submitted', function(m) {
- assert.strictEqual(m.engine_result, 'tesSUCCESS');
-
- var handleMessage = $.remote._handleMessage;
- $.remote._handleMessage = function(){};
-
- var ledgers = 0;
-
- ;(function nextLedger() {
- if (++ledgers > 8) {
- tx.once('final', function() { callback(); });
- $.remote._handleMessage = handleMessage;
- $.remote.disconnect(function() {
- assert(!$.remote._connected);
- var pending = $.remote.getAccount('root')._transactionManager._pending;
- assert.strictEqual(pending._queue.length, 1, 'Pending transactions persisting');
- $.remote.connect();
- });
- } else {
- $.remote._getServer().once('ledger_closed', function() {
- setTimeout(nextLedger, 20);
- });
- $.remote.ledger_accept();
- }
- })();
- });
-
- tx.submit();
- },
-
- function waitLedgers(callback) {
- self.what = 'Wait ledgers';
-
- var ledgers = 0;
-
- ;(function nextLedger() {
- $.remote.once('ledger_closed', function() {
- if (++ledgers === 3) {
- callback();
- } else {
- setTimeout(nextLedger, process.env.CI ? 400 : 100 );
- }
- });
- $.remote.ledger_accept();
- })();
- },
-
- function checkPending(callback) {
- self.what = 'Check pending';
- var pending = $.remote.getAccount('root')._transactionManager._pending;
- assert.strictEqual(pending._queue.length, 0, 'Pending transactions persisting');
- callback();
- },
-
- function verifyBalance(callback) {
- self.what = 'Verify balance';
- testutils.verify_balance($.remote, 'alice', '20000999988', callback);
- }
-
- ]
-
- async.series(steps, function(err) {
- assert(!err, self.what + ': ' + err);
- done();
- });
- });
-
- // Submit request times out
- //
- // Since the transaction ID is generated locally, the
- // transaction should still validate from the account
- // transaction stream, even without a response to the
- // original submit request
-
- test('submission timeout', function(done) {
- var self = this;
-
- var steps = [
-
- function createAccounts(callback) {
- self.what = 'Create accounts';
- testutils.create_accounts($.remote, 'root', '20000.0', [ 'alice' ], callback);
- },
-
- function submitTransaction(callback) {
- self.what = 'Submit a transaction whose submit request times out';
-
- var tx = $.remote.transaction().payment({
- from: 'root',
- to: 'alice',
- amount: Amount.from_human('1 XRP')
- });
-
- var timed_out = false;
-
- $.remote.getAccount('root')._transactionManager._submissionTimeout = 0;
-
- // A response from transaction submission should never
- // actually be received
- tx.once('timeout', function() { timed_out = true; });
-
- tx.once('final', function(m) {
- assert(timed_out, 'Transaction submission failed to time out');
- assert.strictEqual(m.engine_result, 'tesSUCCESS');
- callback();
- });
-
- tx.submit();
-
- testutils.ledger_wait($.remote, tx);
- },
-
- function checkPending(callback) {
- self.what = 'Check pending';
- assert.strictEqual($.remote.getAccount('root')._transactionManager._pending.length(), 0, 'Pending transactions persisting');
- callback();
- },
-
- function verifyBalance(callback) {
- self.what = 'Verify balance';
- testutils.verify_balance($.remote, 'alice', '20000999988', callback);
- }
-
- ]
-
- async.series(steps, function(err) {
- assert(!err, self.what + ': ' + err);
- done();
- });
- });
-
- // Subscribing to accounts_proposed will result in ripple-lib
- // being streamed non-validated (proposed) transactions
- //
- // This test ensures that only validated transactions will
- // trigger a transaction success event
-
- test('subscribe to accounts_proposed', function(done) {
- var self = this;
-
- var series = [
-
- function subscribeToAccountsProposed(callback) {
- self.what = 'Subscribe to accounts_proposed';
-
- $.remote.requestSubscribe()
- .addAccountProposed('root')
- .callback(callback);
- },
-
- function submitTransaction(callback) {
- self.what = 'Submit a transaction';
-
- var tx = $.remote.transaction().accountSet('root');
-
- var receivedProposedTransaction = false;
-
- $.remote.on('transaction', function(tx) {
- if (tx.status === 'proposed') {
- receivedProposedTransaction = true;
- }
- });
-
- tx.submit(function(err, m) {
- assert(!err, err);
- assert(receivedProposedTransaction, 'Did not received proposed transaction from stream');
- assert(m.engine_result, 'tesSUCCESS');
- assert(m.validated, 'Transaction is finalized with invalidated transaction stream response');
- done();
- });
-
- testutils.ledger_wait($.remote, tx);
- }
-
- ]
-
- async.series(series, function(err, m) {
- assert(!err, self.what + ': ' + err);
- done();
- });
- });
-
- // Validate that LastLedgerSequence works
- test('set LastLedgerSequence', function(done) {
- var self = this;
-
- var series = [
-
- function createAccounts(callback) {
- self.what = 'Create accounts';
- testutils.create_accounts($.remote, 'root', '20000.0', [ 'alice' ], callback);
- },
-
- function submitTransaction(callback) {
- var tx = $.remote.transaction().payment('root', 'alice', '1');
- tx.lastLedger(0);
-
- tx.once('submitted', function(m) {
- assert.strictEqual(m.engine_result, 'tefMAX_LEDGER');
- callback();
- });
-
- tx.submit();
- }
-
- ]
-
- async.series(series, function(err) {
- assert(!err, self.what);
- done();
- });
- });
-});