Files
rippled/test/robust-transaction-test.js
2014-02-03 10:42:34 -08:00

445 lines
12 KiB
JavaScript

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();
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;
self.sent_transactions = [ ];
var steps = [
function (callback) {
self.what = 'Create accounts';
testutils.create_accounts($.remote, 'root', '20000.0', [ 'alice', 'bob' ], callback);
},
function (callback) {
self.what = 'Set RequireDestTag';
var tx = $.remote.transaction().account_set('alice');
tx.set_flags('RequireDestTag');
tx.once('submitted', function(m) {
assert.strictEqual('tesSUCCESS', m.engine_result);
});
tx.submit(callback);
},
function (callback) {
$.remote.getAccount('root').on('transaction-outbound', function(tx) {
if (tx.transaction.TransactionType === 'Payment' && tx.transaction.Amount === '1000000') {
self.sent_transactions.push(tx.transaction);
}
});
setTimeout(callback, 100);
},
function (callback) {
self.what = 'Send transaction without a destination tag';
var tx = $.remote.transaction().payment({
from: 'root',
to: 'alice',
amount: Amount.from_human('1XRP')
});
tx.once('submitted', function(m) {
assert.strictEqual('tefDST_TAG_NEEDED', m.engine_result);
});
tx.submit();
//Invoke callback immediately
callback();
},
function (callback) {
self.what = 'Send normal transaction which should succeed';
var tx = $.remote.transaction().payment({
from: 'root',
to: 'bob',
amount: Amount.from_human('1XRP')
});
//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('resubmitted', function() {
self.resubmitted = true;
});
tx.submit(callback);
},
function (callback) {
self.what = 'Send another normal transaction which should succeed';
var tx = $.remote.transaction().payment({
from: 'root',
to: 'bob',
amount: Amount.from_human('1XRP')
});
tx.once('submitted', function(m) {
assert.strictEqual(m.engine_result, 'tesSUCCESS');
});
tx.submit(callback);
},
function (callback) {
setTimeout(callback, 1000 * 2);
},
function checkPending(callback) {
assert.strictEqual($.remote.getAccount('root')._transactionManager._pending.length(), 0, 'Pending transactions persisting');
callback();
},
function (callback) {
testutils.verify_balance($.remote, 'bob', '20002000000', callback);
}
]
async.series(steps, function(err) {
assert(!err, self.what + ': ' + err);
assert(self.resubmitted, 'Transaction failed to resubmit');
assert(self.sent_transactions.length, 'No payments sent');
assert.strictEqual(self.sent_transactions.length, 2, 'Payment submitted more than once');
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;
self.sent_transactions = [ ];
var steps = [
function (callback) {
self.what = 'Create accounts';
testutils.create_accounts($.remote, 'root', '20000.0', [ 'alice' ], callback);
},
function (callback) {
$.remote.getAccount('root').on('transaction-outbound', function(tx) {
if (tx.transaction.TransactionType === 'Payment' && tx.transaction.Amount === '1000000') {
self.sent_transactions.push(tx.transaction);
}
});
setTimeout(callback, 100);
},
function (callback) {
self.what = 'Submit a transaction';
var tx = $.remote.transaction().payment({
from: 'root',
to: 'alice',
amount: Amount.from_human('1XRP')
});
tx.on('submitted', function() {
$.remote.ledger_accept();
});
tx.submit();
process.nextTick(function() {
$.remote.disconnect();
setTimeout(function() {
assert(!$.remote._connected);
tx.once('error', callback);
tx.once('success', function() { callback(); });
$.remote.connect();
}, 450);
});
},
function checkPending(callback) {
assert.strictEqual($.remote.getAccount('root')._transactionManager._pending.length(), 0, 'Pending transactions persisting');
callback();
},
function (callback) {
setTimeout(callback, 1000 * 2);
},
function (callback) {
testutils.verify_balance($.remote, 'alice', '20001000000', callback);
}
]
async.series(steps, function(err) {
assert(!err, self.what + ': ' + err);
assert(self.sent_transactions.length, 'Payment did not send');
assert.strictEqual(self.sent_transactions.length, 1, 'Payment sent more than once');
done();
});
});
test('temporary server disconnection -- reconnect after max ledger wait', function(done) {
var self = this;
self.sent_transactions = [ ];
var steps = [
function (callback) {
self.what = 'Create accounts';
testutils.create_accounts($.remote, 'root', '20000.0', [ 'alice' ], callback);
},
function (callback) {
$.remote.getAccount('root').on('transaction-outbound', function(tx) {
if (tx.transaction.TransactionType === 'Payment' && tx.transaction.Amount === '1000000') {
self.sent_transactions.push(tx.transaction);
}
});
callback();
},
function (callback) {
self.what = 'Submit a transaction';
var tx = $.remote.transaction().payment({
from: 'root',
to: 'alice',
amount: Amount.from_human('1XRP')
});
tx.once('submitted', function(m) {
assert.strictEqual(m.engine_result, 'tesSUCCESS');
process.nextTick(function() {
$.remote.disconnect();
});
setTimeout(function() {
assert(!$.remote._connected);
tx.once('error', callback);
tx.once('success', function() { callback(); });
$.remote.connect();
}, 2000);
});
tx.submit();
},
function checkPending(callback) {
assert.strictEqual($.remote.getAccount('root')._transactionManager._pending.length(), 0, 'Pending transactions persisting');
callback();
},
function (callback) {
setTimeout(callback, 1000 * 2);
},
function (callback) {
testutils.verify_balance($.remote, 'alice', '20001000000', callback);
}
]
async.series(steps, function(err) {
assert(!err, self.what + ': ' + err);
assert(self.sent_transactions.length, 'Payment did not send');
assert.strictEqual(self.sent_transactions.length, 1, 'Payment sent more than once');
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;
$.remote.local_signing = true;
self.sent_transactions = [ ];
var steps = [
function (callback) {
self.what = 'Create accounts';
testutils.create_accounts($.remote, 'root', '20000.0', [ 'alice' ], callback);
},
function (callback) {
$.remote.getAccount('root').on('transaction-outbound', function(tx) {
if (tx.transaction.TransactionType === 'Payment' && tx.transaction.Amount === '1000000') {
self.sent_transactions.push(tx.transaction);
}
});
setTimeout(callback, 100);
},
function (callback) {
self.what = 'Submit a transaction whose submit request times out';
var tx = $.remote.transaction().payment({
from: 'root',
to: 'alice',
amount: Amount.from_human('1XRP')
});
$.remote.getAccount('root')._transactionManager._submissionTimeout = 0.00001;
// A response from transaction submission should never
// actually be received
tx.once('timeout', function() { self.timed_out = true; });
tx.submit(callback);
},
function (callback) {
setTimeout(callback, 1000 * 2);
},
function checkPending(callback) {
assert.strictEqual($.remote.getAccount('root')._transactionManager._pending.length(), 0, 'Pending transactions persisting');
callback();
},
function (callback) {
testutils.verify_balance($.remote, 'alice', '20001000000', callback);
}
]
async.series(steps, function(err) {
assert(!err, self.what + ': ' + err);
assert.strictEqual(self.sent_transactions.length, 1, 'Payment sent more than once');
//assert(self.timed_out, 'Transaction submission failed to time out');
assert(self.sent_transactions.length, 'Payment was not sent');
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 (callback) {
self.what = 'Subscribe to accounts_proposed';
$.remote.requestSubscribe()
.addAccountProposed('root')
.callback(callback);
},
function (callback) {
self.what = 'Create accounts';
var payment = $.remote.transaction().payment({
from: 'root',
to: 'alice',
amount: Amount.from_human('20000XRP')
});
payment.submit(function(err, m) {
assert.ifError(err);
assert(m.engine_result, 'tesSUCCESS');
assert(m.validated, 'Transaction is finalized with invalidated transaction stream response');
done();
});
}
]
async.series(series, function(err, m) {
assert(!err, self.what + ': ' + err);
done();
});
});
});