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