Files
rippled/src/test/app/Batch_test.cpp
2026-02-12 11:17:34 +00:00

4335 lines
166 KiB
C++

#include <test/jtx.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/utility.h>
#include <xrpld/app/misc/HashRouter.h>
#include <xrpld/app/misc/NetworkOPs.h>
#include <xrpld/app/misc/Transaction.h>
#include <xrpld/app/tx/apply.h>
#include <xrpld/app/tx/detail/Batch.h>
#include <xrpl/protocol/Batch.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/Sign.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
namespace xrpl {
namespace test {
class Batch_test : public beast::unit_test::suite
{
struct TestLedgerData
{
int index;
std::string txType;
std::string result;
std::string txHash;
std::optional<std::string> batchID;
};
struct TestBatchData
{
std::string result;
std::string txHash;
};
Json::Value
getTxByIndex(Json::Value const& jrr, int const index)
{
for (auto const& txn : jrr[jss::result][jss::ledger][jss::transactions])
{
if (txn[jss::metaData][sfTransactionIndex.jsonName] == index)
return txn;
}
return {};
}
Json::Value
getLastLedger(jtx::Env& env)
{
Json::Value params;
params[jss::ledger_index] = env.closed()->seq();
params[jss::transactions] = true;
params[jss::expand] = true;
return env.rpc("json", "ledger", to_string(params));
}
void
validateInnerTxn(jtx::Env& env, std::string const& batchID, TestLedgerData const& ledgerResult)
{
Json::Value const jrr = env.rpc("tx", ledgerResult.txHash)[jss::result];
BEAST_EXPECT(jrr[sfTransactionType.jsonName] == ledgerResult.txType);
BEAST_EXPECT(jrr[jss::meta][sfTransactionResult.jsonName] == ledgerResult.result);
BEAST_EXPECT(jrr[jss::meta][sfParentBatchID.jsonName] == batchID);
}
void
validateClosedLedger(jtx::Env& env, std::vector<TestLedgerData> const& ledgerResults)
{
auto const jrr = getLastLedger(env);
auto const transactions = jrr[jss::result][jss::ledger][jss::transactions];
BEAST_EXPECT(transactions.size() == ledgerResults.size());
for (TestLedgerData const& ledgerResult : ledgerResults)
{
auto const txn = getTxByIndex(jrr, ledgerResult.index);
BEAST_EXPECT(txn[jss::hash].asString() == ledgerResult.txHash);
BEAST_EXPECT(txn.isMember(jss::metaData));
Json::Value const meta = txn[jss::metaData];
BEAST_EXPECT(txn[sfTransactionType.jsonName] == ledgerResult.txType);
BEAST_EXPECT(meta[sfTransactionResult.jsonName] == ledgerResult.result);
if (ledgerResult.batchID)
validateInnerTxn(env, *ledgerResult.batchID, ledgerResult);
}
}
template <typename... Args>
std::pair<std::vector<std::string>, std::string>
submitBatch(jtx::Env& env, TER const& result, Args&&... args)
{
auto batchTxn = env.jt(std::forward<Args>(args)...);
env(batchTxn, jtx::ter(result));
auto const ids = batchTxn.stx->getBatchTransactionIDs();
std::vector<std::string> txIDs;
for (auto const& id : ids)
txIDs.push_back(strHex(id));
TxID const batchID = batchTxn.stx->getTransactionID();
return std::make_pair(txIDs, strHex(batchID));
}
static uint256
getCheckIndex(AccountID const& account, std::uint32_t uSequence)
{
return keylet::check(account, uSequence).key;
}
static std::unique_ptr<Config>
makeSmallQueueConfig(
std::map<std::string, std::string> extraTxQ = {},
std::map<std::string, std::string> extraVoting = {})
{
auto p = test::jtx::envconfig();
auto& section = p->section("transaction_queue");
section.set("ledgers_in_queue", "2");
section.set("minimum_queue_size", "2");
section.set("min_ledgers_to_compute_size_limit", "3");
section.set("max_ledger_counts_to_store", "100");
section.set("retry_sequence_percent", "25");
section.set("normal_consensus_increase_percent", "0");
for (auto const& [k, v] : extraTxQ)
section.set(k, v);
return p;
}
auto
openLedgerFee(jtx::Env& env, XRPAmount const& batchFee)
{
using namespace jtx;
auto const& view = *env.current();
auto metrics = env.app().getTxQ().getMetrics(view);
return toDrops(metrics.openLedgerFeeLevel, batchFee) + 1;
}
void
testEnable(FeatureBitset features)
{
using namespace test::jtx;
using namespace std::literals;
bool const withInnerSigFix = features[fixBatchInnerSigs];
for (bool const withBatch : {true, false})
{
testcase << "enabled: Batch " << (withBatch ? "enabled" : "disabled")
<< ", Inner Sig Fix: " << (withInnerSigFix ? "enabled" : "disabled");
auto const amend = withBatch ? features : features - featureBatch;
test::jtx::Env env{*this, amend};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
env.fund(XRP(10000), alice, bob, carol);
env.close();
// ttBatch
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const txResult = withBatch ? ter(tesSUCCESS) : ter(temDISABLED);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(1)), seq + 1),
batch::inner(pay(alice, bob, XRP(1)), seq + 2),
txResult);
env.close();
}
// tfInnerBatchTxn
// If the feature is disabled, the transaction fails with
// temINVALID_FLAG. If the feature is enabled, the transaction fails
// early in checkValidity()
{
auto const txResult = withBatch ? ter(telENV_RPC_FAILED) : ter(temINVALID_FLAG);
env(pay(alice, bob, XRP(1)), txflags(tfInnerBatchTxn), txResult);
env.close();
}
env.close();
}
}
void
testPreflight(FeatureBitset features)
{
testcase("preflight");
using namespace test::jtx;
using namespace std::literals;
//----------------------------------------------------------------------
// preflight
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
env.fund(XRP(10000), alice, bob, carol);
env.close();
// temBAD_FEE: preflight1
{
env(batch::outer(alice, env.seq(alice), XRP(-1), tfAllOrNothing), ter(temBAD_FEE));
env.close();
}
// DEFENSIVE: temINVALID_FLAG: Batch: inner batch flag.
// ACTUAL: telENV_RPC_FAILED: checkValidity()
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 0);
env(batch::outer(alice, seq, batchFee, tfInnerBatchTxn), ter(telENV_RPC_FAILED));
env.close();
}
// temINVALID_FLAG: Batch: invalid flags.
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 0);
env(batch::outer(alice, seq, batchFee, tfDisallowXRP), ter(temINVALID_FLAG));
env.close();
}
// temINVALID_FLAG: Batch: too many flags.
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 0);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
txflags(tfAllOrNothing | tfOnlyOne),
ter(temINVALID_FLAG));
env.close();
}
// temARRAY_EMPTY: Batch: txns array must have at least 2 entries.
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 0);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing), ter(temARRAY_EMPTY));
env.close();
}
// temARRAY_EMPTY: Batch: txns array must have at least 2 entries.
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 0);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(1)), seq + 1),
ter(temARRAY_EMPTY));
env.close();
}
// DEFENSIVE: temARRAY_TOO_LARGE: Batch: txns array exceeds 8 entries.
// ACTUAL: telENV_RPC_FAILED: isRawTransactionOkay()
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 9);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(1)), seq + 1),
batch::inner(pay(alice, bob, XRP(1)), seq + 2),
batch::inner(pay(alice, bob, XRP(1)), seq + 3),
batch::inner(pay(alice, bob, XRP(1)), seq + 4),
batch::inner(pay(alice, bob, XRP(1)), seq + 5),
batch::inner(pay(alice, bob, XRP(1)), seq + 6),
batch::inner(pay(alice, bob, XRP(1)), seq + 7),
batch::inner(pay(alice, bob, XRP(1)), seq + 8),
batch::inner(pay(alice, bob, XRP(1)), seq + 9),
ter(telENV_RPC_FAILED));
env.close();
}
// temREDUNDANT: Batch: duplicate Txn found.
{
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const seq = env.seq(alice);
auto jt = env.jtnofill(
batch::outer(alice, env.seq(alice), batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), seq + 1),
batch::inner(pay(alice, bob, XRP(10)), seq + 1));
env(jt.jv, batch::sig(bob), ter(temREDUNDANT));
env.close();
}
// DEFENSIVE: temINVALID: Batch: batch cannot have inner batch txn.
// ACTUAL: telENV_RPC_FAILED: isRawTransactionOkay()
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(batch::outer(alice, seq, batchFee, tfAllOrNothing), seq),
batch::inner(pay(alice, bob, XRP(1)), seq + 2),
ter(telENV_RPC_FAILED));
env.close();
}
// temINVALID_FLAG: Batch: inner txn must have the
// tfInnerBatchTxn flag.
{
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const seq = env.seq(alice);
auto tx1 = batch::inner(pay(alice, bob, XRP(10)), seq + 1);
tx1[jss::Flags] = 0;
auto jt = env.jtnofill(
batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx1,
batch::inner(pay(alice, bob, XRP(10)), seq + 2));
env(jt.jv, batch::sig(bob), ter(temINVALID_FLAG));
env.close();
}
// temBAD_SIGNATURE: Batch: inner txn cannot include TxnSignature.
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto jt = env.jt(pay(alice, bob, XRP(1)));
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(jt.jv, seq + 1),
batch::inner(pay(alice, bob, XRP(1)), seq + 2),
ter(temBAD_SIGNATURE));
env.close();
}
// temBAD_SIGNER: Batch: inner txn cannot include Signers.
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto tx1 = pay(alice, bob, XRP(1));
tx1[sfSigners.jsonName] = Json::arrayValue;
tx1[sfSigners.jsonName][0U][sfSigner.jsonName] = Json::objectValue;
tx1[sfSigners.jsonName][0U][sfSigner.jsonName][sfAccount.jsonName] = alice.human();
tx1[sfSigners.jsonName][0U][sfSigner.jsonName][sfSigningPubKey.jsonName] = strHex(alice.pk());
tx1[sfSigners.jsonName][0U][sfSigner.jsonName][sfTxnSignature.jsonName] = "DEADBEEF";
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(tx1, seq + 1),
batch::inner(pay(alice, bob, XRP(1)), seq + 2),
ter(temBAD_SIGNER));
env.close();
}
// temBAD_REGKEY: Batch: inner txn must include empty
// SigningPubKey.
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto tx1 = batch::inner(pay(alice, bob, XRP(1)), seq + 1);
tx1[jss::SigningPubKey] = strHex(alice.pk());
auto jt = env.jtnofill(
batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx1,
batch::inner(pay(alice, bob, XRP(1)), seq + 2));
env(jt.jv, ter(temBAD_REGKEY));
env.close();
}
// temINVALID_INNER_BATCH: Batch: inner txn preflight failed.
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(1)), seq + 1),
// amount can't be negative
batch::inner(pay(alice, bob, XRP(-1)), seq + 2),
ter(temINVALID_INNER_BATCH));
env.close();
}
// temBAD_FEE: Batch: inner txn must have a fee of 0.
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto tx1 = batch::inner(pay(alice, bob, XRP(1)), seq + 1);
tx1[jss::Fee] = to_string(env.current()->fees().base);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx1,
batch::inner(pay(alice, bob, XRP(2)), seq + 2),
ter(temBAD_FEE));
env.close();
}
// temBAD_FEE: Inner txn with negative fee
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto tx1 = batch::inner(pay(alice, bob, XRP(1)), seq + 1);
tx1[jss::Fee] = "-1";
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx1,
batch::inner(pay(alice, bob, XRP(2)), seq + 2),
ter(temBAD_FEE));
env.close();
}
// temBAD_FEE: Inner txn with non-integer fee
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto tx1 = batch::inner(pay(alice, bob, XRP(1)), seq + 1);
tx1[jss::Fee] = "1.5";
env.set_parse_failure_expected(true);
try
{
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx1,
batch::inner(pay(alice, bob, XRP(2)), seq + 2));
fail("Expected parse_error for fractional fee");
}
catch (jtx::parse_error const&)
{
BEAST_EXPECT(true);
}
env.set_parse_failure_expected(false);
}
// temSEQ_AND_TICKET: Batch: inner txn cannot have both Sequence
// and TicketSequence.
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto tx1 = batch::inner(pay(alice, bob, XRP(1)), 0, 1);
tx1[jss::Sequence] = seq + 1;
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx1,
batch::inner(pay(alice, bob, XRP(2)), seq + 2),
ter(temSEQ_AND_TICKET));
env.close();
}
// temSEQ_AND_TICKET: Batch: inner txn must have either Sequence or
// TicketSequence.
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(1)), 0),
batch::inner(pay(alice, bob, XRP(2)), seq + 2),
ter(temSEQ_AND_TICKET));
env.close();
}
// temREDUNDANT: Batch: duplicate sequence found:
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(1)), seq + 1),
batch::inner(pay(alice, bob, XRP(2)), seq + 1),
ter(temREDUNDANT));
env.close();
}
// temREDUNDANT: Batch: duplicate ticket found:
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(1)), 0, seq + 1),
batch::inner(pay(alice, bob, XRP(2)), 0, seq + 1),
ter(temREDUNDANT));
env.close();
}
// temREDUNDANT: Batch: duplicate ticket & sequence found:
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(1)), 0, seq + 1),
batch::inner(pay(alice, bob, XRP(2)), seq + 1),
ter(temREDUNDANT));
env.close();
}
// DEFENSIVE: temARRAY_TOO_LARGE: Batch: signers array exceeds 8
// entries.
// ACTUAL: telENV_RPC_FAILED: isRawTransactionOkay()
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 9, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), seq + 1),
batch::inner(pay(alice, bob, XRP(5)), seq + 2),
batch::sig(bob, carol, alice, bob, carol, alice, bob, carol, alice, alice),
ter(telENV_RPC_FAILED));
env.close();
}
// temBAD_SIGNER: Batch: signer cannot be the outer account
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 2, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), seq + 1),
batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)),
batch::sig(alice, bob),
ter(temBAD_SIGNER));
env.close();
}
// temREDUNDANT: Batch: duplicate signer found
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 2, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), seq + 1),
batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)),
batch::sig(bob, bob),
ter(temREDUNDANT));
env.close();
}
// temBAD_SIGNER: Batch: no account signature for inner txn.
// Note: Extra signature by bob
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), seq + 1),
batch::inner(pay(alice, bob, XRP(5)), seq + 2),
batch::sig(bob),
ter(temBAD_SIGNER));
env.close();
}
// temBAD_SIGNER: Batch: no account signature for inner txn.
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), seq + 1),
batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)),
batch::sig(carol),
ter(temBAD_SIGNER));
env.close();
}
// temBAD_SIGNATURE: Batch: invalid batch txn signature.
{
auto const seq = env.seq(alice);
auto const bobSeq = env.seq(bob);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto jt = env.jtnofill(
batch::outer(alice, env.seq(alice), batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), seq + 1),
batch::inner(pay(bob, alice, XRP(5)), bobSeq));
Serializer msg;
serializeBatch(msg, tfAllOrNothing, jt.stx->getBatchTransactionIDs());
auto const sig = xrpl::sign(bob.pk(), bob.sk(), msg.slice());
jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName][sfAccount.jsonName] = bob.human();
jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName][sfSigningPubKey.jsonName] = strHex(alice.pk());
jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName][sfTxnSignature.jsonName] =
strHex(Slice{sig.data(), sig.size()});
env(jt.jv, ter(temBAD_SIGNATURE));
env.close();
}
// temBAD_SIGNER: Batch: invalid batch signers.
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 2, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), seq + 1),
batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)),
batch::inner(pay(carol, alice, XRP(5)), env.seq(carol)),
batch::sig(bob),
ter(temBAD_SIGNER));
env.close();
}
}
void
testPreclaim(FeatureBitset features)
{
testcase("preclaim");
using namespace test::jtx;
using namespace std::literals;
//----------------------------------------------------------------------
// preclaim
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
auto const dave = Account("dave");
auto const elsa = Account("elsa");
auto const frank = Account("frank");
auto const phantom = Account("phantom");
env.memoize(phantom);
env.fund(XRP(10000), alice, bob, carol, dave, elsa, frank);
env.close();
//----------------------------------------------------------------------
// checkSign.checkSingleSign
// tefBAD_AUTH: Bob is not authorized to sign for Alice
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 3, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), seq + 1),
batch::inner(pay(alice, bob, XRP(20)), seq + 2),
sig(bob),
ter(tefBAD_AUTH));
env.close();
}
//----------------------------------------------------------------------
// checkBatchSign.checkMultiSign
// tefNOT_MULTI_SIGNING: SignersList not enabled
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 3, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), seq + 1),
batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)),
batch::msig(bob, {dave, carol}),
ter(tefNOT_MULTI_SIGNING));
env.close();
}
env(signers(alice, 2, {{bob, 1}, {carol, 1}}));
env.close();
env(signers(bob, 2, {{carol, 1}, {dave, 1}, {elsa, 1}}));
env.close();
// tefBAD_SIGNATURE: Account not in SignersList
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 3, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), seq + 1),
batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)),
batch::msig(bob, {carol, frank}),
ter(tefBAD_SIGNATURE));
env.close();
}
// tefBAD_SIGNATURE: Wrong publicKey type
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 3, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), seq + 1),
batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)),
batch::msig(bob, {carol, Account("dave", KeyType::ed25519)}),
ter(tefBAD_SIGNATURE));
env.close();
}
// tefMASTER_DISABLED: Master key disabled
{
env(regkey(elsa, frank));
env(fset(elsa, asfDisableMaster), sig(elsa));
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 3, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), seq + 1),
batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)),
batch::msig(bob, {carol, elsa}),
ter(tefMASTER_DISABLED));
env.close();
}
// tefBAD_SIGNATURE: Signer does not exist
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 3, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), seq + 1),
batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)),
batch::msig(bob, {carol, phantom}),
ter(tefBAD_SIGNATURE));
env.close();
}
// tefBAD_SIGNATURE: Signer has not enabled RegularKey
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 3, 2);
Account const davo{"davo", KeyType::ed25519};
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), seq + 1),
batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)),
batch::msig(bob, {carol, Reg{dave, davo}}),
ter(tefBAD_SIGNATURE));
env.close();
}
// tefBAD_SIGNATURE: Wrong RegularKey Set
{
env(regkey(dave, frank));
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 3, 2);
Account const davo{"davo", KeyType::ed25519};
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), seq + 1),
batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)),
batch::msig(bob, {carol, Reg{dave, davo}}),
ter(tefBAD_SIGNATURE));
env.close();
}
// tefBAD_QUORUM
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 2, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), seq + 1),
batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)),
batch::msig(bob, {carol}),
ter(tefBAD_QUORUM));
env.close();
}
// tesSUCCESS: BatchSigners.Signers
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 3, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), seq + 1),
batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)),
batch::msig(bob, {carol, dave}),
ter(tesSUCCESS));
env.close();
}
// tesSUCCESS: Multisign + BatchSigners.Signers
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 4, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), seq + 1),
batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)),
batch::msig(bob, {carol, dave}),
msig(bob, carol),
ter(tesSUCCESS));
env.close();
}
//----------------------------------------------------------------------
// checkBatchSign.checkSingleSign
// tefBAD_AUTH: Inner Account is not signer
{
auto const ledSeq = env.current()->seq();
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, phantom, XRP(1000)), seq + 1),
batch::inner(noop(phantom), ledSeq),
batch::sig(Reg{phantom, carol}),
ter(tefBAD_AUTH));
env.close();
}
// tefBAD_AUTH: Account is not signer
{
auto const ledSeq = env.current()->seq();
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(1000)), seq + 1),
batch::inner(noop(bob), ledSeq),
batch::sig(Reg{bob, carol}),
ter(tefBAD_AUTH));
env.close();
}
// tesSUCCESS: Signed With Regular Key
{
env(regkey(bob, carol));
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(1)), seq + 1),
batch::inner(pay(bob, alice, XRP(2)), env.seq(bob)),
batch::sig(Reg{bob, carol}),
ter(tesSUCCESS));
env.close();
}
// tesSUCCESS: Signed With Master Key
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(1)), seq + 1),
batch::inner(pay(bob, alice, XRP(2)), env.seq(bob)),
batch::sig(bob),
ter(tesSUCCESS));
env.close();
}
// tefMASTER_DISABLED: Signed With Master Key Disabled
{
env(regkey(bob, carol));
env(fset(bob, asfDisableMaster), sig(bob));
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(1)), seq + 1),
batch::inner(pay(bob, alice, XRP(2)), env.seq(bob)),
batch::sig(bob),
ter(tefMASTER_DISABLED));
env.close();
}
}
void
testBadRawTxn(FeatureBitset features)
{
testcase("bad raw txn");
using namespace test::jtx;
using namespace std::literals;
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
// Invalid: sfTransactionType
{
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const seq = env.seq(alice);
auto tx1 = batch::inner(pay(alice, bob, XRP(10)), seq + 1);
tx1.removeMember(jss::TransactionType);
auto jt = env.jtnofill(
batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx1,
batch::inner(pay(alice, bob, XRP(10)), seq + 2));
env(jt.jv, batch::sig(bob), ter(telENV_RPC_FAILED));
env.close();
}
// Invalid: sfAccount
{
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const seq = env.seq(alice);
auto tx1 = batch::inner(pay(alice, bob, XRP(10)), seq + 1);
tx1.removeMember(jss::Account);
auto jt = env.jtnofill(
batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx1,
batch::inner(pay(alice, bob, XRP(10)), seq + 2));
env(jt.jv, batch::sig(bob), ter(telENV_RPC_FAILED));
env.close();
}
// Invalid: sfSequence
{
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const seq = env.seq(alice);
auto tx1 = batch::inner(pay(alice, bob, XRP(10)), seq + 1);
tx1.removeMember(jss::Sequence);
auto jt = env.jtnofill(
batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx1,
batch::inner(pay(alice, bob, XRP(10)), seq + 2));
env(jt.jv, batch::sig(bob), ter(telENV_RPC_FAILED));
env.close();
}
// Invalid: sfFee
{
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const seq = env.seq(alice);
auto tx1 = batch::inner(pay(alice, bob, XRP(10)), seq + 1);
tx1.removeMember(jss::Fee);
auto jt = env.jtnofill(
batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx1,
batch::inner(pay(alice, bob, XRP(10)), seq + 2));
env(jt.jv, batch::sig(bob), ter(telENV_RPC_FAILED));
env.close();
}
// Invalid: sfSigningPubKey
{
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const seq = env.seq(alice);
auto tx1 = batch::inner(pay(alice, bob, XRP(10)), seq + 1);
tx1.removeMember(jss::SigningPubKey);
auto jt = env.jtnofill(
batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx1,
batch::inner(pay(alice, bob, XRP(10)), seq + 2));
env(jt.jv, batch::sig(bob), ter(telENV_RPC_FAILED));
env.close();
}
}
void
testBadSequence(FeatureBitset features)
{
testcase("bad sequence");
using namespace test::jtx;
using namespace std::literals;
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
auto const USD = gw["USD"];
env.fund(XRP(10000), alice, bob, gw);
env.close();
env.trust(USD(1000), alice, bob);
env(pay(gw, alice, USD(100)));
env(pay(gw, bob, USD(100)));
env.close();
env(noop(bob), ter(tesSUCCESS));
env.close();
// Invalid: Alice Sequence is a past sequence
{
auto const preAliceSeq = env.seq(alice);
auto const preAlice = env.balance(alice);
auto const preAliceUSD = env.balance(alice, USD.issue());
auto const preBobSeq = env.seq(bob);
auto const preBob = env.balance(bob);
auto const preBobUSD = env.balance(bob, USD.issue());
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), preAliceSeq - 10),
batch::inner(pay(bob, alice, XRP(5)), preBobSeq),
batch::sig(bob));
env.close();
{
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
};
validateClosedLedger(env, testCases);
}
env.close();
{
// next ledger is empty
std::vector<TestLedgerData> testCases = {};
validateClosedLedger(env, testCases);
}
// Alice pays fee & Bob should not be affected.
BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1);
BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD);
BEAST_EXPECT(env.seq(bob) == preBobSeq);
BEAST_EXPECT(env.balance(bob) == preBob);
BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD);
}
// Invalid: Alice Sequence is a future sequence
{
auto const preAliceSeq = env.seq(alice);
auto const preAlice = env.balance(alice);
auto const preAliceUSD = env.balance(alice, USD.issue());
auto const preBobSeq = env.seq(bob);
auto const preBob = env.balance(bob);
auto const preBobUSD = env.balance(bob, USD.issue());
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), preAliceSeq + 10),
batch::inner(pay(bob, alice, XRP(5)), preBobSeq),
batch::sig(bob));
env.close();
{
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
};
validateClosedLedger(env, testCases);
}
env.close();
{
// next ledger is empty
std::vector<TestLedgerData> testCases = {};
validateClosedLedger(env, testCases);
}
// Alice pays fee & Bob should not be affected.
BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1);
BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD);
BEAST_EXPECT(env.seq(bob) == preBobSeq);
BEAST_EXPECT(env.balance(bob) == preBob);
BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD);
}
// Invalid: Bob Sequence is a past sequence
{
auto const preAliceSeq = env.seq(alice);
auto const preAlice = env.balance(alice);
auto const preAliceUSD = env.balance(alice, USD.issue());
auto const preBobSeq = env.seq(bob);
auto const preBob = env.balance(bob);
auto const preBobUSD = env.balance(bob, USD.issue());
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), preAliceSeq + 1),
batch::inner(pay(bob, alice, XRP(5)), preBobSeq - 10),
batch::sig(bob));
env.close();
{
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
};
validateClosedLedger(env, testCases);
}
env.close();
{
// next ledger is empty
std::vector<TestLedgerData> testCases = {};
validateClosedLedger(env, testCases);
}
// Alice pays fee & Bob should not be affected.
BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1);
BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD);
BEAST_EXPECT(env.seq(bob) == preBobSeq);
BEAST_EXPECT(env.balance(bob) == preBob);
BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD);
}
// Invalid: Bob Sequence is a future sequence
{
auto const preAliceSeq = env.seq(alice);
auto const preAlice = env.balance(alice);
auto const preAliceUSD = env.balance(alice, USD.issue());
auto const preBobSeq = env.seq(bob);
auto const preBob = env.balance(bob);
auto const preBobUSD = env.balance(bob, USD.issue());
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), preAliceSeq + 1),
batch::inner(pay(bob, alice, XRP(5)), preBobSeq + 10),
batch::sig(bob));
env.close();
{
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
};
validateClosedLedger(env, testCases);
}
env.close();
{
// next ledger is empty
std::vector<TestLedgerData> testCases = {};
validateClosedLedger(env, testCases);
}
// Alice pays fee & Bob should not be affected.
BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1);
BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD);
BEAST_EXPECT(env.seq(bob) == preBobSeq);
BEAST_EXPECT(env.balance(bob) == preBob);
BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD);
}
// Invalid: Outer and Inner Sequence are the same
{
auto const preAliceSeq = env.seq(alice);
auto const preAlice = env.balance(alice);
auto const preAliceUSD = env.balance(alice, USD.issue());
auto const preBobSeq = env.seq(bob);
auto const preBob = env.balance(bob);
auto const preBobUSD = env.balance(bob, USD.issue());
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), preAliceSeq),
batch::inner(pay(bob, alice, XRP(5)), preBobSeq),
batch::sig(bob));
env.close();
{
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
};
validateClosedLedger(env, testCases);
}
env.close();
{
// next ledger is empty
std::vector<TestLedgerData> testCases = {};
validateClosedLedger(env, testCases);
}
// Alice pays fee & Bob should not be affected.
BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1);
BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD);
BEAST_EXPECT(env.seq(bob) == preBobSeq);
BEAST_EXPECT(env.balance(bob) == preBob);
BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD);
}
}
void
testBadOuterFee(FeatureBitset features)
{
testcase("bad outer fee");
using namespace test::jtx;
using namespace std::literals;
// Bad Fee Without Signer
{
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
env(noop(bob), ter(tesSUCCESS));
env.close();
// Bad Fee: Should be batch::calcBatchFee(env, 0, 2)
auto const batchFee = batch::calcBatchFee(env, 0, 1);
auto const aliceSeq = env.seq(alice);
env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
batch::inner(pay(alice, bob, XRP(15)), aliceSeq + 2),
ter(telINSUF_FEE_P));
env.close();
}
// Bad Fee With MultiSign
{
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
env.fund(XRP(10000), alice, bob, carol);
env.close();
env(noop(bob), ter(tesSUCCESS));
env.close();
env(signers(alice, 2, {{bob, 1}, {carol, 1}}));
env.close();
// Bad Fee: Should be batch::calcBatchFee(env, 2, 2)
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const aliceSeq = env.seq(alice);
env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
batch::inner(pay(alice, bob, XRP(15)), aliceSeq + 2),
msig(bob, carol),
ter(telINSUF_FEE_P));
env.close();
}
// Bad Fee With MultiSign + BatchSigners
{
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
env.fund(XRP(10000), alice, bob, carol);
env.close();
env(noop(bob), ter(tesSUCCESS));
env.close();
env(signers(alice, 2, {{bob, 1}, {carol, 1}}));
env.close();
// Bad Fee: Should be batch::calcBatchFee(env, 3, 2)
auto const batchFee = batch::calcBatchFee(env, 2, 2);
auto const aliceSeq = env.seq(alice);
auto const bobSeq = env.seq(bob);
env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
batch::inner(pay(bob, alice, XRP(5)), bobSeq),
batch::sig(bob),
msig(bob, carol),
ter(telINSUF_FEE_P));
env.close();
}
// Bad Fee With MultiSign + BatchSigners.Signers
{
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
env.fund(XRP(10000), alice, bob, carol);
env.close();
env(noop(bob), ter(tesSUCCESS));
env.close();
env(signers(alice, 2, {{bob, 1}, {carol, 1}}));
env.close();
env(signers(bob, 2, {{alice, 1}, {carol, 1}}));
env.close();
// Bad Fee: Should be batch::calcBatchFee(env, 4, 2)
auto const batchFee = batch::calcBatchFee(env, 3, 2);
auto const aliceSeq = env.seq(alice);
auto const bobSeq = env.seq(bob);
env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
batch::inner(pay(bob, alice, XRP(5)), bobSeq),
batch::msig(bob, {alice, carol}),
msig(bob, carol),
ter(telINSUF_FEE_P));
env.close();
}
// Bad Fee With BatchSigners
{
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
env(noop(bob), ter(tesSUCCESS));
env.close();
// Bad Fee: Should be batch::calcBatchFee(env, 1, 2)
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const aliceSeq = env.seq(alice);
auto const bobSeq = env.seq(bob);
env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
batch::inner(pay(bob, alice, XRP(5)), bobSeq),
batch::sig(bob),
ter(telINSUF_FEE_P));
env.close();
}
// Bad Fee Dynamic Fee Calculation
{
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
auto const USD = gw["USD"];
env.fund(XRP(10000), alice, bob, gw);
env.close();
auto const ammCreate = [&alice](STAmount const& amount, STAmount const& amount2) {
Json::Value jv;
jv[jss::Account] = alice.human();
jv[jss::Amount] = amount.getJson(JsonOptions::none);
jv[jss::Amount2] = amount2.getJson(JsonOptions::none);
jv[jss::TradingFee] = 0;
jv[jss::TransactionType] = jss::AMMCreate;
return jv;
};
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const seq = env.seq(alice);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(ammCreate(XRP(10), USD(10)), seq + 1),
batch::inner(pay(alice, bob, XRP(10)), seq + 2),
ter(telINSUF_FEE_P));
env.close();
}
}
void
testCalculateBaseFee(FeatureBitset features)
{
testcase("calculate base fee");
using namespace test::jtx;
using namespace std::literals;
// telENV_RPC_FAILED: Batch: txns array exceeds 8 entries.
{
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
auto const batchFee = batch::calcBatchFee(env, 0, 9);
auto const aliceSeq = env.seq(alice);
env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::inner(pay(alice, bob, XRP(1)), aliceSeq),
ter(telENV_RPC_FAILED));
env.close();
}
// temARRAY_TOO_LARGE: Batch: txns array exceeds 8 entries.
{
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
auto const batchFee = batch::calcBatchFee(env, 0, 9);
auto const aliceSeq = env.seq(alice);
auto jt = env.jtnofill(
batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::inner(pay(alice, bob, XRP(1)), aliceSeq));
env.app().openLedger().modify([&](OpenView& view, beast::Journal j) {
auto const result = xrpl::apply(env.app(), view, *jt.stx, tapNONE, j);
BEAST_EXPECT(!result.applied && result.ter == temARRAY_TOO_LARGE);
return result.applied;
});
}
// telENV_RPC_FAILED: Batch: signers array exceeds 8 entries.
{
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
auto const aliceSeq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 9, 2);
env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
batch::inner(pay(alice, bob, XRP(5)), aliceSeq + 2),
batch::sig(bob, bob, bob, bob, bob, bob, bob, bob, bob, bob),
ter(telENV_RPC_FAILED));
env.close();
}
// temARRAY_TOO_LARGE: Batch: signers array exceeds 8 entries.
{
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
auto const batchFee = batch::calcBatchFee(env, 0, 9);
auto const aliceSeq = env.seq(alice);
auto jt = env.jtnofill(
batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
batch::inner(pay(alice, bob, XRP(5)), aliceSeq + 2),
batch::sig(bob, bob, bob, bob, bob, bob, bob, bob, bob, bob));
env.app().openLedger().modify([&](OpenView& view, beast::Journal j) {
auto const result = xrpl::apply(env.app(), view, *jt.stx, tapNONE, j);
BEAST_EXPECT(!result.applied && result.ter == temARRAY_TOO_LARGE);
return result.applied;
});
}
}
void
testAllOrNothing(FeatureBitset features)
{
testcase("all or nothing");
using namespace test::jtx;
using namespace std::literals;
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
auto const USD = gw["USD"];
env.fund(XRP(10000), alice, bob, gw);
env.close();
// all
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(1)), seq + 1),
batch::inner(pay(alice, bob, XRP(2)), seq + 2));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "Payment", "tesSUCCESS", txIDs[0], batchID},
{2, "Payment", "tesSUCCESS", txIDs[1], batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 3);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
}
// tec failure
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(1)), seq + 1),
// tecUNFUNDED_PAYMENT: alice does not have enough XRP
batch::inner(pay(alice, bob, XRP(9999)), seq + 2));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
};
validateClosedLedger(env, testCases);
// Alice consumes sequence
BEAST_EXPECT(env.seq(alice) == seq + 1);
// Alice pays Fee; Bob should not be affected
BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob);
}
// tef failure
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(1)), seq + 1),
// tefNO_AUTH_REQUIRED: trustline auth is not required
batch::inner(trust(alice, USD(1000), tfSetfAuth), seq + 2));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
};
validateClosedLedger(env, testCases);
// Alice consumes sequence
BEAST_EXPECT(env.seq(alice) == seq + 1);
// Alice pays Fee; Bob should not be affected
BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob);
}
// ter failure
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(1)), seq + 1),
// terPRE_TICKET: ticket does not exist
batch::inner(trust(alice, USD(1000), tfSetfAuth), 0, seq + 2));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
};
validateClosedLedger(env, testCases);
// Alice consumes sequence
BEAST_EXPECT(env.seq(alice) == seq + 1);
// Alice pays Fee; Bob should not be affected
BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob);
}
}
void
testOnlyOne(FeatureBitset features)
{
testcase("only one");
using namespace test::jtx;
using namespace std::literals;
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
auto const dave = Account("dave");
auto const gw = Account("gw");
auto const USD = gw["USD"];
env.fund(XRP(10000), alice, bob, carol, dave, gw);
env.close();
// all transactions fail
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 3);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfOnlyOne),
// tecUNFUNDED_PAYMENT: alice does not have enough XRP
batch::inner(pay(alice, bob, XRP(9999)), seq + 1),
// tecUNFUNDED_PAYMENT: alice does not have enough XRP
batch::inner(pay(alice, bob, XRP(9999)), seq + 2),
// tecUNFUNDED_PAYMENT: alice does not have enough XRP
batch::inner(pay(alice, bob, XRP(9999)), seq + 3));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "Payment", "tecUNFUNDED_PAYMENT", txIDs[0], batchID},
{2, "Payment", "tecUNFUNDED_PAYMENT", txIDs[1], batchID},
{3, "Payment", "tecUNFUNDED_PAYMENT", txIDs[2], batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 4);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob);
}
// first transaction fails
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 3);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfOnlyOne),
// tecUNFUNDED_PAYMENT: alice does not have enough XRP
batch::inner(pay(alice, bob, XRP(9999)), seq + 1),
batch::inner(pay(alice, bob, XRP(1)), seq + 2),
batch::inner(pay(alice, bob, XRP(2)), seq + 3));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "Payment", "tecUNFUNDED_PAYMENT", txIDs[0], batchID},
{2, "Payment", "tesSUCCESS", txIDs[1], batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 3);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
}
// tec failure
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 3);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfOnlyOne),
batch::inner(pay(alice, bob, XRP(1)), seq + 1),
// tecUNFUNDED_PAYMENT: alice does not have enough XRP
batch::inner(pay(alice, bob, XRP(9999)), seq + 2),
batch::inner(pay(alice, bob, XRP(2)), seq + 3));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "Payment", "tesSUCCESS", txIDs[0], batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 2);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
}
// tef failure
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 3);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfOnlyOne),
// tefNO_AUTH_REQUIRED: trustline auth is not required
batch::inner(trust(alice, USD(1000), tfSetfAuth), seq + 1),
batch::inner(pay(alice, bob, XRP(1)), seq + 1),
batch::inner(pay(alice, bob, XRP(2)), seq + 3));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "Payment", "tesSUCCESS", txIDs[1], batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 2);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - batchFee - XRP(1));
BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
}
// ter failure
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 3);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfOnlyOne),
// terPRE_TICKET: ticket does not exist
batch::inner(trust(alice, USD(1000), tfSetfAuth), 0, seq + 1),
batch::inner(pay(alice, bob, XRP(1)), seq + 1),
batch::inner(pay(alice, bob, XRP(2)), seq + 3));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "Payment", "tesSUCCESS", txIDs[1], batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 2);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - batchFee - XRP(1));
BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
}
// tec (tecKILLED) error
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const preCarol = env.balance(carol);
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 6);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfOnlyOne),
batch::inner(offer(alice, alice["USD"](100), XRP(100), tfImmediateOrCancel), seq + 1),
batch::inner(offer(alice, alice["USD"](100), XRP(100), tfImmediateOrCancel), seq + 2),
batch::inner(offer(alice, alice["USD"](100), XRP(100), tfImmediateOrCancel), seq + 3),
batch::inner(pay(alice, bob, XRP(100)), seq + 4),
batch::inner(pay(alice, carol, XRP(100)), seq + 5),
batch::inner(pay(alice, dave, XRP(100)), seq + 6));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "OfferCreate", "tecKILLED", txIDs[0], batchID},
{2, "OfferCreate", "tecKILLED", txIDs[1], batchID},
{3, "OfferCreate", "tecKILLED", txIDs[2], batchID},
{4, "Payment", "tesSUCCESS", txIDs[3], batchID},
};
validateClosedLedger(env, testCases);
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(100) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(100));
BEAST_EXPECT(env.balance(carol) == preCarol);
}
}
void
testUntilFailure(FeatureBitset features)
{
testcase("until failure");
using namespace test::jtx;
using namespace std::literals;
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
auto const dave = Account("dave");
auto const gw = Account("gw");
auto const USD = gw["USD"];
env.fund(XRP(10000), alice, bob, carol, dave, gw);
env.close();
// first transaction fails
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 4);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfUntilFailure),
// tecUNFUNDED_PAYMENT: alice does not have enough XRP
batch::inner(pay(alice, bob, XRP(9999)), seq + 1),
batch::inner(pay(alice, bob, XRP(1)), seq + 2),
batch::inner(pay(alice, bob, XRP(2)), seq + 3),
batch::inner(pay(alice, bob, XRP(3)), seq + 4));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "Payment", "tecUNFUNDED_PAYMENT", txIDs[0], batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 2);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob);
}
// all transactions succeed
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 4);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfUntilFailure),
batch::inner(pay(alice, bob, XRP(1)), seq + 1),
batch::inner(pay(alice, bob, XRP(2)), seq + 2),
batch::inner(pay(alice, bob, XRP(3)), seq + 3),
batch::inner(pay(alice, bob, XRP(4)), seq + 4));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "Payment", "tesSUCCESS", txIDs[0], batchID},
{2, "Payment", "tesSUCCESS", txIDs[1], batchID},
{3, "Payment", "tesSUCCESS", txIDs[2], batchID},
{4, "Payment", "tesSUCCESS", txIDs[3], batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 5);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(10) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(10));
}
// tec error
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 4);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfUntilFailure),
batch::inner(pay(alice, bob, XRP(1)), seq + 1),
batch::inner(pay(alice, bob, XRP(2)), seq + 2),
// tecUNFUNDED_PAYMENT: alice does not have enough XRP
batch::inner(pay(alice, bob, XRP(9999)), seq + 3),
batch::inner(pay(alice, bob, XRP(3)), seq + 4));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "Payment", "tesSUCCESS", txIDs[0], batchID},
{2, "Payment", "tesSUCCESS", txIDs[1], batchID},
{3, "Payment", "tecUNFUNDED_PAYMENT", txIDs[2], batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 4);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
}
// tef error
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 4);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfUntilFailure),
batch::inner(pay(alice, bob, XRP(1)), seq + 1),
batch::inner(pay(alice, bob, XRP(2)), seq + 2),
// tefNO_AUTH_REQUIRED: trustline auth is not required
batch::inner(trust(alice, USD(1000), tfSetfAuth), seq + 3),
batch::inner(pay(alice, bob, XRP(3)), seq + 4));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "Payment", "tesSUCCESS", txIDs[0], batchID},
{2, "Payment", "tesSUCCESS", txIDs[1], batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 3);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
}
// ter error
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 4);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfUntilFailure),
batch::inner(pay(alice, bob, XRP(1)), seq + 1),
batch::inner(pay(alice, bob, XRP(2)), seq + 2),
// terPRE_TICKET: ticket does not exist
batch::inner(trust(alice, USD(1000), tfSetfAuth), 0, seq + 3),
batch::inner(pay(alice, bob, XRP(3)), seq + 4));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "Payment", "tesSUCCESS", txIDs[0], batchID},
{2, "Payment", "tesSUCCESS", txIDs[1], batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 3);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
}
// tec (tecKILLED) error
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const preCarol = env.balance(carol);
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 4);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfUntilFailure),
batch::inner(pay(alice, bob, XRP(100)), seq + 1),
batch::inner(pay(alice, carol, XRP(100)), seq + 2),
batch::inner(offer(alice, alice["USD"](100), XRP(100), tfImmediateOrCancel), seq + 3),
batch::inner(pay(alice, dave, XRP(100)), seq + 4));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "Payment", "tesSUCCESS", txIDs[0], batchID},
{2, "Payment", "tesSUCCESS", txIDs[1], batchID},
{3, "OfferCreate", "tecKILLED", txIDs[2], batchID},
};
validateClosedLedger(env, testCases);
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(200) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(100));
BEAST_EXPECT(env.balance(carol) == preCarol + XRP(100));
}
}
void
testIndependent(FeatureBitset features)
{
testcase("independent");
using namespace test::jtx;
using namespace std::literals;
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
auto const gw = Account("gw");
auto const USD = gw["USD"];
env.fund(XRP(10000), alice, bob, carol, gw);
env.close();
// multiple transactions fail
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 4);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfIndependent),
batch::inner(pay(alice, bob, XRP(1)), seq + 1),
// tecUNFUNDED_PAYMENT: alice does not have enough XRP
batch::inner(pay(alice, bob, XRP(9999)), seq + 2),
// tecUNFUNDED_PAYMENT: alice does not have enough XRP
batch::inner(pay(alice, bob, XRP(9999)), seq + 3),
batch::inner(pay(alice, bob, XRP(3)), seq + 4));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "Payment", "tesSUCCESS", txIDs[0], batchID},
{2, "Payment", "tecUNFUNDED_PAYMENT", txIDs[1], batchID},
{3, "Payment", "tecUNFUNDED_PAYMENT", txIDs[2], batchID},
{4, "Payment", "tesSUCCESS", txIDs[3], batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 5);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(4) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(4));
}
// tec error
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 4);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfIndependent),
batch::inner(pay(alice, bob, XRP(1)), seq + 1),
batch::inner(pay(alice, bob, XRP(2)), seq + 2),
// tecUNFUNDED_PAYMENT: alice does not have enough XRP
batch::inner(pay(alice, bob, XRP(9999)), seq + 3),
batch::inner(pay(alice, bob, XRP(3)), seq + 4));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "Payment", "tesSUCCESS", txIDs[0], batchID},
{2, "Payment", "tesSUCCESS", txIDs[1], batchID},
{3, "Payment", "tecUNFUNDED_PAYMENT", txIDs[2], batchID},
{4, "Payment", "tesSUCCESS", txIDs[3], batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 5);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(6) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(6));
}
// tef error
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 4);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfIndependent),
batch::inner(pay(alice, bob, XRP(1)), seq + 1),
batch::inner(pay(alice, bob, XRP(2)), seq + 2),
// tefNO_AUTH_REQUIRED: trustline auth is not required
batch::inner(trust(alice, USD(1000), tfSetfAuth), seq + 3),
batch::inner(pay(alice, bob, XRP(3)), seq + 3));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "Payment", "tesSUCCESS", txIDs[0], batchID},
{2, "Payment", "tesSUCCESS", txIDs[1], batchID},
{3, "Payment", "tesSUCCESS", txIDs[3], batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 4);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - batchFee - XRP(6));
BEAST_EXPECT(env.balance(bob) == preBob + XRP(6));
}
// ter error
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 4);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfIndependent),
batch::inner(pay(alice, bob, XRP(1)), seq + 1),
batch::inner(pay(alice, bob, XRP(2)), seq + 2),
// terPRE_TICKET: ticket does not exist
batch::inner(trust(alice, USD(1000), tfSetfAuth), 0, seq + 3),
batch::inner(pay(alice, bob, XRP(3)), seq + 3));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "Payment", "tesSUCCESS", txIDs[0], batchID},
{2, "Payment", "tesSUCCESS", txIDs[1], batchID},
{3, "Payment", "tesSUCCESS", txIDs[3], batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 4);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - batchFee - XRP(6));
BEAST_EXPECT(env.balance(bob) == preBob + XRP(6));
}
// tec (tecKILLED) error
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const preCarol = env.balance(carol);
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 3);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfIndependent),
batch::inner(pay(alice, bob, XRP(100)), seq + 1),
batch::inner(pay(alice, carol, XRP(100)), seq + 2),
batch::inner(offer(alice, alice["USD"](100), XRP(100), tfImmediateOrCancel), seq + 3));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "Payment", "tesSUCCESS", txIDs[0], batchID},
{2, "Payment", "tesSUCCESS", txIDs[1], batchID},
{3, "OfferCreate", "tecKILLED", txIDs[2], batchID},
};
validateClosedLedger(env, testCases);
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(200) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(100));
BEAST_EXPECT(env.balance(carol) == preCarol + XRP(100));
}
}
void
doTestInnerSubmitRPC(FeatureBitset features, bool withBatch)
{
bool const withInnerSigFix = features[fixBatchInnerSigs];
std::string const testName = [&]() {
std::stringstream ss;
ss << "inner submit rpc: batch " << (withBatch ? "enabled" : "disabled")
<< ", inner sig fix: " << (withInnerSigFix ? "enabled" : "disabled") << ": ";
return ss.str();
}();
auto const amend = withBatch ? features : features - featureBatch;
using namespace test::jtx;
using namespace std::literals;
test::jtx::Env env{*this, amend};
if (!BEAST_EXPECT(amend[featureBatch] == withBatch))
return;
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
auto submitAndValidate = [&](std::string caseName,
Slice const& slice,
int line,
std::optional<std::string> expectedEnabled = std::nullopt,
std::optional<std::string> expectedDisabled = std::nullopt,
bool expectInvalidFlag = false) {
testcase << testName << caseName << (expectInvalidFlag ? " - Expected to reach tx engine!" : "");
auto const jrr = env.rpc("submit", strHex(slice))[jss::result];
auto const expected = withBatch ? expectedEnabled.value_or(
"fails local checks: Malformed: Invalid inner batch "
"transaction.")
: expectedDisabled.value_or("fails local checks: Empty SigningPubKey.");
if (expectInvalidFlag)
{
expect(
jrr[jss::status] == "success" && jrr[jss::engine_result] == "temINVALID_FLAG",
pretty(jrr),
__FILE__,
line);
}
else
{
expect(
jrr[jss::status] == "error" && jrr[jss::error] == "invalidTransaction" &&
jrr[jss::error_exception] == expected,
pretty(jrr),
__FILE__,
line);
}
env.close();
};
// Invalid RPC Submission: TxnSignature
// + has `TxnSignature` field
// - has no `SigningPubKey` field
// - has no `Signers` field
// + has `tfInnerBatchTxn` flag
{
auto txn = batch::inner(pay(alice, bob, XRP(1)), env.seq(alice));
txn[sfTxnSignature] = "DEADBEEF";
STParsedJSONObject parsed("test", txn.getTxn());
Serializer s;
parsed.object->add(s);
submitAndValidate("TxnSignature set", s.slice(), __LINE__);
}
// Invalid RPC Submission: SigningPubKey
// - has no `TxnSignature` field
// + has `SigningPubKey` field
// - has no `Signers` field
// + has `tfInnerBatchTxn` flag
{
auto txn = batch::inner(pay(alice, bob, XRP(1)), env.seq(alice));
txn[sfSigningPubKey] = strHex(alice.pk());
STParsedJSONObject parsed("test", txn.getTxn());
Serializer s;
parsed.object->add(s);
submitAndValidate(
"SigningPubKey set", s.slice(), __LINE__, std::nullopt, "fails local checks: Invalid signature.");
}
// Invalid RPC Submission: Signers
// - has no `TxnSignature` field
// + has empty `SigningPubKey` field
// + has `Signers` field
// + has `tfInnerBatchTxn` flag
{
auto txn = batch::inner(pay(alice, bob, XRP(1)), env.seq(alice));
txn[sfSigners] = Json::arrayValue;
STParsedJSONObject parsed("test", txn.getTxn());
Serializer s;
parsed.object->add(s);
submitAndValidate(
"Signers set", s.slice(), __LINE__, std::nullopt, "fails local checks: Invalid Signers array size.");
}
{
// Fully signed inner batch transaction
auto const txn = batch::inner(pay(alice, bob, XRP(1)), env.seq(alice));
auto const jt = env.jt(txn.getTxn());
STParsedJSONObject parsed("test", jt.jv);
Serializer s;
parsed.object->add(s);
submitAndValidate("Fully signed", s.slice(), __LINE__, std::nullopt, std::nullopt, !withBatch);
}
// Invalid RPC Submission: tfInnerBatchTxn
// - has no `TxnSignature` field
// + has empty `SigningPubKey` field
// - has no `Signers` field
// + has `tfInnerBatchTxn` flag
{
auto txn = batch::inner(pay(alice, bob, XRP(1)), env.seq(alice));
STParsedJSONObject parsed("test", txn.getTxn());
Serializer s;
parsed.object->add(s);
submitAndValidate(
"No signing fields set",
s.slice(),
__LINE__,
"fails local checks: Empty SigningPubKey.",
"fails local checks: Empty SigningPubKey.",
withBatch && !withInnerSigFix);
}
// Invalid RPC Submission: tfInnerBatchTxn pseudo-transaction
// - has no `TxnSignature` field
// + has empty `SigningPubKey` field
// - has no `Signers` field
// + has `tfInnerBatchTxn` flag
{
STTx amendTx(ttAMENDMENT, [seq = env.closed()->header().seq + 1](auto& obj) {
obj.setAccountID(sfAccount, AccountID());
obj.setFieldH256(sfAmendment, fixBatchInnerSigs);
obj.setFieldU32(sfLedgerSequence, seq);
obj.setFieldU32(sfFlags, tfInnerBatchTxn);
});
auto txn = batch::inner(amendTx.getJson(JsonOptions::none), env.seq(alice));
STParsedJSONObject parsed("test", txn.getTxn());
Serializer s;
parsed.object->add(s);
submitAndValidate(
"Pseudo-transaction",
s.slice(),
__LINE__,
withInnerSigFix ? "fails local checks: Empty SigningPubKey."
: "fails local checks: Cannot submit pseudo transactions.",
"fails local checks: Empty SigningPubKey.");
}
}
void
testInnerSubmitRPC(FeatureBitset features)
{
for (bool const withBatch : {true, false})
{
doTestInnerSubmitRPC(features, withBatch);
}
}
void
testAccountActivation(FeatureBitset features)
{
testcase("account activation");
using namespace test::jtx;
using namespace std::literals;
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice);
env.close();
env.memoize(bob);
auto const preAlice = env.balance(alice);
auto const ledSeq = env.current()->seq();
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(1000)), seq + 1),
batch::inner(fset(bob, asfAllowTrustLineClawback), ledSeq),
batch::sig(bob));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "Payment", "tesSUCCESS", txIDs[0], batchID},
{2, "AccountSet", "tesSUCCESS", txIDs[1], batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 2);
// Bob consumes sequences (# of txns)
BEAST_EXPECT(env.seq(bob) == ledSeq + 1);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1000) - batchFee);
BEAST_EXPECT(env.balance(bob) == XRP(1000));
}
void
testAccountSet(FeatureBitset features)
{
testcase("account set");
using namespace test::jtx;
using namespace std::literals;
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto tx1 = batch::inner(noop(alice), seq + 1);
std::string domain = "example.com";
tx1[sfDomain] = strHex(domain);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx1,
batch::inner(pay(alice, bob, XRP(1)), seq + 2));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "AccountSet", "tesSUCCESS", txIDs[0], batchID},
{2, "Payment", "tesSUCCESS", txIDs[1], batchID},
};
validateClosedLedger(env, testCases);
auto const sle = env.le(keylet::account(alice));
BEAST_EXPECT(sle);
BEAST_EXPECT(sle->getFieldVL(sfDomain) == Blob(domain.begin(), domain.end()));
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 3);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
}
void
testAccountDelete(FeatureBitset features)
{
testcase("account delete");
using namespace test::jtx;
using namespace std::literals;
// tfIndependent: account delete success
{
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
incLgrSeqForAccDel(env, alice);
for (int i = 0; i < 5; ++i)
env.close();
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2) + env.current()->fees().increment;
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfIndependent),
batch::inner(pay(alice, bob, XRP(1)), seq + 1),
batch::inner(acctdelete(alice, bob), seq + 2),
// terNO_ACCOUNT: alice does not exist
batch::inner(pay(alice, bob, XRP(2)), seq + 3));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "Payment", "tesSUCCESS", txIDs[0], batchID},
{2, "AccountDelete", "tesSUCCESS", txIDs[1], batchID},
};
validateClosedLedger(env, testCases);
// Alice does not exist; Bob receives Alice's XRP
BEAST_EXPECT(!env.le(keylet::account(alice)));
BEAST_EXPECT(env.balance(bob) == preBob + (preAlice - batchFee));
}
// tfIndependent: account delete fails
{
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
incLgrSeqForAccDel(env, alice);
for (int i = 0; i < 5; ++i)
env.close();
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
env.trust(bob["USD"](1000), alice);
env.close();
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2) + env.current()->fees().increment;
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfIndependent),
batch::inner(pay(alice, bob, XRP(1)), seq + 1),
// tecHAS_OBLIGATIONS: alice has obligations
batch::inner(acctdelete(alice, bob), seq + 2),
batch::inner(pay(alice, bob, XRP(2)), seq + 3));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "Payment", "tesSUCCESS", txIDs[0], batchID},
{2, "AccountDelete", "tecHAS_OBLIGATIONS", txIDs[1], batchID},
{3, "Payment", "tesSUCCESS", txIDs[2], batchID},
};
validateClosedLedger(env, testCases);
// Alice does not exist; Bob receives XRP
BEAST_EXPECT(env.le(keylet::account(alice)));
BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
}
// tfAllOrNothing: account delete fails
{
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
incLgrSeqForAccDel(env, alice);
for (int i = 0; i < 5; ++i)
env.close();
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2) + env.current()->fees().increment;
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(1)), seq + 1),
batch::inner(acctdelete(alice, bob), seq + 2),
// terNO_ACCOUNT: alice does not exist
batch::inner(pay(alice, bob, XRP(2)), seq + 3));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
};
validateClosedLedger(env, testCases);
// Alice still exists; Bob is unchanged
BEAST_EXPECT(env.le(keylet::account(alice)));
BEAST_EXPECT(env.balance(bob) == preBob);
}
}
void
testLoan(FeatureBitset features)
{
testcase("loan");
using namespace test::jtx;
test::jtx::Env env{*this, features | featureSingleAssetVault | featureLendingProtocol | featureMPTokensV1};
Account const issuer{"issuer"};
// For simplicity, lender will be the sole actor for the vault &
// brokers.
Account const lender{"lender"};
// Borrower only wants to borrow
Account const borrower{"borrower"};
// Fund the accounts and trust lines with the same amount so that tests
// can use the same values regardless of the asset.
env.fund(XRP(100'000), issuer, noripple(lender, borrower));
env.close();
// Just use an XRP asset
PrettyAsset const asset{xrpIssue(), 1'000'000};
Vault vault{env};
auto const deposit = asset(50'000);
auto const debtMaximumValue = asset(25'000).value();
auto const coverDepositValue = asset(1000).value();
auto [tx, vaultKeylet] = vault.create({.owner = lender, .asset = asset});
env(tx);
env.close();
BEAST_EXPECT(env.le(vaultKeylet));
env(vault.deposit({.depositor = lender, .id = vaultKeylet.key, .amount = deposit}));
env.close();
auto const brokerKeylet = keylet::loanbroker(lender.id(), env.seq(lender));
{
using namespace loanBroker;
env(set(lender, vaultKeylet.key),
managementFeeRate(TenthBips16(100)),
debtMaximum(debtMaximumValue),
coverRateMinimum(TenthBips32(percentageToTenthBips(10))),
coverRateLiquidation(TenthBips32(percentageToTenthBips(25))));
env(coverDeposit(lender, brokerKeylet.key, coverDepositValue));
env.close();
}
{
using namespace loan;
using namespace std::chrono_literals;
auto const lenderSeq = env.seq(lender);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const loanKeylet = keylet::loan(brokerKeylet.key, 1);
{
auto const [txIDs, batchID] = submitBatch(
env,
temBAD_SIGNATURE,
batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
batch::inner(
env.json(
set(lender, brokerKeylet.key, asset(1000).value()),
// Not allowed to include the counterparty signature
sig(sfCounterpartySignature, borrower),
sig(none),
fee(none),
seq(none)),
lenderSeq + 1),
batch::inner(pay(lender, loanKeylet.key, STAmount{asset, asset(500).value()}), lenderSeq + 2));
}
{
auto const [txIDs, batchID] = submitBatch(
env,
temINVALID_INNER_BATCH,
batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
batch::inner(
env.json(
set(lender, brokerKeylet.key, asset(1000).value()),
// Counterparty must be set
sig(none),
fee(none),
seq(none)),
lenderSeq + 1),
batch::inner(pay(lender, loanKeylet.key, STAmount{asset, asset(500).value()}), lenderSeq + 2));
}
{
auto const [txIDs, batchID] = submitBatch(
env,
temBAD_SIGNER,
batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
batch::inner(
env.json(
set(lender, brokerKeylet.key, asset(1000).value()),
// Counterparty must sign the outer transaction
counterparty(borrower.id()),
sig(none),
fee(none),
seq(none)),
lenderSeq + 1),
batch::inner(pay(lender, loanKeylet.key, STAmount{asset, asset(500).value()}), lenderSeq + 2));
}
{
// LoanSet normally charges at least 2x base fee, but since the
// signature check is done by the batch, it only charges the
// base fee.
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const [txIDs, batchID] = submitBatch(
env,
TER(tesSUCCESS),
batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
batch::inner(
env.json(
set(lender, brokerKeylet.key, asset(1000).value()),
counterparty(borrower.id()),
sig(none),
fee(none),
seq(none)),
lenderSeq + 1),
batch::inner(
pay(
// However, this inner transaction will fail,
// because the lender is not allowed to draw the
// transaction
lender,
loanKeylet.key,
STAmount{asset, asset(500).value()}),
lenderSeq + 2),
batch::sig(borrower));
}
env.close();
BEAST_EXPECT(env.le(brokerKeylet));
BEAST_EXPECT(!env.le(loanKeylet));
{
// LoanSet normally charges at least 2x base fee, but since the
// signature check is done by the batch, it only charges the
// base fee.
auto const lenderSeq = env.seq(lender);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const [txIDs, batchID] = submitBatch(
env,
TER(tesSUCCESS),
batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
batch::inner(
env.json(
set(lender, brokerKeylet.key, asset(1000).value()),
counterparty(borrower.id()),
sig(none),
fee(none),
seq(none)),
lenderSeq + 1),
batch::inner(manage(lender, loanKeylet.key, tfLoanImpair), lenderSeq + 2),
batch::sig(borrower));
}
env.close();
BEAST_EXPECT(env.le(brokerKeylet));
if (auto const sleLoan = env.le(loanKeylet); BEAST_EXPECT(sleLoan))
{
BEAST_EXPECT(sleLoan->isFlag(lsfLoanImpaired));
}
}
}
void
testObjectCreateSequence(FeatureBitset features)
{
testcase("object create w/ sequence");
using namespace test::jtx;
using namespace std::literals;
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
auto const USD = gw["USD"];
env.fund(XRP(10000), alice, bob, gw);
env.close();
env.trust(USD(1000), alice, bob);
env(pay(gw, alice, USD(100)));
env(pay(gw, bob, USD(100)));
env.close();
// success
{
auto const aliceSeq = env.seq(alice);
auto const bobSeq = env.seq(bob);
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const preAliceUSD = env.balance(alice, USD.issue());
auto const preBobUSD = env.balance(bob, USD.issue());
auto const batchFee = batch::calcBatchFee(env, 1, 2);
uint256 const chkID{getCheckIndex(bob, env.seq(bob))};
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::inner(check::create(bob, alice, USD(10)), bobSeq),
batch::inner(check::cash(alice, chkID, USD(10)), aliceSeq + 1),
batch::sig(bob));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "CheckCreate", "tesSUCCESS", txIDs[0], batchID},
{2, "CheckCash", "tesSUCCESS", txIDs[1], batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == aliceSeq + 2);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(bob) == bobSeq + 1);
// Alice pays Fee; Bob XRP Unchanged
BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob);
// Alice pays USD & Bob receives USD
BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD + USD(10));
BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD - USD(10));
}
// failure
{
env(fset(alice, asfRequireDest));
env.close();
auto const aliceSeq = env.seq(alice);
auto const bobSeq = env.seq(bob);
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const preAliceUSD = env.balance(alice, USD.issue());
auto const preBobUSD = env.balance(bob, USD.issue());
auto const batchFee = batch::calcBatchFee(env, 1, 2);
uint256 const chkID{getCheckIndex(bob, env.seq(bob))};
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, aliceSeq, batchFee, tfIndependent),
// tecDST_TAG_NEEDED - alice has enabled asfRequireDest
batch::inner(check::create(bob, alice, USD(10)), bobSeq),
batch::inner(check::cash(alice, chkID, USD(10)), aliceSeq + 1),
batch::sig(bob));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "CheckCreate", "tecDST_TAG_NEEDED", txIDs[0], batchID},
{2, "CheckCash", "tecNO_ENTRY", txIDs[1], batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == aliceSeq + 2);
// Bob consumes sequences (# of txns)
BEAST_EXPECT(env.seq(bob) == bobSeq + 1);
// Alice pays Fee; Bob XRP Unchanged
BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob);
// Alice pays USD & Bob receives USD
BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD);
BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD);
}
}
void
testObjectCreateTicket(FeatureBitset features)
{
testcase("object create w/ ticket");
using namespace test::jtx;
using namespace std::literals;
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
auto const USD = gw["USD"];
env.fund(XRP(10000), alice, bob, gw);
env.close();
env.trust(USD(1000), alice, bob);
env(pay(gw, alice, USD(100)));
env(pay(gw, bob, USD(100)));
env.close();
auto const aliceSeq = env.seq(alice);
auto const bobSeq = env.seq(bob);
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const preAliceUSD = env.balance(alice, USD.issue());
auto const preBobUSD = env.balance(bob, USD.issue());
auto const batchFee = batch::calcBatchFee(env, 1, 3);
uint256 const chkID{getCheckIndex(bob, bobSeq + 1)};
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::inner(ticket::create(bob, 10), bobSeq),
batch::inner(check::create(bob, alice, USD(10)), 0, bobSeq + 1),
batch::inner(check::cash(alice, chkID, USD(10)), aliceSeq + 1),
batch::sig(bob));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "TicketCreate", "tesSUCCESS", txIDs[0], batchID},
{2, "CheckCreate", "tesSUCCESS", txIDs[1], batchID},
{3, "CheckCash", "tesSUCCESS", txIDs[2], batchID},
};
validateClosedLedger(env, testCases);
BEAST_EXPECT(env.seq(alice) == aliceSeq + 2);
BEAST_EXPECT(env.seq(bob) == bobSeq + 10 + 1);
BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob);
BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD + USD(10));
BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD - USD(10));
}
void
testObjectCreate3rdParty(FeatureBitset features)
{
testcase("object create w/ 3rd party");
using namespace test::jtx;
using namespace std::literals;
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
auto const gw = Account("gw");
auto const USD = gw["USD"];
env.fund(XRP(10000), alice, bob, carol, gw);
env.close();
env.trust(USD(1000), alice, bob);
env(pay(gw, alice, USD(100)));
env(pay(gw, bob, USD(100)));
env.close();
auto const aliceSeq = env.seq(alice);
auto const bobSeq = env.seq(bob);
auto const carolSeq = env.seq(carol);
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const preCarol = env.balance(carol);
auto const preAliceUSD = env.balance(alice, USD.issue());
auto const preBobUSD = env.balance(bob, USD.issue());
auto const batchFee = batch::calcBatchFee(env, 2, 2);
uint256 const chkID{getCheckIndex(bob, env.seq(bob))};
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(carol, carolSeq, batchFee, tfAllOrNothing),
batch::inner(check::create(bob, alice, USD(10)), bobSeq),
batch::inner(check::cash(alice, chkID, USD(10)), aliceSeq),
batch::sig(alice, bob));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "CheckCreate", "tesSUCCESS", txIDs[0], batchID},
{2, "CheckCash", "tesSUCCESS", txIDs[1], batchID},
};
validateClosedLedger(env, testCases);
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
BEAST_EXPECT(env.seq(bob) == bobSeq + 1);
BEAST_EXPECT(env.seq(carol) == carolSeq + 1);
BEAST_EXPECT(env.balance(alice) == preAlice);
BEAST_EXPECT(env.balance(bob) == preBob);
BEAST_EXPECT(env.balance(carol) == preCarol - batchFee);
BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD + USD(10));
BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD - USD(10));
}
void
testTickets(FeatureBitset features)
{
{
testcase("tickets outer");
using namespace test::jtx;
using namespace std::literals;
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
std::uint32_t aliceTicketSeq{env.seq(alice) + 1};
env(ticket::create(alice, 10));
env.close();
auto const aliceSeq = env.seq(alice);
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, 0, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(1)), aliceSeq + 0),
batch::inner(pay(alice, bob, XRP(2)), aliceSeq + 1),
ticket::use(aliceTicketSeq));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "Payment", "tesSUCCESS", txIDs[0], batchID},
{2, "Payment", "tesSUCCESS", txIDs[1], batchID},
};
validateClosedLedger(env, testCases);
auto const sle = env.le(keylet::account(alice));
BEAST_EXPECT(sle);
BEAST_EXPECT(sle->getFieldU32(sfOwnerCount) == 9);
BEAST_EXPECT(sle->getFieldU32(sfTicketCount) == 9);
BEAST_EXPECT(env.seq(alice) == aliceSeq + 2);
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
}
{
testcase("tickets inner");
using namespace test::jtx;
using namespace std::literals;
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
std::uint32_t aliceTicketSeq{env.seq(alice) + 1};
env(ticket::create(alice, 10));
env.close();
auto const aliceSeq = env.seq(alice);
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq),
batch::inner(pay(alice, bob, XRP(2)), 0, aliceTicketSeq + 1));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "Payment", "tesSUCCESS", txIDs[0], batchID},
{2, "Payment", "tesSUCCESS", txIDs[1], batchID},
};
validateClosedLedger(env, testCases);
auto const sle = env.le(keylet::account(alice));
BEAST_EXPECT(sle);
BEAST_EXPECT(sle->getFieldU32(sfOwnerCount) == 8);
BEAST_EXPECT(sle->getFieldU32(sfTicketCount) == 8);
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
}
{
testcase("tickets outer inner");
using namespace test::jtx;
using namespace std::literals;
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
std::uint32_t aliceTicketSeq{env.seq(alice) + 1};
env(ticket::create(alice, 10));
env.close();
auto const aliceSeq = env.seq(alice);
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, 0, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1),
batch::inner(pay(alice, bob, XRP(2)), aliceSeq),
ticket::use(aliceTicketSeq));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "Payment", "tesSUCCESS", txIDs[0], batchID},
{2, "Payment", "tesSUCCESS", txIDs[1], batchID},
};
validateClosedLedger(env, testCases);
auto const sle = env.le(keylet::account(alice));
BEAST_EXPECT(sle);
BEAST_EXPECT(sle->getFieldU32(sfOwnerCount) == 8);
BEAST_EXPECT(sle->getFieldU32(sfTicketCount) == 8);
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
}
}
void
testSequenceOpenLedger(FeatureBitset features)
{
testcase("sequence open ledger");
using namespace test::jtx;
using namespace std::literals;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
// Before Batch Txn w/ retry following ledger
{
// IMPORTANT: The batch txn is applied first, then the noop txn.
// Because of this ordering, the noop txn is not applied and is
// overwritten by the payment in the batch transaction. Because the
// terPRE_SEQ is outside of the batch this noop transaction will ge
// reapplied in the following ledger
test::jtx::Env env{*this, features};
env.fund(XRP(10000), alice, bob, carol);
env.close();
auto const aliceSeq = env.seq(alice);
auto const carolSeq = env.seq(carol);
// AccountSet Txn
auto const noopTxn = env.jt(noop(alice), seq(aliceSeq + 2));
auto const noopTxnID = to_string(noopTxn.stx->getTransactionID());
env(noopTxn, ter(terPRE_SEQ));
// Batch Txn
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(carol, carolSeq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::inner(pay(alice, bob, XRP(2)), aliceSeq + 1),
batch::sig(alice));
env.close();
{
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "Payment", "tesSUCCESS", txIDs[0], batchID},
{2, "Payment", "tesSUCCESS", txIDs[1], batchID},
};
validateClosedLedger(env, testCases);
}
env.close();
{
// next ledger contains noop txn
std::vector<TestLedgerData> testCases = {
{0, "AccountSet", "tesSUCCESS", noopTxnID, std::nullopt},
};
validateClosedLedger(env, testCases);
}
}
// Before Batch Txn w/ same sequence
{
// IMPORTANT: The batch txn is applied first, then the noop txn.
// Because of this ordering, the noop txn is not applied and is
// overwritten by the payment in the batch transaction.
test::jtx::Env env{*this, features};
env.fund(XRP(10000), alice, bob);
env.close();
auto const aliceSeq = env.seq(alice);
// AccountSet Txn
auto const noopTxn = env.jt(noop(alice), seq(aliceSeq + 1));
env(noopTxn, ter(terPRE_SEQ));
// Batch Txn
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(1)), aliceSeq + 1),
batch::inner(pay(alice, bob, XRP(2)), aliceSeq + 2));
env.close();
{
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "Payment", "tesSUCCESS", txIDs[0], batchID},
{2, "Payment", "tesSUCCESS", txIDs[1], batchID},
};
validateClosedLedger(env, testCases);
}
env.close();
{
// next ledger is empty
std::vector<TestLedgerData> testCases = {};
validateClosedLedger(env, testCases);
}
}
// After Batch Txn w/ same sequence
{
// IMPORTANT: The batch txn is applied first, then the noop txn.
// Because of this ordering, the noop txn is not applied and is
// overwritten by the payment in the batch transaction.
test::jtx::Env env{*this, features};
env.fund(XRP(10000), alice, bob);
env.close();
auto const aliceSeq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(1)), aliceSeq + 1),
batch::inner(pay(alice, bob, XRP(2)), aliceSeq + 2));
auto const noopTxn = env.jt(noop(alice), seq(aliceSeq + 1));
auto const noopTxnID = to_string(noopTxn.stx->getTransactionID());
env(noopTxn, ter(tesSUCCESS));
env.close();
{
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "Payment", "tesSUCCESS", txIDs[0], batchID},
{2, "Payment", "tesSUCCESS", txIDs[1], batchID},
};
validateClosedLedger(env, testCases);
}
env.close();
{
// next ledger is empty
std::vector<TestLedgerData> testCases = {};
validateClosedLedger(env, testCases);
}
}
// Outer Batch terPRE_SEQ
{
test::jtx::Env env{*this, features};
env.fund(XRP(10000), alice, bob, carol);
env.close();
auto const aliceSeq = env.seq(alice);
auto const carolSeq = env.seq(carol);
// Batch Txn
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const [txIDs, batchID] = submitBatch(
env,
terPRE_SEQ,
batch::outer(carol, carolSeq + 1, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::inner(pay(alice, bob, XRP(2)), aliceSeq + 1),
batch::sig(alice));
// AccountSet Txn
auto const noopTxn = env.jt(noop(carol), seq(carolSeq));
auto const noopTxnID = to_string(noopTxn.stx->getTransactionID());
env(noopTxn, ter(tesSUCCESS));
env.close();
{
std::vector<TestLedgerData> testCases = {
{0, "AccountSet", "tesSUCCESS", noopTxnID, std::nullopt},
{1, "Batch", "tesSUCCESS", batchID, std::nullopt},
{2, "Payment", "tesSUCCESS", txIDs[0], batchID},
{3, "Payment", "tesSUCCESS", txIDs[1], batchID},
};
validateClosedLedger(env, testCases);
}
env.close();
{
// next ledger contains no transactions
std::vector<TestLedgerData> testCases = {};
validateClosedLedger(env, testCases);
}
}
}
void
testTicketsOpenLedger(FeatureBitset features)
{
testcase("tickets open ledger");
using namespace test::jtx;
using namespace std::literals;
auto const alice = Account("alice");
auto const bob = Account("bob");
// Before Batch Txn w/ same ticket
{
// IMPORTANT: The batch txn is applied first, then the noop txn.
// Because of this ordering, the noop txn is not applied and is
// overwritten by the payment in the batch transaction.
test::jtx::Env env{*this, features};
env.fund(XRP(10000), alice, bob);
env.close();
std::uint32_t aliceTicketSeq{env.seq(alice) + 1};
env(ticket::create(alice, 10));
env.close();
auto const aliceSeq = env.seq(alice);
// AccountSet Txn
auto const noopTxn = env.jt(noop(alice), ticket::use(aliceTicketSeq + 1));
auto const noopTxnID = to_string(noopTxn.stx->getTransactionID());
env(noopTxn, ter(tesSUCCESS));
// Batch Txn
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, 0, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1),
batch::inner(pay(alice, bob, XRP(2)), aliceSeq),
ticket::use(aliceTicketSeq));
env.close();
{
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "Payment", "tesSUCCESS", txIDs[0], batchID},
{2, "Payment", "tesSUCCESS", txIDs[1], batchID},
};
validateClosedLedger(env, testCases);
}
env.close();
{
// next ledger is empty
std::vector<TestLedgerData> testCases = {};
validateClosedLedger(env, testCases);
}
}
// After Batch Txn w/ same ticket
{
// IMPORTANT: The batch txn is applied first, then the noop txn.
// Because of this ordering, the noop txn is not applied and is
// overwritten by the payment in the batch transaction.
test::jtx::Env env{*this, features};
env.fund(XRP(10000), alice, bob);
env.close();
std::uint32_t aliceTicketSeq{env.seq(alice) + 1};
env(ticket::create(alice, 10));
env.close();
auto const aliceSeq = env.seq(alice);
// Batch Txn
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, 0, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1),
batch::inner(pay(alice, bob, XRP(2)), aliceSeq),
ticket::use(aliceTicketSeq));
// AccountSet Txn
auto const noopTxn = env.jt(noop(alice), ticket::use(aliceTicketSeq + 1));
env(noopTxn);
env.close();
{
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "Payment", "tesSUCCESS", txIDs[0], batchID},
{2, "Payment", "tesSUCCESS", txIDs[1], batchID},
};
validateClosedLedger(env, testCases);
}
env.close();
{
// next ledger is empty
std::vector<TestLedgerData> testCases = {};
validateClosedLedger(env, testCases);
}
}
}
void
testObjectsOpenLedger(FeatureBitset features)
{
testcase("objects open ledger");
using namespace test::jtx;
using namespace std::literals;
auto const alice = Account("alice");
auto const bob = Account("bob");
// Consume Object Before Batch Txn
{
// IMPORTANT: The initial result of `CheckCash` is tecNO_ENTRY
// because the create transaction has not been applied because the
// batch will run in the close ledger process. The batch will be
// allied and then retry this transaction in the current ledger.
test::jtx::Env env{*this, features};
env.fund(XRP(10000), alice, bob);
env.close();
std::uint32_t aliceTicketSeq{env.seq(alice) + 1};
env(ticket::create(alice, 10));
env.close();
auto const aliceSeq = env.seq(alice);
// CheckCash Txn
uint256 const chkID{getCheckIndex(alice, aliceSeq)};
auto const objTxn = env.jt(check::cash(bob, chkID, XRP(10)));
auto const objTxnID = to_string(objTxn.stx->getTransactionID());
env(objTxn, ter(tecNO_ENTRY));
// Batch Txn
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, 0, batchFee, tfAllOrNothing),
batch::inner(check::create(alice, bob, XRP(10)), aliceSeq),
batch::inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1),
ticket::use(aliceTicketSeq));
env.close();
{
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "CheckCreate", "tesSUCCESS", txIDs[0], batchID},
{2, "Payment", "tesSUCCESS", txIDs[1], batchID},
{3, "CheckCash", "tesSUCCESS", objTxnID, std::nullopt},
};
validateClosedLedger(env, testCases);
}
env.close();
{
// next ledger is empty
std::vector<TestLedgerData> testCases = {};
validateClosedLedger(env, testCases);
}
}
// Create Object Before Batch Txn
{
test::jtx::Env env{*this, features};
env.fund(XRP(10000), alice, bob);
env.close();
std::uint32_t aliceTicketSeq{env.seq(alice) + 1};
env(ticket::create(alice, 10));
env.close();
auto const aliceSeq = env.seq(alice);
auto const bobSeq = env.seq(bob);
// CheckCreate Txn
uint256 const chkID{getCheckIndex(alice, aliceSeq)};
auto const objTxn = env.jt(check::create(alice, bob, XRP(10)));
auto const objTxnID = to_string(objTxn.stx->getTransactionID());
env(objTxn, ter(tesSUCCESS));
// Batch Txn
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, 0, batchFee, tfAllOrNothing),
batch::inner(check::cash(bob, chkID, XRP(10)), bobSeq),
batch::inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1),
ticket::use(aliceTicketSeq),
batch::sig(bob));
env.close();
{
std::vector<TestLedgerData> testCases = {
{0, "CheckCreate", "tesSUCCESS", objTxnID, std::nullopt},
{1, "Batch", "tesSUCCESS", batchID, std::nullopt},
{2, "CheckCash", "tesSUCCESS", txIDs[0], batchID},
{3, "Payment", "tesSUCCESS", txIDs[1], batchID},
};
validateClosedLedger(env, testCases);
}
}
// After Batch Txn
{
// IMPORTANT: The initial result of `CheckCash` is tecNO_ENTRY
// because the create transaction has not been applied because the
// batch will run in the close ledger process. The batch will be
// applied and then retry this transaction in the current ledger.
test::jtx::Env env{*this, features};
env.fund(XRP(10000), alice, bob);
env.close();
std::uint32_t aliceTicketSeq{env.seq(alice) + 1};
env(ticket::create(alice, 10));
env.close();
auto const aliceSeq = env.seq(alice);
// Batch Txn
auto const batchFee = batch::calcBatchFee(env, 0, 2);
uint256 const chkID{getCheckIndex(alice, aliceSeq)};
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, 0, batchFee, tfAllOrNothing),
batch::inner(check::create(alice, bob, XRP(10)), aliceSeq),
batch::inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1),
ticket::use(aliceTicketSeq));
// CheckCash Txn
auto const objTxn = env.jt(check::cash(bob, chkID, XRP(10)));
auto const objTxnID = to_string(objTxn.stx->getTransactionID());
env(objTxn, ter(tecNO_ENTRY));
env.close();
{
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "CheckCreate", "tesSUCCESS", txIDs[0], batchID},
{2, "Payment", "tesSUCCESS", txIDs[1], batchID},
{3, "CheckCash", "tesSUCCESS", objTxnID, std::nullopt},
};
validateClosedLedger(env, testCases);
}
}
}
void
testPseudoTxn(FeatureBitset features)
{
testcase("pseudo txn with tfInnerBatchTxn");
using namespace test::jtx;
using namespace std::literals;
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
STTx const stx = STTx(ttAMENDMENT, [&](auto& obj) {
obj.setAccountID(sfAccount, AccountID());
obj.setFieldH256(sfAmendment, uint256(2));
obj.setFieldU32(sfLedgerSequence, env.seq(alice));
obj.setFieldU32(sfFlags, tfInnerBatchTxn);
});
std::string reason;
BEAST_EXPECT(isPseudoTx(stx));
BEAST_EXPECT(!passesLocalChecks(stx, reason));
BEAST_EXPECT(reason == "Cannot submit pseudo transactions.");
env.app().openLedger().modify([&](OpenView& view, beast::Journal j) {
auto const result = xrpl::apply(env.app(), view, stx, tapNONE, j);
BEAST_EXPECT(!result.applied && result.ter == temINVALID_FLAG);
return result.applied;
});
}
void
testOpenLedger(FeatureBitset features)
{
testcase("batch open ledger");
// IMPORTANT: When a transaction is submitted outside of a batch and
// another transaction is part of the batch, the batch might fail
// because the sequence is out of order. This is because the canonical
// order of transactions is determined by the account first. So in this
// case, alice's batch comes after bob's self submitted transaction even
// though the payment was submitted after the batch.
using namespace test::jtx;
using namespace std::literals;
test::jtx::Env env{*this, features};
XRPAmount const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
env(noop(bob), ter(tesSUCCESS));
env.close();
auto const aliceSeq = env.seq(alice);
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const bobSeq = env.seq(bob);
// Alice Pays Bob (Open Ledger)
auto const payTxn1 = env.jt(pay(alice, bob, XRP(10)), seq(aliceSeq));
auto const payTxn1ID = to_string(payTxn1.stx->getTransactionID());
env(payTxn1, ter(tesSUCCESS));
// Alice & Bob Atomic Batch
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, aliceSeq + 1, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 2),
batch::inner(pay(bob, alice, XRP(5)), bobSeq),
batch::sig(bob));
// Bob pays Alice (Open Ledger)
auto const payTxn2 = env.jt(pay(bob, alice, XRP(5)), seq(bobSeq + 1));
auto const payTxn2ID = to_string(payTxn2.stx->getTransactionID());
env(payTxn2, ter(terPRE_SEQ));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Payment", "tesSUCCESS", payTxn1ID, std::nullopt},
{1, "Batch", "tesSUCCESS", batchID, std::nullopt},
{2, "Payment", "tesSUCCESS", txIDs[0], batchID},
{3, "Payment", "tesSUCCESS", txIDs[1], batchID},
};
validateClosedLedger(env, testCases);
env.close();
{
// next ledger includes the payment txn
std::vector<TestLedgerData> testCases = {
{0, "Payment", "tesSUCCESS", payTxn2ID, std::nullopt},
};
validateClosedLedger(env, testCases);
}
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == aliceSeq + 3);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(bob) == bobSeq + 2);
// Alice pays XRP & Fee; Bob receives XRP & pays Fee
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(10) - batchFee - baseFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(10) - baseFee);
}
void
testBatchTxQueue(FeatureBitset features)
{
testcase("batch tx queue");
using namespace test::jtx;
using namespace std::literals;
// only outer batch transactions are counter towards the queue size
{
test::jtx::Env env{
*this,
makeSmallQueueConfig({{"minimum_txn_in_ledger_standalone", "2"}}),
features,
nullptr,
beast::severities::kError};
auto alice = Account("alice");
auto bob = Account("bob");
auto carol = Account("carol");
// Fund across several ledgers so the TxQ metrics stay restricted.
env.fund(XRP(10000), noripple(alice, bob));
env.close(env.now() + 5s, 10000ms);
env.fund(XRP(10000), noripple(carol));
env.close(env.now() + 5s, 10000ms);
// Fill the ledger
env(noop(alice));
env(noop(alice));
env(noop(alice));
checkMetrics(*this, env, 0, std::nullopt, 3, 2);
env(noop(carol), ter(terQUEUED));
checkMetrics(*this, env, 1, std::nullopt, 3, 2);
auto const aliceSeq = env.seq(alice);
auto const bobSeq = env.seq(bob);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
// Queue Batch
{
env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
batch::inner(pay(bob, alice, XRP(5)), bobSeq),
batch::sig(bob),
ter(terQUEUED));
}
checkMetrics(*this, env, 2, std::nullopt, 3, 2);
// Replace Queued Batch
{
env(batch::outer(alice, aliceSeq, openLedgerFee(env, batchFee), tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
batch::inner(pay(bob, alice, XRP(5)), bobSeq),
batch::sig(bob),
ter(tesSUCCESS));
env.close();
}
checkMetrics(*this, env, 0, 12, 1, 6);
}
// inner batch transactions are counter towards the ledger tx count
{
test::jtx::Env env{
*this,
makeSmallQueueConfig({{"minimum_txn_in_ledger_standalone", "2"}}),
features,
nullptr,
beast::severities::kError};
auto alice = Account("alice");
auto bob = Account("bob");
auto carol = Account("carol");
// Fund across several ledgers so the TxQ metrics stay restricted.
env.fund(XRP(10000), noripple(alice, bob));
env.close(env.now() + 5s, 10000ms);
env.fund(XRP(10000), noripple(carol));
env.close(env.now() + 5s, 10000ms);
// Fill the ledger leaving room for 1 queued transaction
env(noop(alice));
env(noop(alice));
checkMetrics(*this, env, 0, std::nullopt, 2, 2);
auto const aliceSeq = env.seq(alice);
auto const bobSeq = env.seq(bob);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
// Batch Successful
{
env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
batch::inner(pay(bob, alice, XRP(5)), bobSeq),
batch::sig(bob),
ter(tesSUCCESS));
}
checkMetrics(*this, env, 0, std::nullopt, 3, 2);
env(noop(carol), ter(terQUEUED));
checkMetrics(*this, env, 1, std::nullopt, 3, 2);
}
}
void
testBatchNetworkOps(FeatureBitset features)
{
testcase("batch network ops");
using namespace test::jtx;
using namespace std::literals;
Env env(*this, envconfig(), features, nullptr, beast::severities::kDisabled);
auto alice = Account("alice");
auto bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
auto submitTx = [&](std::uint32_t flags) -> uint256 {
auto jt = env.jt(pay(alice, bob, XRP(1)), txflags(flags));
Serializer s;
jt.stx->add(s);
env.app().getOPs().submitTransaction(jt.stx);
return jt.stx->getTransactionID();
};
auto processTxn = [&](std::uint32_t flags) -> uint256 {
auto jt = env.jt(pay(alice, bob, XRP(1)), txflags(flags));
Serializer s;
jt.stx->add(s);
std::string reason;
auto transaction = std::make_shared<Transaction>(jt.stx, reason, env.app());
env.app().getOPs().processTransaction(transaction, false, true, NetworkOPs::FailHard::yes);
return transaction->getID();
};
// Validate: NetworkOPs::submitTransaction()
{
// Submit a tx with tfInnerBatchTxn
uint256 const txBad = submitTx(tfInnerBatchTxn);
BEAST_EXPECT(env.app().getHashRouter().getFlags(txBad) == HashRouterFlags::UNDEFINED);
}
// Validate: NetworkOPs::processTransaction()
{
uint256 const txid = processTxn(tfInnerBatchTxn);
// HashRouter::getFlags() should return LedgerFlags::BAD
BEAST_EXPECT(env.app().getHashRouter().getFlags(txid) == HashRouterFlags::BAD);
}
}
void
testBatchDelegate(FeatureBitset features)
{
testcase("batch delegate");
using namespace test::jtx;
using namespace std::literals;
// delegated non atomic inner
{
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
auto const USD = gw["USD"];
env.fund(XRP(10000), alice, bob, gw);
env.close();
env(delegate::set(alice, bob, {"Payment"}));
env.close();
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const seq = env.seq(alice);
auto tx = batch::inner(pay(alice, bob, XRP(1)), seq + 1);
tx[jss::Delegate] = bob.human();
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx,
batch::inner(pay(alice, bob, XRP(2)), seq + 2));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "Payment", "tesSUCCESS", txIDs[0], batchID},
{2, "Payment", "tesSUCCESS", txIDs[1], batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 3);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
}
// delegated atomic inner
{
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
auto const gw = Account("gw");
auto const USD = gw["USD"];
env.fund(XRP(10000), alice, bob, carol, gw);
env.close();
env(delegate::set(bob, carol, {"Payment"}));
env.close();
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const preCarol = env.balance(carol);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const aliceSeq = env.seq(alice);
auto const bobSeq = env.seq(bob);
auto tx = batch::inner(pay(bob, alice, XRP(1)), bobSeq);
tx[jss::Delegate] = carol.human();
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
tx,
batch::inner(pay(alice, bob, XRP(2)), aliceSeq + 1),
batch::sig(bob));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "Payment", "tesSUCCESS", txIDs[0], batchID},
{2, "Payment", "tesSUCCESS", txIDs[1], batchID},
};
validateClosedLedger(env, testCases);
BEAST_EXPECT(env.seq(alice) == aliceSeq + 2);
BEAST_EXPECT(env.seq(bob) == bobSeq + 1);
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
// NOTE: Carol would normally pay the fee for delegated txns, but
// because the batch is atomic, the fee is paid by the batch
BEAST_EXPECT(env.balance(carol) == preCarol);
}
// delegated non atomic inner (AccountSet)
// this also makes sure tfInnerBatchTxn won't block delegated AccountSet
// with granular permission
{
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
auto const USD = gw["USD"];
env.fund(XRP(10000), alice, bob, gw);
env.close();
env(delegate::set(alice, bob, {"AccountDomainSet"}));
env.close();
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const seq = env.seq(alice);
auto tx = batch::inner(noop(alice), seq + 1);
std::string const domain = "example.com";
tx[sfDomain.jsonName] = strHex(domain);
tx[jss::Delegate] = bob.human();
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx,
batch::inner(pay(alice, bob, XRP(2)), seq + 2));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "AccountSet", "tesSUCCESS", txIDs[0], batchID},
{2, "Payment", "tesSUCCESS", txIDs[1], batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 3);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(2) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(2));
}
// delegated non atomic inner (MPTokenIssuanceSet)
// this also makes sure tfInnerBatchTxn won't block delegated
// MPTokenIssuanceSet with granular permission
{
test::jtx::Env env{*this, features};
Account alice{"alice"};
Account bob{"bob"};
env.fund(XRP(100000), alice, bob);
env.close();
auto const mptID = makeMptID(env.seq(alice), alice);
MPTTester mpt(env, alice, {.fund = false});
env.close();
mpt.create({.flags = tfMPTCanLock});
env.close();
// alice gives granular permission to bob of MPTokenIssuanceLock
env(delegate::set(alice, bob, {"MPTokenIssuanceLock", "MPTokenIssuanceUnlock"}));
env.close();
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
Json::Value jv1;
jv1[sfTransactionType] = jss::MPTokenIssuanceSet;
jv1[sfAccount] = alice.human();
jv1[sfDelegate] = bob.human();
jv1[sfSequence] = seq + 1;
jv1[sfMPTokenIssuanceID] = to_string(mptID);
jv1[sfFlags] = tfMPTLock;
Json::Value jv2;
jv2[sfTransactionType] = jss::MPTokenIssuanceSet;
jv2[sfAccount] = alice.human();
jv2[sfDelegate] = bob.human();
jv2[sfSequence] = seq + 2;
jv2[sfMPTokenIssuanceID] = to_string(mptID);
jv2[sfFlags] = tfMPTUnlock;
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(jv1, seq + 1),
batch::inner(jv2, seq + 2));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "MPTokenIssuanceSet", "tesSUCCESS", txIDs[0], batchID},
{2, "MPTokenIssuanceSet", "tesSUCCESS", txIDs[1], batchID},
};
validateClosedLedger(env, testCases);
}
// delegated non atomic inner (TrustSet)
// this also makes sure tfInnerBatchTxn won't block delegated TrustSet
// with granular permission
{
test::jtx::Env env{*this, features};
Account gw{"gw"};
Account alice{"alice"};
Account bob{"bob"};
env.fund(XRP(10000), gw, alice, bob);
env(fset(gw, asfRequireAuth));
env.close();
env(trust(alice, gw["USD"](50)));
env.close();
env(delegate::set(gw, bob, {"TrustlineAuthorize", "TrustlineFreeze"}));
env.close();
auto const seq = env.seq(gw);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto jv1 = trust(gw, gw["USD"](0), alice, tfSetfAuth);
jv1[sfDelegate] = bob.human();
auto jv2 = trust(gw, gw["USD"](0), alice, tfSetFreeze);
jv2[sfDelegate] = bob.human();
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(gw, seq, batchFee, tfAllOrNothing),
batch::inner(jv1, seq + 1),
batch::inner(jv2, seq + 2));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "TrustSet", "tesSUCCESS", txIDs[0], batchID},
{2, "TrustSet", "tesSUCCESS", txIDs[1], batchID},
};
validateClosedLedger(env, testCases);
}
// inner transaction not authorized by the delegating account.
{
test::jtx::Env env{*this, features};
Account gw{"gw"};
Account alice{"alice"};
Account bob{"bob"};
env.fund(XRP(10000), gw, alice, bob);
env(fset(gw, asfRequireAuth));
env.close();
env(trust(alice, gw["USD"](50)));
env.close();
env(delegate::set(gw, bob, {"TrustlineAuthorize", "TrustlineFreeze"}));
env.close();
auto const seq = env.seq(gw);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto jv1 = trust(gw, gw["USD"](0), alice, tfSetFreeze);
jv1[sfDelegate] = bob.human();
auto jv2 = trust(gw, gw["USD"](0), alice, tfClearFreeze);
jv2[sfDelegate] = bob.human();
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(gw, seq, batchFee, tfIndependent),
batch::inner(jv1, seq + 1),
// terNO_DELEGATE_PERMISSION: not authorized to clear freeze
batch::inner(jv2, seq + 2));
env.close();
std::vector<TestLedgerData> testCases = {
{0, "Batch", "tesSUCCESS", batchID, std::nullopt},
{1, "TrustSet", "tesSUCCESS", txIDs[0], batchID},
};
validateClosedLedger(env, testCases);
}
}
void
testValidateRPCResponse(FeatureBitset features)
{
// Verifying that the RPC response from submit includes
// the account_sequence_available, account_sequence_next,
// open_ledger_cost and validated_ledger_index fields.
testcase("Validate RPC response");
using namespace jtx;
Env env(*this, features);
Account const alice("alice");
Account const bob("bob");
env.fund(XRP(10000), alice, bob);
env.close();
// tes
{
auto const baseFee = env.current()->fees().base;
auto const aliceSeq = env.seq(alice);
auto jtx = env.jt(pay(alice, bob, XRP(1)));
Serializer s;
jtx.stx->add(s);
auto const jr = env.rpc("submit", strHex(s.slice()))[jss::result];
env.close();
BEAST_EXPECT(jr.isMember(jss::account_sequence_available));
BEAST_EXPECT(jr[jss::account_sequence_available].asUInt() == aliceSeq + 1);
BEAST_EXPECT(jr.isMember(jss::account_sequence_next));
BEAST_EXPECT(jr[jss::account_sequence_next].asUInt() == aliceSeq + 1);
BEAST_EXPECT(jr.isMember(jss::open_ledger_cost));
BEAST_EXPECT(jr[jss::open_ledger_cost] == to_string(baseFee));
BEAST_EXPECT(jr.isMember(jss::validated_ledger_index));
}
// tec failure
{
auto const baseFee = env.current()->fees().base;
auto const aliceSeq = env.seq(alice);
env(fset(bob, asfRequireDest));
auto jtx = env.jt(pay(alice, bob, XRP(1)), seq(aliceSeq));
Serializer s;
jtx.stx->add(s);
auto const jr = env.rpc("submit", strHex(s.slice()))[jss::result];
env.close();
BEAST_EXPECT(jr.isMember(jss::account_sequence_available));
BEAST_EXPECT(jr[jss::account_sequence_available].asUInt() == aliceSeq + 1);
BEAST_EXPECT(jr.isMember(jss::account_sequence_next));
BEAST_EXPECT(jr[jss::account_sequence_next].asUInt() == aliceSeq + 1);
BEAST_EXPECT(jr.isMember(jss::open_ledger_cost));
BEAST_EXPECT(jr[jss::open_ledger_cost] == to_string(baseFee));
BEAST_EXPECT(jr.isMember(jss::validated_ledger_index));
}
// tem failure
{
auto const baseFee = env.current()->fees().base;
auto const aliceSeq = env.seq(alice);
auto jtx = env.jt(pay(alice, bob, XRP(1)), seq(aliceSeq + 1));
Serializer s;
jtx.stx->add(s);
auto const jr = env.rpc("submit", strHex(s.slice()))[jss::result];
env.close();
BEAST_EXPECT(jr.isMember(jss::account_sequence_available));
BEAST_EXPECT(jr[jss::account_sequence_available].asUInt() == aliceSeq);
BEAST_EXPECT(jr.isMember(jss::account_sequence_next));
BEAST_EXPECT(jr[jss::account_sequence_next].asUInt() == aliceSeq);
BEAST_EXPECT(jr.isMember(jss::open_ledger_cost));
BEAST_EXPECT(jr[jss::open_ledger_cost] == to_string(baseFee));
BEAST_EXPECT(jr.isMember(jss::validated_ledger_index));
}
}
void
testBatchCalculateBaseFee(FeatureBitset features)
{
using namespace jtx;
Env env(*this, features);
Account const alice("alice");
Account const bob("bob");
Account const carol("carol");
env.fund(XRP(10000), alice, bob, carol);
env.close();
auto getBaseFee = [&](JTx const& jtx) -> XRPAmount {
Serializer s;
jtx.stx->add(s);
return Batch::calculateBaseFee(*env.current(), *jtx.stx);
};
// bad: Inner Batch transaction found
{
auto const seq = env.seq(alice);
XRPAmount const batchFee = batch::calcBatchFee(env, 0, 2);
auto jtx = env.jt(
batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(batch::outer(alice, seq, batchFee, tfAllOrNothing), seq),
batch::inner(pay(alice, bob, XRP(1)), seq + 2));
XRPAmount const txBaseFee = getBaseFee(jtx);
BEAST_EXPECT(txBaseFee == XRPAmount(INITIAL_XRP));
}
// bad: Raw Transactions array exceeds max entries.
{
auto const seq = env.seq(alice);
XRPAmount const batchFee = batch::calcBatchFee(env, 0, 2);
auto jtx = env.jt(
batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(1)), seq + 1),
batch::inner(pay(alice, bob, XRP(1)), seq + 2),
batch::inner(pay(alice, bob, XRP(1)), seq + 3),
batch::inner(pay(alice, bob, XRP(1)), seq + 4),
batch::inner(pay(alice, bob, XRP(1)), seq + 5),
batch::inner(pay(alice, bob, XRP(1)), seq + 6),
batch::inner(pay(alice, bob, XRP(1)), seq + 7),
batch::inner(pay(alice, bob, XRP(1)), seq + 8),
batch::inner(pay(alice, bob, XRP(1)), seq + 9));
XRPAmount const txBaseFee = getBaseFee(jtx);
BEAST_EXPECT(txBaseFee == XRPAmount(INITIAL_XRP));
}
// bad: Signers array exceeds max entries.
{
auto const seq = env.seq(alice);
XRPAmount const batchFee = batch::calcBatchFee(env, 0, 2);
auto jtx = env.jt(
batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(10)), seq + 1),
batch::inner(pay(alice, bob, XRP(5)), seq + 2),
batch::sig(bob, carol, alice, bob, carol, alice, bob, carol, alice, alice));
XRPAmount const txBaseFee = getBaseFee(jtx);
BEAST_EXPECT(txBaseFee == XRPAmount(INITIAL_XRP));
}
// good:
{
auto const seq = env.seq(alice);
XRPAmount const batchFee = batch::calcBatchFee(env, 0, 2);
auto jtx = env.jt(
batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(1)), seq + 1),
batch::inner(pay(bob, alice, XRP(2)), seq + 2));
XRPAmount const txBaseFee = getBaseFee(jtx);
BEAST_EXPECT(txBaseFee == batchFee);
}
}
void
testWithFeats(FeatureBitset features)
{
testEnable(features);
testPreflight(features);
testPreclaim(features);
testBadRawTxn(features);
testBadSequence(features);
testBadOuterFee(features);
testCalculateBaseFee(features);
testAllOrNothing(features);
testOnlyOne(features);
testUntilFailure(features);
testIndependent(features);
testInnerSubmitRPC(features);
testAccountActivation(features);
testAccountSet(features);
testAccountDelete(features);
testLoan(features);
testObjectCreateSequence(features);
testObjectCreateTicket(features);
testObjectCreate3rdParty(features);
testTickets(features);
testSequenceOpenLedger(features);
testTicketsOpenLedger(features);
testObjectsOpenLedger(features);
testPseudoTxn(features);
testOpenLedger(features);
testBatchTxQueue(features);
testBatchNetworkOps(features);
testBatchDelegate(features);
testValidateRPCResponse(features);
testBatchCalculateBaseFee(features);
}
public:
void
run() override
{
using namespace test::jtx;
auto const sa = testable_amendments();
testWithFeats(sa - fixBatchInnerSigs);
testWithFeats(sa);
}
};
BEAST_DEFINE_TESTSUITE(Batch, app, xrpl);
} // namespace test
} // namespace xrpl