Files
rippled/src/test/app/Batch_test.cpp
Ed Hennis 89979022f2 Merge remote-tracking branch 'upstream/develop' into ximinez/lending-XLS-66
* upstream/develop:
  refactor: Retire ImmediateOfferKilled amendment (5973)
  ci: Update CI image hashes to use netstat (5987)
  chore: Remove version number in find_dependency for OpenSSL (5985)
  refactor: Modularize shamap and nodestore (5668)
  refactor: Retire fixMasterKeyAsRegularKey amendment (5959)
  refactor: Retire fixReducedOffersV1 amendment (5972)
  refactor: Retire fixAmendmentMajorityCalc amendment (5961)
  refactor: Clean up `TxMeta` (5845)
  fix: Address permission delegation vulnerability (5825)
2025-11-03 13:09:03 -05:00

4373 lines
164 KiB
C++

//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <test/jtx.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/utility.h>
#include <xrpld/app/misc/HashRouter.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 ripple {
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)
{
testcase("enabled");
using namespace test::jtx;
using namespace std::literals;
for (bool const withBatch : {true, false})
{
auto const amend = withBatch ? features : features - featureBatch;
test::jtx::Env env{*this, envconfig(), 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, envconfig()};
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();
}
// 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 = ripple::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, envconfig()};
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, envconfig()};
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, envconfig()};
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, envconfig()};
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, envconfig()};
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, envconfig()};
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, envconfig()};
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, envconfig()};
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, envconfig()};
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, envconfig()};
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, envconfig()};
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 =
ripple::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, envconfig()};
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, envconfig()};
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 =
ripple::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, envconfig()};
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, envconfig()};
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, envconfig()};
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, envconfig()};
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
testInnerSubmitRPC(FeatureBitset features)
{
testcase("inner submit rpc");
using namespace test::jtx;
using namespace std::literals;
test::jtx::Env env{*this, envconfig()};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
auto submitAndValidate = [&](Slice const& slice) {
auto const jrr = env.rpc("submit", strHex(slice))[jss::result];
BEAST_EXPECT(
jrr[jss::status] == "error" &&
jrr[jss::error] == "invalidTransaction" &&
jrr[jss::error_exception] ==
"fails local checks: Malformed: Invalid inner batch "
"transaction.");
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(s.slice());
}
// 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(s.slice());
}
// 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(s.slice());
}
// 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);
auto const jrr = env.rpc("submit", strHex(s.slice()))[jss::result];
BEAST_EXPECTS(
jrr[jss::status] == "error" &&
jrr[jss::error] == "invalidTransaction" &&
jrr[jss::error_exception] ==
"fails local checks: Empty SigningPubKey.",
to_string(jrr));
env.close();
}
}
void
testAccountActivation(FeatureBitset features)
{
testcase("account activation");
using namespace test::jtx;
using namespace std::literals;
test::jtx::Env env{*this, envconfig()};
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, envconfig()};
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, envconfig()};
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, envconfig()};
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, envconfig()};
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,
envconfig(),
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,
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,
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, envconfig()};
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, envconfig()};
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, envconfig()};
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, envconfig()};
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, envconfig()};
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, envconfig()};
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, envconfig()};
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, envconfig()};
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, envconfig()};
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, envconfig()};
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, envconfig()};
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, envconfig()};
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, envconfig()};
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, envconfig()};
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, envconfig()};
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, envconfig()};
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 = ripple::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 bobs 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, envconfig()};
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"}}),
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"}}),
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, envconfig()};
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, envconfig()};
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, envconfig()};
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, envconfig()};
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, envconfig()};
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, envconfig()};
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);
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);
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);
}
};
BEAST_DEFINE_TESTSUITE(Batch, app, ripple);
} // namespace test
} // namespace ripple