diff --git a/API-CHANGELOG.md b/API-CHANGELOG.md index c7a31d27fa4..d9db3118b6a 100644 --- a/API-CHANGELOG.md +++ b/API-CHANGELOG.md @@ -71,6 +71,12 @@ This release contains bug fixes only and no API changes. This release contains bug fixes only and no API changes. +## Unreleased + +### Additions and bugfixes + +- `simulate`: There is now additional support for simulating transactions in past ledgers, by providing the `ledger_index` or `ledger_hash`. + ## XRP Ledger server version 2.5.0 [Version 2.5.0](https://github.com/XRPLF/rippled/releases/tag/2.5.0) was released on Jun 24, 2025. diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index 0264f625eb2..dc5aa99f2c3 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -627,7 +627,7 @@ JSS(trusted_validator_keys); // out: ValidatorList JSS(tx); // out: STTx, AccountTx* JSS(tx_blob); // in/out: Submit, // in: TransactionSign, AccountTx* -JSS(tx_hash); // in: TransactionEntry +JSS(tx_hash); // in: TransactionEntry, simulate JSS(tx_json); // in/out: TransactionSign // out: TransactionEntry JSS(tx_signing_hash); // out: TransactionSign diff --git a/include/xrpl/tx/apply.h b/include/xrpl/tx/apply.h index 49b30fea02f..f844af9a4f6 100644 --- a/include/xrpl/tx/apply.h +++ b/include/xrpl/tx/apply.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -115,6 +116,14 @@ enum class ApplyTransactionResult { Retry }; +std::optional> +applyBatchTransactions( + ServiceRegistry& registry, + OpenView& batchView, + STTx const& batchTxn, + ApplyFlags flags, + beast::Journal j); + /** Transaction application helper Provides more detailed logging and decodes the diff --git a/src/libxrpl/ledger/ApplyStateTable.cpp b/src/libxrpl/ledger/ApplyStateTable.cpp index e31b39b437a..ef26776f28c 100644 --- a/src/libxrpl/ledger/ApplyStateTable.cpp +++ b/src/libxrpl/ledger/ApplyStateTable.cpp @@ -246,11 +246,8 @@ ApplyStateTable::apply( metadata = meta; } - if (!isDryRun) - { - to.rawTxInsert(tx.getTransactionID(), sTx, sMeta); - apply(to); - } + to.rawTxInsert(tx.getTransactionID(), sTx, sMeta); + apply(to); return metadata; } diff --git a/src/libxrpl/tx/apply.cpp b/src/libxrpl/tx/apply.cpp index def82aca187..fb8f107d785 100644 --- a/src/libxrpl/tx/apply.cpp +++ b/src/libxrpl/tx/apply.cpp @@ -135,11 +135,12 @@ apply( }); } -static bool +std::optional> applyBatchTransactions( ServiceRegistry& registry, OpenView& batchView, STTx const& batchTxn, + ApplyFlags flags, beast::Journal j) { XRPL_ASSERT( @@ -149,41 +150,57 @@ applyBatchTransactions( auto const parentBatchId = batchTxn.getTransactionID(); auto const mode = batchTxn.getFlags(); - auto applyOneTransaction = [®istry, &j, &parentBatchId, &batchView](STTx&& tx) { + auto applyOneTransaction = [®istry, &j, &parentBatchId, &batchView, &flags](STTx&& tx) { OpenView perTxBatchView(batch_view, batchView); - auto const ret = apply(registry, perTxBatchView, parentBatchId, tx, tapBATCH, j); - XRPL_ASSERT( - ret.applied == (isTesSuccess(ret.ter) || isTecClaim(ret.ter)), - "Inner transaction should not be applied"); + auto const ret = + apply(registry, perTxBatchView, parentBatchId, tx, (flags & tapDRY_RUN) | tapBATCH, j); + if (flags & tapDRY_RUN) + { + XRPL_ASSERT(ret.applied == false, "Inner transaction should not be applied in dry run"); + } + else + { + XRPL_ASSERT( + ret.applied == (isTesSuccess(ret.ter) || isTecClaim(ret.ter)), + "Inner transaction should not be applied"); + } JLOG(j.debug()) << "BatchTrace[" << parentBatchId << "]: " << tx.getTransactionID() << " " << (ret.applied ? "applied" : "failure") << ": " << transToken(ret.ter); // If the transaction should be applied push its changes to the // whole-batch view. - if (ret.applied && (isTesSuccess(ret.ter) || isTecClaim(ret.ter))) + if ((ret.applied || flags & tapDRY_RUN) && (isTesSuccess(ret.ter) || isTecClaim(ret.ter))) perTxBatchView.apply(batchView); return ret; }; - int applied = 0; + std::vector results; for (STObject rb : batchTxn.getFieldArray(sfRawTransactions)) { auto const result = applyOneTransaction(STTx{std::move(rb)}); - XRPL_ASSERT( - result.applied == (isTesSuccess(result.ter) || isTecClaim(result.ter)), - "Outer Batch failure, inner transaction should not be applied"); + if (flags & tapDRY_RUN) + { + XRPL_ASSERT( + result.applied == false, "Inner transaction should not be applied in dry run"); + } + else + { + XRPL_ASSERT( + result.applied == (isTesSuccess(result.ter) || isTecClaim(result.ter)), + "Outer Batch failure, inner transaction should not be applied"); + } - if (result.applied) - ++applied; + if (result.applied || flags & tapDRY_RUN) + results.push_back(result); if (!isTesSuccess(result.ter)) { if (mode & tfAllOrNothing) - return false; + return std::nullopt; if (mode & tfUntilFailure) break; @@ -192,7 +209,12 @@ applyBatchTransactions( break; } - return applied != 0; + if (results.empty()) + { + return std::nullopt; + } + + return results; } ApplyTransactionResult @@ -224,7 +246,7 @@ applyTransaction( { OpenView wholeBatchView(batch_view, view); - if (applyBatchTransactions(registry, wholeBatchView, txn, j)) + if (applyBatchTransactions(registry, wholeBatchView, txn, flags, j)) wholeBatchView.apply(view); } diff --git a/src/test/rpc/Simulate_test.cpp b/src/test/rpc/Simulate_test.cpp index d1b1cd02fcd..fa111179bb4 100644 --- a/src/test/rpc/Simulate_test.cpp +++ b/src/test/rpc/Simulate_test.cpp @@ -47,7 +47,7 @@ class Simulate_test : public beast::unit_test::suite BEAST_EXPECT(tx_json[jss::TransactionType] == tx[jss::TransactionType]); BEAST_EXPECT(tx_json[jss::Account] == tx[jss::Account]); BEAST_EXPECT(tx_json[jss::SigningPubKey] == tx.get(jss::SigningPubKey, "")); - BEAST_EXPECT(tx_json[jss::TxnSignature] == tx.get(jss::TxnSignature, "")); + BEAST_EXPECT(!tx_json.isMember(jss::TxnSignature)); BEAST_EXPECT(tx_json[jss::Fee] == tx.get(jss::Fee, expectedFee)); BEAST_EXPECT(tx_json[jss::Sequence] == tx.get(jss::Sequence, expectedSequence)); } @@ -68,18 +68,24 @@ class Simulate_test : public beast::unit_test::suite jtx::Env& env, Json::Value const& tx, std::function const& validate, - bool testSerialized = true) + bool testSerialized = true, + Json::Value const& extraParams = Json::objectValue) { env.close(); - Json::Value params; + Json::Value params(extraParams); params[jss::tx_json] = tx; validate(env.rpc("json", "simulate", to_string(params)), tx); params[jss::binary] = true; validate(env.rpc("json", "simulate", to_string(params)), tx); - validate(env.rpc("simulate", to_string(tx)), tx); - validate(env.rpc("simulate", to_string(tx), "binary"), tx); + if (extraParams.size() == 0) + { + // haven't added support to CLI mode for ledger_index/ledger_hash + // yet + validate(env.rpc("simulate", to_string(tx)), tx); + validate(env.rpc("simulate", to_string(tx), "binary"), tx); + } if (testSerialized) { @@ -90,14 +96,17 @@ class Simulate_test : public beast::unit_test::suite auto const tx_blob = strHex(parsed.object->getSerializer().peekData()); if (BEAST_EXPECT(parsed.object.has_value())) { - Json::Value params; + Json::Value params(extraParams); params[jss::tx_blob] = tx_blob; validate(env.rpc("json", "simulate", to_string(params)), tx); params[jss::binary] = true; validate(env.rpc("json", "simulate", to_string(params)), tx); } - validate(env.rpc("simulate", tx_blob), tx); - validate(env.rpc("simulate", tx_blob, "binary"), tx); + if (extraParams.size() == 0) + { + validate(env.rpc("simulate", tx_blob), tx); + validate(env.rpc("simulate", tx_blob, "binary"), tx); + } } BEAST_EXPECTS(env.current()->txCount() == 0, std::to_string(env.current()->txCount())); @@ -158,7 +167,8 @@ class Simulate_test : public beast::unit_test::suite auto const resp = env.rpc("json", "simulate", to_string(params)); BEAST_EXPECT( resp[jss::result][jss::error_message] == - "Neither `tx_blob` nor `tx_json` included."); + "Must include one of 'transactions', 'tx_json', 'tx_blob', " + "'tx_hash', or 'ctid'."); } { // Providing both `tx_json` and `tx_blob` @@ -169,7 +179,68 @@ class Simulate_test : public beast::unit_test::suite auto const resp = env.rpc("json", "simulate", to_string(params)); BEAST_EXPECT( resp[jss::result][jss::error_message] == - "Can only include one of `tx_blob` and `tx_json`."); + "Cannot include more than one of 'transactions', 'tx_json', " + "'tx_blob', 'tx_hash', and 'ctid'."); + } + { + // Providing both `tx_json` and `tx_hash` + Json::Value params = Json::objectValue; + params[jss::tx_json] = Json::objectValue; + params[jss::tx_hash] = "ABCDEF1234567890"; + params[jss::ledger_index] = 1; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Cannot include more than one of 'transactions', 'tx_json', " + "'tx_blob', 'tx_hash', and 'ctid'."); + } + { + // Providing both `tx_json` and `ctid` + Json::Value params = Json::objectValue; + params[jss::tx_json] = Json::objectValue; + params[jss::ctid] = "ABCDEF1234567890"; + params[jss::ledger_index] = 1; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Cannot include more than one of 'transactions', 'tx_json', " + "'tx_blob', 'tx_hash', and 'ctid'."); + } + { + // Providing both `tx_blob` and `tx_hash` + Json::Value params = Json::objectValue; + params[jss::tx_blob] = "1200"; + params[jss::tx_hash] = "ABCDEF1234567890"; + params[jss::ledger_index] = 1; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Cannot include more than one of 'transactions', 'tx_json', " + "'tx_blob', 'tx_hash', and 'ctid'."); + } + { + // Providing both `tx_blob` and `ctid` + Json::Value params = Json::objectValue; + params[jss::tx_blob] = "1200"; + params[jss::ctid] = "ABCDEF1234567890"; + params[jss::ledger_index] = 1; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Cannot include more than one of 'transactions', 'tx_json', " + "'tx_blob', 'tx_hash', and 'ctid'."); + } + { + // Providing both `tx_hash` and `ctid` + Json::Value params = Json::objectValue; + params[jss::tx_hash] = "ABCDEF1234567890"; + params[jss::ctid] = "ABCDEF1234567890"; + params[jss::ledger_index] = 1; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Cannot include more than one of 'transactions', 'tx_json', " + "'tx_blob', 'tx_hash', and 'ctid'."); } { // `binary` isn't a boolean @@ -394,6 +465,219 @@ class Simulate_test : public beast::unit_test::suite BEAST_EXPECT( resp[jss::result][jss::error_message] == "Transaction should not be signed."); } + { + // non-string CTID + Json::Value params; + params[jss::ctid] = 1; + params[jss::ledger_index] = 1; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field 'ctid'."); + BEAST_EXPECT(resp[jss::result][jss::error_code] == rpcINVALID_PARAMS); + } + // non-string tx hash + { + // Non-string tx_hash + Json::Value params; + params[jss::tx_hash] = 12345; + params[jss::ledger_index] = 1; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field 'tx_hash'."); + BEAST_EXPECT(resp[jss::result][jss::error_code] == rpcINVALID_PARAMS); + } + { + // tx_hash with missing ledger_index/ledger_hash (current ledger) + Json::Value params; + params[jss::tx_hash] = "ABCDEF1234567890"; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Cannot use `tx_hash` without `ledger_index` or " + "`ledger_hash`."); + } + { + // ctid with missing ledger_index/ledger_hash (current ledger) + Json::Value params; + params[jss::ctid] = "ABCDEF1234567890"; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Cannot use `ctid` without `ledger_index` or `ledger_hash`."); + } + { + // invalid tx_hash + Json::Value params; + params[jss::tx_hash] = "ABCDEF"; + params[jss::ledger_index] = 1; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field 'tx_hash'."); + BEAST_EXPECT(resp[jss::result][jss::error_code] == rpcINVALID_PARAMS); + } + { + // tx_hash not found + Json::Value params; + params[jss::tx_hash] = std::string(64, 'A'); + params[jss::ledger_index] = 1; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECTS( + resp[jss::result][jss::error_message] == "Transaction not found.", + resp.toStyledString()); + BEAST_EXPECT(resp[jss::result][jss::error_code] == rpcTXN_NOT_FOUND); + } + { + // Invalid ctid + Json::Value params; + params[jss::ctid] = "123456"; + params[jss::ledger_index] = 1; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field 'ctid'."); + BEAST_EXPECT(resp[jss::result][jss::error_code] == rpcINVALID_PARAMS); + } + { + // ctid not found + Json::Value params; + params[jss::ctid] = "CBADEF1234567890"; + params[jss::ledger_index] = 1; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT(resp[jss::result][jss::error_message] == "Transaction not found."); + BEAST_EXPECT(resp[jss::result][jss::error_code] == rpcTXN_NOT_FOUND); + } + { + // Unknown field in tx_json + Json::Value params; + Json::Value tx_json = Json::objectValue; + tx_json[jss::TransactionType] = jss::AccountSet; + tx_json[jss::Account] = env.master.human(); + tx_json["UnknownField"] = "value"; + params[jss::tx_json] = tx_json; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Field 'tx_json.UnknownField' is unknown."); + } + { + // TicketSequence autofill returns 0 + Json::Value params; + Json::Value tx_json = Json::objectValue; + tx_json[jss::TransactionType] = jss::AccountSet; + tx_json[jss::Account] = env.master.human(); + tx_json[sfTicketSequence.jsonName] = 1; + params[jss::tx_json] = tx_json; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT(resp[jss::result][jss::tx_json][sfSequence.jsonName] == 0); + } + { + // Invalid ledger_index type + Json::Value params; + Json::Value tx_json = Json::objectValue; + tx_json[jss::TransactionType] = jss::AccountSet; + tx_json[jss::Account] = env.master.human(); + params[jss::tx_json] = tx_json; + params[jss::ledger_index] = Json::arrayValue; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Invalid field 'ledger_index', not string or number."); + } + { + // Invalid ledger_hash type + Json::Value params; + Json::Value tx_json = Json::objectValue; + tx_json[jss::TransactionType] = jss::AccountSet; + tx_json[jss::Account] = env.master.human(); + params[jss::tx_json] = tx_json; + params[jss::ledger_hash] = Json::arrayValue; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Invalid field 'ledger_hash', not hex string."); + } + { + // Invalid transactions array type + Json::Value params; + params[jss::transactions] = "not_an_array"; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Invalid field 'transactions', not array."); + } + { + // Empty transactions array + Json::Value params; + params[jss::transactions] = Json::arrayValue; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Invalid field 'transactions', not nonempty array."); + } + { + // Non-object transaction in transactions array + Json::Value params; + Json::Value txs = Json::arrayValue; + txs.append("not_an_object"); + params[jss::transactions] = txs; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Invalid field 'transactions', not array of objects."); + } + { + // Missing tx_json in transaction object in transactions array + Json::Value params; + Json::Value txs = Json::arrayValue; + Json::Value txObj = Json::objectValue; + txs.append(txObj); + params[jss::transactions] = txs; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Must include one of 'tx_json', 'tx_blob', 'tx_hash', or " + "'ctid' in each transaction."); + } + { + // Non-object tx_json in transaction object in transactions array + Json::Value params; + Json::Value txs = Json::arrayValue; + Json::Value txObj = Json::objectValue; + txObj[jss::tx_json] = "not_an_object"; + txs.append(txObj); + params[jss::transactions] = txs; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECTS( + resp[jss::result][jss::error_message] == "Invalid field 'tx_json', not object.", + resp.toStyledString()); + } + { + // Multiple fields in transaction object in transactions array + Json::Value params; + Json::Value txs = Json::arrayValue; + Json::Value txObj = Json::objectValue; + txObj[jss::tx_json] = Json::objectValue; + txObj[jss::tx_blob] = "ABCDEF1234567890"; + txs.append(txObj); + params[jss::transactions] = txs; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Cannot include more than one of 'tx_json', 'tx_blob', " + "'tx_hash', and 'ctid' in each transaction."); + } + { + // too long array of transactions + Json::Value params; + Json::Value txs = Json::arrayValue; + for (int i = 0; i < 1001; ++i) + { + Json::Value txObj = Json::objectValue; + txObj[jss::tx_json] = noop(env.master); + txs.append(txObj); + } + params[jss::transactions] = txs; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Cannot include more than 1000 transactions in 'transactions' " + "array."); + } } void @@ -431,35 +715,6 @@ class Simulate_test : public beast::unit_test::suite } } - void - testInvalidTransactionType() - { - testcase("Invalid transaction type"); - - using namespace jtx; - - Env env(*this); - - Account const alice{"alice"}; - Account const bob{"bob"}; - env.fund(XRP(1000000), alice, bob); - env.close(); - - auto const batchFee = batch::calcBatchFee(env, 0, 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)); - - jt.jv.removeMember(jss::TxnSignature); - Json::Value params; - params[jss::tx_json] = jt.jv; - auto const resp = env.rpc("json", "simulate", to_string(params)); - BEAST_EXPECT(resp[jss::result][jss::error] == "notImpl"); - BEAST_EXPECT(resp[jss::result][jss::error_message] == "Not implemented."); - } - void testSuccessfulTransaction() { @@ -1144,13 +1399,295 @@ class Simulate_test : public beast::unit_test::suite } } + void + testSuccessfulPastLedger() + { + testcase("Successful past transaction"); + + using namespace jtx; + Env env{*this}; + Account const alice{"alice"}; + auto const netID = env.app().config().NETWORK_ID; + env.fund(XRP(1000), alice); + env.close(); + + auto const ledgerSeq = env.current()->header().seq; + auto const aliceSeq = env.seq(alice); + env.close(); + + Json::Value tx = pay(alice, env.master, XRP(700)); + env(tx); + auto const txHash = to_string(env.tx()->getTransactionID()); + auto const ctid = *RPC::encodeCTID(env.current()->header().seq, 0, netID); + env.close(); + + { + // tx should fail in the current ledger + Json::Value request = Json::objectValue; + request[jss::tx_json] = tx; + auto const result = env.rpc("json", "simulate", to_string(request)); + BEAST_EXPECTS( + result[jss::result][jss::engine_result] == "tecUNFUNDED_PAYMENT", + result.toStyledString()); + } + + { + auto validateOutput = [&](Json::Value const& resp, Json::Value const& tx) { + auto result = resp[jss::result]; + checkBasicReturnValidity(result, tx, aliceSeq, env.current()->fees().base); + + BEAST_EXPECTS(result[jss::engine_result] == "tesSUCCESS", result.toStyledString()); + BEAST_EXPECT(result[jss::engine_result_code] == 0); + BEAST_EXPECT( + result[jss::engine_result_message] == + "The simulated transaction would have been applied."); + + if (BEAST_EXPECT(result.isMember(jss::meta) || result.isMember(jss::meta_blob))) + { + Json::Value const metadata = getJsonMetadata(result); + + if (BEAST_EXPECT(metadata.isMember(sfAffectedNodes.jsonName))) + { + BEAST_EXPECT(metadata[sfAffectedNodes.jsonName].size() == 2); + auto const masterMode = metadata[sfAffectedNodes.jsonName][0u]; + if (BEAST_EXPECT(masterMode.isMember(sfModifiedNode.jsonName))) + { + auto modifiedNode = masterMode[sfModifiedNode]; + BEAST_EXPECT(modifiedNode[sfLedgerEntryType] == "AccountRoot"); + auto finalFields = modifiedNode[sfFinalFields]; + BEAST_EXPECTS( + finalFields[sfBalance] == + (XRP(99999999700) - env.current()->fees().base * 2).getJson(), + metadata.toStyledString()); + } + auto const aliceNode = metadata[sfAffectedNodes.jsonName][1u]; + if (BEAST_EXPECT(aliceNode.isMember(sfModifiedNode.jsonName))) + { + auto modifiedNode = aliceNode[sfModifiedNode]; + BEAST_EXPECT(modifiedNode[sfLedgerEntryType] == "AccountRoot"); + auto finalFields = modifiedNode[sfFinalFields]; + BEAST_EXPECT( + finalFields[sfBalance] == + (XRP(300) - env.current()->fees().base).getJson()); + } + } + BEAST_EXPECT(metadata[sfTransactionIndex.jsonName] == 0); + BEAST_EXPECT(metadata[sfTransactionResult.jsonName] == "tesSUCCESS"); + } + }; + + // test with autofill + Json::Value extraParams = Json::objectValue; + extraParams[jss::ledger_index] = ledgerSeq; + testTx(env, tx, validateOutput, true, extraParams); + + tx[sfSigningPubKey] = ""; + tx[sfTxnSignature] = ""; + tx[sfSequence] = aliceSeq; + tx[sfFee] = env.current()->fees().base.jsonClipped().asString(); + + // test without autofill + testTx(env, tx, validateOutput, true, extraParams); + + // test with hash + { + Json::Value params(extraParams); + params[jss::tx_hash] = txHash; + validateOutput(env.rpc("json", "simulate", to_string(params)), tx); + } + // test with ctid + { + Json::Value params(extraParams); + params[jss::ctid] = ctid; + validateOutput(env.rpc("json", "simulate", to_string(params)), tx); + } + } + } + + void + testMultipleTransactions() + { + testcase("Multiple transactions"); + + using namespace jtx; + Env env{*this}; + + Account const alice{"alice"}; + env.fund(XRP(1000), alice); + env.close(); + auto const aliceSeq = env.seq(alice); + + Json::Value tx1; + tx1[jss::tx_json] = pay(alice, env.master, XRP(500)); + Json::Value tx2; + tx2[jss::tx_json] = pay(alice, env.master, XRP(200)); + // TODO: fix sequence autofilling + tx2[jss::tx_json][jss::Sequence] = env.seq(alice) + 1; + + auto validateOutput = [&](Json::Value const& resp, Json::Value const& txs) { + auto totalResult = resp[jss::result]; + BEAST_EXPECT(totalResult.isMember(jss::transactions)); + BEAST_EXPECT(totalResult[jss::transactions].size() == 2); + + for (unsigned j = 0; j < 2; ++j) + { + auto const result = totalResult[jss::transactions][j]; + auto const tx = txs[j][jss::tx_json]; + checkBasicReturnValidity(result, tx, aliceSeq + j, env.current()->fees().base); + BEAST_EXPECTS(result[jss::engine_result] == "tesSUCCESS", result.toStyledString()); + BEAST_EXPECT(result[jss::engine_result_code] == 0); + BEAST_EXPECT( + result[jss::engine_result_message] == + "The simulated transaction would have been applied."); + + if (BEAST_EXPECT(result.isMember(jss::meta) || result.isMember(jss::meta_blob))) + { + Json::Value const metadata = getJsonMetadata(result); + + if (BEAST_EXPECT(metadata.isMember(sfAffectedNodes.jsonName))) + { + BEAST_EXPECT(metadata[sfAffectedNodes.jsonName].size() == 2); + auto const masterMode = metadata[sfAffectedNodes.jsonName][0u]; + if (BEAST_EXPECT(masterMode.isMember(sfModifiedNode.jsonName))) + { + auto modifiedNode = masterMode[sfModifiedNode]; + BEAST_EXPECT(modifiedNode[sfLedgerEntryType] == "AccountRoot"); + auto finalFields = modifiedNode[sfFinalFields]; + BEAST_EXPECT(finalFields[sfAccount] == env.master.human()); + BEAST_EXPECT( + finalFields[sfBalance] == + (XRP(99999999500) + XRP(200 * j) - env.current()->fees().base * 2) + .getJson()); + } + auto const aliceNode = metadata[sfAffectedNodes.jsonName][1u]; + if (BEAST_EXPECT(aliceNode.isMember(sfModifiedNode.jsonName))) + { + auto modifiedNode = aliceNode[sfModifiedNode]; + BEAST_EXPECT(modifiedNode[sfLedgerEntryType] == "AccountRoot"); + auto finalFields = modifiedNode[sfFinalFields]; + BEAST_EXPECT(finalFields[sfAccount] == alice.human()); + BEAST_EXPECT( + finalFields[sfBalance] == + (XRP(500) - XRP(200 * j) - env.current()->fees().base * (j + 1)) + .getJson()); + } + } + BEAST_EXPECT(metadata[sfTransactionIndex.jsonName] == j); + BEAST_EXPECT(metadata[sfTransactionResult.jsonName] == "tesSUCCESS"); + } + } + }; + + Json::Value params = Json::objectValue; + Json::Value txs = Json::arrayValue; + txs.append(tx1); + txs.append(tx2); + params[jss::transactions] = txs; + + validateOutput(env.rpc("json", "simulate", to_string(params)), txs); + } + + void + testBatchTransaction() + { + testcase("Batch transaction"); + + using namespace jtx; + Env env{*this}; + + Account const alice{"alice"}; + env.fund(XRP(1000), alice); + env.close(); + + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + Json::Value batchTx = batch::outer(alice, seq, batchFee, tfIndependent); + Json::Value tx1 = Json::objectValue; + tx1[jss::RawTransaction] = + batch::inner(pay(alice, env.master, XRP(500)), env.seq(alice) + 1).getTxn(); + Json::Value tx2 = Json::objectValue; + tx2[jss::RawTransaction] = + batch::inner(pay(alice, env.master, XRP(200)), env.seq(alice) + 2).getTxn(); + batchTx[jss::RawTransactions] = Json::arrayValue; + batchTx[jss::RawTransactions].append(tx1); + batchTx[jss::RawTransactions].append(tx2); + + Json::Value params = Json::objectValue; + params[jss::tx_json] = batchTx; + + auto validateOutput = [&](Json::Value const& resp, Json::Value const& batchTx) { + auto totalResult = resp[jss::result]; + BEAST_EXPECT(totalResult.isMember(jss::transactions)); + BEAST_EXPECT(totalResult[jss::transactions].size() == 3); + + { + auto const outerTx = totalResult[jss::transactions][0u]; + checkBasicReturnValidity(outerTx, batchTx, seq, batchFee); + BEAST_EXPECT(outerTx[jss::engine_result] == "tesSUCCESS"); + BEAST_EXPECT(outerTx[jss::engine_result_code] == 0); + BEAST_EXPECT( + outerTx[jss::engine_result_message] == + "The simulated transaction would have been applied."); + } + + for (unsigned j = 0; j < 2; ++j) + { + auto const result = totalResult[jss::transactions][j + 1u]; + auto const tx = batchTx[jss::RawTransactions][j][jss::RawTransaction]; + checkBasicReturnValidity(result, tx, seq + j, "0"); + BEAST_EXPECTS(result[jss::engine_result] == "tesSUCCESS", result.toStyledString()); + BEAST_EXPECT(result[jss::engine_result_code] == 0); + BEAST_EXPECT( + result[jss::engine_result_message] == + "The simulated transaction would have been applied."); + + if (BEAST_EXPECT(result.isMember(jss::meta) || result.isMember(jss::meta_blob))) + { + Json::Value const metadata = getJsonMetadata(result); + + if (BEAST_EXPECT(metadata.isMember(sfAffectedNodes.jsonName))) + { + BEAST_EXPECT(metadata[sfAffectedNodes.jsonName].size() == 2); + auto const masterMode = metadata[sfAffectedNodes.jsonName][0u]; + if (BEAST_EXPECT(masterMode.isMember(sfModifiedNode.jsonName))) + { + auto modifiedNode = masterMode[sfModifiedNode]; + BEAST_EXPECT(modifiedNode[sfLedgerEntryType] == "AccountRoot"); + auto finalFields = modifiedNode[sfFinalFields]; + BEAST_EXPECT(finalFields[sfAccount] == env.master.human()); + BEAST_EXPECT( + finalFields[sfBalance] == + (XRP(99999999500) + XRP(200 * j) - env.current()->fees().base * 2) + .getJson()); + } + auto const aliceNode = metadata[sfAffectedNodes.jsonName][1u]; + if (BEAST_EXPECT(aliceNode.isMember(sfModifiedNode.jsonName))) + { + auto modifiedNode = aliceNode[sfModifiedNode]; + BEAST_EXPECT(modifiedNode[sfLedgerEntryType] == "AccountRoot"); + auto finalFields = modifiedNode[sfFinalFields]; + BEAST_EXPECT(finalFields[sfAccount] == alice.human()); + BEAST_EXPECT( + finalFields[sfBalance] == + (XRP(500) - XRP(200 * j) - env.current()->fees().base * 4) + .getJson()); + } + } + BEAST_EXPECT(metadata[sfTransactionIndex.jsonName] == j + 1); + BEAST_EXPECT(metadata[sfTransactionResult.jsonName] == "tesSUCCESS"); + } + } + }; + + validateOutput(env.rpc("json", "simulate", to_string(params)), batchTx); + } + public: void run() override { testParamErrors(); testFeeError(); - testInvalidTransactionType(); testSuccessfulTransaction(); testTransactionNonTecFailure(); testTransactionTecFailure(); @@ -1161,6 +1698,9 @@ class Simulate_test : public beast::unit_test::suite testDeleteExpiredCredentials(); testSuccessfulTransactionNetworkID(); testSuccessfulTransactionAdditionalMetadata(); + testSuccessfulPastLedger(); + testMultipleTransactions(); + testBatchTransaction(); } }; diff --git a/src/xrpld/rpc/handlers/LedgerData.cpp b/src/xrpld/rpc/handlers/LedgerData.cpp index 308f4b4436f..bb6eafa9699 100644 --- a/src/xrpld/rpc/handlers/LedgerData.cpp +++ b/src/xrpld/rpc/handlers/LedgerData.cpp @@ -43,7 +43,7 @@ doLedgerData(RPC::JsonContext& context) return RPC::expected_field_error(jss::marker, "valid"); } - bool const isBinary = params[jss::binary].asBool(); + bool const isBinary = params.get(jss::binary, false).asBool(); int limit = -1; if (params.isMember(jss::limit)) diff --git a/src/xrpld/rpc/handlers/Simulate.cpp b/src/xrpld/rpc/handlers/Simulate.cpp index 5703597beb4..8dc32762e81 100644 --- a/src/xrpld/rpc/handlers/Simulate.cpp +++ b/src/xrpld/rpc/handlers/Simulate.cpp @@ -1,11 +1,14 @@ #include #include +#include #include #include +#include #include #include #include #include +#include #include #include @@ -19,8 +22,14 @@ namespace xrpl { +constexpr int const MAX_SIMULATE_TXS = 1000; + static Expected -getAutofillSequence(Json::Value const& tx_json, RPC::JsonContext& context) +getAutofillSequence( + Json::Value const& tx_json, + RPC::JsonContext& context, + std::shared_ptr lpLedger, + bool const isCurrentLedger) { // autofill Sequence bool const hasTicketSeq = tx_json.isMember(sfTicketSequence.jsonName); @@ -38,9 +47,12 @@ getAutofillSequence(Json::Value const& tx_json, RPC::JsonContext& context) return Unexpected( RPC::make_error(rpcSRC_ACT_MALFORMED, RPC::invalid_field_message("tx.Account"))); } - std::shared_ptr const sle = - context.app.openLedger().current()->read(keylet::account(*srcAddressID)); - if (!hasTicketSeq && !sle) + std::shared_ptr const sle = lpLedger->read(keylet::account(*srcAddressID)); + if (hasTicketSeq) + { + return 0; + } + if (!sle) { JLOG(context.app.journal("Simulate").debug()) << "Failed to find source account " @@ -48,8 +60,10 @@ getAutofillSequence(Json::Value const& tx_json, RPC::JsonContext& context) return Unexpected(rpcError(rpcSRC_ACT_NOT_FOUND)); } + if (!isCurrentLedger) + return sle->getFieldU32(sfSequence); - return hasTicketSeq ? 0 : context.app.getTxQ().nextQueuableSeq(sle).value(); + return context.app.getTxQ().nextQueuableSeq(sle).value(); } static std::optional @@ -92,37 +106,51 @@ autofillSignature(Json::Value& sigObject) } } - if (!sigObject.isMember(jss::TxnSignature)) - { - // autofill TxnSignature - sigObject[jss::TxnSignature] = ""; - } - else if (sigObject[jss::TxnSignature] != "") + if (sigObject.isMember(jss::TxnSignature)) { - // Transaction must not be signed - return rpcError(rpcTX_SIGNED); + if (sigObject[jss::TxnSignature] != "") + { + // Transaction must not be signed + return rpcError(rpcTX_SIGNED); + } + else + { + sigObject.removeMember(jss::TxnSignature); + } } return std::nullopt; } static std::optional -autofillTx(Json::Value& tx_json, RPC::JsonContext& context) +autofillTx( + Json::Value& tx_json, + RPC::JsonContext& context, + std::shared_ptr lpLedger, + bool const isCurrentLedger) { if (!tx_json.isMember(jss::Fee)) { // autofill Fee // Must happen after all the other autofills happen // Error handling/messaging works better that way - auto feeOrError = RPC::getCurrentNetworkFee( - context.role, - context.app.config(), - context.app.getFeeTrack(), - context.app.getTxQ(), - context.app, - tx_json); - if (feeOrError.isMember(jss::error)) - return feeOrError; - tx_json[jss::Fee] = feeOrError; + if (isCurrentLedger) + { + auto feeOrError = RPC::getCurrentNetworkFee( + context.role, + context.app.config(), + context.app.getFeeTrack(), + context.app.getTxQ(), + context.app, + tx_json); + if (feeOrError.isMember(jss::error)) + return feeOrError; + tx_json[jss::Fee] = feeOrError; + } + else + { + // can't calculate server load for a past ledger + tx_json[jss::Fee] = lpLedger->fees().base.jsonClipped(); + } } if (auto error = autofillSignature(tx_json)) @@ -130,7 +158,7 @@ autofillTx(Json::Value& tx_json, RPC::JsonContext& context) if (!tx_json.isMember(jss::Sequence)) { - auto const seq = getAutofillSequence(tx_json, context); + auto const seq = getAutofillSequence(tx_json, context, lpLedger, isCurrentLedger); if (!seq) return seq.error(); tx_json[sfSequence.jsonName] = *seq; @@ -147,18 +175,93 @@ autofillTx(Json::Value& tx_json, RPC::JsonContext& context) } static Json::Value -getTxJsonFromParams(Json::Value const& params) +getTxJsonFromHistory(RPC::JsonContext& context, bool const isCurrentLedger) { - Json::Value tx_json; + auto const params = context.params; + uint256 hash; + if (params.isMember(jss::tx_hash)) + { + auto const tx_hash = params[jss::tx_hash]; + if (!tx_hash.isString()) + { + return RPC::invalid_field_error(jss::tx_hash); + } + if (isCurrentLedger) + { + return RPC::make_param_error( + "Cannot use `tx_hash` without `ledger_index` or " + "`ledger_hash`."); + } + if (!hash.parseHex(context.params[jss::tx_hash].asString())) + return RPC::invalid_field_error(jss::tx_hash); + } + else if (params.isMember(jss::ctid)) + { + auto const ctid = params[jss::ctid]; + if (!ctid.isString()) + { + return RPC::invalid_field_error(jss::ctid); + } + if (isCurrentLedger) + { + return RPC::make_param_error( + "Cannot use `ctid` without `ledger_index` or `ledger_hash`."); + } + auto decodedCTID = RPC::decodeCTID(context.params[jss::ctid].asString()); + if (!decodedCTID) + { + return RPC::invalid_field_error(jss::ctid); + } + auto const [ledgerSq, txId, _] = *decodedCTID; + if (auto const optHash = context.app.getLedgerMaster().txnIdFromIndex(ledgerSq, txId); + optHash) + { + hash = *optHash; + } + else + { + return RPC::make_error(rpcTXN_NOT_FOUND); + } + } + using TxPair = std::pair, std::shared_ptr>; + auto ec{rpcSUCCESS}; + std::variant v = context.app.getMasterTransaction().fetch(hash, ec); + if (std::get_if(&v)) + { + return RPC::make_error(rpcTXN_NOT_FOUND); + } - if (params.isMember(jss::tx_blob)) + auto [txn, _meta] = std::get(v); + Json::Value tx_json = txn->getJson(JsonOptions::none); + for (auto const field : + {jss::SigningPubKey, + jss::TxnSignature, + jss::ctid, + jss::hash, + jss::inLedger, + jss::ledger_index}) + { + if (tx_json.isMember(field)) + tx_json.removeMember(field); + } + if (tx_json.isMember(jss::Signers)) { - if (params.isMember(jss::tx_json)) + for (auto& signer : tx_json[jss::Signers]) { - return RPC::make_param_error("Can only include one of `tx_blob` and `tx_json`."); + signer[jss::Signer].removeMember(jss::TxnSignature); } + } + return tx_json; +} - auto const tx_blob = params[jss::tx_blob]; +static Json::Value +getTxJsonFromParams(RPC::JsonContext& context, Json::Value txInput, bool const isCurrentLedger) +{ + Json::Value tx_json; + + if (txInput.isMember(jss::tx_blob)) + { + auto const tx_blob = txInput[jss::tx_blob]; if (!tx_blob.isString()) { return RPC::invalid_field_error(jss::tx_blob); @@ -178,9 +281,9 @@ getTxJsonFromParams(Json::Value const& params) return RPC::invalid_field_error(jss::tx_blob); } } - else if (params.isMember(jss::tx_json)) + else if (txInput.isMember(jss::tx_json)) { - tx_json = params[jss::tx_json]; + tx_json = txInput[jss::tx_json]; if (!tx_json.isObject()) { return RPC::object_field_error(jss::tx_json); @@ -188,7 +291,15 @@ getTxJsonFromParams(Json::Value const& params) } else { - return RPC::make_param_error("Neither `tx_blob` nor `tx_json` included."); + auto const result = getTxJsonFromHistory(context, isCurrentLedger); + if (result.isMember(jss::error)) + { + return result; + } + else + { + tx_json = result; + } } // basic sanity checks for transaction shape @@ -206,19 +317,17 @@ getTxJsonFromParams(Json::Value const& params) } static Json::Value -simulateTxn(RPC::JsonContext& context, std::shared_ptr transaction) +processResult( + ApplyResult const& result, + // easier to type as this due to sfRawTransactions in batch transactions + STTx const& transaction, + bool const isBinaryOutput, + ReadView const& view) { - Json::Value jvResult; - // Process the transaction - OpenView view = *context.app.openLedger().current(); - auto const result = context.app.getTxQ().apply( - context.app, view, transaction->getSTransaction(), tapDRY_RUN, context.j); - + Json::Value jvResult = Json::objectValue; jvResult[jss::applied] = result.applied; jvResult[jss::ledger_index] = view.seq(); - bool const isBinaryOutput = context.params.get(jss::binary, false).asBool(); - // Convert the TER to human-readable values std::string token; std::string message; @@ -254,65 +363,125 @@ simulateTxn(RPC::JsonContext& context, std::shared_ptr transaction) else { jvResult[jss::meta] = result.metadata->getJson(JsonOptions::none); - RPC::insertDeliveredAmount( - jvResult[jss::meta], view, transaction->getSTransaction(), *result.metadata); - RPC::insertNFTSyntheticInJson( - jvResult, transaction->getSTransaction(), *result.metadata); - RPC::insertMPTokenIssuanceID( - jvResult[jss::meta], transaction->getSTransaction(), *result.metadata); + auto const shared = std::make_shared(transaction); + RPC::insertDeliveredAmount(jvResult[jss::meta], view, shared, *result.metadata); + RPC::insertNFTSyntheticInJson(jvResult, shared, *result.metadata); + RPC::insertMPTokenIssuanceID(jvResult[jss::meta], shared, *result.metadata); } } if (isBinaryOutput) { - auto const txBlob = transaction->getSTransaction()->getSerializer().getData(); + auto const txBlob = transaction.getSerializer().getData(); jvResult[jss::tx_blob] = strHex(makeSlice(txBlob)); } else { - jvResult[jss::tx_json] = transaction->getJson(JsonOptions::none); + jvResult[jss::tx_json] = transaction.getJson(JsonOptions::none); } - return jvResult; } -// { -// tx_blob: XOR tx_json: , -// binary: -// } -Json::Value -doSimulate(RPC::JsonContext& context) +static Json::Value +simulateTxn( + RPC::JsonContext& context, + std::vector> transactions, + std::shared_ptr lpLedger, + bool const isCurrentLedger) { - context.loadType = Resource::feeMediumBurdenRPC; + Json::Value jvTransactions = Json::arrayValue; + std::vector results; + bool const isBinaryOutput = context.params.get(jss::binary, false).asBool(); - Json::Value tx_json; // the tx as a JSON + OpenView origView = OpenView(&*lpLedger); + OpenView view(batch_view, origView); + for (auto const& transaction : transactions) + { + OpenView perTxView(batch_view, view); + auto const txn = *transaction->getSTransaction(); + /*************************************** + * SECURITY NOTE: This technically applies the transaction to the view. + * However, since the `view` is a copy of the ledger it's being applied + * to, and is thrown away immediately after the simulated transactions + * are processed, the original ledger remains unchanged. Therefore, you + * cannot use `simulate` to bypass signature checks and submit + * transactions/modify the current ledger directly. + ***************************************/ + auto const result = apply(context.app, perTxView, txn, tapDRY_RUN, context.j); + if (isTesSuccess(result.ter) || isTecClaim(result.ter)) + perTxView.apply(view); + jvTransactions.append(processResult(result, txn, isBinaryOutput, view)); + + if (isTesSuccess(result.ter) && txn.getTxnType() == ttBATCH) + { + OpenView wholeBatchView(batch_view, view); - // check validity of `binary` param - if (context.params.isMember(jss::binary) && !context.params[jss::binary].isBool()) + if (auto const batchResults = + applyBatchTransactions(context.app, wholeBatchView, txn, tapDRY_RUN, context.j); + batchResults) + { + for (int i = 0; i < batchResults->size(); ++i) + { + auto const& innerResult = (*batchResults)[i]; + auto rawTxn = txn.getFieldArray(sfRawTransactions)[i]; + STTx const txn( + static_cast(rawTxn.getFieldU16(sfTransactionType)), + [&rawTxn](STObject& obj) { obj = STObject(rawTxn); }); + jvTransactions.append( + processResult(innerResult, txn, isBinaryOutput, wholeBatchView)); + } + wholeBatchView.apply(view); + } + } + } + + if (jvTransactions.size() == 1) { - return RPC::invalid_field_error(jss::binary); + return jvTransactions[0u]; } + Json::Value jvFinalResult = Json::objectValue; + jvFinalResult[jss::transactions] = jvTransactions; + return jvFinalResult; +} - for (auto const field : {jss::secret, jss::seed, jss::seed_hex, jss::passphrase}) +bool +checkIsCurrentLedger(Json::Value const params) +{ + if (params.isMember(jss::ledger_index)) { - if (context.params.isMember(field)) + auto const& ledgerIndex = params[jss::ledger_index]; + if (!ledgerIndex.isNull()) { - return RPC::invalid_field_error(field); + return ledgerIndex == jss::current; } } + if (params.isMember(jss::ledger_hash)) + { + if (!params[jss::ledger_hash].isNull()) + return false; + } + return true; +} +Expected, Json::Value> +processTransaction( + RPC::JsonContext& context, + Json::Value txInput, + std::shared_ptr lpLedger, + bool const isCurrentLedger) +{ // get JSON equivalent of transaction - tx_json = getTxJsonFromParams(context.params); + Json::Value tx_json = getTxJsonFromParams(context, txInput, isCurrentLedger); if (tx_json.isMember(jss::error)) - return tx_json; + return Unexpected(tx_json); // autofill fields if they're not included (e.g. `Fee`, `Sequence`) - if (auto error = autofillTx(tx_json, context)) - return *error; + if (auto error = autofillTx(tx_json, context, lpLedger, isCurrentLedger)) + return Unexpected(*error); STParsedJSONObject parsed(std::string(jss::tx_json), tx_json); if (!parsed.object.has_value()) - return parsed.error; + return Unexpected(parsed.error); std::shared_ptr stTx; try @@ -324,20 +493,124 @@ doSimulate(RPC::JsonContext& context) Json::Value jvResult = Json::objectValue; jvResult[jss::error] = "invalidTransaction"; jvResult[jss::error_exception] = e.what(); - return jvResult; + return Unexpected(jvResult); } - if (stTx->getTxnType() == ttBATCH) + std::string reason; + return std::make_shared(stTx, reason, context.app); +} + +// { +// tx_blob: XOR tx_json: XOR tx_hash: XOR ctid: +// XOR +// transactions: [ ] +// (each one of tx_json/tx_blob/tx_hash/ctid), +// binary: +// } +Json::Value +doSimulate(RPC::JsonContext& context) +{ + context.loadType = Resource::feeMediumBurdenRPC; + + // check validity of `binary` param + if (context.params.isMember(jss::binary) && !context.params[jss::binary].isBool()) { - return RPC::make_error(rpcNOT_IMPL); + return RPC::invalid_field_error(jss::binary); } - std::string reason; - auto transaction = std::make_shared(stTx, reason, context.app); + for (auto const field : {jss::secret, jss::seed, jss::seed_hex, jss::passphrase}) + { + if (context.params.isMember(field)) + { + return RPC::invalid_field_error(field); + } + } + + auto const numParams = + (context.params.isMember(jss::transactions) + context.params.isMember(jss::tx_json) + + context.params.isMember(jss::tx_blob) + context.params.isMember(jss::tx_hash) + + context.params.isMember(jss::ctid)); + if (numParams == 0) + { + return RPC::make_param_error( + "Must include one of 'transactions', 'tx_json', 'tx_blob', " + "'tx_hash', or 'ctid'."); + } + // if more than one of these fields is included, error out + if (numParams > 1) + { + return RPC::make_param_error( + "Cannot include more than one of 'transactions', 'tx_json', " + "'tx_blob', " + "'tx_hash', and 'ctid'."); + } + + std::shared_ptr lpLedger; + auto jvResult = RPC::lookupLedger(lpLedger, context); + if (!lpLedger) + return jvResult; + bool const isCurrentLedger = checkIsCurrentLedger(context.params); + + auto transactions = std::vector>{}; + if (context.params.isMember(jss::transactions)) + { + auto const txs = context.params[jss::transactions]; + if (!txs.isArray()) + { + return RPC::expected_field_error(jss::transactions, "array"); + } + if (txs.size() == 0) + { + return RPC::expected_field_error(jss::transactions, "nonempty array"); + } + if (txs.size() > MAX_SIMULATE_TXS) + { + return RPC::make_param_error( + "Cannot include more than " + std::to_string(MAX_SIMULATE_TXS) + + " transactions in 'transactions' array."); + } + for (auto const& txInput : context.params[jss::transactions]) + { + if (!txInput.isObject()) + { + return RPC::expected_field_error(jss::transactions, "array of objects"); + } + { + auto const numParams = txInput.isMember(jss::tx_json) + + txInput.isMember(jss::tx_blob) + txInput.isMember(jss::tx_hash) + + txInput.isMember(jss::ctid); + if (numParams == 0) + { + return RPC::make_param_error( + "Must include one of 'tx_json', 'tx_blob', " + "'tx_hash', or 'ctid' in each transaction."); + } + else if (numParams > 1) + { + return RPC::make_param_error( + "Cannot include more than one of 'tx_json', 'tx_blob', " + "'tx_hash', and 'ctid' in each transaction."); + } + } + + auto transaction = processTransaction(context, txInput, lpLedger, isCurrentLedger); + if (!transaction) + return transaction.error(); + transactions.push_back(transaction.value()); + } + } + else + { + // single transaction + auto transaction = processTransaction(context, context.params, lpLedger, isCurrentLedger); + if (!transaction) + return transaction.error(); + transactions.push_back(transaction.value()); + } // Actually run the transaction through the transaction processor try { - return simulateTxn(context, transaction); + return simulateTxn(context, transactions, lpLedger, isCurrentLedger); } // LCOV_EXCL_START this is just in case, so rippled doesn't crash catch (std::exception const& e)